[Feature] compose: expose tool call batch index/size to tools (GetToolCallInfo), not only GetToolCallID
- Dominant language
- Go
- Stars
- 13k
- Forks
- 1.1k
- Avg merge
- 4h 6m
- Merged PRs (30d)
- 41
Description
## 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.
Contributor guide
Research direction
Start in compose/tool_node.go by reading GetToolCallID, toolCallInfo, genToolCallTasks, and the runToolCallTaskByInvoke and runToolCallTaskByStream context setup sites. Trace how the task index and ToolCalls length flow into each context; done means a public GetToolCallInfo accessor exposes CallID, Index, and Total while preserving GetToolCallID behavior and distinguishing duplicate call IDs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- ai, backend-api-design
- Issue type
- Feature
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100