ToolCall::getId() exists and is stable across replays of the same journaled model response, but ToolCallArgumentResolver::resolveArguments() only maps ToolCall::getArguments() onto the method's declared parameters by name. A tool implementation has no way to receive that id, so a tool needing an idempotency key for its own external call (payment charge, email send) has to invent one itself, typically with uniqid(), which breaks on any retry or replay since it's non-deterministic.
Proposal: a way for a tool method to opt into receiving the current ToolCall's id, for example a reserved parameter name/type-hint the resolver recognizes, or an injectable value object carrying it, without forcing every tool signature to know about ToolCall.
Context: same durable-agent exploration (see https://github.com/gplanchat/durable-dev/blob/spike/agent-durable-symfony-ai/documentation/user/use-cases/durable-agent.md), this is what a saga/idempotent-activity design needs to derive its key from the workflow, not from the tool.
ToolCall::getId()exists and is stable across replays of the same journaled model response, butToolCallArgumentResolver::resolveArguments()only mapsToolCall::getArguments()onto the method's declared parameters by name. A tool implementation has no way to receive that id, so a tool needing an idempotency key for its own external call (payment charge, email send) has to invent one itself, typically withuniqid(), which breaks on any retry or replay since it's non-deterministic.Proposal: a way for a tool method to opt into receiving the current
ToolCall's id, for example a reserved parameter name/type-hint the resolver recognizes, or an injectable value object carrying it, without forcing every tool signature to know aboutToolCall.Context: same durable-agent exploration (see https://github.com/gplanchat/durable-dev/blob/spike/agent-durable-symfony-ai/documentation/user/use-cases/durable-agent.md), this is what a saga/idempotent-activity design needs to derive its key from the workflow, not from the tool.