From 7522be995e253fd7aba7521c5575f59db2b48e66 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 6 Jul 2026 12:04:44 -0700 Subject: [PATCH] docs: deepen package coverage --- apps/website/content/AGENTS.md.template | 16 +- apps/website/content/CLAUDE.md.template | 16 +- .../docs/ag-ui/concepts/architecture.mdx | 6 +- .../ag-ui/getting-started/introduction.mdx | 6 +- .../docs/ag-ui/guides/troubleshooting.mdx | 17 +- .../docs/ag-ui/reference/event-mapping.mdx | 12 +- .../content/docs/chat/a2ui/catalog.mdx | 27 +- .../content/docs/chat/a2ui/overview.mdx | 16 +- .../docs/chat/a2ui/surface-component.mdx | 6 +- .../content/docs/chat/a2ui/surface-store.mdx | 40 +- .../docs/chat/components/chat-popup.mdx | 23 +- .../docs/chat/components/chat-sidebar.mdx | 24 +- .../content/docs/chat/components/chat.mdx | 2 +- .../content/docs/chat/guides/markdown.mdx | 85 +--- .../content/docs/chat/guides/theming.mdx | 2 +- .../docs/langgraph/api/inject-agent.mdx | 18 +- .../api/langgraph-threads-adapter.mdx | 70 +++ .../langgraph/getting-started/quickstart.mdx | 4 +- .../getting-started/introduction.mdx | 8 +- .../licensing/getting-started/quickstart.mdx | 6 +- .../content/docs/licensing/guides/setup.mdx | 2 +- .../content/docs/licensing/reference/api.mdx | 10 + .../content/docs/middleware/api/api-docs.json | 476 ++++++++++++++++++ .../middleware/api/client-tool-helpers.mdx | 17 + .../getting-started/introduction.mdx | 59 +++ .../middleware/getting-started/quickstart.mdx | 98 ++++ .../guides/langgraph-client-tools.mdx | 99 ++++ .../render/api/define-angular-registry.mdx | 27 +- .../content/docs/render/guides/events.mdx | 8 +- .../content/docs/render/guides/registry.mdx | 30 +- .../content/docs/telemetry/api/api-docs.json | 364 ++++++++++++++ .../getting-started/installation.mdx | 66 +++ apps/website/content/prompts/configuration.md | 14 +- .../content/prompts/getting-started.md | 4 +- apps/website/content/prompts/streaming.md | 4 +- apps/website/content/prompts/testing.md | 5 +- .../content/prompts/thread-persistence.md | 4 +- apps/website/public/AGENTS.md | 18 +- apps/website/public/CLAUDE.md | 18 +- apps/website/scripts/generate-api-docs.ts | 19 +- .../docs/[library]/[section]/[slug]/page.tsx | 14 +- apps/website/src/app/docs/page.tsx | 19 +- apps/website/src/app/llms-full.txt/route.ts | 2 + .../src/components/docs/LibraryMark.tsx | 13 +- apps/website/src/lib/docs-config.ts | 47 ++ apps/website/src/lib/docs.spec.ts | 62 ++- apps/website/src/lib/site-metadata.ts | 5 +- 47 files changed, 1699 insertions(+), 209 deletions(-) create mode 100644 apps/website/content/docs/langgraph/api/langgraph-threads-adapter.mdx create mode 100644 apps/website/content/docs/middleware/api/api-docs.json create mode 100644 apps/website/content/docs/middleware/api/client-tool-helpers.mdx create mode 100644 apps/website/content/docs/middleware/getting-started/introduction.mdx create mode 100644 apps/website/content/docs/middleware/getting-started/quickstart.mdx create mode 100644 apps/website/content/docs/middleware/guides/langgraph-client-tools.mdx create mode 100644 apps/website/content/docs/telemetry/getting-started/installation.mdx diff --git a/apps/website/content/AGENTS.md.template b/apps/website/content/AGENTS.md.template index 0cde15bcb..19a54256c 100644 --- a/apps/website/content/AGENTS.md.template +++ b/apps/website/content/AGENTS.md.template @@ -6,7 +6,7 @@ Production-ready chat, durable threads, interrupts, subagents, planning, memory, npm install @threadplane/chat @threadplane/langgraph ## Key requirement -`agent()` MUST be called within an Angular injection context (component constructor or field initializer). Calling it in ngOnInit or any async context throws "NG0203: inject() must be called from an injection context". +`injectAgent()` MUST be called within an Angular injection context (component constructor or field initializer). Calling it in ngOnInit or any async context throws "NG0203: inject() must be called from an injection context". ## Basic usage ```typescript @@ -14,12 +14,12 @@ npm install @threadplane/chat @threadplane/langgraph import type { ApplicationConfig } from '@angular/core'; import { provideAgent } from '@threadplane/langgraph'; export const appConfig: ApplicationConfig = { - providers: [provideAgent({ apiUrl: 'http://localhost:2024' })] + providers: [provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'chat_agent' })] }; // chat.component.ts import { Component } from '@angular/core'; -import { agent } from '@threadplane/langgraph'; +import { injectAgent } from '@threadplane/langgraph'; import { ChatComponent as ThreadplaneChatComponent } from '@threadplane/chat'; @Component({ @@ -29,15 +29,15 @@ import { ChatComponent as ThreadplaneChatComponent } from '@threadplane/chat'; `, }) export class ChatComponent { - chat = agent({ apiUrl: 'http://localhost:2024', assistantId: 'chat_agent' }); + chat = injectAgent(); } ``` ## Key patterns -- Thread persistence: `threadId: signal(localStorage.getItem('t'))` + `onThreadId: (id) => localStorage.setItem('t', id)` -- Global config: `provideAgent({ apiUrl })` in app.config.ts -- Per-call override: pass `apiUrl` directly to `agent()` -- Testing: use `MockAgentTransport` — never mock `agent()` itself +- Thread persistence: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })` +- Global config: `provideAgent({ apiUrl, assistantId })` in app.config.ts +- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree +- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself ## Version check If this file is stale, fetch the latest: https://threadplane.ai/llms-full.txt diff --git a/apps/website/content/CLAUDE.md.template b/apps/website/content/CLAUDE.md.template index 0cde15bcb..19a54256c 100644 --- a/apps/website/content/CLAUDE.md.template +++ b/apps/website/content/CLAUDE.md.template @@ -6,7 +6,7 @@ Production-ready chat, durable threads, interrupts, subagents, planning, memory, npm install @threadplane/chat @threadplane/langgraph ## Key requirement -`agent()` MUST be called within an Angular injection context (component constructor or field initializer). Calling it in ngOnInit or any async context throws "NG0203: inject() must be called from an injection context". +`injectAgent()` MUST be called within an Angular injection context (component constructor or field initializer). Calling it in ngOnInit or any async context throws "NG0203: inject() must be called from an injection context". ## Basic usage ```typescript @@ -14,12 +14,12 @@ npm install @threadplane/chat @threadplane/langgraph import type { ApplicationConfig } from '@angular/core'; import { provideAgent } from '@threadplane/langgraph'; export const appConfig: ApplicationConfig = { - providers: [provideAgent({ apiUrl: 'http://localhost:2024' })] + providers: [provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'chat_agent' })] }; // chat.component.ts import { Component } from '@angular/core'; -import { agent } from '@threadplane/langgraph'; +import { injectAgent } from '@threadplane/langgraph'; import { ChatComponent as ThreadplaneChatComponent } from '@threadplane/chat'; @Component({ @@ -29,15 +29,15 @@ import { ChatComponent as ThreadplaneChatComponent } from '@threadplane/chat'; `, }) export class ChatComponent { - chat = agent({ apiUrl: 'http://localhost:2024', assistantId: 'chat_agent' }); + chat = injectAgent(); } ``` ## Key patterns -- Thread persistence: `threadId: signal(localStorage.getItem('t'))` + `onThreadId: (id) => localStorage.setItem('t', id)` -- Global config: `provideAgent({ apiUrl })` in app.config.ts -- Per-call override: pass `apiUrl` directly to `agent()` -- Testing: use `MockAgentTransport` — never mock `agent()` itself +- Thread persistence: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })` +- Global config: `provideAgent({ apiUrl, assistantId })` in app.config.ts +- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree +- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself ## Version check If this file is stale, fetch the latest: https://threadplane.ai/llms-full.txt diff --git a/apps/website/content/docs/ag-ui/concepts/architecture.mdx b/apps/website/content/docs/ag-ui/concepts/architecture.mdx index bc1b782ec..e6f119b07 100644 --- a/apps/website/content/docs/ag-ui/concepts/architecture.mdx +++ b/apps/website/content/docs/ag-ui/concepts/architecture.mdx @@ -189,15 +189,15 @@ The AG-UI adapter currently covers: - Shared state from `STATE_SNAPSHOT` and `STATE_DELTA`. - Message replacement from `MESSAGES_SNAPSHOT`. - Custom events from `CUSTOM`. +- Interrupts from `CUSTOM` events named `on_interrupt`. +- Subagent/activity progress from `ACTIVITY_SNAPSHOT` and `ACTIVITY_DELTA`. - Citations stored under `state.citations`. These features are intentionally out of scope for the AG-UI adapter today: -- Interrupt workflows. -- Subagents. - **History and time-travel.** AG-UI is an event-stream protocol — it doesn't define a server-side "fetch state of thread X" endpoint, so the adapter can't hydrate prior messages on a `threadId` change the way a checkpoint-aware runtime can. The [Provider choices](#provider-choices) section above describes the two patterns AG-UI consumers use to work around this. -If those are central to your product, use the LangGraph adapter for that surface or build a custom adapter against the `@threadplane/chat` `Agent` contract. The [Writing an Adapter guide](/docs/chat/guides/writing-an-adapter#hydrating-from-a-server-stored-thread) walks through the thread-loading design choice in detail. +If server-side history or time-travel is central to your product, use the LangGraph adapter for that surface or build a custom adapter against the `@threadplane/chat` `Agent` contract. The [Writing an Adapter guide](/docs/chat/guides/writing-an-adapter#hydrating-from-a-server-stored-thread) walks through the thread-loading design choice in detail. ## Next steps diff --git a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx index a567ccfb6..fb3873144 100644 --- a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx @@ -42,10 +42,10 @@ Here's what the first release handles: - `toolCalls` (streaming tool calls via `TOOL_CALL_*` events) - `state` (snapshots and JSON-Patch deltas) - `events$` (custom events; discriminates `state_update`) +- Interrupts (from `CUSTOM` events named `on_interrupt`) +- Subagent/activity progress (from `ACTIVITY_*` events) -Out of scope for now (use `@threadplane/langgraph` if you need these): -- Interrupts -- Subagents +Out of scope for now (use `@threadplane/langgraph` if you need LangGraph Platform-specific APIs): - History / time-travel ## Next steps diff --git a/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx b/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx index f5d13dc5b..fd7d89d75 100644 --- a/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx +++ b/apps/website/content/docs/ag-ui/guides/troubleshooting.mdx @@ -77,21 +77,22 @@ agent.error(); ## Tool call args are empty -`TOOL_CALL_ARGS` parses the event `delta` as JSON. +`TOOL_CALL_ARGS` appends each event `delta` to the tool call's accumulated argument buffer. When the accumulated buffer is valid JSON, the reducer stores the parsed object as the current args. -This works: +This can arrive as one complete payload: ```ts { type: 'TOOL_CALL_ARGS', toolCallId: 't1', delta: '{"query":"Angular"}' } ``` -This becomes `{}`: +Or as fragments: ```ts { type: 'TOOL_CALL_ARGS', toolCallId: 't1', delta: '{"query":' } +{ type: 'TOOL_CALL_ARGS', toolCallId: 't1', delta: '"Angular"}' } ``` -The current reducer replaces args with each parsed payload. It does not assemble partial JSON fragments across multiple `TOOL_CALL_ARGS` events. Emit complete JSON for each args event. +While the buffer is incomplete, the previously parsed args remain in place. If the final buffer still cannot be parsed at `TOOL_CALL_END`, the adapter leaves the last-good parsed args instead of throwing. ## Citations do not show up @@ -132,13 +133,13 @@ Cancellation depends on the AG-UI source implementation. `HttpAgent` supports ab Pass the index of the assistant message you want to replace, not the user message. -## Interrupts, subagents, or history do not work +## History or time-travel does not work -Those flows are not implemented by the AG-UI adapter today. +History and time-travel are not implemented by the AG-UI adapter today. -Current scope is messages, status/loading/error, tool calls, state/custom events, reasoning messages, message snapshots, and citations from state. +Current scope is messages, status/loading/error, tool calls, state/custom events, reasoning messages, message snapshots, interrupts from `CUSTOM` `on_interrupt` events, subagent/activity progress from `ACTIVITY_*` events, and citations from state. -Use `@threadplane/langgraph` for the richer LangGraph-specific surface, or write a custom adapter against the `@threadplane/chat` `Agent` contract if you need AG-UI plus product-specific behavior. +Use `@threadplane/langgraph` if you need LangGraph Platform thread history APIs, or write a custom adapter against the `@threadplane/chat` `Agent` contract if you need AG-UI plus product-specific behavior. ## Isolate with FakeAgent 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 dfcae1bba..3f2eb3543 100644 --- a/apps/website/content/docs/ag-ui/reference/event-mapping.mdx +++ b/apps/website/content/docs/ag-ui/reference/event-mapping.mdx @@ -19,7 +19,7 @@ This page is the compatibility map. If your backend emits these events with the | `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` | Parses `delta` as JSON and replaces args on the matching tool call. | +| `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`. | @@ -113,7 +113,9 @@ The resulting tool call is: } ``` -`TOOL_CALL_ARGS` expects the `delta` value to be parseable JSON. If parsing fails, args become `{}`. The current reducer replaces args with each parsed payload; it does not merge partial JSON fragments. +`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 @@ -171,6 +173,10 @@ If `message` is omitted, no user message is appended, but `runAgent()` still run `agent.stop()` calls `source.abortRun()`. Cancellation depends on the AG-UI source implementation. +## Activity and subagents + +`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. + ## Unsupported protocol areas -Beyond the `on_interrupt` -> `interrupt` mapping documented above (and its `submit({ resume })` reply), the adapter implements no richer interrupt UI flow — no subagents, history, or time-travel. Unknown protocol events are ignored rather than treated as errors. +The adapter supports `CUSTOM` `on_interrupt` events for the runtime-neutral `interrupt()` signal, and it supports ACTIVITY-backed subagent progress. It does not implement history or time-travel. Unknown protocol events are ignored rather than treated as errors. diff --git a/apps/website/content/docs/chat/a2ui/catalog.mdx b/apps/website/content/docs/chat/a2ui/catalog.mdx index 065ed3ce3..27c07d89e 100644 --- a/apps/website/content/docs/chat/a2ui/catalog.mdx +++ b/apps/website/content/docs/chat/a2ui/catalog.mdx @@ -132,7 +132,7 @@ Renders children in a scrollable vertical list (max height 24rem). | `spec` | `Spec` | Injected automatically by the render engine | -For data-driven lists, use the `A2uiChildTemplate` form instead of static `childKeys`. Set `children` to `{"path": "/items", "componentId": "item-template"}` and the surface component will expand the template once per array item. See the [Surface Component](/docs/chat/a2ui/surface-component) page for details. +For data-driven lists, use the `A2uiChildTemplate` form instead of static `childKeys`. Set `children` to `{"template": {"componentId": "item-template", "dataBinding": "/items"}}` and the surface component will expand the template once per array item. See the [Surface Component](/docs/chat/a2ui/surface-component) page for details. ## Interactive Components @@ -155,8 +155,8 @@ Renders a button that dispatches an action when clicked. | `childKeys` | `string[]` | Child component IDs whose rendered output is the button's content (e.g., a `Text` label) | | `primary` | `boolean` | Renders the primary visual style. Defaults to `true` | | `disabled` | `boolean` | Disables the button when `true` | -| `action` | `A2uiAction` | Action to execute on click (event or function call) | -| `validationResult` | `A2uiValidationResult` | Pre-computed validation result — button is disabled if `valid` is `false` | +| `action` | `A2uiAction` | Agent-bound action to emit on click | +| `validationResult` | `A2uiValidationResult` | Pre-computed validation result. The button is disabled if `valid` is `false` | | `emit` | injected | Event emitter provided by the render engine | @@ -166,18 +166,19 @@ The Angular `A2uiButtonComponent` input is `childKeys` (an array) — that's the **Action types:** ```json -// Emit a named event with resolved context (sent back to the agent as v1 action) -{"action": {"event": {"name": "submit", "context": {"email": {"path": "/email"}, "formId": "contact"}}}} - -// Execute a local function (e.g., open a URL) — agent never sees this -{"action": {"functionCall": {"call": "openUrl", "args": {"url": "https://example.com"}}}} +// Emit a named action with resolved context (sent back to the agent as v1 action) +{ + "action": { + "name": "submit", + "context": [ + { "key": "email", "value": { "path": "/email" } }, + { "key": "formId", "value": { "literalString": "contact" } } + ] + } +} ``` -**Validation checks** reference built-in validators by name. If any check fails, the button is automatically disabled: - -```json -{"checks": [{"call": "required", "args": {"value": {"path": "/email"}}, "message": "Email is required"}]} -``` +Validation is data-driven. Send a resolved `validationResult` on the component when the backend has already evaluated validity. ### TextField diff --git a/apps/website/content/docs/chat/a2ui/overview.mdx b/apps/website/content/docs/chat/a2ui/overview.mdx index 7971699f3..dae918978 100644 --- a/apps/website/content/docs/chat/a2ui/overview.mdx +++ b/apps/website/content/docs/chat/a2ui/overview.mdx @@ -17,9 +17,7 @@ assistant text starts with ---a2ui_JSON--- -> A2uiSurfaceComponent renders progressive state through your catalog ``` -This is why A2UI sits between chat and render. Chat owns message streaming. A2UI owns the protocol shapes. The normal chat path uses progressive surface state and `a2uiSlot` so components can appear as their required data arrives. - -There is also a compatibility path: when `A2uiSurfaceComponent` receives a `surface` without a `state`, it calls `surfaceToSpec()` and renders through `@threadplane/render`. That fallback is where render-spec handlers, render events, and json-render state bindings apply. +This is why A2UI sits between chat and render. Chat owns message streaming. A2UI owns the protocol shapes. `A2uiSurfaceComponent` turns the accumulated surface state into a render spec and delegates to `@threadplane/render`, so handlers, render events, and json-render state bindings use the same path for both the preferred `state` input and the legacy `surface` input. ## Message Envelopes @@ -145,7 +143,7 @@ Components use keyed union definitions: This is different from a flat `component: "Text"` shape. The current source expects the keyed union form. -This stream demonstrates the protocol boundary, not a guarantee that every built-in catalog component is fully wired in the progressive chat renderer. The current chat path renders surface state first and pushes resolved `A2uiComponentView.props` directly into Angular components. The richer projection work - unwrapping component definitions, mapping children, creating render state bindings, and turning actions into handlers - lives in the render-spec compatibility path. +This stream demonstrates the protocol boundary. The chat path accumulates the surface state as JSONL arrives, then `A2uiSurfaceComponent` converts the current surface into a render spec: it unwraps keyed component definitions, maps children, creates render state bindings, and turns actions into handlers. ## Data Model @@ -163,7 +161,7 @@ A2UI component props can point at the surface data model. } ``` -In the render-spec compatibility path, `surfaceToSpec()` converts path references into json-render state bindings. Catalog input components can use `emitBinding()` to write back through the render event pipeline. +`surfaceToSpec()` converts path references into json-render state bindings. Catalog input components can use `emitBinding()` to write back through the render event pipeline. ```ts import { emitBinding } from '@threadplane/chat'; @@ -189,7 +187,7 @@ Buttons carry an `A2uiAction`: } ``` -In the render-spec compatibility path, `surfaceToSpec()` turns this into a render `click` binding that calls the built-in `a2ui:event` handler. `A2uiSurfaceComponent` then emits an `A2uiActionMessage`. +`surfaceToSpec()` turns this into a render `click` binding that calls the built-in `a2ui:event` handler. `A2uiSurfaceComponent` then emits an `A2uiActionMessage`. ```json { @@ -208,11 +206,11 @@ In the render-spec compatibility path, `surfaceToSpec()` turns this into a rende If the internal surface object has `sendDataModel: true`, the emitted message also includes `metadata.a2uiClientDataModel` with the current surface data model snapshot. Streamed protocol surfaces created by the current surface store do not set that flag. -In the progressive chat path, `A2uiSurfaceComponent` renders the surface state first. Catalog components receive resolved props as Angular inputs. The current `a2uiSlot` implementation does not wire render-spec handlers, child spec context, or render events into those mounted components. Treat agent-bound action messages as a render-spec compatibility behavior unless your catalog explicitly handles its own event wiring. +Catalog components receive resolved props as Angular inputs from the render engine. Bind `(action)` when you want agent-bound events, and bind `(events)` when you want the lower-level render stream. ## Local Handlers -In the render-spec compatibility path, `A2uiSurfaceComponent` also registers an `a2ui:localAction` handler. Consumer handlers take priority, and the built-in fallback currently supports `openUrl`. +`A2uiSurfaceComponent` also registers an `a2ui:localAction` handler. Consumer handlers take priority, and the built-in fallback currently supports `openUrl`. Use local handlers for client-owned behavior. Use A2UI actions for agent-bound events. @@ -244,7 +242,7 @@ Both paths render structured UI, but they optimize for different jobs. | State | Surface data model | Spec state | | Best fit | Incremental agent-owned surfaces; protocol-level A2UI streams | One-shot rendered content | | Detection | `---a2ui_JSON---` prefix | JSON object content | -| Rendering | Progressive surface state in chat; render-spec fallback when no state is supplied | json-render spec directly | +| Rendering | Surface state converted to a render spec | json-render spec directly | Use A2UI when the agent needs to keep updating a surface and you are working at the protocol boundary. Use json-render when the agent needs a stable, directly rendered structured result. For production interaction that depends on component handlers, state bindings, and child projection, verify the exact A2UI rendering path you are using. diff --git a/apps/website/content/docs/chat/a2ui/surface-component.mdx b/apps/website/content/docs/chat/a2ui/surface-component.mdx index bef6cd45b..74f889e45 100644 --- a/apps/website/content/docs/chat/a2ui/surface-component.mdx +++ b/apps/website/content/docs/chat/a2ui/surface-component.mdx @@ -47,16 +47,14 @@ Before the spec is emitted, each component prop is evaluated against the surface - A literal value — passed through as-is - A path reference `{ path: '/some/pointer' }` — resolved via JSON Pointer against `dataModel` -- A function call `{ call: 'formatCurrency', args: { ... } }` — executed by the built-in function registry -- A template string `"Hello ${/name}"` — interpolated with values from `dataModel` **3. Map actions to `on` bindings** -The internal conversion maps each component's A2UI `action` prop into a render-spec `on` binding on the corresponding element. Event actions map to the `a2ui:event` handler, and function call actions map to the `a2ui:localAction` handler. This bridges the A2UI interaction model to the render-lib event system. +The internal conversion maps each component's A2UI `action` prop into a render-spec `on` binding on the corresponding element. Actions map to the `a2ui:event` handler, which builds an `A2uiActionMessage` for the `(action)` output. **4. Expand template children** -When a component's `children` field is an `A2uiChildTemplate` (`{ path, componentId }`), the surface component expands it over the array at `path` in the data model. Each array item gets its own cloned element with props resolved in that item's scope. +When a component's `children` field is an `A2uiChildTemplate` (`{ template: { componentId, dataBinding } }`), the surface component expands it over the array at the `dataBinding` JSON Pointer in the data model. Each array item gets its own cloned element with props resolved in that item's scope. **5. Render via RenderSpecComponent** diff --git a/apps/website/content/docs/chat/a2ui/surface-store.mdx b/apps/website/content/docs/chat/a2ui/surface-store.mdx index 01d0fc5cd..fc7cd68a2 100644 --- a/apps/website/content/docs/chat/a2ui/surface-store.mdx +++ b/apps/website/content/docs/chat/a2ui/surface-store.mdx @@ -38,7 +38,7 @@ Processes one `A2uiMessage` and updates the internal surfaces signal. All four m | Message type | Behavior | |--------------|----------| | `surfaceUpdate` | Delivers a surface's components — merges the provided components into the surface's component map by `id` so existing components are replaced and others are kept | -| `dataModelUpdate` | Applies a JSON Pointer patch to the surface's data model (see below) | +| `dataModelUpdate` | Applies typed `contents` entries to the surface's data model, optionally under a JSON Pointer `path` (see below) | | `beginRendering` | Marks the surface root and commits the buffered components and data model into a live surface | | `deleteSurface` | Removes the surface from the map entirely | @@ -63,25 +63,37 @@ The surface's `dataModel` is synchronized into the render-lib `StateStore` when ## dataModelUpdate Semantics -The `dataModelUpdate` message uses JSON Pointer (RFC 6901) paths to address values in the data model. +The `dataModelUpdate` message carries a `contents` array of typed data-model entries. When `path` is omitted or `'/'`, those entries update the root data model. When `path` points at a nested object, the entries update that nested object. -| `path` | `value` | Effect | -|--------|---------|--------| -| `undefined` or `'/'` | Object | Replaces the entire data model | -| `/some/path` | Any value | Sets `dataModel[some][path]` to `value` | -| `/some/path` | `undefined` | Deletes the value at that path | +Each entry chooses one typed value field: `valueString`, `valueNumber`, `valueBoolean`, or `valueMap`. `valueMap` is a nested array of `A2uiDataModelEntry` objects. ```json -// Replace entire model -{"dataModelUpdate": {"surfaceId": "s1", "value": {"name": "Alice", "score": 42}}} - -// Set a single field -{"dataModelUpdate": {"surfaceId": "s1", "path": "/score", "value": 99}} +// Update root fields +{ + "dataModelUpdate": { + "surfaceId": "s1", + "contents": [ + { "key": "name", "valueString": "Alice" }, + { "key": "score", "valueNumber": 42 } + ] + } +} -// Delete a field -{"dataModelUpdate": {"surfaceId": "s1", "path": "/score"}} +// Update fields under /profile +{ + "dataModelUpdate": { + "surfaceId": "s1", + "path": "/profile", + "contents": [ + { "key": "approved", "valueBoolean": true }, + { "key": "meta", "valueMap": [{ "key": "source", "valueString": "agent" }] } + ] + } +} ``` +Current merge semantics are additive. Entries replace values at their keys, but missing keys are left alone. A `dataModelUpdate` does not delete a key by omitting it. + ## Usage with createA2uiMessageParser The surface store is designed to work with `createA2uiMessageParser`, which parses raw JSONL chunks into typed `A2uiMessage` objects. diff --git a/apps/website/content/docs/chat/components/chat-popup.mdx b/apps/website/content/docs/chat/components/chat-popup.mdx index b4da573eb..4e513eb29 100644 --- a/apps/website/content/docs/chat/components/chat-popup.mdx +++ b/apps/website/content/docs/chat/components/chat-popup.mdx @@ -27,7 +27,7 @@ import { ChatPopupComponent } from '@threadplane/chat'; selector: 'app-root', standalone: true, imports: [ChatPopupComponent], - providers: [provideAgent({ assistantId: 'chat', threadId: signal(null) })], + providers: [provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'chat', threadId: signal(null) })], changeDetection: ChangeDetectionStrategy.OnPush, template: ` @@ -50,7 +50,14 @@ export class AppComponent { | Input | Type | Default | Description | |-------|------|---------|-------------| | `agent` | `Agent` | **Required** | The agent providing streaming state | +| `views` | `ViewRegistry \| undefined` | `undefined` | A2UI/json-render component registry forwarded to the inner `` | +| `clientTools` | `ClientToolRegistry \| undefined` | `undefined` | Frontend-declared client tools forwarded to the inner `` | +| `modelOptions` | `readonly ChatSelectOption[]` | `[]` | Options for the chat input model picker | +| `showModelPicker` | `boolean` | `true` | Hides the model picker when `false`, even when `modelOptions` is non-empty | +| `selectedModel` | `string` (two-way) | `''` | Current selected model value | | `open` | `boolean` (two-way) | `false` | Two-way bindable. Controls whether the chat window is open | +| `shortcut` | `string \| null` | `'k'` | Single key toggled with Cmd/Ctrl. Set to `null` to disable | +| `closeOnEscape` | `boolean` | `true` | Closes the popup when Escape is pressed | ### Outputs @@ -82,7 +89,7 @@ Control the open state from your component: `, - providers: [provideAgent({ assistantId: 'chat', threadId: signal(null) })], + providers: [provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'chat', threadId: signal(null) })], }) export class AppComponent { chatOpen = false; @@ -100,6 +107,18 @@ Project content into the window header with the `[chatHeader]` slot: ``` +## A2UI and Client Tools + +`ChatPopupComponent` forwards `views` and `clientTools` to the inner `` composition. Pass the same registries you would pass to `` when the popup needs generative UI surfaces or browser-executed tools: + +```html + +``` + ## Styling The popup uses the standard `--tplane-chat-*` token system. To adjust the launcher position: diff --git a/apps/website/content/docs/chat/components/chat-sidebar.mdx b/apps/website/content/docs/chat/components/chat-sidebar.mdx index 388f78a65..4fa3b57a4 100644 --- a/apps/website/content/docs/chat/components/chat-sidebar.mdx +++ b/apps/website/content/docs/chat/components/chat-sidebar.mdx @@ -27,7 +27,7 @@ import { ChatSidebarComponent } from '@threadplane/chat'; selector: 'app-shell', standalone: true, imports: [ChatSidebarComponent], - providers: [provideAgent({ assistantId: 'chat', threadId: signal(null) })], + providers: [provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'chat', threadId: signal(null) })], changeDetection: ChangeDetectionStrategy.OnPush, template: ` @@ -66,7 +66,13 @@ In push-content mode, `ChatSidebarComponent` uses `display: flex` on its host. T | Input | Type | Default | Description | |-------|------|---------|-------------| | `agent` | `Agent` | **Required** | The agent providing streaming state | +| `views` | `ViewRegistry \| undefined` | `undefined` | A2UI/json-render component registry forwarded to the inner `` | +| `clientTools` | `ClientToolRegistry \| undefined` | `undefined` | Frontend-declared client tools forwarded to the inner `` | +| `modelOptions` | `readonly ChatSelectOption[]` | `[]` | Options for the chat input model picker | +| `showModelPicker` | `boolean` | `true` | Hides the model picker when `false`, even when `modelOptions` is non-empty | +| `selectedModel` | `string` (two-way) | `''` | Current selected model value | | `open` | `boolean` (two-way) | `false` | Two-way bindable. Controls whether the sidebar is open | +| `closeOnEscape` | `boolean` | `true` | Closes the sidebar when Escape is pressed | | `pushContent` | `boolean` | `false` | When `true`, the sidebar shifts projected content rather than overlaying it | ### Outputs @@ -105,7 +111,7 @@ In push-content mode, `ChatSidebarComponent` uses `display: flex` on its host. T
`, - providers: [provideAgent({ assistantId: 'chat', threadId: signal(null) })], + providers: [provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'chat', threadId: signal(null) })], }) export class AppShellComponent { sidebarOpen = signal(false); @@ -113,6 +119,20 @@ export class AppShellComponent { } ``` +## A2UI and Client Tools + +`ChatSidebarComponent` forwards `views` and `clientTools` to the inner ``. Use these inputs when the sidebar should render A2UI surfaces or expose browser-declared tools to the agent: + +```html + +
+
+``` + ## Styling Override the sidebar width using a CSS custom property: diff --git a/apps/website/content/docs/chat/components/chat.mdx b/apps/website/content/docs/chat/components/chat.mdx index a90b78318..0eec35804 100644 --- a/apps/website/content/docs/chat/components/chat.mdx +++ b/apps/website/content/docs/chat/components/chat.mdx @@ -79,7 +79,7 @@ export class ChatPageComponent { | `agent` | `Agent` | **Required** | The runtime-neutral agent providing streaming state. `injectAgent()` from `@threadplane/langgraph` returns a compatible `LangGraphAgent`. | | `views` | `ViewRegistry \| undefined` | `undefined` | View registry for generative UI. Maps spec type names to Angular components. Created with `views()` from `@threadplane/chat`. | | `store` | `StateStore \| undefined` | `undefined` | Optional state store for interactive generative UI specs. | -| `handlers` | `Record) => unknown \| Promise>` | `{}` | Event handlers for generative UI specs and A2UI `functionCall` actions. Handlers run in Angular injection context — `inject()` is available inside handler functions. | +| `handlers` | `Record) => unknown \| Promise>` | `{}` | Event handlers for generative UI specs and consumer-owned A2UI local actions. Handlers run in Angular injection context — `inject()` is available inside handler functions. | | `threads` | `Thread[]` | `[]` | List of threads to display in the sidebar. Each thread must have an `id` property. | | `activeThreadId` | `string` | `''` | The ID of the currently active thread, used for highlighting in the sidebar. | | `welcomeDisabled` | `boolean` | `false` | When `true`, suppresses the welcome screen shown for an empty conversation. | diff --git a/apps/website/content/docs/chat/guides/markdown.mdx b/apps/website/content/docs/chat/guides/markdown.mdx index 9b88b19d9..15599ee29 100644 --- a/apps/website/content/docs/chat/guides/markdown.mdx +++ b/apps/website/content/docs/chat/guides/markdown.mdx @@ -1,13 +1,13 @@ # Markdown Rendering -AI messages in `@threadplane/chat` can render full markdown -- headings, code blocks, tables, lists, blockquotes, and inline formatting. The `renderMarkdown()` utility does the work, and `CHAT_MARKDOWN_STYLES` handles the styling. +AI messages in `@threadplane/chat` can render full markdown -- headings, code blocks, tables, lists, blockquotes, math, HTML nodes, and inline formatting. Use `` for the live node-based renderer. Use `renderMarkdown()` only when you need a small sanitized-HTML helper in a custom template. ## How It Works The markdown pipeline has two stages: -1. **Parse**: The `renderMarkdown()` function converts markdown text to sanitized HTML using the `marked` library (dynamically imported). If `marked` is not installed, it falls back to plain text with `
` newline conversion. -2. **Style**: The `CHAT_MARKDOWN_STYLES` constant provides CSS rules scoped under the `.chat-md` class, targeting all common markdown elements. +1. **Streaming renderer**: `` parses markdown into node keys and renders those keys through `cacheplaneMarkdownViews`. +2. **Sanitized HTML helper**: `renderMarkdown()` converts markdown text to sanitized HTML using `marked` when it is installed, falling back to escaped plain text with `
` newline conversion. ## The renderMarkdown() Function @@ -44,79 +44,48 @@ function renderMarkdown(content: string, sanitizer: DomSanitizer): SafeHtml The `marked` library is loaded via a dynamic `import('marked')` at module initialization time. This means it does not block initial bundle loading and resolves before the first render in most cases.
-## CHAT_MARKDOWN_STYLES - -The `CHAT_MARKDOWN_STYLES` constant provides CSS rules for all standard markdown elements. It targets the `.chat-md` class using `::ng-deep` for view encapsulation compatibility. - -```typescript -import { CHAT_MARKDOWN_STYLES } from '@threadplane/chat'; - -@Component({ - styles: [CHAT_MARKDOWN_STYLES], - // ... -}) -export class MyComponent {} -``` - -### Styled Elements - -The stylesheet covers the following markdown elements: - -| Element | Styling | -|---------|---------| -| `p` | Bottom margin of `0.75em`, last child has no bottom margin | -| `code` (inline) | Background `var(--tplane-chat-surface-alt)`, padding, `4px` radius, monospace font | -| `pre` | Background `var(--tplane-chat-surface-alt)`, `12px 16px` padding, horizontal scroll | -| `pre code` | No extra background or padding (inherits from `pre`) | -| `ul`, `ol` | `0.5em` vertical margin, `1.5em` left padding | -| `li` | `0.25em` vertical margin | -| `a` | Text color with underline | -| `strong` | Font weight `600` | -| `blockquote` | Left border `3px solid`, left padding `12px`, muted text color | -| `h1` | `1.25em` font size, weight `600` | -| `h2` | `1.125em` font size, weight `600` | -| `h3` | `1em` font size, weight `600` | -| `table` | Collapsed borders, full width | -| `th` | Alt background, bold, `0.875em` | -| `td` | Standard border and padding | - -All colors reference `--tplane-chat-*` CSS custom properties, so markdown elements automatically respect the active chat theme. - ## Using Markdown in Custom Components -Let's render markdown in a custom message template. Apply both the styles and the `.chat-md` class: +For custom message templates, prefer `` so you get the same node registry, math, citation, table, and code-block behavior as the built-in chat components: ```typescript -import { Component, inject } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; import { + ChatStreamingMdComponent, ChatMessageListComponent, MessageTemplateDirective, - CHAT_MARKDOWN_STYLES, - renderMarkdown, } from '@threadplane/chat'; @Component({ selector: 'app-chat-view', standalone: true, - imports: [ChatMessageListComponent, MessageTemplateDirective], - styles: [CHAT_MARKDOWN_STYLES], + imports: [ChatMessageListComponent, MessageTemplateDirective, ChatStreamingMdComponent], template: ` -
+
`, }) export class ChatViewComponent { - private sanitizer = inject(DomSanitizer); - // chatRef = injectAgent(); // with provideAgent({...}) in the component's providers +} +``` + +If you only need sanitized HTML, call `renderMarkdown()` directly and provide your own styling for the host element: + +```typescript +import { Component, inject } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { renderMarkdown } from '@threadplane/chat'; +@Component({ + selector: 'app-markdown-html', + template: `
`, +}) +export class MarkdownHtmlComponent { + private sanitizer = inject(DomSanitizer); + content = ''; renderMd(content: string | unknown) { if (typeof content !== 'string') return ''; return renderMarkdown(content, this.sanitizer); @@ -124,10 +93,6 @@ export class ChatViewComponent { } ``` - -The markdown styles are scoped to `.chat-md`. Make sure the container element receiving `[innerHTML]` has this class, otherwise the rendered HTML will appear unstyled. - - ## Streaming Markdown with chat-streaming-md `` is the component that renders AI message content token-by-token using the node-based rendering pipeline. It resolves each markdown node type against `MARKDOWN_VIEW_REGISTRY` — a chat-internal DI token exported from `@threadplane/chat`. @@ -216,6 +181,10 @@ The most common mistake is providing `'code'` as an override key — it does not | `'strong'` | Bold emphasis (``) | | `'strikethrough'` | Strikethrough text (``) | | `'inline-code'` | Inline code span (``) | +| `'math-inline'` | Inline math | +| `'math-display'` | Display math block | +| `'html-inline'` | Inline HTML node | +| `'html-block'` | Block HTML node | | `'link'` | Hyperlink (``) | | `'autolink'` | Auto-detected URL or email link | | `'image'` | Image (``) | diff --git a/apps/website/content/docs/chat/guides/theming.mdx b/apps/website/content/docs/chat/guides/theming.mdx index e4f6af13b..09dcb746f 100644 --- a/apps/website/content/docs/chat/guides/theming.mdx +++ b/apps/website/content/docs/chat/guides/theming.mdx @@ -135,5 +135,5 @@ If you previously customized `--chat-*` tokens, rename them to `--tplane-chat-*` | `--chat-success` | `--tplane-chat-success` | -The `CHAT_THEME_STYLES` named export no longer exists in `@threadplane/chat`. Remove imports of that constant - theme tokens are now applied automatically by each component. `CHAT_MARKDOWN_STYLES` remains available for custom markdown renderers. +The `CHAT_THEME_STYLES` and `CHAT_MARKDOWN_STYLES` named exports do not exist in `@threadplane/chat`. Remove imports of those constants - theme tokens are applied automatically by each component, and custom markdown rendering should use `` or your own host styles around `renderMarkdown()`. diff --git a/apps/website/content/docs/langgraph/api/inject-agent.mdx b/apps/website/content/docs/langgraph/api/inject-agent.mdx index b18c26ffc..5ced855d8 100644 --- a/apps/website/content/docs/langgraph/api/inject-agent.mdx +++ b/apps/website/content/docs/langgraph/api/inject-agent.mdx @@ -31,6 +31,7 @@ injectAgent(ref: AgentRef): LangGraphAgent Pass a typed ref handle created with `createAgentRef()` from `@threadplane/chat`. Returns a `LangGraphAgent` where `state()` and `value()` are typed as `T`. The same ref is passed to `provideAgent()` to bind the configuration. ```ts +import { signal } from '@angular/core'; import { createAgentRef } from '@threadplane/chat'; import { injectAgent, provideAgent } from '@threadplane/langgraph'; import type { BaseMessage } from '@langchain/core/messages'; @@ -41,13 +42,17 @@ export interface MyState { summary: string; } export const MY_AGENT = createAgentRef('my-agent'); +export const activeThreadId = signal(localStorage.getItem('threadId')); // Register in app.config.ts providers: provideAgent(MY_AGENT, { apiUrl: 'http://localhost:2024', assistantId: 'my-graph', - threadId: () => localStorage.getItem('threadId') ?? undefined, - onThreadId: (id) => localStorage.setItem('threadId', id), + threadId: activeThreadId, + onThreadId: (id) => { + activeThreadId.set(id); + localStorage.setItem('threadId', id); + }, }); // Inject in a component or service: @@ -64,17 +69,22 @@ Pair it with `provideAgent()` at bootstrap to configure the API URL, assistant i import { bootstrapApplication } from '@angular/platform-browser'; import { provideAgent } from '@threadplane/langgraph'; import { createAgentRef } from '@threadplane/chat'; +import { signal } from '@angular/core'; import { AppComponent } from './app/app.component'; export const MY_AGENT = createAgentRef('my-agent'); +export const activeThreadId = signal(localStorage.getItem('threadId')); bootstrapApplication(AppComponent, { providers: [ provideAgent(MY_AGENT, { apiUrl: 'http://localhost:2024', assistantId: 'my-graph', - threadId: () => localStorage.getItem('threadId') ?? undefined, - onThreadId: (id) => localStorage.setItem('threadId', id), + threadId: activeThreadId, + onThreadId: (id) => { + activeThreadId.set(id); + localStorage.setItem('threadId', id); + }, }), ], }); diff --git a/apps/website/content/docs/langgraph/api/langgraph-threads-adapter.mdx b/apps/website/content/docs/langgraph/api/langgraph-threads-adapter.mdx new file mode 100644 index 000000000..2e33590df --- /dev/null +++ b/apps/website/content/docs/langgraph/api/langgraph-threads-adapter.mdx @@ -0,0 +1,70 @@ +# LangGraphThreadsAdapter + +SDK-backed thread CRUD for LangGraph Platform threads. Use it with chat thread components instead of hand-rolling local thread lists. + +## Configure + +```ts +import { + LANGGRAPH_THREADS_CONFIG, + LangGraphThreadsAdapter, + refreshOnRunEnd, +} from '@threadplane/langgraph'; + +export const appConfig: ApplicationConfig = { + providers: [ + { + provide: LANGGRAPH_THREADS_CONFIG, + useValue: { apiUrl: 'http://localhost:2024' }, + }, + LangGraphThreadsAdapter, + ], +}; +``` + +`LANGGRAPH_THREADS_CONFIG.apiUrl` accepts the same absolute or relative LangGraph API URLs as `provideAgent()`. + +## Pair with chat thread UI + +```ts +import { Component, inject } from '@angular/core'; +import { ChatThreadListComponent, type ThreadActionAdapter } from '@threadplane/chat'; +import { injectAgent, LangGraphThreadsAdapter, refreshOnRunEnd } from '@threadplane/langgraph'; + +@Component({ + standalone: true, + imports: [ChatThreadListComponent], + template: ` + + `, +}) +export class ThreadsPanel { + protected readonly agent = injectAgent(); + protected readonly threads = inject(LangGraphThreadsAdapter); + + protected readonly actions: ThreadActionAdapter = { + rename: (id, title) => this.threads.rename(id, title), + delete: (id) => this.threads.delete(id), + archive: (id) => this.threads.archive(id), + pin: (id) => this.threads.pin(id), + }; + + constructor() { + void this.threads.refresh(); + refreshOnRunEnd(this.agent, () => this.threads.refresh()); + } +} +``` + +The adapter maps SDK threads to the `Thread` type consumed by `ChatThreadListComponent` and `ChatSidenavComponent`. It expects titles in `metadata.title`. + +## Shared SDK client + +Provide `LANGGRAPH_CLIENT` when you want to share an SDK `Client` instance or inject a test double. Provide `LANGGRAPH_CLIENT_OPTIONS` once at the app root to tune the SDK retry budget used by both `FetchStreamTransport` and the threads adapter. + +{/* Auto-rendered from api-docs.json — see page component */} diff --git a/apps/website/content/docs/langgraph/getting-started/quickstart.mdx b/apps/website/content/docs/langgraph/getting-started/quickstart.mdx index 7bc7aa114..84e957bf7 100644 --- a/apps/website/content/docs/langgraph/getting-started/quickstart.mdx +++ b/apps/website/content/docs/langgraph/getting-started/quickstart.mdx @@ -10,9 +10,11 @@ Angular 20+ project with Node.js 18+. If you need setup help, see the [Installat ```bash -npm install @threadplane/langgraph +npm install @threadplane/langgraph @threadplane/chat ``` +Most npm clients install peer dependencies automatically. Strict package managers may also ask you to install the Angular, LangChain, and LangGraph SDK peers listed in the [Installation](/docs/langgraph/getting-started/installation) guide. + diff --git a/apps/website/content/docs/licensing/getting-started/introduction.mdx b/apps/website/content/docs/licensing/getting-started/introduction.mdx index a9f37e637..2b272cbbc 100644 --- a/apps/website/content/docs/licensing/getting-started/introduction.mdx +++ b/apps/website/content/docs/licensing/getting-started/introduction.mdx @@ -18,7 +18,7 @@ The main entry point exports: | `inferNoncommercial()` | returns a default noncommercial hint from `NODE_ENV` | | `LICENSE_PUBLIC_KEY` | bundled public key | -This table covers the functions and the bundled key, not the full export list. The package also exports the supporting types — `LicenseClaims`, `LicenseTier`, `LicenseStatus`, `VerifyResult`/`VerifyReason`, `EvaluateResult`/`EvaluateOptions`, `RunLicenseCheckOptions`, and `EmitNagOptions`. See the [API reference](../reference/api.mdx) for the full type surface. +This table covers the functions and the bundled key, not the full export list. The package also exports the supporting types — `LicenseClaims`, `LicenseTier`, `LicenseStatus`, `VerifyResult`/`VerifyReason`, `EvaluateResult`/`EvaluateOptions`, `RunLicenseCheckOptions`, and `EmitNagOptions`. See the [API reference](/docs/licensing/reference/api) for the full type surface. `@noble/ed25519` is the only peer dependency. @@ -73,6 +73,6 @@ The code returns statuses instead of throwing for normal license states. For me, ## Next steps -- [Quickstart](./quickstart.mdx) — a runnable round-trip: sign, verify, and evaluate a token end to end. -- [Setup](../guides/setup.mdx) — consume the check via `provideChat()` or call `runLicenseCheck()` directly. -- [API reference](../reference/api.mdx) — the full type surface. +- [Quickstart](/docs/licensing/getting-started/quickstart) — a runnable round-trip: sign, verify, and evaluate a token end to end. +- [Setup](/docs/licensing/guides/setup) — consume the check via `provideChat()` or call `runLicenseCheck()` directly. +- [API reference](/docs/licensing/reference/api) — the full type surface. diff --git a/apps/website/content/docs/licensing/getting-started/quickstart.mdx b/apps/website/content/docs/licensing/getting-started/quickstart.mdx index 2a35cc473..91a8ce2f1 100644 --- a/apps/website/content/docs/licensing/getting-started/quickstart.mdx +++ b/apps/website/content/docs/licensing/getting-started/quickstart.mdx @@ -57,6 +57,6 @@ console.log( ## Next steps -- [Setup](../guides/setup.mdx) — consume the check via `provideChat()` or call `runLicenseCheck()` directly. -- [CI and offline use](../guides/ci-and-offline.mdx) — verify and sign without network access. -- [API reference](../reference/api.mdx) — the full type surface. +- [Setup](/docs/licensing/guides/setup) — consume the check via `provideChat()` or call `runLicenseCheck()` directly. +- [CI and offline use](/docs/licensing/guides/ci-and-offline) — verify and sign without network access. +- [API reference](/docs/licensing/reference/api) — the full type surface. diff --git a/apps/website/content/docs/licensing/guides/setup.mdx b/apps/website/content/docs/licensing/guides/setup.mdx index feb382177..566dd5042 100644 --- a/apps/website/content/docs/licensing/guides/setup.mdx +++ b/apps/website/content/docs/licensing/guides/setup.mdx @@ -108,7 +108,7 @@ It's only a default hint. Callers can pass `isNoncommercial` explicitly. ## Gotchas -`runLicenseCheck()` is idempotent for identical `package` and `token` values. A repeated call with the same key returns `licensed` without re-running the check. +`runLicenseCheck()` is idempotent for identical `package` and `token` values. A repeated call with the same key returns the same status computed on the first call without re-running the check. That keeps repeated provider initialization quiet, but it means package authors shouldn't use repeated calls with identical inputs to poll for status. diff --git a/apps/website/content/docs/licensing/reference/api.mdx b/apps/website/content/docs/licensing/reference/api.mdx index 8abe9a952..e5cbdc646 100644 --- a/apps/website/content/docs/licensing/reference/api.mdx +++ b/apps/website/content/docs/licensing/reference/api.mdx @@ -142,3 +142,13 @@ function signLicense( ``` Signs claims with an Ed25519 private key and returns the compact token consumed by `verifyLicense()`. + +## LICENSE_PUBLIC_KEY + +```ts +const LICENSE_PUBLIC_KEY: Uint8Array; +``` + +The bundled Ed25519 public key used by Threadplane package checks. Use it when verifying a real issued license token. + +Use a throwaway public key only in tests or examples where you also create the matching private key with `signLicense()`. diff --git a/apps/website/content/docs/middleware/api/api-docs.json b/apps/website/content/docs/middleware/api/api-docs.json new file mode 100644 index 000000000..63fc67c38 --- /dev/null +++ b/apps/website/content/docs/middleware/api/api-docs.json @@ -0,0 +1,476 @@ +[ + { + "name": "BaseMessage", + "kind": "interface", + "description": "Base class for all types of messages in a conversation. It includes\nproperties like `content`, `name`, and `additional_kwargs`. It also\nincludes methods like `toDict()` and `_getType()`.", + "properties": [ + { + "name": "additional_kwargs", + "type": "object", + "description": "", + "optional": false + }, + { + "name": "content", + "type": "$InferMessageContent", + "description": "Array of content blocks that make up the message content", + "optional": false + }, + { + "name": "id", + "type": "string", + "description": "Unique identifier for this message", + "optional": true + }, + { + "name": "lc_kwargs", + "type": "SerializedFields", + "description": "", + "optional": false + }, + { + "name": "lc_namespace", + "type": "string[]", + "description": "A path to the module that contains the class, eg. [\"langchain\", \"llms\"]\nUsually should be the same as the entrypoint the class is exported from.", + "optional": false + }, + { + "name": "lc_serializable", + "type": "boolean", + "description": "", + "optional": false + }, + { + "name": "name", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "response_metadata", + "type": "NonNullable[\"response_metadata\"]>", + "description": "Metadata about the message", + "optional": false + }, + { + "name": "type", + "type": "TRole", + "description": "The message type/role", + "optional": false + }, + { + "name": "_printableFields", + "type": "unknown", + "description": "", + "optional": false + }, + { + "name": "[toStringTag]", + "type": "unknown", + "description": "", + "optional": false + }, + { + "name": "contentBlocks", + "type": "unknown", + "description": "", + "optional": false + }, + { + "name": "lc_aliases", + "type": "unknown", + "description": "", + "optional": false + }, + { + "name": "lc_attributes", + "type": "unknown", + "description": "", + "optional": false + }, + { + "name": "lc_id", + "type": "unknown", + "description": "", + "optional": false + }, + { + "name": "lc_secrets", + "type": "unknown", + "description": "", + "optional": false + }, + { + "name": "lc_serializable_keys", + "type": "unknown", + "description": "", + "optional": false + }, + { + "name": "text", + "type": "unknown", + "description": "", + "optional": false + } + ], + "methods": [ + { + "name": "_getType", + "signature": "_getType(): MessageType", + "description": "", + "params": [] + }, + { + "name": "_updateId", + "signature": "_updateId(value: string | undefined): void", + "description": "", + "params": [ + { + "name": "value", + "type": "string | undefined", + "description": "", + "optional": false + } + ] + }, + { + "name": "getType", + "signature": "getType(): MessageType", + "description": "", + "params": [] + }, + { + "name": "toDict", + "signature": "toDict(): StoredMessage", + "description": "", + "params": [] + }, + { + "name": "toFormattedString", + "signature": "toFormattedString(format: \"pretty\"): string", + "description": "", + "params": [ + { + "name": "format", + "type": "\"pretty\"", + "description": "", + "optional": true + } + ] + }, + { + "name": "toJSON", + "signature": "toJSON(): Serialized", + "description": "", + "params": [] + }, + { + "name": "toJSONNotImplemented", + "signature": "toJSONNotImplemented(): SerializedNotImplemented<>", + "description": "", + "params": [] + } + ], + "examples": [] + }, + { + "name": "BindableModel", + "kind": "interface", + "description": "A chat model that can bind tools (the LangChain `Runnable.bindTools` surface).", + "properties": [], + "methods": [ + { + "name": "bindTools", + "signature": "bindTools(tools: unknown[], kwargs: unknown): unknown", + "description": "", + "params": [ + { + "name": "tools", + "type": "unknown[]", + "description": "", + "optional": false + }, + { + "name": "kwargs", + "type": "unknown", + "description": "", + "optional": true + } + ] + } + ], + "examples": [] + }, + { + "name": "ClientToolSpec", + "kind": "interface", + "description": "A frontend-declared client tool: name + description + JSON-Schema parameters.", + "properties": [ + { + "name": "description", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "name", + "type": "string", + "description": "", + "optional": false + }, + { + "name": "parameters", + "type": "Record", + "description": "", + "optional": true + } + ], + "examples": [] + }, + { + "name": "ClientToolsState", + "kind": "interface", + "description": "The slice of graph state this middleware reads.", + "properties": [ + { + "name": "client_tools", + "type": "ClientToolSpec[]", + "description": "Fallback channel — the raw run input key.", + "optional": true + }, + { + "name": "messages", + "type": "BaseMessage, MessageType>[]", + "description": "", + "optional": false + }, + { + "name": "tools", + "type": "ClientToolSpec[]", + "description": "Primary channel — AG-UI/LangGraph merges RunAgentInput.tools here.", + "optional": true + } + ], + "examples": [] + }, + { + "name": "OpenAIFunctionTool", + "kind": "interface", + "description": "The explicit OpenAI function-tool shape accepted by ChatModel.bindTools across versions.", + "properties": [ + { + "name": "function", + "type": "object", + "description": "", + "optional": false + }, + { + "name": "type", + "type": "\"function\"", + "description": "", + "optional": false + } + ], + "examples": [] + }, + { + "name": "bindClientTools", + "kind": "function", + "description": "Bind server tools + the client catalog stubs onto `llm`. Call this INSIDE the\nagent node (per-run) — the client catalog arrives in state and may differ per run.", + "signature": "bindClientTools(llm: M, serverTools: unknown[], state: ClientToolsState): ReturnType", + "params": [ + { + "name": "llm", + "type": "M", + "description": "", + "optional": false + }, + { + "name": "serverTools", + "type": "unknown[]", + "description": "", + "optional": false + }, + { + "name": "state", + "type": "ClientToolsState", + "description": "", + "optional": false + } + ], + "returns": { + "type": "ReturnType", + "description": "" + }, + "examples": [] + }, + { + "name": "clientToolNames", + "kind": "function", + "description": "The set of tool names declared by the client in this run.", + "signature": "clientToolNames(state: ClientToolsState): Set", + "params": [ + { + "name": "state", + "type": "ClientToolsState", + "description": "", + "optional": false + } + ], + "returns": { + "type": "Set", + "description": "" + }, + "examples": [] + }, + { + "name": "clientToolsChannel", + "kind": "function", + "description": "State channels for the client-tools catalog. Spread into Annotation.Root so a graph\ndeclares the `tools` (primary) and `client_tools` (fallback) slices in one line:\n\n const State = Annotation.Root({ ...MessagesAnnotation.spec, ...clientToolsChannel() });\n\nBoth are last-value-wins channels (the catalog is replaced per run, not accumulated).", + "signature": "clientToolsChannel(): object", + "params": [], + "returns": { + "type": "object", + "description": "" + }, + "examples": [] + }, + { + "name": "clientToolSpecs", + "kind": "function", + "description": "The client catalog as OpenAI function-tool dicts for `model.bindTools`.", + "signature": "clientToolSpecs(state: ClientToolsState): OpenAIFunctionTool[]", + "params": [ + { + "name": "state", + "type": "ClientToolsState", + "description": "", + "optional": false + } + ], + "returns": { + "type": "OpenAIFunctionTool[]", + "description": "" + }, + "examples": [] + }, + { + "name": "clientToolsRouter", + "kind": "function", + "description": "A prebuilt conditional-edge callback. serverToolNames is bound once at construction;\nthe returned function takes only state.\n\n graph.addConditionalEdges('agent', clientToolsRouter([]), ['tools', END]);", + "signature": "clientToolsRouter(serverToolNames: Iterable, opts: object): (state: ClientToolsState) => string", + "params": [ + { + "name": "serverToolNames", + "type": "Iterable", + "description": "", + "optional": false + }, + { + "name": "opts", + "type": "object", + "description": "", + "optional": true + } + ], + "returns": { + "type": "(state: ClientToolsState) => string", + "description": "" + }, + "examples": [] + }, + { + "name": "hasClientToolCall", + "kind": "function", + "description": "True if the last message calls at least one client tool.", + "signature": "hasClientToolCall(state: ClientToolsState): boolean", + "params": [ + { + "name": "state", + "type": "ClientToolsState", + "description": "", + "optional": false + } + ], + "returns": { + "type": "boolean", + "description": "" + }, + "examples": [] + }, + { + "name": "hasServerToolCall", + "kind": "function", + "description": "True if the last message calls at least one server (non-client) tool.\nA call is server-side when its name is in serverToolNames OR is not a known\nclient tool (unknown tools are assumed server-side).", + "signature": "hasServerToolCall(state: ClientToolsState, serverToolNames: Iterable): boolean", + "params": [ + { + "name": "state", + "type": "ClientToolsState", + "description": "", + "optional": false + }, + { + "name": "serverToolNames", + "type": "Iterable", + "description": "", + "optional": false + } + ], + "returns": { + "type": "boolean", + "description": "" + }, + "examples": [] + }, + { + "name": "lastMessage", + "kind": "function", + "description": "The last message from state.messages, or undefined.", + "signature": "lastMessage(state: ClientToolsState): BaseMessage, MessageType> | undefined", + "params": [ + { + "name": "state", + "type": "ClientToolsState", + "description": "", + "optional": false + } + ], + "returns": { + "type": "BaseMessage, MessageType> | undefined", + "description": "" + }, + "examples": [] + }, + { + "name": "routeAfterAgent", + "kind": "function", + "description": "Routing helper for a LangGraph conditional edge. Returns `toolsNode` when the last\nmessage has a server tool call (dispatch to the server ToolNode); otherwise `end`\n(client-only calls — the browser executes them — and no-tool-call turns both end).", + "signature": "routeAfterAgent(state: ClientToolsState, serverToolNames: Iterable, opts: object): string", + "params": [ + { + "name": "state", + "type": "ClientToolsState", + "description": "", + "optional": false + }, + { + "name": "serverToolNames", + "type": "Iterable", + "description": "", + "optional": false + }, + { + "name": "opts", + "type": "object", + "description": "", + "optional": true + } + ], + "returns": { + "type": "string", + "description": "" + }, + "examples": [] + } +] \ No newline at end of file diff --git a/apps/website/content/docs/middleware/api/client-tool-helpers.mdx b/apps/website/content/docs/middleware/api/client-tool-helpers.mdx new file mode 100644 index 000000000..e1060948d --- /dev/null +++ b/apps/website/content/docs/middleware/api/client-tool-helpers.mdx @@ -0,0 +1,17 @@ +# bindClientTools() + +`bindClientTools()` is the main helper from `@threadplane/middleware/langgraph`. It binds your server tools plus the current run's client-tool catalog onto a LangChain chat model. + +```ts +import { + bindClientTools, + clientToolsChannel, + clientToolsRouter, +} from '@threadplane/middleware/langgraph'; +``` + +There is no root JavaScript entry point for `@threadplane/middleware`; import from the `/langgraph` subpath. + +Related helpers in the same entry point include `clientToolsChannel()`, `clientToolsRouter()`, `clientToolSpecs()`, `clientToolNames()`, `hasClientToolCall()`, `hasServerToolCall()`, `routeAfterAgent()`, and `lastMessage()`. + +{/* Auto-rendered from api-docs.json — see page component */} diff --git a/apps/website/content/docs/middleware/getting-started/introduction.mdx b/apps/website/content/docs/middleware/getting-started/introduction.mdx new file mode 100644 index 000000000..a5c0637f0 --- /dev/null +++ b/apps/website/content/docs/middleware/getting-started/introduction.mdx @@ -0,0 +1,59 @@ +# Introduction + +`@threadplane/middleware` is the backend companion for Threadplane client tools. It lets a browser declare tools, lets the model call those tools, and routes client-tool-only turns back to the browser for execution. + +The package currently publishes one runtime entry point: + +```ts +import { + bindClientTools, + clientToolsChannel, + clientToolsRouter, +} from '@threadplane/middleware/langgraph'; +``` + +There is no root `@threadplane/middleware` JavaScript entry point. Import from `@threadplane/middleware/langgraph`. + +## What it does + +The LangGraph entry point reads a client tool catalog from graph state, converts it into OpenAI function-tool objects, binds those tool stubs onto your chat model, and routes client-tool calls to `END` so the browser can execute them. + +The catalog is read from `state.tools` first. If that channel is absent or empty, it falls back to `state.client_tools`. + +## Runtime flow + +1. The browser sends tool specs with the run request. +2. Your LangGraph node calls `bindClientTools()` inside the run, because the catalog can differ per request. +3. The model emits a tool call for a browser-declared tool. +4. `clientToolsRouter()` routes client-only tool calls to `END`. +5. The browser executes the local tool and resumes the graph with a `ToolMessage`. + +If a turn mixes server tool calls and client tool calls, server tools win the first route. The server tool node runs first, and the client call can surface on a later turn. + +## Public surface + +The entry point exports: + +| API | Purpose | +|-----|---------| +| `clientToolsChannel()` | Adds the `tools` and `client_tools` state channels to a LangGraph annotation. | +| `bindClientTools()` | Binds server tools plus client-declared tool stubs onto a model. | +| `clientToolsRouter()` | Creates a conditional-edge router for server-tool vs client-tool routing. | +| `clientToolSpecs()` | Converts state catalog entries into OpenAI function-tool specs. | +| `clientToolNames()` | Returns the set of client-declared tool names for a run. | +| `hasClientToolCall()` | Checks whether the last message calls a client tool. | +| `hasServerToolCall()` | Checks whether the last message calls a server or unknown tool. | +| `routeAfterAgent()` | Lower-level routing helper used by `clientToolsRouter()`. | +| `lastMessage()` | Reads the last message from state. | + +## When to use it + +Use middleware when you own a LangGraph.js backend and want browser-declared tools from `@threadplane/chat` to participate in model tool calling without executing browser-only code on the server. + +If your backend already speaks AG-UI, use `@threadplane/ag-ui` instead. If your frontend talks directly to LangGraph and does not need browser-executed tools, `@threadplane/langgraph` can run without this middleware. + +## Next steps + +- [Quick Start](/docs/middleware/getting-started/quickstart) - install and wire the LangGraph helper. +- [LangGraph Client Tools](/docs/middleware/guides/langgraph-client-tools) - routing details and server-tool behavior. +- [Client Tool Helpers](/docs/middleware/api/client-tool-helpers) - generated API reference for the exported helpers. diff --git a/apps/website/content/docs/middleware/getting-started/quickstart.mdx b/apps/website/content/docs/middleware/getting-started/quickstart.mdx new file mode 100644 index 000000000..ecd794d8f --- /dev/null +++ b/apps/website/content/docs/middleware/getting-started/quickstart.mdx @@ -0,0 +1,98 @@ +# Quick Start + +Install the middleware package and its LangGraph peer dependencies: + +```bash +npm install @threadplane/middleware @langchain/core @langchain/langgraph +``` + +The package exposes its JavaScript API from `@threadplane/middleware/langgraph`. + +## Add client-tool state channels + +```ts +import { Annotation, END, MessagesAnnotation, StateGraph } from '@langchain/langgraph'; +import { + bindClientTools, + clientToolsChannel, + clientToolsRouter, +} from '@threadplane/middleware/langgraph'; + +const State = Annotation.Root({ + ...MessagesAnnotation.spec, + ...clientToolsChannel(), +}); +``` + +`clientToolsChannel()` adds both `tools` and `client_tools`. The middleware reads `tools` first and falls back to `client_tools`. + +## Bind tools per run + +Call `bindClientTools()` inside the graph node, not once at module load. The browser sends the catalog with each run, so the tool list is request-scoped. + +```ts +const SERVER_TOOLS: unknown[] = []; +const serverToolNames: string[] = []; + +async function agent(state: typeof State.State) { + const llm = bindClientTools(baseLlm, SERVER_TOOLS, state); + const response = await llm.invoke(state.messages); + return { messages: [response] }; +} +``` + +`SERVER_TOOLS` is where your server-owned LangChain tools go. Client tools from state are appended as model-visible function stubs. + +## Route after the agent + +```ts +const graph = new StateGraph(State) + .addNode('agent', agent) + .addEdge('__start__', 'agent') + .addConditionalEdges('agent', clientToolsRouter(serverToolNames), ['tools', END]) + .compile(); +``` + +When the last model message calls a server tool, the router returns the server tools node. When the last model message calls only browser-declared client tools, the router returns `END` so the frontend can execute the call and resume. + +## Complete skeleton + +```ts +import { Annotation, END, MessagesAnnotation, StateGraph } from '@langchain/langgraph'; +import { ChatOpenAI } from '@langchain/openai'; +import { + bindClientTools, + clientToolsChannel, + clientToolsRouter, +} from '@threadplane/middleware/langgraph'; + +const State = Annotation.Root({ + ...MessagesAnnotation.spec, + ...clientToolsChannel(), +}); + +const baseLlm = new ChatOpenAI({ model: 'gpt-4o-mini' }); +const serverTools: unknown[] = []; +const serverToolNames: string[] = []; + +async function agent(state: typeof State.State) { + const llm = bindClientTools(baseLlm, serverTools, state); + const response = await llm.invoke(state.messages); + return { messages: [response] }; +} + +export const graph = new StateGraph(State) + .addNode('agent', agent) + .addEdge('__start__', 'agent') + .addConditionalEdges('agent', clientToolsRouter(serverToolNames), ['tools', END]) + .compile(); +``` + +## Frontend pairing + +On the frontend, declare client tools with `@threadplane/chat` and send them through an adapter that forwards the tool specs into the run input. The middleware consumes those specs on the backend; the browser remains responsible for executing the actual local function, view, or ask tool. + +## Next steps + +- [LangGraph Client Tools](/docs/middleware/guides/langgraph-client-tools) - mixed server/client routing and helper behavior. +- [Client Tool Helpers](/docs/middleware/api/client-tool-helpers) - generated API details. diff --git a/apps/website/content/docs/middleware/guides/langgraph-client-tools.mdx b/apps/website/content/docs/middleware/guides/langgraph-client-tools.mdx new file mode 100644 index 000000000..8cc2df911 --- /dev/null +++ b/apps/website/content/docs/middleware/guides/langgraph-client-tools.mdx @@ -0,0 +1,99 @@ +# LangGraph Client Tools + +Client tools are frontend-declared tools that the model can call but the browser executes. The backend only exposes tool schemas to the model and decides whether a turn should continue on the server or end so the client can run a local tool. + +## State shape + +The middleware reads this state slice: + +```ts +interface ClientToolsState { + messages: BaseMessage[]; + tools?: ClientToolSpec[]; + client_tools?: ClientToolSpec[]; +} +``` + +Each `ClientToolSpec` is: + +```ts +interface ClientToolSpec { + name: string; + description?: string; + parameters?: Record; +} +``` + +`tools` is the primary channel. `client_tools` is a fallback for raw run input keys. + +## Catalog conversion + +`clientToolSpecs(state)` filters out nameless entries and converts each client tool into the OpenAI function-tool shape accepted by LangChain chat models: + +```ts +{ + type: 'function', + function: { + name: spec.name, + description: spec.description ?? '', + parameters: spec.parameters ?? {}, + }, +} +``` + +`bindClientTools(llm, serverTools, state)` binds server tools first, then appends the client-tool stubs for the current run. + +## Routing behavior + +`clientToolsRouter(serverToolNames)` creates a conditional-edge router. + +The router sends the graph to the server tool node when the last model message includes a server tool call. A call is treated as server-side when its name appears in `serverToolNames` or when the name is unknown to the current client catalog. + +The router returns `END` when the last model message has only known client-tool calls or no tool calls. + +```ts +.addConditionalEdges('agent', clientToolsRouter(['lookupOrder']), ['tools', END]) +``` + +Use `serverToolNames` to disambiguate tools you actually execute on the backend. + +## Mixed tool calls + +When a model message contains both server and client tool calls, server work runs first. This keeps backend-owned side effects on the backend and lets the client tool call surface after the graph resumes. + +That behavior comes from `hasServerToolCall()`: any server or unknown tool call routes to the server tool node. + +## Lower-level helpers + +Use these helpers when you need custom routing: + +```ts +import { + clientToolNames, + clientToolSpecs, + hasClientToolCall, + hasServerToolCall, + lastMessage, + routeAfterAgent, +} from '@threadplane/middleware/langgraph'; +``` + +`routeAfterAgent(state, serverToolNames, opts)` is the primitive behind `clientToolsRouter()`. The default destinations are `'tools'` and `'__end__'`, but you can override them: + +```ts +routeAfterAgent(state, ['lookupOrder'], { + toolsNode: 'serverTools', + end: '__end__', +}); +``` + +## Frontend contract + +The backend middleware does not execute browser tools. The frontend must: + +1. send the catalog into the run input; +2. observe the model's tool call; +3. execute the local tool in the browser; +4. resume the graph with a `ToolMessage` containing the result. + +The `@threadplane/chat` client-tools helpers provide the browser-side declaration and execution pieces. diff --git a/apps/website/content/docs/render/api/define-angular-registry.mdx b/apps/website/content/docs/render/api/define-angular-registry.mdx index ba845af4c..d131595c2 100644 --- a/apps/website/content/docs/render/api/define-angular-registry.mdx +++ b/apps/website/content/docs/render/api/define-angular-registry.mdx @@ -28,10 +28,12 @@ function defineAngularRegistry( interface RenderViewEntry { component: AngularComponentRenderer; fallback?: AngularComponentRenderer; + schema?: StandardSchemaV1; + description?: string; } ``` -Use the object form to configure a custom per-entry fallback (see [Per-Component Fallbacks](#per-component-fallbacks) below). A bare component class is shorthand for `{ component }` paired with the library's default fallback. +Use the object form to configure a custom per-entry fallback, a Standard Schema validator, or a description for higher-level tooling. A bare component class is shorthand for `{ component }` paired with the library's default fallback. ### Returns @@ -130,6 +132,29 @@ registry.getEntry('Missing'); // undefined (not registered) An entry that omits `fallback` -- including every bare-component entry like `Text` above -- falls back to the library's `DefaultFallbackComponent`. Once the real component mounts, the choice is monotonic for that element instance: a later prop resolving to `undefined` never reverts it to the fallback. +### Schema-Gated Mounting + +Add `schema` when a component should mount only after resolved props satisfy a Standard Schema validator such as Zod: + +```typescript +import { z } from 'zod'; +import { defineAngularRegistry } from '@threadplane/render'; + +const registry = defineAngularRegistry({ + WeatherCard: { + component: WeatherCardComponent, + fallback: WeatherCardSkeletonComponent, + schema: z.object({ + city: z.string(), + temperature: z.number(), + }), + description: 'Shows current weather for a city.', + }, +}); +``` + +While props are missing or fail validation, the fallback renders. Once the props pass validation and the component mounts, later updates do not move that element back to the fallback. + ## Internal Behavior The function normalizes each input entry into a `NormalizedEntry` (`{ component, fallback, schema?, description? }`) and stores them in an internal `Map` for O(1) lookups. A bare component class is paired with `DefaultFallbackComponent`; an object entry keeps its own `fallback` (or the default) and preserves any `schema`/`description`: diff --git a/apps/website/content/docs/render/guides/events.mdx b/apps/website/content/docs/render/guides/events.mdx index f9f8f5f95..f2d006819 100644 --- a/apps/website/content/docs/render/guides/events.mdx +++ b/apps/website/content/docs/render/guides/events.mdx @@ -269,19 +269,23 @@ onEvent(event: RenderEvent) { case 'lifecycle': console.log('lifecycle:', event.event, event.scope, event.elementType); break; + case 'result': + console.log('component result:', event.elementKey, event.value); + break; } } ``` -`RenderEvent` is a discriminated union of three variants, keyed by `type`: +`RenderEvent` is a discriminated union of four variants, keyed by `type`: | `type` | Interface | Fires when | Notable fields | |--------|-----------|------------|----------------| | `'handler'` | `RenderHandlerEvent` | A handler finishes running | `action`, `params`, `result?` | | `'stateChange'` | `RenderStateChangeEvent` | The store value changes | `path`, `value`, `snapshot` | | `'lifecycle'` | `RenderLifecycleEvent` | A spec or element mounts/destroys | `event` (`'mounted'` \| `'destroyed'`), `scope` (`'spec'` \| `'element'`), `elementKey?`, `elementType?` | +| `'result'` | `RenderResultEvent` | A mounted view component calls `injectRenderHost().result(value)` | `value`, `elementKey?` | -All three interfaces are exported from `@threadplane/render`. This output is the single source the [Lifecycle guide](/docs/render/guides/lifecycle) builds its `RENDER_LIFECYCLE` signals on top of -- both observe the same stream, so there's no double-counting. +All four interfaces are exported from `@threadplane/render`. This output is the single source the [Lifecycle guide](/docs/render/guides/lifecycle) builds its `RENDER_LIFECYCLE` signals on top of -- both observe the same stream, so there's no double-counting. ## Next Steps diff --git a/apps/website/content/docs/render/guides/registry.mdx b/apps/website/content/docs/render/guides/registry.mdx index c0b7731f5..e0fd68eaf 100644 --- a/apps/website/content/docs/render/guides/registry.mdx +++ b/apps/website/content/docs/render/guides/registry.mdx @@ -35,7 +35,7 @@ uiRegistry.names(); // ['Text', 'Card', 'Button', 'Container ### Fallback Rendering -When an element's type isn't registered, it renders that type's configured fallback if one exists, and otherwise renders nothing. Fallbacks also fill a transient gap during rendering: while an element's state-bound props are still resolving, the library can render a fallback to give visual feedback until the real component is ready. Once the real component mounts, it stays mounted -- later re-renders never revert to the fallback. +Fallbacks belong to registered entries. When an element's type is not registered, there is no entry to read and the element renders nothing. When the type is registered, the entry's configured fallback can fill a transient gap while state-bound props are still resolving. Once the real component mounts, it stays mounted -- later re-renders never revert to the fallback. ## The Component Input Contract @@ -161,6 +161,34 @@ export class InputComponent { } ``` +## RenderHost + +Inside a mounted view component, `injectRenderHost()` exposes the element-scoped host: + +```typescript +import { Component, inject } from '@angular/core'; +import { injectRenderHost } from '@threadplane/render'; + +@Component({ + selector: 'app-approval', + standalone: true, + template: ` + + `, +}) +export class ApprovalComponent { + private readonly host = injectRenderHost(); + + approve() { + this.host.set('/approved', true); + this.host.emit('approved'); + this.host.result({ approved: true }); + } +} +``` + +Use `set(path, value)` for state writes, `emit(event, payload?)` for spec `on` handlers, and `result(value)` when the component has produced a value that the host should observe as a `RenderResultEvent`. + ## Recursive Children Container components can render their children by using the `childKeys` and `spec` inputs with `RenderElementComponent`: diff --git a/apps/website/content/docs/telemetry/api/api-docs.json b/apps/website/content/docs/telemetry/api/api-docs.json index 0c63b4e87..f47e20f7b 100644 --- a/apps/website/content/docs/telemetry/api/api-docs.json +++ b/apps/website/content/docs/telemetry/api/api-docs.json @@ -1,4 +1,368 @@ [ + { + "name": "ThreadplaneTelemetryService", + "kind": "class", + "description": "", + "params": [], + "examples": [], + "properties": [], + "methods": [ + { + "name": "capture", + "signature": "capture(event: ThreadplaneTelemetryEvent, properties: Record): Promise", + "description": "", + "params": [ + { + "name": "event", + "type": "ThreadplaneTelemetryEvent", + "description": "", + "optional": false + }, + { + "name": "properties", + "type": "Record", + "description": "", + "optional": true + } + ] + }, + { + "name": "captureRuntimeInstanceCreated", + "signature": "captureRuntimeInstanceCreated(input: ThreadplaneBrowserRuntimeTelemetry): Promise", + "description": "", + "params": [ + { + "name": "input", + "type": "ThreadplaneBrowserRuntimeTelemetry", + "description": "", + "optional": false + } + ] + }, + { + "name": "captureRuntimeRequestCreated", + "signature": "captureRuntimeRequestCreated(input: ThreadplaneBrowserRuntimeTelemetry & { requestType: string }): Promise", + "description": "", + "params": [ + { + "name": "input", + "type": "ThreadplaneBrowserRuntimeTelemetry & { requestType: string }", + "description": "", + "optional": false + } + ] + }, + { + "name": "captureStreamEnded", + "signature": "captureStreamEnded(input: ThreadplaneBrowserStreamTelemetry): Promise", + "description": "", + "params": [ + { + "name": "input", + "type": "ThreadplaneBrowserStreamTelemetry", + "description": "", + "optional": false + } + ] + }, + { + "name": "captureStreamErrored", + "signature": "captureStreamErrored(input: ThreadplaneBrowserStreamErrorTelemetry): Promise", + "description": "", + "params": [ + { + "name": "input", + "type": "ThreadplaneBrowserStreamErrorTelemetry", + "description": "", + "optional": false + } + ] + }, + { + "name": "captureStreamStarted", + "signature": "captureStreamStarted(input: ThreadplaneBrowserStreamTelemetry): Promise", + "description": "", + "params": [ + { + "name": "input", + "type": "ThreadplaneBrowserStreamTelemetry", + "description": "", + "optional": false + } + ] + } + ] + }, + { + "name": "ThreadplaneBrowserRuntimeTelemetry", + "kind": "interface", + "description": "", + "properties": [ + { + "name": "model", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "provider", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "surface", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "transport", + "type": "string", + "description": "", + "optional": false + } + ], + "examples": [] + }, + { + "name": "ThreadplaneBrowserStreamErrorTelemetry", + "kind": "interface", + "description": "", + "properties": [ + { + "name": "durationMs", + "type": "number", + "description": "", + "optional": true + }, + { + "name": "error", + "type": "unknown", + "description": "", + "optional": true + }, + { + "name": "model", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "provider", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "surface", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "transport", + "type": "string", + "description": "", + "optional": false + } + ], + "examples": [] + }, + { + "name": "ThreadplaneBrowserStreamTelemetry", + "kind": "interface", + "description": "", + "properties": [ + { + "name": "durationMs", + "type": "number", + "description": "", + "optional": true + }, + { + "name": "model", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "provider", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "surface", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "transport", + "type": "string", + "description": "", + "optional": false + } + ], + "examples": [] + }, + { + "name": "ThreadplaneTelemetryConfig", + "kind": "interface", + "description": "", + "properties": [ + { + "name": "enabled", + "type": "boolean", + "description": "", + "optional": false + }, + { + "name": "endpoint", + "type": "string", + "description": "Preferred app-owned ingest URL. The browser service POSTs neutral event\npayloads here; the endpoint decides where they ultimately go.", + "optional": true + }, + { + "name": "posthogHost", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "posthogKey", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "sampleRate", + "type": "number", + "description": "", + "optional": true + }, + { + "name": "sink", + "type": "ThreadplaneTelemetrySink", + "description": "Preferred app-owned delivery hook. Use this when the consuming app wants\nto forward events through its own analytics boundary.", + "optional": true + } + ], + "examples": [] + }, + { + "name": "ThreadplaneTelemetryEventPayload", + "kind": "interface", + "description": "", + "properties": [ + { + "name": "event", + "type": "ThreadplaneTelemetryEvent", + "description": "", + "optional": false + }, + { + "name": "properties", + "type": "Record", + "description": "", + "optional": true + } + ], + "examples": [] + }, + { + "name": "CaptureConfig", + "kind": "type", + "description": "", + "signature": "unknown", + "examples": [] + }, + { + "name": "ThreadplaneBrowserEvent", + "kind": "type", + "description": "", + "signature": "ThreadplaneTelemetryEvent", + "examples": [] + }, + { + "name": "ThreadplaneTelemetryEvent", + "kind": "type", + "description": "", + "signature": "\"tplane:browser_provided\" | \"tplane:browser_chat_init\" | \"tplane:runtime_instance_created\" | \"tplane:runtime_request_created\" | \"tplane:stream_started\" | \"tplane:stream_ended\" | \"tplane:stream_errored\"", + "examples": [] + }, + { + "name": "ThreadplaneTelemetrySink", + "kind": "type", + "description": "", + "signature": "(payload: ThreadplaneTelemetryEventPayload) => void | Promise", + "examples": [] + }, + { + "name": "THREADPLANE_TELEMETRY_CONFIG", + "kind": "const", + "description": "", + "signature": "InjectionToken", + "examples": [] + }, + { + "name": "isLocalAnalyticsHost", + "kind": "function", + "description": "", + "signature": "isLocalAnalyticsHost(host: unknown): boolean", + "params": [ + { + "name": "host", + "type": "unknown", + "description": "", + "optional": false + } + ], + "returns": { + "type": "boolean", + "description": "" + }, + "examples": [] + }, + { + "name": "provideThreadplaneTelemetry", + "kind": "function", + "description": "", + "signature": "provideThreadplaneTelemetry(config: ThreadplaneTelemetryConfig): EnvironmentProviders", + "params": [ + { + "name": "config", + "type": "ThreadplaneTelemetryConfig", + "description": "", + "optional": false + } + ], + "returns": { + "type": "EnvironmentProviders", + "description": "" + }, + "examples": [] + }, + { + "name": "shouldCaptureAnalytics", + "kind": "function", + "description": "", + "signature": "shouldCaptureAnalytics(__namedParameters: CaptureConfig): boolean", + "params": [ + { + "name": "__namedParameters", + "type": "CaptureConfig", + "description": "", + "optional": false + } + ], + "returns": { + "type": "boolean", + "description": "" + }, + "examples": [] + }, { "name": "ThreadplaneBrowserEvent", "kind": "type", diff --git a/apps/website/content/docs/telemetry/getting-started/installation.mdx b/apps/website/content/docs/telemetry/getting-started/installation.mdx new file mode 100644 index 000000000..3dfc7c5be --- /dev/null +++ b/apps/website/content/docs/telemetry/getting-started/installation.mdx @@ -0,0 +1,66 @@ +# Installation + +Install the telemetry package: + +```bash +npm install @threadplane/telemetry +``` + +The package publishes separate entry points for browser, Node, and shared utilities: + +```ts +import { provideThreadplaneTelemetry } from '@threadplane/telemetry/browser'; +import { captureEvent, disableTelemetry } from '@threadplane/telemetry/node'; +import { isTelemetryDisabled } from '@threadplane/telemetry'; +``` + +## Optional peers + +`@angular/core` and `posthog-js` are optional peer dependencies. + +Install `@angular/core` when you use the browser Angular provider and service: + +```bash +npm install @threadplane/telemetry @angular/core +``` + +Install `posthog-js` only when you use direct browser PostHog delivery through `posthogKey`. New integrations should prefer `sink` or `endpoint`, because those keep delivery inside your application's analytics boundary. + +```bash +npm install @threadplane/telemetry posthog-js +``` + +## Browser entry point + +Browser telemetry is opt-in. Calling `provideThreadplaneTelemetry({ enabled: true, ... })` is what activates capture. + +```ts +import { provideThreadplaneTelemetry } from '@threadplane/telemetry/browser'; + +provideThreadplaneTelemetry({ + enabled: true, + endpoint: '/api/telemetry', +}); +``` + +## Node entry point + +Node helpers live under `@threadplane/telemetry/node`. + +```ts +import { captureRuntimeInstanceCreated } from '@threadplane/telemetry/node'; + +await captureRuntimeInstanceCreated({ + transport: 'langgraph', + provider: 'openai', + model: 'gpt-4.1', +}); +``` + +Node telemetry respects the environment opt-out variables documented in [Privacy and Opt-Out](/docs/telemetry/guides/privacy-and-opt-out). + +## Next steps + +- [Browser Telemetry](/docs/telemetry/guides/browser) - configure an Angular provider and runtime sink. +- [Node Telemetry](/docs/telemetry/guides/node) - capture helpers for lifecycle and stream events. +- [Events](/docs/telemetry/reference/events) - event names and payload shapes. diff --git a/apps/website/content/prompts/configuration.md b/apps/website/content/prompts/configuration.md index 37a2283a9..ed5d4a960 100644 --- a/apps/website/content/prompts/configuration.md +++ b/apps/website/content/prompts/configuration.md @@ -1,13 +1,13 @@ Configure angular globally and per-component in my Angular application. -Global config (applies to all agent() calls in the app): -In app.config.ts, provideAgent({ apiUrl: 'https://my-langgraph-server.com', }) — import provideAgent from '@threadplane/langgraph'. +Global config: +In app.config.ts, call provideAgent({ apiUrl: 'https://my-langgraph-server.com', assistantId: 'my-agent' }) in the providers array. Import provideAgent from '@threadplane/langgraph'. -Per-call override (overrides global config for one component): -Pass apiUrl directly to agent({ apiUrl: 'https://other-server.com', assistantId: 'my-agent' }) — per-call options take precedence over global config. +Scoped override for one component subtree: +Add providers: [provideAgent({ apiUrl: 'https://other-server.com', assistantId: 'other-agent' })] on that component. Components under that subtree call injectAgent() and receive the scoped singleton. Custom transport (for auth headers, logging, or testing): -Implement the AgentTransport interface from @threadplane/langgraph. Its required method is stream(assistantId, threadId, payload, signal, options), and optional methods cover queued runs, cancellation, history, and updateState. Pass an AgentTransport instance as transport: myTransport to either provideAgent() or agent(). FetchStreamTransport is the default. +Implement the AgentTransport interface from @threadplane/langgraph. Its required method is stream(assistantId, threadId, payload, signal, options), and optional methods cover queued runs, cancellation, history, and updateState. Pass an AgentTransport instance as transport: myTransport to provideAgent(). FetchStreamTransport is the default. -To pass a system prompt to the LangGraph agent per-thread, use the config option: -agent({ config: { configurable: { system_prompt: 'You are a helpful assistant.' } } }) +To pass per-run LangGraph config, pass it when submitting: +chat.submit({ message }, { config: { configurable: { system_prompt: 'You are a helpful assistant.' } } }) diff --git a/apps/website/content/prompts/getting-started.md b/apps/website/content/prompts/getting-started.md index 7e66ab1e8..23ba06f3c 100644 --- a/apps/website/content/prompts/getting-started.md +++ b/apps/website/content/prompts/getting-started.md @@ -2,9 +2,9 @@ Add angular to my Angular 20+ application. Install: npm install @threadplane/langgraph@latest -1. In app.config.ts, add provideAgent({ apiUrl: 'http://localhost:2024' }) to the providers array. Import it from '@threadplane/langgraph'. +1. In app.config.ts, add provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'chat' }) to the providers array. Import it from '@threadplane/langgraph'. -2. Create a ChatComponent that calls agent({ assistantId: 'chat' }) in the constructor or as a field initializer. agent() MUST be called inside an Angular injection context — constructor or field initializer is correct; ngOnInit is not. +2. Create a ChatComponent that calls injectAgent() in the constructor or as a field initializer. injectAgent() MUST be called inside an Angular injection context — constructor or field initializer is correct; ngOnInit is not. 3. The component template should loop over chat.messages() using @for and render each message's content. Add an input field and a button that calls chat.submit({ message: inputValue }). diff --git a/apps/website/content/prompts/streaming.md b/apps/website/content/prompts/streaming.md index 7e9c764eb..6c90e9560 100644 --- a/apps/website/content/prompts/streaming.md +++ b/apps/website/content/prompts/streaming.md @@ -1,12 +1,12 @@ Configure token-by-token streaming in my Angular component that uses angular. -The component already has agent() set up. Now: +The component already has injectAgent() set up from a configured provideAgent() provider. Now: 1. In the template, bind to chat.messages() with @for — each runtime-neutral Message has role and content fields. The template re-renders automatically as tokens arrive because messages() is a Signal. 2. Show a loading indicator while streaming: use chat.isLoading() in an @if block. -3. To throttle rapid re-renders (if performance is a concern), pass throttle: 50 to agent() options — this throttles Signal updates to at most one per 50ms while preserving the final value. +3. To throttle rapid re-renders (if performance is a concern), pass throttle: 50 to provideAgent() config — this throttles Signal updates to at most one per 50ms while preserving the final value. 4. To show the stream status more precisely, bind to chat.status() which returns 'idle' | 'running' | 'error'. Use chat.isLoading() for loading UI. diff --git a/apps/website/content/prompts/testing.md b/apps/website/content/prompts/testing.md index a39018cec..1e87508e9 100644 --- a/apps/website/content/prompts/testing.md +++ b/apps/website/content/prompts/testing.md @@ -4,7 +4,8 @@ Use MockAgentTransport from '@threadplane/langgraph'. It implements AgentTranspo Test setup: const transport = new MockAgentTransport(); -const chat = agent({ transport, assistantId: 'test', apiUrl: '' }); +providers: [provideAgent({ transport, assistantId: 'test', apiUrl: '' })] +const chat = injectAgent(); To emit a streaming response: transport.emit([ @@ -21,4 +22,4 @@ transport.emitError(new Error('Network failure')); expect(chat.status()).toBe('error'); expect(chat.error()).toBeInstanceOf(Error); -Never mock agent() itself — always use MockAgentTransport and test through the real function. +Never mock injectAgent() itself — always use MockAgentTransport and test through the real provider. diff --git a/apps/website/content/prompts/thread-persistence.md b/apps/website/content/prompts/thread-persistence.md index 7be3acc45..c76cce173 100644 --- a/apps/website/content/prompts/thread-persistence.md +++ b/apps/website/content/prompts/thread-persistence.md @@ -4,10 +4,10 @@ Add thread persistence to my Angular component that uses angular, so conversatio 2. Create a signal: threadId = signal(storedId). -3. Pass it to agent: agent({ ..., threadId: this.threadId, onThreadId: (id) => { this.threadId.set(id); localStorage.setItem('chat-thread-id', id); } }). +3. Pass it to provideAgent: provideAgent({ assistantId: 'chat', threadId: this.threadId, onThreadId: (id) => { this.threadId.set(id); localStorage.setItem('chat-thread-id', id); } }). 4. The onThreadId callback fires once when the server creates a new thread. After that, the same thread ID is reused and the full conversation history is restored from the LangGraph server. -5. To start a new conversation, call this.threadId.set(null) — this causes agent to create a fresh thread on the next submit. +5. To start a new conversation, call this.threadId.set(null) — this causes the injected agent to create a fresh thread on the next submit. No changes to the template are needed. diff --git a/apps/website/public/AGENTS.md b/apps/website/public/AGENTS.md index 1d5b40477..8ea908e96 100644 --- a/apps/website/public/AGENTS.md +++ b/apps/website/public/AGENTS.md @@ -1,4 +1,4 @@ -# Threadplane v0.0.46 +# Threadplane v0.0.54 Production-ready chat, durable threads, interrupts, subagents, planning, memory, and generative UI for Angular agent apps. @@ -6,7 +6,7 @@ Production-ready chat, durable threads, interrupts, subagents, planning, memory, npm install @threadplane/chat @threadplane/langgraph ## Key requirement -`agent()` MUST be called within an Angular injection context (component constructor or field initializer). Calling it in ngOnInit or any async context throws "NG0203: inject() must be called from an injection context". +`injectAgent()` MUST be called within an Angular injection context (component constructor or field initializer). Calling it in ngOnInit or any async context throws "NG0203: inject() must be called from an injection context". ## Basic usage ```typescript @@ -14,12 +14,12 @@ npm install @threadplane/chat @threadplane/langgraph import type { ApplicationConfig } from '@angular/core'; import { provideAgent } from '@threadplane/langgraph'; export const appConfig: ApplicationConfig = { - providers: [provideAgent({ apiUrl: 'http://localhost:2024' })] + providers: [provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'chat_agent' })] }; // chat.component.ts import { Component } from '@angular/core'; -import { agent } from '@threadplane/langgraph'; +import { injectAgent } from '@threadplane/langgraph'; import { ChatComponent as ThreadplaneChatComponent } from '@threadplane/chat'; @Component({ @@ -29,15 +29,15 @@ import { ChatComponent as ThreadplaneChatComponent } from '@threadplane/chat'; `, }) export class ChatComponent { - chat = agent({ apiUrl: 'http://localhost:2024', assistantId: 'chat_agent' }); + chat = injectAgent(); } ``` ## Key patterns -- Thread persistence: `threadId: signal(localStorage.getItem('t'))` + `onThreadId: (id) => localStorage.setItem('t', id)` -- Global config: `provideAgent({ apiUrl })` in app.config.ts -- Per-call override: pass `apiUrl` directly to `agent()` -- Testing: use `MockAgentTransport` — never mock `agent()` itself +- Thread persistence: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })` +- Global config: `provideAgent({ apiUrl, assistantId })` in app.config.ts +- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree +- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself ## Version check If this file is stale, fetch the latest: https://threadplane.ai/llms-full.txt diff --git a/apps/website/public/CLAUDE.md b/apps/website/public/CLAUDE.md index 1d5b40477..8ea908e96 100644 --- a/apps/website/public/CLAUDE.md +++ b/apps/website/public/CLAUDE.md @@ -1,4 +1,4 @@ -# Threadplane v0.0.46 +# Threadplane v0.0.54 Production-ready chat, durable threads, interrupts, subagents, planning, memory, and generative UI for Angular agent apps. @@ -6,7 +6,7 @@ Production-ready chat, durable threads, interrupts, subagents, planning, memory, npm install @threadplane/chat @threadplane/langgraph ## Key requirement -`agent()` MUST be called within an Angular injection context (component constructor or field initializer). Calling it in ngOnInit or any async context throws "NG0203: inject() must be called from an injection context". +`injectAgent()` MUST be called within an Angular injection context (component constructor or field initializer). Calling it in ngOnInit or any async context throws "NG0203: inject() must be called from an injection context". ## Basic usage ```typescript @@ -14,12 +14,12 @@ npm install @threadplane/chat @threadplane/langgraph import type { ApplicationConfig } from '@angular/core'; import { provideAgent } from '@threadplane/langgraph'; export const appConfig: ApplicationConfig = { - providers: [provideAgent({ apiUrl: 'http://localhost:2024' })] + providers: [provideAgent({ apiUrl: 'http://localhost:2024', assistantId: 'chat_agent' })] }; // chat.component.ts import { Component } from '@angular/core'; -import { agent } from '@threadplane/langgraph'; +import { injectAgent } from '@threadplane/langgraph'; import { ChatComponent as ThreadplaneChatComponent } from '@threadplane/chat'; @Component({ @@ -29,15 +29,15 @@ import { ChatComponent as ThreadplaneChatComponent } from '@threadplane/chat'; `, }) export class ChatComponent { - chat = agent({ apiUrl: 'http://localhost:2024', assistantId: 'chat_agent' }); + chat = injectAgent(); } ``` ## Key patterns -- Thread persistence: `threadId: signal(localStorage.getItem('t'))` + `onThreadId: (id) => localStorage.setItem('t', id)` -- Global config: `provideAgent({ apiUrl })` in app.config.ts -- Per-call override: pass `apiUrl` directly to `agent()` -- Testing: use `MockAgentTransport` — never mock `agent()` itself +- Thread persistence: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })` +- Global config: `provideAgent({ apiUrl, assistantId })` in app.config.ts +- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree +- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself ## Version check If this file is stale, fetch the latest: https://threadplane.ai/llms-full.txt diff --git a/apps/website/scripts/generate-api-docs.ts b/apps/website/scripts/generate-api-docs.ts index 6d720a842..9a9e0506a 100644 --- a/apps/website/scripts/generate-api-docs.ts +++ b/apps/website/scripts/generate-api-docs.ts @@ -173,6 +173,8 @@ interface LibraryEntryConfig { docSlug: string; /** TypeDoc entry points — usually libs//src/public-api.ts. */ entryPoints: string[]; + /** Optional TypeScript config override when a package has multiple published entrypoint groups. */ + tsconfig?: string; } const LIBRARIES: LibraryEntryConfig[] = [ @@ -181,6 +183,7 @@ const LIBRARIES: LibraryEntryConfig[] = [ { docSlug: 'render', entryPoints: ['libs/render/src/public-api.ts'] }, { docSlug: 'ag-ui', entryPoints: ['libs/ag-ui/src/public-api.ts'] }, { docSlug: 'a2ui', entryPoints: ['libs/a2ui/src/index.ts'] }, + { docSlug: 'middleware', entryPoints: ['libs/middleware/src/langgraph/index.ts'] }, { docSlug: 'licensing', entryPoints: ['libs/licensing/src/index.ts'] }, { docSlug: 'telemetry', @@ -190,6 +193,7 @@ const LIBRARIES: LibraryEntryConfig[] = [ 'libs/telemetry/src/node/index.ts', 'libs/telemetry/src/shared/public-api.ts', ], + tsconfig: 'libs/telemetry/tsconfig.spec.json', }, ]; @@ -203,10 +207,10 @@ async function generateForLibrary(cfg: LibraryEntryConfig): Promise { return; } - const libDir = path.dirname(path.dirname(cfg.entryPoints[0])); - const libTsconfig = fs.existsSync(path.join(libDir, 'tsconfig.lib.json')) + const libDir = findPackageRoot(cfg.entryPoints[0]); + const libTsconfig = cfg.tsconfig ?? (fs.existsSync(path.join(libDir, 'tsconfig.lib.json')) ? path.join(libDir, 'tsconfig.lib.json') - : undefined; + : undefined); const app = await Application.bootstrapWithPlugins({ entryPoints: cfg.entryPoints, @@ -224,6 +228,15 @@ async function generateForLibrary(cfg: LibraryEntryConfig): Promise { console.log(`✓ ${cfg.docSlug}/api/api-docs.json (${entries.length} entries)`); } +function findPackageRoot(entryPoint: string): string { + let dir = path.dirname(entryPoint); + while (dir !== path.dirname(dir)) { + if (fs.existsSync(path.join(dir, 'package.json'))) return dir; + dir = path.dirname(dir); + } + return path.dirname(path.dirname(entryPoint)); +} + async function main() { for (const cfg of LIBRARIES) { await generateForLibrary(cfg); diff --git a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx index 16c6fe776..7f60f703b 100644 --- a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx +++ b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx @@ -27,15 +27,6 @@ function loadApiDocs(library: string): ApiDocEntry[] { return []; } -const API_NAME_MAP: Record> = { - langgraph: { - 'inject-agent': 'injectAgent', - 'provide-agent': 'provideAgent', - 'fetch-stream-transport': 'FetchStreamTransport', - 'mock-stream-transport': 'MockAgentTransport', - }, -}; - interface DocsRouteProps { params: Promise<{ library: string; section: string; slug: string }>; } @@ -92,9 +83,8 @@ export default async function DocsPage({ params }: DocsRouteProps) { {section === 'api' && (() => { const entries = loadApiDocs(library); - const nameMap = API_NAME_MAP[library] ?? {}; - const target = nameMap[slug]; - const apiEntry = target ? entries.find((e: ApiDocEntry) => e.name === target) : null; + const target = doc.title.replace(/\(\)$/, ''); + const apiEntry = entries.find((e: ApiDocEntry) => e.name === target || e.name === doc.title); return apiEntry ? (
diff --git a/apps/website/src/app/docs/page.tsx b/apps/website/src/app/docs/page.tsx index d25376777..ee3369d43 100644 --- a/apps/website/src/app/docs/page.tsx +++ b/apps/website/src/app/docs/page.tsx @@ -76,10 +76,16 @@ interface SupportingLib { title: string; blurb: string; href: string; - glyph: 'key' | 'pulse'; + glyph: 'key' | 'middleware' | 'pulse'; } const SUPPORTING: SupportingLib[] = [ + { + title: 'Middleware', + blurb: 'Backend client-tool routing', + href: '/docs/middleware/getting-started/introduction', + glyph: 'middleware', + }, { title: 'Licensing', blurb: 'Token verification', @@ -119,7 +125,16 @@ function PulseGlyph() { ); } -const GLYPHS = { key: KeyGlyph, pulse: PulseGlyph } as const; +function MiddlewareGlyph() { + return ( + + ); +} + +const GLYPHS = { key: KeyGlyph, middleware: MiddlewareGlyph, pulse: PulseGlyph } as const; const stepLabelStyle = { fontFamily: tokens.typography.eyebrow.family, diff --git a/apps/website/src/app/llms-full.txt/route.ts b/apps/website/src/app/llms-full.txt/route.ts index cbff378f0..21e3f11e4 100644 --- a/apps/website/src/app/llms-full.txt/route.ts +++ b/apps/website/src/app/llms-full.txt/route.ts @@ -6,6 +6,7 @@ import langgraphApiDocs from '../../../content/docs/langgraph/api/api-docs.json' import agUiApiDocs from '../../../content/docs/ag-ui/api/api-docs.json'; import chatApiDocs from '../../../content/docs/chat/api/api-docs.json'; import licensingApiDocs from '../../../content/docs/licensing/api/api-docs.json'; +import middlewareApiDocs from '../../../content/docs/middleware/api/api-docs.json'; import renderApiDocs from '../../../content/docs/render/api/api-docs.json'; import telemetryApiDocs from '../../../content/docs/telemetry/api/api-docs.json'; @@ -15,6 +16,7 @@ const API_DOCS: Record = { langgraph: langgraphApiDocs, chat: chatApiDocs, licensing: licensingApiDocs, + middleware: middlewareApiDocs, render: renderApiDocs, telemetry: telemetryApiDocs, }; diff --git a/apps/website/src/components/docs/LibraryMark.tsx b/apps/website/src/components/docs/LibraryMark.tsx index 480b055a5..e742d8eac 100644 --- a/apps/website/src/components/docs/LibraryMark.tsx +++ b/apps/website/src/components/docs/LibraryMark.tsx @@ -1,7 +1,7 @@ import { tokens } from '@threadplane/design-tokens'; import type { LibraryId } from '../../lib/docs-config'; -type GlyphKey = 'chat' | 'key' | 'pulse'; +type GlyphKey = 'chat' | 'key' | 'middleware' | 'pulse'; type MarkEntry = | { kind: 'logo'; src: string } @@ -13,6 +13,7 @@ const MARKS: Record = { a2ui: { kind: 'logo', src: '/logos/providers/google.svg' }, render: { kind: 'logo', src: '/logos/surface/vercel.svg' }, chat: { kind: 'glyph', glyph: 'chat' }, + middleware: { kind: 'glyph', glyph: 'middleware' }, licensing: { kind: 'glyph', glyph: 'key' }, telemetry: { kind: 'glyph', glyph: 'pulse' }, }; @@ -34,6 +35,15 @@ function KeyGlyph({ s }: { s: number }) { ); } +function MiddlewareGlyph({ s }: { s: number }) { + return ( + + ); +} + function PulseGlyph({ s }: { s: number }) { return (