diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5422355b..c41fa320d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,7 @@ jobs: cache: npm - run: npm ci - run: npx nx lint website + - run: npm run generate-api-docs # nx build website triggers demo:build first (dependsOn in project.json) - run: npx nx build website diff --git a/apps/website/content/docs-v2/api/api-docs.json b/apps/website/content/docs-v2/api/api-docs.json new file mode 100644 index 000000000..1f96975a4 --- /dev/null +++ b/apps/website/content/docs-v2/api/api-docs.json @@ -0,0 +1,762 @@ +[ + { + "name": "FetchStreamTransport", + "kind": "class", + "description": "Production transport that connects to a LangGraph Platform API via HTTP and SSE.\n\nCreates threads automatically if no threadId is provided, and streams events\nusing the LangGraph SDK client.", + "params": [ + { + "name": "apiUrl", + "type": "string", + "description": "Base URL of the LangGraph Platform API", + "optional": false + }, + { + "name": "onThreadId", + "type": "object", + "description": "Optional callback invoked when a new thread is created", + "optional": true + } + ], + "examples": [ + "```typescript\nconst transport = new FetchStreamTransport(\n 'http://localhost:2024',\n (id) => console.log('New thread:', id),\n);\n```" + ], + "properties": [], + "methods": [ + { + "name": "joinStream", + "signature": "joinStream(threadId: string, runId: string, lastEventId: string | undefined, signal: AbortSignal)", + "description": "Join an already-started run without creating a new thread.", + "params": [ + { + "name": "threadId", + "type": "string", + "description": "", + "optional": false + }, + { + "name": "runId", + "type": "string", + "description": "", + "optional": false + }, + { + "name": "lastEventId", + "type": "string | undefined", + "description": "", + "optional": false + }, + { + "name": "signal", + "type": "AbortSignal", + "description": "", + "optional": false + } + ] + }, + { + "name": "stream", + "signature": "stream(assistantId: string, threadId: string | null, payload: unknown, signal: AbortSignal)", + "description": "Open a streaming connection, creating a thread if needed.", + "params": [ + { + "name": "assistantId", + "type": "string", + "description": "", + "optional": false + }, + { + "name": "threadId", + "type": "string | null", + "description": "", + "optional": false + }, + { + "name": "payload", + "type": "unknown", + "description": "", + "optional": false + }, + { + "name": "signal", + "type": "AbortSignal", + "description": "", + "optional": false + } + ] + } + ] + }, + { + "name": "MockStreamTransport", + "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.", + "params": [ + { + "name": "script", + "type": "StreamEvent[][]", + "description": "Array of event batches. Each batch is emitted as a group.", + "optional": false + } + ], + "examples": [ + "```typescript\nconst transport = new MockStreamTransport([\n [{ type: 'values', data: { messages: [aiMsg('Hello')] } }],\n [{ type: 'values', data: { status: 'done' } }],\n]);\n```" + ], + "properties": [], + "methods": [ + { + "name": "close", + "signature": "close()", + "description": "Close the stream. Remaining queued events are drained before completion.", + "params": [] + }, + { + "name": "emit", + "signature": "emit(events: StreamEvent[])", + "description": "Manually emit events into the stream.", + "params": [ + { + "name": "events", + "type": "StreamEvent[]", + "description": "", + "optional": false + } + ] + }, + { + "name": "emitError", + "signature": "emitError(err: Error)", + "description": "Inject an error into the stream.", + "params": [ + { + "name": "err", + "type": "Error", + "description": "", + "optional": false + } + ] + }, + { + "name": "isStreaming", + "signature": "isStreaming()", + "description": "Returns true if a stream is currently active.", + "params": [] + }, + { + "name": "nextBatch", + "signature": "nextBatch()", + "description": "Advance to the next scripted batch and return its events.", + "params": [] + }, + { + "name": "stream", + "signature": "stream(_assistantId: string, _threadId: string | null, _payload: unknown, signal: AbortSignal)", + "description": "Open a streaming connection to an agent and yield events.", + "params": [ + { + "name": "_assistantId", + "type": "string", + "description": "", + "optional": false + }, + { + "name": "_threadId", + "type": "string | null", + "description": "", + "optional": false + }, + { + "name": "_payload", + "type": "unknown", + "description": "", + "optional": false + }, + { + "name": "signal", + "type": "AbortSignal", + "description": "", + "optional": false + } + ] + } + ] + }, + { + "name": "Interrupt", + "kind": "interface", + "description": "An interrupt thrown inside a thread.", + "properties": [ + { + "name": "id", + "type": "string", + "description": "The ID of the interrupt.", + "optional": true + }, + { + "name": "ns", + "type": "string[]", + "description": "The namespace of the interrupt.", + "optional": true + }, + { + "name": "resumable", + "type": "boolean", + "description": "Whether the interrupt can be resumed.", + "optional": true + }, + { + "name": "value", + "type": "TValue", + "description": "The value of the interrupt.", + "optional": true + }, + { + "name": "when", + "type": "string & {} | \"during\"", + "description": "Will be deprecated in the future.", + "optional": true + } + ], + "examples": [] + }, + { + "name": "StreamEvent", + "kind": "interface", + "description": "An event emitted by a LangGraph stream.", + "properties": [ + { + "name": "type", + "type": "\"error\" | \"values\" | \"messages\" | `messages/${string}` | \"updates\" | \"tools\" | \"custom\" | \"metadata\" | \"checkpoints\" | \"tasks\" | \"debug\" | \"events\" | \"interrupt\" | \"interrupts\"", + "description": "Event type identifier (e.g., 'values', 'messages', 'error', 'interrupt').", + "optional": false + } + ], + "examples": [] + }, + { + "name": "StreamResourceConfig", + "kind": "interface", + "description": "Global configuration for streamResource instances.\nProperties set here serve as defaults that can be overridden per-call.", + "properties": [ + { + "name": "apiUrl", + "type": "string", + "description": "Base URL of the LangGraph Platform API (e.g., `'http://localhost:2024'`).", + "optional": true + }, + { + "name": "transport", + "type": "StreamResourceTransport", + "description": "Custom transport implementation. Defaults to FetchStreamTransport.", + "optional": true + } + ], + "examples": [] + }, + { + "name": "StreamResourceOptions", + "kind": "interface", + "description": "Options for creating a streaming resource via streamResource.", + "properties": [ + { + "name": "apiUrl", + "type": "string", + "description": "Base URL of the LangGraph Platform API.", + "optional": false + }, + { + "name": "assistantId", + "type": "string", + "description": "Agent or graph identifier on the LangGraph platform.", + "optional": false + }, + { + "name": "filterSubagentMessages", + "type": "boolean", + "description": "When true, subagent messages are filtered from the main messages signal.", + "optional": true + }, + { + "name": "initialValues", + "type": "Partial", + "description": "Initial state values before the first stream response arrives.", + "optional": true + }, + { + "name": "messagesKey", + "type": "string", + "description": "Key in the state object that contains the messages array. Defaults to `'messages'`.", + "optional": true + }, + { + "name": "onThreadId", + "type": "object", + "description": "Called when a new thread is auto-created by the transport.", + "optional": true + }, + { + "name": "subagentToolNames", + "type": "string[]", + "description": "Tool names that indicate a subagent invocation.", + "optional": true + }, + { + "name": "threadId", + "type": "string | Signal | null", + "description": "Thread ID to connect to. Pass a Signal for reactive thread switching.", + "optional": true + }, + { + "name": "throttle", + "type": "number | false", + "description": "Throttle signal updates in milliseconds. `false` to disable.", + "optional": true + }, + { + "name": "toMessage", + "type": "object", + "description": "Custom message deserializer for non-standard message formats.", + "optional": true + }, + { + "name": "transport", + "type": "StreamResourceTransport", + "description": "Custom transport. Defaults to FetchStreamTransport.", + "optional": true + } + ], + "examples": [] + }, + { + "name": "StreamResourceRef", + "kind": "interface", + "description": "Reactive reference returned by streamResource. All properties are Angular Signals.", + "properties": [ + { + "name": "activeSubagents", + "type": "Signal", + "description": "Filtered list of subagents with status 'running'.", + "optional": false + }, + { + "name": "branch", + "type": "Signal", + "description": "Current branch identifier for time-travel navigation.", + "optional": false + }, + { + "name": "error", + "type": "Signal", + "description": "Last error, if any.", + "optional": false + }, + { + "name": "getMessagesMetadata", + "type": "object", + "description": "Get metadata for a specific message by index.", + "optional": false + }, + { + "name": "getToolCalls", + "type": "object", + "description": "Get tool call results associated with an AI message.", + "optional": false + }, + { + "name": "hasValue", + "type": "Signal", + "description": "True once at least one value or message has been received.", + "optional": false + }, + { + "name": "history", + "type": "Signal[]>", + "description": "Full execution history of the current thread.", + "optional": false + }, + { + "name": "interrupt", + "type": "Signal | undefined>", + "description": "Current interrupt data, if the agent is paused for input.", + "optional": false + }, + { + "name": "interrupts", + "type": "Signal[]>", + "description": "All interrupts received during the current run.", + "optional": false + }, + { + "name": "isLoading", + "type": "Signal", + "description": "True when the resource is actively streaming.", + "optional": false + }, + { + "name": "isThreadLoading", + "type": "Signal", + "description": "True while a thread switch is loading state from the server.", + "optional": false + }, + { + "name": "joinStream", + "type": "object", + "description": "Join an already-running stream by run ID.", + "optional": false + }, + { + "name": "messages", + "type": "Signal, MessageType>[]>", + "description": "Current list of messages from the agent conversation.", + "optional": false + }, + { + "name": "reload", + "type": "object", + "description": "Re-submits the last input to restart the stream.", + "optional": false + }, + { + "name": "setBranch", + "type": "object", + "description": "Set the active branch for time-travel navigation.", + "optional": false + }, + { + "name": "status", + "type": "Signal", + "description": "Current resource status: idle, loading, resolved, or error.", + "optional": false + }, + { + "name": "stop", + "type": "object", + "description": "Abort the current stream.", + "optional": false + }, + { + "name": "subagents", + "type": "Signal>", + "description": "Map of active subagent streams keyed by tool call ID.", + "optional": false + }, + { + "name": "submit", + "type": "object", + "description": "Send a message or resume from an interrupt. Returns immediately.", + "optional": false + }, + { + "name": "switchThread", + "type": "object", + "description": "Switch to a different thread, resetting derived state.", + "optional": false + }, + { + "name": "toolCalls", + "type": "Signal[]>", + "description": "Completed tool calls with their results.", + "optional": false + }, + { + "name": "toolProgress", + "type": "Signal", + "description": "Progress updates for currently executing tools.", + "optional": false + }, + { + "name": "value", + "type": "Signal", + "description": "Current agent state values.", + "optional": false + } + ], + "examples": [] + }, + { + "name": "StreamResourceTransport", + "kind": "interface", + "description": "Transport interface for connecting to a LangGraph agent.", + "properties": [ + { + "name": "joinStream", + "type": "unknown", + "description": "", + "optional": true + }, + { + "name": "stream", + "type": "unknown", + "description": "", + "optional": false + } + ], + "examples": [] + }, + { + "name": "SubagentStreamRef", + "kind": "interface", + "description": "Reference to a subagent's streaming state.", + "properties": [ + { + "name": "messages", + "type": "Signal, MessageType>[]>", + "description": "Messages from the subagent conversation.", + "optional": false + }, + { + "name": "status", + "type": "Signal<\"error\" | \"pending\" | \"running\" | \"complete\">", + "description": "Current execution status of the subagent.", + "optional": false + }, + { + "name": "toolCallId", + "type": "string", + "description": "The tool call ID that spawned this subagent.", + "optional": false + }, + { + "name": "values", + "type": "Signal>", + "description": "Current state values from the subagent.", + "optional": false + } + ], + "examples": [] + }, + { + "name": "SubmitOptions", + "kind": "interface", + "description": "", + "properties": [ + { + "name": "checkpoint", + "type": "Omit | null", + "description": "", + "optional": true + }, + { + "name": "command", + "type": "Command", + "description": "", + "optional": true + }, + { + "name": "config", + "type": "ConfigWithConfigurable", + "description": "", + "optional": true + }, + { + "name": "context", + "type": "ContextType", + "description": "", + "optional": true + }, + { + "name": "durability", + "type": "Durability", + "description": "Whether to checkpoint during the run (or only at the end/interruption).\n- `\"async\"`: Save checkpoint asynchronously while the next step executes (default).\n- `\"sync\"`: Save checkpoint synchronously before the next step starts.\n- `\"exit\"`: Save checkpoint only when the graph exits.", + "optional": true + }, + { + "name": "feedbackKeys", + "type": "string[]", + "description": "", + "optional": true + }, + { + "name": "interruptAfter", + "type": "string[] | \"*\"", + "description": "", + "optional": true + }, + { + "name": "interruptBefore", + "type": "string[] | \"*\"", + "description": "", + "optional": true + }, + { + "name": "metadata", + "type": "Metadata", + "description": "", + "optional": true + }, + { + "name": "multitaskStrategy", + "type": "MultitaskStrategy", + "description": "", + "optional": true + }, + { + "name": "onCompletion", + "type": "OnCompletionBehavior", + "description": "", + "optional": true + }, + { + "name": "onDisconnect", + "type": "DisconnectMode", + "description": "", + "optional": true + }, + { + "name": "onError", + "type": "object", + "description": "Callback that is called when an error occurs during this specific submit call.\nUnlike the hook-level `onError`, this allows handling errors on a per-submit basis,\ne.g. to show a retry button or a specific error message to the user.", + "optional": true + }, + { + "name": "optimisticValues", + "type": "Partial | object", + "description": "", + "optional": true + }, + { + "name": "runId", + "type": "string", + "description": "", + "optional": true + }, + { + "name": "streamMode", + "type": "StreamMode[]", + "description": "", + "optional": true + }, + { + "name": "streamResumable", + "type": "boolean", + "description": "Mark the stream as resumable. All events emitted during the run will be temporarily persisted\nin order to be re-emitted if the stream is re-joined.", + "optional": true + }, + { + "name": "streamSubgraphs", + "type": "boolean", + "description": "Whether or not to stream the nodes of any subgraphs called\nby the assistant.", + "optional": true + }, + { + "name": "threadId", + "type": "string", + "description": "The ID to use when creating a new thread. When provided, this ID will be used\nfor thread creation when threadId is `null` or `undefined`.\nThis enables optimistic UI updates where you know the thread ID\nbefore the thread is actually created.", + "optional": true + } + ], + "examples": [] + }, + { + "name": "ThreadState", + "kind": "interface", + "description": "", + "properties": [ + { + "name": "checkpoint", + "type": "Checkpoint", + "description": "Checkpoint of the thread state", + "optional": false + }, + { + "name": "created_at", + "type": "Optional", + "description": "Time of state creation", + "optional": false + }, + { + "name": "metadata", + "type": "Metadata", + "description": "Metadata for this state", + "optional": false + }, + { + "name": "next", + "type": "string[]", + "description": "The next nodes to execute. If empty, the thread is done until new input is received", + "optional": false + }, + { + "name": "parent_checkpoint", + "type": "Optional", + "description": "The parent checkpoint. If missing, this is the root checkpoint", + "optional": false + }, + { + "name": "tasks", + "type": "ThreadTask[]", + "description": "Tasks to execute in this step. If already attempted, may contain an error", + "optional": false + }, + { + "name": "values", + "type": "ValuesType", + "description": "The state values", + "optional": false + } + ], + "examples": [] + }, + { + "name": "BagTemplate", + "kind": "type", + "description": "Template for the bag type.", + "signature": "unknown", + "examples": [] + }, + { + "name": "InferBag", + "kind": "type", + "description": "Infer the Bag type from an agent, defaulting to the provided Bag.\n\nCurrently returns the provided Bag for all types.\nCan be extended in the future to extract Bag from agent types.", + "signature": "T extends { ~agentTypes: unknown } ? BagTemplate : B", + "examples": [] + }, + { + "name": "ResourceStatus", + "kind": "type", + "description": "Runtime constant mirroring Angular's ResourceStatus string-union type.\nAngular 21 ships ResourceStatus as a pure string-union type (no runtime value),\nso we provide a const-object shim for code that needs runtime comparisons.", + "signature": "NgResourceStatus", + "examples": [] + }, + { + "name": "provideStreamResource", + "kind": "function", + "description": "Angular provider factory that registers global defaults for all\nstreamResource instances in the application.\n\nAdd to your `app.config.ts` or module providers array.", + "signature": "provideStreamResource(config: StreamResourceConfig): Provider", + "params": [ + { + "name": "config", + "type": "StreamResourceConfig", + "description": "Global configuration merged with per-call options", + "optional": false + } + ], + "returns": { + "type": "Provider", + "description": "" + }, + "examples": [ + "```typescript\n// app.config.ts\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideStreamResource({ apiUrl: 'http://localhost:2024' }),\n ],\n};\n```" + ] + }, + { + "name": "streamResource", + "kind": "function", + "description": "Creates a streaming resource connected to a LangGraph agent.\n\nMust be called within an Angular injection context (component constructor,\nfield initializer, or `runInInjectionContext`). Returns a ref object whose\nproperties are Angular Signals that update in real-time as the agent streams.", + "signature": "streamResource(options: StreamResourceOptions>): StreamResourceRef>", + "params": [ + { + "name": "options", + "type": "StreamResourceOptions>", + "description": "Configuration for the streaming resource", + "optional": false + } + ], + "returns": { + "type": "StreamResourceRef>", + "description": "" + }, + "examples": [ + "```typescript\n// In a component field initializer\nconst chat = streamResource<{ messages: BaseMessage[] }>({\n assistantId: 'chat_agent',\n apiUrl: 'http://localhost:2024',\n threadId: signal(this.savedThreadId),\n onThreadId: (id) => localStorage.setItem('threadId', id),\n});\n\n// Access signals in template\n// chat.messages(), chat.status(), chat.error()\n```" + ] + } +] \ No newline at end of file diff --git a/apps/website/content/docs-v2/api/fetch-stream-transport.mdx b/apps/website/content/docs-v2/api/fetch-stream-transport.mdx new file mode 100644 index 000000000..86856dc4f --- /dev/null +++ b/apps/website/content/docs-v2/api/fetch-stream-transport.mdx @@ -0,0 +1,3 @@ +# FetchStreamTransport + +{/* 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 new file mode 100644 index 000000000..3ed2407e0 --- /dev/null +++ b/apps/website/content/docs-v2/api/mock-stream-transport.mdx @@ -0,0 +1,3 @@ +# MockStreamTransport + +{/* 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 new file mode 100644 index 000000000..59d469a11 --- /dev/null +++ b/apps/website/content/docs-v2/api/provide-stream-resource.mdx @@ -0,0 +1,3 @@ +# provideStreamResource() + +{/* 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 62df1cef7..fa1c3417c 100644 --- a/apps/website/content/docs-v2/api/stream-resource.mdx +++ b/apps/website/content/docs-v2/api/stream-resource.mdx @@ -1,38 +1,3 @@ # streamResource() -Creates a streaming resource connected to a LangGraph agent. Must be called within an Angular injection context. - -## Signature - -```typescript -function streamResource(options: StreamResourceOptions): StreamResource -``` - -## Options - -| Parameter | Type | Description | -|-----------|------|-------------| -| `assistantId` | `string` | Agent or graph identifier | -| `apiUrl` | `string` | LangGraph Platform base URL | -| `threadId` | `Signal \| string \| null` | Thread to connect to | -| `onThreadId` | `(id: string) => void` | Called when a new thread is created | -| `onInterrupt` | `(data: unknown) => void` | Called when the agent pauses for input | -| `transport` | `StreamTransport` | Custom transport (default: FetchStreamTransport) | - -## Return type - -streamResource() returns an object with these Signal properties: - -| Property | Type | Description | -|----------|------|-------------| -| `messages()` | `Signal` | Current message list | -| `status()` | `Signal<'idle' \| 'streaming' \| 'error'>` | Stream status | -| `error()` | `Signal` | Last error, if any | -| `threadId()` | `Signal` | Current thread ID | - -## Methods - -| Method | Description | -|--------|-------------| -| `submit(input)` | Send a message or resume from interrupt | -| `history()` | Get execution history for time travel | +{/* Auto-rendered from api-docs.json — see page component */} diff --git a/apps/website/content/docs-v2/concepts/agent-architecture.mdx b/apps/website/content/docs-v2/concepts/agent-architecture.mdx new file mode 100644 index 000000000..3d84d0d24 --- /dev/null +++ b/apps/website/content/docs-v2/concepts/agent-architecture.mdx @@ -0,0 +1,55 @@ +# Agent Architecture + +How AI agents work — the planning, execution, and tool-calling lifecycle that streamResource() connects your Angular app to. + +## The agent loop + +An AI agent follows a cycle: + + + +The user sends a message via `submit()`. streamResource() posts it to LangGraph Platform. + + +The LLM decides what to do next — respond directly, call a tool, or delegate to a subagent. + + +Tools run (database queries, API calls, code execution). Results feed back into state. + + +The agent streams its response token-by-token. streamResource() updates the `messages()` signal in real-time. + + +State is checkpointed. The agent may loop back to Plan, or finish. + + + +## Tool calling + +Agents extend their capabilities through tools. streamResource() tracks tool execution: + +```typescript +const agent = streamResource({ + assistantId: 'research_agent', +}); + +// Currently executing tools +const tools = computed(() => agent.toolProgress()); + +// Completed tool calls with results +const completedTools = computed(() => agent.toolCalls()); +``` + +## Multi-agent patterns + +Complex tasks use multiple agents working together: + +- **Orchestrator** — one agent delegates to specialized subagents +- **Pipeline** — agents process sequentially, each refining the output +- **Debate** — agents review each other's work + +streamResource() supports these patterns through the `subagents()` and `activeSubagents()` signals. + + +Most applications only need a single agent with tools. Add subagents when you need true task delegation with isolated state. + diff --git a/apps/website/content/docs-v2/concepts/angular-signals.mdx b/apps/website/content/docs-v2/concepts/angular-signals.mdx new file mode 100644 index 000000000..cd9677f72 --- /dev/null +++ b/apps/website/content/docs-v2/concepts/angular-signals.mdx @@ -0,0 +1,61 @@ +# Angular Signals + +streamResource() is built on Angular Signals — the reactive primitive introduced in Angular 16+. Every property on a StreamResourceRef is a Signal, making it work seamlessly with OnPush change detection, computed values, and effect callbacks. + +## Signals primer + +A Signal is a reactive value container. When a Signal's value changes, Angular automatically re-renders any template that reads it. + +```typescript +// streamResource returns Signals, not Observables +const chat = streamResource({ assistantId: 'agent' }); + +chat.messages() // Signal — call to read +chat.status() // Signal +chat.error() // Signal +chat.isLoading() // Signal (computed) +``` + +## Computed values + +Use `computed()` to derive new Signals from streamResource signals. + +```typescript +const lastMessage = computed(() => + chat.messages().at(-1)?.content ?? '' +); + +const messageCount = computed(() => + chat.messages().length +); + +const isIdle = computed(() => + chat.status() === 'idle' +); +``` + +## OnPush change detection + +Because Signals trigger change detection automatically, streamResource works perfectly with `ChangeDetectionStrategy.OnPush`. + +```typescript +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @for (msg of chat.messages(); track $index) { +

