diff --git a/apps/website/content/docs/ag-ui/guides/client-tools.mdx b/apps/website/content/docs/ag-ui/guides/client-tools.mdx index 10fb80868..78ff28671 100644 --- a/apps/website/content/docs/ag-ui/guides/client-tools.mdx +++ b/apps/website/content/docs/ag-ui/guides/client-tools.mdx @@ -4,6 +4,165 @@ description: Declare a tool in the Angular app, let an AG-UI agent call it, and # Client Tools -An AG-UI agent can call tools that live in the browser. The Angular app declares the tool, the backend ends its turn when the agent calls it, the browser runs it, and the result is submitted back as a tool message so the run continues. The tool has no server-side implementation. +An AG-UI agent can call tools that live in the browser. The Angular app declares the tool, the backend binds the declaration onto the model without implementing it, and the run ends when the model calls it. The browser executes the tool and submits the result back as a tool message, which starts the next turn. This guide walks the seven files of the running example, from the graph to the components the model fills. -This page is the live example for client tools over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The browser-side tool contract is documented in the [Chat client tools guide](/docs/chat/guides/client-tools). +## What the demo does + +The Run tab shows the prebuilt `` composition wired to five browser-executed tools. Ask "What is the weather in Denver?" and the model calls `get_weather`, whose handler runs in the page and returns a reading the model then summarizes in one sentence. + +Ask it to *show* a weather card and the model calls `weather_card` instead, filling an Angular component's inputs with the readings it invents; the card renders inline in the transcript. Ask it to book something and `confirm_booking` mounts a card with Confirm and Cancel buttons, and whichever you click becomes the tool result that resumes the run. Two further prompts exercise the edges: a terminal snapshot that ends the turn without a spoken summary, and a slow status check you can interrupt with the stop button. + +## How it is built + +Seven files carry the feature: a graph that binds the browser's tool declarations and ends its turn, the server that exposes it over AG-UI, an application config, the schemas the registry and its components share, the file that declares the registry and passes it to ``, and the two components the model fills. Open the Code tab to read them in place. + +### The graph binds client stubs and ends the turn + +The graph has one node. Its `State` declares a `tools` channel alongside `messages`, because that is where the client tool catalog arrives, and `bind_client_tools` reads it there to bind the declarations onto the model for this invocation. There are no server tools, so `route` returns `END` unconditionally. + + + +`bind_client_tools(llm, [], state)` converts each catalog entry into an OpenAI function-tool dict and calls `llm.bind_tools` with it; the empty list is where your own server tools would go. + + +The browser, not the graph, produces the result. Ending the run is the signal the adapter waits for: it treats a tool call with no result as pending only once the run is no longer loading. A graph that looped back to a tools node would leave the browser nothing to do. + + +### Serving the graph over AG-UI + +The server is a FastAPI application. `LangGraphAgent` wraps the compiled graph and `add_langgraph_fastapi_endpoint` mounts it at `/agent` as an AG-UI event stream, and `/ok` is a plain health check. + + + +### Providing the agent + +`provideAgent()` from `@threadplane/ag-ui` registers the agent at the application root, and `provideChat({})` registers the chat defaults. The example resolves its URL at runtime because the host that serves the demo decides which runtime is attached; your own application passes a `url` directly. + + + +### Declaring the tool registry + +`tools()` builds a name-keyed registry, and each entry is one of three declarations from `@threadplane/chat`. `action()` takes a handler that runs in the browser and may return a promise, `view()` takes a component the model fills, and `ask()` takes a component that reports a value back. Every entry carries a description the model reads and a schema authored with `zod/v4`. The `weather_card` and `confirm_booking` entries take their schemas from `schemas.ts`, the file the registry and the two components it points at all import from. + +```ts +import { z } from 'zod/v4'; + +/** Schema for the `weather_card` view tool. */ +export const weatherCardSchema = z.object({ + location: z.string(), + temperatureF: z.number(), + conditions: z.string(), + humidity: z.number(), + windMph: z.number(), +}); + +/** Schema for the `confirm_booking` ask tool. */ +export const confirmBookingSchema = z.object({ summary: z.string() }); +``` + + + +The `view` and `ask` overloads are typed against the paired component: every field the schema produces must be a declared `input()` on that component, so a schema change is a compile error rather than a silent runtime gap. + + +The catalog the model sees is derived by `deriveJsonSchema`, which calls `toJSONSchema` from `zod/v4`. A validator it cannot convert throws with the tool name in the message. + + +### Passing the registry to chat + +The registry reaches the UI through one input. `` builds a coordinator for that registry, which ships the catalog to the agent, runs function tools, mounts view and ask components, and settles each call. + + + +The catalog is attached to every run the adapter starts, as the `tools` field of the AG-UI run input, so the model sees the browser's tools on the first turn and on every continuation. + +### The view component the model fills + +`weather_card` is a `view`: the model supplies the props, the card renders inline, and the coordinator acknowledges the call itself with `{ shown: true }`. The component types its inputs through `ClientToolViewProps`, which is the schema's output plus two framework-supplied props, `status` and `clientTool`. + + + +Inputs are optional because props arrive while the call streams, so the card reads `clientTool()?.phase` and shows a loading badge until the phase is `complete` and the readings have arrived. + +### The ask component that resumes the run + +`confirm_booking` is an `ask`: the model fills `summary`, and the run waits for a person. The component injects the render host and calls `result({ confirmed })`, which the coordinator matches to the pending call by tool name and settles as that call's result. + + + + +Settling writes the emitted value onto the local tool call, and the mounted component is re-rendered with the merged props. That is why `confirmed` is declared with a default of `undefined`: while it is undefined the buttons are live, and once the user answers the template branches to a frozen line with no buttons. + + +### Stopping a tool that is still running + +A function handler receives a context object as its second argument, carrying an `AbortSignal`. `slow_status_check` waits three seconds through a helper that rejects the moment the signal aborts. + + + +The executor wraps the agent's `stop`, so pressing stop aborts every in-flight handler, and a cancelled call is recorded without starting a new run. + +## The round trip in one pass + + + +The coordinator converts the registry into specs, each with a name, a description and a JSON Schema, and the adapter attaches them to every run it starts. + + +Because the tool has no server implementation, the run finishes with a tool call that carries no result. + + +Pending calls are those whose name is in the catalog, whose result is undefined, and which this client has not resolved yet, and only while the agent is not loading. + + +A function handler runs, a view is acknowledged on mount, and an ask waits for the user's value. + + +The adapter records the result on the local tool call, adds a tool message addressed to the call id, and starts the next run, which the model answers. + + + +## Settle, flush, and resolve + +The adapter implements three operations for a produced result, and the coordinator picks between them. + +`settle(id, result)` records the result: the value is written onto the local tool call so the transcript freezes, and a tool message is added to the agent's outgoing messages. `resolve(id, result)` settles and then asks the adapter to continue the run, which is the normal path. `flush()` is where an adapter makes settled results durable without continuing; in `@threadplane/ag-ui` it is a deliberate no-op, because `settle` already placed the tool message in the message list that the next run carries. + +The choice matters at the edges. A tool declared with `followUp: false`, like `weather_snapshot` in this example, is settled and flushed instead of resolved, so the rendered card is the final answer and no summary follows. A cancelled call takes the same path, because continuing the run is exactly what the user asked not to happen. + + +Consecutive client-tool continuations are capped per user turn, at ten by default. Set `[clientToolContinuationPolicy]` on `` to change the maximum or to observe the limit, and use `maxTurns: 0` for no cap. Calls blocked by the limit are settled with an explanatory error so the model never sees an unanswered tool call. + + +## Requirements on the backend + +Three things must hold. The first fails silently, which makes it the one to check first. + + +The catalog is merged into graph state by `ag-ui-langgraph`. If `State` does not declare a `tools` channel the merged value is dropped, `bind_client_tools` binds nothing, and the model never learns the browser has tools. + + +The graph in this example compiles with `MemorySaver()` because `ag-ui-langgraph` reads the thread's graph state through a checkpointer; a graph served this way needs one, and outside development it should be durable. The tool result itself does not depend on it: the browser adds the tool message to its own message list, and the continuation run carries the whole list back. + +Finally, keep the tool names in the registry and the names in the system prompt aligned. The prompt in this example tells the model which tool to call for each kind of request, and a rename on one side alone is the most common reason a demo goes quiet. + +## Mixing server tools and client tools + +Nothing about this design is exclusive. Pass your server tools as the second argument of `bind_client_tools(llm, server_tools, state)` and route a server tool call to your tools node while a client tool call still ends the turn. The `route_after_agent` helper in the same middleware module does that split for you. + +## What's Next + + + + Render a backend tool call's result through a component the frontend owns. + + + Pause a run mid-flight and resume it with a human decision. + + + The runtime-neutral contract behind action, view, and ask. + + + How AG-UI protocol events become the Agent contract the chat UI reads. + + diff --git a/apps/website/content/docs/ag-ui/guides/interrupts.mdx b/apps/website/content/docs/ag-ui/guides/interrupts.mdx index fa852cd2b..1e60f4269 100644 --- a/apps/website/content/docs/ag-ui/guides/interrupts.mdx +++ b/apps/website/content/docs/ag-ui/guides/interrupts.mdx @@ -1,168 +1,155 @@ -# Interrupts (Human-in-the-Loop) +--- +description: How the AG-UI interrupts example pauses a refund graph, emits a CUSTOM on_interrupt event, and resumes from an approval card in Angular +--- -Interrupts let your AG-UI agent pause mid-run and hand control to a human. +# Interrupts -The agent proposes an action, the run freezes, your Angular UI shows an approval dialog, the user decides, and the agent resumes with the human's decision. +Interrupts let an AG-UI agent pause mid-run and hand control to a human. The agent proposes an action, the run freezes, your Angular UI shows an approval card, the person decides, and the agent resumes with that decision. `injectAgent()` from `@threadplane/ag-ui` surfaces the pending interrupt as an Angular Signal, so the approval flow needs no manual event wiring. The running example is a refund authorization, and this guide walks the four files that make it work. -This guide covers the AG-UI adapter specifics. For the broader conceptual model — lifecycle stages, timeout strategies, typed payloads — see the [LangGraph interrupts guide](/docs/langgraph/guides/interrupts). + +Use interrupts when an agent action is irreversible (sending an email, placing an order, issuing a refund), when the agent needs a human decision it cannot make on its own, or when compliance requires explicit approval before execution. + -## The Wire Format +## What the demo does -AG-UI interrupts arrive as a `CUSTOM` event with `name: "on_interrupt"`: +The Run tab shows the prebuilt `` composition in front of a refund graph served over AG-UI. Ask for a refund and the agent acknowledges the draft in the transcript, and then the run stops: a modal card appears with the amount, the customer identifier, and the reason the agent extracted from your request, above three buttons — Cancel, Edit, and Approve. -```json -{ - "type": "CUSTOM", - "name": "on_interrupt", - "value": "{\"kind\":\"refund_approval\",\"amount\":47.50,\"customer_id\":\"cus_a8x2k\",\"reason\":\"Duplicate charge\"}" -} -``` +Two welcome suggestions set it up. "Refund a duplicate charge" asks for $47.50 back to customer `cus_a8x2k`, and "Refund a chargeback" asks for $129.00 with a different justification, so you can watch the same pause fire on two different payloads. -Two things to note: +Approve resumes the graph, which issues a stand-in refund and posts the refund ID into the transcript. Cancel resumes it with a rejection, and the graph says so and issues nothing. Edit keeps the card open, reveals an amount field, and resumes with an amount the operator chose rather than the one the agent proposed. -- The `value` is a **JSON string**, not an object. The `ag-ui-langgraph` Python package serializes the interrupt payload via `dump_json_safe` before emitting the event. -- The adapter `JSON.parse`s the string automatically. Consumers always see the structured object — you never need to parse it yourself. +## How it is built -**Structuring the payload:** Use a `kind` field so `` can match the right interrupt: +Four files carry the feature: a LangGraph graph that stops in the middle of a run, a FastAPI server that fronts it with the AG-UI protocol, an application config that registers the agent, and a component that maps the card's three buttons onto resume payloads. Open the Code tab to read them in place. -```python -decision = interrupt({ - "kind": "refund_approval", - "amount": amount, - "customer_id": customer_id, - "reason": reason, -}) -``` +### The state behind the approval card -## Reading the Interrupt in Your Component +The graph tracks more than a message list. A Pydantic model describes the fields the agent must extract from the conversation, and the state adds the operator's decision and the resulting refund ID to them. -`injectAgent()` exposes a `interrupt()` signal that is populated whenever the adapter receives an `on_interrupt` CUSTOM event. Pair it with `` from `@threadplane/chat` to render an approval dialog without manual event wiring: + -```typescript -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { ChatComponent, ChatApprovalCardComponent } from '@threadplane/chat'; -import { injectAgent } from '@threadplane/ag-ui'; -import type { ChatApprovalAction } from '@threadplane/chat'; - -@Component({ - standalone: true, - imports: [ChatComponent, ChatApprovalCardComponent], - changeDetection: ChangeDetectionStrategy.OnPush, - template: ` - - - `, -}) -export class RefundApprovalComponent { - protected readonly agent = injectAgent(); - - onAction(action: ChatApprovalAction): void { - if (action === 'approve') { - void this.agent.submit({ resume: { approved: true } }); - } else if (action === 'cancel') { - void this.agent.submit({ resume: { approved: false } }); - } - } -} -``` +`decision_approved` is the field the router reads after the pause, which is why the state carries it rather than deriving it later. -`matchKind` filters on `interrupt().value.kind`. The card renders only when the active interrupt matches — other interrupt kinds are ignored. +### Drafting the refund -## Resuming +The first node makes two model calls. One is a structured-output extraction that fills `customer_id`, `amount`, and `reason` — the three values the approval card renders. The other is a streaming call that writes a human acknowledgement into the transcript while the operator reads the card. -Call `agent.submit({ resume })` with your decision object: + -```typescript -// Approve -void this.agent.submit({ resume: { approved: true } }); +Splitting extraction from narration is deliberate: the card needs typed fields, and the transcript needs prose. -// Reject -void this.agent.submit({ resume: { approved: false } }); +### Pausing at interrupt() -// Approve with an edited field -void this.agent.submit({ resume: { approved: true, amount: 35.00 } }); -``` +`interrupt()` freezes the graph and hands its argument to the transport. The argument is any JSON-serializable value, and it becomes the payload your component renders. When the client resumes, that same call returns the resume value, so the node reads like a straight-line function even though a human answered in the middle of it. + + + +Notice the `kind` field on the payload, which is how the frontend tells this interrupt apart from any other one the graph might raise, and notice that the node treats a resume value that is not a dictionary, or whose `approved` is missing or false, as a rejection. + + +LangGraph resumes by running the interrupting node again, and `interrupt()` returns the resume value instead of pausing a second time. Every line above the `interrupt()` call therefore runs twice, so keep that stretch free of side effects. Reading state, as the example does, is safe; charging a card there is not. + + +### Routing on the decision -Under the hood, `submit({ resume })` calls `runAgent({ forwardedProps: { command: { resume } } })`. The server receives `forwarded_props.command.resume` — the convention the [`ag-ui-langgraph`](https://pypi.org/project/ag-ui-langgraph/) package reads to resume the LangGraph checkpoint. +After the pause the graph branches. A conditional edge sends an approved refund to the node that issues it and sends everything else to the end, which is why the rejection message is written by the interrupting node itself. - -In your LangGraph node, `interrupt({...})` returns the `resume` value directly. You do not need to unwrap `forwarded_props` yourself — `ag-ui-langgraph` does that before resuming the graph. + + +The graph compiles with `MemorySaver()` because `ag-ui-langgraph` reads the thread's graph state through a checkpointer, so a graph served this way needs one; a paused graph is also a stored checkpoint, which is what the resume reads back. + + +An in-memory checkpointer loses every paused run when the process restarts. A deployment that must survive a restart needs a durable saver, such as the Postgres checkpointer, behind the same `checkpointer=` argument. -## End-to-End Example +### Serving the graph over AG-UI + +The backend is a FastAPI application. `LangGraphAgent` from the `ag-ui-langgraph` package wraps the compiled graph, and `add_langgraph_fastapi_endpoint` mounts it at a path that speaks the AG-UI event stream. + + + +That wrapper is what turns a LangGraph pause into the AG-UI event described below. + +### The agent provider -`cockpit/ag-ui/interrupts` is a complete Angular + Python example: a refund-authorization agent that drafts a refund, pauses for operator approval, and issues (or cancels) based on the decision. +`provideAgent()` registers the agent once for the whole application, and `provideChat({})` registers the configuration the `` composition reads, here left at its defaults. The example passes a factory because it resolves its endpoint at runtime from the host that serves the demo. -**Angular component** (`cockpit/ag-ui/interrupts/angular/src/app/interrupts.component.ts`): + + +Pass the URL of your AG-UI endpoint directly in your own application, which does not need the factory: ```typescript -import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; -import { - ChatComponent, - ChatApprovalCardComponent, - type ChatApprovalAction, -} from '@threadplane/chat'; -import { injectAgent } from '@threadplane/ag-ui'; - -@Component({ - standalone: true, - imports: [ChatComponent, ChatApprovalCardComponent], - changeDetection: ChangeDetectionStrategy.OnPush, - template: ` - - - `, -}) -export class InterruptsComponent { - protected readonly agent = injectAgent(); - - protected onAction(action: ChatApprovalAction): void { - if (action === 'approve') { - void this.agent.submit({ resume: { approved: true } }); - } else if (action === 'cancel') { - void this.agent.submit({ resume: { approved: false } }); - } - } -} +provideAgent({ + url: 'https://your-backend.example.com/agent', +}); ``` -**Python graph** (`cockpit/ag-ui/interrupts/python/src/graph.py`) uses `ag-ui-langgraph` to front a standard LangGraph graph: - -```python -from langgraph.types import interrupt -from ag_ui_langgraph import LangGraphAgent, add_langgraph_fastapi_endpoint - -def request_approval(state): - decision = interrupt({ - "kind": "refund_approval", - "amount": state["amount"], - "customer_id": state["customer_id"], - "reason": state["reason"], - }) - approved = isinstance(decision, dict) and decision.get("approved") - return {"decision_approved": approved} +### The approval card + +`` from `@threadplane/chat` is a prebuilt composition. Give it the agent and it watches `interrupt()` for you: when a matching payload arrives it opens a native modal dialog, and when the interrupt resolves it closes again. `matchKind` compares the payload's `kind` field, so the card ignores any interrupt that is not a refund approval, and `showEdit` adds the third button. + + + +The card owns the chrome and the buttons; the `#body` template owns the content, and the payload arrives as that template's implicit value. + +### Resuming with the decision + +The card emits an action rather than resuming by itself, which leaves the resume payload entirely up to the component. Approve and Cancel are terminal, so the card closes and the handler submits the matching decision. Edit is not terminal: it flips a signal, the body template grows an amount field, and the Save button submits the edited value. + + + +`submit({ resume })` continues the paused run instead of starting a new one, and whatever you put in `resume` is exactly what `interrupt()` returns on the server. + + +`injectAgent()` must run inside an Angular injection context: a field initializer, as it is here, or a constructor body. + + +## The wire format + +The example never touches the protocol, but it is worth knowing what crosses it. An interrupt reaches the browser as an AG-UI `CUSTOM` event named `on_interrupt`, carrying the value the graph passed to `interrupt()`: + +```json +{ + "type": "CUSTOM", + "name": "on_interrupt", + "value": "{\"kind\":\"refund_approval\",\"amount\":47.5,\"customer_id\":\"cus_a8x2k\",\"reason\":\"Duplicate charge\"}" +} ``` -The `LangGraphAgent` wrapper handles streaming the `CUSTOM on_interrupt` event and reading `forwarded_props.command.resume` on resume. Refer to [`ag-ui-langgraph` on PyPI](https://pypi.org/project/ag-ui-langgraph/) for installation and configuration. +Two things to note. The `ag-ui-langgraph` package serializes the payload, so `value` arrives as a JSON string rather than an object; the adapter parses a string value before it stores the interrupt, so consumers read the parsed payload rather than a JSON string. And the pause is what the reducer records: it sets the `interrupt()` signal, ends the run as paused, and returns the agent to its idle status. + +Resuming travels the other way. For a payload shaped like this one — an application object with no runtime-specific identifiers — `submit({ resume })` sends `forwardedProps.command.resume`, the convention `ag-ui-langgraph` reads to resume the checkpoint. + + +In the graph node, `interrupt({...})` returns the resume value directly. The wrapper reads `forwarded_props.command.resume` and applies it before the graph continues. + + +Other runtimes signal a pause differently, and the adapter follows them: a `RUN_FINISHED` event carrying an interrupt outcome sets the same signal, and a resume for one of those goes out as the protocol's top-level `resume` array instead. The [event mapping reference](/docs/ag-ui/reference/event-mapping) covers the event surface in full. -## Cross-Adapter Parity +## Cross-adapter parity -The consumer Angular code is byte-identical except the `injectAgent` import: +The component code is the same; moving this component to a LangGraph deployment changes one import: ```diff -- import { injectAgent } from '@threadplane/langgraph'; -+ import { injectAgent } from '@threadplane/ag-ui'; +- import { injectAgent } from '@threadplane/ag-ui'; ++ import { injectAgent } from '@threadplane/langgraph'; ``` -``, the `interrupt()` signal, and `submit({ resume })` are part of the runtime-neutral `Agent` contract from `@threadplane/chat`. Switching adapters is a provider change, not a component rewrite. See the [LangGraph interrupts guide](/docs/langgraph/guides/interrupts) for the full HITL pattern including multi-step approvals, typed payloads, and timeout strategies. +The `interrupt()` signal and `submit({ resume })` are part of the runtime-neutral `Agent` contract, and `` is written against it, so switching adapters is a provider change rather than a component rewrite. The [LangGraph interrupts guide](/docs/langgraph/guides/interrupts) covers the parts of the pattern that live above the wire: typed payloads, multi-step approvals, and timeout strategies. + +## What's Next + + + + Every AG-UI event and the adapter state it updates. + + + Let the agent call functions that run in the browser. + + + Typed payloads, repeated approvals, and timeout strategies. + + + Script interrupt events deterministically with the fake agent. + + diff --git a/apps/website/content/docs/ag-ui/guides/json-render.mdx b/apps/website/content/docs/ag-ui/guides/json-render.mdx index 505199da2..6f188a2aa 100644 --- a/apps/website/content/docs/ag-ui/guides/json-render.mdx +++ b/apps/website/content/docs/ag-ui/guides/json-render.mdx @@ -4,6 +4,131 @@ description: Stream a json-render UI spec with AG-UI shared state and let @threa # Generative UI -An AG-UI agent can stream a declarative json-render specification together with shared state. `STATE_SNAPSHOT` and `STATE_DELTA` events keep that state current, and `@threadplane/render` resolves the spec's `$state` bindings against it, so the agent shapes the interface and its data rather than only its text. +An AG-UI agent can author the interface, not only the text. It returns a declarative json-render specification as the assistant message content, and it puts the numbers that specification binds to in graph state, which `ag-ui-langgraph` emits as `STATE_SNAPSHOT` and `STATE_DELTA` events. The `` composition mounts the specification through your `views` registry, syncs the agent state into a render store, and `@threadplane/render` resolves the `$state` bindings against it. -This page is the live example for generative UI over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The rendering engine is introduced in the [Render introduction](/docs/render/getting-started/introduction), and the AG-UI state events in the [event mapping reference](/docs/ag-ui/reference/event-mapping). +The running example is an airline operations dashboard, and this guide walks the four files that build it. + +## What the demo does + +The Run tab shows the prebuilt `` composition. Choose the welcome suggestion "Airline operations dashboard" and the agent authors a layout of KPI cards, a line chart, a bar chart, and a table, then calls the data tools that fill it. The cards render as skeletons first and take their values when the state arrives. + +The second suggestion, "Filter to cancelled flights", is the follow-up worth trying. The agent does not author a new layout for it. It calls one data tool again with new arguments, the new results replace that slice of state, and the dashboard already on screen re-renders in place. + +## How it is built + +Four files carry the feature: a LangGraph graph that authors the layout and returns the data as state, a FastAPI server that exposes it over AG-UI, an application config that registers the agent, and a component that supplies the view registry and the store. Open the Code tab to read them in full. + +### The state the dashboard binds to + +Every value the layout can bind to is a field on the graph state class. The docstring in the example states the reason: fields declared here are the ones `ag-ui-langgraph` includes in the snapshot it sends to the client. + + + +The `$state` pointers the agent writes into the specification, such as `/on_time/value`, address these top-level keys. + + +A pointer that names a field the state class does not declare resolves to nothing, and the component bound to it stays in its skeleton state forever. Add the field to `DashboardState` whenever you teach the agent a new binding. + + +### The tool that authors the layout + +The layout arrives as a tool call. `render_spec` takes the element dictionary and the id of the root element, and returns them serialized as JSON. + + + +The docstring is the contract the model reads, which is why it says to call the tool at most once per turn: layout changes go through this tool, and data refreshes go through the data tools instead. + +### Moving the layout into the assistant message + +A tool result is not message content, so a post-processing node moves it. `wrap_spec_into_ai` finds the most recent `render_spec` tool message and the assistant message whose tool call produced it. + + + +The node is idempotent: it returns early when the parent message already carries content, so looping back through the agent does not wrap the same payload twice. + +### Rewriting the assistant message in place + +The rest of the node replaces both messages, keeping their ids so LangGraph's `add_messages` reducer matches and replaces rather than appends. + + + +The tool message becomes a short placeholder and the assistant message takes the specification JSON. + + +On the client the content classifier in `@threadplane/chat` sees content that begins with `{`, classifies it as `json-render`, and mounts ``, which renders it through `` from `@threadplane/render`. + + +### Returning the tool data as state + +The data tools return their results as tool messages, which the client would otherwise never see as state. `emit_state` walks this turn's messages back to the most recent user message and returns the parsed tool results as top-level state fields. + + + +Returning a field from a node is all that is required: `ag-ui-langgraph` emits the updated state as a snapshot on the wire. + +### The graph and its checkpointer + +The wiring shows the loop. `agent` calls the tools and the `tools` node runs them; `wrap_spec_into_ai` post-processes the result and returns to the agent, and the turn ends through `emit_state`, a short conversational summary, and background title generation. + + + + +`ag-ui-langgraph` reads thread state through the checkpointer, so a graph served this way must compile one, and the example uses `MemorySaver` for development. That is the opposite of a graph served by `langgraph dev` or LangGraph Platform, where the platform supplies persistence and compiling your own saver is an error. See the [persistence guide](/docs/langgraph/guides/persistence) for that case. + + +### Serving the graph over AG-UI + +The server is the standard `ag-ui-langgraph` mount: wrap the compiled graph in a `LangGraphAgent` and attach it to a FastAPI application at a path. The state events that carry the dashboard data are emitted by the adapter because the graph returns those fields. + + + +Nothing in this file is specific to generative UI. + +### Registering the AG-UI agent + +`provideAgent()` from `@threadplane/ag-ui` needs the URL of that endpoint. The example passes a factory because it resolves the URL at runtime from the host that serves the demo; an application of your own passes `url` directly. + + + +`provideChat({})` registers the chat composition defaults alongside it. + +### The view registry and the shared store + +The component supplies the two things the specification needs: a `views` registry that maps each `type` in the specification to an Angular component, and an explicit store for the bindings to resolve against. + + + +The `[store]` input is what makes the dashboard live. + + +`` forwards only an explicit store to the generative-UI surface; without one, the surface falls back to a private store of its own, and backend state never reaches it. + + +## How a binding resolves + +The pieces meet in the browser in a fixed order. + +The adapter reducer in `@threadplane/ag-ui` handles `STATE_SNAPSHOT` by setting the agent's `state` signal to the snapshot, and `STATE_DELTA` by applying the JSON Patch operations to the current value. The `` composition watches that signal and writes each top-level key `k` into the render store under the JSON Pointer `/k`, skipping `messages`. `@json-render/core` then resolves a `{ "$state": "/on_time/value" }` prop by reading that pointer out of the store. + +So the state field `on_time` on the graph becomes the pointer `/on_time` in the store, and the binding `/on_time/value` reaches the `value` key inside it. Pointer syntax, including the `~0` and `~1` escapes, is covered in the [state store guide](/docs/render/guides/state-store). + + +A component registered in `views` is reusable with no changes for the tool-views pattern, where the frontend owns the layout and a tool call supplies the data. The difference is only where the layout comes from. See [Tool Views](/docs/ag-ui/guides/tool-views). + + +## What's Next + + + + Render a frontend component keyed by tool name, with no specification on the wire. + + + The json-render specification format: elements, props, children, and bindings. + + + JSON Pointer paths, reads and writes, and the signal-backed store implementation. + + + How each AG-UI event maps onto the signals the chat composition reads. + + diff --git a/apps/website/content/docs/ag-ui/guides/subagents.mdx b/apps/website/content/docs/ag-ui/guides/subagents.mdx index ab418dc64..fad6b96ea 100644 --- a/apps/website/content/docs/ag-ui/guides/subagents.mdx +++ b/apps/website/content/docs/ag-ui/guides/subagents.mdx @@ -4,6 +4,150 @@ description: Show an AG-UI agent's delegated child runs as attributed subagent c # Subagents -AG-UI carries subagent activity as first-class events. When an agent delegates, each child run appears as an attributed card with its own tool calls and messages instead of being folded into the parent transcript. +AG-UI carries subagent activity as first-class events. When an agent delegates, the child run arrives as a `SUBAGENT_STARTED` event plus message events stamped with a `subagentRunId`, and the adapter keeps that traffic out of the parent transcript: it becomes a card of its own, anchored to the tool call that spawned it. The running example is a trip planner whose orchestrator hands work to three specialists, and this guide walks the four files that make it work. -This page is the live example for subagents over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The card component is documented in [ChatSubagentCard](/docs/chat/components/chat-subagent-card). +## What the demo does + +The Run tab shows the prebuilt `` composition in front of an orchestrator agent served over AG-UI. There are no starter suggestions here, so type a trip request. Give it the trip details up front, for example "Plan a trip from LAX to JFK, one adult, economy, departing next Tuesday morning and returning Friday evening", because a bare origin-and-destination request usually earns a clarifying question about dates before it delegates anything. If the request is ambiguous, for example it does not mention airports, the orchestrator asks a clarifying question before delegating. + +Once it has both airports, the prompt tells it to call the `task` tool three times in a fixed order: research, then booking, then itinerary. Each dispatch appears inline in the transcript as a card carrying that specialist's own answer, streamed token by token inside the card rather than into the parent bubble. The card header shows the role name, the identifier of the tool call it belongs to, a status badge, and a message count. + +While a specialist works, its card is expanded and the badge reads `running`. When the child finishes the badge flips to `complete` and the card collapses, and clicking the header opens it again. After the third card, the orchestrator writes its own summary of the plan as an ordinary assistant message. + +## How it is built + +Four files carry the feature in the Code tab: a LangGraph graph with a single delegation tool, a FastAPI server that mounts the agent, an application config that registers it, and a component that mounts the chat. A fifth file next to the server, `streaming/subagent_emitting_agent.py`, holds the translation from the graph's private events into the protocol's subagent events. + +### Running a specialist as a child LLM + +A subagent here is a plain model call with its own system prompt. What makes it visible is the callback handler passed alongside it, which taps the child's tokens as they arrive and carries the parent tool call identifier with them. + + + +`SubagentStreamHandler` is an async LangChain callback handler: on the first token it dispatches a `message_start` custom event, and then one `message` event per token, each keyed by that tool call identifier. + +### The delegation tool + +The orchestrator binds exactly one tool. `InjectedToolCallId` hands the tool body the identifier of its own call, and that identifier becomes the identity of the whole delegation: the events the tool dispatches carry it, and so does every event the handler emits. The body announces `started` before the child runs, `finished` after it returns, and `error` if it raises. + + + +The docstring is the model-facing contract, which is why the three roles are described there rather than only in the system prompt. + +### The orchestrator loop + +The graph itself is the standard shape: a model node that may emit tool calls, a `ToolNode` that runs them, and an edge back to the model so the orchestrator can read one specialist's result before dispatching the next. + + + +The third node, `generate_title`, is unrelated to delegation; it summarizes the first user message into a thread title and swallows its own errors. + + +The bridge reads thread state through `graph.aget_state`, so a compiled graph without a checkpointer cannot be served at all. `MemorySaver` satisfies that but loses every thread when the process restarts; a deployment that must survive a restart needs a durable saver, such as the Postgres checkpointer, behind the same `checkpointer=` argument. + + +### Serving the graph over AG-UI + +The backend is a FastAPI application. `add_langgraph_fastapi_endpoint` from the `ag-ui-langgraph` package mounts an agent at a path that speaks the AG-UI event stream. The agent mounted here is a subclass of the package's `LangGraphAgent` rather than the class itself. + + + +The subclass wraps the bridge's `run()` generator, which is the async generator the endpoint consumes, and rewrites the stream as it passes through. + +### From custom events to subagent events + +The graph cannot reach the wire directly. Its `subagent_activity` events travel out as AG-UI `CUSTOM` events, and the subclass consumes each one and yields typed protocol events in its place. Every other event passes through untouched. This translation lives in `streaming/subagent_emitting_agent.py`, next to the server rather than in the Code tab. Writing `tid` for the `task` call identifier, the expansion is: + +| phase | emitted event | +| --- | --- | +| `started` | `SUBAGENT_STARTED` with `subagentRunId: -sub`, `name`, and `parentToolCallId: ` | +| `message_start` | `TEXT_MESSAGE_START` with `messageId: -sub-m`, `role: assistant`, and the `subagentRunId` | +| `message` | `TEXT_MESSAGE_CONTENT` with that `messageId`, the raw delta, and the `subagentRunId` | +| `finished` | `TEXT_MESSAGE_END` for the open child message, then `SUBAGENT_FINISHED` with a success outcome | +| `error` | `TEXT_MESSAGE_END` for the open child message, then `SUBAGENT_ERROR` carrying the message | + +One phase can therefore produce two events, which is why the seam is the `run()` generator rather than a one-event-in, one-event-out hook. Unknown phases are logged and dropped; a message phase with no text is dropped silently. + + +The AG-UI encoder serializes pydantic `ag_ui.core` event classes, so the subclass constructs `SubagentStartedEvent`, `TextMessageStartEvent` and friends. Yielding a raw dictionary from that generator breaks the stream. + + +### The agent provider + +`provideAgent()` registers the agent once for the whole application, and `provideChat({})` registers the configuration the `` composition reads, here left at its defaults. Nothing in either provider is subagent-specific. The example passes a factory because it resolves its endpoint at runtime from the host that serves the demo. Your own application does not need the factory. + + + +Pass the URL of your AG-UI endpoint directly: + +```typescript +provideAgent({ + url: 'https://your-backend.example.com/agent', +}); +``` + +### The component + +The component is the whole frontend of this example: one field holding the injected agent, passed to ``. The layout wrapper around it is presentation chrome for the demo, and your own application supplies its own. + + + +There is no subagent wiring in the component at all, because the composition reads the projection the adapter maintains. + +## How a card finds its tool call + +The adapter does the attribution before any component sees it. `SUBAGENT_STARTED` creates an entry keyed by `subagentRunId`, recording the event's `name` and storing `parentToolCallId` as the entry's tool call identifier. Any `TEXT_MESSAGE_START`, `TEXT_MESSAGE_CONTENT`, `TEXT_MESSAGE_END`, `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_END`, or `TOOL_CALL_RESULT` event that carries a `subagentRunId` is routed into that entry instead of the parent message list. An attributed event that arrives before its `SUBAGENT_STARTED` creates the entry rather than being dropped, and the later `SUBAGENT_STARTED` fills in the identity. + +Those entries are projected onto `agent.subagents()`, a `Map` of runtime-neutral `Subagent` wrappers. Each wrapper exposes `toolCallId`, `name`, and signals for `status()`, `messages()`, `toolCalls()`, and `state()`. `SUBAGENT_FINISHED` moves the status to `complete`, `SUBAGENT_ERROR` moves it to `error`, and a finished event whose outcome is `suspended` deliberately leaves the status at `running`, because a suspended run resumes under the same identifier. + +The anchoring happens one level lower, in ``, which the chat composition renders for each message. It indexes `agent.subagents()` by the wrapper's `toolCallId` rather than by the map key, because adapters key their maps differently. When a tool call in the message matches a wrapper, that call renders as a `` instead of an ordinary tool-call card, and it never merges into a group with the calls next to it. + + +The `` primitive renders the same cards as a standalone list, filtered to the subagents that have not reached `complete` or `error`. Use it for a live sidebar; use the inline card, as this example does, when the delegation belongs in the transcript. + + +## What crosses the wire + +One delegation round, captured against this backend with the deltas and the bridge's own step and RAW events elided, looks like this: + +```text +TOOL_CALL_START toolCallId: call_CN5…, toolCallName: task +TOOL_CALL_ARGS toolCallId: call_CN5…, delta: … +TOOL_CALL_END toolCallId: call_CN5… +SUBAGENT_STARTED subagentRunId: call_CN5…-sub, name: research, parentToolCallId: call_CN5… +TEXT_MESSAGE_START messageId: call_CN5…-sub-m1, subagentRunId: call_CN5…-sub +TEXT_MESSAGE_CONTENT messageId: call_CN5…-sub-m1, delta: "L", subagentRunId: call_CN5…-sub +TEXT_MESSAGE_END messageId: call_CN5…-sub-m1, subagentRunId: call_CN5…-sub +SUBAGENT_FINISHED subagentRunId: call_CN5…-sub, outcome: { type: success } +TOOL_CALL_RESULT toolCallId: call_CN5…, content: "LAX: Central Terminal Area …" +``` + +Two details are worth keeping. The tool call is fully announced — start, arguments, end — before the tool body runs, because `TOOL_CALL_END` marks the end of the argument stream rather than the end of execution; the card therefore always attaches to a call the adapter already knows about. And the delegation window nests between `TOOL_CALL_END` and `TOOL_CALL_RESULT`, so the specialist's text and the tool result that repeats it are separate things on the wire. + +## Cross-adapter parity + +The consumer Angular code is the same; moving this component to a LangGraph deployment changes one import. + +```diff +- import { injectAgent } from '@threadplane/ag-ui'; ++ import { injectAgent } from '@threadplane/langgraph'; +``` + +Both adapters project their own delegation traffic onto the same `subagents()` signal of runtime-neutral wrappers, and `` renders whichever it is given. What differs is how the delegation is identified: this example emits protocol subagent events explicitly from a bridge subclass, while the LangGraph adapter recognizes a dispatch tool by name and matches the child run's own namespace. + +## What's Next + + + + The card component, its inputs, and how to replace it with your own template. + + + Every AG-UI event and the adapter state it updates. + + + Render a tool call with your own component instead of the default card. + + + The same delegation pattern on LangGraph, where a `task` tool needs no client configuration. + + diff --git a/apps/website/content/docs/ag-ui/guides/tool-views.mdx b/apps/website/content/docs/ag-ui/guides/tool-views.mdx index ac7780a5b..63a71959a 100644 --- a/apps/website/content/docs/ag-ui/guides/tool-views.mdx +++ b/apps/website/content/docs/ag-ui/guides/tool-views.mdx @@ -4,6 +4,107 @@ description: Render an AG-UI tool call's plain data through a component the fron # Tool Views -A tool view renders an AG-UI tool call's result through a component the frontend owns, keyed by the tool's name. The agent returns plain data, such as a weather reading, and the Angular app decides how it looks. No UI specification crosses the wire. +A tool view renders a backend tool call through a component the frontend owns, keyed by the tool's name. The agent runs the tool and returns plain data, such as a weather reading, and the Angular application decides how that data looks. No UI specification crosses the wire. This guide walks the five files across six excerpts of the running example, from the tool to the card it fills. -This page is the live example for tool views over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The tool-call component surface is documented in [ChatToolCalls](/docs/chat/components/chat-tool-calls). +## What the demo does + +The Run tab shows the prebuilt `` composition in front of a LangGraph agent served over AG-UI. Ask what the weather is in San Francisco and the model calls a tool named `weather_card`. In place of the default tool-call card, the transcript mounts an Angular component: the location and a loading badge while the arguments stream, then a temperature, the conditions, the humidity, and the wind speed. The agent adds a one-sentence confirmation afterwards, because the system prompt tells it to keep the prose short and let the card carry the detail. + +## How it is built + +Five files carry the feature: a graph whose one tool returns plain JSON, the server that exposes it over AG-UI, an application config, the component that registers the view, and the card itself. Open the Code tab to read them in place. + +### The tool returns data, not markup + +The tool is an ordinary LangGraph tool. It takes a location and returns a dictionary, and the values are fixed so the recorded end-to-end fixtures stay stable. + + + +Every key in that dictionary is a name the card declares as an input, which is the only contract between the two sides. + +### The graph runs the tool itself + +The agent node calls a model bound to the tool, and `route` sends a turn that produced a tool call to a `ToolNode` and everything else to the end. The edge from `tools` back to `agent` closes the loop, so the tool executes on the server and its result reaches the browser inside the same run. + + + +The graph compiles with `MemorySaver()` because `ag-ui-langgraph` reads the thread's graph state through a checkpointer, so a graph served this way needs one; outside development it should be durable. + + +Here the backend owns the implementation and the browser only draws the result. A [client tool](/docs/ag-ui/guides/client-tools) inverts that: the graph ends its turn on the tool call, and the browser produces the result and submits it back to continue the run. + + +### Serving the graph over AG-UI + +The server is a FastAPI application. `add_langgraph_fastapi_endpoint` mounts the compiled graph at `/agent` as an AG-UI event stream, and `/ok` is a plain health check. + + + +### Providing the agent + +`provideAgent()` from `@threadplane/ag-ui` registers the agent at the application root, and `provideChat({})` registers the chat defaults. The example resolves its URL at runtime because the host that serves the demo decides which runtime is attached; your own application passes a `url` directly. + + + +### Registering the view under the tool name + +`views()` builds a frozen map from name to component, and the key is the tool name the agent calls — one identifier, not two. Passing that map to `` is the entire wiring, and the component itself is a plain standalone Angular component with no base class to extend. + + + + +`views` is defined in `@threadplane/render` and re-exported from `@threadplane/chat`. The same input also supplies the registry `` uses for render specs the backend sends, so a component registered for a tool call is reusable for generative UI. + + +### The card the tool call fills + +The card declares one signal input per field it renders, plus a `status` of `'running'` or `'complete'`. Every input is optional, because the arguments arrive while the call is still streaming and the result fields arrive later. A `pending` computed decides between the loading badge and the reading. + + + + +The adapter marks a call `complete` on `TOOL_CALL_END`, which is the end of the argument stream, and the result lands afterwards on `TOOL_CALL_RESULT`. That is why `pending` also checks that `temperatureF()` is defined. A card that trusted `status` alone would flash an empty reading between the two events. + + +## How a tool call becomes a component + + + +`TOOL_CALL_START` appends a call with the tool's name, empty arguments, and a status of `running`. Each `TOOL_CALL_ARGS` delta is appended to a per-call buffer and the whole buffer is parsed, so a half-arrived JSON fragment leaves the last good arguments in place. `TOOL_CALL_END` marks the call complete and `TOOL_CALL_RESULT` parses the serialized content onto it. + + +`ag-ui-langgraph` emits a `parentMessageId` for every tool call, so the adapter links the call to that message, creating the message slot when the turn produced no text at all. Each bubble then renders only its own calls. + + +`` keeps the calls whose name is a key in the registry and wraps each one into a synthetic render spec holding a single element of that type. Its props are the live arguments merged with the parsed result, plus `status` and a `clientTool` lifecycle object. + + +The registry is converted with `toRenderRegistry`, and `RenderElement` filters the props down to the inputs the component actually declares, so the extra keys are harmless. + + +Every key in the registry is added to the tool names excluded from ``, so a call that has a view does not also render as a generic card. + + + +## Views alongside client tools + +A `view` declared through the `tools()` registry is the mirror image of this page's pattern. It has no server implementation: the model fills its props, and the client-tools coordinator acknowledges the call on the component's behalf with `{ shown: true }` so the run can continue. A tool view renders a call the server really executed, and the registry only decides how the returned data is drawn. + +The two coexist. `` merges the coordinator's view and ask components into the `views` map you pass, so one transcript can hold both kinds of card without any extra wiring. + +## What's Next + + + + Declare a tool in the browser and let the model call it. + + + How AG-UI protocol events become the Agent contract the chat UI reads. + + + The default tool-call surface a registered view replaces. + + + Render a spec the backend sends through the same component registry. + + diff --git a/apps/website/content/docs/ag-ui/reference/event-mapping.mdx b/apps/website/content/docs/ag-ui/reference/event-mapping.mdx index 703cd9a7a..1cfe9be31 100644 --- a/apps/website/content/docs/ag-ui/reference/event-mapping.mdx +++ b/apps/website/content/docs/ag-ui/reference/event-mapping.mdx @@ -1,98 +1,110 @@ +--- +description: How the adapter turns AG-UI protocol events into Angular Signals, walked through the streaming example and then listed event by event. +--- + # Event Mapping -`@threadplane/ag-ui` reduces AG-UI protocol events into the `Agent` contract from `@threadplane/chat`. +`@threadplane/ag-ui` reduces AG-UI protocol events into the runtime-neutral `Agent` contract from `@threadplane/chat`. Every event that reaches the browser lands on a Signal your template already reads, which is why the same `` composition renders a run from any backend that speaks the protocol. This page is the compatibility map: first the streaming example as the worked case, then every event the adapter handles and the state it changes. -This page is the compatibility map. If your backend emits these events with the expected fields, the chat UI can render the run without knowing which runtime produced it. +## What the demo does -## Summary +The Run tab shows the prebuilt `` composition in front of a one-node LangGraph graph served over AG-UI. The example configures nothing beyond the connection, so the demo opens on the composition's welcome screen — the heading "How can I help?" above a single input, with no suggestions projected into it. -| AG-UI event | Agent field | Behavior | -| --- | --- | --- | -| `RUN_STARTED` | `status`, `isLoading`, `error`, `interrupt` | Sets `status` to `running`, `isLoading` to `true`, clears `error`, and clears any pending `interrupt`. | -| `RUN_FINISHED` | `status`, `isLoading` | Sets `status` to `idle` and `isLoading` to `false`. | -| `RUN_ERROR` | `status`, `isLoading`, `error` | Sets `status` to `error`, stops loading, and stores the event message when present. | -| `TEXT_MESSAGE_START` | `messages` | Creates or reuses an assistant message slot. | -| `TEXT_MESSAGE_CONTENT` | `messages` | Appends `delta` to the message content. | -| `TEXT_MESSAGE_END` | `messages` | No-op today. Content is already accumulated from deltas. | -| `REASONING_MESSAGE_START` | `messages` | Creates or reuses an assistant message slot with `reasoning`. | -| `REASONING_MESSAGE_CONTENT` | `messages` | Appends `delta` to `message.reasoning`. | -| `REASONING_MESSAGE_CHUNK` | `messages` | Treated the same as `REASONING_MESSAGE_CONTENT`. | -| `REASONING_MESSAGE_END` | `messages` | Adds `reasoningDurationMs` when timing is available. | -| `TOOL_CALL_START` | `toolCalls`, `messages` | Adds a running tool call; also links the call to its parent assistant message (via `parentMessageId`), creating a message slot if needed. | -| `TOOL_CALL_ARGS` | `toolCalls` | Accumulates partial JSON fragments, keeps the last-good parsed args, and finalizes parsing on `TOOL_CALL_END`. | -| `TOOL_CALL_RESULT` | `toolCalls` | Stores the tool result on the matching tool call. | -| `TOOL_CALL_END` | `toolCalls` | Marks the matching tool call complete. | -| `STATE_SNAPSHOT` | `state`, `messages` | Replaces state and merges citations from `state.citations`. | -| `STATE_DELTA` | `state`, `messages` | Applies JSON Patch to state and merges citations from `state.citations`. | -| `MESSAGES_SNAPSHOT` | `messages`, `toolCalls` | Replaces the message list and merges snapshot-embedded tool calls into `toolCalls` (by id). | -| `CUSTOM` (name: `on_interrupt`) | `interrupt` | Sets the `interrupt` signal to `{ id, value, resumable: true }`. The value is JSON-parsed when it arrives as a string. | -| `CUSTOM` (other names) | `events$` | Emits a runtime-neutral custom event. | -| unknown event | none | Ignored. Future protocol events do not crash the adapter. | - -## Run lifecycle - -The adapter exposes lifecycle through `status`, `isLoading`, and `error`. +Ask anything and the answer arrives a fragment at a time rather than in one block. The end-to-end test for this example asks "Tell me one quick fact about Angular signals in two sentences." Any prompt does the same work: the graph has one node, the node calls the model once, and the tokens reach the transcript while the call is still running. -```ts -agent.status(); // 'idle' | 'running' | 'error' -agent.isLoading(); // boolean -agent.error(); // unknown -``` +That is the whole feature. It is also the smallest complete AG-UI run, which makes it the right place to read the event sequence off the wire. -`RUN_STARTED` clears a previous error. `RUN_ERROR` and `onRunFailed` both put the agent into the `error` state. +## How it is built -`onRunFailed` comes from the AG-UI subscriber API rather than a protocol event. The adapter treats it like a run failure: `status = 'error'`, `isLoading = false`, and `error` is the thrown value. +Four files carry the demo: a graph with one node, a FastAPI server that fronts it with the AG-UI protocol, an application config that registers the agent, and a component that hands the agent to ``. Open the Code tab to read them in place. -## Messages +### The graph that streams tokens -Text messages are accumulated by `messageId`. +The graph uses LangGraph's `MessagesState`, so its state is the message list and nothing else. One node reads the system prompt from disk, prepends it to the conversation, and awaits a single model call. The model client is constructed with `streaming=True`, which is what makes the call emit token chunks the adapter can forward while the node is still awaiting its result. -```ts -{ type: 'TEXT_MESSAGE_START', messageId: 'm1', role: 'assistant' } -{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: 'Hello' } -{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: ' there' } -{ type: 'TEXT_MESSAGE_END', messageId: 'm1' } -``` + -The resulting message is: + +The graph compiles with `MemorySaver()`. The `ag-ui-langgraph` wrapper calls `graph.aget_state(config)` after the stream drains to build its closing snapshots, and a graph compiled without a checkpointer cannot answer that call. An in-memory saver is a development choice: a deployment that must survive a restart needs a durable one behind the same `checkpointer=` argument. + -```ts -{ id: 'm1', role: 'assistant', content: 'Hello there' } -``` +### Serving the graph over AG-UI -`TEXT_MESSAGE_END` does not finalize anything separately. The content already lives in the signal. +The backend is a FastAPI application. `LangGraphAgent` wraps the compiled graph, `add_langgraph_fastapi_endpoint` mounts it at `/agent` as an AG-UI event stream, and `/ok` is a plain health check. -`MESSAGES_SNAPSHOT` replaces the full message array. Use it when the backend is authoritative for the whole conversation. + -## Reasoning +That wrapper is the piece that translates LangGraph's own stream into the protocol events listed below. -Reasoning events write to the same assistant message when they share the same `messageId` as the final text response. +### Providing the agent -```ts -{ type: 'REASONING_MESSAGE_START', messageId: 'm1', role: 'assistant' } -{ type: 'REASONING_MESSAGE_CONTENT', messageId: 'm1', delta: 'Checking policy.' } -{ type: 'REASONING_MESSAGE_END', messageId: 'm1' } -{ type: 'TEXT_MESSAGE_START', messageId: 'm1', role: 'assistant' } -{ type: 'TEXT_MESSAGE_CONTENT', messageId: 'm1', delta: 'Approved.' } -``` +`provideAgent()` from `@threadplane/ag-ui` registers the agent once for the whole application, and `provideChat({})` registers the chat configuration, here left at its defaults. Your own application passes `{ url: 'https://your-backend.example.com/agent' }` directly; this example passes a factory because it resolves its endpoint at runtime from the host that serves the demo. -The message keeps both fields: + -```ts -{ - id: 'm1', - role: 'assistant', - reasoning: 'Checking policy.', - reasoningDurationMs: 12, - content: 'Approved.', -} -``` +### The component that reads the Signals -The duration is measured in the browser from start to end event. It is useful for display, not billing or tracing. +The component is three lines of behavior. `injectAgent()` returns the agent the provider configured, and `` takes it as an input. Message rendering, the input, the typing indicator, and error display all live inside the composition, which reads the same Signals this page maps. + + + + +`injectAgent()` must run inside an Angular injection context: a field initializer, as it is here, or a constructor body. + + +## The events one turn produces + +One prompt against this graph produces the following sequence. The order is the wrapper's; the effects are the reducer's. + +1. **`RUN_STARTED`.** `status()` becomes `running`, `isLoading()` becomes `true`, and `error()`, `interrupt()`, `customEvents()`, and the subagent map are cleared for the new run. The user message is already in the transcript at this point, because `submit()` appends it optimistically before the run opens. +2. **`STEP_STARTED`** naming the node, `generate`. The reducer has no case for step events, so this one changes nothing. It is on the wire for consumers that want node boundaries. +3. **`TEXT_MESSAGE_START`** on the first non-empty token chunk. The reducer creates the assistant message slot and marks it as streaming. This is the moment the empty bubble appears under your prompt. +4. **`TEXT_MESSAGE_CONTENT`**, once per chunk, each carrying a `delta`. The reducer appends the delta to that message's content, and the composition renders the growing text. This is the streaming the demo exists to show. +5. **`TEXT_MESSAGE_END`** when the model call ends. The reducer treats it as a no-op: the content is already accumulated in the Signal. +6. **`STATE_SNAPSHOT`** when the node exits, carrying the graph state. The reducer replaces `state()` with it and runs the citations bridge over the transcript. This graph publishes no `state.citations`, so nothing is attached. +7. **`STEP_FINISHED`**, then a closing **`STATE_SNAPSHOT`** and **`MESSAGES_SNAPSHOT`** read back from the checkpoint. The snapshot message carries the final message id rather than the streaming chunk id used in step 3, so the reducer maps the in-flight assistant message onto its snapshot counterpart instead of leaving two bubbles, and re-applies citations from `state()` against the new ids. +8. **`RUN_FINISHED`.** The run is marked complete, `status()` returns to `idle`, and `isLoading()` becomes `false`. + +`RAW` events are interleaved through the stream as well, one per upstream LangGraph event. The adapter ignores them, as it ignores any event type it does not know. + +## Event reference + +The adapter exposes `messages`, `status`, `isLoading`, `error`, `toolCalls`, `state`, `interrupt`, `subagents`, and `customEvents` as Signals, `events$` as an Observable, and `clientTools` as a capability, alongside the `submit`, `retry`, `stop`, and `regenerate` methods. The tables below name the surface each event writes to. + +### Run lifecycle + +| AG-UI event | Surface | Behavior | +| --- | --- | --- | +| `RUN_STARTED` | `status`, `isLoading`, `error`, `interrupt`, `customEvents`, `subagents` | Sets `status` to `running` and `isLoading` to `true`; clears the error, the pending interrupt, the accumulated custom events, the subagent map, and any unfinished tool-argument buffer. Ignored when the event's run id does not match the run already bound to this delivery. | +| `RUN_FINISHED` (no outcome, or a success outcome) | `status`, `isLoading`, `messages` | Completes the run, sets `status` to `idle` and `isLoading` to `false`, and settles the run's messages. Ignored on the same run-id gate as `RUN_STARTED`. | +| `RUN_FINISHED` (outcome `{ type: 'interrupt' }`) | `interrupt`, `status`, `isLoading` | Sets `interrupt` to `{ id, value: { interrupts, runId }, resumable: true }`, ends the run as paused, and returns the agent to `idle`. A `CUSTOM` `on_interrupt` that already paused this run keeps its own value. | +| `RUN_ERROR` | `status`, `isLoading`, `error` | Ends the run as failed, sets `status` to `error`, stops loading, and stores the event's `message` as an error (the raw event when no message is present). Ignored on the same run-id gate as `RUN_STARTED`. | +| `STEP_STARTED`, `STEP_FINISHED` | none | Not reduced. Node boundaries pass through untouched. | + +### Messages + +| AG-UI event | Surface | Behavior | +| --- | --- | --- | +| `TEXT_MESSAGE_START` | `messages` | Creates or reuses the assistant message slot for `messageId` and marks it streaming for this run. | +| `TEXT_MESSAGE_CONTENT` | `messages` | Appends `delta` to that message's content. | +| `TEXT_MESSAGE_END` | none | A no-op. The content is already accumulated from the deltas. | +| `REASONING_MESSAGE_START` | `messages` | Creates or reuses the slot with an empty `reasoning` field and records a start time. | +| `REASONING_MESSAGE_CONTENT`, `REASONING_MESSAGE_CHUNK` | `messages` | Appends `delta` to `message.reasoning`. Both event names are handled identically. | +| `REASONING_MESSAGE_END` | `messages` | Sets `reasoningDurationMs` from the browser-measured start and end times. | +| `MESSAGES_SNAPSHOT` | `messages`, `toolCalls` | Replaces the message list with the snapshot, bridges each assistant message's `toolCalls` onto `toolCallIds`, merges snapshot-only tool calls into `toolCalls` by id, keeps the run's in-flight assistant message aligned across the id change, and re-applies citations from the current `state`. | -## Tool calls +Reasoning and text write to the same message when they share a `messageId`, so one assistant bubble can carry both `reasoning` and `content`. The duration is measured in the browser between the start and end events: it is useful for display, not for billing or tracing. -Tool calls are stored independently from messages in `agent.toolCalls()`. +### Tool calls + +| AG-UI event | Surface | Behavior | +| --- | --- | --- | +| `TOOL_CALL_START` | `toolCalls`, `messages` | Adds a tool call with status `running`; when `parentMessageId` is present, links the call to that assistant message, creating the message slot if the turn produced no text. | +| `TOOL_CALL_ARGS` | `toolCalls` | Appends `delta` to a per-call buffer, parses the buffer whenever it becomes valid JSON, and keeps the last good args while more fragments arrive. | +| `TOOL_CALL_END` | `toolCalls` | Parses the accumulated buffer one final time, discards the buffer, and marks the call `complete`. | +| `TOOL_CALL_RESULT` | `toolCalls` | Stores the result on the matching call, parsing `content` as JSON when it arrives as a string. | + +Argument deltas are fragments of one JSON document, not standalone JSON, which is why the reducer buffers rather than parses each delta. If the buffer never parses, `args` stays at the last good value, or `{}` when there was none. ```ts { type: 'TOOL_CALL_START', toolCallId: 'search-1', toolCallName: 'search' } @@ -101,7 +113,7 @@ Tool calls are stored independently from messages in `agent.toolCalls()`. { type: 'TOOL_CALL_END', toolCallId: 'search-1' } ``` -The resulting tool call is: +The resulting entry in `agent.toolCalls()`: ```ts { @@ -113,19 +125,14 @@ The resulting tool call is: } ``` -`TOOL_CALL_ARGS` accepts partial JSON fragments. The reducer appends each `delta` to an internal buffer for the tool call, parses whenever the buffer becomes valid JSON, and keeps the last-good args while more fragments arrive. On `TOOL_CALL_END`, it performs one final parse before marking the call complete. - -If the accumulated buffer never parses, args stay at the last-good value or `{}`. +### State -## State - -`STATE_SNAPSHOT` replaces the full state object: - -```ts -{ type: 'STATE_SNAPSHOT', snapshot: { topic: 'billing' } } -``` +| AG-UI event | Surface | Behavior | +| --- | --- | --- | +| `STATE_SNAPSHOT` | `state`, `messages` | Replaces `state` with `snapshot` and runs the citations bridge over the transcript. | +| `STATE_DELTA` | `state`, `messages` | Applies the JSON Patch operations in `delta` to a clone of the current state, then runs the same bridge. | -`STATE_DELTA` applies JSON Patch operations to the current state: +`STATE_DELTA` carries standard JSON Patch operations: ```ts { @@ -137,64 +144,61 @@ If the accumulated buffer never parses, args stay at the last-good value or `{}` } ``` -After either state event, the adapter also runs the citations bridge. If `state.citations` contains entries keyed by message id, those citations are copied onto the matching messages. - -See [Citations](/docs/ag-ui/guides/citations) for the expected shape. - -## Custom events +The citations bridge reads `state.citations`, an object keyed by message id, and copies the entries onto the matching messages. See [Citations](/docs/ag-ui/guides/citations) for the accepted shapes. -`CUSTOM` events are exposed through `events$`. +### Custom events and interrupts -When the custom event name is `state_update` and the value is an object, the adapter emits: - -```ts -{ type: 'state_update', data: value } -``` - -For every other custom event name, it emits: - -```ts -{ type: 'custom', name, data: value } -``` - -Use `state` for durable UI state. Use `events$` for transient events, observability hooks, or UI side effects that should not be stored as conversation state. - -Every non-`on_interrupt` `CUSTOM` event is fanned out to **two** surfaces, not one. Alongside the `events$` emission above, the reducer also appends `{ name, data }` to the AG-UI-specific `customEvents()` signal documented on [`injectAgent()`](/docs/ag-ui/api/inject-agent#ag-ui-specific-surface) and [`toAgent()`](/docs/ag-ui/api/to-agent). Reach for the signal when you want an accumulated per-run snapshot for reactive rendering (for example, `a2ui-partial` generative UI); reach for `events$` when you want a transient stream for side-effects or telemetry. - -## Submit and stop - -`agent.submit({ message })` performs three steps: - -1. Builds a local user message. -2. Appends it to `messages` and calls `source.addMessage()`. -3. Calls `source.runAgent()`. - -If `message` is omitted, no user message is appended, but `runAgent()` still runs. +| AG-UI event | Surface | Behavior | +| --- | --- | --- | +| `CUSTOM` named `on_interrupt` | `interrupt`, `status`, `isLoading` | Sets `interrupt` to `{ id, value, resumable: true }`, ends the run as paused, and returns the agent to `idle`. A string `value` is JSON-parsed first. | +| `CUSTOM` named `state_update` with an object value | `customEvents`, `events$` | Appends `{ name, data }` to `customEvents` and emits `{ type: 'state_update', data }` on `events$`. | +| `CUSTOM` (every other name) | `customEvents`, `events$` | Appends `{ name, data }` to `customEvents` and emits `{ type: 'custom', name, data }` on `events$`. | -`agent.stop()` calls `source.abortRun()`. Cancellation depends on the AG-UI source implementation. +Every non-interrupt custom event is fanned out to two surfaces, not one. Reach for the `customEvents()` Signal when you want an accumulated per-run snapshot for reactive rendering, and for `events$` when you want a transient stream for side effects or telemetry. The Signal is documented on [`injectAgent()`](/docs/ag-ui/api/inject-agent#ag-ui-specific-surface) and [`toAgent()`](/docs/ag-ui/api/to-agent). -## Activity and subagents +### Subagents and activity -`ACTIVITY_SNAPSHOT` and `ACTIVITY_DELTA` events project into the AG-UI-specific `subagents()` signal. Use that signal for progress cards and nested task views backed by AG-UI activity streams. +| AG-UI event | Surface | Behavior | +| --- | --- | --- | +| `SUBAGENT_STARTED` | `subagents` | Creates the entry for `subagentRunId` with status `running`, or fills in the identity of an entry a child content event created first. `toolCallId` becomes `parentToolCallId`, else the id an earlier activity entry recorded, else `subagentRunId`. | +| `SUBAGENT_FINISHED` | `subagents` | Marks the entry `complete` and records `result`. An outcome of `{ type: 'suspended' }` keeps it `running`. | +| `SUBAGENT_ERROR` | `subagents` | Marks the entry `error` and records `message`. | +| `ACTIVITY_SNAPSHOT` | `subagents` | Creates or merges an activity entry; `replace: true` overwrites its content rather than merging. Entries with `activityType: 'subagent'` project into `subagents()`. | +| `ACTIVITY_DELTA` | `subagents` | Applies a JSON Patch to an existing entry's content. An unknown id or a malformed patch is dropped rather than thrown. | -### `SUBAGENT_*` events +Text and tool events may also carry a `subagentRunId`. An attributed content event feeds that child's card and never the parent transcript, and one that arrives before `SUBAGENT_STARTED` still gets a card, which `SUBAGENT_STARTED` then fills in with identity. A suspended outcome keeps the card running: the run resumes under the same id, and the pause itself surfaces through `interrupt()`, not through the card. -The adapter also consumes the protocol's first-class subagent lifecycle events: +### Everything else -| Event | Required fields | Optional fields | Effect | -|---|---|---|---| -| `SUBAGENT_STARTED` | `subagentRunId`, `name` | `description`, `parentSubagentRunId`, `parentToolCallId`, `parentMessageId` | Creates (or fills in) the subagent's entry with status `running`. | -| `SUBAGENT_FINISHED` | `subagentRunId` | `result`, `outcome` (`{ type: 'success' }` or `{ type: 'suspended', interruptIds? }`) | Marks the entry `complete`; a `suspended` outcome keeps it `running`. | -| `SUBAGENT_ERROR` | `subagentRunId`, `message` | `code` | Marks the entry `error` and records the message. | +| AG-UI event | Surface | Behavior | +| --- | --- | --- | +| `RAW` and any unrecognized type | none | Ignored. A newer protocol version does not crash the adapter. | -Alongside the lifecycle events, ordinary content events (`TEXT_MESSAGE_START/CONTENT/END` and `TOOL_CALL_START/ARGS/END/RESULT`) may carry a `subagentRunId` attribution field. The routing rule: an attributed text or tool event feeds the child's card, never the parent transcript. An attributed content event that arrives before `SUBAGENT_STARTED` still gets a card, which `SUBAGENT_STARTED` then fills in with identity. +## Submit, stop, and failures -Entries in the `subagents()` map are keyed by `subagentRunId`. The `Subagent.toolCallId` field carries `parentToolCallId ?? subagentRunId` and is what anchors the card to its delegation tool call in ``. +`agent.submit({ message })` builds a local user message, appends it to `messages` and to the source agent's own list, and starts the run. When `message` is omitted no user message is appended and the run still starts. `submit({ resume })` is a separate path: it clears the pending interrupt and replays the run with the resume payload, shaped by how that interrupt arrived. `agent.retry()` re-runs the last input without appending the message again, and `agent.regenerate(index)` truncates the transcript back to the preceding user message before re-running. -A `suspended` outcome keeps the card in the `running` state: the subagent run resumes under the same `subagentRunId`, and the interrupt itself surfaces through the runtime-neutral `interrupt()` signal, not through the card. +`agent.stop()` ends the current run locally as aborted, clears the error, and calls `source.abortRun()`. Whether the backend stops producing depends on the AG-UI source implementation. -The legacy convention — `ACTIVITY_SNAPSHOT`/`ACTIVITY_DELTA` with an `activityType` of `subagent` — remains supported and projects into the same signal. +`onRunFailed` comes from the AG-UI subscriber API rather than from a protocol event. The adapter treats it as a run failure: `status` becomes `error`, `isLoading` becomes `false`, and `error` holds the thrown value. An abort that the component requested is not reported as an error. ## Unsupported protocol areas -The adapter supports `CUSTOM` `on_interrupt` events for the runtime-neutral `interrupt()` signal, and it supports subagent progress through both the `SUBAGENT_*` events and the ACTIVITY-backed convention. It does not implement history or time-travel. Unknown protocol events are ignored rather than treated as errors. +The adapter carries interrupts, subagents, activity, state, and client tools. It does not implement history or time travel. Unknown protocol events are ignored rather than treated as errors, so a backend ahead of this adapter degrades to the events it does know. + +## What's Next + + + + Pause a run mid-flight and resume it with a human decision. + + + Let the agent call functions that run in the browser. + + + Read the custom-event surface from your own components. + + + The full Signal surface the adapter exposes. + + diff --git a/apps/website/e2e/docs.spec.ts b/apps/website/e2e/docs.spec.ts index 84d968a0f..a4483c355 100644 --- a/apps/website/e2e/docs.spec.ts +++ b/apps/website/e2e/docs.spec.ts @@ -199,7 +199,7 @@ test.describe('Docs slug page', () => { // The deep link must land on a real section anchor, not just a // well-formed but dangling fragment that scrolls nowhere. - await expect(page).toHaveURL(/\/docs\/langgraph\/.*#.+/); + await expect(page).toHaveURL(/\/docs\/[a-z0-9-]+\/.*#.+/); const url = new URL(page.url()); expect(url.hash.length).toBeGreaterThan(1); await expect(page.locator(url.hash)).toBeVisible(); diff --git a/apps/website/src/lib/docs-example-code.spec.ts b/apps/website/src/lib/docs-example-code.spec.ts index e5b6e0c5c..0e57f93a0 100644 --- a/apps/website/src/lib/docs-example-code.spec.ts +++ b/apps/website/src/lib/docs-example-code.spec.ts @@ -22,12 +22,6 @@ import { */ const PENDING_PAGES = new Set([ '/docs/a2ui/getting-started/introduction', - '/docs/ag-ui/guides/client-tools', - '/docs/ag-ui/guides/interrupts', - '/docs/ag-ui/guides/json-render', - '/docs/ag-ui/guides/subagents', - '/docs/ag-ui/guides/tool-views', - '/docs/ag-ui/reference/event-mapping', '/docs/chat/a2ui/overview', '/docs/chat/components/chat-debug', '/docs/chat/components/chat-input', diff --git a/apps/website/src/lib/example-code.spec.ts b/apps/website/src/lib/example-code.spec.ts index 80da0b4bc..cf2ad488d 100644 --- a/apps/website/src/lib/example-code.spec.ts +++ b/apps/website/src/lib/example-code.spec.ts @@ -110,7 +110,7 @@ describe('sliceRegion', () => { ).toThrow(/unterminated/); }); - it('keeps nested regions intact and ends at the matching endregion', () => { + it('drops inner marker lines and ends at the matching endregion', () => { const source = [ '// #region outer', 'const a = 1;', @@ -121,13 +121,7 @@ describe('sliceRegion', () => { '// #endregion', ].join('\n'); expect(sliceRegion(source, 'outer', 'f.ts')).toBe( - [ - 'const a = 1;', - '// #region inner', - 'const b = 2;', - '// #endregion', - 'const c = 3;', - ].join('\n') + ['const a = 1;', 'const b = 2;', 'const c = 3;'].join('\n') ); expect(sliceRegion(source, 'inner', 'f.ts')).toBe('const b = 2;'); }); @@ -143,13 +137,7 @@ describe('sliceRegion', () => { '// #endregion', ].join('\n'); expect(sliceRegion(source, 'outer', 'f.ts')).toBe( - [ - 'const a = 1;', - '// #region', - 'const b = 2;', - '// #endregion', - 'const c = 3;', - ].join('\n') + ['const a = 1;', 'const b = 2;', 'const c = 3;'].join('\n') ); }); diff --git a/apps/website/src/lib/example-code.ts b/apps/website/src/lib/example-code.ts index 9d4afdce3..1e3a93b38 100644 --- a/apps/website/src/lib/example-code.ts +++ b/apps/website/src/lib/example-code.ts @@ -87,7 +87,11 @@ export function sliceRegion( `${filePath}: "#region ${region}" is unterminated` ); } - const body = lines.slice(start + 1, end); + // Inner markers are the file's own comments, but they are noise on a docs + // page, so every marker line inside the slice is dropped as well. + const body = lines + .slice(start + 1, end) + .filter((line) => !REGION_ANY_START.test(line) && !REGION_END.test(line)); const nonEmpty = body.filter((line) => line.trim().length > 0); const indent = nonEmpty.length === 0 diff --git a/cockpit/ag-ui/client-tools/angular/src/app/client-tools.component.ts b/cockpit/ag-ui/client-tools/angular/src/app/client-tools.component.ts index 78f23292d..5da176634 100644 --- a/cockpit/ag-ui/client-tools/angular/src/app/client-tools.component.ts +++ b/cockpit/ag-ui/client-tools/angular/src/app/client-tools.component.ts @@ -20,6 +20,7 @@ import { weatherCardSchema, confirmBookingSchema } from './schemas'; * that every field the schema produces is a declared `input()` on the paired * component. Mismatches become errors here, not silent runtime failures. */ +// #region tool-registry const clientTools = tools({ get_weather: action( 'Look up the current weather for a location.', @@ -51,7 +52,9 @@ const clientTools = tools({ ConfirmBookingComponent, ), }); +// #endregion +// #region abortable-delay function waitForAbortableDelay(ms: number, signal: AbortSignal): Promise { return new Promise((resolve, reject) => { if (signal.aborted) { @@ -69,7 +72,9 @@ function waitForAbortableDelay(ms: number, signal: AbortSignal): Promise { ); }); } +// #endregion +// #region chat-wiring @Component({ selector: 'app-client-tools', standalone: true, @@ -84,3 +89,4 @@ export class ClientToolsComponent { protected readonly agent = injectAgent(); protected readonly clientTools = clientTools; } +// #endregion diff --git a/cockpit/ag-ui/client-tools/python/docs/guide.md b/cockpit/ag-ui/client-tools/python/docs/guide.md deleted file mode 100644 index 5eb43f1fe..000000000 --- a/cockpit/ag-ui/client-tools/python/docs/guide.md +++ /dev/null @@ -1,261 +0,0 @@ -# Browser-Executed Client Tools with AG-UI - - -Declare tools in the Angular app — with a description, a Zod schema, and a -handler — and have the **model** call them while the **browser** executes them. -Three behaviors are supported: `action` (async function whose return becomes the -tool result), `view` (Angular component the model fills with props, rendered -inline and auto-acknowledged), and `ask` (interactive HITL component whose -emitted value resumes the run). The catalog is shipped to the model via the -AG-UI adapter's native `RunAgentInput.tools` field; the LangGraph backend binds -the client stubs with no server implementation and ends the turn so the browser -runs the tool and re-submits with a `ToolMessage`, which the model then -summarizes. - - - -Declare client tools in the Angular app using `tools()`, `action()`, `view()`, -and `ask()` from `@threadplane/chat`, with schemas authored in `zod/v4`. Pass -the registry to ``. On the backend, declare a `tools` -channel in your LangGraph `State`, call `bind_client_tools(llm, [], state)` from -`threadplane.middleware.langgraph`, and route unconditionally to `END` — there are no -server-side tool implementations. - - - - - -Build a registry with `tools()` from `@threadplane/chat`. Each entry is one of -three behaviors — `action`, `view`, or `ask` — all keyed by the tool name the -model will call. Schemas are authored with `zod/v4`; the AG-UI adapter derives -the JSON Schema the model sees using `zod/v4`'s `toJSONSchema`. - -```typescript -// client-tools.component.ts -import { ChatComponent, tools, action, view, ask } from '@threadplane/chat'; -import { injectAgent } from '@threadplane/ag-ui'; -import { z } from 'zod/v4'; -import { WeatherCardComponent } from './weather-card.component'; -import { ConfirmBookingComponent } from './confirm-booking.component'; - -const clientTools = tools({ - get_weather: action( - 'Look up the current weather for a location.', - z.object({ location: z.string() }), - async ({ location }) => ({ location, temperatureF: 68, conditions: 'Sunny', humidity: 55, windMph: 8 }), - ), - weather_card: view( - 'Display a weather card for a location with the given readings.', - z.object({ - location: z.string(), - temperatureF: z.number(), - conditions: z.string(), - humidity: z.number(), - windMph: z.number(), - }), - WeatherCardComponent, - ), - confirm_booking: ask( - 'Ask the user to confirm a booking before finalizing it.', - z.object({ summary: z.string() }), - ConfirmBookingComponent, - ), -}); -``` - -Pass the registry to `` via the `[clientTools]` input: - -```html - -``` - - - - -**`action` — async function.** When the model calls `get_weather`, the handler -runs in the browser. Its resolved return value becomes the `ToolMessage` content -that re-enters the model: - -```typescript -get_weather: action( - 'Look up the current weather for a location.', - z.object({ location: z.string() }), - async ({ location }) => ({ location, temperatureF: 68, conditions: 'Sunny', humidity: 55, windMph: 8 }), -), -``` - -**`view` — inline component, auto-acknowledged.** When the model calls -`weather_card`, the chat lib mounts `WeatherCardComponent` directly in the -message thread. The component receives the tool call's arguments as Angular -`input()` signals (plus a `status` signal of `'running' | 'complete'`). The -result is acknowledged automatically — no user interaction required: - -```typescript -// weather-card.component.ts -export class WeatherCardComponent { - readonly location = input(); - readonly temperatureF = input(); - readonly conditions = input(); - readonly humidity = input(); - readonly windMph = input(); - readonly status = input<'running' | 'complete'>(); - - readonly pending = computed(() => this.status() !== 'complete' || this.temperatureF() === undefined); -} -``` - -**`ask` — interactive HITL component.** When the model calls `confirm_booking`, -`ConfirmBookingComponent` is mounted. The model fills the `summary` input; the -user responds by clicking Confirm or Cancel. The component calls -`injectRenderHost().result(value)` — that value becomes the `ToolMessage` -content that resumes the run. - -Once the ask resolves, the adapter writes the emitted result back onto the local -tool call; `chat-tool-views` then spreads `{ ...args, ...result, status }` back -into the component's inputs. The component declares an optional `confirmed` -input (defaulting to `undefined`) and uses it to decide whether to render the -interactive card or a frozen, button-less resolved state: - -```typescript -// confirm-booking.component.ts -import { input } from '@angular/core'; -import { injectRenderHost } from '@threadplane/render'; - -export class ConfirmBookingComponent { - readonly summary = input(); - /** Spread back onto props after the ask resolves (undefined while interactive). */ - readonly confirmed = input(undefined); - private readonly host = injectRenderHost(); - - protected respond(confirmed: boolean): void { - this.host.result({ confirmed }); - } -} -``` - -The template branches on `confirmed()` to freeze the card once resolved: - -```html -@if (confirmed() === undefined) { -
-

