#1289·eino

[Feature] compose: expose tool call batch index/size to tools (GetToolCallInfo), not only GetToolCallID

Author: shawtiCreated Sep 14, 2026Updated Sep 14, 2026

Problem

The only per-call information a tool can read from its context.Context is the model-generated call ID: compose.GetToolCallID (compose/tool_node.go:1297-1310, v0.9.19), backed by toolCallInfo whose single field is toolCallID (:1289-1291).

ToolsNode does know more at the moment it builds the context: genToolCallTasks iterates input.ToolCalls[i] (:791-792) and keeps the batch slot i in tasks[i], and runToolCallTaskByInvoke / runToolCallTaskByStream set the context per task (:902, :941). The slot index and the batch size are just not exposed.

Why it matters

A host SDK that needs a structural, model-independent identity for a tool invocation (idempotency keys for side-effecting tools that must survive checkpoint/resume, correlating parallel calls to their position in the assistant turn, batch-level barriers) cannot derive it from the call ID: the ID is minted by the model/gateway, may be empty or duplicated within one batch, and can change when the same turn is regenerated. "The i-th call of this assistant message" is stable across all of those.

Proposed API (additive)

go
// ToolCallInfo describes the tool call currently being executed by a ToolsNode.
type ToolCallInfo struct {
    CallID string // same value GetToolCallID returns today
    Index  int    // position in the assistant message's ToolCalls slice
    Total  int    // len(ToolCalls) for this batch
}

// GetToolCallInfo returns the current tool call info, and false when the
// context is not a ToolsNode execution context.
func GetToolCallInfo(ctx context.Context) (ToolCallInfo, bool)

GetToolCallID keeps its signature and semantics. Population is local to genToolCallTasks (store i / n on toolCallTask) and the two setToolCallInfo sites. Exposing the parent assistant message's ID or the ToolsNode's iteration number would also be welcome but is not required for this request.

Workaround today & its cost

Two options exist, both worse than an official accessor:

  1. Read the graph state from inside the tool via compose.ProcessState[*adk.State] and scan state.Messages for the assistant message that contains the current call ID. adk.State is documented as "exported only for checkpoint backward compatibility. Do not use it directly." (adk/react.go:59-63), so this ties the SDK to a symbol scheduled for retirement, and it still needs a call ID to search by.
  2. Build the mapping call-ID → (turn, index) in an AfterModelRewriteState hook and rebuild it on resume via WithHistoryModifier. This works but re-derives, from the outside, a value the node already holds, and again collapses when two calls in one batch share an ID.

Minimal reproduction (v0.9.19)

go
// inside a tool's InvokableRun:
id := compose.GetToolCallID(ctx)   // "" or duplicated when the model misbehaves
// no way to obtain: which slot of the batch am I, and how large is the batch

Feed an assistant message with two ToolCalls that share ID: "call_1" into a ToolsNode; both tools see the same GetToolCallID result and have no other handle to tell themselves apart.