{{ msg.content }}

+ } + `, +}) +export class ChatComponent { + chat = streamResource({ assistantId: 'agent' }); +} +``` + +## No RxJS required + +Unlike traditional Angular HTTP patterns, streamResource doesn't use Observables. There are no subscriptions to manage, no async pipes needed, and no memory leak risks. + + +Signals are simpler for UI state. They synchronously read the latest value, compose with computed(), and integrate with Angular's template syntax. streamResource handles the async SSE connection internally and surfaces results as Signals. + diff --git a/apps/website/content/docs-v2/concepts/langgraph-basics.mdx b/apps/website/content/docs-v2/concepts/langgraph-basics.mdx new file mode 100644 index 000000000..046e64cf2 --- /dev/null +++ b/apps/website/content/docs-v2/concepts/langgraph-basics.mdx @@ -0,0 +1,51 @@ +# LangGraph Basics + +LangGraph is a framework for building stateful AI agents as directed graphs. This page explains the core concepts for Angular developers who are new to agent development. + +## Graphs, nodes, and edges + +A LangGraph agent is a directed graph where: + + + +Each node performs one action — calling an LLM, querying a database, or making an API request. Nodes receive state and return updated state. + + +Edges connect nodes. Conditional edges route execution based on state, enabling branching logic. + + +All nodes read from and write to a shared state object. This state is what streamResource() exposes through its signals. + + + +## How streamResource connects + +Your Angular app doesn't run the graph — LangGraph Platform does. streamResource() is the bridge: + +1. Your component calls `submit()` with user input +2. FetchStreamTransport sends an HTTP POST to LangGraph Platform +3. The platform runs the graph and streams state updates via SSE +4. streamResource() updates its Signals as events arrive +5. Angular re-renders your templates automatically + +## State design + +The generic type parameter in `streamResource()` defines your agent's state shape. + +```typescript +// Simple chat state +streamResource<{ messages: BaseMessage[] }>({ ... }) + +// Rich agent state with custom fields +interface AgentState { + messages: BaseMessage[]; + plan: string[]; + currentStep: number; + results: Record; +} +streamResource({ ... }) +``` + + +For deeper LangGraph concepts (persistence, interrupts, memory), see the individual guide pages. + diff --git a/apps/website/content/docs-v2/concepts/state-management.mdx b/apps/website/content/docs-v2/concepts/state-management.mdx new file mode 100644 index 000000000..8e9274cad --- /dev/null +++ b/apps/website/content/docs-v2/concepts/state-management.mdx @@ -0,0 +1,69 @@ +# State Management + +How state flows through streamResource() — from LangGraph's server-side state machine to Angular Signals in your templates. + +## State lives on the server + +Unlike traditional Angular state management (NgRx, signals stores), agent state lives on the LangGraph Platform. Your Angular app is a stateless view layer. + +``` +LangGraph Platform (source of truth) + ↓ SSE stream +FetchStreamTransport (transport layer) + ↓ events +streamResource() (signal conversion) + ↓ Signals +Angular templates (reactive rendering) +``` + +## The state shape + +Your state type defines what the agent manages. The `value()` signal exposes the full state object. + +```typescript +interface ProjectState { + messages: BaseMessage[]; + files: string[]; + analysis: { score: number; issues: string[] }; +} + +const agent = streamResource({ + assistantId: 'project_agent', +}); + +// Access any state field as a reactive value +const files = computed(() => agent.value().files); +const score = computed(() => agent.value().analysis.score); +``` + +## Thread state vs application state + + +Thread state (managed by LangGraph) and application state (managed by Angular) are separate concerns. Don't try to sync them — read thread state from signals, manage UI state with Angular signals. + + +```typescript +// Thread state — from the agent +const messages = agent.messages(); // Read-only signal +const agentStatus = agent.status(); // Read-only signal + +// Application state — your Angular code +const sidebarOpen = signal(true); // Your UI state +const selectedTab = signal('chat'); // Your UI state +``` + +## State updates are immutable + +Every state update from the agent creates a new signal value. Angular's change detection picks this up automatically. + +```typescript +// This works with OnPush because the Signal reference changes +@for (msg of agent.messages(); track $index) { +

