Skip to content
45 changes: 40 additions & 5 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 @@ -740,7 +760,18 @@ def _model_message_content(raw: Mapping[str, object], build: RuntimeContextBuild
resumed_content = payload.get("content")
if isinstance(resumed_content, (str, list)):
return parse_multimodal_content(resumed_content)
return _message_content(content)
model_content = _message_content(content)
status = raw.get("execution_status")
if raw.get("role") != "tool" or status not in {"failed", "unknown"}:
return model_content
if not isinstance(model_content, str):
return model_content
label = "Tool failed" if status == "failed" else "Tool outcome is unknown"
result = f"{label}: {model_content}"
remediation = raw.get("safe_remediation")
if isinstance(remediation, str) and remediation.strip():
result += f"\n\nSuggested correction: {remediation.strip()}"
return result


def _prompt_messages(
Expand Down Expand Up @@ -841,6 +872,10 @@ def append_history(raw: Mapping[str, object]) -> None:
content=_model_message_content(raw, build),
tool_calls=provider_tool_calls,
tool_call_id=provider_tool_call_id,
is_error=(
role == "tool"
and raw.get("execution_status") in {"failed", "unknown"}
),
reasoning_content=(
cast(str, raw.get("reasoning_content")) if isinstance(raw.get("reasoning_content"), str) else None
),
Expand Down Expand Up @@ -1456,7 +1491,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 +1546,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 +1696,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
16 changes: 15 additions & 1 deletion backend/app/services/agent_runtime/tool_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@ def __post_init__(self) -> None:
"runtime_default", 60.0, 300.0, "stop_waiting_only"
),
"network_read": ToolDeadlinePolicy(
"network_read", 30.0, 60.0, "stop_waiting_only"
"network_read", 60.0, 60.0, "stop_waiting_only"
),
"image_generation": ToolDeadlinePolicy(
"image_generation", 120.0, 120.0, "stop_waiting_only"
),
"custom_image_generation": ToolDeadlinePolicy(
"custom_image_generation", 600.0, 600.0, "stop_waiting_only"
),
"local_code": ToolDeadlinePolicy(
"local_code", 30.0, 3600.0, "cooperative"
Expand All @@ -93,6 +99,14 @@ def deadline_policy_for_tool(tool_name: str) -> ToolDeadlinePolicy:
return _DEADLINE_POLICIES["agentbay_read"]
if tool_name in {"read_emails", "read_webpage", "jina_read"}:
return _DEADLINE_POLICIES["network_read"]
if tool_name == "generate_image_custom":
return _DEADLINE_POLICIES["custom_image_generation"]
if tool_name in {
"generate_image_siliconflow",
"generate_image_openai",
"generate_image_google",
}:
return _DEADLINE_POLICIES["image_generation"]
return _DEADLINE_POLICIES["runtime_default"]


Expand Down
18 changes: 14 additions & 4 deletions backend/app/services/agent_runtime/tool_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from app.models.agent_run import AgentRun
from app.models.agent_tool_execution import AgentToolExecution
from app.services.builtin_tool_definitions import BUILTIN_TOOL_NAMES

ToolExecutionStatus = Literal[
"not_started",
Expand Down Expand Up @@ -2015,7 +2016,7 @@ async def reconcile_unknown_tool_execution(
if not is_user_reconcilable_unknown_execution(execution):
raise ToolExecutionError(
"tool_execution_reconciliation_not_supported",
"manual reconciliation is only supported for conditional write_file or image-generation receipts",
"manual reconciliation is not supported for this Tool receipt",
)

prior_metadata = (
Expand Down Expand Up @@ -2082,12 +2083,21 @@ def is_user_reconcilable_unknown_execution(execution: AgentToolExecution) -> boo
new tool call, so the original provider request is never replayed.
"""
effect, retry_policy = _execution_metadata(execution)
contract_version = getattr(execution, "contract_version", None)
tool_name = str(getattr(execution, "tool_name", "") or "")
is_registered_dynamic_mcp = (
tool_name not in BUILTIN_TOOL_NAMES
and isinstance(contract_version, str)
and contract_version.startswith(f"registered:{tool_name}:")
and effect == "external_write"
and retry_policy == "never"
)
return (
execution.tool_name == "write_file"
tool_name == "write_file"
and effect == "write"
and retry_policy == "conditional"
) or (
execution.tool_name in _IMAGE_GENERATION_TOOL_NAMES
tool_name in _IMAGE_GENERATION_TOOL_NAMES
and effect == "external_write"
and retry_policy == "never"
)
) or is_registered_dynamic_mcp
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
Loading