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/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/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; }