Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions backend/app/services/agent_runtime/model_step_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
ToolResultStoreError,
)
from app.services.agent_runtime.tool_registry import (
RUNTIME_TOOL_BINDING_KEY,
resolve_registered_tool,
)
from app.services.agent_tools import get_runtime_agent_tools_for_llm
Expand Down Expand Up @@ -426,6 +427,16 @@ def _application_tools_for_model(
]


def _provider_tools(tools: Sequence[Mapping[str, object]]) -> list[dict]:
"""Remove Runtime-only routing facts before sending Tool schemas to a model."""
result: list[dict] = []
for tool in tools:
model_tool = deepcopy(dict(tool))
model_tool.pop(RUNTIME_TOOL_BINDING_KEY, None)
result.append(model_tool)
return result


def _runtime_workset_entry(tool: Mapping[str, object]) -> ToolWorksetEntry:
"""Join one model definition to a stable, secret-free execution route."""
name = _tool_name(tool)
Expand All @@ -449,7 +460,16 @@ def _runtime_workset_entry(tool: Mapping[str, object]) -> ToolWorksetEntry:
dynamic_mcp_names=dynamic_mcp_names,
)
if registered is not None:
return registered.to_workset_entry()
entry = registered.to_workset_entry()
raw_binding = tool.get(RUNTIME_TOOL_BINDING_KEY)
if raw_binding is None:
return entry
binding = ToolExecutionBinding.from_json(raw_binding)
if binding.kind != "mcp" or binding.handler_key != name:
raise ToolContractError(
"Runtime Tool binding does not match its model definition"
)
return replace(entry, binding=binding)
if name in GROUP_READ_TOOL_NAMES:
effect, retry_policy = "read", "safe"
binding_kind = "group"
Expand Down Expand Up @@ -1456,7 +1476,7 @@ async def compact_inputs(
model,
requested_max_output_tokens=requested_output,
static_prompt_tokens=fixed_prompt_tokens,
tool_schema_tokens=_estimate_tokens(tools),
tool_schema_tokens=_estimate_tokens(_provider_tools(tools)),
reserved_runtime_tokens=256,
safety_margin_tokens=256,
compact_threshold_ratio=0.80,
Expand Down Expand Up @@ -1511,7 +1531,7 @@ async def _prepare_messages(
model,
requested_max_output_tokens=requested_output,
static_prompt_tokens=fixed_prompt_tokens,
tool_schema_tokens=_estimate_tokens(tools),
tool_schema_tokens=_estimate_tokens(_provider_tools(tools)),
reserved_runtime_tokens=256,
safety_margin_tokens=256,
)
Expand Down Expand Up @@ -1661,7 +1681,7 @@ async def _call_prepared(
return await self._completion(
model,
messages,
tools=tools,
tools=_provider_tools(tools),
agent_id=agent.id,
supports_vision=bool(model.supports_vision),
)
Expand Down
2 changes: 2 additions & 0 deletions backend/app/services/agent_runtime/node_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,8 @@ async def _model(
repair_limit = (
WRITE_FILE_PROTOCOL_REPAIR_LIMIT
if is_write_file_repair
else 10
if repair_code == "invalid_tool_call"
else 1
)
repair_counter_key = (
Expand Down
2 changes: 1 addition & 1 deletion backend/app/services/agent_runtime/tool_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"reconcile",
]
ToolSideEffectState = Literal["none", "confirmed", "possible", "unknown"]
SAFE_READ_MAX_ATTEMPTS = 3
SAFE_READ_MAX_ATTEMPTS = 10

# These tools dispatch an external image-generation request and can therefore
# leave the provider outcome uncertain after a response timeout. Direct Chat
Expand Down
3 changes: 3 additions & 0 deletions backend/app/services/agent_runtime/tool_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
is_reserved_custom_tool_name,
)

RUNTIME_TOOL_BINDING_KEY = "_runtime_binding"


def _function_contract(model_definition: Mapping[str, object]) -> tuple[str, JsonObject]:
function = model_definition.get("function")
Expand Down Expand Up @@ -188,6 +190,7 @@ def resolve_registered_tool(


__all__ = [
"RUNTIME_TOOL_BINDING_KEY",
"STATIC_REGISTERED_TOOL_NAMES",
"RegisteredTool",
"registered_dynamic_mcp",
Expand Down
2 changes: 1 addition & 1 deletion backend/app/services/agent_runtime/tool_repair_budget.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from app.services.agent_runtime.state import JsonObject

SAME_FINGERPRINT_FAILURE_LIMIT = 10
TOOL_EPISODE_FAILURE_LIMIT = 20
TOOL_EPISODE_FAILURE_LIMIT = 10
_REPAIRABLE_MODEL_ACTIONS = frozenset(
{"repair_arguments", "choose_other_tool"}
)
Expand Down
10 changes: 9 additions & 1 deletion backend/app/services/agent_runtime/tool_step_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ async def __call__(
user_id: uuid.UUID,
session_id: str = "",
on_output: object | None = None,
*,
execution_binding: Mapping[str, object] | None = None,
) -> ToolExecutionOutcome | str: ...


Expand Down Expand Up @@ -1313,9 +1315,14 @@ async def _execute_application_with_controls(
if accepted.entry.tool_name.startswith("agentbay_"):
agentbay_run_token = agentbay_run_scope_id.set(context.run_id)
try:
execution_kwargs = (
{"execution_binding": accepted.entry.binding.to_json()}
if accepted.entry.binding.kind == "mcp"
else {}
)
operation_task = asyncio.create_task(
self._tool_executor(
accepted.entry.tool_name,
accepted.entry.binding.handler_key,
arguments,
agent.id,
(
Expand All @@ -1324,6 +1331,7 @@ async def _execute_application_with_controls(
else agent.creator_id
),
context.session_id or "",
**execution_kwargs,
)
)
finally:
Expand Down
Loading