Get started

AI tool call UI patterns: states, consequences, and recovery

Render each tool invocation as an evidence-backed task record. Runtime events establish what happened. Application policy decides what is allowed. The component presents those facts, and users make only the decisions they are authorized to make.

Updated October 16, 2026

Keep four owners separate

Tool-call frameworks provide useful event and rendering mechanisms. assistant-ui documents custom components for loading, results, errors, human input, and interactive states. Dan Vega's Spring AI 2.0 walkthrough shows intermediate tool events reaching a live interface. An application can feed those signals into its own contract, but framework-specific vocabulary isn't a universal statement of execution truth.

Decide who owns what before designing the card. The runtime owns execution facts such as acceptance, start, emitted chunks, terminal results, failures, and termination acknowledgements. Application policy owns permission, cost limits, confirmation, expiry, retry eligibility, and recovery. Presentation owns labels, hierarchy, disclosure, and enabled controls. The user owns the explicit decisions that policy makes available.

A visual state is not execution evidence

A spinner proves only that the component rendered a spinner. A click on Cancel proves that cancellation was requested, not that active execution stopped. Keep the request visible while the invocation remains in its last evidenced execution state.

Persisted invocation data carries evidence between these owners, but it isn't a fifth authority. A database row can preserve a runtime event or user decision. It cannot make an invented transition true. Record the event source, sequence, time, invocation identity, attempt, and owner so the UI can distinguish an observation from an inference.

Map events into a portable lifecycle

Framework vocabularies differ. Vercel Academy describes steps that can contain parallel calls, later calls, and final synthesis. assistant-ui exposes renderer-oriented statuses, while the Spring AI example streams events from an advisor chain. Normalize those inputs at the application boundary, then render the normalized record.

The matrix below proposes a library-neutral contract. It doesn't claim that every framework emits these state names. Each adapter must document which concrete event, decision, or persisted fact satisfies an entry requirement.

Valid entry evidencePermitted user actionsLikely next transitions
ProposedA proposal record identifies the invocation, tool, draft inputs, source request, and proposal owner. There is no dispatch record.Review or edit allowed inputs, dismiss, or submit when policy permits.Awaiting input, awaiting approval, queued, cancelled, or expired.
Awaiting inputThe application records unresolved required input or a validation result tied to the invocation, plus the layer responsible for collecting it.Supply or correct input, choose another route, withdraw the request, or dismiss it.Proposed, awaiting approval, queued, cancelled, or expired.
Awaiting approvalA pending authorization request identifies the invocation, consequence summary, authorizing party or rule, authorization owner, and any deadline.Approve, deny, inspect details, or withdraw when policy exposes those controls.Queued after approval; cancelled after recorded denial or withdrawal; expired at the deadline.
QueuedThe scheduler or executor acknowledges acceptance and records a queue reference or accepted timestamp.Inspect; request cancellation only when the responsible layer supports it.Running, failed, cancelled, or expired.
RunningAn executor start event, active lease, or equivalent runtime fact is tied to the invocation and current attempt.Inspect progress; request scoped cancellation when supported.Streaming, partially complete, succeeded, failed, or cancelled.
StreamingAt least one ordered output or progress event is recorded while the invocation remains open.Inspect incoming output; request cancellation when supported.Streaming, partially complete, succeeded, failed, or cancelled.
Partially completeOne or more child outcomes or durable result segments exist while required work remains unresolved.Inspect completed parts, request cancellation of the remaining scope, or retry an eligible failed child as a new attempt.Running, streaming, succeeded, failed, or cancelled.
SucceededA terminal success event includes a completion time and a result or result reference that passed the application's declared validation.Inspect, copy, continue to a dependent call, or request a separately authorized reversal if one exists.Terminal for this attempt; a parent may proceed to synthesis or another dependent invocation.
FailedA terminal failure event includes a stable category or code, safe user-facing detail, and an evidence reference. A stack trace isn't required as UI evidence.Inspect safe details, retry when policy permits, edit inputs, or use another route.Terminal for this attempt; a new attempt may enter proposed, awaiting approval, or queued.
CancelledThe layer that owns the current phase acknowledges closure. Before dispatch, the proposal or authorization owner records dismissal, denial, or withdrawal. After dispatch, the executor or orchestrator acknowledges termination. In both cases, record initiator, scope, reason, and time.Inspect the closure reason and any completed effects; recover or begin a new invocation when allowed.Terminal for this attempt. Recovery or retry creates a distinct record.
ExpiredPolicy or runtime records that a declared deadline elapsed before required input, approval, queue start, or result.Review changed conditions, resubmit, or request approval again.Terminal for this attempt; a replacement begins as a new invocation.
Display a state only after its entry evidence exists. Actions remain subject to application policy and tool capability.

