fix: better snapshot and gate

This commit is contained in:
Alex committed 2026-08-22 14:00:15 +01:00
1 parent 29f661f3c7
commit 3f88dc7b57
12 files changed
+449 -22

No files matched your search

+128
View File
@@ -1367,6 +1367,134 @@ class TestHandleStreaming:
# The whole point: the merged payload parses.
_json.loads(executed_call.arguments)
def test_a_distinct_call_reusing_an_index_is_not_treated_as_a_restatement(
self, caplog
):
"""Two complete payloads under one index may be two different calls.
Some OpenAI-compatible gateways tag sequential parallel calls with the
same ``index``. That is indistinguishable from a restatement by payload
shape alone, so the identity decides — and it must be read BEFORE the
id/name overwrite, which would otherwise erase the evidence. Falling
back to concatenation keeps the failure loud: ``tool_action_parser``
logs the unparseable prefix, where a silent replace would run one tool
with the other's arguments and report the turn complete.
"""
import logging as _logging
handler = ConcreteHandler()
agent = Mock()
agent.llm = Mock()
agent.model_id = "test"
agent.tools = []
agent._check_context_limit = Mock(return_value=False)
agent.context_limit_reached = False
agent.llm.__class__.__name__ = "MockLLM"
agent.tool_executor.check_pause = Mock(return_value=None)
chunk1 = LLMResponse(
content="",
tool_calls=[
ToolCall(id="call_A", name="memory_view", arguments='{"a":1}', index=0)
],
finish_reason="",
raw_response={},
)
chunk2 = LLMResponse(
content="",
tool_calls=[
ToolCall(id="call_B", name="memory_write", arguments='{"b":2}', index=0)
],
finish_reason="tool_calls",
raw_response={},
)
handler.parse_response = lambda c: c
def fake_iterate(response):
yield from response
handler._iterate_stream = fake_iterate
def fake_execute(tools_dict, call):
yield {"type": "tool_call", "data": {"status": "pending"}}
return ("result", call.id)
agent._execute_tool_action = Mock(side_effect=fake_execute)
agent.llm.gen_stream = Mock(
return_value=[
LLMResponse(
content="done", tool_calls=[], finish_reason="stop", raw_response={}
)
]
)
with caplog.at_level(_logging.WARNING, logger="application.llm.handlers.base"):
list(
handler.handle_streaming(agent, [chunk1, chunk2], {"1": {"name": "t"}}, [])
)
executed_call = agent._execute_tool_action.call_args[0][1]
# Concatenated, not silently replaced: call A's payload survives in a
# form the parser reports rather than vanishing.
assert executed_call.arguments == '{"a":1}{"b":2}'
assert "tool_call_index_reused_by_distinct_call" in caplog.text
def test_a_restatement_carrying_the_same_identity_still_replaces(self):
"""The guard must not undo the fix: same id/name means one call."""
handler = ConcreteHandler()
agent = Mock()
agent.llm = Mock()
agent.model_id = "test"
agent.tools = []
agent._check_context_limit = Mock(return_value=False)
agent.context_limit_reached = False
agent.llm.__class__.__name__ = "MockLLM"
agent.tool_executor.check_pause = Mock(return_value=None)
chunk1 = LLMResponse(
content="",
tool_calls=[
ToolCall(id="c1", name="search", arguments='{"q":"x"}', index=0)
],
finish_reason="",
raw_response={},
)
# The finish frame repeats the identity as well as the payload.
chunk2 = LLMResponse(
content="",
tool_calls=[
ToolCall(id="c1", name="search", arguments='{"q":"x"}', index=0)
],
finish_reason="tool_calls",
raw_response={},
)
handler.parse_response = lambda c: c
def fake_iterate(response):
yield from response
handler._iterate_stream = fake_iterate
def fake_execute(tools_dict, call):
yield {"type": "tool_call", "data": {"status": "pending"}}
return ("result", call.id)
agent._execute_tool_action = Mock(side_effect=fake_execute)
agent.llm.gen_stream = Mock(
return_value=[
LLMResponse(
content="done", tool_calls=[], finish_reason="stop", raw_response={}
)
]
)
list(handler.handle_streaming(agent, [chunk1, chunk2], {"1": {"name": "t"}}, []))
executed_call = agent._execute_tool_action.call_args[0][1]
assert executed_call.arguments == '{"q":"x"}'
def test_context_limit_adds_system_message(self):
handler = ConcreteHandler()
agent = Mock()