Skip to content

Commit dd87d0a

Browse files
Merge branch 'main' into ads/cap-757-enhancement-enable-fallback-judge-generator
2 parents 1ca5a8c + 677cc1d commit dd87d0a

File tree

22 files changed

+3301
-2649
lines changed

22 files changed

+3301
-2649
lines changed

.github/workflows/pre-commit.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
3434

3535
- name: Install uv
36-
uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5
36+
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
3737
with:
3838
version: "latest"
3939
python-version: ${{ env.PYTHON_VERSION }}

.github/workflows/publish.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
1919

2020
- name: Install uv
21-
uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5
21+
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
2222
with:
2323
version: "latest"
2424
python-version: 3.13

.github/workflows/renovate.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
GITHUB_TOKEN: "${{ steps.app-token.outputs.token }}"
8181

8282
- name: Renovate
83-
uses: renovatebot/github-action@5712c6a41dea6cdf32c72d92a763bd417e6606aa # v44.0.5
83+
uses: renovatebot/github-action@66387ab8c2464d575b933fa44e9e5a86b2822809 # v44.2.4
8484
with:
8585
configurationFile: "${{ env.RENOVATE_ONBOARDING_CONFIG_FILE_NAME }}"
8686
token: "${{ steps.app-token.outputs.token }}"

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
3030

3131
- name: Install uv
32-
uses: astral-sh/setup-uv@ed21f2f24f8dd64503750218de024bcf64c7250a # v7.1.5
32+
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
3333
with:
3434
version: "latest"
3535
python-version: ${{ matrix.python-version }}

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ repos:
1818
- id: trailing-whitespace
1919

2020
- repo: https://github.com/rhysd/actionlint
21-
rev: v1.7.9
21+
rev: v1.7.10
2222
hooks:
2323
- id: actionlint
2424

2525
- repo: https://github.com/adrienverge/yamllint.git
26-
rev: v1.37.1
26+
rev: v1.38.0
2727
hooks:
2828
- id: yamllint
2929
entry: yamllint --strict -c .hooks/linters/yamllint.yaml

dreadnode/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
if t.TYPE_CHECKING:
3939
from dreadnode import agent, airt, eval, optimization, scorers, transforms # noqa: A004
4040
from dreadnode.agent import Agent, tool, tool_method
41-
from dreadnode.data_types import Audio, Image, Table, Video
41+
from dreadnode.data_types import Audio, Image, Message, Table, Video
4242

4343
logger.disable("dreadnode")
4444

@@ -88,6 +88,7 @@
8888
"EnvVar",
8989
"Image",
9090
"Markdown",
91+
"Message",
9192
"Metric",
9293
"MetricDict",
9394
"Object",
@@ -151,6 +152,7 @@
151152
"Image": "dreadnode.data_types",
152153
"Table": "dreadnode.data_types",
153154
"Video": "dreadnode.data_types",
155+
"Message": "dreadnode.data_types",
154156
"Agent": "dreadnode.agent",
155157
"tool": "dreadnode.agent",
156158
"tool_method": "dreadnode.agent",

dreadnode/agent/agent.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from rigging.message import inject_system_content
1515
from ulid import ULID # can't access via rg
1616

17-
from dreadnode.agent.error import MaxStepsError
17+
from dreadnode.agent.error import MaxStepsError, MaxToolCallsError
1818
from dreadnode.agent.events import (
1919
AgentEnd,
2020
AgentError,
@@ -89,7 +89,9 @@ class Agent(Model):
8989
)
9090
"""The agent's core instructions."""
9191
max_steps: int = Config(default=10)
92-
"""The maximum number of steps (generation + tool calls)."""
92+
"""The maximum number of steps (generations)."""
93+
max_tool_calls: int = Config(default=-1)
94+
"""The maximum number of tool calls. Defaults to infinite."""
9395
caching: rg.caching.CacheMode | None = Config(default=None, repr=False)
9496
"""How to handle cache_control entries on inference messages."""
9597

@@ -488,10 +490,16 @@ async def _dispatch(event: AgentEvent) -> t.AsyncIterator[AgentEvent]: # noqa:
488490
raise winning_reaction
489491

490492
# Tool calling
493+
tool_calls = 0
491494

492495
async def _process_tool_call(
493496
tool_call: "rg.tools.ToolCall",
494497
) -> t.AsyncGenerator[AgentEvent, None]:
498+
nonlocal tool_calls
499+
500+
if self.max_tool_calls != -1 and tool_calls >= self.max_tool_calls:
501+
raise Finish("Reached maximum allowed tool calls.")
502+
495503
async for event in _dispatch(
496504
ToolStart(
497505
session_id=session_id,
@@ -513,6 +521,7 @@ async def _process_tool_call(
513521
tool = next((t for t in self.all_tools if t.name == tool_call.name), None)
514522

515523
if tool is not None:
524+
tool_calls += 1
516525
try:
517526
message, stop = await tool.handle_tool_call(tool_call)
518527
except Reaction:
@@ -690,6 +699,9 @@ async def _process_tool_call(
690699
if step >= self.max_steps:
691700
error = MaxStepsError(max_steps=self.max_steps)
692701
stop_reason = "max_steps_reached"
702+
elif self.max_tool_calls != -1 and tool_calls >= self.max_tool_calls:
703+
error = MaxToolCallsError(max_tool_calls=self.max_tool_calls)
704+
stop_reason = "max_tool_calls_reached"
693705
elif error is not None:
694706
stop_reason = "error"
695707
elif events and isinstance(events[-1], AgentStalled):

dreadnode/agent/error.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ class MaxStepsError(Exception):
44
def __init__(self, max_steps: int):
55
super().__init__(f"Maximum steps reached ({max_steps}).")
66
self.max_steps = max_steps
7+
8+
9+
class MaxToolCallsError(Exception):
10+
"""Raise from a hook to stop the agent's run due to reaching the maximum number of tool calls."""
11+
12+
def __init__(self, max_tool_calls: int):
13+
super().__init__(f"Maximum tool calls reached ({max_tool_calls}).")
14+
self.max_tool_calls = max_tool_calls

dreadnode/agent/result.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
if t.TYPE_CHECKING:
99
from dreadnode.agent.agent import Agent
1010

11-
AgentStopReason = t.Literal["finished", "max_steps_reached", "error", "stalled"]
11+
AgentStopReason = t.Literal[
12+
"finished", "max_steps_reached", "max_tool_calls_reached", "error", "stalled"
13+
]
1214

1315

1416
@dataclass(config=ConfigDict(arbitrary_types_allowed=True))

dreadnode/agent/thread.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from copy import deepcopy
2-
31
from pydantic import BaseModel, Field
42
from rigging.generator import Usage
53
from rigging.message import Message
@@ -44,4 +42,12 @@ def last_usage(self) -> Usage | None:
4442
return None
4543

4644
def fork(self) -> "Thread":
47-
return Thread(messages=deepcopy(self.messages), events=deepcopy(self.events))
45+
# Create a new thread with the same messages but empty events
46+
# Events are historical tracking and don't need to be forked
47+
# We construct new Message objects to avoid shared references
48+
forked_messages = []
49+
for msg in self.messages:
50+
# Reconstruct message from its dict representation
51+
msg_dict = msg.model_dump()
52+
forked_messages.append(Message.model_validate(msg_dict))
53+
return Thread(messages=forked_messages, events=[])

0 commit comments

Comments
 (0)