{{ msg.content }}

+} + +// Computed values re-evaluate when dependencies change +const hasErrors = computed(() => + agent.value().analysis.issues.length > 0 +); +``` diff --git a/apps/website/content/docs-v2/getting-started/installation.mdx b/apps/website/content/docs-v2/getting-started/installation.mdx new file mode 100644 index 000000000..f06f1f942 --- /dev/null +++ b/apps/website/content/docs-v2/getting-started/installation.mdx @@ -0,0 +1,102 @@ +# Installation + +Detailed setup guide for streamResource() in your Angular application. + +## Requirements + + + +streamResource() uses Angular Signals and the modern injection context API. Angular 20 or later is required. + + +Required for the build toolchain and package installation. + + +A running LangGraph agent accessible via HTTP. Can be local (langgraph dev) or deployed (LangGraph Cloud). + + + +## Install the package + +```bash +npm install @cacheplane/stream-resource +``` + +This installs the library and its peer dependencies including `@langchain/langgraph-sdk`. + +## Configure the provider + +Add `provideStreamResource()` to your application configuration. This sets global defaults for all streamResource instances. + +```typescript +// app.config.ts +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ + apiUrl: process.env['LANGGRAPH_URL'] ?? 'http://localhost:2024', + }), + ], +}; +``` + + +Any option passed to `streamResource()` directly overrides the global provider config. You can set a default `apiUrl` globally and override it for specific agents. + + +## Environment setup + + + + +For local development, run a LangGraph server: + +```bash +# Start LangGraph dev server +langgraph dev + +# Your agent will be available at http://localhost:2024 +``` + + + + +For production, point to your LangGraph Cloud deployment: + +```typescript +provideStreamResource({ + apiUrl: 'https://your-project.langgraph.app', +}) +``` + + + + +## Verify installation + +Create a minimal test to verify the setup works: + +```typescript +import { streamResource } from '@cacheplane/stream-resource'; + +// In a component +const test = streamResource({ + assistantId: 'chat_agent', +}); + +// If status() returns 'idle', the setup is correct +console.log(test.status()); // 'idle' +``` + +## Next steps + + + + Build your first chat component in 5 minutes + + + Understand how Signals power streamResource + + diff --git a/apps/website/content/docs-v2/getting-started/introduction.mdx b/apps/website/content/docs-v2/getting-started/introduction.mdx index 53fc6dcef..3f09cf406 100644 --- a/apps/website/content/docs-v2/getting-started/introduction.mdx +++ b/apps/website/content/docs-v2/getting-started/introduction.mdx @@ -3,26 +3,61 @@ StreamResource brings full parity with React's `useStream()` hook to Angular 20+. It's the enterprise streaming resource for LangChain and Angular — built natively with Angular Signals, not wrapped or adapted. -StreamResource serves two audiences: Angular developers building AI-powered apps, and AI/agent developers who need an Angular frontend. +StreamResource serves two audiences: **Angular developers** building AI-powered applications, and **AI/agent developers** who need a production Angular frontend for their LangGraph agents. -## What you'll build +## What is streamResource()? -With streamResource(), you can build Angular applications that connect to LangGraph agents with: +`streamResource()` is an Angular function that creates a reactive connection to a LangGraph agent. It returns an object whose properties are Angular Signals — meaning your templates update automatically as the agent streams responses. -- **Token-by-token streaming** via SSE -- **Thread persistence** across sessions -- **Human-in-the-loop** interrupts and approvals -- **Time travel** debugging -- **Deterministic testing** with MockStreamTransport +```typescript +const chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'chat_agent', +}); -## Next steps +// Every property is a Signal +chat.messages() // Signal +chat.status() // Signal<'idle' | 'loading' | 'resolved' | 'error'> +chat.interrupt() // Signal +chat.history() // Signal +``` + +## What you can build + + + +Token-by-token streaming with real-time UI updates. Messages arrive as they're generated. + + +Agents pause for approval, confirmation, or correction. Your UI handles the interrupt and resumes execution. + + +Track multiple subagents working in parallel, each with their own message stream and status. + + +Inspect agent execution history, fork from checkpoints, and explore alternate paths. + + + +## Guides - Build your first streaming chat in 5 minutes + Build a chat component in 5 minutes - Detailed setup and configuration guide + Detailed setup and configuration + + + Token-by-token updates via SSE + + + Thread persistence across sessions + + + Human-in-the-loop approval flows + + + Deterministic testing with MockStreamTransport diff --git a/apps/website/content/docs-v2/getting-started/quickstart.mdx b/apps/website/content/docs-v2/getting-started/quickstart.mdx new file mode 100644 index 000000000..96f8fcff6 --- /dev/null +++ b/apps/website/content/docs-v2/getting-started/quickstart.mdx @@ -0,0 +1,130 @@ +# Quick Start + +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. + +```typescript +// app.config.ts +import { provideStreamResource } from '@cacheplane/stream-resource'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ + apiUrl: 'http://localhost:2024', + }), + ], +}; +``` + +## 3. Create a chat component + +Use `streamResource()` in a component field initializer. Every property on the returned ref is an Angular Signal. + + + + +```typescript +// chat.component.ts +import { Component, 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', +}) +export class ChatComponent { + input = signal(''); + + chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'chat_agent', + threadId: signal(localStorage.getItem('threadId')), + onThreadId: (id) => localStorage.setItem('threadId', id), + }); + + isStreaming = computed(() => this.chat.status() === 'loading'); + + send() { + const msg = this.input(); + if (!msg.trim()) return; + this.chat.submit({ messages: [{ role: 'user', content: msg }] }); + this.input.set(''); + } +} +``` + + + + +```html + +
+ @for (msg of chat.messages(); track $index) { +
+

{{ msg.content }}

+
+ } + + @if (isStreaming()) { +
Agent is thinking...
+ } + +
+ + +
+
+``` + +
+
+ +## 4. Start your LangGraph server + +Make sure your LangGraph agent is running at the URL you configured. + +```bash +langgraph dev +``` + +## 5. Run your app + +```bash +ng serve +``` + +Open `http://localhost:4200` and start chatting with your agent. + +## Next steps + + + + Learn about token-by-token updates and stream modes + + + Keep conversations alive across page refreshes + + + Add human-in-the-loop approval flows + + + Test your agent integration deterministically + + diff --git a/apps/website/content/docs-v2/guides/deployment.mdx b/apps/website/content/docs-v2/guides/deployment.mdx new file mode 100644 index 000000000..ff23ad567 --- /dev/null +++ b/apps/website/content/docs-v2/guides/deployment.mdx @@ -0,0 +1,90 @@ +# Deployment + +Configure streamResource() for production with LangGraph Cloud, environment-based URLs, and error handling patterns. + +## Production configuration + +Point `apiUrl` to your LangGraph Cloud deployment. + + + + +```typescript +// app.config.ts +provideStreamResource({ + apiUrl: environment.langgraphUrl, +}) +``` + +```typescript +// environment.prod.ts +export const environment = { + langgraphUrl: 'https://your-project.langgraph.app', +}; +``` + + + + +```typescript +// app.config.ts +provideStreamResource({ + apiUrl: 'https://your-project.langgraph.app', +}) +``` + + + + +## Error boundaries + +Handle errors gracefully in production. + +```typescript +const chat = streamResource({ + assistantId: 'chat_agent', +}); + +// Reactive error display +hasError = computed(() => chat.status() === 'error'); +errorMessage = computed(() => { + const err = chat.error(); + return err instanceof Error ? err.message : 'Something went wrong'; +}); + +// Retry after error +retry() { + chat.reload(); +} +``` + +## Recovering interrupted streams + +Use `joinStream()` to reconnect to a running stream after a network interruption. + +```typescript +// If you know the run ID (e.g., from a status endpoint) +await chat.joinStream(runId, lastEventId); +// Resumes streaming from where it left off +``` + + +streamResource() is a stateless client. All state lives on the LangGraph Platform. This means your Angular app can be deployed anywhere (CDN, edge, SSR) without state management concerns. + + +## Checklist + + + +Point to your LangGraph Cloud deployment URL. + + +Show user-friendly error messages and retry buttons. + + +Store threadId in localStorage or a backend so users can resume conversations. + + +Set `throttle` option if token-by-token updates are too frequent for your UI. + + diff --git a/apps/website/content/docs-v2/guides/interrupts.mdx b/apps/website/content/docs-v2/guides/interrupts.mdx new file mode 100644 index 000000000..b17d8f5ef --- /dev/null +++ b/apps/website/content/docs-v2/guides/interrupts.mdx @@ -0,0 +1,78 @@ +# Interrupts + +Interrupts let your LangGraph agent pause execution and wait for human input. streamResource() surfaces interrupts as Angular Signals, making it easy to build approval flows, confirmation dialogs, and human-in-the-loop experiences. + + +Use interrupts for human approval, late-binding decisions, or any step where the agent needs external input before continuing. + + +## Basic interrupt handling + +When an agent interrupts, the `interrupt()` signal contains the interrupt data. + + + + +```typescript +// approval.component.ts +interface ApprovalPayload { + action: string; + description: string; + risk: 'low' | 'medium' | 'high'; +} + +const agent = streamResource({ + assistantId: 'approval_agent', +}); + +// Check for pending interrupts +pendingApproval = computed(() => agent.interrupt()); +``` + + + + +```html + +@if (pendingApproval(); as approval) { +
+

Agent needs approval

+

{{ approval.value.description }}

+

Risk level: {{ approval.value.risk }}