Cancelled can cover two different closures

A denied approval and a stopped execution may share the terminal state cancelled, but they require evidence from different owners. Preserve denial, dismissal, withdrawal, or execution termination as separate facts. If your product needs those outcomes to behave differently, introduce distinct terminal states and update every transition consistently.

Classify consequences before choosing controls

Consequence labels describe what a call can affect. They don't grant or remove authority. A single invocation may be read-only, permission-gated, and financially costly at the same time. Microsoft presents schemas, execution logic, state, validation, error handling, and trust as tool-use concerns. The UI should preserve those distinctions.

Setproduct's visual catalog covers approvals, progress, completion, audit trails, and undo in context. The matrices below add the application-policy questions that a reusable component can't answer from appearance alone.

Authorization sourceConfirmation timingAllowed controlsCancellation behavior
Read-onlyData-access policy, user scope, tenant boundary, consent, and any cost rule.Before dispatch when access, sensitivity, cost, or policy requires it; otherwise none.Inspect, submit, withdraw before dispatch, request cancellation after dispatch, and retry within policy.The current owner acknowledges pre-dispatch withdrawal or active termination; returned data may still require retention or disclosure handling.
ReversiblePolicy must identify a supported inverse operation and who may invoke it.Before the original call when required; a later undo isn't a substitute for required approval.Approve, withdraw before dispatch, request cancellation during execution, inspect, and request the verified inverse after success.Cancellation addresses unfinished execution. Undo is a separate invocation for completed effects.
Externally visiblePolicy for publication, messaging, shared records, or third-party systems.Before the first externally visible effect unless recorded standing authorization covers it.Review destination and payload, approve, deny, withdraw before dispatch, request cancellation before commitment, then inspect delivery status.After commitment, cancellation may be impossible; recovery may require correction, retraction, or a follow-up action.
DestructiveResource ownership, deletion policy, retention rules, and an authorized actor.Immediately before commitment, using current target and consequence details.Inspect exact targets, approve, deny, or withdraw before dispatch; request cancellation only before the irreversible boundary.Don't promise cancellation after the irreversible boundary. Expose that boundary before approval.
Permission-gatedA named user, role, delegated grant, policy engine, or other recorded authority.Whenever the current grant doesn't cover the exact action and scope.Approve or deny only for eligible actors; inspect scope; withdraw while pending; request cancellation after dispatch when supported.Expiry, revocation, or scope change invalidates a pending decision and may require a new request.
Financially consequentialBudget owner, payment mandate, spending policy, or an already authorized transaction rule.Before a charge or binding commitment unless an applicable mandate explicitly covers it.Inspect amount, currency, recipient, recurrence, and terms; approve, deny, or withdraw before commitment.After commitment, recovery follows the provider's void, refund, dispute, or correction route rather than a generic undo.
Control selection begins with governing policy and current context, not the label alone.
Undo requirementRecovery routeDurable evidenceImportant caveat
Read-onlyUsually not applicable because no mutation occurred.Retry, change the query, use another source, or remove retained data when policy requires it.Invocation identity, query provenance, access decision, result reference, and relevant cost or retention facts.Read-only doesn't mean harmless, free, public, or permissionless.
ReversibleName and test the inverse operation, its time window, scope, and failure behavior.Invoke the inverse, reconcile partial reversal, or escalate when the inverse fails.Original result, side effects, inverse eligibility, undo invocation, and final reconciliation.Don't label an action reversible merely because the interface has an Undo button.
Externally visibleOnly when the external system provides a real reversal or withdrawal operation.Correct, retract, resend, or contact the affected party according to policy.Reviewed payload, destination, approval, provider acknowledgement, and correction history.A local success state may precede or differ from external delivery.
DestructiveRequired only if a real restoration path exists; otherwise state that no undo is available.Restore from an approved source, reconstruct, or escalate through the owning system.Exact targets, authorization, commitment event, affected resources, and restoration evidence if used.Backups or retention don't prove restoration will meet the user's need.
Permission-gatedNot inherently applicable; it depends on the underlying effect.Request a new grant, narrow scope, use another route, or stop.Requested scope, authority source, decision, actor, time, expiry, and revocation facts.Component styling must not imply that the current user is authorized.
Financially consequentialUse the provider-supported void or refund route only when applicable.Retry with safeguards, change the payment route, void, refund, dispute, or escalate.Amount, currency, recipient, authorization, provider reference, status, and reconciliation result.A retry can duplicate a charge unless the application handles idempotency and provider state.
Write recovery and evidence requirements before designing the component.

