Fix repeated Deep Agents tool calls with identical arguments - #1806
Fix repeated Deep Agents tool calls with identical arguments#18061fanwang wants to merge 7 commits into
Conversation
Signed-off-by: 1fanwang <1fannnw@gmail.com>
…ct-tool-call-cache
There was a problem hiding this comment.
🟡 Changes recommended
The cache-key change needs workflow patching to preserve deterministic replay of existing histories.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Fixes duplicate Deep Agents tool calls being incorrectly served from cache.
Changes:
- Includes tool call IDs in cache keys.
- Adds regression coverage.
- Documents corrected behavior.
File summaries
| File | Description |
|---|---|
temporalio/contrib/deepagents/workflow.py |
Updates tool-result caching. |
tests/contrib/deepagents/test_tools.py |
Tests repeated identical calls. |
CHANGELOG.md |
Records the fix. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ct-tool-call-cache
Replaying a history recorded under the old (name, args) key with the new key raises TMPRL1100: the previously-deduped second call now emits an Activity command the history does not contain (reproduced with a recorded history and Replayer). patched() keeps old histories on the old key while new executions run repeated calls independently.
tool_call_id in the activity input is a per-invocation workflow.uuid4() nonce (nothing supplies __tool_call_id__), so keying the cache by it made every entry permanently unhittable: repeats ran, but cross-continue-as-new reuse died and the carried snapshot grew without bound. Key by (tool identity, per-run occurrence index) instead: replay and post-CAN runs regenerate the carried conversation's calls in order, so the same keys reproduce — repeats each get their own Activity AND completed calls are still reused after continue-as-new. Occurrence counters reset with the cache in set_result_cache. Patch id renamed accordingly (the prior id was never released). Also: the test fixture now honors run_deep_agent's (input, state_snapshot=None) signature contract, and a new test pins key regeneration across a simulated snapshot carry.
|
Thanks for the find - this is a real bug and a good catch. It's true that under However, it looks like there were two latent problems in the original approach, both verified empirically - and now fixed on this branch:
Also merged latest Follow-ups deliberately not in this PR (pre-existing, same family): |
The same served-stale-result bug existed at both sibling dispatch seams: a repeated identical backend op returned the first op's cached result (a read after an intervening write saw stale contents; a repeated execute never ran), and a repeated identical live model call was served the first response, defeating deliberate resampling. All three dispatchers now share one occurrence-keyed helper behind a single patch id (deepagents.cache-key-per-occurrence; the earlier tool-only id was never released). Regression e2e per seam: read-write-read sees fresh state with three backend_op activities, and two identical prompts produce two invoke_model activities with distinct responses. Record-and-replay verified: a main-recorded old-key history replays cleanly, and a new-code history round-trips.
|
Follow-up on the "deliberately not in this PR" items: on reflection they belong here — same bug class, same release train, and shipping the tool fix alone would leave backend reads serving stale file contents. Pushed in fbcd84e:
That closes out everything from the review except the CI-committed old-key replay fixture, which needs a checked-in history file — happy to add if maintainers want it in this PR. |
TLDR: Under
run_deep_agent, the continue-as-new result cache deduped repeated identical calls — a second tool call with the same name+args (and likewise a repeated backend op or identical live model call) was served the first call's cached result instead of running its own Activity. This PR keys the cache by per-run occurrence index at all three dispatch seams, so repeats each run independently while completed calls still reuse across continue-as-new. Patch-gated (deepagents.cache-key-per-occurrence) so in-flight histories replay unchanged.What was changed
call_tool,call_model, andcall_backend_opshare one occurrence-keyed cache-key helper: key = (kind, payload identity, per-run occurrence index). Replay and post-continue-as-new runs regenerate the carried conversation's calls in order, so the same keys reproduce — repeats get their own Activities AND cross-CAN reuse survives (a per-invocation id in the key would orphan every carried entry;tool_call_idin the activity input is aworkflow.uuid4()nonce).workflow.patched("deepagents.cache-key-per-occurrence"): histories recorded under the old payload-only keys replay with the old semantics (verified by recording a history onmainand replaying it through this branch; the ungated version fails with TMPRL1100).Why?
Dedup was wrong at every seam: a side-effecting tool requested twice ran once (and the second call's ToolMessage carried the first call's
tool_call_id); a repeatedreadafter an interveningwritereturned stale contents and a repeatedexecutenever ran; identical live model calls defeat deliberate resampling.How was this tested
Per-seam regression e2e (two identical tool calls → two
invoke_toolactivities; read→write→read → threebackend_opactivities with fresh contents; two identical prompts → twoinvoke_modelactivities with distinct responses), a cross-CAN key-regeneration test, and record-and-replay verification in both directions. Full deepagents suite: 35 passed.