{{ summary() }}

-
- - -
-
-} @else if (confirmed() === true) { -
-

Booking confirmed ✓

-
-} @else { -
-

Booking cancelled

-
-} -``` - - -The `confirmed` input is `undefined` for the entire interactive lifetime of the -card — buttons are live. The moment the user clicks, `host.result({ confirmed })` -is called, the adapter resolves the tool call and writes the emitted value back -onto the stored tool call, and `chat-tool-views` re-renders the component with -`confirmed` set to the user's choice. Declare the input with a default of -`undefined` (not `required`) so Angular does not throw when it is absent on the -first render. - - -
- - -The backend graph must declare a `tools` channel in its `State` so that -`ag-ui-langgraph`'s merged client catalog is retained across the turn. The -`agent` node calls `bind_client_tools(llm, [], state)` from -`threadplane.middleware.langgraph`, which binds the client stubs (no server -implementation) onto the model for this invocation: - -```python -# graph.py -from langchain_core.messages import SystemMessage -from langchain_openai import ChatOpenAI -from langgraph.graph import StateGraph, END -from langgraph.graph.message import add_messages -from langgraph.checkpoint.memory import MemorySaver -from typing_extensions import Annotated, TypedDict - -from threadplane.middleware.langgraph import bind_client_tools - -class State(TypedDict): - # `tools` holds the client tool catalog ag-ui-langgraph merges in from - # RunAgentInput.tools — declared as a channel so the graph retains it. - messages: Annotated[list, add_messages] - tools: list - -_base_llm = ChatOpenAI(model="gpt-4o-mini", streaming=True) - -async def agent(state: State) -> dict: - llm = bind_client_tools(_base_llm, [], state) - system = (PROMPTS_DIR / "client-tools.md").read_text() - response = await llm.ainvoke([SystemMessage(content=system)] + state["messages"]) - return {"messages": [response]} -``` - -Because there are no server tools, there is no tool loop. The `route` function -returns `END` unconditionally — a client tool call ends the turn, the browser -executes the tool, and the re-submitted `ToolMessage` starts a new turn that the -model summarizes: - -```python -def route(state: State) -> str: - return END - -graph = StateGraph(State) -graph.add_node("agent", agent) -graph.set_entry_point("agent") -graph.add_conditional_edges("agent", route, {END: END}) -graph = graph.compile(checkpointer=MemorySaver()) -``` - -Start the backend with: - -```bash -uv run uvicorn src.server:app --port 5325 -``` - - -Three requirements must all be met or the feature silently breaks: - -1. **The `State` must declare a `tools` channel.** `ag-ui-langgraph` merges the - client catalog into `state["tools"]`; if the field is absent the catalog is - dropped and `bind_client_tools` has nothing to bind — the model will not see - any tools. - -2. **Schemas must be authored with `zod/v4`.** The AG-UI adapter derives the - JSON Schema the model receives using `zod/v4`'s `toJSONSchema`. Schemas from - `zod` (v3) produce a different derivation and may not round-trip correctly. - -3. **`ag-ui-langgraph` requires a checkpointer.** The graph must be compiled - with `checkpointer=MemorySaver()` (or an equivalent persistent checkpointer) - or the adapter cannot maintain per-thread state across the action/ask turns. - - - -
- - -A component registered as an `ask` or `view` client tool uses the same render -contract as a backend tool-view component — `input()` signals for props and -`injectRenderHost()` for result emission. This means the same Angular component -can serve double duty: register it in the `views` registry for backend tool-view -rendering and in the client `tools` registry for client-side ask/view calls, with -no changes to the component itself. Mixing server tools and client tools in the -same graph is also supported: pass the server tool list as the second argument to -`bind_client_tools(llm, server_tools, state)` and add the standard server-tool -routing alongside the client-tool END branch. - - - -- [AG-UI Tool Views](/ag-ui/core-capabilities/tool-views/overview/python) — Backend tool call rendered as a frontend component -- [AG-UI JSON Render](/ag-ui/core-capabilities/json-render/overview/python) — Backend shared-state generative UI with `$state` bindings -- [AG-UI A2UI](/ag-ui/core-capabilities/a2ui/overview/python) — Backend-authored A2UI surfaces in message content - diff --git a/cockpit/ag-ui/interrupts/angular/src/app/interrupts.component.ts b/cockpit/ag-ui/interrupts/angular/src/app/interrupts.component.ts index 66894067c..a54bbc8b9 100644 --- a/cockpit/ag-ui/interrupts/angular/src/app/interrupts.component.ts +++ b/cockpit/ag-ui/interrupts/angular/src/app/interrupts.component.ts @@ -60,6 +60,7 @@ const WELCOME_SUGGESTIONS = [
+ + `, @@ -190,6 +192,7 @@ export class InterruptsComponent { void this.agent.submit({ message: text }); } + // #region resume protected onAction(action: ChatApprovalAction): void { if (action === 'approve') { void this.agent.submit({ resume: { approved: true } }); @@ -212,4 +215,5 @@ export class InterruptsComponent { this.editing.set(false); this.editAmount.set(null); } + // #endregion } diff --git a/cockpit/ag-ui/interrupts/python/docs/guide.md b/cockpit/ag-ui/interrupts/python/docs/guide.md deleted file mode 100644 index 84638e5f3..000000000 --- a/cockpit/ag-ui/interrupts/python/docs/guide.md +++ /dev/null @@ -1,139 +0,0 @@ -# Human-in-the-Loop Interrupts with AG-UI and Angular - - -Build a chat interface with human-in-the-loop approval using `provideAgent()` and -`injectAgent()` from `@threadplane/ag-ui`. The LangGraph backend pauses execution for approval -and emits an AG-UI `CUSTOM` `on_interrupt` event; the frontend resumes it with `stream.submit()`. - - - -Add human-in-the-loop approval to this Angular component using `provideAgent()` and `injectAgent()` from `@threadplane/ag-ui`. Use `stream.interrupt()` to display pending approvals, `stream.submit({ resume: true })` to approve and resume execution, and `stream.submit({ resume: false })` to reject. Bind `stream.messages()` in the template via the `` component from `@threadplane/chat`. - - - - - -Set up `provideAgent()` in your app config with the AG-UI backend URL: - -```typescript -// app.config.ts -import { ApplicationConfig } from '@angular/core'; -import { provideAgent } from '@threadplane/ag-ui'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideAgent({ - url: 'http://localhost:5320/agent', - }), - ], -}; -``` - -This makes the configured agent available to all `injectAgent()` calls in your app. - - - - -In your component, call `injectAgent()` to retrieve the configured interrupts agent: - -```typescript -// interrupts.component.ts -import { injectAgent } from '@threadplane/ag-ui'; - -export class InterruptsComponent { - protected readonly stream = injectAgent(); -} -``` - -The resource automatically handles streaming, interrupt detection, and state management. - - - - -Use `stream.interrupt()` to conditionally show a pending approval in the sidebar: - -```html - - -@if (stream.interrupt(); as interrupt) { - -} @else { -

No pending approvals

-} -``` - -When the graph pauses, `stream.interrupt()` returns the interrupt payload. When no interrupt is active, it returns a falsy value. - -
- - -Add methods that resume graph execution with the user's decision: - -```typescript -approve(): void { - this.stream.submit({ resume: true }); -} - -reject(): void { - this.stream.submit({ resume: false }); -} -``` - -Submitting a `resume` payload continues past an interrupt. Submitting `{ resume: false }` signals rejection so the graph can handle it accordingly. - - -You can extend this pattern to pass structured data back to the graph. For example, `stream.submit({ resume: true, edits: { ... } })` lets the user modify the response before approving. - - - - - -The backend wraps the LangGraph `graph` (which uses `interrupt()` from `langgraph.types`) in a -FastAPI app using `ag-ui-langgraph`. When `interrupt()` fires, the package emits an AG-UI `CUSTOM` -`on_interrupt` event that the `@threadplane/ag-ui` adapter surfaces as `stream.interrupt()`. - -```python -# server.py -from fastapi import FastAPI -from ag_ui_langgraph import LangGraphAgent, add_langgraph_fastapi_endpoint -from .graph import graph - -agent = LangGraphAgent(name="interrupts", graph=graph) -app = FastAPI(title="cockpit-ag-ui-interrupts") -add_langgraph_fastapi_endpoint(app, agent, path="/agent") - -@app.get("/ok") -def ok() -> dict: - return {"ok": True} -``` - -Run the backend with: - -```bash -uv run uvicorn src.server:app --port 5320 -``` - - -A checkpointer is required for interrupts to work. Without it, the graph cannot save its state -while paused. The graph in `src/graph.py` uses `MemorySaver` for development. - - - -
- - -The `` component handles message rendering, input, loading states, and error display. Focus your component on interrupt handling logic. - - - -Never expose your LangSmith API key in client-side code. Use server-side environment variables or a proxy. - - - -- [LangGraph Interrupts](/langgraph/core-capabilities/interrupts/overview/python) — The LangGraph variant of this pattern using the LangGraph SDK directly -- [AG-UI Streaming](/ag-ui/core-capabilities/streaming/overview/python) — Basic streaming without interrupts using the AG-UI adapter - diff --git a/cockpit/ag-ui/interrupts/python/src/graph.py b/cockpit/ag-ui/interrupts/python/src/graph.py index cc7277ea8..3385acc09 100644 --- a/cockpit/ag-ui/interrupts/python/src/graph.py +++ b/cockpit/ag-ui/interrupts/python/src/graph.py @@ -22,6 +22,7 @@ PROMPTS_DIR = Path(__file__).parent.parent / "prompts" +# region state class RefundDraft(BaseModel): """Structured fields the agent extracts from the refund request.""" @@ -37,12 +38,14 @@ class RefundState(TypedDict): reason: Optional[str] decision_approved: Optional[bool] refund_id: Optional[str] +# endregion def build_interrupts_graph(): llm = ChatOpenAI(model="gpt-5-mini", streaming=True) extractor = ChatOpenAI(model="gpt-5-mini").with_structured_output(RefundDraft) + # region draft async def draft_refund(state: RefundState) -> dict: """Extract structured refund fields, then acknowledge the draft. @@ -66,7 +69,9 @@ async def draft_refund(state: RefundState) -> dict: "amount": draft.amount, "reason": draft.reason, } + # endregion + # region request-approval def request_approval(state: RefundState) -> dict: """Pause for human approval. Resume value is { approved: bool, amount?: number }.""" amount = state.get("amount") or 0.0 @@ -92,7 +97,9 @@ def request_approval(state: RefundState) -> dict: "decision_approved": True, "amount": final_amount, } + # endregion + # region graph def issue_refund(state: RefundState) -> dict: """Stand-in for the real Stripe call. Logs a fake refund ID.""" customer_id = state.get("customer_id") or "anon" @@ -116,6 +123,7 @@ def route_after_approval(state: RefundState) -> str: graph.add_edge("issue", END) return graph.compile(checkpointer=MemorySaver()) + # endregion graph = build_interrupts_graph() diff --git a/cockpit/ag-ui/json-render/python/docs/guide.md b/cockpit/ag-ui/json-render/python/docs/guide.md deleted file mode 100644 index cdc4d89f7..000000000 --- a/cockpit/ag-ui/json-render/python/docs/guide.md +++ /dev/null @@ -1,126 +0,0 @@ -# Backend-Driven Generative UI with AG-UI Shared State - - -Let the agent author a UI layout AND stream its data over AG-UI. The agent -sends a json-render spec as the assistant message — the chat lib mounts it -against your `views` registry — while the dashboard's numbers arrive as -**agent shared state** (AG-UI `STATE_SNAPSHOT` / `STATE_DELTA`). The spec's -`$state` bindings resolve against that state, so the same layout re-renders -as the backend updates the data. - - - -Render a backend-authored dashboard with `@threadplane/chat` over the AG-UI adapter. Register your view components in the `views` map and pass it to ``, along with an explicit `[store]` — the composition syncs incoming agent state into that store, and the spec's `$state` bindings resolve against it. Have the agent emit a json-render spec (with `$state` bindings) as the assistant message content, and put the data the spec binds to in the LangGraph graph state so `ag-ui-langgraph` emits it as a `STATE_SNAPSHOT`. - - - - - -Build a `views` registry keyed by the component types your spec will reference, and pass it to `` together with an explicit store. Without a `[store]`, each render surface seeds its own isolated store from the spec — the explicit store is what lets backend state (`STATE_SNAPSHOT`) reach the dashboard bindings: - -```typescript -// json-render.component.ts -import { ChatComponent, views } from '@threadplane/chat'; -import { injectAgent } from '@threadplane/ag-ui'; -import { signalStateStore } from '@threadplane/render'; -import { StatCardComponent } from './views/stat-card.component'; -import { DashboardGridComponent } from './views/dashboard-grid.component'; -// …line-chart, bar-chart, data-grid, container - -const dashboardViews = views({ - stat_card: StatCardComponent, - dashboard_grid: DashboardGridComponent, - // … -}); - -// In the component class: -readonly dashStore = signalStateStore({}); -``` - -```html - -``` - - - - -```typescript -// app.config.ts -import { provideAgent } from '@threadplane/ag-ui'; -import { provideChat } from '@threadplane/chat'; - -export const appConfig: ApplicationConfig = { - providers: [provideAgent({ url: '/agent' }), provideChat({})], -}; -``` - - - - -The agent authors the layout once and returns it as JSON. A post-process node -moves that payload into the assistant message content, where the chat lib's -content classifier detects the leading `{` and mounts the render surface. Each -data prop uses a `$state` binding rather than a literal: - -```json -{ - "elements": { - "on_time_card": { - "type": "stat_card", - "props": { "label": "On-time %", "value": { "$state": "/on_time/value" } } - } - }, - "root": "..." -} -``` - - - - -This is the AG-UI-native part. Instead of pushing data through a side channel, -put it in the **graph state** — `ag-ui-langgraph` emits the state object as a -`STATE_SNAPSHOT`, the adapter writes it to the agent's `state` signal, and the -chat composition syncs it into the explicit `[store]` you passed, where the -`$state` bindings resolve: - -```python -# graph.py — emit_state returns the accumulated tool data into state -async def emit_state(state: DashboardState) -> dict: - updates: dict = {} - for msg in reversed(state["messages"]): - if getattr(msg, "type", None) == "tool" and msg.name == "query_airline_kpis": - updates.update(json.loads(msg.content)) # {on_time: {value, delta}, …} - # …other data tools - return updates # becomes top-level state fields → STATE_SNAPSHOT -``` - -The spec binding `/on_time/value` resolves to `state.on_time.value`. Run the -backend with: - -```bash -uv run uvicorn src.server:app --port 5323 -``` - - -A field is only visible to the frontend if it is in the graph's **output -schema** — `ag-ui-langgraph` filters the snapshot to output-schema keys. -Declare every bound field on `DashboardState` (a plain `StateGraph(State)` uses -its state schema as the output schema). Also: `ag-ui-langgraph` requires a -checkpointer — the graph uses `MemorySaver` for development. - - - - - - -The same `views` registry powers tool-driven rendering too — a component you -register here is reusable for the tool-views pattern with no changes. The only -difference is where the layout and data come from: a backend spec + shared -state here, versus a tool call's args/result there. - - - -- [AG-UI Tool Views](/ag-ui/core-capabilities/tool-views/overview/python) — Frontend component keyed by tool name (no spec on the wire) -- [AG-UI A2UI](/ag-ui/core-capabilities/a2ui/overview/python) — Backend-authored A2UI surfaces in message content -- [AG-UI Streaming](/ag-ui/core-capabilities/streaming/overview/python) — Real-time token streaming with the AG-UI adapter - diff --git a/cockpit/ag-ui/json-render/python/src/graph.py b/cockpit/ag-ui/json-render/python/src/graph.py index 8c3c453e2..dc000963c 100644 --- a/cockpit/ag-ui/json-render/python/src/graph.py +++ b/cockpit/ag-ui/json-render/python/src/graph.py @@ -44,6 +44,7 @@ _MAX_TOOL_ITERATIONS = 6 +# region dashboard-state class DashboardState(TypedDict): """Graph state for the airline KPI dashboard. @@ -60,8 +61,9 @@ class DashboardState(TypedDict): on_time_trend: Optional[list] flights_by_airline: Optional[list] recent_disruptions: Optional[list] +# endregion - +# region render-spec-tool @tool async def render_spec(elements: dict, root: str) -> str: """Render an interactive dashboard layout. @@ -84,6 +86,7 @@ async def render_spec(elements: dict, root: str) -> str: chat-lib's content-classifier picks it up. """ return json.dumps({"elements": elements, "root": root}) +# endregion _ALL_TOOLS = [render_spec, *_DATA_TOOLS] @@ -194,6 +197,7 @@ async def finalize(state: DashboardState) -> dict: return {"messages": [AIMessage(**replacement_kwargs)]} +# region wrap-spec-into-ai async def wrap_spec_into_ai(state: DashboardState) -> dict: """Post-process that wraps the most recent render_spec ToolMessage payload into the parent AI tool-call message's content (in place via @@ -232,7 +236,9 @@ async def wrap_spec_into_ai(state: DashboardState) -> dict: payload = render_tool_msg.content if isinstance(render_tool_msg.content, str) else "" if not payload: return {} +# endregion + # region rewrite-assistant-content stripped = payload.strip() if stripped.startswith("```"): lines = stripped.split("\n") @@ -260,8 +266,10 @@ async def wrap_spec_into_ai(state: DashboardState) -> dict: out.append(AIMessage(**replacement_kwargs)) return {"messages": out} + # endregion +# region emit-state async def emit_state(state: DashboardState) -> dict: """Accumulate this turn's tool results into graph state so ag-ui-langgraph emits them as STATE_SNAPSHOT. Walk messages in reverse to the most recent @@ -285,6 +293,7 @@ async def emit_state(state: DashboardState) -> dict: elif getattr(msg, "type", None) == "human": break return updates +# endregion async def respond(state: DashboardState) -> dict: @@ -302,6 +311,7 @@ async def respond(state: DashboardState) -> dict: return {"messages": [response]} +# region graph-wiring _builder = StateGraph(DashboardState) _builder.add_node("agent", agent) _builder.add_node("tools", ToolNode(_ALL_TOOLS)) @@ -324,3 +334,4 @@ async def respond(state: DashboardState) -> dict: # The chat example omits it because LangGraph Cloud provides one at runtime, # but the ag-ui-langgraph/uvicorn runtime needs it explicitly. graph = _builder.compile(checkpointer=MemorySaver()) +# endregion diff --git a/cockpit/ag-ui/streaming/python/docs/guide.md b/cockpit/ag-ui/streaming/python/docs/guide.md deleted file mode 100644 index bb6a4ac09..000000000 --- a/cockpit/ag-ui/streaming/python/docs/guide.md +++ /dev/null @@ -1,122 +0,0 @@ -# Real-Time Streaming with AG-UI and Angular - - -Build a real-time streaming chat interface using `provideAgent()` and -`injectAgent()` from `@threadplane/ag-ui` connected to a LangGraph backend -served locally via the AG-UI adapter. - - - -Add real-time LLM streaming to this Angular component using `@threadplane/ag-ui`. Configure `provideAgent({ url })` in the app config, call `injectAgent()` in the component, then call `stream.submit()` to send messages. Bind `stream.messages()` in the template via the `` component from `@threadplane/chat` — all Signals, no subscriptions needed. - - - - - -Set up `provideAgent()` in your app config with the AG-UI backend URL: - -```typescript -// app.config.ts -import { ApplicationConfig } from '@angular/core'; -import { provideAgent } from '@threadplane/ag-ui'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideAgent({ - url: 'http://localhost:5321/agent', - }), - ], -}; -``` - -This makes the configured agent available to all `injectAgent()` calls in your app. - - - - -In your component, call `injectAgent()` to retrieve the configured streaming agent: - -```typescript -// streaming.component.ts -import { injectAgent } from '@threadplane/ag-ui'; - -export class StreamingComponent { - protected readonly stream = injectAgent(); -} -``` - -The resource automatically handles streaming, connection lifecycle, and state management. - - - - -Use the `` component to render messages reactively: - -```html - -``` - -The template re-renders automatically as tokens arrive — no manual subscriptions or change detection needed. - - - - -Call `stream.submit()` with a message payload: - -```typescript -// streaming.component.ts -send(): void { - const text = this.prompt().trim(); - if (!text || this.stream.isLoading()) return; - this.prompt.set(''); - void this.stream.submit({ message: text }); -} -``` - -The submit call opens a streaming connection to the AG-UI backend. As tokens arrive, `stream.messages()` updates reactively. - - - - -The backend wraps the LangGraph `graph` in a FastAPI app using `ag-ui-langgraph`. The AG-UI adapter translates LangGraph streaming events into the AG-UI protocol, which the `@threadplane/ag-ui` adapter consumes directly. - -```python -# server.py -from fastapi import FastAPI -from ag_ui_langgraph import LangGraphAgent, add_langgraph_fastapi_endpoint -from .graph import graph - -agent = LangGraphAgent(name="streaming", graph=graph) -app = FastAPI(title="cockpit-ag-ui-streaming") -add_langgraph_fastapi_endpoint(app, agent, path="/agent") - -@app.get("/ok") -def ok() -> dict: - return {"ok": True} -``` - -Run the backend with: - -```bash -uv run uvicorn src.server:app --port 5321 -``` - - -A checkpointer is required for `ag-ui-langgraph` to work. Without it, the library cannot call `graph.aget_state()`. The graph in `src/graph.py` uses `MemorySaver` for development. - - - - - - -The `` component handles message rendering, input, loading states, and error display. Focus your component on the `provideAgent()` configuration and any application-specific logic. - - - -Never expose your LangSmith API key in client-side code. Use server-side environment variables or a proxy. - - - -- [LangGraph Streaming](/langgraph/core-capabilities/streaming/overview/python) — The LangGraph variant of this pattern using the LangGraph SDK and LangSmith Cloud directly -- [AG-UI Interrupts](/ag-ui/core-capabilities/interrupts/overview/python) — Human-in-the-loop approval using the AG-UI adapter - diff --git a/cockpit/ag-ui/subagents/python/docs/guide.md b/cockpit/ag-ui/subagents/python/docs/guide.md deleted file mode 100644 index 9e95455d9..000000000 --- a/cockpit/ag-ui/subagents/python/docs/guide.md +++ /dev/null @@ -1,169 +0,0 @@ -# Subagent Cards over AG-UI - - -Render live subagent cards in an Angular chat UI using `provideAgent()` and -`injectAgent()` from `@threadplane/ag-ui`. An orchestrator LangGraph agent -delegates focused subtasks to specialized subagents via a `task` tool; the -backend emits each subagent's run as the protocol's standard `SUBAGENT_STARTED`, -`subagentRunId`-attributed `TEXT_MESSAGE_*` and `SUBAGENT_FINISHED` events, -which the `@threadplane/ag-ui` reducer projects onto `agent.subagents()` for -the `` primitive to render. - - - -Add subagent cards to this Angular component using `@threadplane/ag-ui`. Configure `provideAgent({ url })` in the app config, call `injectAgent()` in the component, and pass the agent to the `` component from `@threadplane/chat`. The orchestrator graph dispatches subagents with a `task` tool and emits `subagent_activity` custom events; the backend's `SubagentEmittingAgent` expands those into the standard AG-UI `SUBAGENT_STARTED` / attributed `TEXT_MESSAGE_*` / `SUBAGENT_FINISHED` events so the chat composition renders a live card per subagent — all Signals, no subscriptions needed. - - - - - -Set up `provideAgent()` in your app config with the AG-UI backend URL: - -```typescript -// app.config.ts -import { ApplicationConfig } from '@angular/core'; -import { provideAgent } from '@threadplane/ag-ui'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideAgent({ - url: 'http://localhost:5326/agent', - }), - ], -}; -``` - -This makes the configured agent available to all `injectAgent()` calls in your app. - - - - -In your component, call `injectAgent()` and pass it to ``. No -subagent-specific wiring is needed — `` renders `` -automatically once `agent.subagents()` populates: - -```typescript -// subagents.component.ts -import { Component } from '@angular/core'; -import { ChatComponent } from '@threadplane/chat'; -import { injectAgent } from '@threadplane/ag-ui'; - -@Component({ - selector: 'app-subagents', - standalone: true, - imports: [ChatComponent], - template: ``, -}) -export class SubagentsComponent { - protected readonly agent = injectAgent(); -} -``` - - - - -The orchestrator binds a `task` tool that dispatches a role-specific -subagent. Before running the subagent it emits a `started` payload; while -the subagent streams, `SubagentStreamHandler` forwards a `message_start` -once and then one `message` per token (the raw delta); after it emits -`finished` — or `error` if the child fails — all keyed by the tool's own -call id: - -```python -# graph.py -from langchain_core.callbacks import adispatch_custom_event -from langchain_core.tools import tool, InjectedToolCallId -from src.streaming.subagent_stream_handler import SubagentStreamHandler - -@tool -async def task(role, task_description, tool_call_id: Annotated[str, InjectedToolCallId]): - await adispatch_custom_event( - "subagent_activity", - {"subagent_id": tool_call_id, "phase": "started", "name": role}, - ) - try: - result = await _run_subagent( - role, task_description, - config={"callbacks": [SubagentStreamHandler(tool_call_id)]}, - ) - except Exception as exc: - await adispatch_custom_event( - "subagent_activity", - {"subagent_id": tool_call_id, "phase": "error", "message": str(exc)}, - ) - raise - await adispatch_custom_event( - "subagent_activity", - {"subagent_id": tool_call_id, "phase": "finished"}, - ) - return result -``` - - - - -The backend wraps the LangGraph `graph` in a FastAPI app using -`ag-ui-langgraph`. `SubagentEmittingAgent` subclasses the bridge's -`LangGraphAgent` and wraps its `run()` generator, expanding each -`subagent_activity` CUSTOM event into the protocol's standard events (ids -derived from the `task` tool call id, `tid`): - -| phase | wire event | -| --- | --- | -| `started {name}` | `SUBAGENT_STARTED {subagentRunId: -sub, name, parentToolCallId: }` | -| `message_start {message_id}` | `TEXT_MESSAGE_START {messageId: -sub-m1, role: assistant, subagentRunId}` | -| `message {message_id, delta}` | `TEXT_MESSAGE_CONTENT {messageId, delta, subagentRunId}` | -| `finished` | `TEXT_MESSAGE_END` for the open message, then `SUBAGENT_FINISHED {outcome: success}` | -| `error {message}` | `TEXT_MESSAGE_END` for the open message, then `SUBAGENT_ERROR {message}` | - -The CUSTOM event itself is consumed; every other bridge event passes through -untouched. Because `parentToolCallId` equals the bridge-native -`TOOL_CALL_START.toolCallId` (which is fully streamed before the tool body -runs), the reducer anchors the card to the `task` call with no bookkeeping: - -```python -# server.py -from fastapi import FastAPI -from ag_ui_langgraph import add_langgraph_fastapi_endpoint -from .graph import graph -from .streaming.subagent_emitting_agent import SubagentEmittingAgent - -app = FastAPI(title="cockpit-ag-ui-subagents") -add_langgraph_fastapi_endpoint( - app, SubagentEmittingAgent(name="subagents", graph=graph), path="/agent" -) - -@app.get("/ok") -def ok() -> dict: - return {"ok": True} -``` - -Run the backend with: - -```bash -uv run uvicorn src.server:app --port 5326 -``` - - -A checkpointer is required for `ag-ui-langgraph` to work. Without it, the library cannot call `graph.aget_state()`. The graph in `src/graph.py` uses `MemorySaver` for development. - - - -The AG-UI encoder only serializes pydantic `ag_ui.core` event classes — yielding a raw dict crashes the stream. `SubagentEmittingAgent` constructs `SubagentStartedEvent`, `TextMessageStartEvent`, and friends from `ag_ui.core` (`ag-ui-protocol>=0.1.22`, the first release with `subagent_run_id` on every event). - - - - - - -The `` component renders `` only while a subagent is in a running state. Under instant replay the run can settle within a single frame, so the live card transits below a render frame — assert on the durable `agent.subagents()` projection (or the tool-call card) rather than the live card element. - - - -Never expose your LangSmith API key in client-side code. Use server-side environment variables or a proxy. - - - -- [Chat Subagents](/chat/core-capabilities/subagents/overview/python) — The LangGraph variant of this pattern using the chat-subagents primitive directly -- [AG-UI Streaming](/ag-ui/core-capabilities/streaming/overview/python) — The minimal streaming pattern using the AG-UI adapter - diff --git a/cockpit/ag-ui/subagents/python/src/graph.py b/cockpit/ag-ui/subagents/python/src/graph.py index 68ca9c542..2f9d03c8a 100644 --- a/cockpit/ag-ui/subagents/python/src/graph.py +++ b/cockpit/ag-ui/subagents/python/src/graph.py @@ -107,6 +107,7 @@ async def generate_title(state: MessagesState, config) -> dict: concise.""" +# region run-subagent async def _run_subagent( role: str, task_description: str, @@ -137,8 +138,10 @@ async def _run_subagent( ] return "\n".join(parts) return "" +# endregion +# region task-tool @tool async def task( role: Literal["research", "booking", "itinerary"], @@ -194,8 +197,10 @@ async def _emit(payload: dict) -> None: raise await _emit({"phase": "finished", "status": "complete"}) return result +# endregion +# region graph def build_subagents_graph(): """Orchestrator LLM with a single `task` tool that dispatches subagents.""" llm = ChatOpenAI(model="gpt-5-mini", streaming=True).bind_tools([task]) @@ -225,6 +230,7 @@ def should_continue(state: MessagesState) -> str: graph.add_edge("tools", "orchestrator") graph.add_edge("generate_title", END) return graph.compile(checkpointer=MemorySaver()) +# endregion # The graph instance — referenced by server.py diff --git a/cockpit/ag-ui/tool-views/python/docs/guide.md b/cockpit/ag-ui/tool-views/python/docs/guide.md deleted file mode 100644 index 249d248e1..000000000 --- a/cockpit/ag-ui/tool-views/python/docs/guide.md +++ /dev/null @@ -1,92 +0,0 @@ -# Tool-Driven View Rendering with AG-UI and Angular - - -Render a frontend component for a tool call by reusing the `views` registry -from `@threadplane/chat`. The agent calls a tool by name and returns plain -data; the frontend owns a component keyed by that name and renders it live -from the call's arguments, result, and status — no UI spec crosses the wire. - - - -Render a custom component for a tool call using `@threadplane/chat`. Register the component in the `views` map keyed by the tool's name, pass `views` to the `` component, and call the matching tool by name from your LangGraph agent. The chat composition bridges the tool call into the render pipeline automatically. - - - - - -Build a `views` registry whose key matches the tool the agent calls: - -```typescript -// tool-views.component.ts -import { views } from '@threadplane/chat'; -import { WeatherCardComponent } from './weather-card.component'; - -readonly views = views({ weather_card: WeatherCardComponent }); -``` - -The key (`weather_card`) is both the registry key and the tool name the -agent calls — one identifier, one mental model. - - - - -```html - -``` - -When a tool call's name matches a `views` key, the chat composition renders -the registered component inline in the transcript instead of the default -tool card. - - - - -The component declares an input per field it renders. It receives the live -arguments while the call streams and the merged result on completion, plus a -`status` of `'running'` or `'complete'`: - -```typescript -// weather-card.component.ts (excerpt) -readonly location = input(); -readonly temperatureF = input(); -readonly status = input<'running' | 'complete'>(); -``` - - - - -The LangGraph agent binds a tool whose name matches the registered view and -returns plain JSON. The tool call and result travel over AG-UI's -`TOOL_CALL_*` events: - -```python -# graph.py -@tool -async def weather_card(location: str) -> dict: - return {"location": location, "temperatureF": 68, "conditions": "Sunny", - "humidity": 55, "windMph": 8} -``` - -Run the backend with: - -```bash -uv run uvicorn src.server:app --port 5322 -``` - - -A checkpointer is required for `ag-ui-langgraph` to call `graph.aget_state()`. -The graph in `src/graph.py` uses `MemorySaver` for development. - - - - - - -The same `views` registry powers backend-sent render specs too — a component -you register here is reusable across tool-driven rendering and spec rendering. - - - -- [AG-UI Streaming](/ag-ui/core-capabilities/streaming/overview/python) — Real-time token streaming with the AG-UI adapter -- [AG-UI Interrupts](/ag-ui/core-capabilities/interrupts/overview/python) — Human-in-the-loop approval using the AG-UI adapter - diff --git a/cockpit/ag-ui/tool-views/python/src/graph.py b/cockpit/ag-ui/tool-views/python/src/graph.py index 23876dde5..18dbee8fc 100644 --- a/cockpit/ag-ui/tool-views/python/src/graph.py +++ b/cockpit/ag-ui/tool-views/python/src/graph.py @@ -22,6 +22,7 @@ PROMPTS_DIR = Path(__file__).parent.parent / "prompts" +# region weather-tool @tool async def weather_card(location: str) -> dict: """Look up the current weather for a location. @@ -43,11 +44,13 @@ async def weather_card(location: str) -> dict: "humidity": 55, "windMph": 8, } +# endregion _TOOLS = [weather_card] +# region graph def build_tool_views_graph(): llm = ChatOpenAI(model="gpt-5-mini", streaming=True).bind_tools(_TOOLS) @@ -69,6 +72,7 @@ def route(state: MessagesState) -> str: graph.add_edge("tools", "agent") return graph.compile(checkpointer=MemorySaver()) +# endregion # The graph instance — referenced by server.py diff --git a/deployments/ag-ui-dev/deps/client_tools/docs/guide.md b/deployments/ag-ui-dev/deps/client_tools/docs/guide.md deleted file mode 100644 index 5eb43f1fe..000000000 --- a/deployments/ag-ui-dev/deps/client_tools/docs/guide.md +++ /dev/null @@ -1,261 +0,0 @@ -# Browser-Executed Client Tools with AG-UI - - -Declare tools in the Angular app — with a description, a Zod schema, and a -handler — and have the **model** call them while the **browser** executes them. -Three behaviors are supported: `action` (async function whose return becomes the -tool result), `view` (Angular component the model fills with props, rendered -inline and auto-acknowledged), and `ask` (interactive HITL component whose -emitted value resumes the run). The catalog is shipped to the model via the -AG-UI adapter's native `RunAgentInput.tools` field; the LangGraph backend binds -the client stubs with no server implementation and ends the turn so the browser -runs the tool and re-submits with a `ToolMessage`, which the model then -summarizes. - - - -Declare client tools in the Angular app using `tools()`, `action()`, `view()`, -and `ask()` from `@threadplane/chat`, with schemas authored in `zod/v4`. Pass -the registry to ``. On the backend, declare a `tools` -channel in your LangGraph `State`, call `bind_client_tools(llm, [], state)` from -`threadplane.middleware.langgraph`, and route unconditionally to `END` — there are no -server-side tool implementations. - - - - - -Build a registry with `tools()` from `@threadplane/chat`. Each entry is one of -three behaviors — `action`, `view`, or `ask` — all keyed by the tool name the -model will call. Schemas are authored with `zod/v4`; the AG-UI adapter derives -the JSON Schema the model sees using `zod/v4`'s `toJSONSchema`. - -```typescript -// client-tools.component.ts -import { ChatComponent, tools, action, view, ask } from '@threadplane/chat'; -import { injectAgent } from '@threadplane/ag-ui'; -import { z } from 'zod/v4'; -import { WeatherCardComponent } from './weather-card.component'; -import { ConfirmBookingComponent } from './confirm-booking.component'; - -const clientTools = tools({ - get_weather: action( - 'Look up the current weather for a location.', - z.object({ location: z.string() }), - async ({ location }) => ({ location, temperatureF: 68, conditions: 'Sunny', humidity: 55, windMph: 8 }), - ), - weather_card: view( - 'Display a weather card for a location with the given readings.', - z.object({ - location: z.string(), - temperatureF: z.number(), - conditions: z.string(), - humidity: z.number(), - windMph: z.number(), - }), - WeatherCardComponent, - ), - confirm_booking: ask( - 'Ask the user to confirm a booking before finalizing it.', - z.object({ summary: z.string() }), - ConfirmBookingComponent, - ), -}); -``` - -Pass the registry to `` via the `[clientTools]` input: - -```html - -``` - - - - -**`action` — async function.** When the model calls `get_weather`, the handler -runs in the browser. Its resolved return value becomes the `ToolMessage` content -that re-enters the model: - -```typescript -get_weather: action( - 'Look up the current weather for a location.', - z.object({ location: z.string() }), - async ({ location }) => ({ location, temperatureF: 68, conditions: 'Sunny', humidity: 55, windMph: 8 }), -), -``` - -**`view` — inline component, auto-acknowledged.** When the model calls -`weather_card`, the chat lib mounts `WeatherCardComponent` directly in the -message thread. The component receives the tool call's arguments as Angular -`input()` signals (plus a `status` signal of `'running' | 'complete'`). The -result is acknowledged automatically — no user interaction required: - -```typescript -// weather-card.component.ts -export class WeatherCardComponent { - readonly location = input(); - readonly temperatureF = input(); - readonly conditions = input(); - readonly humidity = input(); - readonly windMph = input(); - readonly status = input<'running' | 'complete'>(); - - readonly pending = computed(() => this.status() !== 'complete' || this.temperatureF() === undefined); -} -``` - -**`ask` — interactive HITL component.** When the model calls `confirm_booking`, -`ConfirmBookingComponent` is mounted. The model fills the `summary` input; the -user responds by clicking Confirm or Cancel. The component calls -`injectRenderHost().result(value)` — that value becomes the `ToolMessage` -content that resumes the run. - -Once the ask resolves, the adapter writes the emitted result back onto the local -tool call; `chat-tool-views` then spreads `{ ...args, ...result, status }` back -into the component's inputs. The component declares an optional `confirmed` -input (defaulting to `undefined`) and uses it to decide whether to render the -interactive card or a frozen, button-less resolved state: - -```typescript -// confirm-booking.component.ts -import { input } from '@angular/core'; -import { injectRenderHost } from '@threadplane/render'; - -export class ConfirmBookingComponent { - readonly summary = input(); - /** Spread back onto props after the ask resolves (undefined while interactive). */ - readonly confirmed = input(undefined); - private readonly host = injectRenderHost(); - - protected respond(confirmed: boolean): void { - this.host.result({ confirmed }); - } -} -``` - -The template branches on `confirmed()` to freeze the card once resolved: - -```html -@if (confirmed() === undefined) { -
-