Preserve identity across sequential and parallel calls

A task summary helps users scan the work, but it can't replace the records beneath it. Vercel's multi-step example shows why. One step may issue parallel calls, later work may depend on their results, and a final answer may synthesize several outcomes. DAGWorks also treats orchestration as explicit application state rather than one undifferentiated loading phase.

Give users one task-level summary, while retaining an immutable identity for every attempt. Dependencies describe execution order. A parent ID describes grouping. Retry lineage explains why two similar calls exist. Evidence references connect each displayed outcome to recorded events.

{
  "exampleStatus": "illustrative-unexecuted",
  "task": {
    "taskId": "task-trip-options-001",
    "purpose": "Compare travel options and summarize available routes",
    "state": "partially_complete",
    "requiredChildren": ["inv-flight-001", "inv-hotel-001", "inv-rail-001"],
    "synthesisInvocationId": "inv-synthesis-001"
  },
  "invocations": [
    {
      "invocationId": "inv-flight-001",
      "parentId": "task-trip-options-001",
      "dependencies": [],
      "attempt": 1,
      "retryOf": null,
      "state": "succeeded",
      "result": { "summary": "Fixture contains two flight options" },
      "evidenceRefs": ["fixture-event-flight-start", "fixture-event-flight-result"]
    },
    {
      "invocationId": "inv-hotel-001",
      "parentId": "task-trip-options-001",
      "dependencies": [],
      "attempt": 1,
      "retryOf": null,
      "state": "failed",
      "result": { "errorCategory": "upstream-unavailable" },
      "evidenceRefs": ["fixture-event-hotel-failure"]
    },
    {
      "invocationId": "inv-hotel-002",
      "parentId": "task-trip-options-001",
      "dependencies": [],
      "attempt": 2,
      "retryOf": "inv-hotel-001",
      "state": "queued",
      "queueRef": "fixture-queue-hotel-002",
      "result": null,
      "evidenceRefs": ["fixture-decision-retry-hotel", "fixture-event-hotel-queue-accepted"]
    },
    {
      "invocationId": "inv-rail-001",
      "parentId": "task-trip-options-001",
      "dependencies": [],
      "attempt": 1,
      "retryOf": null,
      "state": "cancelled",
      "result": { "scope": "rail-branch-only", "reason": "user-requested" },
      "evidenceRefs": ["fixture-decision-cancel-rail", "fixture-event-rail-cancelled"]
    },
    {
      "invocationId": "inv-synthesis-001",
      "parentId": "task-trip-options-001",
      "dependencies": ["inv-flight-001", "inv-hotel-002", "inv-rail-001"],
      "attempt": 1,
      "retryOf": null,
      "state": "proposed",
      "blockedBy": ["inv-hotel-002"],
      "result": null,
      "evidenceRefs": ["fixture-proposal-synthesis", "fixture-rule-synthesis-dependencies"]
    }
  ]
}
Illustrative and unexecuted fixture. The identifiers and events are examples, not observed product behavior.

