Repository navigation
Tool-level idempotency registry for recovered tool executions — is this worth building? #7970
Replies: 1 comment 2 replies
|
Thanks for raising this. I ran a small offline correctness probe of the sketch, with separate SQLite databases for the registry and simulated service and real process termination at the fault boundary. It covers five scenarios and four variants; it is not a test of an author-provided implementation. With an inherently idempotent set operation, three distinct requests to set A, set B, then restore A ended at B under the argument-keyed cache: the final A reused the first result. Both variants keyed by logical operation ID ended at A. If the worker died after a notification was accepted but before recording done locally, recovery produced two notifications with no registry, an argument-keyed registry, or a client-only operation ID. Receiver-side atomic deduplication using the same operation ID kept it at one. The controls behaved as expected: registered completed retries reused the result; crashing before the effect produced one notification after recovery; repeating the inherently idempotent write after a crash preserved the correct state. Five existing run-idempotency tests also passed on main 3f9a78e. My conclusion is that argument equality alone is insufficient for a generic operation identity, and pending is an unknown outcome rather than proof that nothing happened. Explicit operation identity and effect-recovery contracts are prerequisites for a generic registry. If registration only covers inherently idempotent tools, the notification problem remains outside that scope. |
Uh oh!
There was an error while loading. Please reload this page.
Hi maintainers — I've been reading through the resume / recovery paths and
found a gap I'd like to discuss before proposing anything concrete: when a
tool's result is lost mid-run, DeerFlow has no way to recover it, and the
fallback can cause a side-effecting tool to actually run twice.
What DeerFlow guarantees today (verified against the code)
interrupt_before/after+Command(resume=...))resume from the pending Pregel task, so already-executed tools never re-run;
results persist as
ToolMessages in the checkpointedmessageschannel.DanglingToolCallMiddlewarepatches danglingtool_calls with a syntheticerror
ToolMessage("Tool call was interrupted and did not return a result")on the next model call, so strict providers don't 400 and the model can
decide to retry or give up.
Idempotency-Keyexists ([feat] Support idempotency keys for thread run creation #5257) but is scoped to run creation,not tool execution. MCP tasks persist
remote_task_idoutside checkpoints,but that's a task contract, not a general tool result registry.
Where re-execution still happens
checkpoint is durable (
AIMessagewithtool_call_id), but the tool nodenever finished, so the result is lost forever. The middleware's synthetic
error tells the model "no result" — if the model retries, the tool actually
runs again. The framework cannot distinguish "the tool already succeeded
but the result didn't checkpoint" from "the tool never ran". A second
payment / email / write is the real-world cost.
interrupt()inside a node (extension graphs): resume re-runsthe node from scratch, re-executing tools invoked before the interrupt.
tool_call_ideach time.
Related evidence: [backend] ClarificationMiddleware can duplicate HITL clarification messages on repeated tool-call execution #2350 (duplicate HITL messages from repeated tool
execution), [bug] Delta resume from a paused checkpoint can discard pending tools and report success #7438 (delta resume can discard pending tools), fix: make resume command retries idempotent #6490 (resume
command retry idempotency).
A sketch, for discussion
A
wrap_tool_callmiddleware acting as a per-thread tool executionregistry:
key = sha256(tool_name + canonical_json(args));on a
donehit, skip the tool and reuse the recorded result (stamped withprovenance); on
pendingleft by a dead run, re-execute and overwrite.tool_call_id—retries mint new ids.
("tool_executions", thread_id))with TTL / per-thread cap and a result-size threshold (large results fall
back to today's behavior). Checkpoint-channel ledger as an alternative.
idempotent(side-effecting) orread_only;non-deterministic tools are never registered. Default off for custom tools.
DanglingToolCallMiddleware: registry hit → real result;miss → keep the synthetic-error fallback.
Open questions I'd like your input on
middleware hooks? (The AGENTS.md extension-first policy makes me unsure.)
multi-worker deployments?
rejected RFC I missed? Non-deterministic args (timestamps) worry me.
opt-in from day one?
ToolReceiptMiddlewareand guardrails inthe
wrap_tool_callchain ordering?
Happy to prototype a minimal version behind a config flag if the direction
sounds right. Thanks for reading!
All reactions