+ + +
+} +``` + +
+
+ +## Resuming from an interrupt + +Call `submit()` with the resume payload to continue execution. + +```typescript +approve() { + this.agent.submit(null, { resume: { approved: true } }); +} + +reject() { + this.agent.submit(null, { resume: { approved: false, reason: 'User rejected' } }); +} +``` + +## Multiple interrupts + +The `interrupts()` signal tracks all interrupts received during a run, not just the current one. + +```typescript +// Track interrupt history +allInterrupts = computed(() => agent.interrupts()); +latestInterrupt = computed(() => agent.interrupt()); +interruptCount = computed(() => agent.interrupts().length); +``` + + +Use the BagTemplate generic parameter to type your interrupt payloads for full TypeScript safety. + diff --git a/apps/website/content/docs-v2/guides/memory.mdx b/apps/website/content/docs-v2/guides/memory.mdx new file mode 100644 index 000000000..b72d55fe2 --- /dev/null +++ b/apps/website/content/docs-v2/guides/memory.mdx @@ -0,0 +1,65 @@ +# Memory + +Memory in LangGraph preserves useful context that later steps can read back. streamResource() exposes memory through the messages and state signals, with thread persistence providing cross-session continuity. + + +Short-term memory lives within a thread (conversation history). Long-term memory persists across threads via LangGraph's memory store. + + +## Short-term memory (thread-scoped) + +Every message in a thread is automatically preserved. When you reconnect with the same `threadId`, the full conversation history is restored. + +```typescript +const chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'memory_agent', + threadId: signal(userId()), // User-specific thread +}); + +// Messages accumulate across the conversation +const messageCount = computed(() => chat.messages().length); + +// Resume where you left off on next visit +// threadId persists, so history is restored +``` + +## Accessing agent state as memory + +The `value()` signal contains the full agent state, which can include custom memory fields. + +```typescript +interface AgentState { + messages: BaseMessage[]; + userPreferences: { theme: string; language: string }; + projectContext: { name: string; files: string[] }; +} + +const agent = streamResource({ + assistantId: 'context_agent', + threadId: signal(projectId()), +}); + +// Read memory fields from agent state +const prefs = computed(() => agent.value().userPreferences); +const context = computed(() => agent.value().projectContext); +``` + +## Cross-session memory + +Thread persistence enables memory that spans sessions. The agent decides what to store in its state. + +```typescript +// User returns days later — same threadId resumes context +const agent = streamResource({ + assistantId: 'memory_agent', + threadId: signal(localStorage.getItem('agent-thread')), + onThreadId: (id) => localStorage.setItem('agent-thread', id), +}); + +// Agent recalls past decisions, preferences, and context +// No explicit memory management needed on the Angular side +``` + + +The agent controls what gets stored in memory. streamResource() just surfaces the current state. Design your agent's state schema to include the fields you want to persist. + diff --git a/apps/website/content/docs-v2/guides/persistence.mdx b/apps/website/content/docs-v2/guides/persistence.mdx new file mode 100644 index 000000000..3132b920c --- /dev/null +++ b/apps/website/content/docs-v2/guides/persistence.mdx @@ -0,0 +1,89 @@ +# Persistence + +Thread persistence keeps conversations alive across page refreshes, browser restarts, and session changes. streamResource() manages thread state through the `threadId` signal and `onThreadId` callback. + + +LangGraph checkpoints state at every super-step. streamResource() connects to these checkpoints via thread IDs, letting you resume exactly where you left off. + + +## Basic thread persistence + +Save the thread ID to localStorage so conversations survive page refreshes. + + + + +```typescript +// chat.component.ts +const chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'chat_agent', + threadId: signal(localStorage.getItem('threadId')), + onThreadId: (id) => localStorage.setItem('threadId', id), +}); +``` + + + + +```html + + +@for (msg of chat.messages(); track $index) { +

{{ msg.content }}