Flight, hotel, and rail are parallel children. The hotel retry is a new attempt linked to the failure it replaces. Cancelling rail doesn't cancel flight or hotel. The parent can report partial completion without claiming that the whole task succeeded.

The synthesis invocation remains proposed because its hotel dependency is unresolved. A dependency rule explains why it can't dispatch, but it isn't queue evidence. This example doesn't assume dependency-gated queueing. If a real scheduler supports that feature, the adapter may mark synthesis queued only after recording scheduler acceptance, a queue reference or accepted timestamp, and the scheduler's dependency condition.

A retry starts a new lineage entry

Keep the failed attempt intact. A retry gets a new invocation ID and points back through retryOf. Otherwise, the interface can overwrite evidence needed to explain duplicate effects or a changed result.

Use a copyable tool-call component contract

This contract is library-neutral. An adapter may populate it from assistant-ui statuses, AI SDK steps, Spring AI advisor events, or another runtime. The field names form a proposed application contract; the sources don't prescribe this syntax. The example is explicitly unexecuted.

{
  "contractVersion": "tool-call-ui/1",
  "exampleStatus": "illustrative-unexecuted",
  "identity": {
    "taskId": "task-trip-options-001",
    "invocationId": "inv-booking-001",
    "parentId": "task-trip-options-001",
    "attempt": 1,
    "retryOf": null,
    "dependencies": ["inv-flight-001"]
  },
  "purpose": "Reserve the user-selected itinerary",
  "inputs": {
    "schemaRef": "booking-input-v3",
    "valuesRef": "redacted-input-record-001",
    "provenance": [
      { "field": "itineraryId", "source": "user-selected-result", "sourceRef": "inv-flight-001" },
      { "field": "travelerProfile", "source": "account-record", "sourceRef": "profile-current" }
    ],
    "redactionPolicy": "payment credentials are never rendered in the task card"
  },
  "consequence": {
    "classes": ["externally-visible", "permission-gated", "financially-consequential"],
    "commitmentBoundary": "provider acceptance of purchase request"
  },
  "authorization": {
    "source": "application booking policy",
    "owner": "booking authorization service",
    "requiredDecision": "explicit user approval",
    "scope": "one itinerary, displayed total, displayed currency",
    "state": "pending",
    "expiresAt": "fixture-time"
  },
  "expectedTransitions": [
    "awaiting_approval -> queued(on scheduler acceptance)",
    "awaiting_approval -> cancelled(reason=denied|withdrawn, owner=authorization)",
    "awaiting_approval -> expired",
    "queued -> running",
    "queued|running -> cancelled(on scheduler or executor acknowledgement)",
    "running -> succeeded|failed"
  ],
  "runtimeObservations": [],
  "output": {
    "schemaRef": "booking-result-v2",
    "resultRef": null,
    "summary": null
  },
  "sideEffects": {
    "expected": ["provider reservation", "payment authorization or charge as defined by provider"],
    "observed": [],
    "reconciliationState": "not-executed"
  },
  "closure": {
    "state": null,
    "owner": null,
    "kind": null,
    "initiator": null,
    "scope": null,
    "reason": null,
    "recordedAt": null,
    "acknowledgementRef": null
  },
  "errors": {
    "safeCategory": null,
    "userMessage": null,
    "internalEvidenceRef": null
  },
  "recovery": {
    "beforeDispatch": ["deny", "withdraw", "change selection"],
    "afterDispatchBeforeCommitment": ["request cancellation when supported"],
    "afterCommitment": ["use provider-supported void, refund, or support route when applicable"],
    "undoGuaranteed": false
  },
  "alternateRoutes": ["return to itinerary selection", "continue without booking"],
  "permittedActions": ["inspect", "approve", "deny", "withdraw"],
  "timestamps": {
    "proposedAt": "fixture-time",
    "approvedAt": null,
    "acceptedAt": null,
    "startedAt": null,
    "completedAt": null
  },
  "evidenceRefs": ["fixture-policy-booking", "fixture-selected-itinerary"],
  "acceptance": {
    "correctionOwner": "booking application team",
    "retestTrigger": "runtime adapter, booking policy, provider integration, or component contract changes",
    "disposition": "block-until-executed"
  }
}
Copyable illustrative contract. Replace fixture references with records from the consuming application before treating any state as observed.

