Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/website/content/AGENTS.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,17 @@ export class ChatComponent {
- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree
- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself


## Interrupts and recovery

- Both adapters expose `interrupt()` and `submit({ resume })`; the backend defines the decision payload.
- AG-UI `auto` prefers native interrupt batches regardless of event order. Answer every pending native ID once; `status: 'cancelled'` entries omit `payload`.
- 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 }`.
- 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.
- Capture `interruptSession().generation` when rendering and pass it as the `interruptGeneration` submit option to reject stale controls.
- 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.
- 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.
- An approval card closing or local streaming stopping does not prove backend completion or cancellation.

## Version check
If this file is stale, fetch the latest: https://threadplane.ai/llms-full.txt
12 changes: 12 additions & 0 deletions apps/website/content/CLAUDE.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,17 @@ export class ChatComponent {
- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree
- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself


## Interrupts and recovery

- Both adapters expose `interrupt()` and `submit({ resume })`; the backend defines the decision payload.
- AG-UI `auto` prefers native interrupt batches regardless of event order. Answer every pending native ID once; `status: 'cancelled'` entries omit `payload`.
- 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 }`.
- 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.
- Capture `interruptSession().generation` when rendering and pass it as the `interruptGeneration` submit option to reject stale controls.
- 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.
- 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.
- An approval card closing or local streaming stopping does not prove backend completion or cancellation.

## Version check
If this file is stale, fetch the latest: https://threadplane.ai/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ That distinction lives in the composition.
9. The graph continues to `issue_refund` and finishes.

It is one thread and one persisted state.
If the operator closes the tab and returns later, the interrupt is still pending.
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.

<figure>
<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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ featured: true
---

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`.

**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.
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.
The Angular component is byte-identical except the import.

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

The adapter clears `agent.interrupt()` immediately for snappy UX, then forwards the resume:
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:

```ts
source.runAgent({ forwardedProps: { command: { resume: { approved: true, amount: 99 } } } });
Expand All @@ -430,7 +432,7 @@ Different wire, same Angular surface.
The runtime-neutral `Agent` contract is not a marketing line.
It is the reason this post existed without rewriting the component.
`<chat-approval-card>`, `agent.interrupt()`, and `submit({ resume })` are the stable surface.
`on_interrupt` and `forwardedProps.command.resume` are the AG-UI-specific wire details the adapter hides.
`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.

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`.
Your chat surface does not pick.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ It failed silently, which only means it took longer to notice.

The fix was small once the cause was clear, and it landed in [#888](https://github.com/cacheplane/angular-agent-framework/pull/888).
Both conventions are now recognized.
Within one run the first signal to arrive wins, because Mastra emits both and a doubled interrupt helps nobody.
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.

I want to name the mistake precisely, because "we had a bug" is not the lesson.
We built a protocol adapter and then tested it exclusively against one bridge implementation of that protocol.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ I believe it holds, because the contract is built on the protocol event vocabula
But belief is not measurement, and I am not going to dress one up as the other.

Two smaller caveats.
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.
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.
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.

*Editor's note: the measurement exists.*
Expand Down
10 changes: 9 additions & 1 deletion apps/website/content/docs/ag-ui/api/inject-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ The AG-UI adapter extends the neutral `Agent` contract with AG-UI-specific proto
|-------|------|-------------|
| `customEvents()` | `CustomStreamEvent[]` | Custom events emitted by the backend during a run. Accumulates per run; resets when `RUN_STARTED` arrives. |
| `clientTools` | `ClientToolsCapability` | Browser client-tool catalog, pending calls, and result resolution used by `<chat [clientTools]>`. |
| `ready` | `Promise<void>` | Resolves after configured persisted state is hydrated. |
| `interruptSession()` | `InterruptSessionSnapshot` | Full batch, generation, ownership phase, and retained resume attempt. |
| `reconcileInterrupt()` | `Promise<void>` | Applies authoritative recovery using the application-provided persistence reconciler. |
| `dispose()` | `void` | Stops local work; providers call it on injector destruction. Does not cancel backend checkpoints. |
| `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. |

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

Resume an interrupt by passing a `resume` payload:
Resume a pending single interrupt by passing the payload expected by the backend:

```ts
await chat.submit({ resume: { approved: true } });
```

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.

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.

## Regenerate semantics

`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.
Expand Down
4 changes: 4 additions & 0 deletions apps/website/content/docs/ag-ui/api/provide-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ const agent = injectAgent(); // AgUiAgent<Record<string, unknown>>
| `url` | `string` | HTTP endpoint for the AG-UI backend agent. Required. |
| `agentId` | `string` | Agent identifier, when the endpoint serves more than one agent. |
| `threadId` | `string` | Thread to connect to on start. Omit to begin a fresh conversation. |
| `interruptTransport` | `InterruptTransport` | `auto` (default), `protocol`, `legacy-command`, or `mastra-command`. Native batches win in `auto`; select the Mastra command profile for the Mastra integration. |
| `persistence` | `AgUiInterruptPersistence` | Application-owned store and optional authoritative reconciler. Requires a stable configured `threadId` and scoped namespace. |
| `headers` | `Record<string, string>` | Extra HTTP headers sent with every request. |
| `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. |

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.

## Static versus factory config

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.
Expand Down
8 changes: 8 additions & 0 deletions apps/website/content/docs/ag-ui/api/to-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const agent = toAgent(source, { telemetry: myTelemetrySink });

| Option | Type | Description |
|--------|------|-------------|
| `interruptTransport` | `InterruptTransport` | `auto` (default), `protocol`, `legacy-command`, or `mastra-command`. Native batches take precedence in `auto`. |
| `persistence` | `AgUiInterruptPersistence` | Application-owned durable storage and optional authoritative reconciliation. Requires a stable source `threadId` and scoped namespace. |
| `telemetry` | `AgentRuntimeTelemetrySink \| false` | Optional app-owned sink. Supply one to receive runtime lifecycle events. |
| `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. |

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

| Field | Type | Description |
|-------|------|-------------|
| `ready` | `Promise<void>` | Resolves after persisted state is hydrated; actions also wait for hydration. |
| `interruptSession()` | `InterruptSessionSnapshot` | Current batch, generation, phase, and retained attempt. |
| `reconcileInterrupt()` | `Promise<void>` | Applies authoritative recovery through the configured persistence reconciler. An unknown result leaves recovery blocked. |
| `dispose()` | `void` | Stops local work and unsubscribes. Call when a directly created adapter is no longer needed; it does not cancel backend checkpoints. |
| `customEvents()` | `Signal<CustomStreamEvent[]>` | Custom events accumulated during a run. Resets at the start of each new run. |
| `clientTools` | `ClientToolsCapability` | Browser client-tool catalog, pending calls, and result resolution. The chat composition uses this when you pass `<chat [clientTools]>`. |
| `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. |

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

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.

## CustomStreamEvent

`CustomStreamEvent` is the element type of `AgUiAgent.customEvents`:
Expand Down
Loading
Loading