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 evidence | Permitted user actions | Likely next transitions | |
|---|---|---|---|
| Proposed | A 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 input | The 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 approval | A 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. |
| Queued | The 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. |
| Running | An 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. |
| Streaming | At 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 complete | One 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. |
| Succeeded | A 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. |
| Failed | A 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. |
| Cancelled | The 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. |
| Expired | Policy 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. |
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 source | Confirmation timing | Allowed controls | Cancellation behavior | |
|---|---|---|---|---|
| Read-only | Data-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. |
| Reversible | Policy 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 visible | Policy 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. |
| Destructive | Resource 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-gated | A 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 consequential | Budget 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. |
| Undo requirement | Recovery route | Durable evidence | Important caveat | |
|---|---|---|---|---|
| Read-only | Usually 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. |
| Reversible | Name 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 visible | Only 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. |
| Destructive | Required 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-gated | Not 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 consequential | Use 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. |
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"]
}
]
}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"
}
}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 criteria | Actual observation | Evidence status | Owner, retest trigger, disposition | |
|---|---|---|---|---|
| Approval | Content: 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 closure | Content: 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. |
| Expiry | Content: 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. |
| Progress | Content: 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 completion | Content: 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 cancellation | Content: 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. |
| Retry | Content: 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 synthesis | Content: 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 output | Content: 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. |
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
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
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
Check persisted invocation data
Confirm that identifiers, lineage, observations, decisions, results, closure facts, and evidence references were stored without overwriting earlier attempts.
- 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
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
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
- Tool UI - assistant-ui: assistant-ui documents custom React renderers for tool invocations, including loading, result, error, user-input, and interactive states.
- Spring AI 2.0 Tool Calling: Build a Live Claude-Style UI: Dan Vega shows how Spring AI 2.0 tool calls can be observed through an advisor and intermediate tool events merged into a live stream.
- Multi-Step Conversations & Generative UI: Vercel Academy demonstrates multi-step conversations in which calls can run in parallel, later steps can depend on results, and the system can synthesize a response.
- Agentic Design Pattern #1: Tool Calling: DAGWorks presents tool calling as an orchestration pattern and uses explicit application state to support monitoring and visibility.
- 16 AI agent UI design patterns for plans, approvals and undo: Setproduct catalogs visual patterns for delegated-task phases, including plans, running actions, approvals, completion, and undo.
- Tool Use Design Pattern: Microsoft describes tool schemas, execution logic, message handling, integrations, validation, error handling, state management, and trust as tool-use concerns.
- Design system for AI chat apps: define states, actions, and recovery: Identity Forge's adjacent chat-interface guide separates visible conversation states from evidence, permitted actions, persistence, and recovery.
- Identity Forge: Identity Forge supplies visual-system inputs such as semantic tokens, typography, spacing, layout guidance, DESIGN.md, and implementation exports for coding agents.