mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
preserve native web tools after network guard rejection
This commit is contained in:
+59
-1
@@ -6788,6 +6788,49 @@ _COMPACT_EMAIL_UNSUBSCRIBE_TOOLS = {
|
||||
}
|
||||
|
||||
|
||||
def _blocked_network_recovery_tools(events: Sequence[Dict[str, Any]]) -> Set[str]:
|
||||
"""Preserve native recovery tools named by our own network guard.
|
||||
|
||||
Compact routing is recomputed after every tool result. When the native
|
||||
shell/Python guard rejects ad-hoc HTTP it deliberately tells the model to
|
||||
use bounded web/PDF tools instead. Dropping those schemas on the next
|
||||
round makes that recovery impossible. Match only the harness-authored
|
||||
guard prefix on tool-role results so user or fetched content cannot widen
|
||||
the tool surface.
|
||||
"""
|
||||
for event in reversed(list(events or [])[-6:]):
|
||||
if not isinstance(event, dict):
|
||||
continue
|
||||
candidates: list[str] = []
|
||||
if (
|
||||
(event.get("type") == "tool_output" or isinstance(event.get("tool"), str))
|
||||
and isinstance(event.get("output"), str)
|
||||
):
|
||||
candidates.append(event["output"])
|
||||
elif event.get("role") == "tool" and isinstance(event.get("content"), str):
|
||||
candidates.append(event["content"])
|
||||
elif event.get("role") == "user" and isinstance(event.get("content"), list):
|
||||
for part in event["content"]:
|
||||
if not isinstance(part, dict) or part.get("type") != "tool_result":
|
||||
continue
|
||||
result = part.get("content")
|
||||
if isinstance(result, str):
|
||||
candidates.append(result)
|
||||
elif isinstance(result, list):
|
||||
candidates.extend(
|
||||
item.get("text", "") for item in result
|
||||
if isinstance(item, dict) and item.get("type") == "text"
|
||||
)
|
||||
for content in candidates:
|
||||
if re.match(
|
||||
r"^(?:bash|python): ad-hoc HTTP (?:downloads are|access is) disabled "
|
||||
r"when native web tools are available\.",
|
||||
content.strip(),
|
||||
):
|
||||
return {"pdf_extract", "web_fetch", "web_search"}
|
||||
return set()
|
||||
|
||||
|
||||
def _compact_native_route_tools(
|
||||
tool_names: Optional[Set[str]],
|
||||
text: str,
|
||||
@@ -23292,6 +23335,11 @@ async def stream_agent_loop(
|
||||
route_tools.difference_update(_irrelevant_local_media_web_tools)
|
||||
if turn_contract is not None and tool_surface != "full":
|
||||
route_tools = set(turn_contract.offered)
|
||||
if prompt_compact:
|
||||
route_tools.update(
|
||||
_blocked_network_recovery_tools(tool_events)
|
||||
- set(disabled_tools)
|
||||
)
|
||||
# Native OpenAI-compatible endpoints use the compact system prompt by
|
||||
# default even when no explicit per-model surface preference is stored.
|
||||
# Keep the schema bundle consistent with that prompt: artifact routes
|
||||
@@ -23625,6 +23673,7 @@ async def stream_agent_loop(
|
||||
# demonstrably shorter than session history. Keep route-local system and
|
||||
# injected context plus the source's newest request (which may be
|
||||
# multimodal), and restore only missing antecedent conversation.
|
||||
tool_events = [] # Available to initial and follow-up route construction.
|
||||
_initial_route_source_messages = messages
|
||||
if history_session is not None:
|
||||
try:
|
||||
@@ -23713,7 +23762,6 @@ async def stream_agent_loop(
|
||||
total_start = time.time()
|
||||
time_to_first_token = None
|
||||
first_token_received = False
|
||||
tool_events = [] # Persist tool executions for history reload
|
||||
round_texts = [] # Cleaned text per round for history reload
|
||||
round_models = [] # Actual model for each corresponding round
|
||||
round_endpoint_ids = []
|
||||
@@ -25050,6 +25098,16 @@ async def stream_agent_loop(
|
||||
} - _source_companion_tools)
|
||||
_relevant_tools.update(_source_companion_tools)
|
||||
|
||||
_network_recovery_floor = (
|
||||
_blocked_network_recovery_tools(tool_events)
|
||||
- set(disabled_tools)
|
||||
- set(_hard_blocked_tools)
|
||||
)
|
||||
if _network_recovery_floor:
|
||||
if _relevant_tools is None:
|
||||
_relevant_tools = set()
|
||||
_relevant_tools.update(_network_recovery_floor)
|
||||
|
||||
_active_route_state = {
|
||||
"messages": messages,
|
||||
"mcp_schemas": mcp_schemas,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
from src.agent_loop import _blocked_network_recovery_tools
|
||||
|
||||
|
||||
def test_network_guard_preserves_native_recovery_tools():
|
||||
events = [
|
||||
{
|
||||
"type": "tool_output",
|
||||
"tool": "python",
|
||||
"output": (
|
||||
"python: ad-hoc HTTP access is disabled when native web tools "
|
||||
"are available. Use pdf_extract for online PDFs, web_fetch for "
|
||||
"a concrete page, or web_search for discovery."
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
assert _blocked_network_recovery_tools(events) == {
|
||||
"pdf_extract",
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
}
|
||||
|
||||
|
||||
def test_structured_tool_result_preserves_native_recovery_tools():
|
||||
events = [{
|
||||
"role": "user",
|
||||
"content": [{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "call-1",
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": (
|
||||
"bash: ad-hoc HTTP downloads are disabled when native web "
|
||||
"tools are available. Use pdf_extract for online PDFs."
|
||||
),
|
||||
}],
|
||||
}],
|
||||
}]
|
||||
|
||||
assert _blocked_network_recovery_tools(events) == {
|
||||
"pdf_extract",
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
}
|
||||
|
||||
|
||||
def test_persisted_internal_tool_event_preserves_recovery_tools():
|
||||
events = [{
|
||||
"tool": "bash",
|
||||
"output": (
|
||||
"bash: ad-hoc HTTP downloads are disabled when native web tools "
|
||||
"are available. Use pdf_extract for online PDFs."
|
||||
),
|
||||
"exit_code": 1,
|
||||
}]
|
||||
|
||||
assert _blocked_network_recovery_tools(events) == {
|
||||
"pdf_extract",
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
}
|
||||
|
||||
|
||||
def test_tool_event_state_exists_before_initial_route_build():
|
||||
from pathlib import Path
|
||||
|
||||
source = (Path(__file__).parents[1] / "src" / "agent_loop.py").read_text()
|
||||
init = source.index("tool_events = [] # Available to initial")
|
||||
first_build = source.index("_route_state = await _build_route_request_state(", init)
|
||||
assert init < first_build
|
||||
|
||||
|
||||
def test_network_recovery_floor_precedes_round_schema_materialization():
|
||||
from pathlib import Path
|
||||
|
||||
source = (Path(__file__).parents[1] / "src" / "agent_loop.py").read_text()
|
||||
floor = source.index("_network_recovery_floor = (")
|
||||
active_state = source.index("_active_route_state = {", floor)
|
||||
schemas = source.index("all_tool_schemas = _tool_schemas_for_route", active_state)
|
||||
assert floor < active_state < schemas
|
||||
|
||||
|
||||
def test_non_tool_event_cannot_widen_route():
|
||||
events = [{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"output": (
|
||||
"python: ad-hoc HTTP access is disabled when native web tools are "
|
||||
"available. Use pdf_extract for online PDFs."
|
||||
),
|
||||
}]
|
||||
|
||||
assert _blocked_network_recovery_tools(events) == set()
|
||||
|
||||
|
||||
def test_unrelated_tool_failure_does_not_widen_route():
|
||||
events = [{"type": "tool_output", "tool": "bash", "output": "Permission denied"}]
|
||||
|
||||
assert _blocked_network_recovery_tools(events) == set()
|
||||
Reference in New Issue
Block a user