diff --git a/AGENTS.md b/AGENTS.md index 37cca81b6..bf3117de0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,6 @@ This file is for agents working in this repository. It is contributor-facing, no - `libs/langgraph`: main Angular library (`@ngaf/langgraph`). - `apps/website`: docs and marketing site. -- `packages/mcp`: MCP server package (`@ngaf/langgraph-mcp`). - `e2e/agent-e2e`: end-to-end coverage for the workspace. ## Working in This Repo diff --git a/README.md b/README.md index d622f51f0..35c026eb2 100644 --- a/README.md +++ b/README.md @@ -87,11 +87,11 @@ That's it. `chat.messages()` and `chat.status()` are Angular Signals. Bind them | Error state | `error()` | — | | Runtime-neutral status | `status()` — `'idle' \| 'running' \| 'error'` | partial | | Interrupt / human-in-the-loop | `interrupt()` / `interrupts()` | `interrupt` / `interrupts` | -| Tool call progress | `toolProgress()` | `toolProgress` | +| Tool call progress | `toolCalls()` | `toolCalls` | | Tool calls with results | `toolCalls()` | `toolCalls` | | Branch / history | `branch()` / `history()` / `experimentalBranchTree()` | `branch` / `history` / `experimental_branchTree` | | Pending run queue | `queue()` | `queue` | -| Subagent streaming and lookup helpers | `subagents()` / `activeSubagents()` / `getSubagent()` | `subagents` / `activeSubagents` / helper methods | +| Subagent streaming and lookup helpers | `subagents()` / `getSubagent()` | `subagents` / helper methods | | Reactive thread switching | `Signal` input | prop | | Submit | `submit(values, opts?)` | `submit(values, opts?)` | | Stop | `stop()` | `stop()` | diff --git a/apps/website/content/docs/agent/api/api-docs.json b/apps/website/content/docs/agent/api/api-docs.json index 7974a5992..7c7ac9bdc 100644 --- a/apps/website/content/docs/agent/api/api-docs.json +++ b/apps/website/content/docs/agent/api/api-docs.json @@ -211,7 +211,7 @@ } ], "examples": [ - "```typescript\nconst transport = new MockAgentTransport([\n [{ type: 'values', data: { messages: [aiMsg('Hello')] } }],\n [{ type: 'values', data: { status: 'done' } }],\n]);\n```" + "```typescript\nconst transport = new MockAgentTransport([\n [{ type: 'values', messages: [aiMsg('Hello')] }],\n [{ type: 'values', messages: [aiMsg('Done')] }],\n]);\n```" ], "properties": [ { @@ -405,7 +405,7 @@ { "name": "nextBatch", "signature": "nextBatch()", - "description": "Advance to the next scripted batch and return its events.", + "description": "Advance to the next scripted batch. Pass the returned events to `emit()`.", "params": [] }, { @@ -1661,7 +1661,7 @@ "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```" + "```typescript\n// In a component field initializer\nconst chat = agent({\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```" ] }, { diff --git a/apps/website/content/docs/agent/concepts/agent-architecture.mdx b/apps/website/content/docs/agent/concepts/agent-architecture.mdx index 0f9307d11..4ba9f1ba8 100644 --- a/apps/website/content/docs/agent/concepts/agent-architecture.mdx +++ b/apps/website/content/docs/agent/concepts/agent-architecture.mdx @@ -179,11 +179,13 @@ export class ReactAgentComponent { messages = this.agent.messages; - // Tools currently executing (spinner, progress bar) - activeTools = computed(() => this.agent.toolProgress()); - - // Tools that finished with results (expandable cards) - completedTools = computed(() => this.agent.toolCalls()); + // Tool calls with status, args, and result data + activeTools = computed(() => + this.agent.toolCalls().filter((tool) => tool.status === 'running') + ); + completedTools = computed(() => + this.agent.toolCalls().filter((tool) => tool.status === 'complete') + ); send(text: string) { this.agent.submit({ message: text }); @@ -233,21 +235,23 @@ The LLM reads the docstring to decide when to call a tool. A vague docstring lik ### How Tools Surface in Angular -When the agent calls a tool, agent() exposes the execution lifecycle through two signals: +When the agent calls a tool, agent() exposes the execution lifecycle through `toolCalls()`: - + ```typescript -// toolProgress() — tools currently executing +// toolCalls() — tool calls with status, args, and results // Updates in real time as tools start and complete const agent = agent({ assistantId: 'react_agent', }); -// Each entry has: name, args, status -const activeTools = computed(() => agent.toolProgress()); +// Each entry has: id, name, args, status, and optional result +const activeTools = computed(() => + agent.toolCalls().filter((tool) => tool.status === 'running') +); // Template usage @Component({ @@ -263,18 +267,19 @@ const activeTools = computed(() => agent.toolProgress()); `, }) export class ToolProgressComponent { - activeTools = computed(() => this.agent.toolProgress()); + activeTools = computed(() => + this.agent.toolCalls().filter((tool) => tool.status === 'running') + ); } ``` - + ```typescript -// toolCalls() — completed tool calls with results -// Available after each tool finishes - -const completedTools = computed(() => agent.toolCalls()); +const completedTools = computed(() => + agent.toolCalls().filter((tool) => tool.status === 'complete') +); // Each entry has: name, args, result, duration @Component({ @@ -324,7 +329,7 @@ The `should_continue` conditional edge detects `tool_calls` and routes to the `t LangGraph Platform streams the tool call and result as SSE events to the Angular client. -`toolProgress()` updates during execution. `toolCalls()` updates when the tool completes. Both trigger OnPush change detection. +`toolCalls()` updates as the tool moves through pending, running, complete, and error states. Each update triggers OnPush change detection. @@ -442,10 +447,14 @@ export class MultiAgentComponent { messages = this.orchestrator.messages; // Currently running delegated work with live status - activeTools = computed(() => this.orchestrator.toolProgress()); + activeTools = computed(() => + this.orchestrator.toolCalls().filter((tool) => tool.status === 'running') + ); // Completed tool calls with results - completedTools = computed(() => this.orchestrator.toolCalls()); + completedTools = computed(() => + this.orchestrator.toolCalls().filter((tool) => tool.status === 'complete') + ); send(text: string) { this.orchestrator.submit({ message: text }); @@ -518,7 +527,7 @@ export class AgentComponent { |---|---|---| | Tool throws `ToolException` | Error fed back to LLM, agent retries | `toolCalls()` shows error in result | | Tool throws unexpected error | LangGraph catches it, marks tool as failed | `error()` fires with details | -| LLM returns invalid tool args | ToolNode validation fails, error fed to LLM | `toolProgress()` shows failed status | +| LLM returns invalid tool args | ToolNode validation fails, error fed to LLM | `toolCalls()` shows failed status | | Transport error (network) | N/A | `error()` fires, `status()` becomes `'error'` | | Agent exceeds recursion limit | Graph raises `GraphRecursionError` | `error()` fires with recursion message | @@ -594,7 +603,7 @@ export class DebugTimelineComponent { timeTravel(checkpointId: string) { this.currentCheckpoint.set(checkpointId); - this.agent.submit(null, { checkpointId }); + this.agent.submit({}, { checkpointId }); } } ``` @@ -622,7 +631,7 @@ builder.add_edge("tools", "model") graph = builder.compile() ``` -**Angular signals used:** `messages()`, `toolCalls()`, `toolProgress()`, `status()` +**Angular signals used:** `messages()`, `toolCalls()`, `status()` ### Single Agent with Human-in-the-Loop @@ -640,7 +649,7 @@ def execute_action(state: AgentState) -> dict: return perform_action(state["pending_action"]) ``` -**Angular signals used:** `messages()`, `interrupt()`, `status()` plus `submit(null, { resume })` to approve +**Angular signals used:** `messages()`, `interrupt()`, `status()` plus `submit({ resume })` to approve ### Multi-Agent Supervisor @@ -654,7 +663,7 @@ builder.add_node("analyst", analyst_subgraph) builder.add_conditional_edges("supervisor", route_to_agent) ``` -**Angular signals used:** `messages()`, `subagents()`, `activeSubagents()`, `toolCalls()`, `toolProgress()`, `status()` +**Angular signals used:** `messages()`, `subagents()`, `toolCalls()`, `status()` ### Decision Matrix diff --git a/apps/website/content/docs/agent/concepts/angular-signals.mdx b/apps/website/content/docs/agent/concepts/angular-signals.mdx index cc49199f7..5d9fc04d5 100644 --- a/apps/website/content/docs/agent/concepts/angular-signals.mdx +++ b/apps/website/content/docs/agent/concepts/angular-signals.mdx @@ -41,7 +41,7 @@ Under the hood, agent() receives Server-Sent Events (SSE) over HTTP and feeds th // Simplified view of what agent does internally: // 1. SSE events arrive as an observable stream -const messages$ = new BehaviorSubject([]); +const messages$ = new BehaviorSubject([]); const status$ = new BehaviorSubject('idle'); // 2. Each SSE chunk updates the BehaviorSubject @@ -68,8 +68,8 @@ const chat = agent({ assistantId: 'chat_agent', }); -chat.messages(); // Signal -chat.status(); // Signal +chat.messages(); // Signal +chat.status(); // Signal<'idle' | 'running' | 'error'> chat.error(); // Signal chat.isLoading(); // Signal chat.value(); // Signal @@ -84,7 +84,7 @@ The BehaviorSubject-to-Signal conversion means you get the best of both worlds: ## The Streaming Lifecycle as Signals -Every agent() instance moves through a lifecycle: **idle**, **loading**, tokens arriving, then **resolved** (or **error**). The `status()` Signal reflects each transition in real time. +Every agent() instance moves through a lifecycle: **idle**, **running** while work is in flight, then back to **idle** when the stream completes (or **error** when it fails). The `status()` Signal reflects each transition in real time, while `isLoading()` is the convenience signal for loading UI. @@ -101,39 +101,39 @@ console.log(chat.isLoading()); // false ``` - -After calling `submit()`, the status transitions to `'loading'`. The SSE connection is open and the agent is processing. + +After calling `submit()`, the status transitions to `'running'`. The SSE connection is open and the agent is processing. ```typescript chat.submit({ message: 'Explain quantum computing' }); -console.log(chat.status()); // 'loading' +console.log(chat.status()); // 'running' console.log(chat.isLoading()); // true console.log(chat.messages()); // [] (no tokens yet) ``` - -As the agent generates tokens, the `messages()` Signal updates with each chunk. The status remains `'loading'` throughout. + +As the agent generates tokens, the `messages()` Signal updates with each chunk. The status remains `'running'` throughout. ```typescript // After first few tokens arrive: -console.log(chat.status()); // 'loading' (still streaming) -console.log(chat.messages()); // [AIMessageChunk("Quantum computing uses...")] +console.log(chat.status()); // 'running' (still streaming) +console.log(chat.messages()); // [{ role: 'assistant', content: 'Quantum computing uses...' }] // After more tokens: -console.log(chat.messages()); // [AIMessageChunk("Quantum computing uses qubits...")] +console.log(chat.messages()); // [{ role: 'assistant', content: 'Quantum computing uses qubits...' }] // The message content grows as tokens stream in ``` - -The agent has finished. All tokens have arrived. The status transitions to `'resolved'`. + +The agent has finished. All tokens have arrived. The status transitions back to `'idle'`. ```typescript -console.log(chat.status()); // 'resolved' +console.log(chat.status()); // 'idle' console.log(chat.isLoading()); // false -console.log(chat.messages()); // [AIMessage("Quantum computing uses qubits to...")] +console.log(chat.messages()); // [{ role: 'assistant', content: 'Quantum computing uses qubits to...' }] ``` @@ -168,16 +168,11 @@ const lastMessage = computed(() => chat.messages().at(-1)); // Extract just the assistant's messages const assistantMessages = computed(() => - chat.messages().filter(m => m._getType() === 'ai') + chat.messages().filter(m => m.role === 'assistant') ); // Track which tools the agent is actively calling -const activeTools = computed(() => - chat.messages() - .filter(m => m._getType() === 'ai') - .flatMap(m => m.tool_calls ?? []) - .filter(tc => !tc.result) -); +const activeTools = computed(() => chat.toolCalls()); // Build a user-facing error message const errorDisplay = computed(() => { @@ -195,7 +190,7 @@ const errorDisplay = computed(() => { const viewModel = computed(() => ({ messages: chat.messages(), isStreaming: chat.isLoading(), - canSend: chat.status() !== 'loading', + canSend: !chat.isLoading(), messageCount: messageCount(), error: errorDisplay(), })); @@ -238,10 +233,10 @@ effect(() => { // Track streaming duration for performance monitoring effect(() => { const status = chat.status(); - if (status === 'loading') { + if (status === 'running') { this.streamStart = performance.now(); } - if (status === 'resolved' && this.streamStart) { + if (status === 'idle' && this.streamStart) { const duration = performance.now() - this.streamStart; this.analytics.track('stream_duration_ms', { duration }); this.streamStart = null; @@ -267,7 +262,7 @@ import { agent } from '@ngaf/langgraph'; template: ` @switch (chat.status()) { - @case ('loading') { + @case ('running') {
Agent is responding...
@@ -283,18 +278,18 @@ import { agent } from '@ngaf/langgraph';
@for (message of chat.messages(); track $index) { - @switch (message._getType()) { - @case ('human') { + @switch (message.role) { + @case ('user') {
{{ message.content }}
} - @case ('ai') { + @case ('assistant') {
{{ message.content }} - @for (tool of message.tool_calls ?? []; track tool.id) { + @for (tool of chat.toolCalls(); track tool.id) {
Called: {{ tool.name }}
@@ -460,14 +455,14 @@ import { agent } from '@ngaf/langgraph'; changeDetection: ChangeDetectionStrategy.OnPush, template: ` @for (msg of chat.messages(); track $index) { - @switch (msg._getType()) { - @case ('human') { + @switch (msg.role) { + @case ('user') {
{{ msg.content }}
} - @case ('ai') { + @case ('assistant') {
{{ msg.content }} - @for (tc of msg.tool_calls ?? []; track tc.id) { + @for (tc of chat.toolCalls(); track tc.id) { {{ tc.name }} }
@@ -491,7 +486,7 @@ export class ChatComponent { // Derived state from the Python agent's output toolsUsed = computed(() => this.chat.messages() - .filter(m => m._getType() === 'tool') + .filter(m => m.role === 'tool') .map(m => m.name) ); diff --git a/apps/website/content/docs/agent/concepts/langgraph-basics.mdx b/apps/website/content/docs/agent/concepts/langgraph-basics.mdx index b78bc25c0..639001c51 100644 --- a/apps/website/content/docs/agent/concepts/langgraph-basics.mdx +++ b/apps/website/content/docs/agent/concepts/langgraph-basics.mdx @@ -112,12 +112,12 @@ graph = builder.compile() ```typescript // This is all you need on the Angular side -const chat = agent<{ messages: BaseMessage[] }>({ +const chat = agent({ assistantId: 'chat_agent', }); // chat.messages() updates as the agent streams its response -// chat.status() tells you if it's idle, loading, or done +// chat.status() tells you if it's idle, running, or errored ``` @@ -167,7 +167,7 @@ const agent = agent({ }); // Watch tools execute -const activeTools = computed(() => agent.toolProgress()); +const activeTools = computed(() => agent.toolCalls()); const completedTools = computed(() => agent.toolCalls()); ``` @@ -204,7 +204,7 @@ const pendingAction = computed(() => agent.interrupt()); // User clicks approve → resume the agent approve() { - agent.submit(null, { resume: { approved: true } }); + agent.submit({ resume: { approved: true } }); } ``` @@ -235,7 +235,7 @@ const orchestrator = agent({ filterSubagentMessages: true, }); -const workers = computed(() => orchestrator.activeSubagents()); +const workers = computed(() => [...orchestrator.subagents().values()]); const workerCount = computed(() => workers().length); ``` @@ -309,10 +309,10 @@ Templates re-render automatically via OnPush change detection agent.value() // Signal — full state object // Conversation -agent.messages() // Signal — message history +agent.messages() // Signal — runtime-neutral message history // Lifecycle -agent.status() // Signal — idle/loading/done +agent.status() // Signal — idle/running/error agent.isLoading() // Signal — is the agent running? // Human-in-the-loop @@ -324,11 +324,9 @@ agent.langGraphHistory() // Signal — raw LangGraph checkpoints agent.branch() // Signal — time-travel branch agent.experimentalBranchTree() // Signal> — branch tree -agent.toolCalls() // Signal — tool results -agent.toolProgress() // Signal — active tool execution +agent.toolCalls() // Signal — tool call progress and results agent.queue() // Signal — pending enqueue runs agent.subagents() // Signal> — delegated agents -agent.activeSubagents() // Signal — running workers ``` diff --git a/apps/website/content/docs/agent/concepts/state-management.mdx b/apps/website/content/docs/agent/concepts/state-management.mdx index 3bcfe4f00..a0414c0a8 100644 --- a/apps/website/content/docs/agent/concepts/state-management.mdx +++ b/apps/website/content/docs/agent/concepts/state-management.mdx @@ -199,8 +199,8 @@ const score = computed(() => agent.value().analysis?.score ?? 0); const fileCount = computed(() => agent.value().files.length); const isDone = computed(() => agent.value().progress === 100); -// Direct messages access (shortcut for agent.value().messages) -const messages = agent.messages(); // Signal +// Runtime-neutral message projection for UI +const messages = agent.messages(); // Signal ``` ## State Updates During Streaming @@ -294,9 +294,9 @@ export class ChatComponent { }); // Convenience computed values from thread state - readonly messages = this.agent.messages; // Signal + readonly messages = this.agent.messages; // Signal readonly isLoading = this.agent.isLoading; // Signal - readonly interrupted = this.agent.interrupt; // Signal + readonly interrupted = this.agent.interrupt; // Signal // --- Application state (your Angular signals) --- readonly sidebarOpen = signal(true); @@ -313,7 +313,7 @@ export class ChatComponent { } approve() { - this.agent.submit(null, { resume: { approved: true } }); + this.agent.submit({ resume: { approved: true } }); } } ``` diff --git a/apps/website/content/docs/agent/getting-started/introduction.mdx b/apps/website/content/docs/agent/getting-started/introduction.mdx index afc1f58b0..9eb8b0560 100644 --- a/apps/website/content/docs/agent/getting-started/introduction.mdx +++ b/apps/website/content/docs/agent/getting-started/introduction.mdx @@ -11,17 +11,18 @@ This guide walks you through the complete workflow: build a LangGraph agent in P `agent()` is an Angular function that creates a reactive, streaming connection to a LangGraph agent. It returns an object whose properties are Angular Signals — meaning your templates update automatically as the agent streams responses, token by token. ```typescript -const chat = agent<{ messages: BaseMessage[] }>({ +const chat = agent({ assistantId: 'chat_agent', }); // Every property is a Signal — reactive, synchronous, no subscriptions -chat.messages() // Signal -chat.status() // Signal<'idle' | 'loading' | 'resolved' | 'error'> -chat.error() // Signal -chat.interrupt() // Signal -chat.history() // Signal -chat.langGraphHistory() // Signal +chat.messages(); // Message[] +chat.status(); // 'idle' | 'running' | 'error' +chat.isLoading(); // boolean +chat.error(); // unknown +chat.interrupt?.(); // AgentInterrupt | undefined +chat.history(); // AgentCheckpoint[] +chat.langGraphHistory(); // ThreadState[] ``` No RxJS. No manual subscriptions. No async pipes. Just Signals that work with Angular's `OnPush` change detection out of the box. @@ -155,9 +156,8 @@ export const appConfig: ApplicationConfig = { ```typescript // chat.component.ts -import { Component, signal, computed } from '@angular/core'; +import { ChangeDetectionStrategy, Component, signal, computed } from '@angular/core'; import { agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; @Component({ selector: 'app-chat', @@ -167,15 +167,14 @@ import type { BaseMessage } from '@langchain/core/messages'; export class ChatComponent { input = signal(''); - // Create the streaming resource — this is the core API - chat = agent<{ messages: BaseMessage[] }>({ + chat = agent({ assistantId: 'chat_agent', threadId: signal(localStorage.getItem('threadId')), onThreadId: (id) => localStorage.setItem('threadId', id), }); // Derived signals — compose with computed() - isStreaming = computed(() => this.chat.status() === 'loading'); + isStreaming = computed(() => this.chat.isLoading()); messageCount = computed(() => this.chat.messages().length); send() { diff --git a/apps/website/content/docs/agent/getting-started/quickstart.mdx b/apps/website/content/docs/agent/getting-started/quickstart.mdx index 9dabdcb83..de19d6134 100644 --- a/apps/website/content/docs/agent/getting-started/quickstart.mdx +++ b/apps/website/content/docs/agent/getting-started/quickstart.mdx @@ -43,7 +43,6 @@ Use `agent()` in a component field initializer. Every property on the returned r // chat.component.ts import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core'; import { agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; @Component({ selector: 'app-chat', @@ -54,13 +53,13 @@ export class ChatComponent { input = signal(''); // 'chat_agent' maps to the key in your langgraph.json "graphs" config - chat = agent<{ messages: BaseMessage[] }>({ + chat = agent({ assistantId: 'chat_agent', threadId: signal(localStorage.getItem('threadId')), onThreadId: (id) => localStorage.setItem('threadId', id), }); - isStreaming = computed(() => this.chat.status() === 'loading'); + isStreaming = computed(() => this.chat.isLoading()); send() { const msg = this.input(); diff --git a/apps/website/content/docs/agent/guides/deployment.mdx b/apps/website/content/docs/agent/guides/deployment.mdx index 9c26a0494..242da1dc4 100644 --- a/apps/website/content/docs/agent/guides/deployment.mdx +++ b/apps/website/content/docs/agent/guides/deployment.mdx @@ -355,8 +355,8 @@ Key metrics to track in production: Track stream health from your Angular app: ```typescript -const status = this.chat.status(); // 'idle' | 'streaming' | 'error' -const isStreaming = this.chat.isStreaming(); +const status = this.chat.status(); // 'idle' | 'running' | 'error' +const isStreaming = this.chat.isLoading(); // Log stream lifecycle for your APM tool effect(() => { diff --git a/apps/website/content/docs/agent/guides/interrupts.mdx b/apps/website/content/docs/agent/guides/interrupts.mdx index 08309de6b..6b19b50e8 100644 --- a/apps/website/content/docs/agent/guides/interrupts.mdx +++ b/apps/website/content/docs/agent/guides/interrupts.mdx @@ -143,7 +143,8 @@ import { signal, ChangeDetectionStrategy, } from '@angular/core'; -import { agent, BaseMessage } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; +import type { BaseMessage } from '@langchain/core/messages'; interface ApprovalPayload { action: string; @@ -186,13 +187,13 @@ export class ApprovalComponent { } approve() { - this.agent.submit(null, { + this.agent.submit({ resume: { approved: true }, }); } reject() { - this.agent.submit(null, { + this.agent.submit({ resume: { approved: false, reason: this.rejectionReason() || 'User rejected', @@ -352,7 +353,8 @@ import { computed, ChangeDetectionStrategy, } from '@angular/core'; -import { agent, BaseMessage } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; +import type { BaseMessage } from '@langchain/core/messages'; interface StepApproval { step_number: number; @@ -391,11 +393,11 @@ export class DeployApprovalComponent { allInterrupts = computed(() => this.agent.interrupts()); approveStep() { - this.agent.submit(null, { resume: { approved: true } }); + this.agent.submit({ resume: { approved: true } }); } abortDeploy() { - this.agent.submit(null, { + this.agent.submit({ resume: { approved: false, reason: 'Deployment aborted by user' }, }); } @@ -523,7 +525,7 @@ effect(() => { if (interrupt) { const sub = timer(5 * 60 * 1000).subscribe(() => { // Auto-reject after 5 minutes of inaction - this.agent.submit(null, { + this.agent.submit({ resume: { approved: false, reason: 'Approval timeout' }, }); }); diff --git a/apps/website/content/docs/agent/guides/memory.mdx b/apps/website/content/docs/agent/guides/memory.mdx index 371ed8be0..ad8ea128c 100644 --- a/apps/website/content/docs/agent/guides/memory.mdx +++ b/apps/website/content/docs/agent/guides/memory.mdx @@ -75,7 +75,8 @@ graph = builder.compile() ```typescript import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { agent, BaseMessage } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; +import type { BaseMessage } from '@langchain/core/messages'; interface AgentState { messages: BaseMessage[]; @@ -265,7 +266,8 @@ graph = builder.compile() ```typescript import { Component, computed, signal, ChangeDetectionStrategy } from '@angular/core'; -import { agent, BaseMessage } from '@ngaf/langgraph'; +import { agent } from '@ngaf/langgraph'; +import type { BaseMessage } from '@langchain/core/messages'; @Component({ selector: 'app-longterm-chat', @@ -275,7 +277,7 @@ import { agent, BaseMessage } from '@ngaf/langgraph'; export class LongTermChatComponent { // Each conversation gets a new thread, but the agent // remembers the user across all of them via the Store. - agent = agent<{ messages: BaseMessage[] }>({ + agent = agent({ assistantId: 'memory_agent', config: { configurable: { user_id: 'user_42' } }, }); diff --git a/apps/website/content/docs/agent/guides/persistence.mdx b/apps/website/content/docs/agent/guides/persistence.mdx index 90773a2d3..6386ebeb6 100644 --- a/apps/website/content/docs/agent/guides/persistence.mdx +++ b/apps/website/content/docs/agent/guides/persistence.mdx @@ -119,7 +119,7 @@ import { agent } from '@ngaf/langgraph'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChatComponent { - chat = agent<{ messages: BaseMessage[] }>({ + chat = agent({ assistantId: 'chat_agent', // Restore thread from localStorage on mount threadId: signal(localStorage.getItem('threadId')), @@ -178,7 +178,7 @@ export class ThreadListComponent { threads = signal(this.loadThreads()); activeThreadId = signal(null); - chat = agent<{ messages: BaseMessage[] }>({ + chat = agent({ assistantId: 'chat_agent', threadId: this.activeThreadId, onThreadId: (id) => { @@ -257,7 +257,7 @@ When you pass a Signal as `threadId`, agent() reacts to every change. Set the si ```typescript activeThreadId = signal(null); -chat = agent<{ messages: BaseMessage[] }>({ +chat = agent({ assistantId: 'chat_agent', threadId: this.activeThreadId, // Signal — switches reactively onThreadId: (id) => this.activeThreadId.set(id), diff --git a/apps/website/content/docs/agent/guides/streaming.mdx b/apps/website/content/docs/agent/guides/streaming.mdx index 64d7e8d62..71b9a28f9 100644 --- a/apps/website/content/docs/agent/guides/streaming.mdx +++ b/apps/website/content/docs/agent/guides/streaming.mdx @@ -60,7 +60,6 @@ async for event in graph.astream_events( ```typescript import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; import { agent } from '@ngaf/langgraph'; -import { BaseMessage } from '@langchain/core/messages'; @Component({ selector: 'app-chat', @@ -68,11 +67,11 @@ import { BaseMessage } from '@langchain/core/messages'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChatComponent { - readonly chat = agent<{ messages: BaseMessage[] }>({ + readonly chat = agent({ assistantId: 'chat_agent', }); - readonly isStreaming = computed(() => this.chat.status() === 'loading'); + readonly isStreaming = computed(() => this.chat.isLoading()); send(text: string) { this.chat.submit({ message: text }); @@ -108,7 +107,7 @@ The `status()` signal reports the current lifecycle state of the SSE connection: No active stream. The resource is ready to accept a new message. - + Tokens are arriving over the SSE connection. Signal values update in real-time with each chunk. @@ -126,7 +125,7 @@ LangGraph supports three stream modes. Pass `streamMode` to control what each SS ```typescript // Receives the full agent state after every node execution. // Best for message-based chat interfaces. -const chat = agent<{ messages: BaseMessage[] }>({ +const chat = agent({ assistantId: 'chat_agent', streamMode: 'values', }); @@ -140,7 +139,7 @@ const chat = agent<{ messages: BaseMessage[] }>({ ```typescript // Streams individual message tokens as they are generated. // Best for token-by-token rendering with lowest perceived latency. -const chat = agent<{ messages: BaseMessage[] }>({ +const chat = agent({ assistantId: 'chat_agent', streamMode: 'messages', }); @@ -152,7 +151,7 @@ const chat = agent<{ messages: BaseMessage[] }>({ ```typescript // Emits raw LangGraph run events (on_chain_start, on_llm_stream, etc.). // Best for advanced observability or custom progress indicators. -const chat = agent<{ messages: BaseMessage[] }>({ +const chat = agent({ assistantId: 'chat_agent', streamMode: 'events', }); @@ -175,7 +174,6 @@ If the SSE connection drops or the agent throws, `status()` transitions to `'err ```typescript import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; import { agent } from '@ngaf/langgraph'; -import { BaseMessage } from '@langchain/core/messages'; @Component({ selector: 'app-chat', @@ -183,7 +181,7 @@ import { BaseMessage } from '@langchain/core/messages'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChatComponent { - readonly chat = agent<{ messages: BaseMessage[] }>({ + readonly chat = agent({ assistantId: 'chat_agent', }); @@ -220,7 +218,7 @@ export class ChatComponent { By default Agent emits a signal update for every incoming SSE chunk. On fast connections this can trigger hundreds of renders per second. Use the `throttle` option to coalesce updates. ```typescript -const chat = agent<{ messages: BaseMessage[] }>({ +const chat = agent({ assistantId: 'chat_agent', // Batch incoming chunks and flush at most once every 50 ms throttle: 50, diff --git a/apps/website/content/docs/agent/guides/subgraphs.mdx b/apps/website/content/docs/agent/guides/subgraphs.mdx index 50382a966..74342a37f 100644 --- a/apps/website/content/docs/agent/guides/subgraphs.mdx +++ b/apps/website/content/docs/agent/guides/subgraphs.mdx @@ -88,7 +88,11 @@ export class OrchestratorComponent { subagentToolNames: ['research', 'analyze'], }); - readonly running = computed(() => this.orchestrator.activeSubagents()); + readonly running = computed(() => + [...this.orchestrator.subagents().values()].filter((subagent) => + subagent.status() === 'pending' || subagent.status() === 'running' + ) + ); readonly runningCount = computed(() => this.running().length); send(text: string) { @@ -114,7 +118,11 @@ const orchestrator = agent({ const subagents = computed(() => orchestrator.subagents()); // Only active ones -const running = computed(() => orchestrator.activeSubagents()); +const running = computed(() => + [...orchestrator.subagents().values()].filter((subagent) => + subagent.status() === 'pending' || subagent.status() === 'running' + ) +); const runningCount = computed(() => running().length); // Lookup helpers for common UI paths @@ -167,7 +175,7 @@ const pipelineStatus = computed(() => { return { total: entries.length, pending: entries.filter(([, a]) => a.status() === 'pending').length, - running: entries.filter(([, a]) => a.status() === 'loading').length, + running: entries.filter(([, a]) => a.status() === 'running').length, done: entries.filter(([, a]) => a.status() === 'complete').length, failed: entries.filter(([, a]) => a.status() === 'error').length, }; @@ -206,7 +214,7 @@ export class SubagentProgressComponent { {{ entry[1].status() }} - @if (entry[1].status() === 'loading') { + @if (entry[1].status() === 'running') { } diff --git a/apps/website/content/docs/agent/guides/testing.mdx b/apps/website/content/docs/agent/guides/testing.mdx index 795fb6935..0a3cc1c51 100644 --- a/apps/website/content/docs/agent/guides/testing.mdx +++ b/apps/website/content/docs/agent/guides/testing.mdx @@ -49,14 +49,13 @@ On the Angular side, MockAgentTransport replaces the real HTTP transport. Create ```typescript import { TestBed } from '@angular/core/testing'; import { MockAgentTransport, agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@ngaf/langgraph'; describe('ChatComponent', () => { it('should display agent messages', () => { const transport = new MockAgentTransport(); TestBed.runInInjectionContext(() => { - const chat = agent<{ messages: BaseMessage[] }>({ + const chat = agent({ assistantId: 'test_agent', transport, }); @@ -89,7 +88,7 @@ import { agent } from '@ngaf/langgraph'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChatComponent { - chat = agent<{ messages: BaseMessage[] }>({ + chat = agent({ assistantId: 'chat_agent', }); @@ -104,7 +103,7 @@ export class ChatComponent { ## Scripted Event Sequences -Pass event batches to the constructor for sequential playback. Each call to `nextBatch()` advances one step — giving you frame-by-frame control over what the component sees. +Pass event batches to the constructor for sequential playback. Each call to `nextBatch()` returns one batch; emit that batch to advance what the component sees. ```typescript const transport = new MockAgentTransport([ @@ -115,7 +114,7 @@ const transport = new MockAgentTransport([ ]); TestBed.runInInjectionContext(() => { - const chat = agent<{ messages: BaseMessage[] }>({ + const chat = agent({ assistantId: 'test_agent', transport, }); @@ -123,17 +122,17 @@ TestBed.runInInjectionContext(() => { chat.submit({ message: 'Explain signals' }); // Step through each batch - transport.nextBatch(); + transport.emit(transport.nextBatch()); expect(chat.messages()[0].content).toBe('Analyzing...'); - transport.nextBatch(); + transport.emit(transport.nextBatch()); expect(chat.messages()[0].content).toBe('Here is your answer.'); }); ``` ## Testing the Streaming Lifecycle -The most common test pattern verifies the full submit-to-resolved lifecycle: submit triggers loading, values arrive, and the status settles to resolved. +The most common test pattern verifies the full submit-to-idle lifecycle: submit sets the agent running, values arrive, and the status settles back to idle. @@ -143,14 +142,14 @@ import { TestBed } from '@angular/core/testing'; import { MockAgentTransport, agent } from '@ngaf/langgraph'; describe('streaming lifecycle', () => { - it('should transition through loading → values → resolved', () => { + it('should transition through running → values → idle', () => { const transport = new MockAgentTransport([ [{ type: 'values', messages: [{ role: 'assistant', content: 'Thinking...' }] }], [{ type: 'values', messages: [{ role: 'assistant', content: 'Done!' }] }], ]); TestBed.runInInjectionContext(() => { - const chat = agent<{ messages: BaseMessage[] }>({ + const chat = agent({ assistantId: 'test_agent', transport, }); @@ -159,23 +158,23 @@ describe('streaming lifecycle', () => { expect(chat.status()).toBe('idle'); expect(chat.messages()).toEqual([]); - // Submit triggers loading + // Submit triggers running chat.submit({ message: 'Hello' }); - expect(chat.status()).toBe('loading'); + expect(chat.status()).toBe('running'); expect(chat.isLoading()).toBe(true); // First batch — partial response - transport.nextBatch(); + transport.emit(transport.nextBatch()); expect(chat.messages()[0].content).toBe('Thinking...'); - expect(chat.status()).toBe('loading'); + expect(chat.status()).toBe('running'); // Second batch — final response - transport.nextBatch(); + transport.emit(transport.nextBatch()); expect(chat.messages()[0].content).toBe('Done!'); // Stream completes - transport.complete(); - expect(chat.status()).toBe('resolved'); + transport.close(); + expect(chat.status()).toBe('idle'); expect(chat.isLoading()).toBe(false); }); }); @@ -202,7 +201,7 @@ import { agent } from '@ngaf/langgraph'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChatComponent { - chat = agent<{ messages: BaseMessage[] }>({ + chat = agent({ assistantId: 'chat_agent', }); @@ -231,7 +230,7 @@ describe('interrupt handling', () => { const transport = new MockAgentTransport(); TestBed.runInInjectionContext(() => { - const agent = agent<{ messages: BaseMessage[] }>({ + const chat = agent({ assistantId: 'approval_agent', transport, }); @@ -245,12 +244,12 @@ describe('interrupt handling', () => { ]); // Verify interrupt signal - expect(agent.interrupt()).toBeDefined(); - expect(agent.interrupt()?.value.action).toBe('delete_account'); - expect(agent.interrupt()?.value.risk).toBe('high'); + expect(chat.interrupt()).toBeDefined(); + expect(chat.interrupt()?.value.action).toBe('delete_account'); + expect(chat.interrupt()?.value.risk).toBe('high'); // User approves — resume the agent - agent.submit(null, { resume: { approved: true } }); + chat.submit({ resume: { approved: true } }); // Agent continues after approval transport.emit([ @@ -260,8 +259,8 @@ describe('interrupt handling', () => { }, ]); - expect(agent.interrupt()).toBeNull(); - expect(agent.messages()[0].content).toBe('Account deleted.'); + expect(chat.interrupt()).toBeUndefined(); + expect(chat.messages()[0].content).toBe('Account deleted.'); }); }); }); @@ -289,16 +288,16 @@ import { agent } from '@ngaf/langgraph'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ApprovalComponent { - agent = agent<{ messages: BaseMessage[] }>({ + agent = agent({ assistantId: 'approval_agent', }); approve() { - this.agent.submit(null, { resume: { approved: true } }); + this.agent.submit({ resume: { approved: true } }); } reject() { - this.agent.submit(null, { resume: { approved: false } }); + this.agent.submit({ resume: { approved: false } }); } } ``` @@ -322,7 +321,7 @@ describe('error handling', () => { const transport = new MockAgentTransport(); TestBed.runInInjectionContext(() => { - const chat = agent<{ messages: BaseMessage[] }>({ + const chat = agent({ assistantId: 'test_agent', transport, }); @@ -343,7 +342,7 @@ describe('error handling', () => { const transport = new MockAgentTransport(); TestBed.runInInjectionContext(() => { - const chat = agent<{ messages: BaseMessage[] }>({ + const chat = agent({ assistantId: 'test_agent', transport, }); @@ -389,7 +388,7 @@ import { agent } from '@ngaf/langgraph'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChatComponent { - chat = agent<{ messages: BaseMessage[] }>({ + chat = agent({ assistantId: 'chat_agent', }); private lastMessage = ''; @@ -420,7 +419,7 @@ describe('thread switching', () => { TestBed.runInInjectionContext(() => { const threadId = signal('thread_A'); - const chat = agent<{ messages: BaseMessage[] }>({ + const chat = agent({ assistantId: 'test_agent', threadId, transport, @@ -453,7 +452,7 @@ describe('thread switching', () => { const transport = new MockAgentTransport(); TestBed.runInInjectionContext(() => { - const chat = agent<{ messages: BaseMessage[] }>({ + const chat = agent({ assistantId: 'test_agent', transport, }); diff --git a/apps/website/content/docs/agent/guides/time-travel.mdx b/apps/website/content/docs/agent/guides/time-travel.mdx index b99d4abad..db6947831 100644 --- a/apps/website/content/docs/agent/guides/time-travel.mdx +++ b/apps/website/content/docs/agent/guides/time-travel.mdx @@ -274,7 +274,7 @@ export class ReplayComponent { // Go back one step const previousCheckpoint = this.rawHistory()[history.length - 2]?.checkpoint; if (!previousCheckpoint) return; - this.agent.submit(undefined, { + this.agent.submit({}, { checkpoint: previousCheckpoint, }); } diff --git a/apps/website/content/docs/chat/api/provide-chat.mdx b/apps/website/content/docs/chat/api/provide-chat.mdx index d6343e1fe..71ba457cc 100644 --- a/apps/website/content/docs/chat/api/provide-chat.mdx +++ b/apps/website/content/docs/chat/api/provide-chat.mdx @@ -135,7 +135,7 @@ All chat components work without `provideChat()`. They use defaults: template: ``, }) export class SimpleChatComponent { - chatRef = agent<{ messages: BaseMessage[] }>({ + chatRef = agent({ assistantId: 'chat_agent', threadId: signal(null), }); diff --git a/apps/website/content/docs/chat/components/chat-debug.mdx b/apps/website/content/docs/chat/components/chat-debug.mdx index 718ec6cc2..de77ac7f0 100644 --- a/apps/website/content/docs/chat/components/chat-debug.mdx +++ b/apps/website/content/docs/chat/components/chat-debug.mdx @@ -24,7 +24,6 @@ Use `ChatDebugComponent` during development to understand what your LangGraph ag ```typescript import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; import { agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; import { ChatDebugComponent } from '@ngaf/chat'; @Component({ @@ -39,7 +38,7 @@ import { ChatDebugComponent } from '@ngaf/chat'; `, }) export class DebugPageComponent { - chatRef = agent<{ messages: BaseMessage[] }>({ + chatRef = agent({ assistantId: 'chat_agent', threadId: signal(null), }); diff --git a/apps/website/content/docs/chat/components/chat-input.mdx b/apps/website/content/docs/chat/components/chat-input.mdx index 2fd227b3f..b0aee06cd 100644 --- a/apps/website/content/docs/chat/components/chat-input.mdx +++ b/apps/website/content/docs/chat/components/chat-input.mdx @@ -137,7 +137,6 @@ The component includes accessibility attributes: ```typescript import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; import { agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; import { ChatInputComponent } from '@ngaf/chat'; @Component({ @@ -162,7 +161,7 @@ import { ChatInputComponent } from '@ngaf/chat'; `, }) export class InputDemoComponent { - chatRef = agent<{ messages: BaseMessage[] }>({ + chatRef = agent({ assistantId: 'chat_agent', threadId: signal(null), }); 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 d7e123f9f..fbc52e0d8 100644 --- a/apps/website/content/docs/chat/components/chat-interrupt-panel.mdx +++ b/apps/website/content/docs/chat/components/chat-interrupt-panel.mdx @@ -123,7 +123,6 @@ const interrupt = getInterrupt(chatRef); // Interrupt | undefined ```typescript import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; import { agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; import { ChatComponent, ChatInterruptPanelComponent } from '@ngaf/chat'; import type { InterruptAction } from '@ngaf/chat'; @@ -146,7 +145,7 @@ import type { InterruptAction } from '@ngaf/chat'; `, }) export class InterruptDemoComponent { - chatRef = agent<{ messages: BaseMessage[] }>({ + chatRef = agent({ assistantId: 'interrupt_agent', threadId: signal(null), }); diff --git a/apps/website/content/docs/chat/components/chat-message-list.mdx b/apps/website/content/docs/chat/components/chat-message-list.mdx index ba45bdf30..a186cce46 100644 --- a/apps/website/content/docs/chat/components/chat-message-list.mdx +++ b/apps/website/content/docs/chat/components/chat-message-list.mdx @@ -98,30 +98,28 @@ The `getMessageType()` function maps a runtime-neutral `Message` role to a `Mess ```typescript import { getMessageType } from '@ngaf/chat'; -import type { BaseMessage } from '@langchain/core/messages'; const type = getMessageType(message); // 'human' | 'ai' | 'tool' | 'system' | 'function' ``` **Mapping logic:** -| Message type string | Returns | -|--------------------|---------| -| `'human'` | `'human'` | -| `'ai'` | `'ai'` | +| Runtime-neutral role | Returns | +|----------------------|---------| +| `'user'` | `'human'` | +| `'assistant'` | `'ai'` | | `'tool'` | `'tool'` | | `'system'` | `'system'` | -| `'function'` | `'function'` | | Any other value | `'ai'` (default fallback) | ## Working with Message Content -LangChain messages have a `content` property that can be either a `string` or a structured array. The library exports a `messageContent()` utility (used internally by compositions) that handles both cases: +Runtime-neutral messages have a `content` property that can be either a `string` or a structured array. The library exports a `messageContent()` utility (used internally by compositions) that handles both cases: ```typescript // If content is a string, returns it directly // If content is structured, serializes to JSON -function messageContent(message: BaseMessage): string +function messageContent(message: Message): string ``` For custom templates, you can access `message.content` directly and handle the type yourself: @@ -142,7 +140,6 @@ For custom templates, you can access `message.content` directly and handle the t import { Component, inject, ChangeDetectionStrategy, signal } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; import { ChatMessageListComponent, MessageTemplateDirective, @@ -187,7 +184,7 @@ import { export class MessagesDemoComponent { private sanitizer = inject(DomSanitizer); - chatAgent = agent<{ messages: BaseMessage[] }>({ + chatAgent = agent({ assistantId: 'chat_agent', threadId: signal(null), }); 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 a1a2e2f2e..1f67b3e18 100644 --- a/apps/website/content/docs/chat/components/chat-subagent-card.mdx +++ b/apps/website/content/docs/chat/components/chat-subagent-card.mdx @@ -78,7 +78,6 @@ The `ChatSubagentsComponent` primitive iterates over active subagent streams fro ```typescript import { Component, signal, ChangeDetectionStrategy } from '@angular/core'; import { agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; import { ChatComponent, ChatSubagentsComponent, @@ -110,7 +109,7 @@ import { `, }) export class SubagentDemoComponent { - chatRef = agent<{ messages: BaseMessage[] }>({ + chatRef = agent({ assistantId: 'multi_agent', threadId: signal(null), }); diff --git a/apps/website/content/docs/chat/components/chat.mdx b/apps/website/content/docs/chat/components/chat.mdx index 7ee2d73eb..5b9a2e5c4 100644 --- a/apps/website/content/docs/chat/components/chat.mdx +++ b/apps/website/content/docs/chat/components/chat.mdx @@ -35,7 +35,6 @@ If you need to customize the message layout, add components between sections, or import { Component, signal } from '@angular/core'; import { agent } from '@ngaf/langgraph'; import { ChatComponent } from '@ngaf/chat'; -import type { BaseMessage } from '@langchain/core/messages'; @Component({ selector: 'app-chat-page', @@ -48,7 +47,7 @@ import type { BaseMessage } from '@langchain/core/messages'; `, }) export class ChatPageComponent { - chatRef = agent<{ messages: BaseMessage[] }>({ + chatRef = agent({ assistantId: 'chat_agent', threadId: signal(null), }); @@ -100,7 +99,7 @@ When you pass a non-empty `threads` array, a sidebar appears on the left (hidden `, }) export class ChatWithThreadsComponent { - chatRef = agent<{ messages: BaseMessage[] }>({ + chatRef = agent({ assistantId: 'chat_agent', threadId: signal(null), }); diff --git a/apps/website/content/docs/chat/guides/generative-ui.mdx b/apps/website/content/docs/chat/guides/generative-ui.mdx index 613669be3..3fe8936de 100644 --- a/apps/website/content/docs/chat/guides/generative-ui.mdx +++ b/apps/website/content/docs/chat/guides/generative-ui.mdx @@ -26,7 +26,6 @@ Pass a `ViewRegistry` via the `[views]` input on `ChatComponent`: import { Component, signal } from '@angular/core'; import { agent } from '@ngaf/langgraph'; import { ChatComponent, views } from '@ngaf/chat'; -import type { BaseMessage } from '@langchain/core/messages'; import { WeatherCardComponent } from './weather-card.component'; import { ChartComponent } from './chart.component'; @@ -46,7 +45,7 @@ const myViews = views({ `, }) export class ChatPageComponent { - chatRef = agent<{ messages: BaseMessage[] }>({ + chatRef = agent({ assistantId: 'gen_ui_agent', threadId: signal(null), }); diff --git a/apps/website/content/docs/getting-started.mdx b/apps/website/content/docs/getting-started.mdx index 76151a849..471e916f1 100644 --- a/apps/website/content/docs/getting-started.mdx +++ b/apps/website/content/docs/getting-started.mdx @@ -38,7 +38,7 @@ import { agent } from 'angular'; `, }) export class ChatComponent { - chat = agent<{ messages: BaseMessage[] }>({ + chat = agent({ assistantId: 'chat_agent', }); } diff --git a/apps/website/content/prompts/getting-started.md b/apps/website/content/prompts/getting-started.md index 54ee943ea..cc201d7b6 100644 --- a/apps/website/content/prompts/getting-started.md +++ b/apps/website/content/prompts/getting-started.md @@ -4,7 +4,7 @@ Install: npm install @ngaf/langgraph@latest 1. In app.config.ts, add provideAgent({ apiUrl: 'http://localhost:2024' }) to the providers array. Import it from '@ngaf/langgraph'. -2. Create a ChatComponent that calls agent<{ messages: BaseMessage[] }>({ assistantId: 'chat_agent' }) in the constructor or as a field initializer. agent() MUST be called inside an Angular injection context — constructor or field initializer is correct; ngOnInit is not. +2. Create a ChatComponent that calls agent({ assistantId: 'chat_agent' }) in the constructor or as a field initializer. agent() MUST be called inside an Angular injection context — constructor or field initializer is correct; ngOnInit is not. 3. The component template should loop over chat.messages() using @for and render each message's content. Add an input field and a button that calls chat.submit({ message: inputValue }). diff --git a/apps/website/content/prompts/streaming.md b/apps/website/content/prompts/streaming.md index dce537582..7e9c764eb 100644 --- a/apps/website/content/prompts/streaming.md +++ b/apps/website/content/prompts/streaming.md @@ -2,12 +2,12 @@ Configure token-by-token streaming in my Angular component that uses angular. The component already has agent() set up. Now: -1. In the template, bind to chat.messages() with @for — each BaseMessage has a .content property. The template re-renders automatically as tokens arrive because messages() is a Signal. +1. In the template, bind to chat.messages() with @for — each runtime-neutral Message has role and content fields. The template re-renders automatically as tokens arrive because messages() is a Signal. 2. Show a loading indicator while streaming: use chat.isLoading() in an @if block. 3. To throttle rapid re-renders (if performance is a concern), pass throttle: 50 to agent() options — this throttles Signal updates to at most one per 50ms while preserving the final value. -4. To show the stream status more precisely, bind to chat.status() which returns 'idle' | 'loading' | 'resolved' | 'error'. +4. To show the stream status more precisely, bind to chat.status() which returns 'idle' | 'running' | 'error'. Use chat.isLoading() for loading UI. Do not use async pipe or subscribe() — the signals update automatically. diff --git a/apps/website/public/whitepapers/angular-preview.html b/apps/website/public/whitepapers/angular-preview.html index 96ddc9354..39548d3c0 100644 --- a/apps/website/public/whitepapers/angular-preview.html +++ b/apps/website/public/whitepapers/angular-preview.html @@ -92,7 +92,7 @@

Signal Architecture

  • `isLoading()` — Boolean signal indicating active stream processing
  • `error()` — The current error state, or `undefined` when healthy
  • `interrupt()` — `AgentInterrupt | undefined`, populated when the agent yields control for human input
  • -
  • `status()` — Granular connection state: `'idle'` | `'connecting'` | `'streaming'` | `'interrupted'` | `'error'`
  • +
  • `status()` — Runtime lifecycle state: `'idle'` | `'running'` | `'error'`; use `isLoading()` for loading UI
  • `toolCalls()` — Active tool invocations extracted from the message stream
  • `state()` — The current agent state object from the LangGraph thread
  • diff --git a/apps/website/public/whitepapers/angular.pdf b/apps/website/public/whitepapers/angular.pdf index 0f8da9ae0..463d227cd 100644 Binary files a/apps/website/public/whitepapers/angular.pdf and b/apps/website/public/whitepapers/angular.pdf differ diff --git a/apps/website/scripts/generate-whitepaper.ts b/apps/website/scripts/generate-whitepaper.ts index a77ae6b4d..c0d20895f 100644 --- a/apps/website/scripts/generate-whitepaper.ts +++ b/apps/website/scripts/generate-whitepaper.ts @@ -16,7 +16,7 @@ const CURRENT_API_CONTEXT = `You are writing public technical whitepapers for Ca Use only the current API surface: - Package names are @ngaf/langgraph, @ngaf/render, @ngaf/chat, and @ngaf/ag-ui. - @ngaf/langgraph exposes agent(), provideAgent(), LangGraphAgent, MockAgentTransport, FetchStreamTransport, and mockLangGraphAgent(). -- agent() returns a runtime-neutral chat surface with messages(), status(), isLoading(), error(), toolCalls(), state(), submit(), stop(), regenerate(), interrupt(), subagents(), and LangGraph-specific langGraph* signals. interrupt() is AgentInterrupt | undefined on the runtime-neutral surface. +- agent() returns a runtime-neutral chat surface with messages(), status(), isLoading(), error(), toolCalls(), state(), submit(), stop(), regenerate(), interrupt(), subagents(), and LangGraph-specific langGraph* signals. status() returns only 'idle' | 'running' | 'error'. Use isLoading() for loading UI. interrupt() is AgentInterrupt | undefined on the runtime-neutral surface. - Configure LangGraph with assistantId and apiUrl. Do not use graphId or url as @ngaf/langgraph option names. - @ngaf/chat consumes the runtime-neutral Agent contract and exports ChatComponent, ChatMessageListComponent, ChatInputComponent, ChatToolCallsComponent, ChatToolCallCardComponent, ChatInterruptPanelComponent, and ChatDebugComponent. Selectors are , , , , , , and . - Chat messages use Message[] from @ngaf/chat for the runtime-neutral surface. Raw LangGraph messages, when needed, are exposed through langGraphMessages(). diff --git a/apps/website/src/components/landing/CodeBlock.tsx b/apps/website/src/components/landing/CodeBlock.tsx index 95ff070b7..ce2ebdfd7 100644 --- a/apps/website/src/components/landing/CodeBlock.tsx +++ b/apps/website/src/components/landing/CodeBlock.tsx @@ -5,7 +5,7 @@ const EXAMPLE = `// app.config.ts provideAgent({ apiUrl: 'http://localhost:2024' }) // chat.component.ts -const chat = agent<{ messages: BaseMessage[] }>({ +const chat = agent({ assistantId: 'chat_agent', threadId: signal(this.threadId), onThreadId: (id) => localStorage.setItem('threadId', id), diff --git a/apps/website/src/components/landing/ValueProps.tsx b/apps/website/src/components/landing/ValueProps.tsx index d36bced76..5f21c3984 100644 --- a/apps/website/src/components/landing/ValueProps.tsx +++ b/apps/website/src/components/landing/ValueProps.tsx @@ -14,8 +14,8 @@ const chat = agent({ }); // Every property is a Signal -const messages = chat.messages(); // Signal -const status = chat.status(); // Signal<'idle' | 'streaming'> +const messages = chat.messages(); // Signal +const status = chat.status(); // Signal<'idle' | 'running' | 'error'> const error = chat.error(); // Signal // Reactive composition @@ -53,20 +53,20 @@ const branches = agent.history();`, headline: 'Deterministic Agent Testing', description: 'MockAgentTransport lets you script exact event sequences and step through them in your specs. No flaky SSE connections, no timing issues, no running LangGraph server. Test agent behavior the same way you test any Angular service.', code: `// chat.component.spec.ts -const transport = new MockAgentTransport(); - -transport.script([ - { type: 'values', data: { messages: [aiMsg('Hello')] } }, - { type: 'values', data: { status: 'done' } }, +const transport = new MockAgentTransport([ + [{ type: 'values', messages: [aiMsg('Hello')] }], ]); -const chat = agent({ +const chat = agent({ transport, assistantId: 'test_agent', }); +transport.emit(transport.nextBatch()); +transport.close(); + expect(chat.messages()).toEqual([aiMsg('Hello')]); -expect(chat.status()).toBe('done');`, +expect(chat.status()).toBe('idle');`, lang: 'typescript' as const, }, ]; diff --git a/cockpit/deep-agents/filesystem/python/docs/guide.md b/cockpit/deep-agents/filesystem/python/docs/guide.md index e95fc610a..9d2cd5a66 100644 --- a/cockpit/deep-agents/filesystem/python/docs/guide.md +++ b/cockpit/deep-agents/filesystem/python/docs/guide.md @@ -7,7 +7,7 @@ sidebar displays each operation as it happens. -Add a file operations sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.messages()` to access tool call data, derive `toolCallEntries` with `computed()`, and bind them to the sidebar via the `` component from `@ngaf/chat`. +Add a file operations sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.messages()` to access tool call data, derive `toolCallEntries` with `computed()`, and bind them to the sidebar beside the `` component from `@ngaf/chat`. @@ -90,15 +90,12 @@ Tool calls appear inside AI messages as `tool_calls`. Each entry has a `name` (e
    -Use the `` component and project a sidebar via `ng-template`: +Use the `` component from `@ngaf/chat` and render a sibling sidebar: ```html - - + + + ``` -The `#sidebar` template is projected into the chat layout. Operations render reactively as the agent calls tools. +The sibling panel renders operations reactively as the agent calls tools. diff --git a/cockpit/deep-agents/memory/python/docs/guide.md b/cockpit/deep-agents/memory/python/docs/guide.md index 358226f2c..418dd1f96 100644 --- a/cockpit/deep-agents/memory/python/docs/guide.md +++ b/cockpit/deep-agents/memory/python/docs/guide.md @@ -5,7 +5,7 @@ Build a chat interface where the agent remembers facts about the user across tur -Add a memory sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.value()` to access the agent's `agent_memory` state, derive `memoryEntries` with `computed()`, and bind them to the sidebar via the `` component from `@ngaf/chat`. +Add a memory sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.value()` to access the agent's `agent_memory` state, derive `memoryEntries` with `computed()`, and bind them to the sidebar beside the `` component from `@ngaf/chat`. @@ -74,15 +74,12 @@ export class MemoryComponent { -Use the `` component and project a sidebar via `ng-template`: +Use the `` component from `@ngaf/chat` and render a sibling sidebar: ```html - - + + + ``` -The `#sidebar` template is projected into the chat layout. Memory entries render reactively as the agent learns new facts. +The sibling panel renders memory entries reactively as the agent learns new facts. diff --git a/cockpit/deep-agents/planning/python/docs/guide.md b/cockpit/deep-agents/planning/python/docs/guide.md index 30a762529..b6320ff4b 100644 --- a/cockpit/deep-agents/planning/python/docs/guide.md +++ b/cockpit/deep-agents/planning/python/docs/guide.md @@ -7,7 +7,7 @@ sidebar displays each step's status as the agent works through them. -Add a task planning sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.value()` to access the agent's plan state, derive `planSteps` with `computed()`, and bind them to the sidebar via the `` component from `@ngaf/chat`. +Add a task planning sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.value()` to access the agent's plan state, derive `planSteps` with `computed()`, and bind them to the sidebar beside the `` component from `@ngaf/chat`. @@ -81,15 +81,12 @@ export class PlanningComponent { -Use the `` component and project a sidebar via `ng-template`: +Use the `` component from `@ngaf/chat` and render a sibling sidebar: ```html - - + + + ``` -The `#sidebar` template is projected into the chat layout. Steps render reactively as the agent updates the plan state. +The sibling panel renders steps reactively as the agent updates the plan state. diff --git a/cockpit/deep-agents/sandboxes/python/docs/guide.md b/cockpit/deep-agents/sandboxes/python/docs/guide.md index c73d97a5d..886f30d95 100644 --- a/cockpit/deep-agents/sandboxes/python/docs/guide.md +++ b/cockpit/deep-agents/sandboxes/python/docs/guide.md @@ -7,7 +7,7 @@ sidebar displays each execution as a log entry with code input, stdout output, a -Add a code execution log sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.messages()` to access tool call data from the `run_code` tool, derive `executionLogs` with `computed()`, and bind them to the sidebar via the `` component from `@ngaf/chat`. +Add a code execution log sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.messages()` to access tool call data from the `run_code` tool, derive `executionLogs` with `computed()`, and bind them to the sidebar beside the `` component from `@ngaf/chat`. @@ -97,15 +97,12 @@ The tool result (`tc.output`) is populated after the tool finishes executing. Be -Use the `` component and project a sidebar via `ng-template`: +Use the `` component from `@ngaf/chat` and render a sibling sidebar: ```html - - + + + ``` Each log entry shows the code that was executed, the exit status badge, and the stdout output. diff --git a/cockpit/deep-agents/skills/python/docs/guide.md b/cockpit/deep-agents/skills/python/docs/guide.md index fd3ca9820..0e514f41b 100644 --- a/cockpit/deep-agents/skills/python/docs/guide.md +++ b/cockpit/deep-agents/skills/python/docs/guide.md @@ -7,7 +7,7 @@ summarizer) based on the user's request, and the sidebar displays each skill inv -Add a skill invocation sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.messages()` to access tool call data, derive `skillInvocations` with `computed()`, and bind them to the sidebar via the `` component from `@ngaf/chat`. +Add a skill invocation sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.messages()` to access tool call data, derive `skillInvocations` with `computed()`, and bind them to the sidebar beside the `` component from `@ngaf/chat`. @@ -94,15 +94,12 @@ Each tool call in an AI message maps to a skill invocation card. The `result` fi -Use the `` component and project a sidebar via `ng-template`: +Use the `` component from `@ngaf/chat` and render a sibling sidebar: ```html - - + + + ``` Each invocation card shows the skill name, input args, and result once available. diff --git a/cockpit/deep-agents/subagents/python/docs/guide.md b/cockpit/deep-agents/subagents/python/docs/guide.md index f2ec2ed32..888e6d10e 100644 --- a/cockpit/deep-agents/subagents/python/docs/guide.md +++ b/cockpit/deep-agents/subagents/python/docs/guide.md @@ -7,7 +7,7 @@ agents, and the sidebar displays each subagent's status and message count as the -Add a subagent activity sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.subagents()` to access the live Map of child agent streams, derive `subagentEntries` with `computed()`, and render them in the `` sidebar. +Add a subagent activity sidebar to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.subagents()` to access the live Map of child agent streams, derive `subagentEntries` with `computed()`, and render them beside the `` component. @@ -73,15 +73,12 @@ export class SubagentsComponent { -Use the `` component and project a sidebar via `ng-template`: +Use the `` component from `@ngaf/chat` and render a sibling sidebar: ```html - - + + + ``` Each `entry` is a `[toolCallId, SubagentStreamRef]` tuple. The tool call ID identifies which specialist was invoked; the ref's signals update in real time. diff --git a/cockpit/langgraph/durable-execution/python/docs/guide.md b/cockpit/langgraph/durable-execution/python/docs/guide.md index a5b7fc705..b0d7e5d08 100644 --- a/cockpit/langgraph/durable-execution/python/docs/guide.md +++ b/cockpit/langgraph/durable-execution/python/docs/guide.md @@ -8,7 +8,7 @@ status in real time and exposes a "Retry" button when errors occur. -Add a durable multi-step execution workflow to this Angular component using `agent()` from `@ngaf/langgraph`. Display `stream.status()` as a colour-coded badge, show a `stream.hasValue()` indicator, and render a "Retry" button that calls `stream.reload()` when `stream.error()` is set. Bind `stream.messages()` in the template via the `` component from `@ngaf/chat`. +Add a durable multi-step execution workflow to this Angular component using `agent()` from `@ngaf/langgraph`. Display `stream.status()` as a colour-coded badge, show a `stream.hasValue()` indicator, and render a "Retry" button that calls `stream.reload()` when `stream.error()` is set. Bind the conversation with `` from `@ngaf/chat`. @@ -55,7 +55,7 @@ export class DurableExecutionComponent { -Use `stream.status()` to display real-time execution state. The signal returns `'idle'`, `'loading'`, `'resolved'`, or `'error'`: +Use `stream.status()` to display real-time execution state. The signal returns `'idle'`, `'running'`, or `'error'`: ```html @@ -66,9 +66,8 @@ Use `stream.status()` to display real-time execution state. The signal returns ` ```typescript statusBadgeColor(): string { switch (this.stream.status()) { - case 'loading': - case 'reloading': return '#2563eb'; - case 'resolved': return '#16a34a'; + case 'running': return '#2563eb'; + case 'idle': return '#16a34a'; case 'error': return '#dc2626'; default: return '#6b7280'; } @@ -151,7 +150,7 @@ For production, replace `MemorySaver` with `PostgresCheckpointer` for durable pe -The `` component handles message rendering, input, loading states, and error display. Keep your component focused on status monitoring and retry logic. +The `` component handles message rendering, input, loading states, and error display. Keep your component focused on status monitoring and retry logic. diff --git a/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts b/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts index 1e943b9d3..09a63e329 100644 --- a/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts +++ b/cockpit/langgraph/interrupts/angular/src/app/interrupts.component.ts @@ -12,12 +12,12 @@ import { ApprovalCardComponent } from './views/approval-card.component'; * * The LangGraph backend pauses execution when it needs human approval. * The `stream.interrupt()` signal provides the interrupt data, and - * `stream.submit(null)` resumes execution with the human's decision. + * `stream.submit({ resume })` resumes execution with the human's decision. * * Key integration points: * - `stream.interrupt()` — current pause data (undefined when not interrupted) * - `ChatInterruptPanelComponent` — renders the approval UI with action buttons - * - `stream.submit(null)` — resumes the graph (LangGraph convention) + * - `stream.submit({ resume })` — resumes the graph with a payload */ @Component({ selector: 'app-interrupts', @@ -54,8 +54,7 @@ export class InterruptsComponent { /** * Handle an interrupt action from the panel. * - * Submitting null resumes the graph unconditionally — this is the - * LangGraph convention for "proceed without modification". + * Submitting a resume payload continues the graph. * * In a production app, 'edit' would let the user modify the response * before approval, and 'respond' would send a reply payload. @@ -65,6 +64,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.agent.submit({ resume: null }); + void this.agent.submit({ resume: true }); } } diff --git a/cockpit/langgraph/interrupts/python/docs/guide.md b/cockpit/langgraph/interrupts/python/docs/guide.md index 848da849e..322ce3cf5 100644 --- a/cockpit/langgraph/interrupts/python/docs/guide.md +++ b/cockpit/langgraph/interrupts/python/docs/guide.md @@ -7,7 +7,7 @@ and the frontend resumes it with `stream.submit()`. -Add human-in-the-loop approval to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.interrupt()` to display pending approvals, `stream.submit(null)` to approve and resume execution, and `stream.submit({ resume: false })` to reject. Bind `stream.messages()` in the template via the `` component from `@ngaf/chat`. +Add human-in-the-loop approval to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.interrupt()` to display pending approvals, `stream.submit({ resume: true })` to approve and resume execution, and `stream.submit({ resume: false })` to reject. Bind `stream.messages()` in the template via the `` component from `@ngaf/chat`. @@ -55,23 +55,17 @@ The resource automatically handles streaming, interrupt detection, and state man Use `stream.interrupt()` to conditionally show a pending approval in the sidebar: ```html - - - @if (stream.interrupt()) { -
    -

    {{ stream.interrupt() }}

    - - -
    - } @else { -

    No pending approvals

    - } -
    -
    + + +@if (stream.interrupt(); as interrupt) { + +} @else { +

    No pending approvals

    +} ``` When the graph pauses, `stream.interrupt()` returns the interrupt payload. When no interrupt is active, it returns a falsy value. @@ -83,7 +77,7 @@ Add methods that resume graph execution with the user's decision: ```typescript approve(): void { - this.stream.submit(null); + this.stream.submit({ resume: true }); } reject(): void { @@ -91,7 +85,7 @@ reject(): void { } ``` -Submitting `null` is the LangGraph convention for continuing past an interrupt. Submitting `{ resume: false }` signals rejection so the graph can handle it accordingly. +Submitting a `resume` payload continues past an interrupt. Submitting `{ resume: false }` signals rejection so the graph can handle it accordingly. You can extend this pattern to pass structured data back to the graph. For example, `stream.submit({ resume: true, edits: { ... } })` lets the user modify the response before approving. @@ -141,7 +135,7 @@ A checkpointer is required for interrupts to work. Without it, the graph cannot
    -The `` component handles message rendering, input, loading states, and error display. Focus your component on interrupt handling logic. +The `` component handles message rendering, input, loading states, and error display. Focus your component on interrupt handling logic. diff --git a/cockpit/langgraph/memory/python/docs/guide.md b/cockpit/langgraph/memory/python/docs/guide.md index 2adc9a14b..23d716c27 100644 --- a/cockpit/langgraph/memory/python/docs/guide.md +++ b/cockpit/langgraph/memory/python/docs/guide.md @@ -8,7 +8,7 @@ and displays it in a live sidebar. -Add persistent agent memory to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.value()` to access the `memory` field in graph state, derive a reactive `memoryEntries` signal with Angular's `computed()`, and render the facts in a sidebar panel via the `` component from `@ngaf/chat`. +Add persistent agent memory to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.value()` to access the `memory` field in graph state, derive a reactive `memoryEntries` signal with Angular's `computed()`, and render the facts in a sidebar panel beside the `` component from `@ngaf/chat`. @@ -75,15 +75,12 @@ Cast the return type of `stream.value()` to your state shape to get proper type
    -Use the `` component and project the memory panel via `ng-template`: +Use the `` component from `@ngaf/chat` and render a sibling memory panel: ```html - - + + +
    } - - + ``` Facts appear in the sidebar in real time as the agent learns them. diff --git a/cockpit/langgraph/persistence/python/docs/guide.md b/cockpit/langgraph/persistence/python/docs/guide.md index d22cc38fb..96f7e79c7 100644 --- a/cockpit/langgraph/persistence/python/docs/guide.md +++ b/cockpit/langgraph/persistence/python/docs/guide.md @@ -7,7 +7,7 @@ can be resumed using `stream.switchThread(id)`. -Add thread persistence to this Angular component using `agent()` from `@ngaf/langgraph`. Use the `onThreadId` callback to capture thread IDs, `stream.switchThread(id)` to resume conversations, and `stream.switchThread(null)` to start fresh. Bind `stream.messages()` in the template via the `` component from `@ngaf/chat`. +Add thread persistence to this Angular component using `agent()` from `@ngaf/langgraph`. Use the `onThreadId` callback to capture thread IDs, `stream.switchThread(id)` to resume conversations, and `stream.switchThread(null)` to start fresh. Bind `stream.messages()` in the template beside the `` component from `@ngaf/chat`. @@ -63,15 +63,12 @@ Store thread IDs in `localStorage` to survive full page reloads. On app init, re -Use the `` component from `@ngaf/chat` and project a sidebar via `ng-template`: +Use the `` component from `@ngaf/chat` and render a sibling sidebar: ```html - - + + + ``` -The `#sidebar` template is projected into the chat layout, giving you a thread picker alongside the conversation. +The sibling panel gives you a thread picker alongside the conversation. @@ -144,7 +140,7 @@ For production, replace `MemorySaver` with `PostgresCheckpointer` for durable pe -The `` component handles message rendering, input, loading states, and error display. Focus your component on thread management logic. +The `` component handles message rendering, input, loading states, and error display. Focus your component on thread management logic. diff --git a/cockpit/langgraph/streaming/python/docs/guide.md b/cockpit/langgraph/streaming/python/docs/guide.md index 989ff85ab..11c196c3a 100644 --- a/cockpit/langgraph/streaming/python/docs/guide.md +++ b/cockpit/langgraph/streaming/python/docs/guide.md @@ -57,7 +57,7 @@ Use Angular's control flow to render messages reactively: ```html @for (msg of stream.messages(); track $index) { -
    +
    {{ msg.content }}
    } @@ -125,4 +125,3 @@ Never expose your LangSmith API key in client-side code. Use server-side environ - [Chat Messages](/chat/core-capabilities/messages/overview/python) — Learn how ChatMessagesComponent renders messages - [Chat Input](/chat/core-capabilities/input/overview/python) — Explore ChatInputComponent for message submission - diff --git a/cockpit/langgraph/subgraphs/python/docs/guide.md b/cockpit/langgraph/subgraphs/python/docs/guide.md index ba4e0c1b0..c4abd5f4d 100644 --- a/cockpit/langgraph/subgraphs/python/docs/guide.md +++ b/cockpit/langgraph/subgraphs/python/docs/guide.md @@ -7,7 +7,7 @@ subgraph, and the sidebar tracks each subagent's status in real time using `stre -Add a subgraph-powered orchestrator to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.subagents()` to track active child subgraph executions, and derive a `subagentEntries` signal with `computed()` for template iteration. Bind `stream.messages()` via the `` component from `@ngaf/chat`. +Add a subgraph-powered orchestrator to this Angular component using `agent()` from `@ngaf/langgraph`. Use `stream.subagents()` to track active child subgraph executions, and derive a `subagentEntries` signal with `computed()` for template iteration. Bind `stream.messages()` beside the `` component from `@ngaf/chat`. @@ -53,15 +53,12 @@ export class SubgraphsComponent { -Use `` from `@ngaf/chat` and project a sidebar via `ng-template`: +Use `` from `@ngaf/chat` and render a sibling sidebar: ```html - - + + + ``` -The `@empty` block renders when no subagents are active. Each entry shows a truncated run ID and the current status (`'running'`, `'done'`, or `'error'`). +The `@empty` block renders when no subagents are active. Each entry shows a truncated run ID and the current status (`'pending'`, `'running'`, `'complete'`, or `'error'`). Use `computed()` to derive `subagentEntries` from the Map. This ensures Angular's change detection picks up updates when subagents are added or their status changes. @@ -124,7 +120,7 @@ Child subgraphs can have their own state, checkpointers, and tools. This pattern -The `` component handles message rendering, input, loading states, and error display. Focus your component on subagent tracking logic. +The `` component handles message rendering, input, loading states, and error display. Focus your component on subagent tracking logic. diff --git a/cockpit/langgraph/time-travel/python/docs/guide.md b/cockpit/langgraph/time-travel/python/docs/guide.md index 6acd58509..542343e21 100644 --- a/cockpit/langgraph/time-travel/python/docs/guide.md +++ b/cockpit/langgraph/time-travel/python/docs/guide.md @@ -54,12 +54,9 @@ Use `stream.history()` to render checkpoints and `stream.branch()` to highlight the active one: ```html - - + + + ``` Each entry in `stream.history()` is a `ThreadState` snapshot with a diff --git a/libs/cockpit-docs/project.json b/libs/cockpit-docs/project.json index e20e63948..d0cd10fa2 100644 --- a/libs/cockpit-docs/project.json +++ b/libs/cockpit-docs/project.json @@ -6,6 +6,7 @@ "targets": { "build": { "executor": "@nx/js:tsc", + "dependsOn": ["^build"], "outputs": ["{workspaceRoot}/dist/libs/cockpit-docs"], "options": { "outputPath": "dist/libs/cockpit-docs", diff --git a/libs/cockpit-docs/src/lib/docs-bundle.ts b/libs/cockpit-docs/src/lib/docs-bundle.ts index 568829766..5333ee214 100644 --- a/libs/cockpit-docs/src/lib/docs-bundle.ts +++ b/libs/cockpit-docs/src/lib/docs-bundle.ts @@ -3,7 +3,7 @@ import { type CockpitLanguage, type CockpitPageId, type CockpitProduct, -} from '../../../cockpit-registry/src/index'; +} from '@ngaf/cockpit-registry'; export interface DocsBundle { product: CockpitProduct; diff --git a/libs/cockpit-docs/tsconfig.json b/libs/cockpit-docs/tsconfig.json index 190834ef3..a29e3d9a4 100644 --- a/libs/cockpit-docs/tsconfig.json +++ b/libs/cockpit-docs/tsconfig.json @@ -2,8 +2,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "composite": false, - "emitDeclarationOnly": false, - "baseUrl": "." + "emitDeclarationOnly": false }, "files": [], "include": [], diff --git a/libs/langgraph/src/lib/agent.fn.ts b/libs/langgraph/src/lib/agent.fn.ts index b173cb447..af7b84b5e 100644 --- a/libs/langgraph/src/lib/agent.fn.ts +++ b/libs/langgraph/src/lib/agent.fn.ts @@ -71,7 +71,7 @@ import { extractCitations } from './internals/extract-citations'; * in real time as LangGraph streams messages, values, tool calls, interrupts, * subagent state, and checkpoint history. * - * @typeParam T - The state shape returned by the agent (e.g., `{ messages: BaseMessage[] }`) + * @typeParam T - The state shape returned by the agent * @typeParam Bag - Optional bag template for typed interrupts and submit payloads * @param options - Configuration for the LangGraph agent * @returns A {@link LangGraphAgent} with reactive signals and action methods @@ -79,7 +79,7 @@ import { extractCitations } from './internals/extract-citations'; * @example * ```typescript * // In a component field initializer - * const chat = agent<{ messages: BaseMessage[] }>({ + * const chat = agent({ * assistantId: 'chat_agent', * apiUrl: 'http://localhost:2024', * threadId: signal(this.savedThreadId), diff --git a/libs/langgraph/src/lib/transport/mock-stream.transport.ts b/libs/langgraph/src/lib/transport/mock-stream.transport.ts index 56db5d2e4..4e0ad3ed0 100644 --- a/libs/langgraph/src/lib/transport/mock-stream.transport.ts +++ b/libs/langgraph/src/lib/transport/mock-stream.transport.ts @@ -11,8 +11,8 @@ import type { ThreadState } from '@langchain/langgraph-sdk'; * @example * ```typescript * const transport = new MockAgentTransport([ - * [{ type: 'values', data: { messages: [aiMsg('Hello')] } }], - * [{ type: 'values', data: { status: 'done' } }], + * [{ type: 'values', messages: [aiMsg('Hello')] }], + * [{ type: 'values', messages: [aiMsg('Done')] }], * ]); * ``` */ @@ -37,7 +37,7 @@ export class MockAgentTransport implements AgentTransport { this.script = script; } - /** Advance to the next scripted batch and return its events. */ + /** Advance to the next scripted batch. Pass the returned events to `emit()`. */ nextBatch(): StreamEvent[] { if (this.scriptIndex >= this.script.length) return []; return this.script[this.scriptIndex++]; diff --git a/package-lock.json b/package-lock.json index 591417529..0eeacfdaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,6 @@ "@cacheplane/partial-markdown": "^0.3.0", "@langchain/core": "^1.1.33", "@langchain/langgraph-sdk": "^1.7.4", - "@modelcontextprotocol/sdk": "^1.27.1", "@noble/ed25519": "^2.3.0", "drizzle-orm": "^0.45.2", "framer-motion": "^12.38.0", @@ -8456,6 +8455,7 @@ "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.14.1" @@ -10544,323 +10544,6 @@ "react": ">=16" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", - "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@module-federation/bridge-react-webpack-plugin": { "version": "0.21.6", "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.21.6.tgz", @@ -12095,10 +11778,6 @@ "node": ">= 10" } }, - "node_modules/@ngaf/langgraph-mcp": { - "resolved": "packages/mcp", - "link": true - }, "node_modules/@ngaf/minting-service": { "resolved": "apps/minting-service", "link": true @@ -22972,6 +22651,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -22988,6 +22668,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -24365,6 +24046,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -24473,6 +24155,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -24486,6 +24169,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -25243,6 +24927,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -25269,6 +24954,7 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -25385,6 +25071,7 @@ "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4", @@ -26134,6 +25821,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -27080,6 +26768,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -27222,6 +26911,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, "license": "MIT" }, "node_modules/ejs": { @@ -27268,6 +26958,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -27414,6 +27105,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -27423,6 +27115,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -27439,6 +27132,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -27564,6 +27258,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -28012,6 +27707,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -28057,6 +27753,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -28069,6 +27766,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -28179,6 +27877,7 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "dev": true, "license": "MIT", "dependencies": { "ip-address": "10.1.0" @@ -28316,6 +28015,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -28372,6 +28072,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, "funding": [ { "type": "github", @@ -29018,6 +28719,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -29153,6 +28855,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -29215,6 +28918,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -29252,6 +28956,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -29495,6 +29200,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -29754,6 +29460,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -29782,6 +29489,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -30026,6 +29734,7 @@ "version": "4.12.8", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.9.0" @@ -30249,6 +29958,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -30551,6 +30261,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -30590,6 +30301,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -31203,6 +30915,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -31531,12 +31244,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, "license": "MIT" }, "node_modules/json-schema-typed": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -33112,6 +32827,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -35542,6 +35258,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -35551,6 +35268,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -35591,6 +35309,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -35613,6 +35332,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -36151,6 +35871,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -36451,6 +36172,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.20.0" @@ -37751,6 +37473,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -37764,6 +37487,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -37959,6 +37683,7 @@ "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -38038,6 +37763,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -38506,6 +38232,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -38970,6 +38697,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -38986,12 +38714,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, "license": "MIT" }, "node_modules/router/node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -39079,6 +38809,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, "license": "MIT" }, "node_modules/sass": { @@ -39846,6 +39577,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, "license": "ISC" }, "node_modules/shallow-clone": { @@ -39963,6 +39695,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -39982,6 +39715,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -39998,6 +39732,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -40016,6 +39751,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -40507,6 +40243,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -41460,6 +41197,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -42258,6 +41996,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -42393,6 +42132,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -43455,6 +43195,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -43732,6 +43473,7 @@ "version": "3.25.1", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.25 || ^4" @@ -43746,20 +43488,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "packages/mcp": { - "name": "@ngaf/langgraph-mcp", - "version": "0.0.1", - "license": "MIT", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0" - }, - "bin": { - "langgraph-mcp": "src/index.js" - }, - "devDependencies": { - "typescript": "^5.4.0" - } } } } diff --git a/package.json b/package.json index a8f24acfb..1adf24e63 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,6 @@ "@cacheplane/partial-markdown": "^0.3.0", "@langchain/core": "^1.1.33", "@langchain/langgraph-sdk": "^1.7.4", - "@modelcontextprotocol/sdk": "^1.27.1", "@noble/ed25519": "^2.3.0", "drizzle-orm": "^0.45.2", "framer-motion": "^12.38.0", diff --git a/packages/mcp/package-smoke.test.mjs b/packages/mcp/package-smoke.test.mjs deleted file mode 100644 index 219e16e53..000000000 --- a/packages/mcp/package-smoke.test.mjs +++ /dev/null @@ -1,63 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { createRequire } from 'node:module'; -import { fileURLToPath } from 'node:url'; - -const require = createRequire(import.meta.url); -const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const outputRoot = path.join(workspaceRoot, 'dist/packages/mcp'); -const projectJsonPath = path.join(workspaceRoot, 'packages/mcp/project.json'); -const packageJsonPath = path.join(outputRoot, 'package.json'); - -test('source package manifest entrypoints exist locally', () => { - const packageJson = require('./package.json'); - const binPath = packageJson.bin['@ngaf/langgraph-mcp']; - - assert.equal(fs.existsSync(path.join(workspaceRoot, 'packages/mcp', packageJson.main)), true); - assert.equal(fs.existsSync(path.join(workspaceRoot, 'packages/mcp', binPath)), true); -}); - -function loadBuiltPackageJson() { - return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); -} - -test('built package manifest entrypoints resolve inside Nx output', () => { - const packageJson = loadBuiltPackageJson(); - const binPath = packageJson.bin['@ngaf/langgraph-mcp']; - - assert.equal(fs.existsSync(path.join(outputRoot, packageJson.main)), true); - assert.equal(fs.existsSync(path.join(outputRoot, binPath)), true); - assert.equal(fs.existsSync(path.join(outputRoot, packageJson.types)), true); -}); - -test('built package can discover bundled api docs from package root', () => { - const loaderPath = path.join(outputRoot, 'src/data/loader.js'); - const originalCwd = process.cwd(); - const tempCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-package-smoke-')); - - process.chdir(tempCwd); - - try { - delete require.cache[require.resolve(loaderPath)]; - const { getAllSymbolNames } = require(loaderPath); - - assert.equal(fs.existsSync(path.join(outputRoot, 'api-docs.json')), true); - assert.equal(fs.existsSync(path.join(tempCwd, 'api-docs.json')), false); - assert.equal(fs.existsSync(path.join(tempCwd, 'apps/website/public/api-docs.json')), false); - assert.equal(getAllSymbolNames().includes('agent'), true); - } finally { - process.chdir(originalCwd); - fs.rmSync(tempCwd, { recursive: true, force: true }); - } -}); - -test('project declares a named MCP smoke target', () => { - const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf8')); - - assert.equal(projectJson.targets.test.executor, 'nx:run-commands'); - assert.deepEqual(projectJson.targets.test.dependsOn, ['build']); - assert.equal(projectJson.targets.test.options.command, 'npm --prefix packages/mcp run smoke'); -}); diff --git a/packages/mcp/package.json b/packages/mcp/package.json deleted file mode 100644 index 3073dbc0b..000000000 --- a/packages/mcp/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@ngaf/langgraph-mcp", - "version": "0.0.1", - "private": true, - "description": "MCP server for the @ngaf/langgraph library", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/cacheplane/angular-agent-framework.git", - "directory": "packages/mcp" - }, - "homepage": "https://github.com/cacheplane/angular-agent-framework#readme", - "bugs": { - "url": "https://github.com/cacheplane/angular-agent-framework/issues" - }, - "main": "src/index.js", - "bin": { - "@ngaf/langgraph-mcp": "src/index.js" - }, - "scripts": { - "build": "tsc -p tsconfig.json", - "smoke": "node --test package-smoke.test.mjs", - "start": "node src/index.js" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0" - }, - "devDependencies": { - "typescript": "^5.4.0" - } -} diff --git a/packages/mcp/project.json b/packages/mcp/project.json deleted file mode 100644 index f2dd8a732..000000000 --- a/packages/mcp/project.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "mcp", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "packages/mcp/src", - "projectType": "library", - "targets": { - "build": { - "executor": "@nx/js:tsc", - "outputs": ["{workspaceRoot}/dist/packages/mcp"], - "options": { - "outputPath": "dist/packages/mcp", - "main": "packages/mcp/src/index.ts", - "tsConfig": "packages/mcp/tsconfig.json", - "assets": [ - { - "input": "apps/website/content/docs/agent/api", - "glob": "api-docs.json", - "output": "." - } - ] - } - }, - "test": { - "executor": "nx:run-commands", - "dependsOn": ["build"], - "options": { - "command": "npm --prefix packages/mcp run smoke" - } - } - } -} diff --git a/packages/mcp/src/data/loader.ts b/packages/mcp/src/data/loader.ts deleted file mode 100644 index b692c115a..000000000 --- a/packages/mcp/src/data/loader.ts +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT -import fs from 'fs'; -import path from 'path'; - -export interface ApiDocsJson { - children?: ApiSymbol[]; -} - -export interface ApiSymbol { - name: string; - kind?: string; - kindString?: string; - description?: string; - params?: { - name: string; - type?: string; - description?: string; - }[]; - comment?: { summary?: { text: string }[] }; - signatures?: { - parameters?: { - name: string; - type: { name?: string }; - comment?: { summary?: { text: string }[] }; - }[]; - }[]; -} - -let cachedDocs: ApiDocsJson | null = null; - -export function getApiDocs(): ApiDocsJson { - if (cachedDocs) return cachedDocs; - const candidates = [ - path.join(__dirname, '../../api-docs.json'), - path.join(__dirname, '../../../../apps/website/content/docs/agent/api/api-docs.json'), - ]; - for (const p of candidates) { - if (fs.existsSync(p)) { - const parsed = JSON.parse(fs.readFileSync(p, 'utf8')) as ApiDocsJson | ApiSymbol[]; - cachedDocs = Array.isArray(parsed) ? { children: parsed } : parsed; - return cachedDocs; - } - } - return { children: [] }; -} - -export function findSymbol(name: string): ApiSymbol | undefined { - const docs = getApiDocs(); - return docs.children?.find((c) => c.name === name); -} - -export function getAllSymbolNames(): string[] { - return getApiDocs().children?.map((c) => c.name) ?? []; -} diff --git a/packages/mcp/src/index.js b/packages/mcp/src/index.js deleted file mode 100755 index b46a53639..000000000 --- a/packages/mcp/src/index.js +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env node -const { spawnSync } = require('node:child_process'); -const path = require('node:path'); - -const entrypoint = path.join(__dirname, 'index.ts'); -const result = spawnSync( - process.execPath, - ['--import', 'tsx', entrypoint, ...process.argv.slice(2)], - { stdio: 'inherit' } -); - -if (result.error) { - throw result.error; -} - -process.exit(result.status ?? 1); diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts deleted file mode 100644 index 6c217b6ac..000000000 --- a/packages/mcp/src/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env node -// SPDX-License-Identifier: MIT -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; -import { getApiReferenceTool, handleGetApiReference } from './tools/get-api-reference.js'; -import { searchDocsTool, handleSearchDocs } from './tools/search-docs.js'; -import { getExampleTool, handleGetExample } from './tools/get-example.js'; -import { scaffoldChatComponentTool, handleScaffoldChatComponent } from './tools/scaffold-chat-component.js'; -import { addAgentTool, handleAddAgent } from './tools/add-agent.js'; -import { getThreadPersistencePatternTool, handleGetThreadPersistencePattern } from './tools/get-thread-persistence-pattern.js'; - -const server = new Server( - { name: 'angular', version: '0.1.0' }, - { capabilities: { tools: {} } } -); - -const TOOLS = [ - getApiReferenceTool, - searchDocsTool, - getExampleTool, - scaffoldChatComponentTool, - addAgentTool, - getThreadPersistencePatternTool, -]; - -server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); - -server.setRequestHandler(CallToolRequestSchema, async (req) => { - const { name, arguments: args } = req.params; - const a = (args ?? {}) as Record; - switch (name) { - case 'get_api_reference': return handleGetApiReference(a); - case 'search_docs': return handleSearchDocs(a); - case 'get_example': return handleGetExample(a); - case 'scaffold_chat_component': return handleScaffoldChatComponent(a); - case 'add_angular': return handleAddAgent(a); - case 'get_thread_persistence_pattern': return handleGetThreadPersistencePattern(a); - default: return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] }; - } -}); - -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); -} - -main().catch(console.error); diff --git a/packages/mcp/src/tools/add-agent.ts b/packages/mcp/src/tools/add-agent.ts deleted file mode 100644 index f210af151..000000000 --- a/packages/mcp/src/tools/add-agent.ts +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: MIT -import fs from 'fs'; - -export const addAgentTool = { - name: 'add_angular', - description: 'Generate npm install command and app.config.ts diff to add angular', - inputSchema: { - type: 'object', - properties: { - appConfigPath: { type: 'string', description: 'Path to app.config.ts' }, - }, - required: ['appConfigPath'], - }, -}; - -export function handleAddAgent(args: Record) { - const appConfigPath = args['appConfigPath'] as string; - - if (!fs.existsSync(appConfigPath)) { - return { content: [{ type: 'text', text: `File not found or is not an Angular app.config.ts: ${appConfigPath}` }] }; - } - const content = fs.readFileSync(appConfigPath, 'utf8'); - if (!content.includes('ApplicationConfig') && !content.includes('appConfig')) { - return { content: [{ type: 'text', text: `File does not appear to be an Angular app.config.ts: ${appConfigPath}` }] }; - } - - const result = `Steps to add angular: - -1. Install the package: -\`\`\`bash -npm install @ngaf/langgraph -\`\`\` - -2. Apply this change to ${appConfigPath}: -\`\`\`diff -+import { provideAgent } from '@ngaf/langgraph'; - - export const appConfig: ApplicationConfig = { - providers: [ -+ provideAgent({ apiUrl: 'REPLACE_WITH_YOUR_LANGGRAPH_URL' }), - // ... existing providers - ] - }; -\`\`\` - -Replace REPLACE_WITH_YOUR_LANGGRAPH_URL with your LangGraph server URL (e.g. http://localhost:2024).`; - - return { content: [{ type: 'text', text: result }] }; -} diff --git a/packages/mcp/src/tools/get-api-reference.ts b/packages/mcp/src/tools/get-api-reference.ts deleted file mode 100644 index 0ae2a1226..000000000 --- a/packages/mcp/src/tools/get-api-reference.ts +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -import { findSymbol, getAllSymbolNames } from '../data/loader.js'; - -export const getApiReferenceTool = { - name: 'get_api_reference', - description: 'Get the full API documentation for a angular symbol', - inputSchema: { - type: 'object', - properties: { symbol: { type: 'string', description: 'Symbol name, e.g. "agent"' } }, - required: ['symbol'], - }, -}; - -export function handleGetApiReference(args: Record) { - const symbol = args['symbol'] as string; - const entry = findSymbol(symbol); - if (!entry) { - return { content: [{ type: 'text', text: `Symbol not found: "${symbol}". Available: ${getAllSymbolNames().join(', ')}` }] }; - } - const summary = entry.description ?? entry.comment?.summary?.map((s) => s.text).join('') ?? ''; - const params = entry.params?.map((p) => - ` ${p.name}: ${p.type ?? 'unknown'} — ${p.description ?? ''}` - ).join('\n') ?? entry.signatures?.[0]?.parameters?.map((p) => { - const pSummary = p.comment?.summary?.map((s) => s.text).join('') ?? ''; - return ` ${p.name}: ${p.type?.name ?? 'unknown'} — ${pSummary}`; - }).join('\n') ?? ''; - const text = [ - `## ${entry.name}`, - `Kind: ${entry.kind ?? entry.kindString ?? 'unknown'}`, - summary, - params ? `Parameters:\n${params}` : '', - ].filter(Boolean).join('\n\n'); - return { content: [{ type: 'text', text }] }; -} diff --git a/packages/mcp/src/tools/get-example.ts b/packages/mcp/src/tools/get-example.ts deleted file mode 100644 index 18aebe4b4..000000000 --- a/packages/mcp/src/tools/get-example.ts +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-License-Identifier: MIT -const EXAMPLES: Record = { - 'basic-chat': `// Basic chat component with angular -import { Component } from '@angular/core'; -import { agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; - -@Component({ - selector: 'app-chat', - template: \` - @for (msg of chat.messages(); track $index) { -

    {{ msg.content }}

    - } - @if (chat.isLoading()) {

    Thinking…

    } - - - \`, -}) -export class ChatComponent { - chat = agent<{ messages: BaseMessage[] }>({ assistantId: 'chat_agent' }); - send(content: string) { - if (!content.trim()) return; - this.chat.submit({ message: content }); - } -}`, - - 'thread-persistence': `// Thread persistence with localStorage -import { Component, signal } from '@angular/core'; -import { agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; - -@Component({ selector: 'app-chat', template: '' }) -export class ChatComponent { - threadId = signal(localStorage.getItem('chat-thread')); - chat = agent<{ messages: BaseMessage[] }>({ - assistantId: 'chat_agent', - threadId: this.threadId, - onThreadId: (id) => { this.threadId.set(id); localStorage.setItem('chat-thread', id); }, - }); - newThread() { this.threadId.set(null); localStorage.removeItem('chat-thread'); } -}`, - - 'system-prompt': `// System prompt configuration per session -import { Component } from '@angular/core'; -import { agent } from '@ngaf/langgraph'; - -@Component({ selector: 'app-chat', template: '' }) -export class ChatComponent { - chat = agent({ - assistantId: 'chat_agent', - config: { configurable: { system_prompt: 'You are a helpful coding assistant.' } }, - }); -}`, - - 'mock-testing': `// Unit testing with MockAgentTransport -import { TestBed } from '@angular/core/testing'; -import { agent, MockAgentTransport } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; - -describe('ChatComponent', () => { - it('updates messages when transport emits', () => { - TestBed.runInInjectionContext(() => { - const transport = new MockAgentTransport(); - const chat = agent<{ messages: BaseMessage[] }>({ assistantId: 'test', transport }); - transport.emit([ - { type: 'messages', messages: [[{ type: 'ai', content: 'Hello!' }, { id: '1' }]] }, - ]); - expect(chat.messages()).toHaveLength(1); - expect(chat.messages()[0].content).toBe('Hello!'); - }); - }); -});`, - - 'interrupts': `// Handling interrupts (human-in-the-loop) -import { Component } from '@angular/core'; -import { agent } from '@ngaf/langgraph'; - -@Component({ - selector: 'app-chat', - template: \` - @if (chat.interrupt(); as interrupt) { -
    -

    {{ interrupt.value }}

    - - -
    - } - \`, -}) -export class ChatComponent { - chat = agent({ assistantId: 'agent_with_interrupts' }); - approve() { this.chat.submit(null, { command: { resume: true } }); } - reject() { this.chat.submit(null, { command: { resume: false } }); } -}`, - - 'subagent-progress': `// Showing subagent tool call progress -import { Component } from '@angular/core'; -import { agent } from '@ngaf/langgraph'; - -@Component({ - selector: 'app-chat', - template: \` - @for (tool of chat.toolProgress(); track tool.name) { -

    {{ tool.name }}: {{ tool.status }}

    - } - \`, -}) -export class ChatComponent { - chat = agent({ assistantId: 'research_agent' }); -}`, - - 'custom-transport': `// Custom transport with auth headers -import { AgentTransport } from '@ngaf/langgraph'; - -export class AuthTransport implements AgentTransport { - async *stream(input: unknown, _options: unknown): AsyncGenerator { - const token = await getAuthToken(); // your auth logic - const res = await fetch('/api/stream', { - method: 'POST', - headers: { Authorization: \`Bearer \${token}\`, 'Content-Type': 'application/json' }, - body: JSON.stringify(input), - }); - const reader = res.body!.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\\n'); - buffer = lines.pop()!; - for (const line of lines) { - if (line.startsWith('data: ')) yield JSON.parse(line.slice(6)); - } - } - } -}`, -}; - -const VALID_PATTERNS = Object.keys(EXAMPLES); - -export const getExampleTool = { - name: 'get_example', - description: 'Get a complete runnable code example for a angular pattern', - inputSchema: { - type: 'object', - properties: { - pattern: { - type: 'string', - description: `Pattern name. One of: ${VALID_PATTERNS.join(', ')}`, - }, - }, - required: ['pattern'], - }, -}; - -export function handleGetExample(args: Record) { - const pattern = args['pattern'] as string; - const example = EXAMPLES[pattern]; - if (!example) { - return { content: [{ type: 'text', text: `Unknown pattern: "${pattern}". Available: ${VALID_PATTERNS.join(', ')}` }] }; - } - return { content: [{ type: 'text', text: `\`\`\`typescript\n${example}\n\`\`\`` }] }; -} diff --git a/packages/mcp/src/tools/get-thread-persistence-pattern.ts b/packages/mcp/src/tools/get-thread-persistence-pattern.ts deleted file mode 100644 index 3230482c4..000000000 --- a/packages/mcp/src/tools/get-thread-persistence-pattern.ts +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -const PATTERNS: Record = { - localStorage: `// Thread persistence with localStorage -threadId = signal(localStorage.getItem('chat-thread-id')); - -chat = agent({ - assistantId: 'chat_agent', - threadId: this.threadId, - onThreadId: (id: string) => { - this.threadId.set(id); - localStorage.setItem('chat-thread-id', id); - }, -}); - -// To start a new conversation: -// this.threadId.set(null); localStorage.removeItem('chat-thread-id');`, - - sessionStorage: `// Thread persistence with sessionStorage (clears on tab close) -threadId = signal(sessionStorage.getItem('chat-thread-id')); - -chat = agent({ - assistantId: 'chat_agent', - threadId: this.threadId, - onThreadId: (id: string) => { - this.threadId.set(id); - sessionStorage.setItem('chat-thread-id', id); - }, -});`, - - custom: `// Thread persistence with a custom store -// TODO: replace saveThread / loadThread with your store (e.g. NgRx, a service, IndexedDB) -threadId = signal(loadThread()); - -chat = agent({ - assistantId: 'chat_agent', - threadId: this.threadId, - onThreadId: (id: string) => { - this.threadId.set(id); - saveThread(id); // TODO: replace with your store - }, -});`, -}; - -export const getThreadPersistencePatternTool = { - name: 'get_thread_persistence_pattern', - description: 'Get Angular code pattern for thread persistence with a specific storage type', - inputSchema: { - type: 'object', - properties: { - storageType: { - type: 'string', - enum: ['localStorage', 'sessionStorage', 'custom'], - description: 'Storage mechanism to use', - }, - }, - required: ['storageType'], - }, -}; - -export function handleGetThreadPersistencePattern(args: Record) { - const storageType = args['storageType'] as string; - const pattern = PATTERNS[storageType]; - if (!pattern) { - return { content: [{ type: 'text', text: `Unknown storageType: "${storageType}". Use: localStorage, sessionStorage, or custom` }] }; - } - return { content: [{ type: 'text', text: `\`\`\`typescript\n${pattern}\n\`\`\`` }] }; -} diff --git a/packages/mcp/src/tools/scaffold-chat-component.ts b/packages/mcp/src/tools/scaffold-chat-component.ts deleted file mode 100644 index e4b30e6f4..000000000 --- a/packages/mcp/src/tools/scaffold-chat-component.ts +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -export const scaffoldChatComponentTool = { - name: 'scaffold_chat_component', - description: 'Generate a complete Angular chat component using angular', - inputSchema: { - type: 'object', - properties: { - componentName: { type: 'string', description: 'Component class name, e.g. "ChatComponent"' }, - apiUrl: { type: 'string', description: 'LangGraph server URL' }, - assistantId: { type: 'string', description: 'LangGraph assistant/graph ID' }, - threadPersistence: { type: 'boolean', description: 'Include localStorage thread persistence' }, - }, - required: ['componentName', 'apiUrl', 'assistantId', 'threadPersistence'], - }, -}; - -export function handleScaffoldChatComponent(args: Record) { - const { componentName, apiUrl, assistantId, threadPersistence } = args as { - componentName: string; apiUrl: string; assistantId: string; threadPersistence: boolean; - }; - - const selector = componentName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, ''); - const persistenceImport = threadPersistence ? ', signal' : ''; - const persistenceFields = threadPersistence - ? `\n threadId = signal(localStorage.getItem('${selector}-thread'));` - : ''; - const persistenceOptions = threadPersistence - ? `\n threadId: this.threadId,\n onThreadId: (id: string) => {\n this.threadId.set(id);\n localStorage.setItem('${selector}-thread', id);\n },` - : ''; - - const code = `import { Component${persistenceImport} } from '@angular/core'; -import { agent } from '@ngaf/langgraph'; -import type { BaseMessage } from '@langchain/core/messages'; - -@Component({ - selector: 'app-${selector}', - template: \` -
    - @for (msg of chat.messages(); track $index) { -
    - {{ msg.content }} -
    - } - @if (chat.isLoading()) { -
    Thinking…
    - } -
    -
    - - -
    - \`, -}) -export class ${componentName} {${persistenceFields} - - chat = agent<{ messages: BaseMessage[] }>({ - apiUrl: '${apiUrl}', - assistantId: '${assistantId}',${persistenceOptions} - }); - - send(e: Event) { - e.preventDefault(); - const form = e.target as HTMLFormElement; - const input = form.querySelector('input') as HTMLInputElement; - const content = input.value.trim(); - if (!content) return; - input.value = ''; - this.chat.submit({ message: content }); - } -}`; - - return { content: [{ type: 'text', text: `\`\`\`typescript\n${code}\n\`\`\`` }] }; -} diff --git a/packages/mcp/src/tools/search-docs.ts b/packages/mcp/src/tools/search-docs.ts deleted file mode 100644 index 858b71570..000000000 --- a/packages/mcp/src/tools/search-docs.ts +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -import { getApiDocs } from '../data/loader.js'; - -export const searchDocsTool = { - name: 'search_docs', - description: 'Search angular documentation by keyword or phrase', - inputSchema: { - type: 'object', - properties: { query: { type: 'string', description: 'Search query' } }, - required: ['query'], - }, -}; - -export function handleSearchDocs(args: Record) { - const query = (args['query'] as string).toLowerCase(); - const docs = getApiDocs(); - const matches: string[] = []; - for (const child of docs.children ?? []) { - const text = [ - child.name, - child.comment?.summary?.map((s) => s.text).join('') ?? '', - ...(child.signatures?.[0]?.parameters?.map( - (p) => `${p.name}: ${p.comment?.summary?.map((s) => s.text).join('') ?? ''}` - ) ?? []), - ].join(' ').toLowerCase(); - if (text.includes(query)) { - const summary = child.comment?.summary?.map((s) => s.text).join('') ?? ''; - matches.push(`## ${child.name}\n${summary}`); - } - if (matches.length >= 5) break; - } - if (matches.length === 0) { - return { content: [{ type: 'text', text: `No results for: "${args['query']}"` }] }; - } - return { content: [{ type: 'text', text: matches.join('\n\n---\n\n') }] }; -} diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json deleted file mode 100644 index f34edc2dd..000000000 --- a/packages/mcp/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "target": "ES2020", - "module": "CommonJS", - "moduleResolution": "node", - "esModuleInterop": true, - "strict": true, - "declaration": true, - "skipLibCheck": true - }, - "include": ["src/**/*.ts"] -}