+} +``` + +
+
+ +## Reactive thread switching + +Pass a Signal as `threadId` to reactively switch between conversations. + +```typescript +// conversation-list.component.ts +activeThreadId = signal(null); + +chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'chat_agent', + threadId: this.activeThreadId, // Signal — switches reactively + onThreadId: (id) => this.activeThreadId.set(id), +}); + +// Switch to a different conversation +selectThread(id: string) { + this.activeThreadId.set(id); + // streamResource automatically loads the new thread's state +} +``` + + +Use the `isThreadLoading()` signal to show a loading indicator while thread state is being fetched from the server. + + +## Manual thread switching + +Use `switchThread()` for imperative thread changes that also reset derived state. + +```typescript +// Reset and start a new conversation +newConversation() { + this.chat.switchThread(null); + // Creates a new thread on next submit +} + +// Switch to a specific thread +loadConversation(threadId: string) { + this.chat.switchThread(threadId); +} +``` + +## Checkpoint recovery + +When a connection drops, streamResource() can rejoin an in-progress run. + +```typescript +// Rejoin a running stream +await chat.joinStream(runId, lastEventId); +// Picks up from where the connection was lost +``` diff --git a/apps/website/content/docs-v2/guides/subgraphs.mdx b/apps/website/content/docs-v2/guides/subgraphs.mdx new file mode 100644 index 000000000..ed76391bf --- /dev/null +++ b/apps/website/content/docs-v2/guides/subgraphs.mdx @@ -0,0 +1,59 @@ +# Subgraphs + +Subgraphs let you compose complex agents from smaller, focused units. streamResource() tracks subagent execution through dedicated signals, giving you visibility into delegated work. + + +LangGraph calls them subgraphs (modular graph composition). Deep Agents calls them subagents (task delegation). streamResource() supports both patterns through the same API. + + +## Tracking subagent execution + +The `subagents()` signal contains a Map of active subagent streams. + +```typescript +const orchestrator = streamResource({ + assistantId: 'orchestrator', + subagentToolNames: ['research', 'analyze', 'summarize'], +}); + +// All subagent streams +const subagents = computed(() => orchestrator.subagents()); + +// Only active ones +const running = computed(() => orchestrator.activeSubagents()); +const runningCount = computed(() => running().length); +``` + +## Subagent stream details + +Each `SubagentStreamRef` provides its own signals. + +```typescript +// Access a specific subagent +const researchAgent = computed(() => + orchestrator.subagents().get('research-tool-call-id') +); + +// Track its progress +const researchStatus = computed(() => researchAgent()?.status()); +const researchMessages = computed(() => researchAgent()?.messages() ?? []); +``` + +## Filtering subagent messages + +By default, subagent messages appear in the parent's `messages()` signal. Filter them out for a cleaner parent view. + +```typescript +const orchestrator = streamResource({ + assistantId: 'orchestrator', + filterSubagentMessages: true, // Hide subagent messages from parent + subagentToolNames: ['research', 'analyze'], +}); + +// Parent messages only (no subagent chatter) +const parentMessages = computed(() => orchestrator.messages()); +``` + + +Set `subagentToolNames` to the tool names that spawn subagents. streamResource() uses this to identify which tool calls create subagent streams. + diff --git a/apps/website/content/docs-v2/guides/testing.mdx b/apps/website/content/docs-v2/guides/testing.mdx new file mode 100644 index 000000000..5b1cad068 --- /dev/null +++ b/apps/website/content/docs-v2/guides/testing.mdx @@ -0,0 +1,106 @@ +# Testing + +MockStreamTransport lets you test agent interactions deterministically without a running LangGraph server. Script exact event sequences and step through them in your Angular test specs. + + +MockStreamTransport eliminates network dependencies, timing issues, and server state. Every test run produces identical results. + + +## Basic test setup + +Create a MockStreamTransport with scripted events and pass it to streamResource. + +```typescript +import { TestBed } from '@angular/core/testing'; +import { MockStreamTransport } from '@cacheplane/stream-resource'; +import type { StreamEvent } from '@cacheplane/stream-resource'; + +describe('ChatComponent', () => { + it('should display agent messages', () => { + const transport = new MockStreamTransport(); + + TestBed.runInInjectionContext(() => { + const chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'test_agent', + transport, + }); + + // Emit a values event + transport.emit([ + { type: 'values', messages: [{ role: 'assistant', content: 'Hello!' }] }, + ]); + + expect(chat.messages().length).toBe(1); + expect(chat.messages()[0].content).toBe('Hello!'); + }); + }); +}); +``` + +## Scripting event sequences + +Pass event batches to the constructor for sequential playback. + +```typescript +const transport = new MockStreamTransport([ + // Batch 1: Initial response + [{ type: 'values', messages: [{ role: 'assistant', content: 'Analyzing...' }] }], + // Batch 2: Final response + [{ type: 'values', messages: [{ role: 'assistant', content: 'Done!' }] }], +]); + +// Advance through batches +const batch1 = transport.nextBatch(); // First batch +const batch2 = transport.nextBatch(); // Second batch +``` + +## Testing interrupts + +Script an interrupt event to test human-in-the-loop flows. + +```typescript +it('should handle interrupts', () => { + const transport = new MockStreamTransport(); + + TestBed.runInInjectionContext(() => { + const agent = streamResource({ + assistantId: 'approval_agent', + transport, + }); + + // Emit an interrupt + transport.emit([ + { type: 'interrupt', value: { action: 'delete', risk: 'high' } }, + ]); + + expect(agent.interrupt()).toBeDefined(); + expect(agent.interrupt()?.value.risk).toBe('high'); + }); +}); +``` + +## Testing errors + +Inject errors to test error handling. + +```typescript +it('should surface errors', () => { + const transport = new MockStreamTransport(); + + TestBed.runInInjectionContext(() => { + const chat = streamResource({ + assistantId: 'test_agent', + transport, + }); + + transport.emitError(new Error('Connection lost')); + + expect(chat.error()).toBeDefined(); + expect(chat.status()).toBe('error'); + }); +}); +``` + + +streamResource() must be called within an Angular injection context. In tests, wrap calls in `TestBed.runInInjectionContext()`. + diff --git a/apps/website/content/docs-v2/guides/time-travel.mdx b/apps/website/content/docs-v2/guides/time-travel.mdx new file mode 100644 index 000000000..d45cab714 --- /dev/null +++ b/apps/website/content/docs-v2/guides/time-travel.mdx @@ -0,0 +1,55 @@ +# Time Travel + +Time travel lets you inspect earlier states and replay alternate execution paths. streamResource() exposes the full checkpoint history and branch navigation through Angular Signals. + + +Debug agent decisions, explore alternate paths, and build undo/redo experiences for your users. + + +## Browsing execution history + +The `history()` signal contains an array of `ThreadState` checkpoints. + +```typescript +const agent = streamResource({ + assistantId: 'agent', + threadId: signal(threadId), +}); + +// Full execution timeline +const checkpoints = computed(() => agent.history()); +const checkpointCount = computed(() => agent.history().length); +``` + +## Forking from a checkpoint + +Submit with a specific checkpoint to branch execution from an earlier state. + +```typescript +// Fork from the 3rd checkpoint with new input +forkFromCheckpoint(index: number) { + const checkpoint = this.agent.history()[index]; + this.agent.submit( + { messages: [{ role: 'user', content: 'Try a different approach' }] }, + { checkpoint: checkpoint.checkpoint } + ); +} +``` + +## Branch navigation + +Use `branch()` and `setBranch()` to navigate between execution branches. + +```typescript +// Current branch +const activeBranch = computed(() => agent.branch()); + +// Switch to a different branch +selectBranch(branchId: string) { + agent.setBranch(branchId); +} +``` + + +Time travel is most useful during development. Inspect why an agent chose a particular path, then fork to test alternatives without restarting the conversation. + diff --git a/apps/website/scripts/generate-api-docs.ts b/apps/website/scripts/generate-api-docs.ts index 4b5299d06..48d8f2848 100644 --- a/apps/website/scripts/generate-api-docs.ts +++ b/apps/website/scripts/generate-api-docs.ts @@ -1,30 +1,137 @@ -import { Application, TSConfigReader } from 'typedoc'; +import { Application, TSConfigReader, ReflectionKind } from 'typedoc'; import fs from 'fs'; import path from 'path'; +interface ApiParam { + name: string; + type: string; + description: string; + optional?: boolean; +} + +interface ApiMethod { + name: string; + signature: string; + description: string; + params?: ApiParam[]; +} + +interface ApiDocEntry { + name: string; + kind: 'function' | 'class' | 'interface' | 'type'; + description: string; + signature?: string; + params?: ApiParam[]; + returns?: { type: string; description: string }; + examples?: string[]; + properties?: ApiParam[]; + methods?: ApiMethod[]; +} + +function extractDescription(comment: any): string { + if (!comment?.summary) return ''; + return comment.summary.map((p: any) => p.text ?? '').join('').trim(); +} + +function extractExamples(comment: any): string[] { + if (!comment?.blockTags) return []; + return comment.blockTags + .filter((t: any) => t.tag === '@example') + .map((t: any) => t.content.map((c: any) => c.text ?? '').join('').trim()); +} + +function extractType(typeObj: any): string { + if (!typeObj) return 'unknown'; + if (typeObj.type === 'intrinsic') return typeObj.name; + if (typeObj.type === 'reference') return typeObj.name + (typeObj.typeArguments ? `<${typeObj.typeArguments.map(extractType).join(', ')}>` : ''); + if (typeObj.type === 'union') return typeObj.types.map(extractType).join(' | '); + if (typeObj.type === 'literal') return JSON.stringify(typeObj.value); + if (typeObj.type === 'reflection') return 'object'; + if (typeObj.type === 'array') return `${extractType(typeObj.elementType)}[]`; + return typeObj.toString?.() ?? 'unknown'; +} + +function extractParams(sig: any): ApiParam[] { + if (!sig?.parameters) return []; + return sig.parameters.map((p: any) => ({ + name: p.name, + type: extractType(p.type), + description: extractDescription(p.comment), + optional: p.flags?.isOptional ?? false, + })); +} + +function reflectionToEntry(ref: any): ApiDocEntry | null { + const kind = ref.kind; + const desc = extractDescription(ref.comment); + const examples = extractExamples(ref.comment); + + if (kind === ReflectionKind.Function) { + const sig = ref.signatures?.[0]; + return { + name: ref.name, + kind: 'function', + description: desc || extractDescription(sig?.comment), + signature: sig ? `${ref.name}(${(sig.parameters ?? []).map((p: any) => `${p.name}: ${extractType(p.type)}`).join(', ')}): ${extractType(sig.type)}` : ref.name, + params: extractParams(sig), + returns: sig?.type ? { type: extractType(sig.type), description: '' } : undefined, + examples: examples.length ? examples : extractExamples(sig?.comment), + }; + } + + if (kind === ReflectionKind.Class) { + const props = (ref.children ?? []) + .filter((c: any) => c.kind === ReflectionKind.Property) + .map((c: any) => ({ name: c.name, type: extractType(c.type), description: extractDescription(c.comment), optional: c.flags?.isOptional })); + const methods = (ref.children ?? []) + .filter((c: any) => c.kind === ReflectionKind.Method) + .map((c: any) => { + const sig = c.signatures?.[0]; + return { name: c.name, signature: `${c.name}(${(sig?.parameters ?? []).map((p: any) => `${p.name}: ${extractType(p.type)}`).join(', ')})`, description: extractDescription(c.comment) || extractDescription(sig?.comment), params: extractParams(sig) }; + }); + const ctorSig = (ref.children ?? []).find((c: any) => c.kind === ReflectionKind.Constructor)?.signatures?.[0]; + return { + name: ref.name, + kind: 'class', + description: desc, + params: ctorSig ? extractParams(ctorSig) : undefined, + examples, + properties: props, + methods, + }; + } + + if (kind === ReflectionKind.Interface) { + const props = (ref.children ?? []).map((c: any) => ({ + name: c.name, + type: extractType(c.type), + description: extractDescription(c.comment), + optional: c.flags?.isOptional, + })); + return { name: ref.name, kind: 'interface', description: desc, properties: props, examples }; + } + + if (kind === ReflectionKind.TypeAlias) { + return { name: ref.name, kind: 'type', description: desc, signature: extractType(ref.type), examples }; + } + + return null; +} + async function main() { - // Detect library entry point const candidates = [ 'libs/stream-resource/src/public-api.ts', 'packages/stream-resource/src/public-api.ts', - 'libs/stream-resource/src/index.ts', - 'packages/stream-resource/src/index.ts', ]; const entryPoint = candidates.find((p) => fs.existsSync(p)); if (!entryPoint) { - console.warn('Library entry point not found — generating placeholder api-docs.json'); - const outDir = 'apps/website/public'; + console.warn('Library entry point not found — generating empty api-docs.json'); + const outDir = 'apps/website/content/docs-v2/api'; fs.mkdirSync(outDir, { recursive: true }); - fs.writeFileSync(path.join(outDir, 'api-docs.json'), JSON.stringify({ - name: 'stream-resource', - comment: { summary: [{ text: 'API documentation placeholder — run after library is built.' }] }, - children: [], - }, null, 2)); - console.log('✓ api-docs.json (placeholder)'); + fs.writeFileSync(path.join(outDir, 'api-docs.json'), JSON.stringify([], null, 2)); return; } - // Find the library tsconfig that includes the source files const libDir = path.dirname(path.dirname(entryPoint)); const libTsconfig = fs.existsSync(path.join(libDir, 'tsconfig.lib.json')) ? path.join(libDir, 'tsconfig.lib.json') @@ -38,9 +145,17 @@ async function main() { app.options.addReader(new TSConfigReader()); const project = await app.convert(); if (!project) throw new Error('TypeDoc failed to convert project'); - fs.mkdirSync('apps/website/public', { recursive: true }); - await app.generateJson(project, 'apps/website/public/api-docs.json'); - console.log('✓ api-docs.json written'); + + const entries: ApiDocEntry[] = []; + for (const child of project.children ?? []) { + const entry = reflectionToEntry(child); + if (entry) entries.push(entry); + } + + const outDir = 'apps/website/content/docs-v2/api'; + fs.mkdirSync(outDir, { recursive: true }); + fs.writeFileSync(path.join(outDir, 'api-docs.json'), JSON.stringify(entries, null, 2)); + console.log(`✓ api-docs.json written (${entries.length} entries)`); } main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/apps/website/src/app/docs/[[...slug]]/page.tsx b/apps/website/src/app/docs/[[...slug]]/page.tsx index eb0ecd886..c30d4ff41 100644 --- a/apps/website/src/app/docs/[[...slug]]/page.tsx +++ b/apps/website/src/app/docs/[[...slug]]/page.tsx @@ -3,6 +3,27 @@ import { DocsSidebarNew } from '../../../components/docs/DocsSidebarNew'; import { MdxRendererNew } from '../../../components/docs/MdxRenderer'; import { DocsSearch } from '../../../components/docs/DocsSearch'; import { getDocBySlug, getAllDocSlugs } from '../../../lib/docs-new'; +import { ApiDocRenderer, type ApiDocEntry } from '../../../components/docs/ApiDocRenderer'; +import fs from 'fs'; +import path from 'path'; + +function loadApiDocs(): ApiDocEntry[] { + const candidates = [ + path.join(process.cwd(), 'apps', 'website', 'content', 'docs-v2', 'api', 'api-docs.json'), + path.join(process.cwd(), 'content', 'docs-v2', 'api', 'api-docs.json'), + ]; + for (const p of candidates) { + if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf8')); + } + return []; +} + +const API_NAME_MAP: Record = { + 'stream-resource': 'streamResource', + 'provide-stream-resource': 'provideStreamResource', + 'fetch-stream-transport': 'FetchStreamTransport', + 'mock-stream-transport': 'MockStreamTransport', +}; export function generateStaticParams() { return getAllDocSlugs().map(({ section, slug }) => ({ slug: [section, slug] })); @@ -25,6 +46,16 @@ export default async function DocsPage({ params }: { params: Promise<{ slug?: st
+ {section === 'api' && (() => { + const entries = loadApiDocs(); + const target = API_NAME_MAP[slug]; + const apiEntry = target ? entries.find((e: ApiDocEntry) => e.name === target) : null; + return apiEntry ? ( +
+ +
+ ) : null; + })()}
); diff --git a/apps/website/src/components/docs/ApiDocRenderer.tsx b/apps/website/src/components/docs/ApiDocRenderer.tsx new file mode 100644 index 000000000..7841666a1 --- /dev/null +++ b/apps/website/src/components/docs/ApiDocRenderer.tsx @@ -0,0 +1,141 @@ +import { tokens } from '../../../lib/design-tokens'; + +interface ApiParam { + name: string; + type: string; + description: string; + optional?: boolean; +} + +interface ApiMethod { + name: string; + signature: string; + description: string; + params?: ApiParam[]; +} + +export interface ApiDocEntry { + name: string; + kind: 'function' | 'class' | 'interface' | 'type'; + description: string; + signature?: string; + params?: ApiParam[]; + returns?: { type: string; description: string }; + examples?: string[]; + properties?: ApiParam[]; + methods?: ApiMethod[]; +} + +function KindBadge({ kind }: { kind: string }) { + return ( + {kind} + ); +} + +function ParamTable({ params }: { params: ApiParam[] }) { + return ( + + + + {['Parameter', 'Type', 'Description'].map((h) => ( + + ))} + + + + {params.map((p) => ( + + + + + + ))} + +
{h}
{p.name}{p.optional ? '?' : ''}{p.type}{p.description}
+ ); +} + +export function ApiDocRenderer({ entry }: { entry: ApiDocEntry }) { + return ( +
+
+ {entry.name} + +
+ +

{entry.description}

+ + {entry.signature && ( +
+
+            {entry.signature}
+          
+
+ )} + + {entry.params && entry.params.length > 0 && ( +
+

Parameters

+ +
+ )} + + {entry.returns && ( +
+

Returns

+ {entry.returns.type} +
+ )} + + {entry.properties && entry.properties.length > 0 && ( +
+

Properties

+ +
+ )} + + {entry.methods && entry.methods.length > 0 && ( +
+

Methods

+ {entry.methods.map((m) => ( +
+ {m.signature} + {m.description &&

{m.description}

} + {m.params && m.params.length > 0 && } +
+ ))} +
+ )} + + {entry.examples && entry.examples.length > 0 && ( +
+

Examples

+ {entry.examples.map((ex, i) => ( +
+
+                {ex.replace(/^```\w*\n?/, '').replace(/\n?```$/, '')}
+              
+
+ ))} +
+ )} +
+ ); +} diff --git a/docs/superpowers/plans/2026-04-04-docs-content-authoring.md b/docs/superpowers/plans/2026-04-04-docs-content-authoring.md new file mode 100644 index 000000000..7113263db --- /dev/null +++ b/docs/superpowers/plans/2026-04-04-docs-content-authoring.md @@ -0,0 +1,1406 @@ +# Docs Content Authoring Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Write all 15 remaining docs pages with real code examples from cockpit implementations, using the MDX component system (Callout, Steps, Tabs, CardGroup). + +**Architecture:** Each task writes one MDX file to `apps/website/content/docs-v2/[section]/[slug].mdx`. Content uses cockpit capability patterns adapted for streamResource() Angular usage. All tasks are independent and can be parallelized. The 3 existing placeholder pages (introduction, streaming, stream-resource API) are already written. + +**Tech Stack:** MDX with custom components (Callout, Steps, Tabs, Tab, CardGroup, Card) + +**Note:** Tasks are grouped by section for readability. All tasks within a group are independent of each other. The MDX components available are: ``, ``, ``, ``. + +--- + +### Task 1: Quick Start Guide + +**Files:** +- Create: `apps/website/content/docs-v2/getting-started/quickstart.mdx` + +- [ ] **Step 1: Write the quickstart page** + +This is the most important onboarding page. Walk through building a chat component in 5 minutes. + +```mdx +# Quick Start + +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. + +```typescript +// app.config.ts +import { provideStreamResource } from '@cacheplane/stream-resource'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ + apiUrl: 'http://localhost:2024', + }), + ], +}; +``` + +## 3. Create a chat component + +Use `streamResource()` in a component field initializer. Every property on the returned ref is an Angular Signal. + + + + +```typescript +// chat.component.ts +import { Component, 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', +}) +export class ChatComponent { + input = signal(''); + + chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'chat_agent', + threadId: signal(localStorage.getItem('threadId')), + onThreadId: (id) => localStorage.setItem('threadId', id), + }); + + isStreaming = computed(() => this.chat.status() === 'loading'); + + send() { + const msg = this.input(); + if (!msg.trim()) return; + this.chat.submit({ messages: [{ role: 'user', content: msg }] }); + this.input.set(''); + } +} +``` + + + + +```html + +
+ @for (msg of chat.messages(); track $index) { +
+

{{ msg.content }}

+
+ } + + @if (isStreaming()) { +
Agent is thinking...
+ } + +
+ + +
+
+``` + +
+
+ +## 4. Start your LangGraph server + +Make sure your LangGraph agent is running at the URL you configured. + +```bash +langgraph dev +``` + +## 5. Run your app + +```bash +ng serve +``` + +Open `http://localhost:4200` and start chatting with your agent. + +## Next steps + + + + Learn about token-by-token updates and stream modes + + + Keep conversations alive across page refreshes + + + Add human-in-the-loop approval flows + + + Test your agent integration deterministically + + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/getting-started/quickstart.mdx +git commit -m "docs(website): write Quick Start guide" +``` + +--- + +### Task 2: Installation Guide + +**Files:** +- Create: `apps/website/content/docs-v2/getting-started/installation.mdx` + +- [ ] **Step 1: Write the installation page** + +```mdx +# Installation + +Detailed setup guide for streamResource() in your Angular application. + +## Requirements + + + +streamResource() uses Angular Signals and the modern injection context API. Angular 20 or later is required. + + +Required for the build toolchain and package installation. + + +A running LangGraph agent accessible via HTTP. Can be local (langgraph dev) or deployed (LangGraph Cloud). + + + +## Install the package + +```bash +npm install @cacheplane/stream-resource +``` + +This installs the library and its peer dependencies including `@langchain/langgraph-sdk`. + +## Configure the provider + +Add `provideStreamResource()` to your application configuration. This sets global defaults for all streamResource instances. + +```typescript +// app.config.ts +import { ApplicationConfig } from '@angular/core'; +import { provideStreamResource } from '@cacheplane/stream-resource'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideStreamResource({ + apiUrl: process.env['LANGGRAPH_URL'] ?? 'http://localhost:2024', + }), + ], +}; +``` + + +Any option passed to `streamResource()` directly overrides the global provider config. You can set a default `apiUrl` globally and override it for specific agents. + + +## Environment setup + + + + +For local development, run a LangGraph server: + +```bash +# Start LangGraph dev server +langgraph dev + +# Your agent will be available at http://localhost:2024 +``` + + + + +For production, point to your LangGraph Cloud deployment: + +```typescript +provideStreamResource({ + apiUrl: 'https://your-project.langgraph.app', +}) +``` + + + + +## Verify installation + +Create a minimal test to verify the setup works: + +```typescript +import { streamResource } from '@cacheplane/stream-resource'; + +// In a component +const test = streamResource({ + assistantId: 'chat_agent', +}); + +// If status() returns 'idle', the setup is correct +console.log(test.status()); // 'idle' +``` + +## Next steps + + + + Build your first chat component in 5 minutes + + + Understand how Signals power streamResource + + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/getting-started/installation.mdx +git commit -m "docs(website): write Installation guide" +``` + +--- + +### Task 3: Persistence Guide + +**Files:** +- Create: `apps/website/content/docs-v2/guides/persistence.mdx` + +- [ ] **Step 1: Write the persistence guide** + +Source: `cockpit/langgraph/persistence/` — state checkpointing and thread recovery. + +```mdx +# Persistence + +Thread persistence keeps conversations alive across page refreshes, browser restarts, and session changes. streamResource() manages thread state through the `threadId` signal and `onThreadId` callback. + + +LangGraph checkpoints state at every super-step. streamResource() connects to these checkpoints via thread IDs, letting you resume exactly where you left off. + + +## Basic thread persistence + +Save the thread ID to localStorage so conversations survive page refreshes. + + + + +```typescript +// chat.component.ts +const chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'chat_agent', + threadId: signal(localStorage.getItem('threadId')), + onThreadId: (id) => localStorage.setItem('threadId', id), +}); +``` + + + + +```html + + +@for (msg of chat.messages(); track $index) { +

