generative-a11y
Lifecycle

Tool lifecycle

Tell users when an app action starts, makes progress, finishes, or fails without exposing private arguments or results.

Report the start, progress, and result

Tool arguments can arrive before execution. Send tool.started only when the operation begins, meaningful tool.progress milestones while it runs, and one tool.completed or tool.failed event when it ends. Numeric progress is optional; when present, use a value from 0 to 1.

Use the runtime already connected to your application:

Report a tool lifecycle
runtime.dispatch({
  type: "tool.started",
  toolId: "report-1",
  label: "Prepare report",
});
runtime.dispatch({
  type: "tool.progress",
  toolId: "report-1",
  label: "Prepare report",
  progress: 0.5,
  message: "Halfway complete",
});
runtime.dispatch({
  type: "tool.completed",
  toolId: "report-1",
  label: "Prepare report",
});

Field reference for all four events:

EventFieldRequiredMeaning
tool.startedtoolIdyesStable ID for the logical operation.
toolInstanceIdnoIdentifies one attempt when the tool runs repeatedly.
labelyesShort, translated, user-facing name of the operation.
tool.progresstoolIdyesThe same ID from the start.
toolInstanceIdnoMust match the start's instance when one was given.
labelyesShort, translated, user-facing name.
progressnoNormalized progress from 0 to 1.
messagenoShort, translated milestone copy.
tool.completedtoolIdyesThe same ID from the start.
toolInstanceIdnoMust match the start's instance when one was given.
labelyesShort, translated, user-facing name.
summarynoShort result copy, announced when present.
tool.failedtoolIdyesThe same ID from the start.
toolInstanceIdnoMust match the start's instance when one was given.
labelyesShort, translated, user-facing name.
errornoDiagnostic-only detail. Never announced.
announcementnoShort, translated, safe failure copy. Announced.

Every field has a privacy contract. label, message, summary, and announcement are user-facing copy and must already be translated and safe to share. error is diagnostic-only and is never announced, which is why backend detail belongs there. Raw tool results are not a field at all; they stay private by construction.

Keep one tool ID

Keep toolId stable for the operation across start, progress, completion, or failure. The ID is the thread that ties the lifecycle together; the runtime diagnoses progress for an unknown toolId as an unknown tool and produces nothing. Labels may change between events if your copy does, but the ID must not. For repeated executions of the same logical tool, add toolInstanceId so late output from an earlier run is diagnosed as stale instead of being attached to the new attempt. See stable IDs for the general rules.

Separate repeated executions
runtime.dispatch({
  type: "tool.started",
  toolId: "web-search",
  toolInstanceId: "search-1",
  label: "Search the web",
});
runtime.dispatch({
  type: "tool.completed",
  toolId: "web-search",
  toolInstanceId: "search-1",
  label: "Search the web",
  summary: "Found 4 sources.",
});

Provide localized copy

Labels and progress messages are user-facing copy; supply short, translated text. The runtime never invents wording: start announcements are rendered from a catalog message parameterized by your label, progress announcements use your message when you provide one and a catalog fallback otherwise, and completions announce your summary when present. The catalog itself is construction-time and locale-tagged, and events carry a locale field so prepared announcements inherit the right locale. If your app supports multiple languages, translate the labels before dispatching; the runtime cannot translate for you, and an English label in a Japanese UI is a bug in your host, not in the library.

Report progress milestones

Send tool.progress for meaningful milestones only, not for every tick of a progress callback. When progress is present it must be a finite number from 0 to 1; values outside that range are diagnosed as invalid events. When progress announcements are enabled, the runtime coalesces them into buckets of progressEveryPercent (25 percent in the balanced policy), so a burst of updates within one bucket produces a single announcement and the rest are diagnosed as below the progress threshold.

The balanced policy keeps tool verbosity deliberately low:

SettingBalanced valueEffect
announceStarttrueStart announcements exist...
announceStartAfterMs1,500...but delayed 1.5 seconds, so quick tools stay
quiet and the listener hears only the completion.
announceProgressfalseProgress is silent by default.
progressEveryPercent25Bucket size when progress is enabled.
announceCompletiontrueCompletions are announced.
announceFailuretrueFailures are announced on the polite channel.

A tool that finishes before the 1.5-second start delay never announces a start at all: the terminal event cancels the pending start announcement, and the listener hears a single completion. That is the point. Most tool calls in an agent loop are short, and announcing every start would be a drumbeat of noise. If your tool is genuinely long-running and users need the early signal, keep the default; the start announcement arrives 1.5 seconds in, which is early enough to matter and late enough to skip the trivial.

Summarize results briefly

tool.completed accepts a summary: one short sentence about the result, announced when present. Use it for outcomes the user is waiting on: "Found 4 sources.", "Report saved to Downloads.", "No conflicts found." When summary is absent, the runtime falls back to a catalog message built from the label. Do not stuff raw tool output into summary; it is announced verbatim, so it must be short, translated, and free of backend detail. If the honest summary is "the tool returned 40 kilobytes of JSON," the summary is not the place for it.

Keep backend data out of announcements

Use error for debugging; generative-a11y never announces it. Set announcement only when your app has a short, translated message that is safe to share. Raw tool results also stay private.

The failure path deserves an example because it is the one place where panic writes bad copy:

Report a tool failure safely
runtime.dispatch({
  type: "tool.failed",
  toolId: "report-1",
  label: "Prepare report",
  error: "template render: missing variable 'q3_revenue' (report.tpl:42)",
  announcement: "The report could not be prepared. Check the template.",
});

The listener hears only the announcement, on the polite channel. The error field, with its filename, line number, and variable name, goes to the diagnostic trace and your logs. Note that tool failures announce on the polite channel, not the assertive one: a failed tool is status information, and the balanced policy reserves the assertive channel for response failures and urgent interaction requests. If a tool failure genuinely demands interruption, that is your host's decision to make through its own UI, not something to smuggle into this event.

What these events prove

A tool.completed event proves your host reported a completion and the runtime prepared the corresponding announcement. It does not prove the tool did the right thing, that the summary is accurate, or what a screen reader spoke. Tool events describe your app's reporting of its own work. Test the heard experience with real screen readers, and keep the diagnostic fields honest so the trace is worth reading when something goes wrong.