Carry earlier tool results across turns as typed established results - #258
Open
vahid-ahmadi wants to merge 1 commit into
Open
Carry earlier tool results across turns as typed established results#258vahid-ahmadi wants to merge 1 commit into
vahid-ahmadi wants to merge 1 commit into
Conversation
The runtime keeps no server-side turn state, and a turn's tool results die with it. Earlier output reached later turns only as text the client appended to assistant prose, where the model could not tell it apart from its own earlier words. Nothing marked an earlier figure as authoritative, so later prose could restate a computed number differently. That is the mechanism behind the observed Universal Credit contradiction. Carry prior results as a declared `tool_results` field on the assistant message: tool name, serialised result, and the input that produced it. Bound them deterministically and render them as an ESTABLISHED TOOL RESULTS FROM EARLIER TURNS system block, appended after the cache breakpoint. The block constrains prose without licensing skipped computation: an answer must not contradict an established figure and must state the difference when a recomputed value disagrees, but a turn that needs a number still calls the tool. The content is client-supplied and is never treated as a calculation source. Eval cases carry the same field through the shared renderer, so cross-turn consistency is graded against the block the runtime actually builds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5Zsotd2SND15F9efcePoP
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
64 tasks
|
Beta preview is ready.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #258 +/- ##
==========================================
+ Coverage 68.00% 68.49% +0.48%
==========================================
Files 109 110 +1
Lines 6827 6884 +57
Branches 1390 1394 +4
==========================================
+ Hits 4643 4715 +72
+ Misses 1771 1754 -17
- Partials 413 415 +2
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #250 (§1 — the state and provenance problem, and cross-turn numerical consistency).
Root cause of the Universal Credit contradiction
#250 asks the request-routing redesign to "preserve calculated values, their source tool results, and validated intent across turns so later prose cannot silently contradict earlier tool output". Tracing the actual mechanism:
ToolExecutionContextand everyresult_idhandle are created per turn (tools/context.py) and die with it.\n\n---\nTool results:\n[Tool: name] {...}(ChatPage.tsx).So the model had the earlier numbers, as unlabelled prose indistinguishable from its own earlier words. Nothing marked the earlier figure as authoritative, and the system prompt said only that every number "must come directly from a tool result you just computed in this turn" — which says nothing about agreeing with what the conversation already established. That is how prose claiming entitlement ended around £6,000–£7,000 could sit alongside tool output showing entitlement at £9,000.
Two further consequences of the prose glue: every prior turn's results were replayed in full with no bound, growing without limit across a conversation; and on the Continue path the assistant prefill ended with tool JSON rather than the partial prose it was meant to continue.
The change
Prior results become a declared part of the request:
tool_resultson the assistant message that produced them, carryingtool_name, the serialisedresult, and thetool_input.backend/chat/prior_results.pybounds them deterministically — most recent 20, each truncated to 2,000 chars, whole block capped at 20,000 — and rendersESTABLISHED TOOL RESULTS FROM EARLIER TURNS, appended after the cache breakpoint so it never invalidates the cached prompt.The system prompt rule is deliberately two-sided:
The block is client-supplied and is not evidence that a tool ever ran, so it is never treated as a calculation source. That is stated in the module docstring and the runtime skill doc.
Regression cases
evals/cases/answer/cross_turn.yamlis transcript-derived from the 17 August session:answer_cross_turn_uc_entitlement_not_contradicted— the established result shows £1,102.50 of UC at £9,000. The answer may not name a cut-off below that. Verified as a real guard: substituting the observed failing prose ("runs out at about £6,500") makes the case fail withforbidden regex matched: '£\s?6,?[05]00'; ungrounded number(s): [6500.0].answer_cross_turn_recomputed_value_change_is_stated— a recomputed £1.5bn against an established £1.2bn must state both and explain the difference.trajectory_established_results_do_not_replace_computing— a follow-up asking for a new income still calls the tool.The eval harness gained
prior_tool_resultsonanswer,trajectory, andtool_loopcases, rendered through the same function the orchestrator uses, so these grade the block the runtime actually builds rather than a copy of it.test_evaluation.pynow counts every case file in a suite directory instead of two named files, so a new case file cannot silently fall outside the offline run it guards.Verification
make eval-ai-offline: 119 passed, 0 failed (116 onmain).backend/tests/test_prior_tool_results.py— 11 tests over the bounds, the rendered block, the request contract, and cache-breakpoint placement.I could not run the frontend checks. There is no Node toolchain on this machine, so
make test-frontend,npm run build, andtsc --noEmitwere not executed. The frontend change (toApiMessages, replacing both glue sites) and its new test inChatPage.test.tsxare unverified and need CI or a local run before merge — please treat that as the main review risk here.Relationship to the rest of §1
This addresses the state and provenance half. The gateway still runs only on the opening turn — a follow-up gets a plan only through
resume_gateway_proposal— so reusing validated intent and continuing multi-step plans on later turns is still open. Worth a separate PR once the shape here is agreed, since it changes gateway invocation rather than the turn contract.🤖 Generated with Claude Code
https://claude.ai/code/session_01W5Zsotd2SND15F9efcePoP