{{ msg.content }}

+} +``` + +
+
+ +## Reactive thread switching + +Pass a Signal as `threadId` to reactively switch between conversations. + +```typescript +// conversation-list.component.ts +activeThreadId = signal(null); + +chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'chat_agent', + threadId: this.activeThreadId, // Signal — switches reactively + onThreadId: (id) => this.activeThreadId.set(id), +}); + +// Switch to a different conversation +selectThread(id: string) { + this.activeThreadId.set(id); + // streamResource automatically loads the new thread's state +} +``` + + +Use the `isThreadLoading()` signal to show a loading indicator while thread state is being fetched from the server. + + +## Manual thread switching + +Use `switchThread()` for imperative thread changes that also reset derived state. + +```typescript +// Reset and start a new conversation +newConversation() { + this.chat.switchThread(null); + // Creates a new thread on next submit +} + +// Switch to a specific thread +loadConversation(threadId: string) { + this.chat.switchThread(threadId); +} +``` + +## Checkpoint recovery + +When a connection drops, streamResource() can rejoin an in-progress run. + +```typescript +// Rejoin a running stream +await chat.joinStream(runId, lastEventId); +// Picks up from where the connection was lost +``` +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/guides/persistence.mdx +git commit -m "docs(website): write Persistence guide" +``` + +--- + +### Task 4: Interrupts Guide + +**Files:** +- Create: `apps/website/content/docs-v2/guides/interrupts.mdx` + +- [ ] **Step 1: Write the interrupts guide** + +Source: `cockpit/langgraph/interrupts/` — human-in-the-loop pausing with typed payloads. + +```mdx +# Interrupts + +Interrupts let your LangGraph agent pause execution and wait for human input. streamResource() surfaces interrupts as Angular Signals, making it easy to build approval flows, confirmation dialogs, and human-in-the-loop experiences. + + +Use interrupts for human approval, late-binding decisions, or any step where the agent needs external input before continuing. + + +## Basic interrupt handling + +When an agent interrupts, the `interrupt()` signal contains the interrupt data. + + + + +```typescript +// approval.component.ts +interface ApprovalPayload { + action: string; + description: string; + risk: 'low' | 'medium' | 'high'; +} + +const agent = streamResource({ + assistantId: 'approval_agent', +}); + +// Check for pending interrupts +pendingApproval = computed(() => agent.interrupt()); +``` + + + + +```html + +@if (pendingApproval(); as approval) { +
+

Agent needs approval

+

{{ approval.value.description }}

+

Risk level: {{ approval.value.risk }}

+ + +
+} +``` + +
+
+ +## Resuming from an interrupt + +Call `submit()` with the resume payload to continue execution. + +```typescript +approve() { + this.agent.submit(null, { resume: { approved: true } }); +} + +reject() { + this.agent.submit(null, { resume: { approved: false, reason: 'User rejected' } }); +} +``` + +## Multiple interrupts + +The `interrupts()` signal tracks all interrupts received during a run, not just the current one. + +```typescript +// Track interrupt history +allInterrupts = computed(() => agent.interrupts()); +latestInterrupt = computed(() => agent.interrupt()); +interruptCount = computed(() => agent.interrupts().length); +``` + + +Use the BagTemplate generic parameter to type your interrupt payloads for full TypeScript safety. + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/guides/interrupts.mdx +git commit -m "docs(website): write Interrupts guide" +``` + +--- + +### Task 5: Memory Guide + +**Files:** +- Create: `apps/website/content/docs-v2/guides/memory.mdx` + +- [ ] **Step 1: Write the memory guide** + +Source: `cockpit/langgraph/memory/` + `cockpit/deep-agents/memory/` — durable context retention. + +```mdx +# Memory + +Memory in LangGraph preserves useful context that later steps can read back. streamResource() exposes memory through the messages and state signals, with thread persistence providing cross-session continuity. + + +Short-term memory lives within a thread (conversation history). Long-term memory persists across threads via LangGraph's memory store. + + +## Short-term memory (thread-scoped) + +Every message in a thread is automatically preserved. When you reconnect with the same `threadId`, the full conversation history is restored. + +```typescript +const chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'memory_agent', + threadId: signal(userId()), // User-specific thread +}); + +// Messages accumulate across the conversation +const messageCount = computed(() => chat.messages().length); + +// Resume where you left off on next visit +// threadId persists, so history is restored +``` + +## Accessing agent state as memory + +The `value()` signal contains the full agent state, which can include custom memory fields. + +```typescript +interface AgentState { + messages: BaseMessage[]; + userPreferences: { theme: string; language: string }; + projectContext: { name: string; files: string[] }; +} + +const agent = streamResource({ + assistantId: 'context_agent', + threadId: signal(projectId()), +}); + +// Read memory fields from agent state +const prefs = computed(() => agent.value().userPreferences); +const context = computed(() => agent.value().projectContext); +``` + +## Cross-session memory + +Thread persistence enables memory that spans sessions. The agent decides what to store in its state. + +```typescript +// User returns days later — same threadId resumes context +const agent = streamResource({ + assistantId: 'memory_agent', + threadId: signal(localStorage.getItem('agent-thread')), + onThreadId: (id) => localStorage.setItem('agent-thread', id), +}); + +// Agent recalls past decisions, preferences, and context +// No explicit memory management needed on the Angular side +``` + + +The agent controls what gets stored in memory. streamResource() just surfaces the current state. Design your agent's state schema to include the fields you want to persist. + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/guides/memory.mdx +git commit -m "docs(website): write Memory guide" +``` + +--- + +### Task 6: Time Travel Guide + +**Files:** +- Create: `apps/website/content/docs-v2/guides/time-travel.mdx` + +- [ ] **Step 1: Write the time travel guide** + +Source: `cockpit/langgraph/time-travel/` — checkpoint inspection and execution branching. + +```mdx +# Time Travel + +Time travel lets you inspect earlier states and replay alternate execution paths. streamResource() exposes the full checkpoint history and branch navigation through Angular Signals. + + +Debug agent decisions, explore alternate paths, and build undo/redo experiences for your users. + + +## Browsing execution history + +The `history()` signal contains an array of `ThreadState` checkpoints. + +```typescript +const agent = streamResource({ + assistantId: 'agent', + threadId: signal(threadId), +}); + +// Full execution timeline +const checkpoints = computed(() => agent.history()); +const checkpointCount = computed(() => agent.history().length); +``` + +## Forking from a checkpoint + +Submit with a specific checkpoint to branch execution from an earlier state. + +```typescript +// Fork from the 3rd checkpoint with new input +forkFromCheckpoint(index: number) { + const checkpoint = this.agent.history()[index]; + this.agent.submit( + { messages: [{ role: 'user', content: 'Try a different approach' }] }, + { checkpoint: checkpoint.checkpoint } + ); +} +``` + +## Branch navigation + +Use `branch()` and `setBranch()` to navigate between execution branches. + +```typescript +// Current branch +const activeBranch = computed(() => agent.branch()); + +// Switch to a different branch +selectBranch(branchId: string) { + agent.setBranch(branchId); +} +``` + + +Time travel is most useful during development. Inspect why an agent chose a particular path, then fork to test alternatives without restarting the conversation. + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/guides/time-travel.mdx +git commit -m "docs(website): write Time Travel guide" +``` + +--- + +### Task 7: Subgraphs Guide + +**Files:** +- Create: `apps/website/content/docs-v2/guides/subgraphs.mdx` + +- [ ] **Step 1: Write the subgraphs guide** + +Source: `cockpit/langgraph/subgraphs/` + `cockpit/deep-agents/subagents/` — modular agent composition. + +```mdx +# Subgraphs + +Subgraphs let you compose complex agents from smaller, focused units. streamResource() tracks subagent execution through dedicated signals, giving you visibility into delegated work. + + +LangGraph calls them subgraphs (modular graph composition). Deep Agents calls them subagents (task delegation). streamResource() supports both patterns through the same API. + + +## Tracking subagent execution + +The `subagents()` signal contains a Map of active subagent streams. + +```typescript +const orchestrator = streamResource({ + assistantId: 'orchestrator', + subagentToolNames: ['research', 'analyze', 'summarize'], +}); + +// All subagent streams +const subagents = computed(() => orchestrator.subagents()); + +// Only active ones +const running = computed(() => orchestrator.activeSubagents()); +const runningCount = computed(() => running().length); +``` + +## Subagent stream details + +Each `SubagentStreamRef` provides its own signals. + +```typescript +// Access a specific subagent +const researchAgent = computed(() => + orchestrator.subagents().get('research-tool-call-id') +); + +// Track its progress +const researchStatus = computed(() => researchAgent()?.status()); +const researchMessages = computed(() => researchAgent()?.messages() ?? []); +``` + +## Filtering subagent messages + +By default, subagent messages appear in the parent's `messages()` signal. Filter them out for a cleaner parent view. + +```typescript +const orchestrator = streamResource({ + assistantId: 'orchestrator', + filterSubagentMessages: true, // Hide subagent messages from parent + subagentToolNames: ['research', 'analyze'], +}); + +// Parent messages only (no subagent chatter) +const parentMessages = computed(() => orchestrator.messages()); +``` + + +Set `subagentToolNames` to the tool names that spawn subagents. streamResource() uses this to identify which tool calls create subagent streams. + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/guides/subgraphs.mdx +git commit -m "docs(website): write Subgraphs guide" +``` + +--- + +### Task 8: Testing Guide + +**Files:** +- Create: `apps/website/content/docs-v2/guides/testing.mdx` + +- [ ] **Step 1: Write the testing guide** + +Source: `MockStreamTransport` from the library — deterministic agent testing. + +```mdx +# Testing + +MockStreamTransport lets you test agent interactions deterministically without a running LangGraph server. Script exact event sequences and step through them in your Angular test specs. + + +MockStreamTransport eliminates network dependencies, timing issues, and server state. Every test run produces identical results. + + +## Basic test setup + +Create a MockStreamTransport with scripted events and pass it to streamResource. + +```typescript +import { TestBed } from '@angular/core/testing'; +import { MockStreamTransport } from '@cacheplane/stream-resource'; +import type { StreamEvent } from '@cacheplane/stream-resource'; + +describe('ChatComponent', () => { + it('should display agent messages', () => { + const transport = new MockStreamTransport(); + + TestBed.runInInjectionContext(() => { + const chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'test_agent', + transport, + }); + + // Emit a values event + transport.emit([ + { type: 'values', messages: [{ role: 'assistant', content: 'Hello!' }] }, + ]); + + expect(chat.messages().length).toBe(1); + expect(chat.messages()[0].content).toBe('Hello!'); + }); + }); +}); +``` + +## Scripting event sequences + +Pass event batches to the constructor for sequential playback. + +```typescript +const transport = new MockStreamTransport([ + // Batch 1: Initial response + [{ type: 'values', messages: [{ role: 'assistant', content: 'Analyzing...' }] }], + // Batch 2: Final response + [{ type: 'values', messages: [{ role: 'assistant', content: 'Done!' }] }], +]); + +// Advance through batches +const batch1 = transport.nextBatch(); // First batch +const batch2 = transport.nextBatch(); // Second batch +``` + +## Testing interrupts + +Script an interrupt event to test human-in-the-loop flows. + +```typescript +it('should handle interrupts', () => { + const transport = new MockStreamTransport(); + + TestBed.runInInjectionContext(() => { + const agent = streamResource({ + assistantId: 'approval_agent', + transport, + }); + + // Emit an interrupt + transport.emit([ + { type: 'interrupt', value: { action: 'delete', risk: 'high' } }, + ]); + + expect(agent.interrupt()).toBeDefined(); + expect(agent.interrupt()?.value.risk).toBe('high'); + }); +}); +``` + +## Testing errors + +Inject errors to test error handling. + +```typescript +it('should surface errors', () => { + const transport = new MockStreamTransport(); + + TestBed.runInInjectionContext(() => { + const chat = streamResource({ + assistantId: 'test_agent', + transport, + }); + + transport.emitError(new Error('Connection lost')); + + expect(chat.error()).toBeDefined(); + expect(chat.status()).toBe('error'); + }); +}); +``` + + +streamResource() must be called within an Angular injection context. In tests, wrap calls in `TestBed.runInInjectionContext()`. + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/guides/testing.mdx +git commit -m "docs(website): write Testing guide" +``` + +--- + +### Task 9: Deployment Guide + +**Files:** +- Create: `apps/website/content/docs-v2/guides/deployment.mdx` + +- [ ] **Step 1: Write the deployment guide** + +Source: `cockpit/langgraph/deployment-runtime/` — production configuration. + +```mdx +# Deployment + +Configure streamResource() for production with LangGraph Cloud, environment-based URLs, and error handling patterns. + +## Production configuration + +Point `apiUrl` to your LangGraph Cloud deployment. + + + + +```typescript +// app.config.ts +provideStreamResource({ + apiUrl: environment.langgraphUrl, +}) +``` + +```typescript +// environment.prod.ts +export const environment = { + langgraphUrl: 'https://your-project.langgraph.app', +}; +``` + + + + +```typescript +// app.config.ts +provideStreamResource({ + apiUrl: 'https://your-project.langgraph.app', +}) +``` + + + + +## Error boundaries + +Handle errors gracefully in production. + +```typescript +const chat = streamResource({ + assistantId: 'chat_agent', +}); + +// Reactive error display +hasError = computed(() => chat.status() === 'error'); +errorMessage = computed(() => { + const err = chat.error(); + return err instanceof Error ? err.message : 'Something went wrong'; +}); + +// Retry after error +retry() { + chat.reload(); +} +``` + +## Recovering interrupted streams + +Use `joinStream()` to reconnect to a running stream after a network interruption. + +```typescript +// If you know the run ID (e.g., from a status endpoint) +await chat.joinStream(runId, lastEventId); +// Resumes streaming from where it left off +``` + + +streamResource() is a stateless client. All state lives on the LangGraph Platform. This means your Angular app can be deployed anywhere (CDN, edge, SSR) without state management concerns. + + +## Checklist + + + +Point to your LangGraph Cloud deployment URL. + + +Show user-friendly error messages and retry buttons. + + +Store threadId in localStorage or a backend so users can resume conversations. + + +Set `throttle` option if token-by-token updates are too frequent for your UI. + + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/guides/deployment.mdx +git commit -m "docs(website): write Deployment guide" +``` + +--- + +### Task 10: Angular Signals Concept + +**Files:** +- Create: `apps/website/content/docs-v2/concepts/angular-signals.mdx` + +- [ ] **Step 1: Write the Angular Signals concept page** + +```mdx +# Angular Signals + +streamResource() is built on Angular Signals — the reactive primitive introduced in Angular 16+. Every property on a StreamResourceRef is a Signal, making it work seamlessly with OnPush change detection, computed values, and effect callbacks. + +## Signals primer + +A Signal is a reactive value container. When a Signal's value changes, Angular automatically re-renders any template that reads it. + +```typescript +// streamResource returns Signals, not Observables +const chat = streamResource({ assistantId: 'agent' }); + +chat.messages() // Signal — call to read +chat.status() // Signal +chat.error() // Signal +chat.isLoading() // Signal (computed) +``` + +## Computed values + +Use `computed()` to derive new Signals from streamResource signals. + +```typescript +const lastMessage = computed(() => + chat.messages().at(-1)?.content ?? '' +); + +const messageCount = computed(() => + chat.messages().length +); + +const isIdle = computed(() => + chat.status() === 'idle' +); +``` + +## OnPush change detection + +Because Signals trigger change detection automatically, streamResource works perfectly with `ChangeDetectionStrategy.OnPush`. + +```typescript +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @for (msg of chat.messages(); track $index) { +