The component doesn't need to display every field at once, but it does need access to the fields required for its current claim. A compact running card might show purpose, current state, elapsed time, and a cancellation request control. Its disclosure panel can carry inputs, provenance, event history, and evidence references. Before showing the approval control, a financially consequential approval should display the exact scope and commitment boundary.

Name controls for what they do. Before dispatch, Deny, Dismiss, or Withdraw closes a proposal or authorization request through that lifecycle's owner. After scheduler acceptance, Cancel requests termination from the scheduler, executor, or orchestrator. You can use one button label, but the underlying record still needs to distinguish these decisions.

Record expected and observed behavior together

A contract remains an expectation until someone exercises a named consumer. The record below names a fictional consumer: TripDesk's booking-assistant tool panel. It is intentionally unexecuted. Every observation says so, evidence remains missing, and the overall disposition is blocked. This is a test plan, not a benchmark.

Use fixture content that exposes the behavior under review: a permission-gated booking with an expiring price, three parallel search branches, one completed branch, one failed branch, one active branch, a scoped cancellation request, a retry, and a long structured itinerary containing nested legs, policies, fees, and warnings.

Expected behavior and inspection criteriaActual observationEvidence statusOwner, retest trigger, disposition
ApprovalContent: booking total, currency, itinerary, consequence, authority, and expiry. Check that Approve and Deny target the recorded invocation and remain unavailable to an ineligible actor. Fail if approval is inferred, scope is hidden, or a stale decision dispatches the call.Not executed.Missing: no decision event, dispatch event, screenshot, or persisted record.Booking policy and UI owners. Retest after authorization and renderer integration. Block.
Pre-dispatch closureContent: proposed call, pending approval, and separate dismissal, denial, and withdrawal fixtures. Check that the proposal or authorization owner records kind, initiator, scope, reason, and time. Fail if the UI waits for a nonexistent executor acknowledgement or loses the decision type.Not executed.Missing: no proposal-owner or authorization-owner closure record and no rendered terminal state.Proposal, authorization, and component owners. Retest after pre-dispatch closure mapping exists. Block.
ExpiryContent: pending approval with a declared deadline. Check that expiry comes from policy or runtime evidence, disables stale approval, preserves the record, and offers a fresh request. Fail if a client timer alone claims authoritative expiry or the old approval remains usable.Not executed.Missing: no authoritative expiry event or post-expiry interaction record.Policy and runtime-adapter owners. Retest when expiry events and replacement flow exist. Block.
ProgressContent: one queued branch, one running branch, and ordered progress events. Check that labels follow recorded events and event order. Fail if a spinner implies execution before acceptance or start evidence, or progress moves backward without explanation.Not executed.Missing: no queue acceptance, start, progress-sequence, or render evidence.Runtime-adapter and component owners. Retest after event mapping is instrumented. Block.
Partial completionContent: flight succeeded, hotel failed, and rail still running. Check for separate child outcomes and a parent summary that says incomplete. Fail if the parent says succeeded, successful data disappears, or the failed branch erases sibling results.Not executed.Missing: no child terminal events, parent aggregation record, or rendered hierarchy.Orchestration and task-summary owners. Retest after parallel aggregation is implemented. Block.
Active cancellationContent: request cancellation of only the rail branch while flight remains complete and hotel remains retryable. Show requested cancellation separately from acknowledged termination. Fail if siblings stop, the UI claims cancelled before the executor or orchestrator acknowledges it, or completed effects are described as undone.Not executed.Missing: no cancellation request, execution-layer acknowledgement, scope, or post-cancellation state record.Executor and orchestration owners. Retest after scoped cancellation is supported. Block.
RetryContent: retry the failed hotel lookup. Check for a new invocation ID, attempt 2, retryOf link, current authorization check, scheduler acceptance before queued is displayed, and preserved attempt 1. Fail if the old record is overwritten or a consequential call repeats without current policy checks.Not executed.Missing: no retry decision, lineage, scheduler acceptance, second dispatch, or result evidence.Orchestration and persistence owners. Retest after attempt lineage is stored and rendered. Block.
Dependency-gated synthesisContent: synthesis depends on flight, hotel retry, and rail closure. Keep synthesis proposed while a required dependency is unresolved. If the scheduler supports dependency-gated queueing, require its acceptance record, queue reference, and dependency condition. Fail if a dependency rule alone is presented as queue evidence.Not executed.Missing: no scheduler capability record, dependency resolution, acceptance event, or synthesis result.Scheduler-adapter and orchestration owners. Retest after the queueing model is declared and instrumented. Block.
Long structured outputContent: nested itinerary legs, fare rules, fees, warnings, provider references, and an intentionally long policy field. Check hierarchy, wrapping, truncation disclosure, copy behavior, and access to the complete stored result. Fail if critical fees or warnings are clipped, fields are silently dropped, or the card claims completeness without a result reference.Not executed.Missing: no structured fixture render, viewport inspection, or result-integrity comparison.Component and data-mapping owners. Retest with the versioned long-output fixture across supported conditions. Block.
Named consumer: TripDesk booking assistant, tool-task panel. Status: illustrative and not executed.

