moeru-ai/airi · error · Error

Tool call " " is ambiguous. Select an invocation.

Error message

Tool call "${callId}" is ambiguous. Select an invocation.

What it means

findInvocation resolves a tool call ID to a single concrete invocation. When a tool call ID appears more than once in the message's candidates (e.g. parallel or repeated invocations of the same tool), it cannot pick one, so it throws to force the caller to disambiguate via an invocationId. This is an internal invariant guard protecting rerun correctness.

Solutions

  1. Pass an explicit invocationId in the rerun payload so findInvocation can select the exact occurrence.
  2. Inspect the target assistant message's tool call candidates and pick the intended invocation before rerunning.
  3. If duplication is unexpected, check upstream message construction for duplicated tool call entries.

Example fix

// before
await rerunToolCall({ toolCallId: 'call_1', toolName: 'search' })
// after
await rerunToolCall({ toolCallId: 'call_1', toolName: 'search', invocationId: 'inv_2' })
Defensive patterns

Strategy: validation

Validate before calling

// Count invocations for the callId before rerunning
const matches = message.toolCalls.filter(c => c.id === toolCallId)
if (matches.length > 1 && !invocationId)
  throw new Error(`Tool call "${toolCallId}" is ambiguous; pass an invocationId.`)

Try / catch

try {
  await rerunToolCall(payload)
} catch (e) {
  if (e.message.includes('is ambiguous')) {
    // surface a picker UI listing invocations for this callId
  }
}

Prevention

When it happens

Trigger: Calling findInvocation (via target/invocation helpers in tool-call-rerun.ts) with a callId that has no invocationId while the assistant message contains 2+ tool call candidates with that same call ID.

Common situations: Re-running a tool call from a message where the model issued the same tool call multiple times (parallel tool use, or duplicated call IDs after message merging/editing), and the rerun payload omitted the invocationId discriminator.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17). Data as JSON: /api/errors/f1b81d1df0a4515a. Report an issue: GitHub.

Appendix: source

Thrown at packages/stage-ui/src/stores/tool-call-rerun.ts:54

  payload: ToolCallRerunPayload<TToolset>
  resolveTools: () => Promise<Tool[]>
}

type ToolCallResultInput = Omit<ChatSlicesToolCallResult, 'type'>
type ToolExecuteOptions = NonNullable<Parameters<Tool['execute']>[1]>

function findInvocation(message: ChatAssistantMessage, callId: string, invocationId?: string) {
  const candidates = message.generationTranscript?.rounds.flatMap((round, roundIndex) =>
    round.toolInvocations.filter(call => call.callId === callId).map(call => ({ call, roundIndex })),
  )
  if (invocationId !== undefined) {
    const matches = candidates?.filter(candidate => candidate.call.id === invocationId)
    if (!candidates || matches?.length !== 1)
      throw new Error(`Tool invocation "${invocationId}" is missing or ambiguous.`)
    return { ...matches[0], occurrence: candidates.indexOf(matches[0]) }
  }
  if (candidates && candidates.length > 1)
    throw new Error(`Tool call "${callId}" is ambiguous. Select an invocation.`)
  return candidates?.[0] ? { ...candidates[0], occurrence: 0 } : undefined
}

/**
 * Replaces one execution and its display results, preserving their order.
 * Native continuation for this round and later rounds becomes stale after the edit.
 * Repeated provider call ids require an AIRI invocation id.
 */
export function replaceToolCallResult(message: ChatAssistantMessage, result: ToolCallResultInput, invocationId?: string): ChatAssistantMessage {
  const target = findInvocation(message, result.id, invocationId)
  const occurrence = target?.occurrence ?? 0
  const toolResult = { id: result.id, isError: result.isError, result: result.result }
  const resultSlice: ChatSlicesToolCallResult = { type: 'tool-call-result', ...toolResult }
  let generationTranscript = message.generationTranscript
  if (generationTranscript && target) {
    generationTranscript = {
      ...generationTranscript,
      rounds: generationTranscript.rounds.map((round, index) => index < target.roundIndex

View on GitHub (pinned to 438a067dde)