{{ msg.content }}

+ } + `, +}) +export class ChatComponent { + chat = streamResource({ assistantId: 'agent' }); +} +``` + +## No RxJS required + +Unlike traditional Angular HTTP patterns, streamResource doesn't use Observables. There are no subscriptions to manage, no async pipes needed, and no memory leak risks. + + +Signals are simpler for UI state. They synchronously read the latest value, compose with computed(), and integrate with Angular's template syntax. streamResource handles the async SSE connection internally and surfaces results as Signals. + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/concepts/angular-signals.mdx +git commit -m "docs(website): write Angular Signals concept page" +``` + +--- + +### Task 11: LangGraph Basics Concept + +**Files:** +- Create: `apps/website/content/docs-v2/concepts/langgraph-basics.mdx` + +- [ ] **Step 1: Write the LangGraph Basics page** + +```mdx +# LangGraph Basics + +LangGraph is a framework for building stateful AI agents as directed graphs. This page explains the core concepts for Angular developers who are new to agent development. + +## Graphs, nodes, and edges + +A LangGraph agent is a directed graph where: + + + +Each node performs one action — calling an LLM, querying a database, or making an API request. Nodes receive state and return updated state. + + +Edges connect nodes. Conditional edges route execution based on state, enabling branching logic. + + +All nodes read from and write to a shared state object. This state is what streamResource() exposes through its signals. + + + +## How streamResource connects + +Your Angular app doesn't run the graph — LangGraph Platform does. streamResource() is the bridge: + +1. Your component calls `submit()` with user input +2. FetchStreamTransport sends an HTTP POST to LangGraph Platform +3. The platform runs the graph and streams state updates via SSE +4. streamResource() updates its Signals as events arrive +5. Angular re-renders your templates automatically + +## State design + +The generic type parameter in `streamResource()` defines your agent's state shape. + +```typescript +// Simple chat state +streamResource<{ messages: BaseMessage[] }>({ ... }) + +// Rich agent state with custom fields +interface AgentState { + messages: BaseMessage[]; + plan: string[]; + currentStep: number; + results: Record; +} +streamResource({ ... }) +``` + + +For deeper LangGraph concepts (persistence, interrupts, memory), see the individual guide pages. + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/concepts/langgraph-basics.mdx +git commit -m "docs(website): write LangGraph Basics concept page" +``` + +--- + +### Task 12: Agent Architecture Concept + +**Files:** +- Create: `apps/website/content/docs-v2/concepts/agent-architecture.mdx` + +- [ ] **Step 1: Write the Agent Architecture page** + +```mdx +# Agent Architecture + +How AI agents work — the planning, execution, and tool-calling lifecycle that streamResource() connects your Angular app to. + +## The agent loop + +An AI agent follows a cycle: + + + +The user sends a message via `submit()`. streamResource() posts it to LangGraph Platform. + + +The LLM decides what to do next — respond directly, call a tool, or delegate to a subagent. + + +Tools run (database queries, API calls, code execution). Results feed back into state. + + +The agent streams its response token-by-token. streamResource() updates the `messages()` signal in real-time. + + +State is checkpointed. The agent may loop back to Plan, or finish. + + + +## Tool calling + +Agents extend their capabilities through tools. streamResource() tracks tool execution: + +```typescript +const agent = streamResource({ + assistantId: 'research_agent', +}); + +// Currently executing tools +const tools = computed(() => agent.toolProgress()); + +// Completed tool calls with results +const completedTools = computed(() => agent.toolCalls()); +``` + +## Multi-agent patterns + +Complex tasks use multiple agents working together: + +- **Orchestrator** — one agent delegates to specialized subagents +- **Pipeline** — agents process sequentially, each refining the output +- **Debate** — agents review each other's work + +streamResource() supports these patterns through the `subagents()` and `activeSubagents()` signals. + + +Most applications only need a single agent with tools. Add subagents when you need true task delegation with isolated state. + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/concepts/agent-architecture.mdx +git commit -m "docs(website): write Agent Architecture concept page" +``` + +--- + +### Task 13: State Management Concept + +**Files:** +- Create: `apps/website/content/docs-v2/concepts/state-management.mdx` + +- [ ] **Step 1: Write the State Management page** + +```mdx +# State Management + +How state flows through streamResource() — from LangGraph's server-side state machine to Angular Signals in your templates. + +## State lives on the server + +Unlike traditional Angular state management (NgRx, signals stores), agent state lives on the LangGraph Platform. Your Angular app is a stateless view layer. + +``` +LangGraph Platform (source of truth) + ↓ SSE stream +FetchStreamTransport (transport layer) + ↓ events +streamResource() (signal conversion) + ↓ Signals +Angular templates (reactive rendering) +``` + +## The state shape + +Your state type defines what the agent manages. The `value()` signal exposes the full state object. + +```typescript +interface ProjectState { + messages: BaseMessage[]; + files: string[]; + analysis: { score: number; issues: string[] }; +} + +const agent = streamResource({ + assistantId: 'project_agent', +}); + +// Access any state field as a reactive value +const files = computed(() => agent.value().files); +const score = computed(() => agent.value().analysis.score); +``` + +## Thread state vs application state + + +Thread state (managed by LangGraph) and application state (managed by Angular) are separate concerns. Don't try to sync them — read thread state from signals, manage UI state with Angular signals. + + +```typescript +// Thread state — from the agent +const messages = agent.messages(); // Read-only signal +const agentStatus = agent.status(); // Read-only signal + +// Application state — your Angular code +const sidebarOpen = signal(true); // Your UI state +const selectedTab = signal('chat'); // Your UI state +``` + +## State updates are immutable + +Every state update from the agent creates a new signal value. Angular's change detection picks this up automatically. + +```typescript +// This works with OnPush because the Signal reference changes +@for (msg of agent.messages(); track $index) { +

{{ msg.content }}