{{ summary() }}

-
- - -
-
-} @else if (confirmed() === true) { -
-

Booking confirmed ✓

-
-} @else { -
-

Booking cancelled

-
-} -``` - - -The `confirmed` input is `undefined` for the entire interactive lifetime of the -card — buttons are live. The moment the user clicks, `host.result({ confirmed })` -is called, the adapter resolves the tool call and writes the emitted value back -onto the stored tool call, and `chat-tool-views` re-renders the component with -`confirmed` set to the user's choice. Declare the input with a default of -`undefined` (not `required`) so Angular does not throw when it is absent on the -first render. - - -
- - -The backend graph must declare a `tools` channel in its `State` so that -`ag-ui-langgraph`'s merged client catalog is retained across the turn. The -`agent` node calls `bind_client_tools(llm, [], state)` from -`threadplane.middleware.langgraph`, which binds the client stubs (no server -implementation) onto the model for this invocation: - -```python -# graph.py -from langchain_core.messages import SystemMessage -from langchain_openai import ChatOpenAI -from langgraph.graph import StateGraph, END -from langgraph.graph.message import add_messages -from langgraph.checkpoint.memory import MemorySaver -from typing_extensions import Annotated, TypedDict - -from threadplane.middleware.langgraph import bind_client_tools - -class State(TypedDict): - # `tools` holds the client tool catalog ag-ui-langgraph merges in from - # RunAgentInput.tools — declared as a channel so the graph retains it. - messages: Annotated[list, add_messages] - tools: list - -_base_llm = ChatOpenAI(model="gpt-4o-mini", streaming=True) - -async def agent(state: State) -> dict: - llm = bind_client_tools(_base_llm, [], state) - system = (PROMPTS_DIR / "client-tools.md").read_text() - response = await llm.ainvoke([SystemMessage(content=system)] + state["messages"]) - return {"messages": [response]} -``` - -Because there are no server tools, there is no tool loop. The `route` function -returns `END` unconditionally — a client tool call ends the turn, the browser -executes the tool, and the re-submitted `ToolMessage` starts a new turn that the -model summarizes: - -```python -def route(state: State) -> str: - return END - -graph = StateGraph(State) -graph.add_node("agent", agent) -graph.set_entry_point("agent") -graph.add_conditional_edges("agent", route, {END: END}) -graph = graph.compile(checkpointer=MemorySaver()) -``` - -Start the backend with: - -```bash -uv run uvicorn src.server:app --port 5325 -``` - - -Three requirements must all be met or the feature silently breaks: - -1. **The `State` must declare a `tools` channel.** `ag-ui-langgraph` merges the - client catalog into `state["tools"]`; if the field is absent the catalog is - dropped and `bind_client_tools` has nothing to bind — the model will not see - any tools. - -2. **Schemas must be authored with `zod/v4`.** The AG-UI adapter derives the - JSON Schema the model receives using `zod/v4`'s `toJSONSchema`. Schemas from - `zod` (v3) produce a different derivation and may not round-trip correctly. - -3. **`ag-ui-langgraph` requires a checkpointer.** The graph must be compiled - with `checkpointer=MemorySaver()` (or an equivalent persistent checkpointer) - or the adapter cannot maintain per-thread state across the action/ask turns. - - - -
- - -A component registered as an `ask` or `view` client tool uses the same render -contract as a backend tool-view component — `input()` signals for props and -`injectRenderHost()` for result emission. This means the same Angular component -can serve double duty: register it in the `views` registry for backend tool-view -rendering and in the client `tools` registry for client-side ask/view calls, with -no changes to the component itself. Mixing server tools and client tools in the -same graph is also supported: pass the server tool list as the second argument to -`bind_client_tools(llm, server_tools, state)` and add the standard server-tool -routing alongside the client-tool END branch. - - - -- [AG-UI Tool Views](/ag-ui/core-capabilities/tool-views/overview/python) — Backend tool call rendered as a frontend component -- [AG-UI JSON Render](/ag-ui/core-capabilities/json-render/overview/python) — Backend shared-state generative UI with `$state` bindings -- [AG-UI A2UI](/ag-ui/core-capabilities/a2ui/overview/python) — Backend-authored A2UI surfaces in message content - diff --git a/deployments/ag-ui-dev/deps/interrupts/docs/guide.md b/deployments/ag-ui-dev/deps/interrupts/docs/guide.md deleted file mode 100644 index 84638e5f3..000000000 --- a/deployments/ag-ui-dev/deps/interrupts/docs/guide.md +++ /dev/null @@ -1,139 +0,0 @@ -# Human-in-the-Loop Interrupts with AG-UI and Angular - - -Build a chat interface with human-in-the-loop approval using `provideAgent()` and -`injectAgent()` from `@threadplane/ag-ui`. The LangGraph backend pauses execution for approval -and emits an AG-UI `CUSTOM` `on_interrupt` event; the frontend resumes it with `stream.submit()`. - - - -Add human-in-the-loop approval to this Angular component using `provideAgent()` and `injectAgent()` from `@threadplane/ag-ui`. Use `stream.interrupt()` to display pending approvals, `stream.submit({ resume: true })` to approve and resume execution, and `stream.submit({ resume: false })` to reject. Bind `stream.messages()` in the template via the `` component from `@threadplane/chat`. - - - - - -Set up `provideAgent()` in your app config with the AG-UI backend URL: - -```typescript -// app.config.ts -import { ApplicationConfig } from '@angular/core'; -import { provideAgent } from '@threadplane/ag-ui'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideAgent({ - url: 'http://localhost:5320/agent', - }), - ], -}; -``` - -This makes the configured agent available to all `injectAgent()` calls in your app. - - - - -In your component, call `injectAgent()` to retrieve the configured interrupts agent: - -```typescript -// interrupts.component.ts -import { injectAgent } from '@threadplane/ag-ui'; - -export class InterruptsComponent { - protected readonly stream = injectAgent(); -} -``` - -The resource automatically handles streaming, interrupt detection, and state management. - - - - -Use `stream.interrupt()` to conditionally show a pending approval in the sidebar: - -```html - - -@if (stream.interrupt(); as interrupt) { - -} @else { -

