From 45d83900bfb5ec5354dd01fc941a82830084bef6 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 4 Apr 2026 17:03:54 -0700 Subject: [PATCH 1/4] docs(website): polish quickstart and installation with fixes --- .../docs-v2/getting-started/installation.mdx | 51 ++++++++++++++----- .../docs-v2/getting-started/quickstart.mdx | 39 ++++++++++---- 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/apps/website/content/docs-v2/getting-started/installation.mdx b/apps/website/content/docs-v2/getting-started/installation.mdx index 8200f7f67..32ccc01d8 100644 --- a/apps/website/content/docs-v2/getting-started/installation.mdx +++ b/apps/website/content/docs-v2/getting-started/installation.mdx @@ -32,11 +32,12 @@ Add `provideStreamResource()` to your application configuration. This sets globa // app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideStreamResource } from '@cacheplane/stream-resource'; +import { environment } from '../environments/environment'; export const appConfig: ApplicationConfig = { providers: [ provideStreamResource({ - apiUrl: process.env['LANGGRAPH_URL'] ?? 'http://localhost:2024', + apiUrl: environment.langgraphUrl, }), ], }; @@ -51,7 +52,14 @@ Any option passed to `streamResource()` directly overrides the global provider c -For local development, run a LangGraph server: +For local development, configure your environment and run a LangGraph server: + +```typescript +// src/environments/environment.ts +export const environment = { + langgraphUrl: 'http://localhost:2024', +}; +``` ```bash # Start LangGraph dev server @@ -66,9 +74,10 @@ langgraph dev For production, point to your LangGraph Cloud deployment: ```typescript -provideStreamResource({ - apiUrl: 'https://your-project.langgraph.app', -}) +// src/environments/environment.prod.ts +export const environment = { + langgraphUrl: 'https://your-project.langgraph.app', +}; ``` @@ -76,19 +85,27 @@ provideStreamResource({ ## Verify installation -Create a minimal test to verify the setup works: +Create a minimal component to verify the setup works. `streamResource()` must be called in an injection context (a component field initializer or inside `inject()`). ```typescript -import { streamResource } from '@cacheplane/stream-resource'; +// In a component field initializer (injection context) +const test = streamResource({ assistantId: 'chat_agent' }); +console.log(test.status()); // 'idle' — setup is correct +``` -// In a component -const test = streamResource({ - assistantId: 'chat_agent', -}); +## Troubleshooting -// If status() returns 'idle', the setup is correct -console.log(test.status()); // 'idle' -``` + + +**Version mismatch** -- If you see errors about missing APIs or unknown decorators, confirm your Angular version is 20 or later. Run `ng version` to check. Earlier versions do not support the injection context APIs that streamResource() relies on. + +**CORS errors** -- If the browser console shows `Access-Control-Allow-Origin` errors, your LangGraph server is not configured for cross-origin requests. The LangGraph dev server allows all origins by default. For production, make sure your deployment's CORS policy includes your Angular app's domain. + +**Connection refused** -- If you see `ERR_CONNECTION_REFUSED`, verify your LangGraph server is running and that the `apiUrl` matches the correct host and port. Run `langgraph dev` and confirm the server starts at the expected address (default `http://localhost:2024`). + +**"NullInjectorError: No provider for StreamResourceConfig"** -- You forgot to add `provideStreamResource()` to your `appConfig` providers array. See the [Configure the provider](#configure-the-provider) section above. + + ## Next steps @@ -99,4 +116,10 @@ console.log(test.status()); // 'idle' Understand how Signals power streamResource + + Graphs, nodes, edges, and state for Angular developers + + + Complete streamResource() function reference + diff --git a/apps/website/content/docs-v2/getting-started/quickstart.mdx b/apps/website/content/docs-v2/getting-started/quickstart.mdx index de1aee2fb..90cce9b3b 100644 --- a/apps/website/content/docs-v2/getting-started/quickstart.mdx +++ b/apps/website/content/docs-v2/getting-started/quickstart.mdx @@ -6,13 +6,15 @@ Build a streaming chat component with streamResource() in 5 minutes. Angular 20+ project with Node.js 18+. If you need setup help, see the [Installation](/docs/getting-started/installation) guide. -## 1. Install + + ```bash npm install @cacheplane/stream-resource ``` -## 2. Configure the provider + + Add `provideStreamResource()` to your application config with your LangGraph Platform URL. @@ -29,7 +31,8 @@ export const appConfig: ApplicationConfig = { }; ``` -## 3. Create a chat component + + Use `streamResource()` in a component field initializer. Every property on the returned ref is an Angular Signal. @@ -38,17 +41,19 @@ Use `streamResource()` in a component field initializer. Every property on the r ```typescript // chat.component.ts -import { Component, signal, computed } from '@angular/core'; +import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core'; import { streamResource } from '@cacheplane/stream-resource'; import type { BaseMessage } from '@langchain/core/messages'; @Component({ selector: 'app-chat', templateUrl: './chat.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChatComponent { input = signal(''); + // 'chat_agent' maps to the key in your langgraph.json "graphs" config chat = streamResource<{ messages: BaseMessage[] }>({ assistantId: 'chat_agent', threadId: signal(localStorage.getItem('threadId')), @@ -82,6 +87,11 @@ export class ChatComponent {
Agent is thinking...
} + + @if (chat.error(); as err) { +
{{ err.message }}
+ } +
-## 4. Start your LangGraph server + + Make sure your LangGraph agent is running at the URL you configured. @@ -104,7 +115,8 @@ Make sure your LangGraph agent is running at the URL you configured. langgraph dev ``` -## 5. Run your app + + ```bash ng serve @@ -112,9 +124,12 @@ ng serve Open `http://localhost:4200` and start chatting with your agent. + + + ## Next steps - + Learn about token-by-token updates and stream modes @@ -124,7 +139,13 @@ Open `http://localhost:4200` and start chatting with your agent. Add human-in-the-loop approval flows - - Test your agent integration deterministically + + Deep dive into how Signals power streamResource + + + Graphs, nodes, edges, and state for Angular developers + + + Complete streamResource() function reference From 0783024a8f6a1b306784b1535418468e48547bc4 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 4 Apr 2026 17:04:34 -0700 Subject: [PATCH 2/4] docs(website): expand API reference pages with navigation and context --- .../docs-v2/api/fetch-stream-transport.mdx | 43 ++++++++- .../docs-v2/api/mock-stream-transport.mdx | 87 +++++++++++++++++-- .../docs-v2/api/provide-stream-resource.mdx | 48 +++++++++- .../content/docs-v2/api/stream-resource.mdx | 53 ++++++++++- 4 files changed, 218 insertions(+), 13 deletions(-) diff --git a/apps/website/content/docs-v2/api/fetch-stream-transport.mdx b/apps/website/content/docs-v2/api/fetch-stream-transport.mdx index be313baed..790698e84 100644 --- a/apps/website/content/docs-v2/api/fetch-stream-transport.mdx +++ b/apps/website/content/docs-v2/api/fetch-stream-transport.mdx @@ -2,7 +2,12 @@ `FetchStreamTransport` is the production-ready transport that opens a real server-sent event connection using the browser's `fetch` API and reads a `ReadableStream` response body. It is the default transport you register with `provideStreamResource` in production builds. -You rarely need to interact with `FetchStreamTransport` directly — simply provide it once at the application level and every `streamResource` will use it automatically. You would reach for it explicitly only when constructing a resource outside the normal DI tree or when you need to override the transport for a single resource while keeping the global default intact. +## When you interact with it directly + +In most apps you will never import or inject `FetchStreamTransport` by name — you register it once in `provideStreamResource` and forget about it. The two cases where you reach for it explicitly are: + +1. **Per-resource override** — you want one resource to use a different transport than the global default while everything else stays on `FetchStreamTransport`. +2. **Outside the DI tree** — you are constructing a resource in a context where global providers are not available and you need to supply the transport manually. ```ts import { inject } from '@angular/core'; @@ -15,10 +20,46 @@ const events = streamResource({ }); ``` +## How it works + +`FetchStreamTransport` makes a `fetch` call to the given URL and expects the server to respond with `Content-Type: text/event-stream`. It then reads the `ReadableStream` body line-by-line, parses SSE `data:` fields, and emits each parsed JSON value into the resource signal. + +The transport handles: + +- **Backpressure** — reads chunks at the pace the browser delivers them +- **Cancellation** — aborts the underlying `fetch` when `interrupt()` is called or the resource is destroyed +- **Error propagation** — network errors and non-2xx responses surface through `resource.error()` + `FetchStreamTransport` implements the `StreamTransport` interface. You can create custom transports (e.g. WebSocket-backed) by implementing the same interface and providing them in place of this class. +## What's Next + + + + Learn how the SSE lifecycle maps to resource signals and how to handle reconnects. + + + Server configuration for SSE: headers, timeouts, and edge runtime considerations. + + + The test-time counterpart — push values synchronously without a real server. + + + {/* Auto-rendered from api-docs.json — see page component */} diff --git a/apps/website/content/docs-v2/api/mock-stream-transport.mdx b/apps/website/content/docs-v2/api/mock-stream-transport.mdx index d9ebd13c8..31fbb93fd 100644 --- a/apps/website/content/docs-v2/api/mock-stream-transport.mdx +++ b/apps/website/content/docs-v2/api/mock-stream-transport.mdx @@ -2,31 +2,100 @@ `MockStreamTransport` is a test-friendly transport that replaces real network calls with an in-memory event emitter. Use it in unit and component tests to push values on demand and assert against your component's reactive state without a running server. +## Complete test example + +The pattern below covers the full lifecycle: configure the transport in `TestBed`, create the component, emit values, and assert signal state. + ```ts +import { Component, inject } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { provideStreamResource, MockStreamTransport, + streamResource, } from '@cacheplane/stream-resource'; -beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideStreamResource({ transport: MockStreamTransport })], +@Component({ template: '' }) +class RepoComponent { + readonly repo = streamResource<{ name: string }>({ + url: () => '/api/repos/42', }); -}); +} -it('reflects streamed value', () => { - const transport = TestBed.inject(MockStreamTransport); - // Emit a value into the stream - transport.emit('/api/repos/42', { id: 42, name: 'my-repo' }); - // Assert your component's signal updated accordingly +describe('RepoComponent', () => { + let transport: MockStreamTransport; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RepoComponent], + providers: [provideStreamResource({ transport: MockStreamTransport })], + }); + transport = TestBed.inject(MockStreamTransport); + }); + + it('reflects the streamed value', () => { + const fixture = TestBed.createComponent(RepoComponent); + fixture.detectChanges(); + + // Push a value into the stream — synchronous, no fakeAsync needed + transport.emit('/api/repos/42', { name: 'my-repo' }); + fixture.detectChanges(); + + expect(fixture.componentInstance.repo.value()).toEqual({ name: 'my-repo' }); + expect(fixture.componentInstance.repo.status()).toBe('streaming'); + }); + + it('surfaces errors through the error signal', () => { + const fixture = TestBed.createComponent(RepoComponent); + fixture.detectChanges(); + + transport.error('/api/repos/42', new Error('not found')); + fixture.detectChanges(); + + expect(fixture.componentInstance.repo.status()).toBe('error'); + expect(fixture.componentInstance.repo.error()).toBeInstanceOf(Error); + }); }); ``` +## MockStreamTransport API + +| Method | Description | +|--------|-------------| +| `emit(url, value)` | Pushes a single value into the stream at the given URL path. | +| `error(url, err)` | Triggers an error on the stream at the given URL path. | +| `complete(url)` | Closes the stream cleanly, as if the server sent the final event. | + Because `MockStreamTransport` is synchronous by default, you can emit values and assert state changes in the same test tick — no `fakeAsync` or `tick` required. +## What's Next + + + + Full testing patterns including component harnesses and multi-stream scenarios. + + + The production transport that MockStreamTransport replaces in tests. + + + Full reference for the primitive you are testing against. + + + {/* Auto-rendered from api-docs.json — see page component */} diff --git a/apps/website/content/docs-v2/api/provide-stream-resource.mdx b/apps/website/content/docs-v2/api/provide-stream-resource.mdx index 4863cf489..3375f0b2f 100644 --- a/apps/website/content/docs-v2/api/provide-stream-resource.mdx +++ b/apps/website/content/docs-v2/api/provide-stream-resource.mdx @@ -1,6 +1,8 @@ # provideStreamResource() -`provideStreamResource` is the provider factory that registers `stream-resource` in Angular's dependency injection system. Call it inside `bootstrapApplication` (or an `ApplicationConfig`) to configure the transport and any global defaults used by every `streamResource` in your app. +`provideStreamResource` is the provider factory that registers `stream-resource` in Angular's dependency injection system. Call it once inside `bootstrapApplication` (or an `ApplicationConfig`) to configure the transport and any global defaults used by every `streamResource` in your app. + +This is the single configuration point for the entire library. Rather than configuring each resource individually, you declare your transport here and every `streamResource` call throughout the app inherits it automatically. ```ts import { bootstrapApplication } from '@angular/platform-browser'; @@ -19,10 +21,54 @@ bootstrapApplication(AppComponent, { }); ``` +## Global configuration + +| Option | Type | Description | +|--------|------|-------------| +| `transport` | `Type` | The transport class to inject when resources request `StreamTransport`. Required. | + +## Swapping transports by environment + +Because `provideStreamResource` accepts a class token, you can vary the transport based on your environment without touching any component code: + +```ts +// main.ts — production +provideStreamResource({ transport: FetchStreamTransport }) + +// main.spec.ts / TestBed — tests +provideStreamResource({ transport: MockStreamTransport }) +``` + Swap `FetchStreamTransport` for `MockStreamTransport` (or any custom class implementing the `StreamTransport` interface) to change the transport for all resources at once — useful for testing or SSR. +## What's Next + + + + Step-by-step setup guide including peer dependencies and NgModule support. + + + Configure transports for production, SSR, and edge runtimes. + + + Full reference for the core primitive you configure here. + + + {/* Auto-rendered from api-docs.json — see page component */} diff --git a/apps/website/content/docs-v2/api/stream-resource.mdx b/apps/website/content/docs-v2/api/stream-resource.mdx index e383d3164..fb4135410 100644 --- a/apps/website/content/docs-v2/api/stream-resource.mdx +++ b/apps/website/content/docs-v2/api/stream-resource.mdx @@ -2,6 +2,8 @@ `streamResource` is the core primitive of the library. It creates a reactive resource that opens a server-sent event stream, tracks loading and error states, and exposes the latest emitted value — all within Angular's signal-based reactivity model. +When the `url` signal changes, the resource tears down the previous connection and opens a fresh one automatically. You never write subscription management or cleanup logic yourself. + ```ts import { streamResource } from '@cacheplane/stream-resource'; @@ -12,10 +14,31 @@ const repo = streamResource({ }); // Use in template -// repo.value() — latest emitted value (or undefined) -// repo.status() — 'idle' | 'loading' | 'streaming' | 'error' +// repo.value() — latest emitted value (or undefined) +// repo.status() — 'idle' | 'loading' | 'streaming' | 'error' +// repo.error() — the thrown error, when status is 'error' +// repo.interrupt() — call to cancel the stream immediately ``` +## Key signals + +| Signal | Type | Description | +|--------|------|-------------| +| `value()` | `T \| undefined` | The latest value emitted by the stream. Starts as `undefined` and updates with each SSE event. | +| `status()` | `'idle' \| 'loading' \| 'streaming' \| 'error'` | Lifecycle state of the current connection. | +| `error()` | `unknown` | The error thrown when `status()` is `'error'`. `undefined` otherwise. | +| `interrupt()` | `() => void` | Closes the active stream without an error — useful for user-initiated cancellation. | + +## When to use + +Use `streamResource` whenever your UI needs to react to a live data stream from the server: + +- **AI / LLM responses** — stream tokens into a chat bubble as they arrive +- **Live feeds** — stock tickers, activity logs, or progress updates +- **Long-running jobs** — subscribe to backend task progress over SSE + +For plain HTTP requests that return a single value and complete, Angular's built-in `resource()` or `httpResource()` is a better fit. + `streamResource` must be called during construction, inside an injection context (e.g. a component constructor, field initializer, or a function @@ -23,4 +46,30 @@ const repo = streamResource({ will throw. +## What's Next + + + + Build your first streaming component end-to-end in under five minutes. + + + Deep dive into SSE lifecycle, error handling, and reconnect strategies. + + + Understand how stream-resource integrates with Angular's reactivity model. + + + {/* Auto-rendered from api-docs.json — see page component */} From 9d5c1dc507a39457ce9e57b02032eda77e52fe03 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 4 Apr 2026 17:05:16 -0700 Subject: [PATCH 3/4] docs(website): polish streaming, time-travel, subgraphs with Python code --- .../content/docs-v2/guides/streaming.mdx | 66 ++++++++- .../content/docs-v2/guides/subgraphs.mdx | 121 +++++++++++++--- .../content/docs-v2/guides/time-travel.mdx | 134 +++++++++++++++++- 3 files changed, 297 insertions(+), 24 deletions(-) diff --git a/apps/website/content/docs-v2/guides/streaming.mdx b/apps/website/content/docs-v2/guides/streaming.mdx index 0a9df886a..3e10aec03 100644 --- a/apps/website/content/docs-v2/guides/streaming.mdx +++ b/apps/website/content/docs-v2/guides/streaming.mdx @@ -6,19 +6,67 @@ StreamResource provides token-by-token streaming from LangGraph agents via Serve Make sure you've completed the Installation guide first. -## Basic streaming +## How streaming works -Create a `streamResource` in your component, pass it a message, and bind to the resulting signals. +Streaming starts on the agent side. LangGraph's `astream()` method controls what data is sent over the SSE connection. On the Angular side, `streamResource()` consumes those events and maps them to Signals. + + +```python +from langgraph.graph import END, START, MessagesState, StateGraph +from langchain_openai import ChatOpenAI + +llm = ChatOpenAI(model="gpt-5-mini", streaming=True) + +def call_model(state: MessagesState) -> dict: + response = llm.invoke(state["messages"]) + return {"messages": [response]} + +builder = StateGraph(MessagesState) +builder.add_node("call_model", call_model) +builder.add_edge(START, "call_model") +builder.add_edge("call_model", END) + +graph = builder.compile() + +# Stream modes control what SSE chunks contain: + +# "values" — full state snapshot after each node +async for chunk in graph.astream( + {"messages": [("user", "Hello")]}, + stream_mode="values", +): + print(chunk) + +# "messages" — individual message tokens as generated +async for chunk in graph.astream( + {"messages": [("user", "Hello")]}, + stream_mode="messages", +): + print(chunk) + +# "events" — raw run events (on_chain_start, on_llm_stream, etc.) +async for event in graph.astream_events( + {"messages": [("user", "Hello")]}, + version="v2", +): + print(event["event"], event.get("data")) +``` + + ```typescript -import { Component, computed } from '@angular/core'; +import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; import { streamResource } from '@cacheplane/stream-resource'; import { BaseMessage } from '@langchain/core/messages'; -@Component({ selector: 'app-chat', templateUrl: './chat.component.html' }) +@Component({ + selector: 'app-chat', + templateUrl: './chat.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) export class ChatComponent { readonly chat = streamResource<{ messages: BaseMessage[] }>({ assistantId: 'chat_agent', @@ -60,7 +108,7 @@ The `status()` signal reports the current lifecycle state of the SSE connection: No active stream. The resource is ready to accept a new message. - + Tokens are arriving over the SSE connection. Signal values update in real-time with each chunk. @@ -125,11 +173,15 @@ If the SSE connection drops or the agent throws, `status()` transitions to `'err ```typescript -import { Component, computed, effect } from '@angular/core'; +import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; import { streamResource } from '@cacheplane/stream-resource'; import { BaseMessage } from '@langchain/core/messages'; -@Component({ selector: 'app-chat', templateUrl: './chat.component.html' }) +@Component({ + selector: 'app-chat', + templateUrl: './chat.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) export class ChatComponent { readonly chat = streamResource<{ messages: BaseMessage[] }>({ assistantId: 'chat_agent', diff --git a/apps/website/content/docs-v2/guides/subgraphs.mdx b/apps/website/content/docs-v2/guides/subgraphs.mdx index 5519b47be..3c0f114fa 100644 --- a/apps/website/content/docs-v2/guides/subgraphs.mdx +++ b/apps/website/content/docs-v2/guides/subgraphs.mdx @@ -6,6 +6,102 @@ Subgraphs let you compose complex agents from smaller, focused units. streamReso LangGraph calls them subgraphs (modular graph composition). Deep Agents calls them subagents (task delegation). streamResource() supports both patterns through the same API. +## How subgraph composition works + +Subgraph composition starts on the agent side. Each subgraph is a fully compiled `StateGraph` that can be added as a node in a parent graph. + + + + +```python +from langgraph.graph import END, START, MessagesState, StateGraph +from langchain_openai import ChatOpenAI + +llm = ChatOpenAI(model="gpt-5-mini") + +# --- Research subgraph --- +def search_web(state: MessagesState) -> dict: + query = state["messages"][-1].content + results = web_search(query) + return {"messages": [{"role": "assistant", "content": results}]} + +def summarize_results(state: MessagesState) -> dict: + response = llm.invoke(state["messages"]) + return {"messages": [response]} + +research_builder = StateGraph(MessagesState) +research_builder.add_node("search", search_web) +research_builder.add_node("summarize", summarize_results) +research_builder.add_edge(START, "search") +research_builder.add_edge("search", "summarize") +research_builder.add_edge("summarize", END) + +research_subgraph = research_builder.compile() + +# --- Analysis subgraph --- +def analyze_data(state: MessagesState) -> dict: + response = llm.invoke([ + {"role": "system", "content": "Analyze the data and provide insights."}, + *state["messages"], + ]) + return {"messages": [response]} + +analysis_builder = StateGraph(MessagesState) +analysis_builder.add_node("analyze", analyze_data) +analysis_builder.add_edge(START, "analyze") +analysis_builder.add_edge("analyze", END) + +analysis_subgraph = analysis_builder.compile() + +# --- Parent orchestrator --- +def route_task(state: MessagesState) -> str: + last = state["messages"][-1].content.lower() + if "research" in last or "search" in last: + return "research" + return "analyze" + +builder = StateGraph(MessagesState) +builder.add_node("research", research_subgraph) +builder.add_node("analyze", analysis_subgraph) +builder.add_conditional_edges(START, route_task) +builder.add_edge("research", END) +builder.add_edge("analyze", END) + +graph = builder.compile() +``` + + + + +```typescript +import { Component, computed, inject, effect, ChangeDetectionStrategy } from '@angular/core'; +import { streamResource } from '@cacheplane/stream-resource'; + +@Component({ + selector: 'app-orchestrator', + templateUrl: './orchestrator.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OrchestratorComponent { + readonly orchestrator = streamResource({ + assistantId: 'orchestrator', + subagentToolNames: ['research', 'analyze'], + }); + + readonly running = computed(() => this.orchestrator.activeSubagents()); + readonly runningCount = computed(() => this.running().length); + + send(text: string) { + this.orchestrator.submit({ + messages: [{ role: 'user', content: text }], + }); + } +} +``` + + + + ## Tracking subagent execution The `subagents()` signal contains a Map of active subagent streams. Use it to inspect the full set of delegated tasks and their current state. @@ -64,7 +160,7 @@ const pipelineStatus = computed(() => { return { total: entries.length, pending: entries.filter(([, a]) => a.status() === 'pending').length, - running: entries.filter(([, a]) => a.status() === 'streaming').length, + running: entries.filter(([, a]) => a.status() === 'loading').length, done: entries.filter(([, a]) => a.status() === 'complete').length, failed: entries.filter(([, a]) => a.status() === 'error').length, }; @@ -78,21 +174,12 @@ Render live progress for each subagent using the signals above. ```typescript -import { computed } from '@angular/core'; +import { Component, computed, inject, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-subagent-progress', - template: ` - @for (entry of subagentEntries(); track entry[0]) { -
- {{ entry[0] }} - {{ entry[1].status() }} - @if (entry[1].status() === 'error') { - {{ entry[1].error()?.message }} - } -
- } - `, + templateUrl: './progress-panel.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SubagentProgressComponent { orchestrator = inject(OrchestratorService).resource; @@ -103,9 +190,8 @@ export class SubagentProgressComponent { } ```
- + ```html - @for (entry of subagentEntries(); track entry[0]) {
{{ entry[0] }} @@ -113,7 +199,7 @@ export class SubagentProgressComponent { {{ entry[1].status() }} - @if (entry[1].status() === 'streaming') { + @if (entry[1].status() === 'loading') { } @@ -186,6 +272,9 @@ Use **subagents** when tasks are independent and can run in parallel, when each Understand how streamResource() surfaces tokens, status, and errors in real time. + + Inspect earlier states and replay alternate execution paths with checkpoint history. + Write unit and integration tests for orchestrator graphs and subagent interactions. diff --git a/apps/website/content/docs-v2/guides/time-travel.mdx b/apps/website/content/docs-v2/guides/time-travel.mdx index 743025e54..570c1c084 100644 --- a/apps/website/content/docs-v2/guides/time-travel.mdx +++ b/apps/website/content/docs-v2/guides/time-travel.mdx @@ -6,6 +6,97 @@ Time travel lets you inspect earlier states and replay alternate execution paths Debug agent decisions, explore alternate paths, and build undo/redo experiences for your users. Time travel works with any LangGraph agent that persists checkpoints to a thread. +## How checkpointing works + +Time travel depends on checkpointing on the agent side. LangGraph automatically saves a checkpoint after every node execution when you compile your graph with a checkpointer. + + + + +```python +from langgraph.graph import END, START, MessagesState, StateGraph +from langgraph.checkpoint.memory import MemorySaver +from langchain_openai import ChatOpenAI + +llm = ChatOpenAI(model="gpt-5-mini") + +def call_model(state: MessagesState) -> dict: + response = llm.invoke(state["messages"]) + return {"messages": [response]} + +builder = StateGraph(MessagesState) +builder.add_node("call_model", call_model) +builder.add_edge(START, "call_model") +builder.add_edge("call_model", END) + +# Compile with a checkpointer to enable time travel +checkpointer = MemorySaver() +graph = builder.compile(checkpointer=checkpointer) + +# Run the graph with a thread ID +config = {"configurable": {"thread_id": "user_123"}} +result = graph.invoke( + {"messages": [("user", "What is LangGraph?")]}, + config=config, +) + +# Browse checkpoint history server-side +for state in graph.get_state_history(config): + print(f"Step: {state.metadata.get('step', '?')}") + print(f" Checkpoint: {state.config['configurable']['checkpoint_id']}") + print(f" Messages: {len(state.values.get('messages', []))}") + +# Replay from a specific checkpoint +past_config = { + "configurable": { + "thread_id": "user_123", + "checkpoint_id": "", + } +} +past_state = graph.get_state(past_config) +``` + + + + +```typescript +import { Component, inject, computed, ChangeDetectionStrategy } from '@angular/core'; +import { streamResource } from '@cacheplane/stream-resource'; +import { AgentService } from './agent.service'; + +@Component({ + selector: 'app-history-viewer', + templateUrl: './history-viewer.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HistoryViewerComponent { + private agentService = inject(AgentService); + readonly agent = this.agentService.agent; + + readonly checkpoints = computed(() => this.agent.history()); + readonly checkpointCount = computed(() => this.agent.history().length); + + readonly activeIndex = computed(() => + this.checkpoints().length - 1 + ); + + fork(index: number) { + const checkpoint = this.checkpoints()[index]; + this.agent.submit( + { messages: [{ role: 'user', content: 'Try a different approach' }] }, + { checkpoint: checkpoint.checkpoint } + ); + } + + formatTime(isoString: string): string { + return new Date(isoString).toLocaleTimeString(); + } +} +``` + + + + ## Browsing execution history The `history()` signal contains an array of `ThreadState` checkpoints ordered from oldest to newest. Each checkpoint captures the complete agent state at that point in execution, including messages, intermediate results, and any custom state fields. @@ -79,13 +170,14 @@ Expose checkpoint history directly in your component to let users scrub through ```typescript -import { Component, inject, computed } from '@angular/core'; +import { Component, inject, computed, ChangeDetectionStrategy } from '@angular/core'; import { streamResource } from '@cacheplane/stream-resource'; import { AgentService } from './agent.service'; @Component({ selector: 'app-history-viewer', templateUrl: './history-viewer.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class HistoryViewerComponent { private agentService = inject(AgentService); @@ -152,6 +244,43 @@ compareCheckpoints(indexA: number, indexB: number) { Use the comparison result to render a diff view, highlight changed fields in your UI, or log what the agent modified during a specific step. +## Replaying with modified input + +Combine forking with new input to explore how the agent would have responded differently. This is the core of the undo/redo experience. + +```typescript +@Component({ + selector: 'app-replay', + templateUrl: './replay.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReplayComponent { + readonly agent = inject(AgentService).agent; + + readonly history = computed(() => this.agent.history()); + readonly canUndo = computed(() => this.history().length > 1); + + undo() { + const history = this.history(); + if (history.length < 2) return; + + // Go back one step + const previousCheckpoint = history[history.length - 2]; + this.agent.submit(undefined, { + checkpoint: previousCheckpoint.checkpoint, + }); + } + + replayWith(index: number, newMessage: string) { + const checkpoint = this.history()[index]; + this.agent.submit( + { messages: [{ role: 'user', content: newMessage }] }, + { checkpoint: checkpoint.checkpoint } + ); + } +} +``` + Time travel is most useful during development. Inspect why an agent chose a particular path by comparing adjacent checkpoints, then fork to test alternatives without restarting the conversation. Combine `history()` with Angular DevTools to watch checkpoint arrays update in real time as the agent streams. @@ -165,6 +294,9 @@ Time travel is most useful during development. Inspect why an agent chose a part Understand how streamResource() surfaces incremental updates and how history integrates with live streaming state. + + Compose multi-agent systems with orchestrators and track subagent execution. + Full reference for streamResource() options, signals, and the submit() API including checkpoint parameters. From bb41a7a550d6ee543c553d6a8b3936bf49c22a3e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 4 Apr 2026 17:07:07 -0700 Subject: [PATCH 4/4] fix(website): fix Callout type warn in installation --- apps/website/content/docs-v2/getting-started/installation.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/website/content/docs-v2/getting-started/installation.mdx b/apps/website/content/docs-v2/getting-started/installation.mdx index 32ccc01d8..83ff4fe0f 100644 --- a/apps/website/content/docs-v2/getting-started/installation.mdx +++ b/apps/website/content/docs-v2/getting-started/installation.mdx @@ -95,7 +95,7 @@ console.log(test.status()); // 'idle' — setup is correct ## Troubleshooting - + **Version mismatch** -- If you see errors about missing APIs or unknown decorators, confirm your Angular version is 20 or later. Run `ng version` to check. Earlier versions do not support the injection context APIs that streamResource() relies on.