+} + +// Computed values re-evaluate when dependencies change +const hasErrors = computed(() => + agent.value().analysis.issues.length > 0 +); +``` +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/concepts/state-management.mdx +git commit -m "docs(website): write State Management concept page" +``` + +--- + +### Task 14: Update Existing Placeholder — Introduction + +**Files:** +- Modify: `apps/website/content/docs-v2/getting-started/introduction.mdx` + +The existing introduction is decent but needs expansion to match the quality of the other pages. Add more detail about the dual audience and link to all guide pages. + +- [ ] **Step 1: Enhance the introduction page** + +Replace the full content of `apps/website/content/docs-v2/getting-started/introduction.mdx`: + +```mdx +# Introduction + +StreamResource brings full parity with React's `useStream()` hook to Angular 20+. It's the enterprise streaming resource for LangChain and Angular — built natively with Angular Signals, not wrapped or adapted. + + +StreamResource serves two audiences: **Angular developers** building AI-powered applications, and **AI/agent developers** who need a production Angular frontend for their LangGraph agents. + + +## What is streamResource()? + +`streamResource()` is an Angular function that creates a reactive connection to a LangGraph agent. It returns an object whose properties are Angular Signals — meaning your templates update automatically as the agent streams responses. + +```typescript +const chat = streamResource<{ messages: BaseMessage[] }>({ + assistantId: 'chat_agent', +}); + +// Every property is a Signal +chat.messages() // Signal +chat.status() // Signal<'idle' | 'loading' | 'resolved' | 'error'> +chat.interrupt() // Signal +chat.history() // Signal +``` + +## What you can build + + + +Token-by-token streaming with real-time UI updates. Messages arrive as they're generated. + + +Agents pause for approval, confirmation, or correction. Your UI handles the interrupt and resumes execution. + + +Track multiple subagents working in parallel, each with their own message stream and status. + + +Inspect agent execution history, fork from checkpoints, and explore alternate paths. + + + +## Guides + + + + Build a chat component in 5 minutes + + + Detailed setup and configuration + + + Token-by-token updates via SSE + + + Thread persistence across sessions + + + Human-in-the-loop approval flows + + + Deterministic testing with MockStreamTransport + + +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/website/content/docs-v2/getting-started/introduction.mdx +git commit -m "docs(website): enhance Introduction page with expanded content" +``` + +--- + +### Task 15: Final Build Verification + +- [ ] **Step 1: Build the website** + +Run: `npx nx build website --skip-nx-cache 2>&1 | tail -10` +Expected: Build succeeds with all 19 doc pages generated + +- [ ] **Step 2: Verify all pages render** + +Open each page in the browser and verify content loads: +- http://localhost:3000/docs/getting-started/introduction +- http://localhost:3000/docs/getting-started/quickstart +- http://localhost:3000/docs/getting-started/installation +- http://localhost:3000/docs/guides/streaming (existing) +- http://localhost:3000/docs/guides/persistence +- http://localhost:3000/docs/guides/interrupts +- http://localhost:3000/docs/guides/memory +- http://localhost:3000/docs/guides/time-travel +- http://localhost:3000/docs/guides/subgraphs +- http://localhost:3000/docs/guides/testing +- http://localhost:3000/docs/guides/deployment +- http://localhost:3000/docs/concepts/angular-signals +- http://localhost:3000/docs/concepts/langgraph-basics +- http://localhost:3000/docs/concepts/agent-architecture +- http://localhost:3000/docs/concepts/state-management +- http://localhost:3000/docs/api/stream-resource (existing) + +- [ ] **Step 3: Commit any fixes** + +```bash +git add -A +git commit -m "fix(website): docs content polish" +``` diff --git a/libs/stream-resource/src/lib/stream-resource.fn.ts b/libs/stream-resource/src/lib/stream-resource.fn.ts index 21b862d28..517459317 100644 --- a/libs/stream-resource/src/lib/stream-resource.fn.ts +++ b/libs/stream-resource/src/lib/stream-resource.fn.ts @@ -24,6 +24,32 @@ import { import type { ThreadState, ToolProgress, ToolCallWithResult } from '@langchain/langgraph-sdk'; import { createStreamManagerBridge } from './internals/stream-manager.bridge'; +/** + * Creates a streaming resource connected to a LangGraph agent. + * + * Must be called within an Angular injection context (component constructor, + * field initializer, or `runInInjectionContext`). Returns a ref object whose + * properties are Angular Signals that update in real-time as the agent streams. + * + * @typeParam T - The state shape returned by the agent (e.g., `{ messages: BaseMessage[] }`) + * @typeParam Bag - Optional bag template for typed interrupts and submit payloads + * @param options - Configuration for the streaming resource + * @returns A {@link StreamResourceRef} with reactive signals and action methods + * + * @example + * ```typescript + * // In a component field initializer + * const chat = streamResource<{ messages: BaseMessage[] }>({ + * assistantId: 'chat_agent', + * apiUrl: 'http://localhost:2024', + * threadId: signal(this.savedThreadId), + * onThreadId: (id) => localStorage.setItem('threadId', id), + * }); + * + * // Access signals in template + * // chat.messages(), chat.status(), chat.error() + * ``` + */ export function streamResource< T = Record, Bag extends BagTemplate = BagTemplate, diff --git a/libs/stream-resource/src/lib/stream-resource.provider.ts b/libs/stream-resource/src/lib/stream-resource.provider.ts index 966466cd3..685d250ee 100644 --- a/libs/stream-resource/src/lib/stream-resource.provider.ts +++ b/libs/stream-resource/src/lib/stream-resource.provider.ts @@ -2,14 +2,39 @@ import { InjectionToken, Provider } from '@angular/core'; import { StreamResourceTransport } from './stream-resource.types'; +/** + * Global configuration for streamResource instances. + * Properties set here serve as defaults that can be overridden per-call. + */ export interface StreamResourceConfig { + /** Base URL of the LangGraph Platform API (e.g., `'http://localhost:2024'`). */ apiUrl?: string; + /** Custom transport implementation. Defaults to {@link FetchStreamTransport}. */ transport?: StreamResourceTransport; } export const STREAM_RESOURCE_CONFIG = new InjectionToken('STREAM_RESOURCE_CONFIG'); +/** + * Angular provider factory that registers global defaults for all + * streamResource instances in the application. + * + * Add to your `app.config.ts` or module providers array. + * + * @param config - Global configuration merged with per-call options + * @returns An Angular Provider for dependency injection + * + * @example + * ```typescript + * // app.config.ts + * export const appConfig: ApplicationConfig = { + * providers: [ + * provideStreamResource({ apiUrl: 'http://localhost:2024' }), + * ], + * }; + * ``` + */ export function provideStreamResource(config: StreamResourceConfig): Provider { return { provide: STREAM_RESOURCE_CONFIG, diff --git a/libs/stream-resource/src/lib/stream-resource.types.ts b/libs/stream-resource/src/lib/stream-resource.types.ts index 2cdc9455b..0c9749c7d 100644 --- a/libs/stream-resource/src/lib/stream-resource.types.ts +++ b/libs/stream-resource/src/lib/stream-resource.types.ts @@ -37,7 +37,9 @@ export type ResourceStatus = NgResourceStatus; // ── Transport interface ────────────────────────────────────────────────────── +/** An event emitted by a LangGraph stream. */ export interface StreamEvent { + /** Event type identifier (e.g., 'values', 'messages', 'error', 'interrupt'). */ type: | 'values' | 'messages' @@ -56,7 +58,9 @@ export interface StreamEvent { [key: string]: unknown; } +/** Transport interface for connecting to a LangGraph agent. */ export interface StreamResourceTransport { + /** Open a streaming connection to an agent and yield events. */ stream( assistantId: string, threadId: string | null, @@ -75,63 +79,104 @@ export interface StreamResourceTransport { // ── Options ────────────────────────────────────────────────────────────────── +/** Options for creating a streaming resource via {@link streamResource}. */ export interface StreamResourceOptions { + /** Base URL of the LangGraph Platform API. */ apiUrl: string; + /** Agent or graph identifier on the LangGraph platform. */ assistantId: string; + /** Thread ID to connect to. Pass a Signal for reactive thread switching. */ threadId?: Signal | string | null; + /** Called when a new thread is auto-created by the transport. */ onThreadId?: (id: string) => void; + /** Initial state values before the first stream response arrives. */ initialValues?: Partial; + /** Key in the state object that contains the messages array. Defaults to `'messages'`. */ messagesKey?: string; + /** Throttle signal updates in milliseconds. `false` to disable. */ throttle?: number | false; + /** Custom message deserializer for non-standard message formats. */ toMessage?: (msg: unknown) => BaseMessage; + /** Custom transport. Defaults to FetchStreamTransport. */ transport?: StreamResourceTransport; + /** When true, subagent messages are filtered from the main messages signal. */ filterSubagentMessages?: boolean; + /** Tool names that indicate a subagent invocation. */ subagentToolNames?: string[]; } // ── SubagentStreamRef ──────────────────────────────────────────────────────── +/** Reference to a subagent's streaming state. */ export interface SubagentStreamRef { + /** The tool call ID that spawned this subagent. */ toolCallId: string; + /** Current execution status of the subagent. */ status: Signal<'pending' | 'running' | 'complete' | 'error'>; + /** Current state values from the subagent. */ values: Signal>; + /** Messages from the subagent conversation. */ messages: Signal; } // ── StreamResourceRef ──────────────────────────────────────────────────────── +/** Reactive reference returned by {@link streamResource}. All properties are Angular Signals. */ export interface StreamResourceRef { // ResourceRef compatible members (duck-typed, not inherited) + /** Current agent state values. */ value: Signal; + /** Current resource status: idle, loading, resolved, or error. */ status: Signal; + /** True when the resource is actively streaming. */ isLoading: Signal; + /** Last error, if any. */ error: Signal; + /** True once at least one value or message has been received. */ hasValue: Signal; + /** Re-submits the last input to restart the stream. */ reload: () => void; // Streaming state + /** Current list of messages from the agent conversation. */ messages: Signal; + /** Current interrupt data, if the agent is paused for input. */ interrupt: Signal | undefined>; + /** All interrupts received during the current run. */ interrupts: Signal[]>; + /** Progress updates for currently executing tools. */ toolProgress: Signal; + /** Completed tool calls with their results. */ toolCalls: Signal; // Thread & history + /** Current branch identifier for time-travel navigation. */ branch: Signal; + /** Full execution history of the current thread. */ history: Signal[]>; + /** True while a thread switch is loading state from the server. */ isThreadLoading: Signal; // Subagents + /** Map of active subagent streams keyed by tool call ID. */ subagents: Signal>; + /** Filtered list of subagents with status 'running'. */ activeSubagents: Signal; // Actions + /** Send a message or resume from an interrupt. Returns immediately. */ submit: (values: ResolvedBag['UpdateType'] | null, opts?: SubmitOptions) => Promise; + /** Abort the current stream. */ stop: () => Promise; + /** Switch to a different thread, resetting derived state. */ switchThread: (threadId: string | null) => void; + /** Join an already-running stream by run ID. */ joinStream: (runId: string, lastEventId?: string) => Promise; + /** Set the active branch for time-travel navigation. */ setBranch: (branch: string) => void; + /** Get metadata for a specific message by index. */ getMessagesMetadata: (msg: BaseMessage, idx?: number) => MessageMetadata> | undefined; + /** Get tool call results associated with an AI message. */ getToolCalls: (msg: CoreAIMessage) => ToolCallWithResult[]; } diff --git a/libs/stream-resource/src/lib/transport/fetch-stream.transport.ts b/libs/stream-resource/src/lib/transport/fetch-stream.transport.ts index 6344215de..756e4fabd 100644 --- a/libs/stream-resource/src/lib/transport/fetch-stream.transport.ts +++ b/libs/stream-resource/src/lib/transport/fetch-stream.transport.ts @@ -2,15 +2,34 @@ import { Client } from '@langchain/langgraph-sdk'; import { StreamResourceTransport, StreamEvent } from '../stream-resource.types'; +/** + * Production transport that connects to a LangGraph Platform API via HTTP and SSE. + * + * Creates threads automatically if no threadId is provided, and streams events + * using the LangGraph SDK client. + * + * @example + * ```typescript + * const transport = new FetchStreamTransport( + * 'http://localhost:2024', + * (id) => console.log('New thread:', id), + * ); + * ``` + */ export class FetchStreamTransport implements StreamResourceTransport { private client: Client; private onThreadId?: (id: string) => void; + /** + * @param apiUrl - Base URL of the LangGraph Platform API + * @param onThreadId - Optional callback invoked when a new thread is created + */ constructor(apiUrl: string, onThreadId?: (id: string) => void) { this.client = new Client({ apiUrl }); this.onThreadId = onThreadId; } + /** Open a streaming connection, creating a thread if needed. */ async *stream( assistantId: string, threadId: string | null, @@ -38,6 +57,7 @@ export class FetchStreamTransport implements StreamResourceTransport { } } + /** Join an already-started run without creating a new thread. */ async *joinStream( threadId: string, runId: string, diff --git a/libs/stream-resource/src/lib/transport/mock-stream.transport.ts b/libs/stream-resource/src/lib/transport/mock-stream.transport.ts index e9be661ed..8a1ed5f6e 100644 --- a/libs/stream-resource/src/lib/transport/mock-stream.transport.ts +++ b/libs/stream-resource/src/lib/transport/mock-stream.transport.ts @@ -1,6 +1,20 @@ // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 import { StreamResourceTransport, StreamEvent } from '../stream-resource.types'; +/** + * Test transport for deterministic agent testing without a real LangGraph server. + * + * Script event batches upfront, then emit them manually or step through them + * in your test specs. Supports error injection and close control. + * + * @example + * ```typescript + * const transport = new MockStreamTransport([ + * [{ type: 'values', data: { messages: [aiMsg('Hello')] } }], + * [{ type: 'values', data: { status: 'done' } }], + * ]); + * ``` + */ export class MockStreamTransport implements StreamResourceTransport { private script: StreamEvent[][]; private scriptIndex = 0; @@ -11,30 +25,36 @@ export class MockStreamTransport implements StreamResourceTransport { private closed = false; private pendingError: Error | null = null; + /** @param script - Array of event batches. Each batch is emitted as a group. */ constructor(script: StreamEvent[][] = []) { this.script = script; } + /** Advance to the next scripted batch and return its events. */ nextBatch(): StreamEvent[] { if (this.scriptIndex >= this.script.length) return []; return this.script[this.scriptIndex++]; } + /** Manually emit events into the stream. */ emit(events: StreamEvent[]): void { this.eventQueue.push(...events); this.flush(); } + /** Inject an error into the stream. */ emitError(err: Error): void { this.pendingError = err; this.flush(); } + /** Close the stream. Remaining queued events are drained before completion. */ close(): void { this.closed = true; this.flush(); } + /** Returns true if a stream is currently active. */ isStreaming(): boolean { return this.streaming; }