Keep accessibility claims within their evidence boundary

The record can require keyboard, focus, announcement, zoom, contrast, and assistive-technology checks for supported product conditions. Don't claim accessibility conformance until those evaluations have been run and recorded in the named consumer.

Locate the first divergent layer

When expected and displayed states disagree, inspect the chain in order. Compare the runtime event with the adapter mapping. Then inspect application policy, persisted invocation data, component rendering, and the recorded user decision. Stop at the first layer where expected and actual facts diverge.

  1. 1

    Check runtime events

    Confirm that the event belongs to the current invocation and attempt, has the expected sequence, and carries the terminal or intermediate fact the adapter expects.

  2. 2

    Check application policy

    Confirm that consequence classification, authorization source, expiry, permitted actions, pre-dispatch closure, active cancellation, retry, and recovery match the current context.

  3. 3

    Check persisted invocation data

    Confirm that identifiers, lineage, observations, decisions, results, closure facts, and evidence references were stored without overwriting earlier attempts.

  4. 4

    Check component rendering

    Confirm that the component maps the normalized record to the right label, disclosure, hierarchy, and controls without manufacturing a stronger claim.

  5. 5

    Check recorded user decisions

    Confirm that approval, denial, dismissal, withdrawal, cancellation, retry, and alternate-route selections target the intended invocation and remain within the user's authority.

  6. 6

    Choose a disposition

    Accept when the required evidence matches the contract. Revise when the intended behavior is sound but a correctable layer diverges. Block when required authority, runtime evidence, recovery, or safe presentation is missing.

Keep Identity Forge at the visual-system boundary

Identity Forge can supply the upstream visual-system inputs for a tool-call interface: semantic light and dark tokens, typography, spacing, layout guidance, motifs, DESIGN.md, and implementation exports. These artifacts can keep task cards, approval panels, disclosures, and status treatments visually coherent.

The consuming application still owns tool schemas, authorization, execution, side effects, persistence, cancellation, recovery, runtime evidence, product-specific accessibility evaluation, and acceptance. A design kit can't prove that an approval is valid, a payment completed, a cancellation reached the executor, or an interface is safe to ship.

Give the tool-call components a consistent visual input

Once your application owns the lifecycle and policy contract, use an Identity Forge kit to supply the visual tokens, typography, spacing, and implementation guidance around it.

Start with one real consumer and one consequential tool. Fill the contract, declare who owns each phase, run the acceptance cases, and resolve the first divergent layer before adding another tool-call pattern.

Sources