fix(agent): harden tool-call handling against malformed upstream relays
Combine malformed tool-call handling with placeholder filtering and a no-tools fallback so a relay that returns tool_use blocks with null id/name/input can no longer crash a turn or permanently wedge a session. Adapted to the ContextGovernor architecture (context governance now lives in nanobot/agent/context_governance.py, not runner.py): - ToolCallRequest.has_valid_name(): single source of truth for "usable name" (non-empty string). - tool_hints.format_tool_hints(): skip tool calls with a non-string/empty name instead of raising AttributeError on the whole turn. - ContextGovernor.strip_placeholder_assistant_messages() and strip_malformed_tool_calls() (plus the _tool_call_name_is_valid helper): history-cleaning staticmethods invoked at the START of prepare_for_model() — strip_placeholder, then strip_malformed, then the existing drop_orphan/backfill chain. Both only repair the model-facing copy and leave persisted history untouched (return a copy, or the same list when nothing changes). Also wired into runner's minimal-repair path. - AgentRunner._drop_malformed_tool_calls(): returns (dropped, all_dropped, original_finish_reason); clears finish_reason to "stop" when all calls are dropped. - AgentRunner._malformed_tool_call_retry_messages() + _request_model malformed_retry flag: when an all-dropped tool_calls response comes back, retry once with a corrective note; if the retry STILL comes back all-dropped, fall back to _request_no_tools for graceful text degradation. Tests for the history-cleaning methods live with ContextGovernor in tests/agent/test_runner_governance.py; response-layer and tool-hint tests stay on AgentRunner / tool_hints. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
Xubin Ren
co-authored by
Claude Opus 4.8
parent
d7152cdbdd
commit
8248d075db
@@ -306,3 +306,22 @@ class TestToolHintMaxLength:
|
||||
short = _hint([_tc("list_dir", {"path": long_path})], max_length=40)
|
||||
long = _hint([_tc("list_dir", {"path": long_path})], max_length=120)
|
||||
assert len(long) > len(short)
|
||||
|
||||
|
||||
class TestToolHintMalformedCalls:
|
||||
"""Malformed tool calls must not crash hint formatting (see HKUDS/nanobot)."""
|
||||
|
||||
def test_none_name_is_skipped(self):
|
||||
"""A tool call with name=None should be skipped, not raise AttributeError."""
|
||||
result = _hint([_tc(None, None)])
|
||||
assert result == ""
|
||||
|
||||
def test_empty_name_is_skipped(self):
|
||||
"""A tool call with an empty name should be skipped."""
|
||||
result = _hint([_tc("", {"path": "foo.txt"})])
|
||||
assert result == ""
|
||||
|
||||
def test_none_name_mixed_with_valid_call(self):
|
||||
"""A degenerate call must not suppress hints for the valid calls beside it."""
|
||||
result = _hint([_tc(None, None), _tc("read_file", {"path": "foo.txt"})])
|
||||
assert result == "read foo.txt"
|
||||
|
||||
Reference in New Issue
Block a user