Skip to content

Commit b6932e3

Browse files
authored
docs: align public interrupt and recovery guidance
Align package and website interrupt documentation, add verified decision and recovery examples, and synchronize public machine-readable guidance.
1 parent 1852fcb commit b6932e3

30 files changed

Lines changed: 628 additions & 51 deletions

apps/website/content/AGENTS.md.template

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,17 @@ export class ChatComponent {
7070
- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree
7171
- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself
7272

73+
74+
## Interrupts and recovery
75+
76+
- Both adapters expose `interrupt()` and `submit({ resume })`; the backend defines the decision payload.
77+
- AG-UI `auto` prefers native interrupt batches regardless of event order. Answer every pending native ID once; `status: 'cancelled'` entries omit `payload`.
78+
- For Mastra, set `interruptTransport: 'mastra-command'`. The adapter sends the decision in `forwardedProps.command.resume` and observed correlation IDs in `command.interruptEvent`. The campsite example rejects with `{ approved: false }`.
79+
- AG-UI restoration is opt-in through `persistence`: stable `threadId`, scoped namespace, and application-owned atomic compare-and-swap storage. Await `agent.ready` before rendering restored decisions.
80+
- Capture `interruptSession().generation` when rendering and pass it as the `interruptGeneration` submit option to reject stale controls.
81+
- Retry the retained decision only after proven non-dispatch or authoritative reconciliation. Configure `persistence.reconcile` and call `agent.reconcileInterrupt()` for uncertain outcomes; a network error alone does not prove non-execution.
82+
- These recovery APIs are AG-UI extensions. LangGraph uses its own thread/checkpoint APIs. Client storage cannot recreate a lost backend checkpoint or guarantee exactly-once side effects.
83+
- An approval card closing or local streaming stopping does not prove backend completion or cancellation.
84+
7385
## Version check
7486
If this file is stale, fetch the latest: https://threadplane.ai/llms-full.txt

apps/website/content/CLAUDE.md.template

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,17 @@ export class ChatComponent {
7070
- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree
7171
- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself
7272

73+
74+
## Interrupts and recovery
75+
76+
- Both adapters expose `interrupt()` and `submit({ resume })`; the backend defines the decision payload.
77+
- AG-UI `auto` prefers native interrupt batches regardless of event order. Answer every pending native ID once; `status: 'cancelled'` entries omit `payload`.
78+
- For Mastra, set `interruptTransport: 'mastra-command'`. The adapter sends the decision in `forwardedProps.command.resume` and observed correlation IDs in `command.interruptEvent`. The campsite example rejects with `{ approved: false }`.
79+
- AG-UI restoration is opt-in through `persistence`: stable `threadId`, scoped namespace, and application-owned atomic compare-and-swap storage. Await `agent.ready` before rendering restored decisions.
80+
- Capture `interruptSession().generation` when rendering and pass it as the `interruptGeneration` submit option to reject stale controls.
81+
- Retry the retained decision only after proven non-dispatch or authoritative reconciliation. Configure `persistence.reconcile` and call `agent.reconcileInterrupt()` for uncertain outcomes; a network error alone does not prove non-execution.
82+
- These recovery APIs are AG-UI extensions. LangGraph uses its own thread/checkpoint APIs. Client storage cannot recreate a lost backend checkpoint or guarantee exactly-once side effects.
83+
- An approval card closing or local streaming stopping does not prove backend completion or cancellation.
84+
7385
## Version check
7486
If this file is stale, fetch the latest: https://threadplane.ai/llms-full.txt

apps/website/content/blog/2026-05-28-human-in-the-loop-langgraph-agents-in-angular.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ That distinction lives in the composition.
203203
9. The graph continues to `issue_refund` and finishes.
204204

205205
It is one thread and one persisted state.
206-
If the operator closes the tab and returns later, the interrupt is still pending.
206+
If the operator returns to the same thread and its server checkpoint is retained, the interrupt can still be pending. Preserve the thread ID and use durable backend storage; closing the tab does not itself configure restoration.
207207

208208
<figure>
209209
<img src="/blog/2026-05-28-human-in-the-loop-langgraph-agents-in-angular/3.png" alt="The chat after approval, showing the agent's draft summary and a confirmation: 'Refund of $47.50 issued to cus_a8x2k. Refund ID: re_demo__a8x2k.'" width="1280" height="800" />

apps/website/content/blog/2026-06-04-human-in-the-loop-ag-ui-agents-in-angular.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ featured: true
88
---
99

1010
This is how to pause an AG-UI agent in Angular for human approval before it runs a high-stakes tool, using a `CUSTOM` `on_interrupt` event and the `<chat-approval-card>` composition from `@threadplane/chat`.
11+
12+
**Updated September 9, 2026:** This tutorial uses the LangGraph compatibility bridge. The adapter also supports native interrupt batches, which take precedence in `auto` mode; Mastra requires its explicit `mastra-command` profile. Resume now claims a pending batch, and uncertain delivery requires authoritative reconciliation before retry. Client restoration is opt-in and cannot replace the server checkpointer.
1113
The example is the same refund agent from [Human-in-the-Loop LangGraph Agents in Angular](/blog/2026-05-28-human-in-the-loop-langgraph-agents-in-angular) — wired through the AG-UI adapter instead.
1214
The Angular component is byte-identical except the import.
1315

@@ -406,7 +408,7 @@ When the operator clicks Approve:
406408
this.agent.submit({ resume: { approved: true, amount: this.editAmount() ?? payload.amount } });
407409
```
408410

409-
The adapter clears `agent.interrupt()` immediately for snappy UX, then forwards the resume:
411+
The adapter claims the pending interrupt before dispatching the resume. The card closing is a local UI transition, not proof that the refund completed. For this compatibility bridge, the request carries:
410412

411413
```ts
412414
source.runAgent({ forwardedProps: { command: { resume: { approved: true, amount: 99 } } } });
@@ -430,7 +432,7 @@ Different wire, same Angular surface.
430432
The runtime-neutral `Agent` contract is not a marketing line.
431433
It is the reason this post existed without rewriting the component.
432434
`<chat-approval-card>`, `agent.interrupt()`, and `submit({ resume })` are the stable surface.
433-
`on_interrupt` and `forwardedProps.command.resume` are the AG-UI-specific wire details the adapter hides.
435+
`on_interrupt` and `forwardedProps.command.resume` are this LangGraph bridge's compatibility wire details. Other AG-UI runtimes can require native correlated entries or an explicit command profile.
434436

435437
My recommendation is simple: pick the adapter that matches your backend — LangGraph SDK direct → `@threadplane/langgraph`; anything AG-UI-fronted, including LangGraph-via-`ag-ui-langgraph``@threadplane/ag-ui`.
436438
Your chat surface does not pick.

apps/website/content/blog/2026-08-31-we-measured-the-runtime-swap.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ It failed silently, which only means it took longer to notice.
8686

8787
The fix was small once the cause was clear, and it landed in [#888](https://github.com/cacheplane/angular-agent-framework/pull/888).
8888
Both conventions are now recognized.
89-
Within one run the first signal to arrive wins, because Mastra emits both and a doubled interrupt helps nobody.
89+
Updated September 9, 2026: the adapter retains both forms in one session. Native batches take precedence in `auto` mode regardless of arrival order; the Mastra integration explicitly selects `mastra-command` so the resume includes its decision and suspended-tool correlation.
9090

9191
I want to name the mistake precisely, because "we had a bug" is not the lesson.
9292
We built a protocol adapter and then tested it exclusively against one bridge implementation of that protocol.

apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ I believe it holds, because the contract is built on the protocol event vocabula
299299
But belief is not measurement, and I am not going to dress one up as the other.
300300

301301
Two smaller caveats.
302-
The interrupt path accepts two conventions — the protocol-standard `RUN_FINISHED` interrupt outcome, and the `CUSTOM` `on_interrupt` event the LangGraph bridge emits — and within a single run the first signal wins.
302+
The interrupt path accepts two conventions — the protocol-standard `RUN_FINISHED` interrupt outcome, and the `CUSTOM` `on_interrupt` event the LangGraph bridge emits — and retains both in one session. Updated September 9, 2026: native batches take precedence in `auto` mode regardless of arrival order; Mastra uses the explicit `mastra-command` profile.
303303
And the subagent path rests on the protocol's `SUBAGENT_*` events, which the adapter consumes directly; each measured runtime reaches them through a small in-tree emitter, as [the measurement post](/blog/we-measured-the-runtime-swap#what-stayed-partial) records.
304304

305305
*Editor's note: the measurement exists.*

apps/website/content/docs/ag-ui/api/inject-agent.mdx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ The AG-UI adapter extends the neutral `Agent` contract with AG-UI-specific proto
7676
|-------|------|-------------|
7777
| `customEvents()` | `CustomStreamEvent[]` | Custom events emitted by the backend during a run. Accumulates per run; resets when `RUN_STARTED` arrives. |
7878
| `clientTools` | `ClientToolsCapability` | Browser client-tool catalog, pending calls, and result resolution used by `<chat [clientTools]>`. |
79+
| `ready` | `Promise<void>` | Resolves after configured persisted state is hydrated. |
80+
| `interruptSession()` | `InterruptSessionSnapshot` | Full batch, generation, ownership phase, and retained resume attempt. |
81+
| `reconcileInterrupt()` | `Promise<void>` | Applies authoritative recovery using the application-provided persistence reconciler. |
82+
| `dispose()` | `void` | Stops local work; providers call it on injector destruction. Does not cancel backend checkpoints. |
7983
| `subagents()` | `Map<string, Subagent>` | Subagent runs from `SUBAGENT_*` events, keyed by `subagentRunId`, plus the `ACTIVITY_*` convention (`activityType: 'subagent'`, keyed by `messageId`), projected to the neutral subagent contract. |
8084

8185
`injectAgent()` returns the `AgUiAgent` type — the neutral `Agent` contract plus these AG-UI-specific fields — so they are reachable directly, no cast required:
@@ -100,12 +104,16 @@ Use the runtime-neutral submit shape for normal chat input:
100104
await chat.submit({ message: 'Summarize this document' });
101105
```
102106

103-
Resume an interrupt by passing a `resume` payload:
107+
Resume a pending single interrupt by passing the payload expected by the backend:
104108

105109
```ts
106110
await chat.submit({ resume: { approved: true } });
107111
```
108112

113+
Native batches require every pending ID exactly once; cancelled entries omit payload. Capture `chat.interruptSession().generation` when rendering approval controls and pass `{ interruptGeneration: generation }` as the second submit argument to reject stale decisions. For the Mastra integration, configure `interruptTransport: 'mastra-command'` on the provider.
114+
115+
Await `chat.ready` before showing restored controls. `retry()` can resend the retained decision after a proven pre-dispatch failure; uncertain delivery requires `chat.reconcileInterrupt()` with authoritative backend evidence before another attempt. These extensions do not imply the same recovery API on the LangGraph adapter.
116+
109117
## Regenerate semantics
110118

111119
`regenerate(assistantMessageIndex)` has replace semantics: it keeps the user message before the selected assistant message, removes the selected assistant message and all later messages, syncs the rollback to the AG-UI source, then reruns with no new user message appended.

apps/website/content/docs/ag-ui/api/provide-agent.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,13 @@ const agent = injectAgent(); // AgUiAgent<Record<string, unknown>>
4646
| `url` | `string` | HTTP endpoint for the AG-UI backend agent. Required. |
4747
| `agentId` | `string` | Agent identifier, when the endpoint serves more than one agent. |
4848
| `threadId` | `string` | Thread to connect to on start. Omit to begin a fresh conversation. |
49+
| `interruptTransport` | `InterruptTransport` | `auto` (default), `protocol`, `legacy-command`, or `mastra-command`. Native batches win in `auto`; select the Mastra command profile for the Mastra integration. |
50+
| `persistence` | `AgUiInterruptPersistence` | Application-owned store and optional authoritative reconciler. Requires a stable configured `threadId` and scoped namespace. |
4951
| `headers` | `Record<string, string>` | Extra HTTP headers sent with every request. |
5052
| `telemetry` | `AgentRuntimeTelemetrySink \| false` | Omit for automatic development-only collection, pass `false` to disable it, or pass an app-owned sink to receive the runtime lifecycle events yourself. |
5153

54+
With persistence enabled, await `agent.ready` before displaying restored approval controls. The store must implement atomic `compareAndSwap`; browser state alone cannot restore a lost backend checkpoint. The provider disposes the adapter when its injector is destroyed, stopping local work without cancelling backend checkpoints.
55+
5256
## Static versus factory config
5357

5458
Pass a plain `AgentConfig` object when the URL is known up front. Pass a `() => AgentConfig` factory when the config depends on runtime DI state — the factory runs inside an Angular injection context, so it may call `inject()` to read services, route params, or environment tokens.

apps/website/content/docs/ag-ui/api/to-agent.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const agent = toAgent(source, { telemetry: myTelemetrySink });
2424

2525
| Option | Type | Description |
2626
|--------|------|-------------|
27+
| `interruptTransport` | `InterruptTransport` | `auto` (default), `protocol`, `legacy-command`, or `mastra-command`. Native batches take precedence in `auto`. |
28+
| `persistence` | `AgUiInterruptPersistence` | Application-owned durable storage and optional authoritative reconciliation. Requires a stable source `threadId` and scoped namespace. |
2729
| `telemetry` | `AgentRuntimeTelemetrySink \| false` | Optional app-owned sink. Supply one to receive runtime lifecycle events. |
2830
| `a2uiClientCapabilities` | `{ supportedCatalogIds: string[]; inlineCatalogs?: unknown[] }` | A2UI catalog negotiation to advertise to the agent. Seeded once into the AG-UI shared state under the `a2ui_client_capabilities` key, so every `RunAgentInput.state` carries it. Use `a2uiClientCapabilities()` from `@threadplane/chat` for the renderer's standard value. |
2931

@@ -33,12 +35,18 @@ const agent = toAgent(source, { telemetry: myTelemetrySink });
3335

3436
| Field | Type | Description |
3537
|-------|------|-------------|
38+
| `ready` | `Promise<void>` | Resolves after persisted state is hydrated; actions also wait for hydration. |
39+
| `interruptSession()` | `InterruptSessionSnapshot` | Current batch, generation, phase, and retained attempt. |
40+
| `reconcileInterrupt()` | `Promise<void>` | Applies authoritative recovery through the configured persistence reconciler. An unknown result leaves recovery blocked. |
41+
| `dispose()` | `void` | Stops local work and unsubscribes. Call when a directly created adapter is no longer needed; it does not cancel backend checkpoints. |
3642
| `customEvents()` | `Signal<CustomStreamEvent[]>` | Custom events accumulated during a run. Resets at the start of each new run. |
3743
| `clientTools` | `ClientToolsCapability` | Browser client-tool catalog, pending calls, and result resolution. The chat composition uses this when you pass `<chat [clientTools]>`. |
3844
| `subagents()` | `Signal<Map<string, Subagent>>` | Subagent runs from `SUBAGENT_*` events, keyed by `subagentRunId`, plus the `ACTIVITY_*` convention (`activityType: 'subagent'`, keyed by `messageId`), projected to the neutral subagent contract. |
3945

4046
The standard `Agent` signals (`messages`, `status`, `isLoading`, `error`, `toolCalls`, `state`, `interrupt`) and actions (`submit`, `retry`, `stop`, `regenerate`) are all present.
4147

48+
Capture `interruptSession().generation` when rendering a decision and pass it to `submit(input, { interruptGeneration })` to reject stale controls. A proven pre-dispatch failure retains the exact decision for `retry()`; uncertain delivery requires authoritative reconciliation first. These recovery extensions belong to `AgUiAgent`, not the neutral `Agent` contract.
49+
4250
## CustomStreamEvent
4351

4452
`CustomStreamEvent` is the element type of `AgUiAgent.customEvents`:

0 commit comments

Comments
 (0)