No pending approvals

-} -``` - -When the graph pauses, `stream.interrupt()` returns the interrupt payload. When no interrupt is active, it returns a falsy value. - -
- - -Add methods that resume graph execution with the user's decision: - -```typescript -approve(): void { - this.stream.submit({ resume: true }); -} - -reject(): void { - this.stream.submit({ resume: false }); -} -``` - -Submitting a `resume` payload continues past an interrupt. Submitting `{ resume: false }` signals rejection so the graph can handle it accordingly. - - -You can extend this pattern to pass structured data back to the graph. For example, `stream.submit({ resume: true, edits: { ... } })` lets the user modify the response before approving. - - - - - -The backend wraps the LangGraph `graph` (which uses `interrupt()` from `langgraph.types`) in a -FastAPI app using `ag-ui-langgraph`. When `interrupt()` fires, the package emits an AG-UI `CUSTOM` -`on_interrupt` event that the `@threadplane/ag-ui` adapter surfaces as `stream.interrupt()`. - -```python -# server.py -from fastapi import FastAPI -from ag_ui_langgraph import LangGraphAgent, add_langgraph_fastapi_endpoint -from .graph import graph - -agent = LangGraphAgent(name="interrupts", graph=graph) -app = FastAPI(title="cockpit-ag-ui-interrupts") -add_langgraph_fastapi_endpoint(app, agent, path="/agent") - -@app.get("/ok") -def ok() -> dict: - return {"ok": True} -``` - -Run the backend with: - -```bash -uv run uvicorn src.server:app --port 5320 -``` - - -A checkpointer is required for interrupts to work. Without it, the graph cannot save its state -while paused. The graph in `src/graph.py` uses `MemorySaver` for development. - - - -
- - -The `` component handles message rendering, input, loading states, and error display. Focus your component on interrupt handling logic. - - - -Never expose your LangSmith API key in client-side code. Use server-side environment variables or a proxy. - - - -- [LangGraph Interrupts](/langgraph/core-capabilities/interrupts/overview/python) — The LangGraph variant of this pattern using the LangGraph SDK directly -- [AG-UI Streaming](/ag-ui/core-capabilities/streaming/overview/python) — Basic streaming without interrupts using the AG-UI adapter - diff --git a/deployments/ag-ui-dev/deps/interrupts/src/graph.py b/deployments/ag-ui-dev/deps/interrupts/src/graph.py index cc7277ea8..3385acc09 100644 --- a/deployments/ag-ui-dev/deps/interrupts/src/graph.py +++ b/deployments/ag-ui-dev/deps/interrupts/src/graph.py @@ -22,6 +22,7 @@ PROMPTS_DIR = Path(__file__).parent.parent / "prompts" +# region state class RefundDraft(BaseModel): """Structured fields the agent extracts from the refund request.""" @@ -37,12 +38,14 @@ class RefundState(TypedDict): reason: Optional[str] decision_approved: Optional[bool] refund_id: Optional[str] +# endregion def build_interrupts_graph(): llm = ChatOpenAI(model="gpt-5-mini", streaming=True) extractor = ChatOpenAI(model="gpt-5-mini").with_structured_output(RefundDraft) + # region draft async def draft_refund(state: RefundState) -> dict: """Extract structured refund fields, then acknowledge the draft. @@ -66,7 +69,9 @@ async def draft_refund(state: RefundState) -> dict: "amount": draft.amount, "reason": draft.reason, } + # endregion + # region request-approval def request_approval(state: RefundState) -> dict: """Pause for human approval. Resume value is { approved: bool, amount?: number }.""" amount = state.get("amount") or 0.0 @@ -92,7 +97,9 @@ def request_approval(state: RefundState) -> dict: "decision_approved": True, "amount": final_amount, } + # endregion + # region graph def issue_refund(state: RefundState) -> dict: """Stand-in for the real Stripe call. Logs a fake refund ID.""" customer_id = state.get("customer_id") or "anon" @@ -116,6 +123,7 @@ def route_after_approval(state: RefundState) -> str: graph.add_edge("issue", END) return graph.compile(checkpointer=MemorySaver()) + # endregion graph = build_interrupts_graph() diff --git a/deployments/ag-ui-dev/deps/json_render/docs/guide.md b/deployments/ag-ui-dev/deps/json_render/docs/guide.md deleted file mode 100644 index cdc4d89f7..000000000 --- a/deployments/ag-ui-dev/deps/json_render/docs/guide.md +++ /dev/null @@ -1,126 +0,0 @@ -# Backend-Driven Generative UI with AG-UI Shared State - - -Let the agent author a UI layout AND stream its data over AG-UI. The agent -sends a json-render spec as the assistant message — the chat lib mounts it -against your `views` registry — while the dashboard's numbers arrive as -**agent shared state** (AG-UI `STATE_SNAPSHOT` / `STATE_DELTA`). The spec's -`$state` bindings resolve against that state, so the same layout re-renders -as the backend updates the data. - - - -Render a backend-authored dashboard with `@threadplane/chat` over the AG-UI adapter. Register your view components in the `views` map and pass it to ``, along with an explicit `[store]` — the composition syncs incoming agent state into that store, and the spec's `$state` bindings resolve against it. Have the agent emit a json-render spec (with `$state` bindings) as the assistant message content, and put the data the spec binds to in the LangGraph graph state so `ag-ui-langgraph` emits it as a `STATE_SNAPSHOT`. - - - - - -Build a `views` registry keyed by the component types your spec will reference, and pass it to `` together with an explicit store. Without a `[store]`, each render surface seeds its own isolated store from the spec — the explicit store is what lets backend state (`STATE_SNAPSHOT`) reach the dashboard bindings: - -```typescript -// json-render.component.ts -import { ChatComponent, views } from '@threadplane/chat'; -import { injectAgent } from '@threadplane/ag-ui'; -import { signalStateStore } from '@threadplane/render'; -import { StatCardComponent } from './views/stat-card.component'; -import { DashboardGridComponent } from './views/dashboard-grid.component'; -// …line-chart, bar-chart, data-grid, container - -const dashboardViews = views({ - stat_card: StatCardComponent, - dashboard_grid: DashboardGridComponent, - // … -}); - -// In the component class: -readonly dashStore = signalStateStore({}); -``` - -```html - -``` - - - - -```typescript -// app.config.ts -import { provideAgent } from '@threadplane/ag-ui'; -import { provideChat } from '@threadplane/chat'; - -export const appConfig: ApplicationConfig = { - providers: [provideAgent({ url: '/agent' }), provideChat({})], -}; -``` - - - - -The agent authors the layout once and returns it as JSON. A post-process node -moves that payload into the assistant message content, where the chat lib's -content classifier detects the leading `{` and mounts the render surface. Each -data prop uses a `$state` binding rather than a literal: - -```json -{ - "elements": { - "on_time_card": { - "type": "stat_card", - "props": { "label": "On-time %", "value": { "$state": "/on_time/value" } } - } - }, - "root": "..." -} -``` - - - - -This is the AG-UI-native part. Instead of pushing data through a side channel, -put it in the **graph state** — `ag-ui-langgraph` emits the state object as a -`STATE_SNAPSHOT`, the adapter writes it to the agent's `state` signal, and the -chat composition syncs it into the explicit `[store]` you passed, where the -`$state` bindings resolve: - -```python -# graph.py — emit_state returns the accumulated tool data into state -async def emit_state(state: DashboardState) -> dict: - updates: dict = {} - for msg in reversed(state["messages"]): - if getattr(msg, "type", None) == "tool" and msg.name == "query_airline_kpis": - updates.update(json.loads(msg.content)) # {on_time: {value, delta}, …} - # …other data tools - return updates # becomes top-level state fields → STATE_SNAPSHOT -``` - -The spec binding `/on_time/value` resolves to `state.on_time.value`. Run the -backend with: - -```bash -uv run uvicorn src.server:app --port 5323 -``` - - -A field is only visible to the frontend if it is in the graph's **output -schema** — `ag-ui-langgraph` filters the snapshot to output-schema keys. -Declare every bound field on `DashboardState` (a plain `StateGraph(State)` uses -its state schema as the output schema). Also: `ag-ui-langgraph` requires a -checkpointer — the graph uses `MemorySaver` for development. - - - - - - -The same `views` registry powers tool-driven rendering too — a component you -register here is reusable for the tool-views pattern with no changes. The only -difference is where the layout and data come from: a backend spec + shared -state here, versus a tool call's args/result there. - - - -- [AG-UI Tool Views](/ag-ui/core-capabilities/tool-views/overview/python) — Frontend component keyed by tool name (no spec on the wire) -- [AG-UI A2UI](/ag-ui/core-capabilities/a2ui/overview/python) — Backend-authored A2UI surfaces in message content -- [AG-UI Streaming](/ag-ui/core-capabilities/streaming/overview/python) — Real-time token streaming with the AG-UI adapter - diff --git a/deployments/ag-ui-dev/deps/json_render/src/graph.py b/deployments/ag-ui-dev/deps/json_render/src/graph.py index 8c3c453e2..dc000963c 100644 --- a/deployments/ag-ui-dev/deps/json_render/src/graph.py +++ b/deployments/ag-ui-dev/deps/json_render/src/graph.py @@ -44,6 +44,7 @@ _MAX_TOOL_ITERATIONS = 6 +# region dashboard-state class DashboardState(TypedDict): """Graph state for the airline KPI dashboard. @@ -60,8 +61,9 @@ class DashboardState(TypedDict): on_time_trend: Optional[list] flights_by_airline: Optional[list] recent_disruptions: Optional[list] +# endregion - +# region render-spec-tool @tool async def render_spec(elements: dict, root: str) -> str: """Render an interactive dashboard layout. @@ -84,6 +86,7 @@ async def render_spec(elements: dict, root: str) -> str: chat-lib's content-classifier picks it up. """ return json.dumps({"elements": elements, "root": root}) +# endregion _ALL_TOOLS = [render_spec, *_DATA_TOOLS] @@ -194,6 +197,7 @@ async def finalize(state: DashboardState) -> dict: return {"messages": [AIMessage(**replacement_kwargs)]} +# region wrap-spec-into-ai async def wrap_spec_into_ai(state: DashboardState) -> dict: """Post-process that wraps the most recent render_spec ToolMessage payload into the parent AI tool-call message's content (in place via @@ -232,7 +236,9 @@ async def wrap_spec_into_ai(state: DashboardState) -> dict: payload = render_tool_msg.content if isinstance(render_tool_msg.content, str) else "" if not payload: return {} +# endregion + # region rewrite-assistant-content stripped = payload.strip() if stripped.startswith("```"): lines = stripped.split("\n") @@ -260,8 +266,10 @@ async def wrap_spec_into_ai(state: DashboardState) -> dict: out.append(AIMessage(**replacement_kwargs)) return {"messages": out} + # endregion +# region emit-state async def emit_state(state: DashboardState) -> dict: """Accumulate this turn's tool results into graph state so ag-ui-langgraph emits them as STATE_SNAPSHOT. Walk messages in reverse to the most recent @@ -285,6 +293,7 @@ async def emit_state(state: DashboardState) -> dict: elif getattr(msg, "type", None) == "human": break return updates +# endregion async def respond(state: DashboardState) -> dict: @@ -302,6 +311,7 @@ async def respond(state: DashboardState) -> dict: return {"messages": [response]} +# region graph-wiring _builder = StateGraph(DashboardState) _builder.add_node("agent", agent) _builder.add_node("tools", ToolNode(_ALL_TOOLS)) @@ -324,3 +334,4 @@ async def respond(state: DashboardState) -> dict: # The chat example omits it because LangGraph Cloud provides one at runtime, # but the ag-ui-langgraph/uvicorn runtime needs it explicitly. graph = _builder.compile(checkpointer=MemorySaver()) +# endregion diff --git a/deployments/ag-ui-dev/deps/streaming/docs/guide.md b/deployments/ag-ui-dev/deps/streaming/docs/guide.md deleted file mode 100644 index bb6a4ac09..000000000 --- a/deployments/ag-ui-dev/deps/streaming/docs/guide.md +++ /dev/null @@ -1,122 +0,0 @@ -# Real-Time Streaming with AG-UI and Angular - - -Build a real-time streaming chat interface using `provideAgent()` and -`injectAgent()` from `@threadplane/ag-ui` connected to a LangGraph backend -served locally via the AG-UI adapter. - - - -Add real-time LLM streaming to this Angular component using `@threadplane/ag-ui`. Configure `provideAgent({ url })` in the app config, call `injectAgent()` in the component, then call `stream.submit()` to send messages. Bind `stream.messages()` in the template via the `` component from `@threadplane/chat` — all Signals, no subscriptions needed. - - - - - -Set up `provideAgent()` in your app config with the AG-UI backend URL: - -```typescript -// app.config.ts -import { ApplicationConfig } from '@angular/core'; -import { provideAgent } from '@threadplane/ag-ui'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideAgent({ - url: 'http://localhost:5321/agent', - }), - ], -}; -``` - -This makes the configured agent available to all `injectAgent()` calls in your app. - - - - -In your component, call `injectAgent()` to retrieve the configured streaming agent: - -```typescript -// streaming.component.ts -import { injectAgent } from '@threadplane/ag-ui'; - -export class StreamingComponent { - protected readonly stream = injectAgent(); -} -``` - -The resource automatically handles streaming, connection lifecycle, and state management. - - - - -Use the `` component to render messages reactively: - -```html - -``` - -The template re-renders automatically as tokens arrive — no manual subscriptions or change detection needed. - - - - -Call `stream.submit()` with a message payload: - -```typescript -// streaming.component.ts -send(): void { - const text = this.prompt().trim(); - if (!text || this.stream.isLoading()) return; - this.prompt.set(''); - void this.stream.submit({ message: text }); -} -``` - -The submit call opens a streaming connection to the AG-UI backend. As tokens arrive, `stream.messages()` updates reactively. - - - - -The backend wraps the LangGraph `graph` in a FastAPI app using `ag-ui-langgraph`. The AG-UI adapter translates LangGraph streaming events into the AG-UI protocol, which the `@threadplane/ag-ui` adapter consumes directly. - -```python -# server.py -from fastapi import FastAPI -from ag_ui_langgraph import LangGraphAgent, add_langgraph_fastapi_endpoint -from .graph import graph - -agent = LangGraphAgent(name="streaming", graph=graph) -app = FastAPI(title="cockpit-ag-ui-streaming") -add_langgraph_fastapi_endpoint(app, agent, path="/agent") - -@app.get("/ok") -def ok() -> dict: - return {"ok": True} -``` - -Run the backend with: - -```bash -uv run uvicorn src.server:app --port 5321 -``` - - -A checkpointer is required for `ag-ui-langgraph` to work. Without it, the library cannot call `graph.aget_state()`. The graph in `src/graph.py` uses `MemorySaver` for development. - - - - - - -The `` component handles message rendering, input, loading states, and error display. Focus your component on the `provideAgent()` configuration and any application-specific logic. - - - -Never expose your LangSmith API key in client-side code. Use server-side environment variables or a proxy. - - - -- [LangGraph Streaming](/langgraph/core-capabilities/streaming/overview/python) — The LangGraph variant of this pattern using the LangGraph SDK and LangSmith Cloud directly -- [AG-UI Interrupts](/ag-ui/core-capabilities/interrupts/overview/python) — Human-in-the-loop approval using the AG-UI adapter - diff --git a/deployments/ag-ui-dev/deps/subagents/docs/guide.md b/deployments/ag-ui-dev/deps/subagents/docs/guide.md deleted file mode 100644 index 9e95455d9..000000000 --- a/deployments/ag-ui-dev/deps/subagents/docs/guide.md +++ /dev/null @@ -1,169 +0,0 @@ -# Subagent Cards over AG-UI - - -Render live subagent cards in an Angular chat UI using `provideAgent()` and -`injectAgent()` from `@threadplane/ag-ui`. An orchestrator LangGraph agent -delegates focused subtasks to specialized subagents via a `task` tool; the -backend emits each subagent's run as the protocol's standard `SUBAGENT_STARTED`, -`subagentRunId`-attributed `TEXT_MESSAGE_*` and `SUBAGENT_FINISHED` events, -which the `@threadplane/ag-ui` reducer projects onto `agent.subagents()` for -the `` primitive to render. - - - -Add subagent cards to this Angular component using `@threadplane/ag-ui`. Configure `provideAgent({ url })` in the app config, call `injectAgent()` in the component, and pass the agent to the `` component from `@threadplane/chat`. The orchestrator graph dispatches subagents with a `task` tool and emits `subagent_activity` custom events; the backend's `SubagentEmittingAgent` expands those into the standard AG-UI `SUBAGENT_STARTED` / attributed `TEXT_MESSAGE_*` / `SUBAGENT_FINISHED` events so the chat composition renders a live card per subagent — all Signals, no subscriptions needed. - - - - - -Set up `provideAgent()` in your app config with the AG-UI backend URL: - -```typescript -// app.config.ts -import { ApplicationConfig } from '@angular/core'; -import { provideAgent } from '@threadplane/ag-ui'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideAgent({ - url: 'http://localhost:5326/agent', - }), - ], -}; -``` - -This makes the configured agent available to all `injectAgent()` calls in your app. - - - - -In your component, call `injectAgent()` and pass it to ``. No -subagent-specific wiring is needed — `` renders `` -automatically once `agent.subagents()` populates: - -```typescript -// subagents.component.ts -import { Component } from '@angular/core'; -import { ChatComponent } from '@threadplane/chat'; -import { injectAgent } from '@threadplane/ag-ui'; - -@Component({ - selector: 'app-subagents', - standalone: true, - imports: [ChatComponent], - template: ``, -}) -export class SubagentsComponent { - protected readonly agent = injectAgent(); -} -``` - - - - -The orchestrator binds a `task` tool that dispatches a role-specific -subagent. Before running the subagent it emits a `started` payload; while -the subagent streams, `SubagentStreamHandler` forwards a `message_start` -once and then one `message` per token (the raw delta); after it emits -`finished` — or `error` if the child fails — all keyed by the tool's own -call id: - -```python -# graph.py -from langchain_core.callbacks import adispatch_custom_event -from langchain_core.tools import tool, InjectedToolCallId -from src.streaming.subagent_stream_handler import SubagentStreamHandler - -@tool -async def task(role, task_description, tool_call_id: Annotated[str, InjectedToolCallId]): - await adispatch_custom_event( - "subagent_activity", - {"subagent_id": tool_call_id, "phase": "started", "name": role}, - ) - try: - result = await _run_subagent( - role, task_description, - config={"callbacks": [SubagentStreamHandler(tool_call_id)]}, - ) - except Exception as exc: - await adispatch_custom_event( - "subagent_activity", - {"subagent_id": tool_call_id, "phase": "error", "message": str(exc)}, - ) - raise - await adispatch_custom_event( - "subagent_activity", - {"subagent_id": tool_call_id, "phase": "finished"}, - ) - return result -``` - - - - -The backend wraps the LangGraph `graph` in a FastAPI app using -`ag-ui-langgraph`. `SubagentEmittingAgent` subclasses the bridge's -`LangGraphAgent` and wraps its `run()` generator, expanding each -`subagent_activity` CUSTOM event into the protocol's standard events (ids -derived from the `task` tool call id, `tid`): - -| phase | wire event | -| --- | --- | -| `started {name}` | `SUBAGENT_STARTED {subagentRunId: -sub, name, parentToolCallId: }` | -| `message_start {message_id}` | `TEXT_MESSAGE_START {messageId: -sub-m1, role: assistant, subagentRunId}` | -| `message {message_id, delta}` | `TEXT_MESSAGE_CONTENT {messageId, delta, subagentRunId}` | -| `finished` | `TEXT_MESSAGE_END` for the open message, then `SUBAGENT_FINISHED {outcome: success}` | -| `error {message}` | `TEXT_MESSAGE_END` for the open message, then `SUBAGENT_ERROR {message}` | - -The CUSTOM event itself is consumed; every other bridge event passes through -untouched. Because `parentToolCallId` equals the bridge-native -`TOOL_CALL_START.toolCallId` (which is fully streamed before the tool body -runs), the reducer anchors the card to the `task` call with no bookkeeping: - -```python -# server.py -from fastapi import FastAPI -from ag_ui_langgraph import add_langgraph_fastapi_endpoint -from .graph import graph -from .streaming.subagent_emitting_agent import SubagentEmittingAgent - -app = FastAPI(title="cockpit-ag-ui-subagents") -add_langgraph_fastapi_endpoint( - app, SubagentEmittingAgent(name="subagents", graph=graph), path="/agent" -) - -@app.get("/ok") -def ok() -> dict: - return {"ok": True} -``` - -Run the backend with: - -```bash -uv run uvicorn src.server:app --port 5326 -``` - - -A checkpointer is required for `ag-ui-langgraph` to work. Without it, the library cannot call `graph.aget_state()`. The graph in `src/graph.py` uses `MemorySaver` for development. - - - -The AG-UI encoder only serializes pydantic `ag_ui.core` event classes — yielding a raw dict crashes the stream. `SubagentEmittingAgent` constructs `SubagentStartedEvent`, `TextMessageStartEvent`, and friends from `ag_ui.core` (`ag-ui-protocol>=0.1.22`, the first release with `subagent_run_id` on every event). - - - - - - -The `` component renders `` only while a subagent is in a running state. Under instant replay the run can settle within a single frame, so the live card transits below a render frame — assert on the durable `agent.subagents()` projection (or the tool-call card) rather than the live card element. - - - -Never expose your LangSmith API key in client-side code. Use server-side environment variables or a proxy. - - - -- [Chat Subagents](/chat/core-capabilities/subagents/overview/python) — The LangGraph variant of this pattern using the chat-subagents primitive directly -- [AG-UI Streaming](/ag-ui/core-capabilities/streaming/overview/python) — The minimal streaming pattern using the AG-UI adapter - diff --git a/deployments/ag-ui-dev/deps/subagents/src/graph.py b/deployments/ag-ui-dev/deps/subagents/src/graph.py index 68ca9c542..2f9d03c8a 100644 --- a/deployments/ag-ui-dev/deps/subagents/src/graph.py +++ b/deployments/ag-ui-dev/deps/subagents/src/graph.py @@ -107,6 +107,7 @@ async def generate_title(state: MessagesState, config) -> dict: concise.""" +# region run-subagent async def _run_subagent( role: str, task_description: str, @@ -137,8 +138,10 @@ async def _run_subagent( ] return "\n".join(parts) return "" +# endregion +# region task-tool @tool async def task( role: Literal["research", "booking", "itinerary"], @@ -194,8 +197,10 @@ async def _emit(payload: dict) -> None: raise await _emit({"phase": "finished", "status": "complete"}) return result +# endregion +# region graph def build_subagents_graph(): """Orchestrator LLM with a single `task` tool that dispatches subagents.""" llm = ChatOpenAI(model="gpt-5-mini", streaming=True).bind_tools([task]) @@ -225,6 +230,7 @@ def should_continue(state: MessagesState) -> str: graph.add_edge("tools", "orchestrator") graph.add_edge("generate_title", END) return graph.compile(checkpointer=MemorySaver()) +# endregion # The graph instance — referenced by server.py diff --git a/deployments/ag-ui-dev/deps/tool_views/docs/guide.md b/deployments/ag-ui-dev/deps/tool_views/docs/guide.md deleted file mode 100644 index 249d248e1..000000000 --- a/deployments/ag-ui-dev/deps/tool_views/docs/guide.md +++ /dev/null @@ -1,92 +0,0 @@ -# Tool-Driven View Rendering with AG-UI and Angular - - -Render a frontend component for a tool call by reusing the `views` registry -from `@threadplane/chat`. The agent calls a tool by name and returns plain -data; the frontend owns a component keyed by that name and renders it live -from the call's arguments, result, and status — no UI spec crosses the wire. - - - -Render a custom component for a tool call using `@threadplane/chat`. Register the component in the `views` map keyed by the tool's name, pass `views` to the `` component, and call the matching tool by name from your LangGraph agent. The chat composition bridges the tool call into the render pipeline automatically. - - - - - -Build a `views` registry whose key matches the tool the agent calls: - -```typescript -// tool-views.component.ts -import { views } from '@threadplane/chat'; -import { WeatherCardComponent } from './weather-card.component'; - -readonly views = views({ weather_card: WeatherCardComponent }); -``` - -The key (`weather_card`) is both the registry key and the tool name the -agent calls — one identifier, one mental model. - - - - -```html - -``` - -When a tool call's name matches a `views` key, the chat composition renders -the registered component inline in the transcript instead of the default -tool card. - - - - -The component declares an input per field it renders. It receives the live -arguments while the call streams and the merged result on completion, plus a -`status` of `'running'` or `'complete'`: - -```typescript -// weather-card.component.ts (excerpt) -readonly location = input(); -readonly temperatureF = input(); -readonly status = input<'running' | 'complete'>(); -``` - - - - -The LangGraph agent binds a tool whose name matches the registered view and -returns plain JSON. The tool call and result travel over AG-UI's -`TOOL_CALL_*` events: - -```python -# graph.py -@tool -async def weather_card(location: str) -> dict: - return {"location": location, "temperatureF": 68, "conditions": "Sunny", - "humidity": 55, "windMph": 8} -``` - -Run the backend with: - -```bash -uv run uvicorn src.server:app --port 5322 -``` - - -A checkpointer is required for `ag-ui-langgraph` to call `graph.aget_state()`. -The graph in `src/graph.py` uses `MemorySaver` for development. - - - - - - -The same `views` registry powers backend-sent render specs too — a component -you register here is reusable across tool-driven rendering and spec rendering. - - - -- [AG-UI Streaming](/ag-ui/core-capabilities/streaming/overview/python) — Real-time token streaming with the AG-UI adapter -- [AG-UI Interrupts](/ag-ui/core-capabilities/interrupts/overview/python) — Human-in-the-loop approval using the AG-UI adapter - diff --git a/deployments/ag-ui-dev/deps/tool_views/src/graph.py b/deployments/ag-ui-dev/deps/tool_views/src/graph.py index 23876dde5..18dbee8fc 100644 --- a/deployments/ag-ui-dev/deps/tool_views/src/graph.py +++ b/deployments/ag-ui-dev/deps/tool_views/src/graph.py @@ -22,6 +22,7 @@ PROMPTS_DIR = Path(__file__).parent.parent / "prompts" +# region weather-tool @tool async def weather_card(location: str) -> dict: """Look up the current weather for a location. @@ -43,11 +44,13 @@ async def weather_card(location: str) -> dict: "humidity": 55, "windMph": 8, } +# endregion _TOOLS = [weather_card] +# region graph def build_tool_views_graph(): llm = ChatOpenAI(model="gpt-5-mini", streaming=True).bind_tools(_TOOLS) @@ -69,6 +72,7 @@ def route(state: MessagesState) -> str: graph.add_edge("tools", "agent") return graph.compile(checkpointer=MemorySaver()) +# endregion # The graph instance — referenced by server.py diff --git a/docs/superpowers/plans/2026-09-06-docs-example-first-products.md b/docs/superpowers/plans/2026-09-06-docs-example-first-products.md new file mode 100644 index 000000000..bd3ca0310 --- /dev/null +++ b/docs/superpowers/plans/2026-09-06-docs-example-first-products.md @@ -0,0 +1,102 @@ +# Example-first docs — remaining products (PRs 3–8) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rewrite the 33 remaining mapped docs pages (ag-ui 6, render 6, deep-agents 5, runtimes 3, a2ui 1, chat 12) so each teaches through its running example via ``, absorb and delete their walkthroughs, and finish with the walkthrough retirement guard. + +**Architecture:** Same contract as the LangGraph pilot (`2026-09-06-docs-example-first-langgraph-pilot.md`, merged as #1029): the page procedure below is copied from it and amended with what the pilot taught. One PR per product, sequential (each removes lines from the same guard file). Pages inside a product are disjoint files and may be implemented in parallel; the product's `PENDING_PAGES` lines are removed in one commit at the START of the product PR so page agents never edit the guard file. + +**Spec:** `docs/superpowers/specs/2026-09-05-docs-example-first-content-design.md` (§3 page shape, §5 rollout). + +**Decisions fixed (carry over from the pilot)** +- Sequential `` blocks only; never inside ``/``. +- No contractions anywhere on a rewritten page; no "cockpit" in prose (the runtime-connection helper is referred to by role: "the host that serves the demo"); no competitor names; US spelling; sentence-case `##`/`###` headings except `## What's Next`. +- Walkthrough ``/``/``/`` are dropped; ``/``/`` become `Callout` only when the page does not already say it. Walkthroughs have been wrong about checkpointers, `setBranch`, `provideAgent({ config })` and UI affordances on every pilot page: trust the code, never the walkthrough. +- Frontmatter `description:` under 180 characters, one sentence. +- Every API named in a fence is verified against `libs/*/src` before it is written; every hand-written fence is valid in its language. +- `server.py` files (ag-ui) are 13–19 lines: include whole when the page discusses the wire; otherwise mention by name. +- Region markers are comments only; inline Angular templates take ``; run `npx nx build ` after marking a template. `deployments/ag-ui-dev/deps//**` is TRACKED and generated from `cockpit/ag-ui/**`: after marking an ag-ui source run `npx tsx scripts/generate-ag-ui-deployment-config.ts` and stage ONLY `deployments/ag-ui-dev/deps//`. Other products produce no tracked deployments diff (confirm with `git status --short deployments`). +- Local prod build: `rm -rf apps/website/.next dist/apps/website/.next && GROWTH_FORM_POLICY=growth_v1 npx nx build website`; output in `dist/apps/website/.next`. +- Unit runs: `npx nx test website --skip-nx-cache` from the root (the retirement spec is cwd-coupled). + +## The page procedure + +- [ ] **P1. Read everything first.** The current page, the walkthrough(s), every example file, `docs/gtm/voice.md` lines 1–60, and two finished pilot pages (`apps/website/content/docs/langgraph/guides/persistence.mdx`, `interrupts.mdx`) for tone and heading specificity. Note which hand-written snippets duplicate example code (replace with ``) and which teach something the example lacks (keep as fences). +- [ ] **P2. Regions.** For example files over ~60 lines add `#region name` … `#endregion` pairs (`//` TS, `#` Python, `` HTML) around the parts the page discusses; kebab-case names from the real code; nesting allowed; keep line-level regions under ~40 lines. Files under ~60 lines are included whole. +- [ ] **P3. Rewrite to the shape.** Title + lead → `## What the demo does` (two to four sentences: what the Run tab shows, one or two things to try, from the component's welcome suggestions or the prompt file) → `## How it is built` with sentence-case `###` headings that name a concept, walking the example in build order (backend first, then config, then component/template), one to three sentences before each block, at most one plain sentence after (a callout is fine) → the page's own concept sections the example does not show, trimmed of snippets the example now covers → `## What's Next` (keep the CardGroup; for stub pages create one with two to four real routes under `/docs/`). +- [ ] **P4. Delete the walkthrough(s)** with `git rm`. +- [ ] **P5. Do NOT touch `apps/website/src/lib/docs-example-code.spec.ts`** (the product's lines are already removed). The guard's "includes at least one example file" case may list sibling pages still in progress; your own page must not be listed. +- [ ] **P6. Generators** if anything under `cockpit/` changed (see Decisions). +- [ ] **P7. Verify.** `cd apps/website && npx vitest run docs-example-code public-copy docs-search docs.spec` (own page not in any failure); from the root `npx nx test website --skip-nx-cache` may show only the sibling-pending failure class; `grep -n -i cockpit ` → nothing; the contraction grep → nothing but `## What's Next`; prod build and `grep -c 'data-example-file=' dist/apps/website/.next/server/app.html` equals the tag count; remove both build dirs. +- [ ] **P8. Commit** the page, marked sources, deleted walkthrough(s), and own deployments dir in ONE commit `docs(): teaches through the running example`, with the Co-Authored-By trailer. + +## Product PRs (sequential; branch `blove/docs-example-first-` from `origin/main`) + +Each product PR: (1) one commit removing the product's `PENDING_PAGES` lines; (2) page tasks in parallel, each followed by a factual review that greps the libs for every API named and a fix pass; (3) close-out: `npx nx run-many -t test,lint --projects=website,cockpit-registry,cockpit-shell,scripts --skip-nx-cache`, both generators with no unstaged drift, prod build with per-page tag counts, `npx nx e2e website --skip-nx-cache`; a final cross-page read for consistency; PR with auto-merge. + +### PR 3: ag-ui (6 pages) +| page | mdx lines | walkthrough | assets (lines) | +| --- | --- | --- | --- | +| `/docs/ag-ui/guides/client-tools` | 10 (stub) | `cockpit/ag-ui/client-tools/python/docs/guide.md` (262) | `client-tools.component.ts` 87, `weather-card.component.ts` 66, `confirm-booking.component.ts` 69, `app.config.ts` 20, `graph.py` 55, `server.py` 13 | +| `/docs/ag-ui/guides/interrupts` | 169 | `cockpit/ag-ui/interrupts/python/docs/guide.md` (140) | `interrupts.component.ts` 216, `app.config.ts` 20, `graph.py` 122, `server.py` 13 | +| `/docs/ag-ui/guides/json-render` | 10 (stub) | `cockpit/ag-ui/json-render/python/docs/guide.md` (127) | `json-render.component.ts` 62, `app.config.ts` 20, `graph.py` 327, `server.py` 13 | +| `/docs/ag-ui/guides/subagents` | 10 (stub) | `cockpit/ag-ui/subagents/python/docs/guide.md` (170) | `subagents.component.ts` 34, `app.config.ts` 20, `graph.py` 232, `server.py` 19 | +| `/docs/ag-ui/guides/tool-views` | 10 (stub) | `cockpit/ag-ui/tool-views/python/docs/guide.md` (93) | `tool-views.component.ts` 28, `weather-card.component.ts` 52, `app.config.ts` 20, `graph.py` 76, `server.py` 13 | +| `/docs/ag-ui/reference/event-mapping` | 201 | `cockpit/ag-ui/streaming/python/docs/guide.md` (123) | `streaming.component.ts` 30, `app.config.ts` 20, `graph.py` 55, `server.py` 13 | + +Notes: the ag-ui adapter is event-driven (`@threadplane/ag-ui`); verify claims in `libs/ag-ui/src`. `event-mapping` is a reference page: keep its event table, add the example walk as the worked case. Two `weather-card.component.ts` basenames exist across capabilities but each page's asset list has only its own, so basenames resolve. + +### PR 4: render (6 pages) +| page | mdx | walkthrough | assets | +| --- | --- | --- | --- | +| `/docs/render/api/provide-render` | 221 | `cockpit/render/computed-functions/python/docs/guide.md` (105) | `computed-functions.component.ts` 349, `app.config.ts` 16, `graph.py` 40 | +| `/docs/render/api/render-spec-component` | 194 | `cockpit/render/element-rendering/python/docs/guide.md` (102) | `element-rendering.component.ts` 432, `app.config.ts` 9, `graph.py` 41 | +| `/docs/render/guides/registry` | 290 | `cockpit/render/registry/python/docs/guide.md` (84) | `registry.component.ts` 357, `app.config.ts` 9, `graph.py` 40 | +| `/docs/render/guides/repeat-loops` | 10 (stub) | `cockpit/render/repeat-loops/python/docs/guide.md` (111) | `repeat-loops.component.ts` 436, `app.config.ts` 9, `graph.py` 40 | +| `/docs/render/guides/specs` | 327 | `cockpit/render/spec-rendering/python/docs/guide.md` (103) | `spec-rendering.component.ts` 357, `app.config.ts` 9, `graph.py` 41 | +| `/docs/render/guides/state-store` | 251 | `cockpit/render/state-management/python/docs/guide.md` (91) | `state-management.component.ts` 453, `app.config.ts` 9, `graph.py` 40 | + +Notes: the render examples have no agent (the graph is a stub, `app.config.ts` is 9 lines); the component is the whole example, so regions carry the page. The two `api/` pages are API references: keep their reference tables and add the example as the worked case. Verify against `libs/render/src`. + +### PR 5: deep-agents (5 pages) +| page | mdx | walkthrough | assets | +| --- | --- | --- | --- | +| `/docs/deep-agents/capabilities/filesystem` | 112 | `.../filesystem/python/docs/guide.md` (136) | `filesystem.component.ts` 289, `app.config.ts` 22, `graph.py` 86 | +| `/docs/deep-agents/capabilities/memory` | 125 | `.../memory/python/docs/guide.md` (127) | `memory.component.ts` 225, `app.config.ts` 22, `graph.py` 93 | +| `/docs/deep-agents/capabilities/planning` | 105 | `.../planning/python/docs/guide.md` (132) | `planning.component.ts` 192, `app.config.ts` 22, `graph.py` 94 | +| `/docs/deep-agents/capabilities/skills` | 112 | `.../skills/python/docs/guide.md` (129) | `skills.component.ts` 241, `app.config.ts` 22, `graph.py` 187 | +| `/docs/deep-agents/capabilities/subagents` | 87 | `.../subagents/python/docs/guide.md` (112) | `subagents.component.ts` 129, `app.config.ts` 30, `graph.py` 119 | + +Notes: real `deepagents` 0.7.11 (memory note `deep-agents-rebuild`); task-tool subagents need zero adapter config. Verify against `libs/langgraph/src` and the graph. + +### PR 6: runtimes (3 pages) +| page | mdx | walkthrough | assets | +| --- | --- | --- | --- | +| `/docs/runtimes/aws-strands/overview` | 54 | `cockpit/runtimes/aws-strands/python/docs/guide.md` (47) | `aws-strands.component.ts` 286, `app.config.ts` 20, `agent.py` 248, `server.py` 13 | +| `/docs/runtimes/mastra/overview` | 60 | `cockpit/runtimes/mastra/angular/docs/guide.md` (69) | `mastra.component.ts` 276, `app.config.ts` 20, `deployments/ag-ui-mastra/agents.mjs` 152, `server.mjs` 164 | +| `/docs/runtimes/microsoft-agent-framework/overview` | 62 | `.../microsoft-agent-framework/python/docs/guide.md` (39) | `microsoft-agent-framework.component.ts` 257, `app.config.ts` 20, `agent.py` 219, `server.py` 18 | + +Notes: these are portability pages (memory note `runtime-portability-matrix`); keep the matrix claims exactly as the wire-capture docs state them. Mastra's backend files live under `deployments/ag-ui-mastra/` (tracked, hand-maintained, not generated). + +### PR 7: a2ui (1 page) +| `/docs/a2ui/getting-started/introduction` | 81 | `cockpit/ag-ui/a2ui/python/docs/guide.md` (128) | `a2ui.component.ts` 41, `app.config.ts` 20, `graph.py` 849, `server.py` 13 | + +Notes: the graph is 849 lines; use tight regions (the A2UI message construction, one surface update). A2UI v0.9.1 (memory note `a2ui-v09-stable-migration`): verify props against the official schemas, never invent. + +### PR 8: chat (12 pages) — also deletes `PENDING_PAGES` and adds the retirement guard +| page | mdx | walkthrough | assets | +| --- | --- | --- | --- | +| `/docs/chat/a2ui/overview` | 296 | `cockpit/chat/a2ui/python/docs/guide.md` (129) | `a2ui.component.ts` 49, `app.config.ts` 22, `graph.py` 848 | +| `/docs/chat/components/chat-debug` | 107 | `cockpit/chat/debug/python/docs/guide.md` (64) | `debug.component.ts` 24, `app.config.ts` 22, `graph.py` 112 | +| `/docs/chat/components/chat-input` | 183 | `cockpit/chat/input/python/docs/guide.md` (74) | `input.component.ts` 188, `app.config.ts` 22, `graph.py` 92 | +| `/docs/chat/components/chat-interrupt-panel` | 174 | `cockpit/chat/interrupts/python/docs/guide.md` (97) | `interrupts.component.ts` 121, `app.config.ts` 22, `graph.py` 147 | +| `/docs/chat/components/chat-subagent-card` | 146 | `cockpit/chat/subagents/python/docs/guide.md` (88) | `subagents.component.ts` 103, `app.config.ts` 26, `graph.py` 249 | +| `/docs/chat/components/chat-tool-calls` | 70 | `cockpit/chat/tool-calls/python/docs/guide.md` (76) | `tool-calls.component.ts` 104, `app.config.ts` 22, `graph.py` 100 | +| `/docs/chat/components/chat-trace` | 213 | `cockpit/chat/timeline/python/docs/guide.md` (83) | `timeline.component.ts` 62, `app.config.ts` 22, `graph.py` 92 | +| `/docs/chat/concepts/message-model` | 274 | `cockpit/chat/messages/python/docs/guide.md` (76) | `messages.component.ts` 165, `app.config.ts` 23, `graph.py` 94 | +| `/docs/chat/guides/client-tools` | 316 | `cockpit/langgraph/client-tools/python/docs/guide.md` (220) | `client-tools.component.ts` 100, `weather-card.component.ts` 66, `confirm-booking.component.ts` 68, `app.config.ts` 23, `graph.py` 54 | +| `/docs/chat/guides/generative-ui` | 208 | `cockpit/chat/generative-ui/python/docs/guide.md` (74) | `generative-ui.component.ts` 72, `app.config.ts` 22, `graph.py` 317 | +| `/docs/chat/guides/theming` | 169 | `cockpit/chat/theming/python/docs/guide.md` (78) | `theming.component.ts` 175, `app.config.ts` 22, `graph.py` 92 | +| `/docs/chat/guides/thread-routing` | 187 | `cockpit/chat/threads/python/docs/guide.md` (89) | `threads.component.ts` 170, `app.config.ts` 38, `graph.py` 116 | + +Notes: component pages are API-shaped; keep the API tables and add the example as the worked case. Client-tools: the flush() contract (memory note `client-tool-flush-contract`). Close-out for PR 8 additionally: delete `PENDING_PAGES` and its "lists only mapped pages as pending" case (keep the lower-bound case), and add to `apps/website/src/lib/cockpit-retirement.spec.ts` two cases: no file matches `cockpit/**/docs/guide.md`, and no descriptor in `capabilityModules` has a `docsAssetPaths` key.