diff --git a/pageindex/local_chat.py b/pageindex/local_chat.py index 4634d7a65..e32932d2b 100644 --- a/pageindex/local_chat.py +++ b/pageindex/local_chat.py @@ -1130,11 +1130,11 @@ def capture(params): envelope["usage"] = _anthropic_usage(turns, envelope.get("usage") or {}) # The full turn sequence (assistant tool_use + user tool_result + final), # valid for verbatim append to the caller's history. The runner appends - # a turn to its params only when it executed tools from it — content - # carried tool_use blocks and the turn was not a refusal. stop_reason - # alone cannot tell: a max_tokens turn with complete tool_use blocks - # still executes. Whether final's tool_use ids already sit in the - # history is the ground truth for "already appended". + # a turn to its params only when it executed tools from it, and which + # turns qualify is vendor policy that has changed across anthropic + # releases, so stop_reason alone cannot tell. Whether final's tool_use + # ids already sit in the history is the ground truth for "already + # appended". new_messages = [_dump_message(message) for message in conversation[len(prepared):]] final_blocks = [_dump_block(item) for item in final.content] diff --git a/tests/test_local_chat.py b/tests/test_local_chat.py index 4605c52a5..64d09cdd6 100644 --- a/tests/test_local_chat.py +++ b/tests/test_local_chat.py @@ -664,8 +664,17 @@ def test_responses_stream_events_validate_as_official_events( try: import anthropic _HAS_ANTHROPIC = True + # anthropic 1.1 made every non-tool_use stop reason terminal: the runner + # no longer executes tool_use blocks from a max_tokens-cut turn. + try: + _ANTHROPIC_RUNS_CUT_TOOL_TURNS = tuple( + int(piece) for piece in anthropic.__version__.split(".")[:2] + ) < (1, 1) + except ValueError: + _ANTHROPIC_RUNS_CUT_TOOL_TURNS = False # unparseable: assume current except ImportError: _HAS_ANTHROPIC = False + _ANTHROPIC_RUNS_CUT_TOOL_TURNS = False needs_anthropic = pytest.mark.skipif(not _HAS_ANTHROPIC, reason="anthropic not installed") @@ -1653,9 +1662,12 @@ def test_messages_max_turns_truncation_round_trippable(client, store_path, def test_messages_tool_use_cut_by_max_tokens_not_duplicated(client, store_path, fake_anthropic): - """A max_tokens turn with complete tool_use blocks still executes and - is appended by the runner — keying the re-append guard on stop_reason - duplicated the tool_use id and broke verbatim continuation.""" + """A max_tokens turn carrying complete tool_use blocks: anthropic < 1.1 + executes and appends it, 1.1+ treats it as terminal and never executes. + The envelope keys on the runner's history rather than stop_reason + (keying on stop_reason once duplicated the tool_use id), so on either + side the id never duplicates and the appendable messages stay valid + for verbatim continuation.""" seed_doc(store_path, "pi-a", "report.pdf") calls = fake_anthropic([ _anthropic_message([_anthropic_tool_use()], "max_tokens"), @@ -1666,8 +1678,15 @@ def test_messages_tool_use_cut_by_max_tokens_not_duplicated(client, assert len(calls) == 1 assert result["stop_reason"] == "max_tokens" roles = [message["role"] for message in result["messages"]] - assert roles == ["assistant", "user"] # tool_use, tool_result — no dup - assert json.dumps(result["messages"]).count('"tu_1"') == 2 # use + result + if _ANTHROPIC_RUNS_CUT_TOOL_TURNS: + assert roles == ["assistant", "user"] # tool_use then tool_result + assert json.dumps(result["messages"]).count('"tu_1"') == 2 # use + result + else: + # Unexecuted tool_use has no tool_result: stripped from the + # appendable history, still visible in content. + assert roles == [] + assert json.dumps(result["messages"]).count('"tu_1"') == 0 + assert [block["type"] for block in result["content"]] == ["tool_use"] @needs_anthropic