Skip to content
49 changes: 34 additions & 15 deletions apps/website/content/docs/langgraph/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
{
"name": "AgentLifecycleRegistry",
"kind": "class",
"description": "Optional registry that collects per-instance agent lifecycles within\nan Angular injection context. External instrumentation packages\n(e.g. cockpit-telemetry) provide this token and read from it.\n\n`@threadplane/langgraph` does NOT provide this itself — the configured agent\ninstance writes to the registry only when an external consumer has provided it.",
"description": "Application-wide registry of every live agent's AgentLifecycle.\n\nIt is `providedIn: 'root'`, so it always exists and every agent registers\ninto the same instance regardless of which injector built it — an agent\ncreated in a route or component injector is still visible from the root.\nExternal instrumentation packages read `lifecycles()` to observe every agent\nin the application without owning the provider graph.\n\nRegistration is scoped to the agent's lifetime: an agent unregisters when the\ninjector that created it is destroyed, so `lifecycles()` never accumulates\nlifecycles for agents that are gone.",
"params": [],
"examples": [],
"properties": [
{
"name": "lifecycles",
"type": "Signal<readonly AgentLifecycle[]>",
"description": "Reactive list of registered lifecycles.",
"description": "Reactive list of the lifecycles of every currently live agent.",
"optional": false
}
],
Expand All @@ -26,6 +26,19 @@
"optional": false
}
]
},
{
"name": "unregister",
"signature": "unregister(lifecycle: AgentLifecycle): void",
"description": "Drop a lifecycle when its agent's injector is destroyed.",
"params": [
{
"name": "lifecycle",
"type": "AgentLifecycle",
"description": "",
"optional": false
}
]
}
]
},
Expand Down Expand Up @@ -563,7 +576,7 @@
{
"name": "MockAgentTransport",
"kind": "class",
"description": "Test transport for deterministic agent testing without a real LangGraph server.\n\nScript event batches upfront, then emit them manually or step through them\nin your test specs. Supports error injection and close control.",
"description": "Test transport for deterministic agent testing without a real LangGraph server.\n\nScript event batches upfront, then emit them manually or step through them\nin your test specs. Supports error injection and close control.\n\n`emit()`, `emitError()`, `close()` and `flush()` are awaitable: the returned\npromise settles once the adapter has consumed everything queued so far (and\none macrotask later, so throttled signal writes have landed), which removes\nthe hand-rolled `await new Promise(resolve => setTimeout(resolve, 0))` flush\nfrom specs.",
"params": [
{
"name": "script",
Expand All @@ -573,7 +586,7 @@
}
],
"examples": [
"```typescript\nconst transport = new MockAgentTransport([\n [{ type: 'values', messages: [aiMsg('Hello')] }],\n [{ type: 'values', messages: [aiMsg('Done')] }],\n]);\n```"
"```typescript\nconst transport = new MockAgentTransport([\n [{ type: 'values', messages: [aiMsg('Hello')] }],\n [{ type: 'values', messages: [aiMsg('Done')] }],\n]);\nawait transport.emit(transport.nextBatch());\n```"
],
"properties": [
{
Expand Down Expand Up @@ -641,8 +654,8 @@
},
{
"name": "close",
"signature": "close(): void",
"description": "Close the stream. Remaining queued events are drained before completion.",
"signature": "close(): Promise<void>",
"description": "Close the stream. Remaining queued events are drained before completion.\nResolves once the run has finished.",
"params": []
},
{
Expand Down Expand Up @@ -684,8 +697,8 @@
},
{
"name": "emit",
"signature": "emit(events: StreamEvent[]): void",
"description": "Manually emit events into the stream.",
"signature": "emit(events: StreamEvent[]): Promise<void>",
"description": "Manually emit events into the stream.\n\nAwait the returned promise: it resolves once the adapter has pulled this\nbatch out of the stream (or the run has ended), so signals are settled and\nassertions read live state rather than the value from before the emit.",
"params": [
{
"name": "events",
Expand All @@ -697,8 +710,8 @@
},
{
"name": "emitError",
"signature": "emitError(err: Error): void",
"description": "Inject an error into the stream.",
"signature": "emitError(err: Error): Promise<void>",
"description": "Inject an error into the stream. Resolves once the stream has thrown.",
"params": [
{
"name": "err",
Expand All @@ -708,6 +721,12 @@
}
]
},
{
"name": "flush",
"signature": "flush(): Promise<void>",
"description": "Resolve once everything emitted so far has been consumed, without emitting\nanything new. Useful after driving the agent by some other route (a\n`submit()`, a `switchThread()`) that has to reach the transport first.",
"params": []
},
{
"name": "getHistory",
"signature": "getHistory(threadId: string, signal: AbortSignal): Promise<ThreadState<DefaultValues>[]>",
Expand Down Expand Up @@ -975,7 +994,7 @@
{
"name": "streamErrorAt",
"type": "Signal<object | null>",
"description": "Epoch ms + classification of the most recent stream error. Resets on switchThread().",
"description": "Epoch ms + failure class of the most recent stream error. Resets on switchThread().\n\n`kind` is the AgentErrorKind of the normalized `AgentError`\n(`connection` | `auth` | `server` | `interrupted` | `aborted`) — the same\nvalue `agent.error()?.kind` carries. For a failure the runtime could not\nnormalize it falls back to the error's constructor name.",
"optional": false
},
{
Expand Down Expand Up @@ -1512,8 +1531,8 @@
{
"name": "interrupt",
"type": "Signal<AgentInterrupt | undefined>",
"description": "",
"optional": true
"description": "Current human-in-the-loop pause, or `undefined` when the run is not paused.\n\nNarrowed from the neutral `Agent` contract, where `interrupt` is optional\nbecause a runtime without human-in-the-loop support omits it. The LangGraph\nadapter always provides it, so `injectAgent().interrupt()` type-checks\ndirectly under `strictNullChecks` — no `?.()` needed.",
"optional": false
},
{
"name": "isLoading",
Expand Down Expand Up @@ -1946,7 +1965,7 @@
{
"name": "interrupt",
"type": "WritableSignal<AgentInterrupt | undefined>",
"description": "",
"description": "Current human-in-the-loop pause, or `undefined` when the run is not paused.\n\nNarrowed from the neutral `Agent` contract, where `interrupt` is optional\nbecause a runtime without human-in-the-loop support omits it. The LangGraph\nadapter always provides it, so `injectAgent().interrupt()` type-checks\ndirectly under `strictNullChecks` — no `?.()` needed.",
"optional": false
},
{
Expand Down Expand Up @@ -2499,7 +2518,7 @@
{
"name": "provideAgent",
"kind": "function",
"description": "Wire the LangGraph adapter into Angular's dependency injection.\n\nRegisters a singleton `LangGraphAgent` constructed from `config`. Retrieve it\nin any component with `injectAgent()`. Provide this at the application root\n(`app.config.ts`) for an app-wide agent.\n\nTo use a different agent in a component subtree, re-provide\n`provideAgent({...})` in that component's `providers: []` array —\nAngular's hierarchical DI scopes the singleton accordingly.\n\n**Static vs factory config.** Pass a plain `AgentConfig` object when the\nconfig is known up front. Pass a `() => AgentConfig` factory when the config\ndepends on runtime/DI state — the factory runs inside an Angular injection\ncontext, so it may call `inject()` to read services, route params, or\ncomponent-scoped signals.\n\n**Typed state via AgentRef.** Pass a typed ref as the first argument to flow\nthe state shape from `provideAgent` to `injectAgent` without repeating the\ngeneric at every call site.\n\n**Several agents at one injector level.** Each `provideAgent(ref, …)` call\nbuilds its own agent from its own config, so two (or more) refs may be\nprovided side by side in a single `providers` array and `injectAgent(refA)`\n/ `injectAgent(refB)` return distinct agents. The ref-less `injectAgent()`\nresolves a single shared token, which can only point at one of them: when\nmore than one ref is provided at the same level the **last** call wins.\nAlways inject by ref when an injector provides more than one agent.",
"description": "Wire the LangGraph adapter into Angular's dependency injection.\n\nRegisters a singleton `LangGraphAgent` constructed from `config`. Retrieve it\nin any component with `injectAgent()`. Provide this at the application root\n(`app.config.ts`) for an app-wide agent.\n\nTo use a different agent in a component subtree, re-provide\n`provideAgent({...})` in that component's `providers: []` array —\nAngular's hierarchical DI scopes the singleton accordingly.\n\n**Static vs factory config.** Pass a plain `AgentConfig` object when the\nconfig is known up front. Pass a `() => AgentConfig` factory when the config\ndepends on runtime/DI state — the factory runs inside an Angular injection\ncontext, so it may call `inject()` to read services, route params, or\ncomponent-scoped signals.\n\n**Typed state via AgentRef.** Pass a typed ref as the first argument to flow\nthe state shape from `provideAgent` to `injectAgent` without repeating the\ngeneric at every call site.\n\n**Several agents at one injector level.** Each `provideAgent(ref, …)` call\nbuilds its own agent from its own config, so two (or more) refs may be\nprovided side by side in a single `providers` array and `injectAgent(refA)`\n/ `injectAgent(refB)` return distinct agents. The ref-less `injectAgent()`\nresolves a single shared token, which can only point at one of them: when\nmore than one ref is provided at the same level the **last** call wins, and\nresolving it in dev mode logs a `console.warn` naming every competing ref.\nThe `AGENT_LIFECYCLE` token follows the same rule. Always inject by ref when\nan injector provides more than one agent.\n\n**Lifecycle token.** Every form also provides `AGENT_LIFECYCLE`, so\n`inject(AGENT_LIFECYCLE)` returns the same object as `injectAgent().lifecycle`\nwithout reaching for the agent itself.",
"signature": "provideAgent(ref: AgentRef<T>, configOrFactory: AgentConfig<T, BagTemplate> | () => AgentConfig<T>): Provider[]",
"params": [
{
Expand Down
25 changes: 19 additions & 6 deletions apps/website/content/docs/langgraph/api/mock-stream-transport.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,20 @@ describe('ChatComponent', () => {
fixture.detectChanges();

const stream = fixture.componentInstance.chat.submit({ message: 'Hello' });
transport.emit([
await transport.emit([
{
type: 'values',
messages: [{ type: 'ai', content: 'Hi there' }],
},
]);
transport.close();
await transport.close();
await stream;
fixture.detectChanges();

// submit() appends the user message optimistically, so the streamed
// assistant reply is the last entry, not the only one.
expect(fixture.componentInstance.chat.messages()).toEqual([
expect.objectContaining({ role: 'user', content: 'Hello' }),
expect.objectContaining({ role: 'assistant', content: 'Hi there' }),
]);
expect(fixture.componentInstance.chat.status()).toBe('idle');
Expand All @@ -69,7 +72,7 @@ describe('ChatComponent', () => {
fixture.detectChanges();

const stream = fixture.componentInstance.chat.submit({ message: 'Hello' });
transport.emitError(new Error('not found'));
await transport.emitError(new Error('not found'));
await stream;
fixture.detectChanges();

Expand All @@ -85,9 +88,10 @@ describe('ChatComponent', () => {
|--------|-------------|
| `constructor(script?: StreamEvent[][])` | Optionally seeds scripted event batches for manual stepping (defaults to `[]`). |
| `nextBatch()` | Returns the next scripted event batch. |
| `emit(events)` | Pushes one or more `StreamEvent` objects into the active stream. |
| `emitError(err)` | Makes the active stream reject with `err`. The runtime catches it, so `submit()` still resolves — assert on `status()` and `error()` instead of on a rejected promise. |
| `close()` | Closes the active stream after queued events drain. |
| `emit(events)` | Pushes one or more `StreamEvent` objects into the active stream. Returns a promise that resolves once the adapter has consumed the batch. |
| `emitError(err)` | Makes the active stream reject with `err`. The runtime catches it, so `submit()` still resolves — assert on `status()` and `error()` instead of on a rejected promise. Returns a promise that resolves once the stream has thrown. |
| `close()` | Closes the active stream after queued events drain. Returns a promise that resolves once the run has finished. |
| `flush()` | Resolves once everything emitted so far has been consumed, without emitting anything new. |
| `isStreaming()` | Returns whether a stream is currently active. |

Messages inside a `values` event are raw LangGraph messages, so their role comes from `type` (`'human'`, `'ai'`, `'tool'`, `'system'`), not from a `role` field. The projector reads `_getType()` or the raw `type` and falls back to `'ai'`; a `role` key is never read.
Expand All @@ -99,6 +103,15 @@ The transport also records calls in `streams`, `createdQueuedRuns`, `cancelledRu
or `close()`. This makes stream state and payload assertions deterministic.
</Callout>

<Callout type="warning" title="Await every emit">
`emit()`, `emitError()`, `close()` and `flush()` are asynchronous. `stream()`
is an async generator, so nothing has reached the signals at the moment
`emit()` returns. Await the promise before asserting — a spec that reads
signals synchronously after an emit reads stale state and passes or fails for
the wrong reason. `flush()` covers the same wait when the agent was driven by
something other than an emit.
</Callout>

## What's Next

<CardGroup cols={3}>
Expand Down
19 changes: 18 additions & 1 deletion apps/website/content/docs/langgraph/api/provide-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ provideAgent({
const chat = injectAgent();
```

Several agents may also coexist at one injector level. Each `provideAgent(ref, …)` call builds its own agent from its own config, so `injectAgent(refA)` and `injectAgent(refB)` return distinct instances. The ref-less `injectAgent()` resolves a single shared token, which can only point at one of them — the last ref-form call at that level wins. Always inject by ref when an injector provides more than one agent.
Several agents may also coexist at one injector level. Each `provideAgent(ref, …)` call builds its own agent from its own config, so `injectAgent(refA)` and `injectAgent(refB)` return distinct instances. The ref-less `injectAgent()` resolves a single shared token, which can only point at one of them — the last ref-form call at that level wins, and resolving it in development mode logs a warning naming every competing ref. Always inject by ref when an injector provides more than one agent.

```ts
export const LIVE = createAgentRef<ChatState>('live');
Expand All @@ -95,6 +95,23 @@ providers: [
// injectAgent(LIVE) !== injectAgent(REPLAY)
```

<Callout type="warning" title="Dev-mode warning on an ambiguous ref-less inject">
In development mode, resolving the ref-less token while several refs share the injector level logs a `console.warn` naming every competing ref and the one that won. It fires only on the ambiguous path: `injectAgent(LIVE)` and `injectAgent(REPLAY)` are unambiguous and stay silent. Production builds never log it.
</Callout>

## The `AGENT_LIFECYCLE` token

Every form of `provideAgent()` also provides `AGENT_LIFECYCLE`, so instrumentation can read an agent's [lifecycle signals](/docs/langgraph/guides/lifecycle) without depending on the agent itself:

```ts
import { inject } from '@angular/core';
import { AGENT_LIFECYCLE } from '@threadplane/langgraph';

const lifecycle = inject(AGENT_LIFECYCLE); // === injectAgent().lifecycle
```

The token follows the same last-one-wins rule as the ref-less `injectAgent()`: with several refs at one injector level it resolves the last ref's lifecycle. Read `injectAgent(ref).lifecycle` instead when that is the case.

## Transcript node filtering

LangGraph streams `messages-tuple` chunks for every LLM node in a run. If your graph has side-effect LLM nodes, such as a title generator or evaluator, set `transcriptNodeNames` so only your conversational node updates `messages()`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ The UI lifecycle is intentionally boring — and boring is the goal.
5. `stop()` aborts the active run when supported, and `retry()` clears `error()` and re-runs the last submitted input after a failure.
6. `regenerate(index)` rolls back from an assistant message and reruns from the preceding user message.

LangGraph adds deeper lifecycle and history surfaces. `@threadplane/langgraph` exposes `injectAgent().lifecycle` and exports `AgentLifecycle`, `AgentLifecycleRegistry`, and the low-level `AGENT_LIFECYCLE` token. Those are useful for telemetry, debugging, persistence, and time-travel UI. They are not required by `@threadplane/chat`.
LangGraph adds deeper lifecycle and history surfaces. `@threadplane/langgraph` exposes `injectAgent().lifecycle`, provides the `AGENT_LIFECYCLE` token from every `provideAgent()` call, and ships a root-provided `AgentLifecycleRegistry` that collects the lifecycle of every live agent. Those are useful for telemetry, debugging, persistence, and time-travel UI. They are not required by `@threadplane/chat`. See [Agent lifecycle signals](/docs/langgraph/guides/lifecycle).

`LangGraphAgent` also narrows one optional member of the neutral contract: `interrupt` is required, because the LangGraph adapter always provides it. Code written against `LangGraphAgent` calls `agent.interrupt()` directly; code written against the neutral `Agent` still needs `interrupt?.()`.

## Testing And Mocks

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ chat.messages(); // Message[]
chat.status(); // 'idle' | 'running' | 'error'
chat.isLoading(); // boolean
chat.error(); // AgentError | undefined
chat.interrupt?.(); // AgentInterrupt | undefined
chat.interrupt(); // AgentInterrupt | undefined
chat.history(); // AgentCheckpoint[]
chat.langGraphHistory(); // ThreadState[]
```
Expand Down
Loading
Loading