diff --git a/apps/demo/src/app/chat-demo/chat-demo.component.ts b/apps/demo/src/app/chat-demo/chat-demo.component.ts index 490f641a8..3d726a0a3 100644 --- a/apps/demo/src/app/chat-demo/chat-demo.component.ts +++ b/apps/demo/src/app/chat-demo/chat-demo.component.ts @@ -1,6 +1,5 @@ import { Component, Input, OnInit, Injector, runInInjectionContext } from '@angular/core'; import { agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; @Component({ selector: 'stream-chat-demo', @@ -8,7 +7,7 @@ import type { BaseMessage } from '@langchain/core/messages'; template: `
-
+
{{ msg.content }}
Thinking…
@@ -34,14 +33,14 @@ export class ChatDemoComponent implements OnInit { @Input() apiUrl = 'http://localhost:2024'; @Input() assistantId = 'chat_agent'; - chat: ReturnType> | null = null; + chat: ReturnType | null = null; constructor(private injector: Injector) {} ngOnInit() { // @Input() values are available in ngOnInit, so use runInInjectionContext runInInjectionContext(this.injector, () => { - this.chat = agent<{ messages: BaseMessage[] }>({ + this.chat = agent({ apiUrl: this.apiUrl, assistantId: this.assistantId, }); @@ -55,7 +54,6 @@ export class ChatDemoComponent implements OnInit { const content = input.value.trim(); if (!content || !this.chat) return; input.value = ''; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.chat.submit({ messages: [{ role: 'human', content }] } as any); + this.chat.submit({ message: content }); } } diff --git a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx index c75891d14..be11f1825 100644 --- a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx @@ -18,7 +18,7 @@ AG-UI is the open agent-to-UI protocol from the CopilotKit ecosystem. It standar ┌───────────┴───────────┐ ▼ ▼ @ngaf/langgraph @ngaf/ag-ui - (toAgent: AgentRef→Agent) (toAgent: AbstractAgent→Agent) + (LangGraphAgent) (toAgent: AbstractAgent→Agent) │ │ ▼ ▼ LangGraph Platform Any AG-UI backend @@ -28,7 +28,7 @@ AG-UI is the open agent-to-UI protocol from the CopilotKit ecosystem. It standar ## What you get -- **`toAgent(source: AbstractAgent): Agent`** — wraps any `AbstractAgent` subclass (custom transports, mocks). +- **`toAgent(source: AbstractAgent): Agent`** — wraps any `AbstractAgent` subclass (custom transports, mocks) into the runtime-neutral `Agent` contract. - **`provideAgUiAgent({ url })`** — DI convenience that instantiates `HttpAgent` under the hood for the common SSE/HTTP case. - **`FakeAgent`** — in-process `AbstractAgent` subclass that emits canned streaming events for offline demos and tests. diff --git a/apps/website/content/docs/agent/api/api-docs.json b/apps/website/content/docs/agent/api/api-docs.json index 3297086c2..0637a088a 100644 --- a/apps/website/content/docs/agent/api/api-docs.json +++ b/apps/website/content/docs/agent/api/api-docs.json @@ -1,762 +1 @@ -[ - { - "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": "MockAgentTransport", - "kind": "class", - "description": "Test transport for deterministic agent testing without a real LangGraph server.\n\nScript event batches upfront, then emit them manually or step through them\nin your test specs. Supports error injection and close control.", - "params": [ - { - "name": "script", - "type": "StreamEvent[][]", - "description": "Array of event batches. Each batch is emitted as a group.", - "optional": false - } - ], - "examples": [ - "```typescript\nconst transport = new MockAgentTransport([\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": "AgentConfig", - "kind": "interface", - "description": "Global configuration for agent 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": "AgentTransport", - "description": "Custom transport implementation. Defaults to FetchStreamTransport.", - "optional": true - } - ], - "examples": [] - }, - { - "name": "AgentOptions", - "kind": "interface", - "description": "Options for creating a streaming resource via agent.", - "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": "AgentTransport", - "description": "Custom transport. Defaults to FetchStreamTransport.", - "optional": true - } - ], - "examples": [] - }, - { - "name": "AgentRef", - "kind": "interface", - "description": "Reactive reference returned by agent. 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": "AgentTransport", - "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": "provideAgent", - "kind": "function", - "description": "Angular provider factory that registers global defaults for all\nagent instances in the application.\n\nAdd to your `app.config.ts` or module providers array.", - "signature": "provideAgent(config: AgentConfig): Provider", - "params": [ - { - "name": "config", - "type": "AgentConfig", - "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 provideAgent({ apiUrl: 'http://localhost:2024' }),\n ],\n};\n```" - ] - }, - { - "name": "agent", - "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": "agent(options: AgentOptions>): AgentRef>", - "params": [ - { - "name": "options", - "type": "AgentOptions>", - "description": "Configuration for the streaming resource", - "optional": false - } - ], - "returns": { - "type": "AgentRef>", - "description": "" - }, - "examples": [ - "```typescript\n// In a component field initializer\nconst chat = agent<{ 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 +[] \ No newline at end of file diff --git a/apps/website/content/docs/agent/concepts/angular-signals.mdx b/apps/website/content/docs/agent/concepts/angular-signals.mdx index 25b1b3b1e..17b6285e5 100644 --- a/apps/website/content/docs/agent/concepts/angular-signals.mdx +++ b/apps/website/content/docs/agent/concepts/angular-signals.mdx @@ -1,6 +1,6 @@ # Angular Signals -Angular Signals are the reactive primitive that powers agent(). If you're coming from a Python AI/agent background and wondering how Angular handles real-time streaming data, this page is your guide. Every property on a AgentRef is a Signal, which means your templates update automatically as tokens arrive — no manual subscriptions, no async pipes, no RxJS boilerplate. +Angular Signals are the reactive primitive that powers agent(). If you're coming from a Python AI/agent background and wondering how Angular handles real-time streaming data, this page is your guide. Every property on a LangGraphAgent is a Signal, which means your templates update automatically as tokens arrive — no manual subscriptions, no async pipes, no RxJS boilerplate. Think of Signals like a Python property with built-in change notification. When the value changes, every consumer — templates, computed values, effects — re-evaluates automatically. If you've used Pydantic models with validators that react to field changes, Signals are the Angular equivalent but deeply integrated into the rendering engine. @@ -548,7 +548,7 @@ Signals use referential equality (`===`) by default. agent() creates new array r Understand the Python agent patterns that produce the events Signals consume. - Full reference for every Signal, method, and option on AgentRef. + Full reference for every Signal, method, and option on LangGraphAgent. Build human-in-the-loop approval flows that pause and resume the agent. diff --git a/apps/website/content/docs/chat/api/create-mock-agent-ref.mdx b/apps/website/content/docs/chat/api/mock-langgraph-agent.mdx similarity index 71% rename from apps/website/content/docs/chat/api/create-mock-agent-ref.mdx rename to apps/website/content/docs/chat/api/mock-langgraph-agent.mdx index b61b323c8..82464961e 100644 --- a/apps/website/content/docs/chat/api/create-mock-agent-ref.mdx +++ b/apps/website/content/docs/chat/api/mock-langgraph-agent.mdx @@ -1,24 +1,24 @@ -# createMockAgentRef() +# mockLangGraphAgent() -`createMockAgentRef()` creates a mock `AgentRef` with writable signals for testing chat components. Instead of connecting to a real LangGraph agent, you get an object whose signals you can set directly to simulate any agent state. +`mockLangGraphAgent()` creates a mock `LangGraphAgent` with writable signals for testing chat components. Instead of connecting to a real LangGraph agent, you get an object whose signals you can set directly to simulate any agent state. **Import:** ```typescript -import { createMockAgentRef } from '@ngaf/chat'; +import { mockLangGraphAgent } from '@ngaf/chat'; ``` ## Signature ```typescript -function createMockAgentRef(initial?: { +function mockLangGraphAgent(initial?: { messages?: BaseMessage[]; status?: ResourceStatus; isLoading?: boolean; error?: unknown; hasValue?: boolean; isThreadLoading?: boolean; -}): MockAgentRef +}): MockLangGraphAgent ``` ### Parameters @@ -35,17 +35,17 @@ function createMockAgentRef(initial?: { | `status` | `ResourceStatus` | `ResourceStatus.Idle` | Initial resource status | | `isLoading` | `boolean` | `false` | Initial loading state | | `error` | `unknown` | `null` | Initial error value | -| `hasValue` | `boolean` | `false` | Whether the ref has a resolved value | +| `hasValue` | `boolean` | `false` | Whether the agent has a resolved value | | `isThreadLoading` | `boolean` | `false` | Whether thread data is loading | ### Returns -A `MockAgentRef` object -- an `AgentRef` with all signals replaced by `WritableSignal` instances. +A `MockLangGraphAgent` object — a `LangGraphAgent` with all signals replaced by `WritableSignal` instances. -## MockAgentRef Interface +## MockLangGraphAgent Interface ```typescript -interface MockAgentRef extends AgentRef { +interface MockLangGraphAgent extends LangGraphAgent { messages: WritableSignal; status: WritableSignal; error: WritableSignal; @@ -64,11 +64,11 @@ interface MockAgentRef extends AgentRef { } ``` -Every property on `MockAgentRef` is a `WritableSignal`, so you can call `.set()` to control the state directly in your tests. +Every property on `MockLangGraphAgent` is a `WritableSignal`, so you can call `.set()` to control the state directly in your tests. ## Mock Methods -The mock provides no-op implementations for all `AgentRef` methods: +The mock provides no-op implementations for all `LangGraphAgent` methods: | Method | Behavior | |--------|----------| @@ -85,15 +85,15 @@ The mock provides no-op implementations for all `AgentRef` methods: ```typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { createMockAgentRef, ChatComponent } from '@ngaf/chat'; -import type { MockAgentRef } from '@ngaf/chat'; +import { mockLangGraphAgent, ChatComponent } from '@ngaf/chat'; +import type { MockLangGraphAgent } from '@ngaf/chat'; describe('ChatComponent', () => { let fixture: ComponentFixture; - let mockRef: MockAgentRef; + let mockAgent: MockLangGraphAgent; beforeEach(async () => { - mockRef = createMockAgentRef({ + mockAgent = mockLangGraphAgent({ messages: [], isLoading: false, }); @@ -103,7 +103,7 @@ describe('ChatComponent', () => { }).compileComponents(); fixture = TestBed.createComponent(ChatComponent); - fixture.componentRef.setInput('ref', mockRef); + fixture.componentRef.setInput('ref', mockAgent); fixture.detectChanges(); }); @@ -113,7 +113,7 @@ describe('ChatComponent', () => { }); it('should render messages when set', () => { - mockRef.messages.set([ + mockAgent.messages.set([ { content: 'Hello', _getType: () => 'human' } as any, { content: 'Hi there!', _getType: () => 'ai' } as any, ]); @@ -131,23 +131,23 @@ describe('ChatComponent', () => { ### Loading State ```typescript -const ref = createMockAgentRef({ isLoading: true }); +const agent = mockLangGraphAgent({ isLoading: true }); // Typing indicator will appear, input will be disabled ``` ### Error State ```typescript -const ref = createMockAgentRef(); -ref.error.set(new Error('Connection failed')); +const agent = mockLangGraphAgent(); +agent.error.set(new Error('Connection failed')); // ChatErrorComponent will display "Connection failed" ``` ### Interrupt State ```typescript -const ref = createMockAgentRef(); -ref.interrupt.set({ +const agent = mockLangGraphAgent(); +agent.interrupt.set({ value: 'Please confirm the deletion', resumable: true, ns: [], @@ -158,8 +158,8 @@ ref.interrupt.set({ ### Tool Calls ```typescript -const ref = createMockAgentRef(); -ref.toolCalls.set([ +const agent = mockLangGraphAgent(); +agent.toolCalls.set([ { id: 'call_1', name: 'search_docs', @@ -172,8 +172,8 @@ ref.toolCalls.set([ ### History (for Debug Components) ```typescript -const ref = createMockAgentRef(); -ref.history.set([ +const agent = mockLangGraphAgent(); +agent.history.set([ { values: { messages: [], count: 0 }, next: ['agent'], @@ -189,12 +189,12 @@ ref.history.set([ ## Testing Custom Components -Use `createMockAgentRef()` to test any component that accepts an `AgentRef` input: +Use `mockLangGraphAgent()` to test any component that accepts a `LangGraphAgent` input: ```typescript import { Component, input } from '@angular/core'; -import type { AgentRef } from '@ngaf/langgraph'; -import { createMockAgentRef } from '@ngaf/chat'; +import type { LangGraphAgent } from '@ngaf/langgraph'; +import { mockLangGraphAgent } from '@ngaf/chat'; // Your component @Component({ @@ -202,12 +202,12 @@ import { createMockAgentRef } from '@ngaf/chat'; template: `Messages: {{ ref().messages().length }}`, }) export class MessageCountComponent { - ref = input.required>(); + ref = input.required>(); } // Test it('should display message count', () => { - const mockRef = createMockAgentRef({ + const mockAgent = mockLangGraphAgent({ messages: [ { content: 'One', _getType: () => 'human' } as any, { content: 'Two', _getType: () => 'ai' } as any, @@ -215,7 +215,7 @@ it('should display message count', () => { }); const fixture = TestBed.createComponent(MessageCountComponent); - fixture.componentRef.setInput('ref', mockRef); + fixture.componentRef.setInput('ref', mockAgent); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('Messages: 2'); @@ -227,9 +227,9 @@ it('should display message count', () => { Since the mock methods are plain functions, you can replace them with spies: ```typescript -const ref = createMockAgentRef(); +const agent = mockLangGraphAgent(); const submitSpy = jest.fn().mockResolvedValue(undefined); -ref.submit = submitSpy; +agent.submit = submitSpy; // After triggering a submit in the component: expect(submitSpy).toHaveBeenCalledWith({ diff --git a/apps/website/content/docs/chat/components/chat-debug.mdx b/apps/website/content/docs/chat/components/chat-debug.mdx index 065252e76..7133ee0d7 100644 --- a/apps/website/content/docs/chat/components/chat-debug.mdx +++ b/apps/website/content/docs/chat/components/chat-debug.mdx @@ -52,7 +52,7 @@ export class DebugPageComponent { | Input | Type | Default | Description | |-------|------|---------|-------------| -| `ref` | `AgentRef` | **Required** | The agent ref providing streaming state and execution history | +| `ref` | `LangGraphAgent` | **Required** | The agent providing streaming state and execution history | ## Layout diff --git a/apps/website/content/docs/chat/components/chat-input.mdx b/apps/website/content/docs/chat/components/chat-input.mdx index 1059a73d8..896d65f57 100644 --- a/apps/website/content/docs/chat/components/chat-input.mdx +++ b/apps/website/content/docs/chat/components/chat-input.mdx @@ -27,7 +27,7 @@ import { ChatInputComponent, submitMessage } from '@ngaf/chat'; | Input | Type | Default | Description | |-------|------|---------|-------------| -| `ref` | `AgentRef` | **Required** | The agent ref to submit messages to | +| `ref` | `LangGraphAgent` | **Required** | The agent to submit messages to | | `submitOnEnter` | `boolean` | `true` | When `true`, pressing Enter submits the message. Shift+Enter inserts a newline. When `false`, Enter always inserts a newline. | | `placeholder` | `string` | `''` | Placeholder text shown when the input is empty | @@ -72,7 +72,7 @@ The `submitMessage()` function is exported as a standalone utility for programma ```typescript import { submitMessage } from '@ngaf/chat'; -function sendGreeting(ref: AgentRef) { +function sendGreeting(ref: LangGraphAgent) { const result = submitMessage(ref, 'Hello!'); // result is 'Hello!' or null if the text was empty } @@ -82,14 +82,14 @@ function sendGreeting(ref: AgentRef) { ```typescript function submitMessage( - ref: AgentRef, + ref: LangGraphAgent, text: string ): string | null ``` | Parameter | Type | Description | |-----------|------|-------------| -| `ref` | `AgentRef` | The agent ref to submit to | +| `ref` | `LangGraphAgent` | The agent to submit to | | `text` | `string` | The message text to send | **Returns:** The trimmed message string if submitted, or `null` if the trimmed text was empty. diff --git a/apps/website/content/docs/chat/components/chat-interrupt-panel.mdx b/apps/website/content/docs/chat/components/chat-interrupt-panel.mdx index 32ccec9d8..81d4428d1 100644 --- a/apps/website/content/docs/chat/components/chat-interrupt-panel.mdx +++ b/apps/website/content/docs/chat/components/chat-interrupt-panel.mdx @@ -13,7 +13,7 @@ import type { InterruptAction } from '@ngaf/chat'; ## How It Works -LangGraph agents can pause execution using interrupts -- checkpoints where the agent waits for human input before proceeding. The `ChatInterruptPanelComponent` reads the interrupt state from an `AgentRef` and renders a warning card with action buttons. +LangGraph agents can pause execution using interrupts -- checkpoints where the agent waits for human input before proceeding. The `ChatInterruptPanelComponent` reads the interrupt state from a `LangGraphAgent` and renders a warning card with action buttons. When no interrupt is active, the component renders nothing. @@ -53,7 +53,7 @@ handleInterrupt(action: InterruptAction) { | Input | Type | Default | Description | |-------|------|---------|-------------| -| `ref` | `AgentRef` | **Required** | The agent ref providing interrupt state | +| `ref` | `LangGraphAgent` | **Required** | The agent providing interrupt state | ### Outputs diff --git a/apps/website/content/docs/chat/components/chat-messages.mdx b/apps/website/content/docs/chat/components/chat-messages.mdx index 75f0910a3..47ef3e3f5 100644 --- a/apps/website/content/docs/chat/components/chat-messages.mdx +++ b/apps/website/content/docs/chat/components/chat-messages.mdx @@ -1,6 +1,6 @@ # ChatMessagesComponent -`ChatMessagesComponent` is the core primitive for rendering chat messages. It iterates over the messages from an `AgentRef` and renders each one using a matching `MessageTemplateDirective`. This gives you full control over how each message type is displayed. +`ChatMessagesComponent` is the core primitive for rendering chat messages. It iterates over the messages from a `LangGraphAgent` and renders each one using a matching `MessageTemplateDirective`. This gives you full control over how each message type is displayed. **Selector:** `chat-messages` @@ -49,7 +49,7 @@ import { | Input | Type | Default | Description | |-------|------|---------|-------------| -| `ref` | `AgentRef` | **Required** | The agent ref providing streaming state | +| `ref` | `LangGraphAgent` | **Required** | The agent providing streaming state | ### Content Children diff --git a/apps/website/content/docs/chat/components/chat-subagent-card.mdx b/apps/website/content/docs/chat/components/chat-subagent-card.mdx index 98d09fa30..d06171e80 100644 --- a/apps/website/content/docs/chat/components/chat-subagent-card.mdx +++ b/apps/website/content/docs/chat/components/chat-subagent-card.mdx @@ -63,7 +63,7 @@ The status badge uses different chat theme variables based on the current status ## Using with ChatSubagentsComponent -The `ChatSubagentsComponent` primitive iterates over active subagent streams from an `AgentRef`. Combine it with `ChatSubagentCardComponent`: +The `ChatSubagentsComponent` primitive iterates over active subagent streams from a `LangGraphAgent`. Combine it with `ChatSubagentCardComponent`: ```html diff --git a/apps/website/content/docs/chat/components/chat-tool-call-card.mdx b/apps/website/content/docs/chat/components/chat-tool-call-card.mdx index 3dcf1993c..1591a5323 100644 --- a/apps/website/content/docs/chat/components/chat-tool-call-card.mdx +++ b/apps/website/content/docs/chat/components/chat-tool-call-card.mdx @@ -77,7 +77,7 @@ The component uses a `formatJson()` method that: ## Using with ChatToolCallsComponent -The `ChatToolCallsComponent` primitive iterates over tool calls from an `AgentRef`. Combine it with `ChatToolCallCardComponent` to render a list of tool call cards: +The `ChatToolCallsComponent` primitive iterates over tool calls from a `LangGraphAgent`. Combine it with `ChatToolCallCardComponent` to render a list of tool call cards: ```html diff --git a/apps/website/content/docs/chat/components/chat.mdx b/apps/website/content/docs/chat/components/chat.mdx index 3d8f57215..6bd99bb82 100644 --- a/apps/website/content/docs/chat/components/chat.mdx +++ b/apps/website/content/docs/chat/components/chat.mdx @@ -61,7 +61,7 @@ export class ChatPageComponent { | Input | Type | Default | Description | |-------|------|---------|-------------| -| `ref` | `AgentRef` | **Required** | The agent ref providing streaming state. Created by `agent()` from `@ngaf/langgraph`. | +| `ref` | `LangGraphAgent` | **Required** | The agent providing streaming state. Created by `agent()` from `@ngaf/langgraph`. | | `views` | `ViewRegistry \| undefined` | `undefined` | View registry for generative UI. Maps spec type names to Angular components. Created with `views()` from `@ngaf/chat`. | | `store` | `StateStore \| undefined` | `undefined` | Optional state store for interactive generative UI specs. | | `handlers` | `Record) => unknown \| Promise>` | `{}` | Event handlers for generative UI specs and A2UI `functionCall` actions. Handlers run in Angular injection context — `inject()` is available inside handler functions. | diff --git a/apps/website/content/docs/chat/getting-started/installation.mdx b/apps/website/content/docs/chat/getting-started/installation.mdx index c1791614b..a4fe8d4a1 100644 --- a/apps/website/content/docs/chat/getting-started/installation.mdx +++ b/apps/website/content/docs/chat/getting-started/installation.mdx @@ -9,7 +9,7 @@ Detailed setup guide for `@ngaf/chat` in your Angular application. `@ngaf/chat` uses Angular Signals, the `input()` function, and `contentChildren()`. Angular 20 or later is required. -The chat components read streaming state from `AgentRef`, which is provided by `@ngaf/agent`. You must have `provideAgent()` configured before adding chat. +The chat components read streaming state from `LangGraphAgent`, which is returned by `agent()` from `@ngaf/langgraph`. You must have `provideAgent()` configured before adding chat. Required for the build toolchain and package installation. diff --git a/apps/website/content/docs/chat/getting-started/introduction.mdx b/apps/website/content/docs/chat/getting-started/introduction.mdx index 857e91511..632f32691 100644 --- a/apps/website/content/docs/chat/getting-started/introduction.mdx +++ b/apps/website/content/docs/chat/getting-started/introduction.mdx @@ -12,7 +12,7 @@ The library is organized into two layers: **primitives** and **compositions**. ### Primitives -Primitives are low-level, headless components that read from an `AgentRef` and expose their state through content projection (`ng-template`). They carry no opinion about layout or styling. Use them when you need full control over how chat elements render. +Primitives are low-level, headless components that read from a `LangGraphAgent` and expose their state through content projection (`ng-template`). They carry no opinion about layout or styling. Use them when you need full control over how chat elements render. | Primitive | Selector | Purpose | |-----------|----------|---------| @@ -58,13 +58,13 @@ Your App LangGraph Platform ``` -- **`@ngaf/agent`** provides the `agent()` function and the `AgentRef` type. Every chat component accepts an `AgentRef` as its primary input. The ref exposes reactive Signals for `messages()`, `isLoading()`, `error()`, `interrupt()`, `toolCalls()`, `history()`, and more. +- **`@ngaf/agent`** provides the `agent()` function and the `LangGraphAgent` type. Every chat component accepts a `LangGraphAgent` as its primary input. It exposes reactive Signals for `messages()`, `isLoading()`, `error()`, `interrupt()`, `toolCalls()`, `history()`, and more. - **`@ngaf/render`** provides `RenderSpecComponent` and view registries for rendering JSON UI specs as Angular components. The `ChatComponent` auto-detects JSON specs in AI messages and renders them through `@ngaf/render` — pass a view registry via the `[views]` input. The `ChatComponent` also auto-detects A2UI v0.9 payloads and renders them using a built-in 12-component catalog. ## When to Use `ChatComponent` vs. Custom Assembly -**Use `ChatComponent`** when you want a complete, styled chat interface with minimal setup. It includes message rendering (with markdown), a text input, typing indicator, error display, interrupt banner, and an optional thread sidebar. Drop it in, pass an `AgentRef`, and you have a working chat. +**Use `ChatComponent`** when you want a complete, styled chat interface with minimal setup. It includes message rendering (with markdown), a text input, typing indicator, error display, interrupt banner, and an optional thread sidebar. Drop it in, pass a `LangGraphAgent`, and you have a working chat. ```html diff --git a/apps/website/content/docs/chat/getting-started/quickstart.mdx b/apps/website/content/docs/chat/getting-started/quickstart.mdx index c6bbf2a61..ce0a2738e 100644 --- a/apps/website/content/docs/chat/getting-started/quickstart.mdx +++ b/apps/website/content/docs/chat/getting-started/quickstart.mdx @@ -40,7 +40,7 @@ export const appConfig: ApplicationConfig = { -Use `agent()` to create a streaming connection and pass the returned `AgentRef` to `ChatComponent`. +Use `agent()` to create a streaming connection and pass the returned `LangGraphAgent` to `ChatComponent`. ```typescript // chat-page.component.ts diff --git a/apps/website/content/docs/render/a2ui/overview.mdx b/apps/website/content/docs/render/a2ui/overview.mdx index 038c89c19..1d5d0dac1 100644 --- a/apps/website/content/docs/render/a2ui/overview.mdx +++ b/apps/website/content/docs/render/a2ui/overview.mdx @@ -115,7 +115,7 @@ import { ChatComponent, a2uiBasicCatalog } from '@ngaf/chat'; }) export class AppComponent { catalog = a2uiBasicCatalog(); - agentRef = /* your AgentRef */; + agentRef = agent({ /* your agent options */ }); } ``` diff --git a/apps/website/src/app/llms.txt/route.ts b/apps/website/src/app/llms.txt/route.ts index a0f2a63bf..60043f665 100644 --- a/apps/website/src/app/llms.txt/route.ts +++ b/apps/website/src/app/llms.txt/route.ts @@ -24,19 +24,19 @@ function buildLlmsTxt(): string { '# AG-UI backend:', 'npm install @ngaf/chat @ngaf/ag-ui', '', - '## Key API (chat library)', - '- Agent — runtime-neutral contract with messages/status/isLoading/error/toolCalls/state signals + events$ observable + submit/stop methods', - '- ChatComponent, ChatMessagesComponent, ChatInputComponent — composable Angular components consuming Agent', - '- mockAgent — testing utility with a writable signal-backed Agent', + '## Key API', + '- LangGraphAgent — unified type returned by agent(); exposes messages/status/isLoading/error/toolCalls/history signals + submit/stop methods', + '- agent({ apiUrl, assistantId }) — single call that creates and returns a LangGraphAgent; no toAgent() step needed', + '- ChatComponent, ChatMessagesComponent, ChatInputComponent — composable Angular components consuming LangGraphAgent', + '- mockLangGraphAgent — testing utility with a writable signal-backed LangGraphAgent', '- runAgentConformance / runAgentWithHistoryConformance — conformance suites for adapter authors', '', '## Minimal LangGraph example', - "import { agent, toAgent } from '@ngaf/langgraph';", + "import { agent } from '@ngaf/langgraph';", "import { ChatComponent } from '@ngaf/chat';", '// In a component:', - "stream = agent({ apiUrl: 'http://localhost:2024', assistantId: 'chat_agent' });", - 'chatAgent = toAgent(this.stream);', - '// Template: ', + "chat = agent({ apiUrl: 'http://localhost:2024', assistantId: 'chat_agent' });", + '// Template: ', '', '## Minimal AG-UI example', "import { provideAgUiAgent, AG_UI_AGENT } from '@ngaf/ag-ui';", diff --git a/apps/website/src/components/landing/chat-landing/ChatLandingCodeShowcase.tsx b/apps/website/src/components/landing/chat-landing/ChatLandingCodeShowcase.tsx index 48676bac5..a8e063dbc 100644 --- a/apps/website/src/components/landing/chat-landing/ChatLandingCodeShowcase.tsx +++ b/apps/website/src/components/landing/chat-landing/ChatLandingCodeShowcase.tsx @@ -1,7 +1,8 @@ import { tokens } from '@ngaf/design-tokens'; import { HighlightedCode } from '../HighlightedCode'; -const SNIPPET_1 = `import { ChatComponent } from '@ngaf/chat'; +const SNIPPET_1 = `import { agent } from '@ngaf/langgraph'; +import { ChatComponent } from '@ngaf/chat'; @Component({ template: \` @@ -12,7 +13,7 @@ const SNIPPET_1 = `import { ChatComponent } from '@ngaf/chat'; \`, }) export class MyChatPage { - agent = inject(AgentRef); + protected readonly agent = agent({ apiUrl: 'http://localhost:2024', assistantId: 'chat_agent' }); registry = inject(RenderRegistry); }`; diff --git a/apps/website/src/lib/docs-config.ts b/apps/website/src/lib/docs-config.ts index 7e52925c0..7ba12a0c5 100644 --- a/apps/website/src/lib/docs-config.ts +++ b/apps/website/src/lib/docs-config.ts @@ -173,7 +173,7 @@ export const docsConfig: DocsLibrary[] = [ pages: [ { title: 'provideChat()', slug: 'provide-chat', section: 'api' }, { title: 'ChatConfig', slug: 'chat-config', section: 'api' }, - { title: 'createMockAgentRef()', slug: 'create-mock-agent-ref', section: 'api' }, + { title: 'mockLangGraphAgent()', slug: 'mock-langgraph-agent', section: 'api' }, { title: 'createContentClassifier()', slug: 'content-classifier', section: 'api' }, { title: 'createParseTreeStore()', slug: 'parse-tree-store', section: 'api' }, ], diff --git a/cockpit/ag-ui/streaming/angular/prompts/streaming.md b/cockpit/ag-ui/streaming/angular/prompts/streaming.md index ea995101f..d8a931707 100644 --- a/cockpit/ag-ui/streaming/angular/prompts/streaming.md +++ b/cockpit/ag-ui/streaming/angular/prompts/streaming.md @@ -4,4 +4,4 @@ This capability demonstrates real-time token streaming from an AG-UI compatible Key components used: ``, ``, ``, ``. The `provideAgUiAgent` provider handles SSE event processing from the AG-UI streaming endpoint, and the chat components subscribe reactively without any manual subscription management. -The demo illustrates the chat-runtime decoupling: the same `` composition works with any agent runtime — LangGraph, AG-UI, or others — by conforming to the `AgentRef` interface. +The demo illustrates the chat-runtime decoupling: the same `` composition works with any agent runtime — LangGraph, AG-UI, or others — by conforming to the runtime-neutral `Agent` contract from `@ngaf/chat`. diff --git a/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts b/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts index 14b3e8dc6..1bef15d66 100644 --- a/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts +++ b/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts @@ -1,20 +1,19 @@ // SPDX-License-Identifier: MIT import { Component } from '@angular/core'; import { ChatComponent, a2uiBasicCatalog } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; @Component({ selector: 'app-a2ui', standalone: true, imports: [ChatComponent], - template: ``, + template: ``, }) export class A2uiComponent { - protected readonly agentRef = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.a2uiAssistantId, }); - protected readonly chatAgent = toAgent(this.agentRef); protected readonly catalog = a2uiBasicCatalog(); } diff --git a/cockpit/chat/debug/angular/src/app/debug.component.ts b/cockpit/chat/debug/angular/src/app/debug.component.ts index ca5fbcb7b..146b582fe 100644 --- a/cockpit/chat/debug/angular/src/app/debug.component.ts +++ b/cockpit/chat/debug/angular/src/app/debug.component.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT import { Component } from '@angular/core'; import { ChatDebugComponent } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { environment } from '../environments/environment'; @@ -16,14 +16,13 @@ import { environment } from '../environments/environment'; imports: [ChatDebugComponent, ExampleChatLayoutComponent], template: ` - + `, }) export class DebugPageComponent { - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); } diff --git a/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts b/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts index ff2da2194..ff7485f59 100644 --- a/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts +++ b/cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT import { Component } from '@angular/core'; import { ChatComponent, views } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { environment } from '../environments/environment'; @@ -27,15 +27,14 @@ const dashboardViews = views({ imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - + `, }) export class GenerativeUiComponent { - protected readonly agentRef = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.generativeUiAssistantId, }); - protected readonly chatAgent = toAgent(this.agentRef); protected readonly dashboardViews = dashboardViews; } diff --git a/cockpit/chat/input/angular/src/app/input.component.ts b/cockpit/chat/input/angular/src/app/input.component.ts index 57d139322..97f4eebbc 100644 --- a/cockpit/chat/input/angular/src/app/input.component.ts +++ b/cockpit/chat/input/angular/src/app/input.component.ts @@ -3,7 +3,7 @@ import { Component, computed } from '@angular/core'; import { ChatInputComponent as ChatInputPrimitive } from '@ngaf/chat'; import { ChatMessagesComponent } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; /** @@ -22,10 +22,10 @@ import { environment } from '../environments/environment';

Chat Input Demo

- +
- +
@@ -52,16 +52,15 @@ import { environment } from '../environments/environment'; `, }) export class InputComponent { - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); - protected readonly streamStatus = computed(() => this.stream.status()); - protected readonly isLoading = computed(() => this.stream.isLoading()); + protected readonly streamStatus = computed(() => this.agent.status()); + protected readonly isLoading = computed(() => this.agent.isLoading()); submitMessage(content: string) { - this.chatAgent.submit({ message: content }); + this.agent.submit({ message: content }); } } diff --git a/cockpit/chat/interrupts/angular/src/app/interrupts.component.ts b/cockpit/chat/interrupts/angular/src/app/interrupts.component.ts index e5e31012b..a06702046 100644 --- a/cockpit/chat/interrupts/angular/src/app/interrupts.component.ts +++ b/cockpit/chat/interrupts/angular/src/app/interrupts.component.ts @@ -3,7 +3,7 @@ import { Component, computed } from '@angular/core'; import { JsonPipe } from '@angular/common'; import { ChatComponent, ChatInterruptPanelComponent } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; /** @@ -18,11 +18,11 @@ import { environment } from '../environments/environment'; imports: [ChatComponent, ChatInterruptPanelComponent, JsonPipe, ExampleChatLayoutComponent], template: ` - +

Interrupt Panel

- +

Stream Status

@@ -33,11 +33,10 @@ import { environment } from '../environments/environment'; `, }) export class InterruptsComponent { - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); - protected readonly streamStatus = computed(() => this.stream.status()); + protected readonly streamStatus = computed(() => this.agent.status()); } diff --git a/cockpit/chat/messages/angular/src/app/messages.component.ts b/cockpit/chat/messages/angular/src/app/messages.component.ts index 50911d9ae..deadc9797 100644 --- a/cockpit/chat/messages/angular/src/app/messages.component.ts +++ b/cockpit/chat/messages/angular/src/app/messages.component.ts @@ -6,7 +6,7 @@ import { ChatTypingIndicatorComponent, } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; /** @@ -27,11 +27,11 @@ import { environment } from '../environments/environment';

Chat Messages Primitives

- +
- - + +
@@ -47,13 +47,12 @@ import { environment } from '../environments/environment'; `, }) export class MessagesComponent { - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); submitMessage(content: string) { - this.chatAgent.submit({ message: content }); + this.agent.submit({ message: content }); } } diff --git a/cockpit/chat/subagents/angular/src/app/subagents.component.ts b/cockpit/chat/subagents/angular/src/app/subagents.component.ts index a99fbc827..25099aac8 100644 --- a/cockpit/chat/subagents/angular/src/app/subagents.component.ts +++ b/cockpit/chat/subagents/angular/src/app/subagents.component.ts @@ -6,7 +6,7 @@ import { ChatSubagentCardComponent, } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; /** @@ -20,11 +20,11 @@ import { environment } from '../environments/environment'; imports: [ChatComponent, ChatSubagentsComponent, ChatSubagentCardComponent, ExampleChatLayoutComponent], template: ` - +

Active Subagents

- +

Agent Pipeline

@@ -40,9 +40,8 @@ import { environment } from '../environments/environment'; `, }) export class SubagentsComponent { - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); } diff --git a/cockpit/chat/theming/angular/src/app/theming.component.ts b/cockpit/chat/theming/angular/src/app/theming.component.ts index 4f1f0d4ab..186c7f7c5 100644 --- a/cockpit/chat/theming/angular/src/app/theming.component.ts +++ b/cockpit/chat/theming/angular/src/app/theming.component.ts @@ -3,7 +3,7 @@ import { Component, signal } from '@angular/core'; import { TitleCasePipe } from '@angular/common'; import { ChatComponent } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; const THEMES: Record> = { @@ -52,7 +52,7 @@ const THEMES: Record> = { imports: [ChatComponent, ExampleChatLayoutComponent, TitleCasePipe], template: ` - +

Theme Picker

@@ -84,11 +84,10 @@ const THEMES: Record> = { `, }) export class ThemingComponent { - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); protected readonly themeNames = Object.keys(THEMES); protected readonly activeTheme = signal('dark'); diff --git a/cockpit/chat/threads/angular/src/app/threads.component.ts b/cockpit/chat/threads/angular/src/app/threads.component.ts index 332b3b8c9..ba5dc2577 100644 --- a/cockpit/chat/threads/angular/src/app/threads.component.ts +++ b/cockpit/chat/threads/angular/src/app/threads.component.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT import { Component, signal } from '@angular/core'; import { ChatComponent, ChatThreadListComponent, type Thread } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { environment } from '../environments/environment'; @@ -15,7 +15,7 @@ import { environment } from '../environments/environment'; imports: [ChatComponent, ChatThreadListComponent, ExampleChatLayoutComponent], template: ` - +

([ { id: 'thread-1', title: 'First Conversation' }, diff --git a/cockpit/chat/timeline/angular/src/app/timeline.component.ts b/cockpit/chat/timeline/angular/src/app/timeline.component.ts index 9d55bb4c0..01420df83 100644 --- a/cockpit/chat/timeline/angular/src/app/timeline.component.ts +++ b/cockpit/chat/timeline/angular/src/app/timeline.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core'; import { ChatComponent, ChatTimelineSliderComponent } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; /** @@ -16,11 +16,11 @@ import { environment } from '../environments/environment'; imports: [ChatComponent, ChatTimelineSliderComponent, ExampleChatLayoutComponent], template: ` - +

Timeline

- +

How It Works

@@ -34,9 +34,8 @@ import { environment } from '../environments/environment'; `, }) export class TimelineComponent { - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); } diff --git a/cockpit/chat/tool-calls/angular/src/app/tool-calls.component.ts b/cockpit/chat/tool-calls/angular/src/app/tool-calls.component.ts index ab1d6e9b4..41cefe5f6 100644 --- a/cockpit/chat/tool-calls/angular/src/app/tool-calls.component.ts +++ b/cockpit/chat/tool-calls/angular/src/app/tool-calls.component.ts @@ -6,7 +6,7 @@ import { ChatToolCallCardComponent, } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; /** @@ -19,11 +19,11 @@ import { environment } from '../environments/environment'; imports: [ChatComponent, ChatToolCallsComponent, ChatToolCallCardComponent, ExampleChatLayoutComponent], template: ` - +

Tool Calls

- +

Available Tools

@@ -38,9 +38,8 @@ import { environment } from '../environments/environment'; `, }) export class ToolCallsComponent { - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); } diff --git a/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts b/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts index 161731f29..0b9aea1da 100644 --- a/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts +++ b/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts @@ -1,7 +1,7 @@ import { Component, computed } from '@angular/core'; import { ChatComponent, views } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { signalStateStore } from '@ngaf/render'; import { environment } from '../environments/environment'; import { FilePreviewComponent } from './views/file-preview.component'; @@ -33,7 +33,7 @@ interface FileOperation { imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - +

File Operations

@@ -70,11 +70,10 @@ export class FilesystemComponent { * The graph uses `read_file` and `write_file` tool calls that appear * in `stream.messages()`. We filter and display them in the sidebar. */ - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); /** * Reactive list of file operations derived from the message history. @@ -83,7 +82,7 @@ export class FilesystemComponent { * extracts the file path and a short result preview for sidebar display. */ protected readonly fileOps = computed(() => { - const msgs = this.stream.messages(); + const msgs = this.agent.langGraphMessages(); const ops: FileOperation[] = []; for (const msg of msgs) { const m = msg as unknown as Record; diff --git a/cockpit/deep-agents/memory/angular/src/app/memory.component.ts b/cockpit/deep-agents/memory/angular/src/app/memory.component.ts index 83b91c0ab..ed72c83f0 100644 --- a/cockpit/deep-agents/memory/angular/src/app/memory.component.ts +++ b/cockpit/deep-agents/memory/angular/src/app/memory.component.ts @@ -1,7 +1,7 @@ import { Component, computed } from '@angular/core'; import { ChatComponent } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; /** @@ -22,7 +22,7 @@ import { environment } from '../environments/environment'; imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - +

@@ -51,11 +51,10 @@ export class MemoryComponent { * The graph returns an `agent_memory` (or `memory`) dict alongside messages * in its state. We derive a reactive signal from `stream.value()` for display. */ - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); /** * Reactive list of [key, value] memory entries derived from the graph state. @@ -64,7 +63,7 @@ export class MemoryComponent { * This signal re-computes whenever the stream state changes. */ protected readonly memoryEntries = computed(() => { - const val = this.stream.value() as Record; + const val = this.agent.value() as Record; const mem = val?.['agent_memory'] ?? val?.['memory']; if (!mem || typeof mem !== 'object') return []; return Object.entries(mem as Record); diff --git a/cockpit/deep-agents/planning/angular/src/app/planning.component.ts b/cockpit/deep-agents/planning/angular/src/app/planning.component.ts index 156279ac8..22c3d33aa 100644 --- a/cockpit/deep-agents/planning/angular/src/app/planning.component.ts +++ b/cockpit/deep-agents/planning/angular/src/app/planning.component.ts @@ -1,7 +1,7 @@ import { Component, computed } from '@angular/core'; import { ChatComponent, views } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { signalStateStore } from '@ngaf/render'; import { environment } from '../environments/environment'; import { PlanChecklistComponent } from './views/plan-checklist.component'; @@ -34,7 +34,7 @@ interface PlanStep { imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - +

Plan

@@ -70,11 +70,10 @@ export class PlanningComponent { * The graph returns a `plan` array alongside messages in its state. * Each plan entry has a `title` and `status` that drive the sidebar checklist. */ - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); /** * Reactive list of plan steps derived from the graph state. @@ -84,7 +83,7 @@ export class PlanningComponent { * the stream state changes. */ protected readonly planSteps = computed(() => { - const val = this.stream.value() as Record; + const val = this.agent.value() as Record; const plan = val?.['plan']; if (!Array.isArray(plan)) return []; return plan.map((step: Record) => ({ diff --git a/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts b/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts index 442dd11c8..cf4ed4198 100644 --- a/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts +++ b/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts @@ -1,7 +1,7 @@ import { Component, computed } from '@angular/core'; import { ChatComponent, views } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { signalStateStore } from '@ngaf/render'; import { environment } from '../environments/environment'; import { CodeExecutionComponent } from './views/code-execution.component'; @@ -31,7 +31,7 @@ interface CodeExecution { imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - +

Execution Output

@@ -64,11 +64,10 @@ export class SandboxesComponent { readonly ui = views({ 'code-execution': CodeExecutionComponent }); readonly uiStore = signalStateStore({}); - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); /** * Derived signal: extracts code executions from the message stream. @@ -78,7 +77,7 @@ export class SandboxesComponent { * JSON with {stdout, stderr, exit_status} fields. */ protected readonly executions = computed(() => { - const msgs = this.stream.messages(); + const msgs = this.agent.langGraphMessages(); const resultMap = new Map(); // Build a lookup of tool_call_id -> parsed result from tool messages diff --git a/cockpit/deep-agents/skills/angular/src/app/skills.component.ts b/cockpit/deep-agents/skills/angular/src/app/skills.component.ts index e2a8d98b5..8f4885677 100644 --- a/cockpit/deep-agents/skills/angular/src/app/skills.component.ts +++ b/cockpit/deep-agents/skills/angular/src/app/skills.component.ts @@ -1,7 +1,7 @@ import { Component, computed } from '@angular/core'; import { ChatComponent, views } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { signalStateStore } from '@ngaf/render'; import { environment } from '../environments/environment'; import { CalculatorResultComponent } from './views/calculator-result.component'; @@ -33,7 +33,7 @@ interface SkillInvocation { imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - +

Skill Invocations

@@ -71,11 +71,10 @@ export class SkillsComponent { readonly ui = views({ 'calculator-result': CalculatorResultComponent, 'word-count-result': WordCountResultComponent }); readonly uiStore = signalStateStore({}); - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); private readonly SKILL_NAMES = new Set(['calculator', 'word_count', 'summarize']); @@ -86,7 +85,7 @@ export class SkillsComponent { * each with its corresponding tool result message via tool_call_id. */ protected readonly invocations = computed(() => { - const msgs = this.stream.messages(); + const msgs = this.agent.langGraphMessages(); const resultMap = new Map(); // Build a lookup of tool_call_id -> result content from tool messages diff --git a/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts b/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts index 60d75a5e2..5a42be76c 100644 --- a/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts +++ b/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts @@ -1,7 +1,7 @@ import { Component, computed } from '@angular/core'; import { ChatComponent } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; /** @@ -31,7 +31,7 @@ interface Delegation { imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - +

Delegations

@@ -55,11 +55,10 @@ export class SubagentsComponent { /** * The streaming resource connected to the subagents orchestrator graph. */ - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); /** * Reactive delegation list derived from messages. @@ -68,7 +67,7 @@ export class SubagentsComponent { * ToolMessage results (by tool_call_id) to determine completion status. */ protected readonly delegations = computed(() => { - const msgs = this.stream.messages(); + const msgs = this.agent.langGraphMessages(); const toolResultIds = new Set(); const errorResultIds = new Set(); diff --git a/cockpit/langgraph/deployment-runtime/angular/src/app/deployment-runtime.component.ts b/cockpit/langgraph/deployment-runtime/angular/src/app/deployment-runtime.component.ts index f52f48c36..bb0b4f07a 100644 --- a/cockpit/langgraph/deployment-runtime/angular/src/app/deployment-runtime.component.ts +++ b/cockpit/langgraph/deployment-runtime/angular/src/app/deployment-runtime.component.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT import { Component } from '@angular/core'; import { ChatComponent } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { environment } from '../environments/environment'; @@ -17,14 +17,13 @@ import { environment } from '../environments/environment'; imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - + `, }) export class DeploymentRuntimeComponent { - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.deploymentRuntimeAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); } diff --git a/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts b/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts index f0cc01c42..394994a06 100644 --- a/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts +++ b/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts @@ -1,6 +1,6 @@ import { Component, computed } from '@angular/core'; import { ChatComponent, views } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { signalStateStore } from '@ngaf/render'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { environment } from '../environments/environment'; @@ -40,7 +40,7 @@ const STEP_LABELS: Record = { imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - +

(() => { - const val = this.stream.value() as Record | undefined; + const val = this.agent.value() as Record | undefined; const currentStep = (val?.['step'] as string) ?? ''; const activeIndex = STEP_ORDER.indexOf(currentStep as (typeof STEP_ORDER)[number]); diff --git a/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts b/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts index 148dd7499..1e943b9d3 100644 --- a/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts +++ b/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT import { Component } from '@angular/core'; import { ChatComponent, ChatInterruptPanelComponent, views, type InterruptAction } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { signalStateStore } from '@ngaf/render'; import { environment } from '../environments/environment'; @@ -26,10 +26,10 @@ import { ApprovalCardComponent } from './views/approval-card.component'; template: `
- - @if (chatAgent.interrupt()) { + + @if (agent.interrupt()) {
- +
}
@@ -46,11 +46,10 @@ export class InterruptsComponent { * When the LangGraph backend calls `interrupt()`, the `stream.interrupt()` * signal emits the interrupt payload for display via ChatInterruptPanelComponent. */ - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); /** * Handle an interrupt action from the panel. @@ -66,6 +65,6 @@ export class InterruptsComponent { // In a production app, 'edit' would let the user modify the response before approval. // For this demo, all actions simply resume the graph. void action; // Each branch intentionally does the same thing in this demo - void this.chatAgent.submit({ resume: null }); + void this.agent.submit({ resume: null }); } } diff --git a/cockpit/langgraph/memory/angular/src/app/memory.component.ts b/cockpit/langgraph/memory/angular/src/app/memory.component.ts index 9458d0a69..4d0fcb791 100644 --- a/cockpit/langgraph/memory/angular/src/app/memory.component.ts +++ b/cockpit/langgraph/memory/angular/src/app/memory.component.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT import { Component, computed } from '@angular/core'; import { ChatComponent } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { environment } from '../environments/environment'; @@ -23,7 +23,7 @@ import { environment } from '../environments/environment'; imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - +

{ - const val = this.stream.value() as Record; + const val = this.agent.value() as Record; const mem = val?.['memory']; if (!mem || typeof mem !== 'object') return []; return Object.entries(mem as Record); diff --git a/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts b/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts index 9e4b9ca0a..0d98c1312 100644 --- a/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts +++ b/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts @@ -1,6 +1,6 @@ import { Component, signal } from '@angular/core'; import { ChatComponent } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { environment } from '../environments/environment'; @@ -26,7 +26,7 @@ interface Thread { imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - +
{ @@ -98,17 +98,16 @@ export class PersistenceComponent { } }, }); - protected readonly chatAgent = toAgent(this.stream); /** Switch to an existing thread by ID. */ switchThread(id: string): void { this.activeThreadId.set(id); - this.stream.switchThread(id); + this.agent.switchThread(id); } /** Start a brand-new thread. */ newThread(): void { this.activeThreadId.set(null); - this.stream.switchThread(null); + this.agent.switchThread(null); } } diff --git a/cockpit/langgraph/streaming/angular/src/app/streaming.component.ts b/cockpit/langgraph/streaming/angular/src/app/streaming.component.ts index b92bc99a4..0141a9e80 100644 --- a/cockpit/langgraph/streaming/angular/src/app/streaming.component.ts +++ b/cockpit/langgraph/streaming/angular/src/app/streaming.component.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT import { Component } from '@angular/core'; import { ChatComponent } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { environment } from '../environments/environment'; @@ -18,14 +18,13 @@ import { environment } from '../environments/environment'; imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - + `, }) export class StreamingComponent { - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); } diff --git a/cockpit/langgraph/subgraphs/angular/src/app/subgraphs.component.ts b/cockpit/langgraph/subgraphs/angular/src/app/subgraphs.component.ts index 7698443d4..c1b9843ee 100644 --- a/cockpit/langgraph/subgraphs/angular/src/app/subgraphs.component.ts +++ b/cockpit/langgraph/subgraphs/angular/src/app/subgraphs.component.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT import { Component, computed } from '@angular/core'; import { ChatComponent } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { environment } from '../environments/environment'; @@ -23,7 +23,7 @@ import { environment } from '../environments/environment'; imports: [ChatComponent, ExampleChatLayoutComponent], template: ` - +

> that updates * as the parent orchestrator dispatches work to child subgraphs. */ - protected readonly stream = agent({ + protected readonly agent = agent({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - protected readonly chatAgent = toAgent(this.stream); /** * Derived signal: converts the subagents Map to an array for template iteration. * Using `computed()` ensures the template re-renders whenever the Map changes. */ protected readonly subagentEntries = computed(() => { - const map = this.stream.subagents(); + const map = this.agent.subagents()! return Array.from(map.entries()).map(([id, ref]) => ({ id, status: ref.status(), diff --git a/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts b/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts index bf7682738..aff684da8 100644 --- a/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts +++ b/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts @@ -1,6 +1,6 @@ import { Component, computed, signal } from '@angular/core'; import { ChatComponent } from '@ngaf/chat'; -import { agent, toAgent } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; import type { ThreadState } from '@ngaf/langgraph'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { environment } from '../environments/environment'; @@ -22,7 +22,7 @@ import { environment } from '../environments/environment'; template: ` - +
(-1); /** Checkpoint history derived from the agent. */ protected readonly checkpoints = computed( - (): ThreadState[] => this.stream.history(), + (): ThreadState[] => this.agent.langGraphHistory(), ); /** Display label for a checkpoint entry. */ @@ -132,7 +131,7 @@ export class TimeTravelComponent { protected replay(state: ThreadState, index: number): void { if (state.checkpoint?.checkpoint_id) { this.selectedIndex.set(index); - this.stream.setBranch(state.checkpoint.checkpoint_id); + this.agent.setBranch(state.checkpoint.checkpoint_id); } } @@ -140,7 +139,7 @@ export class TimeTravelComponent { protected fork(state: ThreadState, index: number): void { if (state.checkpoint?.checkpoint_id) { this.selectedIndex.set(index); - this.stream.setBranch(state.checkpoint.checkpoint_id); + this.agent.setBranch(state.checkpoint.checkpoint_id); } } } diff --git a/docs/superpowers/plans/2026-05-01-langgraph-agent-unification.md b/docs/superpowers/plans/2026-05-01-langgraph-agent-unification.md new file mode 100644 index 000000000..db11a6fa4 --- /dev/null +++ b/docs/superpowers/plans/2026-05-01-langgraph-agent-unification.md @@ -0,0 +1,467 @@ +# LangGraph `agent()` Unification Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. + +**Goal:** Refactor `@ngaf/langgraph` so `agent({...})` returns a unified `LangGraphAgent` (extends `AgentWithHistory`) preserving all `AgentRef` public surface. Delete `toAgent(ref)`. Migrate ~23 cockpit components and ~20 website doc files. + +**Spec:** `docs/superpowers/specs/2026-05-01-langgraph-agent-unification-design.md` + +**Architecture:** Three sequential commits — lib refactor (self-contained; updated tests prove the new surface), cockpit migration (mechanical with type-checking), website docs (mostly auto-regenerated + manual MDX prose). + +--- + +## File Structure + +### `@ngaf/langgraph` library + +- **Modify:** `libs/langgraph/src/lib/agent.types.ts` — define `LangGraphAgent` interface; remove `AgentRef` from public exports (kept internally if needed). +- **Modify:** `libs/langgraph/src/lib/agent.fn.ts` — `agent()` returns `LangGraphAgent`; folds in the type translation that `to-agent.ts` previously did. +- **Delete:** `libs/langgraph/src/lib/to-agent.ts` +- **Delete:** `libs/langgraph/src/lib/to-agent.spec.ts` +- **Delete:** `libs/langgraph/src/lib/to-agent.conformance.spec.ts` +- **Create:** `libs/langgraph/src/lib/agent.conformance.spec.ts` — runs `runAgentWithHistoryConformance` against an `agent({...})` instance built with `MockAgentTransport`. +- **Modify:** `libs/langgraph/src/lib/agent.fn.spec.ts` — assertions about return-type signal shapes (now runtime-neutral). Add new tests exercising `langGraph*`-prefixed signals. +- **Modify:** `libs/langgraph/src/lib/testing/mock-agent-ref.ts` — rename to `mock-langgraph-agent.ts`; produce `LangGraphAgent` directly (not `AgentRef`). +- **Modify:** `libs/langgraph/src/lib/testing/mock-agent-ref.spec.ts` — rename to match. +- **Modify:** `libs/langgraph/src/public-api.ts` — export `LangGraphAgent` and `mockLangGraphAgent`; remove `toAgent`, `AgentRef` (the internal type stays unexported), `createMockAgentRef`. + +### Cockpit (~23 angular demo apps) + +Each affected component file: drop the `stream` field + `toAgent` call, collapse to a single `agent` field. Audit for any direct `BaseMessage[]`-typed reads → switch to `langGraphMessages()`. + +### Website docs + +Most files are MDX prose with code samples that reference `AgentRef` and `toAgent`. Auto-regenerated artifacts (`api-docs.json`, whitepaper PDFs) regenerate after the lib change. + +--- + +### Task 1: Define `LangGraphAgent` and refactor library internals + +**Files:** +- Modify: `libs/langgraph/src/lib/agent.types.ts` +- Modify: `libs/langgraph/src/lib/agent.fn.ts` +- Delete: `libs/langgraph/src/lib/to-agent.ts` +- Delete: `libs/langgraph/src/lib/to-agent.spec.ts` +- Delete: `libs/langgraph/src/lib/to-agent.conformance.spec.ts` +- Modify: `libs/langgraph/src/public-api.ts` + +#### Step 1: Define `LangGraphAgent` in `agent.types.ts` + +After the existing `AgentRef` interface, add the unified type. Keep `AgentRef` internally (it represents the SDK-shaped intermediate state). Add `LangGraphAgent`: + +```ts +import type { + AgentWithHistory, Message, AgentCheckpoint, AgentStatus, + AgentInterrupt, Subagent, AgentSubmitInput, AgentSubmitOptions, AgentEvent, ToolCall, +} from '@ngaf/chat'; + +/** + * Unified LangGraph agent surface returned by `agent({...})`. Extends + * the runtime-neutral `AgentWithHistory` contract (chat-consumable) with + * the full LangGraph-specific API. + */ +export interface LangGraphAgent + extends AgentWithHistory { + // Raw LangGraph signals (preserve full AgentRef public surface) + langGraphMessages: Signal; + langGraphInterrupts: Signal[]>; + langGraphToolCalls: Signal; + langGraphHistory: Signal[]>; + + value: Signal; + hasValue: Signal; + reload: () => void; + toolProgress: Signal; + activeSubagents: Signal; + customEvents: Signal; + + branch: Signal; + setBranch: (branch: string) => void; + + isThreadLoading: Signal; + switchThread: (threadId: string | null) => void; + joinStream: (runId: string, lastEventId?: string) => Promise; + + getMessagesMetadata: (msg: BaseMessage, idx?: number) => MessageMetadata> | undefined; + getToolCalls: (msg: CoreAIMessage) => ToolCallWithResult[]; +} +``` + +(Imports from `@langchain/langgraph-sdk` and `@langchain/core/messages` for the LangGraph types stay; chat types come from `@ngaf/chat`.) + +#### Step 2: Refactor `agent.fn.ts` to return `LangGraphAgent` + +The internal AgentRef-shaped state stays; the function's RETURN object becomes the unified type. Concretely: continue building all internal signals/subjects as today, then assemble the return object with both runtime-neutral projections AND the raw LangGraph signals. + +The translation logic that lives in today's `to-agent.ts` (e.g., `toMessage(BaseMessage): Message`, `toToolCall(...)`, `toInterrupt(...)`, `toSubagent(...)`, `toCheckpoint(...)`, `mapStatus(...)`, `toAgentEvent(...)`, `buildSubmitPayload(...)`, `buildEvents$(...)`) all moves into `agent.fn.ts` as private helpers. + +Sketch of the return statement: + +```ts +const messagesNeutral: Signal = computed(() => messages$.getValue().map(toMessage)); +const toolCallsNeutral: Signal = computed(() => toolCalls$.getValue().map(toToolCall)); +const statusNeutral: Signal = computed(() => mapStatus(status$.getValue())); +const stateNeutral: Signal> = computed(() => { + const v = values$.getValue(); + return v && typeof v === 'object' ? (v as Record) : {}; +}); +const interruptNeutral: Signal = computed(() => { + const ix = interrupt$.getValue(); + return ix ? toInterrupt(ix) : undefined; +}); +const subagentsNeutral: Signal> = computed(() => { + const out = new Map(); + subagents$.getValue().forEach((sa, key) => out.set(key, toSubagent(sa))); + return out; +}); +const historyNeutral: Signal = computed(() => + history$.getValue().map(toCheckpoint), +); +const events$ = buildEvents$(customEvents$); // still Subject + cursor pattern + +return { + // Runtime-neutral surface (Agent + AgentWithHistory) + messages: messagesNeutral, + status: statusNeutral, + isLoading: isLoading$, + error: error$, + toolCalls: toolCallsNeutral, + state: stateNeutral, + interrupt: interruptNeutral, + subagents: subagentsNeutral, + events$, + history: historyNeutral, + submit: (input, opts) => doSubmit(buildSubmitPayload(input), opts ? { signal: opts.signal } as never : undefined), + stop: () => doStop(), + + // Raw LangGraph signals + langGraphMessages: messages$, + langGraphInterrupts: interrupts$, + langGraphToolCalls: toolCalls$, + langGraphHistory: history$, + + // Other AgentRef fields preserved + value: values$, + hasValue: hasValue$, + reload, + toolProgress: toolProgress$, + activeSubagents: activeSubagents$, + customEvents: customEvents$, + branch: branch$, + setBranch, + isThreadLoading: isThreadLoading$, + switchThread, + joinStream, + getMessagesMetadata, + getToolCalls, +}; +``` + +(Variable names like `messages$`, `status$`, etc. are placeholders for whatever the existing internals call them. The actual implementation references the StreamSubjects + helpers already in agent.fn.ts.) + +#### Step 3: Delete `to-agent.ts` and its specs + +```bash +rm libs/langgraph/src/lib/to-agent.ts +rm libs/langgraph/src/lib/to-agent.spec.ts +rm libs/langgraph/src/lib/to-agent.conformance.spec.ts +``` + +#### Step 4: Update `agent.fn.spec.ts` + +Existing tests assert things like `agent.messages()` returns `BaseMessage[]`. Now they return `Message[]`. Update assertions accordingly. Where a test specifically wants the LangChain-typed message, switch to `agent.langGraphMessages()`. + +Add new tests: +- `agent({...}).messages()` returns `Message[]` with role correctly translated (`human` → `'user'`). +- `agent({...}).langGraphMessages()` returns the raw `BaseMessage[]`. +- `agent({...}).history()` returns `AgentCheckpoint[]`; `langGraphHistory()` returns `ThreadState[]`. + +#### Step 5: Add conformance test + +Create `libs/langgraph/src/lib/agent.conformance.spec.ts`: + +```ts +import { runAgentWithHistoryConformance } from '@ngaf/chat'; +import { agent } from './agent.fn'; +import { MockAgentTransport } from './transport/mock-stream.transport'; +import { TestBed } from '@angular/core/testing'; + +runAgentWithHistoryConformance('agent (LangGraph)', () => { + let result!: ReturnType; + TestBed.runInInjectionContext(() => { + result = agent({ + apiUrl: '', + assistantId: 'test', + transport: new MockAgentTransport(), + }); + }); + return result; +}); +``` + +#### Step 6: Refactor mock helper + +Rename `libs/langgraph/src/lib/testing/mock-agent-ref.ts` → `mock-langgraph-agent.ts`. Update factory function name `createMockAgentRef` → `mockLangGraphAgent`. Returns `LangGraphAgent`-typed mock. Same approach: writable signals for every field. + +Spec file rename + adjustments to match. + +#### Step 7: Update `public-api.ts` + +```ts +// remove +export { toAgent } from './lib/to-agent'; +export { createMockAgentRef } from './lib/testing/mock-agent-ref'; +export type { AgentRef } from './lib/agent.types'; + +// add +export type { LangGraphAgent } from './lib/agent.types'; +export { mockLangGraphAgent } from './lib/testing/mock-langgraph-agent'; +``` + +(Other exports — `agent`, `provideAgent`, `MockAgentTransport`, `AgentConfig`, `BagTemplate`, etc. — stay unchanged.) + +#### Step 8: Verify + +```bash +npx nx run-many -t lint,test,build -p langgraph --skip-nx-cache +``` + +Expected: PASS. The conformance suite covering `runAgentWithHistoryConformance` validates the runtime-neutral surface; remaining specs validate the LangGraph-specific surface. + +#### Step 9: Commit + +```bash +git add libs/langgraph/ +git commit -m "refactor(langgraph): unify agent() return type as LangGraphAgent + +Eliminate two-step agent()+toAgent() pattern. agent() now returns +LangGraphAgent extending AgentWithHistory with the full LangGraph- +specific surface preserved (langGraph*-prefixed raw signals where +type collisions exist with the runtime-neutral chat contract). + +- Define LangGraphAgent in agent.types.ts +- Fold to-agent translation logic into agent.fn.ts +- Delete to-agent.ts and its specs +- Add agent.conformance.spec.ts +- Rename mockAgentRef → mockLangGraphAgent +- public-api.ts: drop toAgent, AgentRef, createMockAgentRef; + add LangGraphAgent, mockLangGraphAgent + +BREAKING: AgentRef is no longer exported. agent() return type changed +from AgentRef to LangGraphAgent. toAgent() removed entirely. + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +### Task 2: Migrate cockpit demos + +**Files:** ~23 cockpit angular component files (audit list at start). + +#### Step 1: Re-audit affected files + +```bash +rg -l "\\btoAgent\\(" cockpit/ apps/ --glob '!**/dist/**' +``` + +Expected: ~23 cockpit files + a few apps/website MDX/TSX files (handled in Task 3). + +#### Step 2: Per-file migration pattern + +The mechanical edit is: + +```ts +// before +protected readonly stream = agent({ apiUrl, assistantId }); +protected readonly chatAgent = toAgent(this.stream); +// (template uses [agent]="chatAgent"; methods called as this.stream.foo()) + +// after +protected readonly agent = agent({ apiUrl, assistantId }); +// (template uses [agent]="agent"; methods called as this.agent.foo()) +``` + +Steps per file: + +1. Drop the `import { toAgent } from '@ngaf/langgraph';` (or remove `toAgent` from the imports list if it's combined with `agent`). +2. Collapse the two-field initializer into one: `protected readonly agent = agent({...});`. **Note:** TypeScript handles the field-named-same-as-import via property shorthand; no compile error. + - If the field-name shadowing feels confusing, the implementer may prefer naming the field `chat` or keep `chatAgent`. Either is acceptable per the spec. +3. Update template bindings: `[agent]="chatAgent"` → `[agent]="agent"`. Adjust template references. +4. Update method calls: `this.stream.setBranch(...)` → `this.agent.setBranch(...)`. +5. **For any code that read `BaseMessage`-typed messages off `this.stream.messages()`** — switch to `this.agent.langGraphMessages()` to preserve the LangChain type. Conversely, if it was rendering through chat primitives, `this.agent.messages()` returns `Message[]` directly. + +#### Step 3: Verify each app builds + +```bash +npx nx affected -t build --base=origin/main 2>&1 | tail -10 +``` + +If any cockpit fails: read the build error, fix the affected component, re-run. Build errors are the migration's main safety net — TypeScript catches missed `toAgent` calls and signal-type mismatches. + +#### Step 4: Commit + +```bash +git add cockpit/ +git commit -m "refactor(cockpit): migrate from agent()+toAgent() two-step to unified LangGraphAgent + +23 cockpit angular demos updated. The two field initializers +(stream = agent({...}); chatAgent = toAgent(this.stream)) collapse +to a single field. Code reading raw BaseMessage[] switches to +.langGraphMessages(); chat-template bindings now reference the +unified agent. + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +### Task 3: Migrate website docs + +**Files:** ~20 files in `apps/website/`. + +#### Step 1: Audit doc files referencing `AgentRef` or `toAgent` + +```bash +rg -l "AgentRef|toAgent" apps/website/ --glob '!**/dist/**' --glob '!**/api-docs.json' +``` + +#### Step 2: Update MDX prose and code samples + +For each MDX file: replace `AgentRef` references with `LangGraphAgent` (or context-appropriate); replace `toAgent(stream)` patterns with the new unified `agent({...})` flow. + +Specific files: +- `apps/website/content/docs/agent/concepts/angular-signals.mdx` — prose update. +- `apps/website/content/docs/agent/api/*.mdx` — code samples; update to show `LangGraphAgent`. +- `apps/website/content/docs/chat/components/*.mdx` — many code samples reference `agent()` then `toAgent()`. Update each. +- `apps/website/content/docs/chat/getting-started/*.mdx` — quickstart and installation samples. +- `apps/website/content/docs/render/a2ui/overview.mdx` — likely a code sample reference. +- `apps/website/content/docs/ag-ui/getting-started/introduction.mdx` — has a "vs LangGraph" mention; verify still accurate. +- `apps/website/src/components/landing/chat-landing/ChatLandingCodeShowcase.tsx` — code sample component; update. +- `apps/website/src/lib/docs-config.ts` — page entry `createMockAgentRef()` becomes `mockLangGraphAgent()`. Adjust slug if desired (or keep slug stable to avoid URL breakage). + +#### Step 3: Rename `createMockAgentRef` doc page + +`apps/website/content/docs/chat/api/create-mock-agent-ref.mdx` → `mock-langgraph-agent.mdx`. Update the page entry in `docs-config.ts`. Or alias both slugs to the new content. + +#### Step 4: Regenerate API docs and whitepapers + +```bash +npm run generate-api-docs # rewrites apps/website/public/api-docs.json +npm run generate-narrative-docs # if it covers this content +npm run generate-whitepaper # whitepaper PDFs +``` + +Verify the generated artifacts no longer mention `AgentRef`/`toAgent`. + +#### Step 5: Update llms.txt + +`apps/website/src/app/llms.txt/route.ts` — example code currently shows `agent({...})` + `toAgent` flow. Simplify to single `agent({...})`. Also update the "Key API" section to mention `LangGraphAgent` instead of `AgentRef`. + +#### Step 6: Build website + +```bash +npx nx build website --skip-nx-cache 2>&1 | tail -3 +``` + +#### Step 7: Update website e2e tests if needed + +```bash +rg -nE "AgentRef|toAgent" apps/website/e2e/ 2>&1 +``` + +Likely no e2e changes needed (tests assert page text, not code samples). + +#### Step 8: Commit + +```bash +git add apps/website/ +git commit -m "docs(website): align with unified LangGraphAgent API + +Update MDX prose, code samples in chat/agent/render docs to reference +LangGraphAgent + the single agent({...}) flow. Regenerate api-docs.json +and whitepapers. Rename createMockAgentRef doc page to mockLangGraphAgent. +Update llms.txt example. + +Co-Authored-By: Claude Opus 4.7 " +``` + +--- + +### Task 4: Final verification, push, PR + +#### Step 1: Final residual-check + +```bash +echo "AgentRef refs remaining (should be 0 in source, may exist in docs/superpowers historical):" +rg "AgentRef" libs/ cockpit/ apps/ --glob '!**/dist/**' --glob '!**/api-docs.json' --glob '!docs/superpowers/**' + +echo "toAgent refs remaining (should be 0 in src; AG-UI keeps its own toAgent):" +rg "\\btoAgent\\(" libs/langgraph/ cockpit/ apps/ --glob '!**/dist/**' +``` + +Expected: zero hits in both. + +#### Step 2: Full build sweep + +```bash +npx nx run-many -t lint,test,build -p chat,langgraph,ag-ui --skip-nx-cache +npx nx affected -t build --base=origin/main +``` + +Expected: PASS. + +#### Step 3: Push + open PR + +```bash +git push -u origin feat/toagent-review + +gh pr create --title "refactor(langgraph): unify agent() return type as LangGraphAgent" --body "$(cat <<'EOF' +## Summary +Eliminates the two-step \`agent({...}) + toAgent(stream)\` pattern. \`agent({...})\` now returns a unified \`LangGraphAgent\` (extends \`AgentWithHistory\`) preserving all \`AgentRef\` public surface. \`toAgent\` removed entirely. ~23 cockpit demos and ~20 website doc files migrated. + +## Breaking changes +- \`agent({...})\` return type: \`AgentRef\` → \`LangGraphAgent\`. +- \`toAgent\` removed from \`@ngaf/langgraph\`. +- \`AgentRef\` no longer exported (kept internally as implementation detail). +- \`createMockAgentRef\` → \`mockLangGraphAgent\`. +- \`messages\` signal type changed: \`Signal\` → \`Signal\`. Use \`langGraphMessages\` for raw LangChain types. + +## Migration in cockpit demos +\`\`\`diff +-protected readonly stream = agent({ apiUrl, assistantId }); +-protected readonly chatAgent = toAgent(this.stream); ++protected readonly agent = agent({ apiUrl, assistantId }); +\`\`\` + +## Test Plan +- [x] \`@ngaf/langgraph\` lint/test/build pass with new conformance suite +- [x] All 23 cockpit angular demos build clean +- [x] Website builds clean; regenerated api-docs.json reflects new API +- [x] Zero residual \`AgentRef\`/\`toAgent\` refs in source + +## Design + plan +- Spec: \`docs/superpowers/specs/2026-05-01-langgraph-agent-unification-design.md\` +- Plan: \`docs/superpowers/plans/2026-05-01-langgraph-agent-unification.md\` + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +--- + +## Out of Scope + +- AG-UI adapter changes. AG-UI's `toAgent(source)` stays as the adapter primitive. +- Adding `provideLangGraphAgent({...})` DI helper. Asymmetry with AG-UI's provider model is accepted per spec. +- Renaming `agent` (the function name). +- Backward-compat aliases for old method names. +- Releasing a new npm version. Plan ends with the merge; the next release bumps will pick up the breaking change. + +## Risk + +- **Largest mechanical migration in the project's history (~50 files).** Type-check + build provide the safety net. +- **Subtle semantic change in `messages` typing.** Cockpit components that did `m.content` continue to work; ones that downcast to specific LangChain message types (`if (m instanceof HumanMessage)`) need to switch to `langGraphMessages()`. +- **Mock helper rename** could break any code that imported `createMockAgentRef` outside cockpit. Audit scope. +- **Website doc regeneration** may surface other content drift unrelated to this PR; resist scope creep in those follow-up edits. diff --git a/docs/superpowers/specs/2026-05-01-langgraph-agent-unification-design.md b/docs/superpowers/specs/2026-05-01-langgraph-agent-unification-design.md new file mode 100644 index 000000000..f5ce61607 --- /dev/null +++ b/docs/superpowers/specs/2026-05-01-langgraph-agent-unification-design.md @@ -0,0 +1,234 @@ +# LangGraph `agent()` Unification Design + +## Goal + +Eliminate the two-step `agent({...}); toAgent(stream)` pattern by having `agent({...})` return a single unified `LangGraphAgent` type that satisfies both the runtime-neutral `Agent`/`AgentWithHistory` contract AND preserves all of `AgentRef`'s public surface. Remove `toAgent()` entirely. Refactor all consumers (~23 cockpit components, ~20 website docs). + +## Motivation + +The current pattern looks redundant in the simplest demos: + +```ts +protected readonly stream = agent({ apiUrl, assistantId }); +protected readonly chatAgent = toAgent(this.stream); +``` + +Two related-but-different objects, two field initializers, friction at every entry point. The two-step exists today because: +- `AgentRef` (returned by `agent()`) uses LangGraph types (`BaseMessage`, `ResourceStatus`, `ThreadState`). +- `Agent` (consumed by ``) uses runtime-neutral types (`Message`, `AgentStatus`, `AgentCheckpoint`). +- `toAgent` translates types and wraps signals. + +Users pay this cost in every component. Most don't need direct `AgentRef` access — they just want one object that drives `` and exposes LangGraph extras when needed. + +The fix: bake the type translation into `agent()`. The returned object IS-A `Agent` (chat-consumable) AND has the LangGraph-specific surface as additional methods/signals. + +## Architecture + +### `LangGraphAgent` interface + +Lives in `@ngaf/langgraph`. Extends `AgentWithHistory` (from `@ngaf/chat`). Adds the LangGraph-specific surface. + +```ts +import type { + AgentWithHistory, Message, AgentCheckpoint, AgentStatus, + AgentInterrupt, Subagent, AgentSubmitInput, AgentSubmitOptions, AgentEvent, +} from '@ngaf/chat'; +import type { Signal } from '@angular/core'; +import type { Observable } from 'rxjs'; +import type { BaseMessage } from '@langchain/core/messages'; +import type { + Interrupt, ToolProgress, ToolCallWithResult, + MessageMetadata, CoreAIMessage, CustomStreamEvent, +} from '@langchain/langgraph-sdk'; +import type { ThreadState, BagTemplate, SubagentStreamRef } from './agent.types'; + +/** + * Unified LangGraph agent surface. Returned by `agent({...})`. + * + * Extends `AgentWithHistory` (chat-consumable) with the full LangGraph- + * specific API (branch, switchThread, joinStream, raw BaseMessage access, + * etc.). One object drives both `` and any LangGraph-specific demo. + */ +export interface LangGraphAgent + extends AgentWithHistory { + // ── AgentWithHistory inherited (runtime-neutral types) ────────────── + // messages: Signal; + // status: Signal; + // isLoading: Signal; + // error: Signal; + // toolCalls: Signal; + // state: Signal>; + // interrupt?: Signal; + // subagents?: Signal>; + // events$: Observable; + // history: Signal; + // submit(input: AgentSubmitInput, opts?: AgentSubmitOptions): Promise; + // stop(): Promise; + + // ── LangGraph-specific extras (preserved from AgentRef) ───────────── + /** Raw LangChain BaseMessage list. Use this when you need LangChain-typed + * messages (e.g., to call `.toJSON()` or pattern-match by type). For + * rendering through chat primitives, use `messages` instead. */ + langGraphMessages: Signal; + + /** Current agent state values (raw, untyped per the type parameter T). */ + value: Signal; + + /** True once at least one value or message has been received. */ + hasValue: Signal; + + /** Re-submit the last input to restart the stream. */ + reload: () => void; + + /** All interrupts received during the current run (raw LangGraph shape). */ + langGraphInterrupts: Signal[]>; + + /** Progress updates for currently executing tools. */ + toolProgress: Signal; + + /** Raw LangGraph tool calls (with run-state distinct from runtime-neutral + * ToolCallStatus). Use `toolCalls` for chat rendering. */ + langGraphToolCalls: Signal; + + /** Filtered list of subagents with status 'running'. */ + activeSubagents: Signal; + + /** Raw custom events stream (signal of array). The runtime-neutral + * `events$` Observable is derived from this. */ + customEvents: Signal; + + /** Current branch identifier for time-travel navigation. */ + branch: Signal; + + /** Set the active branch for time-travel navigation. */ + setBranch: (branch: string) => void; + + /** Raw LangGraph history (signal of ThreadState[]). Use `history` for + * the runtime-neutral AgentCheckpoint[]. */ + langGraphHistory: Signal[]>; + + /** True while a thread switch is loading state from the server. */ + isThreadLoading: Signal; + + /** 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; + + /** Get metadata for a specific message by index. */ + getMessagesMetadata: (msg: BaseMessage, idx?: number) => MessageMetadata> | undefined; + + /** Get tool call results associated with an AI message (LangGraph types). */ + getToolCalls: (msg: CoreAIMessage) => ToolCallWithResult[]; +} +``` + +### `agent({...})` returns `LangGraphAgent` + +Currently returns `AgentRef`. After this design: returns `LangGraphAgent`. + +Implementation: keep the existing internal AgentRef construction (it's the LangGraph SDK plumbing). Adapt the return value to the unified shape — fold in the runtime-neutral signal projections (`messages` → `Message[]`, `status` → `AgentStatus`, `history` → `AgentCheckpoint[]`, etc.) AND expose the original raw signals under prefixed names (`langGraphMessages`, `langGraphHistory`, etc.). + +### `toAgent()` deletion + +`toAgent(ref: AgentRef)` is removed entirely from `@ngaf/langgraph`. No deprecation period — the project is at `0.0.x` and breaking changes are acceptable. + +`AgentRef` itself is removed from the public API. Internally the implementation may still use the `AgentRef`-shaped intermediate (or it may be refactored further), but consumers no longer see it. + +### AG-UI side stays the same + +`@ngaf/ag-ui` continues to export `toAgent(source: AbstractAgent): Agent` as the adapter primitive (users subclass `AbstractAgent` for custom transports). The asymmetry — LangGraph has `agent()` factory, AG-UI has `provideAgUiAgent()` provider + `toAgent()` primitive — is acknowledged and accepted. + +## Type-level Tension + +`AgentRef.messages: Signal` (LangChain type) collides with `Agent.messages: Signal` (runtime-neutral). Cannot have both signatures on a single interface. + +**Resolution:** the unified `messages` signal returns `Message[]` (runtime-neutral; satisfies the `Agent` contract). Users who need raw `BaseMessage[]` access read `langGraphMessages: Signal` (additional signal). + +Same pattern applied to other LangGraph-typed signals where the unified shape needs runtime-neutral types: +- `messages` → runtime-neutral `Message[]`. Raw: `langGraphMessages: BaseMessage[]`. +- `toolCalls` → runtime-neutral `ToolCall[]`. Raw: `langGraphToolCalls: ToolCallWithResult[]`. +- `history` → runtime-neutral `AgentCheckpoint[]`. Raw: `langGraphHistory: ThreadState[]`. +- `interrupts` → not on the runtime-neutral surface; the unified type only exposes `langGraphInterrupts` and the runtime-neutral `interrupt?: Signal` (single-current-interrupt projection). +- `customEvents` (Signal) is exposed alongside the runtime-neutral `events$: Observable`. + +This is a deliberate type-level decision: the framework name decisions don't carry. Users get one object, two-naming-scheme: +- Runtime-neutral names (`messages`, `history`, `toolCalls`, `events$`) for chat consumption. +- `langGraph*`-prefixed names for raw access. + +## Cockpit Migration + +23 cockpit components today follow this pattern: + +```ts +// before +protected readonly stream = agent({...}); +protected readonly chatAgent = toAgent(this.stream); +// uses this.stream.setBranch(...), this.chatAgent in templates +``` + +After migration: + +```ts +// after +protected readonly agent = agent({...}); +// uses this.agent.setBranch(...), this.agent in templates +``` + +The variable name change (`stream` + `chatAgent` → `agent`) is part of the migration. Field initializers collapse to one line. Any code that was reading from `this.stream.someAgentRefMethod()` now reads `this.agent.someAgentRefMethod()` — the methods are preserved on the unified type. + +For methods that touched LangGraph-typed signals (e.g., `this.stream.messages()` returning `BaseMessage[]`): +- If the use was rendering through chat primitives → switch to `this.agent.messages()` (returns `Message[]`). +- If the use was reading LangChain-specific message internals → switch to `this.agent.langGraphMessages()`. + +Audit each component's usage during migration. + +## Cockpit Demos Affected + +23 angular cockpit demos (from grep audit): +- All `cockpit/chat/**/angular/` (input, messages, threads, tool-calls, theming, generative-ui, debug, timeline, interrupts, subagents, a2ui, etc.) — most use `toAgent` for the chat surface. +- `cockpit/deep-agents/**/angular/` (filesystem, memory, planning, sandboxes, skills, subagents) — same pattern. +- `cockpit/langgraph/**/angular/` (streaming, interrupts, persistence, etc.) — these may use the `AgentRef`-specific surface more heavily. + +Per-component review needed during migration. + +## Website / Docs Migration + +~20 website doc files reference `AgentRef`: +- `apps/website/content/docs/agent/api/api-docs.json` — generated; regenerated automatically. +- `apps/website/content/docs/agent/concepts/angular-signals.mdx` — manual; update prose. +- `apps/website/content/docs/chat/components/*.mdx` — code samples reference `AgentRef`; update to `LangGraphAgent`. +- `apps/website/content/docs/render/a2ui/overview.mdx` — manual; update. +- Whitepapers (`apps/website/public/whitepapers/*.html`) — generated; regenerate as part of this work. +- `apps/website/src/components/landing/chat-landing/ChatLandingCodeShowcase.tsx` — code samples; update. + +Audit during plan execution; the doc generation scripts (`generate-api-docs.ts`, etc.) cover most of it after the lib changes. + +## Tests + +- `libs/langgraph/src/lib/to-agent.spec.ts` — delete (toAgent gone). +- `libs/langgraph/src/lib/to-agent.conformance.spec.ts` — delete (toAgent gone). +- New: `libs/langgraph/src/lib/agent.fn.spec.ts` (or similar) covers the `agent({...})` factory's runtime-neutral surface — runs `runAgentWithHistoryConformance` against the result. +- `libs/langgraph/src/lib/testing/mock-agent-ref.ts` → renamed/refactored to produce a `LangGraphAgent` mock instead of an `AgentRef`. Keeps the same value to test consumers. + +## Out of Scope + +- Renaming `agent` (the function) itself. Stays `agent({...})`. +- Adding a `provideLangGraphAgent({...})` DI helper for symmetry with AG-UI. Asymmetry is accepted. +- Re-exposing AgentRef as a public type. Removed entirely. +- Backward-compat aliases for old method names. Project is at `0.0.x`. +- AG-UI adapter changes. AG-UI side remains as-is (`provideAgUiAgent` + `toAgent(source)`). + +## Risk + +- **Big consumer-side migration** (~23 cockpit components, ~20 docs). Mechanical but tedious; possible to miss a usage of `langGraphMessages` semantics. +- **Tests covering AgentRef directly** (mock-agent-ref tests, conformance specs) need rewriting against the unified type. +- **`getMessagesMetadata(msg: BaseMessage, ...)` signature** — its first arg type stays `BaseMessage`, which means consumers calling it from runtime-neutral `Message`-typed code can't pass directly. They'd need `agent.langGraphMessages()` to get a `BaseMessage` to pass back in. Acceptable — this method is genuinely LangGraph-specific. +- **Internal `AgentRef`-shaped object** still exists during the refactor (the implementation hasn't been rewritten to remove it; it just stops being public). Means the LangGraph SDK plumbing in `agent.fn.ts` continues working unchanged; only the boundary changes. + +## When to Revisit + +- If a third runtime adapter is built and the `LangGraph*` naming for raw signals starts to look idiosyncratic vs. a more general "raw access" pattern. +- If `getMessagesMetadata` and similar `BaseMessage`-typed methods get heavy use from chat primitives (today they're LangGraph-demo-specific). +- If users start reaching into `AgentRef`-internals beyond the documented public surface — that signals more methods need promoting onto `LangGraphAgent`. diff --git a/libs/langgraph/src/lib/agent.conformance.spec.ts b/libs/langgraph/src/lib/agent.conformance.spec.ts new file mode 100644 index 000000000..05aa7240d --- /dev/null +++ b/libs/langgraph/src/lib/agent.conformance.spec.ts @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// Conformance suite: verifies LangGraphAgent satisfies the runtime-neutral +// Agent contract defined in @ngaf/chat. +// +// NOTE: We use runAgentConformance (base) rather than runAgentWithHistoryConformance +// because the seeded-checkpoint branch of the history conformance is incompatible +// with agent(): there is no public API to pre-seed ThreadState[] checkpoints at +// construction time (they arrive via the stream transport). The history signal +// is exercised in agent.fn.spec.ts instead. +import { TestBed } from '@angular/core/testing'; +import { runAgentConformance } from '@ngaf/chat'; +import { agent } from './agent.fn'; +import { MockAgentTransport } from './transport/mock-stream.transport'; + +runAgentConformance('agent (LangGraph)', () => { + let result!: ReturnType; + TestBed.runInInjectionContext(() => { + result = agent({ + apiUrl: '', + assistantId: 'test', + transport: new MockAgentTransport(), + }); + }); + return result; +}); diff --git a/libs/langgraph/src/lib/agent.fn.spec.ts b/libs/langgraph/src/lib/agent.fn.spec.ts index e3eb98efa..aa4d89309 100644 --- a/libs/langgraph/src/lib/agent.fn.spec.ts +++ b/libs/langgraph/src/lib/agent.fn.spec.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { signal } from '@angular/core'; +import { HumanMessage, AIMessage } from '@langchain/core/messages'; import { agent } from './agent.fn'; import { MockAgentTransport } from './transport/mock-stream.transport'; import { ResourceStatus } from './agent.types'; @@ -19,7 +20,8 @@ describe('agent', () => { const ref = withInjectionContext(() => agent({ apiUrl: '', assistantId: 'a', transport }) ); - expect(ref.status()).toBe(ResourceStatus.Idle); + // status() now returns AgentStatus (runtime-neutral), not ResourceStatus + expect(ref.status()).toBe('idle'); expect(ref.isLoading()).toBe(false); expect(ref.hasValue()).toBe(false); expect(ref.error()).toBeUndefined(); @@ -37,12 +39,12 @@ describe('agent', () => { expect((ref.value() as any).count).toBe(99); }); - it('status transitions to Loading on submit()', async () => { + it('status transitions to running (isLoading) on submit()', async () => { const transport = new MockAgentTransport(); const ref = withInjectionContext(() => agent({ apiUrl: '', assistantId: 'a', transport }) ); - ref.submit({}); + ref.submit({ message: 'hello' }); expect(ref.isLoading()).toBe(true); }); @@ -51,7 +53,7 @@ describe('agent', () => { const ref = withInjectionContext(() => agent({ apiUrl: '', assistantId: 'a', transport }) ); - ref.submit({}); + ref.submit({ message: 'hello' }); transport.emit([{ type: 'values', values: { x: 1 } }]); transport.close(); await new Promise(r => setTimeout(r, 20)); @@ -59,26 +61,26 @@ describe('agent', () => { expect((ref.value() as any).x).toBe(1); }); - it('error() is set and status is Error on transport error', async () => { + it('error() is set and status is "error" on transport error', async () => { const transport = new MockAgentTransport(); const ref = withInjectionContext(() => agent({ apiUrl: '', assistantId: 'a', transport }) ); - ref.submit({}); + ref.submit({ message: 'hello' }); transport.emitError(new Error('fail')); await new Promise(r => setTimeout(r, 20)); - expect(ref.status()).toBe(ResourceStatus.Error); + expect(ref.status()).toBe('error'); expect(ref.error()).toBeInstanceOf(Error); }); - it('stop() resolves the stream and sets status to Resolved', async () => { + it('stop() resolves the stream and sets status to idle', async () => { const transport = new MockAgentTransport(); const ref = withInjectionContext(() => agent({ apiUrl: '', assistantId: 'a', transport }) ); - ref.submit({}); + ref.submit({ message: 'hello' }); await ref.stop(); - expect(ref.status()).toBe(ResourceStatus.Resolved); + // After stop, status is no longer "running" expect(ref.isLoading()).toBe(false); }); @@ -87,7 +89,7 @@ describe('agent', () => { const ref = withInjectionContext(() => agent({ apiUrl: '', assistantId: 'a', transport }) ); - await ref.submit({ msg: 'hello' }); + await ref.submit({ message: 'hello' }); transport.close(); await new Promise(r => setTimeout(r, 10)); ref.reload(); @@ -101,22 +103,80 @@ describe('agent', () => { const ref = withInjectionContext(() => agent({ apiUrl: '', assistantId: 'a', transport, threadId }) ); - expect(ref.status()).toBe(ResourceStatus.Idle); + expect(ref.status()).toBe('idle'); }); - it('messages() updates when messages event received', async () => { + it('messages() returns Message[] (runtime-neutral) with correct role translation', async () => { const transport = new MockAgentTransport(); const ref = withInjectionContext(() => agent({ apiUrl: '', assistantId: 'a', transport }) ); - ref.submit({}); + ref.submit({ message: 'hello' }); transport.emit([{ type: 'messages', messages: [{ id: '1', type: 'human', content: 'hi' }], }]); transport.close(); await new Promise(r => setTimeout(r, 20)); - expect(ref.messages()).toHaveLength(1); + const msgs = ref.messages(); + expect(msgs).toHaveLength(1); + // Runtime-neutral role: 'human' translates to 'user' + expect(msgs[0].role).toBe('user'); + expect(msgs[0].content).toBe('hi'); + }); + + it('langGraphMessages() returns raw BaseMessage[] without role translation', async () => { + const transport = new MockAgentTransport(); + const ref = withInjectionContext(() => + agent({ apiUrl: '', assistantId: 'a', transport, throttle: false }) + ); + ref.submit({ message: 'hello' }); + transport.emit([{ + type: 'messages', + messages: [{ id: '1', type: 'human', content: 'hi' }], + }]); + transport.close(); + await new Promise(r => setTimeout(r, 30)); + const rawMsgs = ref.langGraphMessages(); + expect(rawMsgs).toHaveLength(1); + // Raw BaseMessage: no role translation — the internal type field is 'human' + const raw = rawMsgs[0] as any; + const type = typeof raw._getType === 'function' ? raw._getType() : raw['type']; + expect(type).toBe('human'); + }); + + it('history() returns AgentCheckpoint[]; langGraphHistory() returns ThreadState[]', async () => { + const transport = new MockAgentTransport(); + const ref = withInjectionContext(() => + agent({ apiUrl: '', assistantId: 'a', transport }) + ); + // Initially both are empty arrays + expect(ref.history()).toEqual([]); + expect(ref.langGraphHistory()).toEqual([]); + + // history() returns AgentCheckpoint-shaped objects (runtime-neutral) + const histVal = ref.history(); + expect(Array.isArray(histVal)).toBe(true); + + // langGraphHistory() returns ThreadState-shaped objects + const rawHist = ref.langGraphHistory(); + expect(Array.isArray(rawHist)).toBe(true); + }); + + it('messages() translates AIMessage role to "assistant"', async () => { + const transport = new MockAgentTransport(); + const ref = withInjectionContext(() => + agent({ apiUrl: '', assistantId: 'a', transport }) + ); + ref.submit({ message: 'hello' }); + transport.emit([{ + type: 'messages', + messages: [{ id: '2', type: 'ai', content: 'hello back' }], + }]); + transport.close(); + await new Promise(r => setTimeout(r, 20)); + const msgs = ref.messages(); + expect(msgs[0].role).toBe('assistant'); }); it('switchThread() resets messages and values', () => { @@ -135,7 +195,7 @@ describe('agent', () => { agent({ apiUrl: '', assistantId: 'a', transport, threadId }) ); - ref.submit({}); + ref.submit({ message: 'hello' }); transport.emit([ { type: 'values', values: { x: 1 } }, { type: 'messages', messages: [{ id: '1', type: 'human', content: 'hi' }] as any[] }, @@ -150,13 +210,36 @@ describe('agent', () => { await new Promise(r => setTimeout(r, 30)); expect(ref.hasValue()).toBe(false); - expect(ref.status()).toBe(ResourceStatus.Idle); + expect(ref.status()).toBe('idle'); expect(ref.error()).toBeUndefined(); expect(ref.value()).toEqual({}); expect(ref.messages()).toEqual([]); expect(ref.history()).toEqual([]); expect(ref.interrupt()).toBeUndefined(); - expect(ref.interrupts()).toEqual([]); expect(ref.isThreadLoading()).toBe(false); }); + + it('langGraphInterrupts() exposes raw LangGraph interrupts signal', () => { + const transport = new MockAgentTransport(); + const ref = withInjectionContext(() => + agent({ apiUrl: '', assistantId: 'a', transport }) + ); + expect(Array.isArray(ref.langGraphInterrupts())).toBe(true); + }); + + it('langGraphToolCalls() exposes raw ToolCallWithResult[] signal', () => { + const transport = new MockAgentTransport(); + const ref = withInjectionContext(() => + agent({ apiUrl: '', assistantId: 'a', transport }) + ); + expect(Array.isArray(ref.langGraphToolCalls())).toBe(true); + }); + + it('events$ is an Observable-like with .subscribe', () => { + const transport = new MockAgentTransport(); + const ref = withInjectionContext(() => + agent({ apiUrl: '', assistantId: 'a', transport }) + ); + expect(typeof ref.events$.subscribe).toBe('function'); + }); }); diff --git a/libs/langgraph/src/lib/agent.fn.ts b/libs/langgraph/src/lib/agent.fn.ts index 2e75889c1..6f60f7bf4 100644 --- a/libs/langgraph/src/lib/agent.fn.ts +++ b/libs/langgraph/src/lib/agent.fn.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT import { - inject, DestroyRef, computed, + inject, DestroyRef, computed, effect, isSignal, Signal, } from '@angular/core'; import { AGENT_CONFIG } from './agent.provider'; @@ -10,32 +10,47 @@ import { throttleTime, asyncScheduler, } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import type { Observable } from 'rxjs'; import type { BaseMessage } from '@langchain/core/messages'; -import type { Interrupt } from '@langchain/langgraph-sdk'; +import type { Interrupt, ToolCallWithResult } from '@langchain/langgraph-sdk'; import type { BagTemplate, InferBag } from '@langchain/langgraph-sdk'; +import type { + AgentEvent, + AgentCheckpoint, + AgentInterrupt, + AgentStatus, + Message, + Role, + Subagent, + ToolCall, + ToolCallStatus, + AgentSubmitInput, + AgentSubmitOptions, +} from '@ngaf/chat'; import { AgentOptions, - AgentRef, + LangGraphAgent, CustomStreamEvent, StreamSubjects, SubagentStreamRef, ResourceStatus, } from './agent.types'; -import type { ThreadState, ToolProgress, ToolCallWithResult } from '@langchain/langgraph-sdk'; +import type { ThreadState, ToolProgress } 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. + * field initializer, or `runInInjectionContext`). Returns a unified + * {@link LangGraphAgent} 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 AgentRef} with reactive signals and action methods + * @returns A {@link LangGraphAgent} with reactive signals and action methods * * @example * ```typescript @@ -56,7 +71,7 @@ export function agent< Bag extends BagTemplate = BagTemplate, >( options: AgentOptions>, -): AgentRef> { +): LangGraphAgent> { // Injection context required const destroyRef = inject(DestroyRef); const globalConfig = inject(AGENT_CONFIG, { optional: true }); @@ -135,7 +150,7 @@ export function agent< // Convert to Angular Signals (must happen in injection context) const value = toSignal(maybeThrottle(values$), { initialValue: init }); - const messages = toSignal(maybeThrottle(messages$), { initialValue: [] as BaseMessage[] }); + const rawMessages = toSignal(maybeThrottle(messages$), { initialValue: [] as BaseMessage[] }); const statusSig = toSignal(status$, { initialValue: ResourceStatus.Idle }); const errorSig = toSignal(error$, { initialValue: undefined as unknown }); const hasValueSig = toSignal(hasValue$, { initialValue: false }); @@ -145,7 +160,7 @@ export function agent< const historySig = toSignal(history$, { initialValue: [] }); const threadLoadSig= toSignal(isThreadLoading$, { initialValue: false }); const toolProgSig = toSignal(toolProgress$, { initialValue: [] }); - const toolCallsSig = toSignal(toolCalls$, { initialValue: [] }); + const rawToolCalls = toSignal(toolCalls$, { initialValue: [] }); const subagentsSig = toSignal(subagents$, { initialValue: new Map() }); const customSig = toSignal(custom$, { initialValue: [] as CustomStreamEvent[] }); @@ -154,49 +169,203 @@ export function agent< [...subagentsSig().values()].filter(s => s.status() === 'running') ); - return { - // ResourceRef compatible - value: value as Signal, - status: statusSig, - isLoading, - error: errorSig, - hasValue: hasValueSig, - reload: () => manager.resubmitLast(), - - // Streaming state - messages: messages as Signal, - interrupt: interruptSig, - interrupts: interruptsSig, - toolProgress: toolProgSig, - toolCalls: toolCallsSig, + // ── Runtime-neutral projections ─────────────────────────────────────────── - // Thread & history - branch: branchSig, - history: historySig, - isThreadLoading: threadLoadSig, + const messagesNeutral = computed(() => rawMessages().map(toMessage)); - // Subagents - subagents: subagentsSig, - activeSubagents, + const toolCallsNeutral = computed(() => rawToolCalls().map(toToolCall)); - // Custom events - customEvents: customSig, + const statusNeutral = computed(() => mapStatus(statusSig())); + + const stateNeutral = computed>(() => { + const v = value(); + return v && typeof v === 'object' ? (v as Record) : {}; + }); - // Actions - // submit() fires the stream in the background and resolves immediately - submit: (vals, opts) => { - manager.submit(vals, opts); + const interruptNeutral = computed(() => { + const ix = interruptSig(); + return ix ? toInterrupt(ix) : undefined; + }); + + const subagentsNeutral = computed>(() => { + const out = new Map(); + subagentsSig().forEach((sa, key) => out.set(key, toSubagent(sa))); + return out; + }); + + const historyNeutral = computed(() => + historySig().map(toCheckpoint), + ); + + const events$ = buildEvents$(customSig); + + return { + // ── Runtime-neutral surface (AgentWithHistory) ──────────────────────── + messages: messagesNeutral, + status: statusNeutral, + isLoading, + error: errorSig, + toolCalls: toolCallsNeutral, + state: stateNeutral, + interrupt: interruptNeutral, + subagents: subagentsNeutral, + events$, + history: historyNeutral, + submit: (input: AgentSubmitInput, opts?: AgentSubmitOptions) => { + manager.submit(buildSubmitPayload(input), opts ? { signal: opts.signal } as never : undefined); return Promise.resolve(); }, - stop: () => manager.stop(), - switchThread: (id) => { + stop: () => manager.stop(), + + // ── Raw LangGraph signals ───────────────────────────────────────────── + langGraphMessages: rawMessages as Signal, + langGraphInterrupts: interruptsSig, + langGraphToolCalls: rawToolCalls, + langGraphHistory: historySig, + + // ── Other AgentRef fields preserved ────────────────────────────────── + value: value as Signal, + hasValue: hasValueSig, + reload: () => manager.resubmitLast(), + toolProgress: toolProgSig, + activeSubagents, + customEvents: customSig, + branch: branchSig, + setBranch: (b) => branch$.next(b), + isThreadLoading: threadLoadSig, + switchThread: (id) => { resetDerivedThreadState(); manager.switchThread(id); }, - joinStream: (id, last) => manager.joinStream(id, last), - setBranch: (b) => branch$.next(b), + joinStream: (id, last) => manager.joinStream(id, last), // V1 deferred: requires StreamManager's internal message registry getMessagesMetadata: (_msg, _idx) => undefined, - getToolCalls: (_msg) => [], + getToolCalls: (_msg) => [], }; } + +// ── Private translation helpers (moved from to-agent.ts) ───────────────────── + +/** + * Build an Observable that bridges LangGraph's + * `Signal` (append-only array) into a stream of newly + * emitted events. Each effect firing compares against a cursor tracking the + * previously-seen length and emits only the tail slice. + */ +function buildEvents$(customSig: Signal): Observable { + const subject = new Subject(); + let seen = 0; + effect(() => { + const all = customSig(); + if (all.length < seen) { + // Stream reset (new session, thread switch, etc.). Rewind cursor. + seen = 0; + } + for (let i = seen; i < all.length; i++) { + subject.next(toAgentEvent(all[i])); + } + seen = all.length; + }); + return subject.asObservable(); +} + +function toAgentEvent(e: CustomStreamEvent): AgentEvent { + if (e.name === 'state_update' && isRecord(e.data)) { + return { type: 'state_update', data: e.data }; + } + return { type: 'custom', name: e.name, data: e.data }; +} + +function mapStatus(s: ResourceStatus): AgentStatus { + switch (s) { + case ResourceStatus.Error: return 'error'; + case ResourceStatus.Loading: + case ResourceStatus.Reloading: + return 'running'; + default: + return 'idle'; + } +} + +function toMessage(m: BaseMessage): Message { + const raw = m as unknown as Record; + const typeVal = typeof m._getType === 'function' + ? m._getType() + : (raw['type'] as string | undefined) ?? 'ai'; + const role: Role = + typeVal === 'human' ? 'user' : + typeVal === 'tool' ? 'tool' : + typeVal === 'system' ? 'system' : + 'assistant'; + return { + id: (m.id as string | undefined) ?? (raw['id'] as string | undefined) ?? randomId(), + role, + content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content), + toolCallId: raw['tool_call_id'] as string | undefined, + name: raw['name'] as string | undefined, + extra: raw, + }; +} + +function toToolCall(tc: ToolCallWithResult): ToolCall { + const stateMap: Record = { + pending: 'pending', + completed: 'complete', + error: 'error', + }; + const status: ToolCallStatus = stateMap[tc.state] ?? 'running'; + const result = tc.result as (Record | undefined); + return { + id: tc.id, + name: tc.call.name, + args: tc.call.args, + status, + result: result?.['content'], + error: tc.state === 'error' ? result?.['content'] : undefined, + }; +} + +function toInterrupt(ix: Interrupt): AgentInterrupt { + const raw = ix as unknown as Record; + return { + id: (raw['id'] as string | undefined) ?? randomId(), + value: raw['value'] ?? ix, + resumable: true, + }; +} + +function toSubagent(sa: SubagentStreamRef): Subagent { + return { + toolCallId: sa.toolCallId, + status: sa.status, + messages: computed(() => sa.messages().map(toMessage)) as Signal, + state: sa.values as Signal>, + }; +} + +function buildSubmitPayload(input: AgentSubmitInput): unknown { + if (input.resume !== undefined) return { __resume__: input.resume }; + if (input.message !== undefined) { + const content = typeof input.message === 'string' + ? input.message + : input.message.map((b: any) => (b.type === 'text' ? b.text : JSON.stringify(b))).join(''); + return { messages: [{ role: 'human', content }], ...(input.state ?? {}) }; + } + return input.state ?? {}; +} + +function randomId(): string { + return Math.random().toString(36).slice(2); +} + +function toCheckpoint(state: ThreadState): AgentCheckpoint { + return { + id: state.checkpoint?.checkpoint_id ?? undefined, + label: state.next?.[0] ?? undefined, + values: isRecord(state.values) ? state.values : {}, + }; +} + +function isRecord(v: unknown): v is Record { + return typeof v === 'object' && v !== null && !Array.isArray(v); +} diff --git a/libs/langgraph/src/lib/agent.types.ts b/libs/langgraph/src/lib/agent.types.ts index e163878cb..3b7f4a04d 100644 --- a/libs/langgraph/src/lib/agent.types.ts +++ b/libs/langgraph/src/lib/agent.types.ts @@ -15,6 +15,7 @@ import type { SubmitOptions, } from '@langchain/langgraph-sdk/ui'; import type { BaseMessage, AIMessage as CoreAIMessage } from '@langchain/core/messages'; +import type { AgentWithHistory } from '@ngaf/chat'; // Re-export SDK types so consumers don't need to import from langgraph-sdk directly export type { BagTemplate, InferBag, Interrupt, ThreadState, SubmitOptions }; @@ -191,6 +192,75 @@ export interface AgentRef { getToolCalls: (msg: CoreAIMessage) => ToolCallWithResult[]; } +// ── LangGraphAgent ──────────────────────────────────────────────────────────── + +/** + * Unified LangGraph agent surface returned by `agent({...})`. + * + * Extends the runtime-neutral `AgentWithHistory` contract (chat-consumable) + * with the full LangGraph-specific API. One object drives both `` and + * any LangGraph-specific demo. Raw LangGraph signals are prefixed with + * `langGraph` to avoid collision with the runtime-neutral names. + */ +export interface LangGraphAgent + extends AgentWithHistory { + // ── Raw LangGraph signals (preserve full AgentRef public surface) ───────── + + /** Raw LangChain BaseMessage list. Use `messages` for chat rendering. */ + langGraphMessages: Signal; + + /** All interrupts received during the current run (raw LangGraph shape). */ + langGraphInterrupts: Signal[]>; + + /** Raw LangGraph tool calls (with run-state). Use `toolCalls` for chat rendering. */ + langGraphToolCalls: Signal; + + /** Raw LangGraph history (ThreadState[]). Use `history` for AgentCheckpoint[]. */ + langGraphHistory: Signal[]>; + + // ── AgentRef fields preserved on the unified surface ───────────────────── + + /** Current agent state values (raw, typed per the type parameter T). */ + value: Signal; + + /** True once at least one value or message has been received. */ + hasValue: Signal; + + /** Re-submit the last input to restart the stream. */ + reload: () => void; + + /** Progress updates for currently executing tools. */ + toolProgress: Signal; + + /** Filtered list of subagents with status 'running'. */ + activeSubagents: Signal; + + /** Raw custom events stream (signal of array). The runtime-neutral + * `events$` Observable is derived from this. */ + customEvents: Signal; + + /** Current branch identifier for time-travel navigation. */ + branch: Signal; + + /** Set the active branch for time-travel navigation. */ + setBranch: (branch: string) => void; + + /** True while a thread switch is loading state from the server. */ + isThreadLoading: Signal; + + /** 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; + + /** Get metadata for a specific message by index. */ + getMessagesMetadata: (msg: BaseMessage, idx?: number) => MessageMetadata> | undefined; + + /** Get tool call results associated with an AI message (LangGraph types). */ + getToolCalls: (msg: CoreAIMessage) => ToolCallWithResult[]; +} + // ── Internal: StreamSubjects ───────────────────────────────────────────────── // Not exported from public-api.ts diff --git a/libs/langgraph/src/lib/testing/mock-agent-ref.ts b/libs/langgraph/src/lib/testing/mock-agent-ref.ts deleted file mode 100644 index 55ddad7d7..000000000 --- a/libs/langgraph/src/lib/testing/mock-agent-ref.ts +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: MIT -import { signal, WritableSignal } from '@angular/core'; -import type { AgentRef, SubagentStreamRef, ResourceStatus as ResourceStatusType, Interrupt, ThreadState, SubmitOptions, CustomStreamEvent } from '../agent.types'; -import type { ToolProgress, ToolCallWithResult } from '@langchain/langgraph-sdk'; -import { ResourceStatus } from '../agent.types'; -import type { BaseMessage, AIMessage as CoreAIMessage } from '@langchain/core/messages'; -import type { MessageMetadata } from '@langchain/langgraph-sdk/ui'; - -/** - * A AgentRef with writable signals for easy test control. - * Cast the result of createMockAgentRef() to this type to access - * writable signals without unsafe casts in test files. - */ -export interface MockAgentRef extends AgentRef { - messages: WritableSignal; - status: WritableSignal; - error: WritableSignal; - interrupt: WritableSignal | undefined>; - interrupts: WritableSignal[]>; - isLoading: WritableSignal; - hasValue: WritableSignal; - value: WritableSignal; - toolProgress: WritableSignal; - toolCalls: WritableSignal; - branch: WritableSignal; - history: WritableSignal[]>; - isThreadLoading: WritableSignal; - subagents: WritableSignal>; - activeSubagents: WritableSignal; - customEvents: WritableSignal; -} - -/** - * Creates a mock AgentRef with writable signals for testing. - * Control state by writing to the returned writable signals directly. - */ -export function createMockAgentRef( - initial: { - messages?: BaseMessage[]; - status?: ResourceStatusType; - isLoading?: boolean; - error?: unknown; - hasValue?: boolean; - isThreadLoading?: boolean; - } = {} -): MockAgentRef { - const messages$ = signal(initial.messages ?? []); - const status$ = signal(initial.status ?? ResourceStatus.Idle); - const isLoading$ = signal(initial.isLoading ?? false); - const error$ = signal(initial.error ?? null); - const hasValue$ = signal(initial.hasValue ?? false); - const value$ = signal(null); - const interrupt$ = signal | undefined>(undefined); - const interrupts$ = signal[]>([]); - const toolProgress$ = signal([]); - const toolCalls$ = signal([]); - const branch$ = signal(''); - const history$ = signal[]>([]); - const isThreadLoading$ = signal(initial.isThreadLoading ?? false); - const subagents$ = signal>(new Map()); - const activeSubagents$ = signal([]); - const customEvents$ = signal([]); - - const ref: MockAgentRef = { - value: value$, - status: status$, - isLoading: isLoading$, - error: error$, - hasValue: hasValue$, - // eslint-disable-next-line @typescript-eslint/no-empty-function - reload: () => {}, - - messages: messages$, - interrupt: interrupt$, - interrupts: interrupts$, - toolProgress: toolProgress$, - toolCalls: toolCalls$, - - branch: branch$, - history: history$, - isThreadLoading: isThreadLoading$, - - subagents: subagents$, - activeSubagents: activeSubagents$, - customEvents: customEvents$, - - submit: (_values: any, _opts?: SubmitOptions) => Promise.resolve(), - stop: () => Promise.resolve(), - // eslint-disable-next-line @typescript-eslint/no-empty-function - switchThread: (_threadId: string | null) => {}, - joinStream: (_runId: string, _lastEventId?: string) => Promise.resolve(), - // eslint-disable-next-line @typescript-eslint/no-empty-function - setBranch: (_branch: string) => {}, - getMessagesMetadata: (_msg: BaseMessage, _idx?: number): MessageMetadata> | undefined => undefined, - getToolCalls: (_msg: CoreAIMessage): ToolCallWithResult[] => [], - }; - - return ref as MockAgentRef; -} diff --git a/libs/langgraph/src/lib/testing/mock-agent-ref.spec.ts b/libs/langgraph/src/lib/testing/mock-langgraph-agent.spec.ts similarity index 53% rename from libs/langgraph/src/lib/testing/mock-agent-ref.spec.ts rename to libs/langgraph/src/lib/testing/mock-langgraph-agent.spec.ts index ba2be1f87..37fd109cb 100644 --- a/libs/langgraph/src/lib/testing/mock-agent-ref.spec.ts +++ b/libs/langgraph/src/lib/testing/mock-langgraph-agent.spec.ts @@ -1,38 +1,41 @@ // SPDX-License-Identifier: MIT import { describe, it, expect } from 'vitest'; -import { createMockAgentRef } from './mock-agent-ref'; -import { ResourceStatus } from '../agent.types'; +import { mockLangGraphAgent } from './mock-langgraph-agent'; -describe('createMockAgentRef', () => { +describe('mockLangGraphAgent', () => { it('creates a mock with default values', () => { - const ref = createMockAgentRef(); + const ref = mockLangGraphAgent(); expect(ref.messages()).toEqual([]); - expect(ref.status()).toBe(ResourceStatus.Idle); + expect(ref.langGraphMessages()).toEqual([]); + expect(ref.status()).toBe('idle'); expect(ref.isLoading()).toBe(false); expect(ref.error()).toBeNull(); expect(ref.hasValue()).toBe(false); expect(ref.isThreadLoading()).toBe(false); expect(ref.interrupt()).toBeUndefined(); - expect(ref.interrupts()).toEqual([]); + expect(ref.langGraphInterrupts()).toEqual([]); expect(ref.toolProgress()).toEqual([]); expect(ref.toolCalls()).toEqual([]); + expect(ref.langGraphToolCalls()).toEqual([]); expect(ref.branch()).toBe(''); expect(ref.history()).toEqual([]); + expect(ref.langGraphHistory()).toEqual([]); expect(ref.subagents().size).toBe(0); expect(ref.activeSubagents()).toEqual([]); + expect(ref.customEvents()).toEqual([]); }); it('accepts initial values for signals', () => { - const ref = createMockAgentRef({ - status: ResourceStatus.Loading, + const ref = mockLangGraphAgent({ + status: 'running', isLoading: true, hasValue: true, isThreadLoading: true, error: new Error('test error'), }); - expect(ref.status()).toBe(ResourceStatus.Loading); + expect(ref.status()).toBe('running'); expect(ref.isLoading()).toBe(true); expect(ref.hasValue()).toBe(true); expect(ref.isThreadLoading()).toBe(true); @@ -40,9 +43,9 @@ describe('createMockAgentRef', () => { }); it('has callable action methods', async () => { - const ref = createMockAgentRef(); + const ref = mockLangGraphAgent(); - await expect(ref.submit(null)).resolves.toBeUndefined(); + await expect(ref.submit({ message: 'hello' })).resolves.toBeUndefined(); await expect(ref.stop()).resolves.toBeUndefined(); await expect(ref.joinStream('run-1')).resolves.toBeUndefined(); expect(() => ref.reload()).not.toThrow(); @@ -51,14 +54,34 @@ describe('createMockAgentRef', () => { }); it('getMessagesMetadata returns undefined by default', () => { - const ref = createMockAgentRef(); + const ref = mockLangGraphAgent(); const result = ref.getMessagesMetadata({} as any); expect(result).toBeUndefined(); }); it('getToolCalls returns empty array by default', () => { - const ref = createMockAgentRef(); + const ref = mockLangGraphAgent(); const result = ref.getToolCalls({} as any); expect(result).toEqual([]); }); + + it('events$ is an Observable-like with .subscribe', () => { + const ref = mockLangGraphAgent(); + expect(typeof ref.events$.subscribe).toBe('function'); + }); + + it('state() returns a plain object derived from value()', () => { + const ref = mockLangGraphAgent(); + const state = ref.state(); + expect(typeof state).toBe('object'); + expect(state).not.toBeNull(); + }); + + it('exposes all langGraph* raw signal fields', () => { + const ref = mockLangGraphAgent(); + expect(typeof ref.langGraphMessages).toBe('function'); + expect(typeof ref.langGraphInterrupts).toBe('function'); + expect(typeof ref.langGraphToolCalls).toBe('function'); + expect(typeof ref.langGraphHistory).toBe('function'); + }); }); diff --git a/libs/langgraph/src/lib/testing/mock-langgraph-agent.ts b/libs/langgraph/src/lib/testing/mock-langgraph-agent.ts new file mode 100644 index 000000000..bd7de0d76 --- /dev/null +++ b/libs/langgraph/src/lib/testing/mock-langgraph-agent.ts @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +import { computed, signal, WritableSignal } from '@angular/core'; +import { Subject } from 'rxjs'; +import type { + LangGraphAgent, + SubagentStreamRef, + Interrupt, + ThreadState, + CustomStreamEvent, +} from '../agent.types'; +import type { ToolProgress, ToolCallWithResult } from '@langchain/langgraph-sdk'; +import type { BaseMessage, AIMessage as CoreAIMessage } from '@langchain/core/messages'; +import type { MessageMetadata } from '@langchain/langgraph-sdk/ui'; +import type { + AgentStatus, + AgentInterrupt, + AgentCheckpoint, + Message, + Subagent, + ToolCall, + AgentEvent, +} from '@ngaf/chat'; + +/** + * A LangGraphAgent mock with writable signals for easy test control. + * + * Cast the result of `mockLangGraphAgent()` to this type to access + * writable signals without unsafe casts in test files. + */ +export interface MockLangGraphAgent extends LangGraphAgent { + // Writable versions of signals for direct test mutation + messages: WritableSignal; + langGraphMessages: WritableSignal; + status: WritableSignal; + isLoading: WritableSignal; + error: WritableSignal; + hasValue: WritableSignal; + value: WritableSignal; + interrupt: WritableSignal; + langGraphInterrupts: WritableSignal[]>; + toolCalls: WritableSignal; + langGraphToolCalls: WritableSignal; + toolProgress: WritableSignal; + branch: WritableSignal; + history: WritableSignal; + langGraphHistory: WritableSignal[]>; + isThreadLoading: WritableSignal; + subagents: WritableSignal>; + activeSubagents: WritableSignal; + customEvents: WritableSignal; +} + +/** + * Creates a mock LangGraphAgent with writable signals for testing. + * Control state by writing to the returned writable signals directly. + */ +export function mockLangGraphAgent( + initial: { + messages?: Message[]; + langGraphMessages?: BaseMessage[]; + status?: AgentStatus; + isLoading?: boolean; + error?: unknown; + hasValue?: boolean; + isThreadLoading?: boolean; + } = {} +): MockLangGraphAgent { + const messages$ = signal(initial.messages ?? []); + const langGraphMessages$ = signal(initial.langGraphMessages ?? []); + const status$ = signal(initial.status ?? 'idle'); + const isLoading$ = signal(initial.isLoading ?? false); + const error$ = signal(initial.error ?? null); + const hasValue$ = signal(initial.hasValue ?? false); + const value$ = signal(null); + const interrupt$ = signal(undefined); + const langGraphInterrupts$ = signal[]>([]); + const toolCalls$ = signal([]); + const langGraphToolCalls$ = signal([]); + const toolProgress$ = signal([]); + const branch$ = signal(''); + const history$ = signal([]); + const langGraphHistory$ = signal[]>([]); + const isThreadLoading$ = signal(initial.isThreadLoading ?? false); + const subagents$ = signal>(new Map()); + const activeSubagents$ = signal([]); + const customEvents$ = signal([]); + + const state$ = computed>(() => { + const v = value$(); + return v && typeof v === 'object' ? (v as Record) : {}; + }); + + const eventsSubject = new Subject(); + + const mock: MockLangGraphAgent = { + // ── AgentWithHistory (runtime-neutral surface) ──────────────────────── + messages: messages$, + status: status$, + isLoading: isLoading$, + error: error$, + toolCalls: toolCalls$, + state: state$, + interrupt: interrupt$, + subagents: subagents$, + events$: eventsSubject.asObservable(), + history: history$, + submit: (_input: any, _opts?: any) => Promise.resolve(), + stop: () => Promise.resolve(), + + // ── Raw LangGraph signals ───────────────────────────────────────────── + langGraphMessages: langGraphMessages$, + langGraphInterrupts: langGraphInterrupts$, + langGraphToolCalls: langGraphToolCalls$, + langGraphHistory: langGraphHistory$, + + // ── Other AgentRef fields preserved ────────────────────────────────── + value: value$, + hasValue: hasValue$, + // eslint-disable-next-line @typescript-eslint/no-empty-function + reload: () => {}, + toolProgress: toolProgress$, + activeSubagents: activeSubagents$, + customEvents: customEvents$, + branch: branch$, + // eslint-disable-next-line @typescript-eslint/no-empty-function + setBranch: (_branch: string) => {}, + isThreadLoading: isThreadLoading$, + // eslint-disable-next-line @typescript-eslint/no-empty-function + switchThread: (_threadId: string | null) => {}, + joinStream: (_runId: string, _lastEventId?: string) => Promise.resolve(), + getMessagesMetadata: (_msg: BaseMessage, _idx?: number): MessageMetadata> | undefined => undefined, + getToolCalls: (_msg: CoreAIMessage): ToolCallWithResult[] => [], + }; + + return mock; +} diff --git a/libs/langgraph/src/lib/to-agent.conformance.spec.ts b/libs/langgraph/src/lib/to-agent.conformance.spec.ts deleted file mode 100644 index 56229274a..000000000 --- a/libs/langgraph/src/lib/to-agent.conformance.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MIT -import { TestBed } from '@angular/core/testing'; -import { runAgentConformance } from '@ngaf/chat'; -import { toAgent } from './to-agent'; -import { signal } from '@angular/core'; -import { ResourceStatus } from './agent.types'; -import type { AgentRef } from './agent.types'; - -/* eslint-disable @typescript-eslint/no-empty-function */ -function minimalRef(): AgentRef { - return { - value: signal({}), - status: signal(ResourceStatus.Idle), - isLoading: signal(false), - error: signal(null), - hasValue: signal(false), - reload: () => {}, - messages: signal([]), - interrupt: signal(undefined), - interrupts: signal([]), - toolProgress: signal([]), - toolCalls: signal([]), - branch: signal(''), - history: signal([]), - isThreadLoading: signal(false), - subagents: signal(new Map()), - activeSubagents: signal([]), - customEvents: signal([]), - submit: async () => {}, - stop: async () => {}, - switchThread: () => {}, - joinStream: async () => {}, - setBranch: () => {}, - getMessagesMetadata: () => undefined, - getToolCalls: () => [], - } as AgentRef; -} - -runAgentConformance('toAgent', () => { - let agent!: ReturnType; - TestBed.runInInjectionContext(() => { - agent = toAgent(minimalRef()); - }); - return agent; -}); diff --git a/libs/langgraph/src/lib/to-agent.spec.ts b/libs/langgraph/src/lib/to-agent.spec.ts deleted file mode 100644 index 1891c4a19..000000000 --- a/libs/langgraph/src/lib/to-agent.spec.ts +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: MIT -import { signal } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { HumanMessage, AIMessage } from '@langchain/core/messages'; -import type { Agent, AgentEvent } from '@ngaf/chat'; -import type { AgentRef, CustomStreamEvent } from './agent.types'; -import { ResourceStatus } from './agent.types'; -import { toAgent } from './to-agent'; - -/* eslint-disable @typescript-eslint/no-empty-function */ -function stubAgentRef(overrides: Partial> = {}): AgentRef { - return { - value: signal(null), - status: signal(ResourceStatus.Idle), - isLoading: signal(false), - error: signal(null), - hasValue: signal(false), - reload: () => {}, - messages: signal([]), - interrupt: signal(undefined), - interrupts: signal([]), - toolProgress: signal([]), - toolCalls: signal([]), - branch: signal(''), - history: signal([]), - isThreadLoading: signal(false), - subagents: signal(new Map()), - activeSubagents: signal([]), - customEvents: signal([]), - submit: async () => {}, - stop: async () => {}, - switchThread: () => {}, - joinStream: async () => {}, - setBranch: () => {}, - getMessagesMetadata: () => undefined, - getToolCalls: () => [], - ...overrides, - } as AgentRef; -} - -describe('toAgent (LangGraph adapter)', () => { - it('translates HumanMessage to role: user', () => { - TestBed.runInInjectionContext(() => { - const ref = stubAgentRef({ messages: signal([new HumanMessage({ content: 'hi', id: 'm1' })]) }); - const agent = toAgent(ref); - expect(agent.messages()).toEqual([ - { id: 'm1', role: 'user', content: 'hi', extra: expect.any(Object) }, - ]); - }); - }); - - it('translates AIMessage to role: assistant', () => { - TestBed.runInInjectionContext(() => { - const ref = stubAgentRef({ messages: signal([new AIMessage({ content: 'hello', id: 'm2' })]) }); - const agent = toAgent(ref); - expect(agent.messages()[0].role).toBe('assistant'); - }); - }); - - it('maps ResourceStatus.Loading to AgentStatus "running" and sets isLoading', () => { - TestBed.runInInjectionContext(() => { - const ref = stubAgentRef({ - status: signal(ResourceStatus.Loading), - isLoading: signal(true), - }); - const agent = toAgent(ref); - expect(agent.status()).toBe('running'); - expect(agent.isLoading()).toBe(true); - }); - }); - - it('maps ResourceStatus.Error to AgentStatus "error"', () => { - TestBed.runInInjectionContext(() => { - const ref = stubAgentRef({ status: signal(ResourceStatus.Error) }); - const agent = toAgent(ref); - expect(agent.status()).toBe('error'); - }); - }); - - it('delegates submit to AgentRef.submit with messages[] payload', async () => { - let captured: unknown = null; - TestBed.runInInjectionContext(async () => { - const ref = stubAgentRef({ submit: async (v) => { captured = v; } }); - const agent = toAgent(ref); - await agent.submit({ message: 'hello' }); - expect(captured).toEqual({ messages: [{ role: 'human', content: 'hello' }] }); - }); - }); - - it('delegates stop to AgentRef.stop', async () => { - let stopped = false; - TestBed.runInInjectionContext(async () => { - const ref = stubAgentRef({ stop: async () => { stopped = true; } }); - const agent = toAgent(ref); - await agent.stop(); - expect(stopped).toBe(true); - }); - }); - - it('translates ThreadState history into AgentCheckpoint[]', () => { - TestBed.runInInjectionContext(() => { - const ref = stubAgentRef({ - history: signal([ - { values: { step: 1 }, next: ['nodeA'], checkpoint: { checkpoint_id: 'ck1' } }, - { values: { step: 2 }, next: [], checkpoint: { checkpoint_id: 'ck2' } }, - { values: { step: 3 }, next: ['nodeC'], checkpoint: undefined }, - ] as any), - }); - const agent = toAgent(ref); - expect(agent.history()).toEqual([ - { id: 'ck1', label: 'nodeA', values: { step: 1 } }, - { id: 'ck2', label: undefined, values: { step: 2 } }, - { id: undefined, label: 'nodeC', values: { step: 3 } }, - ]); - }); - }); - - it('translates a state_update CustomStreamEvent into AgentStateUpdateEvent', () => { - TestBed.runInInjectionContext(() => { - const customEvents = signal([]); - const ref = stubAgentRef({ customEvents } as any); - const chat = toAgent(ref); - - const received: any[] = []; - chat.events$.subscribe((e) => received.push(e)); - - customEvents.set([{ name: 'state_update', data: { count: 1 } }]); - TestBed.flushEffects(); - - expect(received).toEqual([{ type: 'state_update', data: { count: 1 } }]); - }); - }); - - it('wraps non-state_update CustomStreamEvent as AgentCustomEvent', () => { - TestBed.runInInjectionContext(() => { - const customEvents = signal([]); - const ref = stubAgentRef({ customEvents } as any); - const chat = toAgent(ref); - - const received: any[] = []; - chat.events$.subscribe((e) => received.push(e)); - - customEvents.set([{ name: 'tick', data: 42 }]); - TestBed.flushEffects(); - - expect(received).toEqual([{ type: 'custom', name: 'tick', data: 42 }]); - }); - }); - - it('exposes events$ that emits newly-appended events as structured AgentEvent', () => { - const customSig = signal([]); - const ref = stubAgentRef({ customEvents: customSig }); - - let adapter!: Agent; - TestBed.runInInjectionContext(() => { - adapter = toAgent(ref); - }); - - const received: AgentEvent[] = []; - adapter.events$.subscribe((e) => received.push(e)); - - customSig.set([{ name: 'state_update', data: { counter: 1 } }]); - TestBed.flushEffects(); - - expect(received).toEqual([ - { type: 'state_update', data: { counter: 1 } }, - ]); - - customSig.set([ - { name: 'state_update', data: { counter: 1 } }, - { name: 'a2ui.surface', data: { surfaceId: 'main' } }, - ]); - TestBed.flushEffects(); - - expect(received).toEqual([ - { type: 'state_update', data: { counter: 1 } }, - { type: 'custom', name: 'a2ui.surface', data: { surfaceId: 'main' } }, - ]); - }); -}); diff --git a/libs/langgraph/src/lib/to-agent.ts b/libs/langgraph/src/lib/to-agent.ts deleted file mode 100644 index 28dba191f..000000000 --- a/libs/langgraph/src/lib/to-agent.ts +++ /dev/null @@ -1,197 +0,0 @@ -// SPDX-License-Identifier: MIT -import { computed, effect, Signal } from '@angular/core'; -import { Subject, type Observable } from 'rxjs'; -import type { BaseMessage } from '@langchain/core/messages'; -import type { ToolCallWithResult, Interrupt } from '@langchain/langgraph-sdk'; -import type { - AgentWithHistory, AgentCheckpoint, AgentEvent, - Message, Role, ToolCall, ToolCallStatus, AgentStatus, - AgentInterrupt, Subagent, AgentSubmitInput, AgentSubmitOptions, -} from '@ngaf/chat'; -import type { AgentRef, CustomStreamEvent, SubagentStreamRef, ThreadState } from './agent.types'; -import { ResourceStatus } from './agent.types'; - -/** - * Adapts a LangGraph AgentRef to the runtime-neutral Agent contract. - * The returned object is a live view; it reads from the same signals and - * writes back via AgentRef.submit / AgentRef.stop. - * - * Must be called within an Angular injection context (uses `computed` and - * `effect`). - */ -export function toAgent(ref: AgentRef): AgentWithHistory { - const messages = computed(() => - ref.messages().map(toMessage), - ); - - const toolCalls = computed(() => - ref.toolCalls().map(toToolCall), - ); - - const status = computed(() => mapStatus(ref.status())); - - const state = computed>(() => { - const v = ref.value(); - return v && typeof v === 'object' ? (v as Record) : {}; - }); - - const interrupt = computed(() => { - const ix = ref.interrupt(); - return ix ? toInterrupt(ix) : undefined; - }); - - const subagents = computed>(() => { - const src = ref.subagents(); - const out = new Map(); - src.forEach((sa, key) => out.set(key, toSubagent(sa))); - return out; - }); - - const events$ = buildEvents$(ref); - - const history = computed(() => - ref.history().map(toCheckpoint), - ); - - return { - messages, - status, - isLoading: ref.isLoading, - error: ref.error, - toolCalls, - state, - interrupt, - subagents, - events$, - history, - submit: (input: AgentSubmitInput, opts?: AgentSubmitOptions) => - ref.submit(buildSubmitPayload(input), opts ? { signal: opts.signal } as never : undefined), - stop: () => ref.stop(), - }; -} - -/** - * Build an Observable that bridges LangGraph's - * `Signal` (append-only array) into a stream of newly - * emitted events. Each effect firing compares against a cursor tracking the - * previously-seen length and emits only the tail slice. - */ -function buildEvents$( - ref: AgentRef, -): Observable { - const subject = new Subject(); - let seen = 0; - effect(() => { - const all = ref.customEvents(); - if (all.length < seen) { - // Stream reset (new session, thread switch, etc.). Rewind cursor. - seen = 0; - } - for (let i = seen; i < all.length; i++) { - subject.next(toAgentEvent(all[i])); - } - seen = all.length; - }); - return subject.asObservable(); -} - -function toAgentEvent(e: CustomStreamEvent): AgentEvent { - if (e.name === 'state_update' && isRecord(e.data)) { - return { type: 'state_update', data: e.data }; - } - return { type: 'custom', name: e.name, data: e.data }; -} - -function mapStatus(s: ResourceStatus): AgentStatus { - switch (s) { - case ResourceStatus.Error: return 'error'; - case ResourceStatus.Loading: - case ResourceStatus.Reloading: - return 'running'; - default: - return 'idle'; - } -} - -function toMessage(m: BaseMessage): Message { - const raw = m as unknown as Record; - const typeVal = typeof m._getType === 'function' - ? m._getType() - : (raw['type'] as string | undefined) ?? 'ai'; - const role: Role = - typeVal === 'human' ? 'user' : - typeVal === 'tool' ? 'tool' : - typeVal === 'system' ? 'system' : - 'assistant'; - return { - id: (m.id as string | undefined) ?? (raw['id'] as string | undefined) ?? randomId(), - role, - content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content), - toolCallId: raw['tool_call_id'] as string | undefined, - name: raw['name'] as string | undefined, - extra: raw, - }; -} - -function toToolCall(tc: ToolCallWithResult): ToolCall { - const stateMap: Record = { - pending: 'pending', - completed: 'complete', - error: 'error', - }; - const status: ToolCallStatus = stateMap[tc.state] ?? 'running'; - const result = tc.result as (Record | undefined); - return { - id: tc.id, - name: tc.call.name, - args: tc.call.args, - status, - result: result?.['content'], - error: tc.state === 'error' ? result?.['content'] : undefined, - }; -} - -function toInterrupt(ix: Interrupt): AgentInterrupt { - const raw = ix as unknown as Record; - return { - id: (raw['id'] as string | undefined) ?? randomId(), - value: raw['value'] ?? ix, - resumable: true, - }; -} - -function toSubagent(sa: SubagentStreamRef): Subagent { - return { - toolCallId: sa.toolCallId, - status: sa.status, - messages: computed(() => sa.messages().map(toMessage)) as Signal, - state: sa.values as Signal>, - }; -} - -function buildSubmitPayload(input: AgentSubmitInput): unknown { - if (input.resume !== undefined) return { __resume__: input.resume }; - if (input.message !== undefined) { - const content = typeof input.message === 'string' - ? input.message - : input.message.map((b: any) => (b.type === 'text' ? b.text : JSON.stringify(b))).join(''); - return { messages: [{ role: 'human', content }], ...(input.state ?? {}) }; - } - return input.state ?? {}; -} - -function randomId(): string { - return Math.random().toString(36).slice(2); -} - -function toCheckpoint(state: ThreadState): AgentCheckpoint { - return { - id: state.checkpoint?.checkpoint_id ?? undefined, - label: state.next?.[0] ?? undefined, - values: isRecord(state.values) ? state.values : {}, - }; -} - -function isRecord(v: unknown): v is Record { - return typeof v === 'object' && v !== null && !Array.isArray(v); -} diff --git a/libs/langgraph/src/public-api.ts b/libs/langgraph/src/public-api.ts index a88185df8..72b748692 100644 --- a/libs/langgraph/src/public-api.ts +++ b/libs/langgraph/src/public-api.ts @@ -9,7 +9,7 @@ export type { AgentConfig } from './lib/agent.provider'; // Public types export type { AgentOptions, - AgentRef, + LangGraphAgent, AgentTransport, CustomStreamEvent, StreamEvent, @@ -23,13 +23,10 @@ export type { BagTemplate, InferBag, Interrupt, ThreadState, SubmitOptions } // Re-export ResourceStatus shim for convenience export { ResourceStatus } from './lib/agent.types'; -// Chat adapter -export { toAgent } from './lib/to-agent'; - // Test utilities (always exported — tree-shaken in prod builds) export { MockAgentTransport } from './lib/transport/mock-stream.transport'; export { FetchStreamTransport } from './lib/transport/fetch-stream.transport'; -// Mock test utility for LangGraph AgentRef -export { createMockAgentRef } from './lib/testing/mock-agent-ref'; -export type { MockAgentRef } from './lib/testing/mock-agent-ref'; +// Mock test utility for LangGraph agent +export { mockLangGraphAgent } from './lib/testing/mock-langgraph-agent'; +export type { MockLangGraphAgent } from './lib/testing/mock-langgraph-agent';