Skip to content

AcpToolCallTracker

Defined in: packages/agent/src/modules/acp/acpToolCalls.ts:69

Tracks the tool calls a turn has opened, so a permission request can name the one it is about.

A dialect’s update mapper extends this and keeps the fields fed: trackToolStart when the model announces a call, appendToolArgs for each argument delta, trackToolSettled when a result lands.

new AcpToolCallTracker(): AcpToolCallTracker

AcpToolCallTracker

protected readonly openToolCalls: [string, string][] = []

Defined in: packages/agent/src/modules/acp/acpToolCalls.ts:86

Tool calls the model has requested but which have neither produced a result nor been claimed by a permission request, in the order they were announced, as [toolCallId, toolName].

Kept so the approval bridge can name the tool call a permission request is ABOUT. The gate’s PendingToolInterrupt carries the tool’s name and arguments but no call id — the graph suspends inside the middleware wrapping the call, which is downstream of where the id lives — while the client has already drawn that call from this stream. This queue reconnects the two.


protected readonly toolArgs: Map<string, string>

Defined in: packages/agent/src/modules/acp/acpToolCalls.ts:75

Streamed argument text per tool call id, reassembled from tool_args deltas. Held rather than forwarded per delta because ACP has no argument-delta update: rawInput is a whole value, so it can only be sent once the deltas stop arriving.

protected appendToolArgs(id, delta): void

Defined in: packages/agent/src/modules/acp/acpToolCalls.ts:95

Accumulates one streamed argument delta.

string

string

void


claimToolCallId(name, args?): string | undefined

Defined in: packages/agent/src/modules/acp/acpToolCalls.ts:175

Takes the id of the unclaimed tool call that matches name and args, removing it from the queue, or undefined when there is none.

Matched on the ARGUMENTS, because position is not a reliable discriminator here. The arguments are the one value both sides hold: the gate’s PendingToolInterrupt carries them, and this tracker still holds the streamed argument text at claim time (it is only discarded on tool_result, which exists solely on the resumed run).

Where the exact match degrades, named rather than implied: this side is JSON reassembled from the model’s streamed argument deltas, and a local model that ignores disable_parallel_tool_use can merge sibling calls’ buffers into invalid JSON ({}{}, {"steps":3}{} — see the AG-UI server’s parseToolArguments note). parseToolArgs deliberately does not carry that path’s recovery, so such a payload stays a raw string, matches nothing, and falls back to position — on exactly the model class most likely to emit sloppy parallel calls. The fallback is still the best remaining answer and the cost is which row a client attaches the prompt to, never which command the human rules on.

Position was tried and is wrong in both directions, which is why it is only the fallback. The runner drains suspended calls as a BATCH — every pending call decided in turn before anything resumes — so a model emitting two parallel calls of one tool has both open and neither running. Returning “the most recent” handed BOTH requests the second call’s id. But plain oldest-first is no better in the case that is normal at the rated rungs: this method is reached only from the human-approval callback, which sits behind the gate’s earlier exits (not-gated, deny list, bypass, the hardline floor, the allow list, and the rater’s own arms), so a batch where one call is settled without a human and its sibling escalates leaves the settled call’s id unclaimed at the head of the queue — and oldest-first then hands the request the wrong one, with every upstream ordering assumption perfectly intact.

Consuming is right either way, and the queue order still decides between two calls whose arguments are genuinely identical, where either answer is equally true.

Absent rather than guessed when nothing matches — a permission request pointing at the WRONG call is worse than one pointing at no call, because a client would then attach the answer to a call the user never saw.

string

Record<string, unknown>

string | undefined


protected rawInputFor(id): unknown

Defined in: packages/agent/src/modules/acp/acpToolCalls.ts:106

The reassembled arguments for a call, as a value for rawInput.

string

unknown


protected takeRawInputUpdate(id): unknown

Defined in: packages/agent/src/modules/acp/acpToolCalls.ts:120

[[TUI-C100]] — the rawInput a call still owes the client, or undefined when it owes none.

rawInput is a whole value with no delta form, so it can only be sent once the arguments have stopped arriving — but it must not wait for the call to finish. A call held at the approval gate has its arguments long since complete and is precisely the one whose arguments matter most: they are what the human is being asked to rule on, and they are all a client has to draw the row the permission request points at. Sent when the arguments are known, and not resent unchanged, so a client’s rendering never flickers back to a bare id.

string

unknown


protected trackToolSettled(id): void

Defined in: packages/agent/src/modules/acp/acpToolCalls.ts:130

Forgets a call that has produced its result.

string

void


protected trackToolStart(id, name): void

Defined in: packages/agent/src/modules/acp/acpToolCalls.ts:89

Records a newly announced call and opens its argument buffer.

string

string

void