From 44b8c0327fe81394ecd689590a7c1de5737aa16c Mon Sep 17 00:00:00 2001 From: Gagan Singh Date: Tue, 10 Mar 2026 22:57:22 -0700 Subject: [PATCH 1/7] sdk updates for capturing tool calls --- package-lock.json | 12 +++++-- package.json | 12 +++++++ src/annotate.ts | 5 +++ src/client.ts | 16 ++++++--- src/index.ts | 5 ++- src/langchain.ts | 86 +++++++++++++++++++++++++++++++++++++++++++++++ src/message.ts | 83 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 src/annotate.ts create mode 100644 src/langchain.ts diff --git a/package-lock.json b/package-lock.json index d05a722..8a9d5a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "llpsdk", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "llpsdk", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "ws": "^8.18.0" @@ -22,6 +22,14 @@ }, "engines": { "node": ">=18.0.0" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.0" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + } } }, "node_modules/@biomejs/biome": { diff --git a/package.json b/package.json index 5ed9cdc..d007e99 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,18 @@ ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" + }, + "./langchain": { + "import": "./dist/langchain.js", + "types": "./dist/langchain.d.ts" + } + }, + "peerDependencies": { + "@langchain/core": ">=0.3.0" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true } }, "files": [ diff --git a/src/annotate.ts b/src/annotate.ts new file mode 100644 index 0000000..60f5fb7 --- /dev/null +++ b/src/annotate.ts @@ -0,0 +1,5 @@ +import type { ToolCall } from './message.js'; + +export interface Annotater { + annotateToolCall(toolCall: ToolCall): Promise; +} diff --git a/src/client.ts b/src/client.ts index d2c91bb..54275c9 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9,7 +9,8 @@ import { PlatformError, TimeoutError, } from './errors.js'; -import { PresenceMessage, TextMessage } from './message.js'; +import type { Annotater } from './annotate.js'; +import { PresenceMessage, TextMessage, ToolCall } from './message.js'; import { ConnectionStatus, PresenceStatus } from './presence.js'; export interface LLPClientConfig { @@ -19,10 +20,10 @@ export interface LLPClientConfig { readonly maxQueueSize?: number; // Default: 32 } -export type MessageHandler = (msg: TextMessage) => Promise; +export type MessageHandler = (annotater: Annotater, msg: TextMessage) => Promise; export type PresenceHandler = (msg: PresenceMessage) => void | Promise; -export class LLPClient { +export class LLPClient implements Annotater { private ws: WebSocket | null = null; private status: ConnectionStatus = ConnectionStatus.Disconnected; private sessionId: string | null = null; @@ -141,6 +142,13 @@ export class LLPClient { }); } + async annotateToolCall(toolCall: ToolCall): Promise { + if (this.status !== ConnectionStatus.Authenticated) { + throw new NotAuthenticatedError('Must be authenticated to annotate tool calls'); + } + this.enqueue(toolCall.encode()); + } + async sendAsyncMessage(msg: TextMessage): Promise { if (this.status !== ConnectionStatus.Authenticated) { throw new NotAuthenticatedError('Must be authenticated to send messages'); @@ -267,7 +275,7 @@ export class LLPClient { // Otherwise, call the message handler if (this.messageHandler) { try { - const reply = await this.messageHandler(msg); + const reply = await this.messageHandler(this, msg); await this.sendAsyncMessage(reply); } catch (err) { console.error('Error in message handler:', err); diff --git a/src/index.ts b/src/index.ts index 065ec9d..493d9a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,8 +19,11 @@ export { TimeoutError, } from './errors.js'; +// Annotater interface +export type { Annotater } from './annotate.js'; + // Message types -export { PresenceMessage, TextMessage } from './message.js'; +export { PresenceMessage, TextMessage, ToolCall } from './message.js'; // Status enums export { diff --git a/src/langchain.ts b/src/langchain.ts new file mode 100644 index 0000000..30da732 --- /dev/null +++ b/src/langchain.ts @@ -0,0 +1,86 @@ +/** + * LangChain integration for the LLP SDK. + * + * Import via the sub-path export: + * import { LLPToolCallMiddleware } from 'llpsdk/langchain'; + * + * Requires @langchain/core as a peer dependency. + */ + +import type { Annotater } from './annotate.js'; +import type { TextMessage } from './message.js'; + +// LangChain calls these methods by name at runtime. +// We type only what we need from @langchain/core to stay dependency-light. +interface ToolInfo { + name?: string; +} + +function serialize(value: unknown): string { + try { + return typeof value === 'string' ? value : JSON.stringify(value); + } catch { + return String(value); + } +} + +/** + * LangChain callback handler that captures tool calls and reports them + * to the LLP platform via `annotater.annotateToolCall()`. + * + * Instantiate once per inbound message and pass to `chain.invoke()`: + * + * ```ts + * client.onMessage(async (annotater, msg) => { + * const middleware = new LLPToolCallMiddleware(msg, annotater); + * const result = await chain.invoke(input, { callbacks: [middleware] }); + * return msg.reply(result); + * }); + * ``` + */ +export class LLPToolCallMiddleware { + private readonly pending = new Map< + string, + { name: string; parameters: string; startMs: number } + >(); + + constructor( + private readonly msg: TextMessage, + private readonly annotater: Annotater, + ) {} + + handleToolStart( + tool: ToolInfo | null | undefined, + input: unknown, + runId: string, + ): void { + const name = tool?.name ?? 'unknown'; + const parameters = serialize(input); + this.pending.set(runId, { name, parameters, startMs: Date.now() }); + } + + handleToolEnd(output: unknown, runId: string): void { + const entry = this.pending.get(runId); + if (!entry) return; + this.pending.delete(runId); + + const durationMs = Date.now() - entry.startMs; + const tc = this.msg.toolCall(entry.name, entry.parameters, serialize(output), durationMs); + this.annotater + .annotateToolCall(tc) + .catch((err) => console.warn('[LLP] Failed to annotate tool call:', err)); + } + + handleToolError(err: unknown, runId: string): void { + const entry = this.pending.get(runId); + if (!entry) return; + this.pending.delete(runId); + + const durationMs = Date.now() - entry.startMs; + const error = err instanceof Error ? err : new Error(String(err)); + const tc = this.msg.toolCallException(entry.name, entry.parameters, error, durationMs); + this.annotater + .annotateToolCall(tc) + .catch((e) => console.warn('[LLP] Failed to annotate tool call exception:', e)); + } +} diff --git a/src/message.ts b/src/message.ts index d7c1fed..19ea2c7 100644 --- a/src/message.ts +++ b/src/message.ts @@ -2,6 +2,57 @@ import { randomUUID } from 'node:crypto'; import { TextMessageEmptyError, TextMessageReplyError } from './errors.js'; import type { PresenceStatus } from './presence.js'; +// ============================================================================= +// ToolCall +// ============================================================================= + +export class ToolCall { + readonly id: string; + readonly recipient: string; + readonly name: string; + readonly parameters: string; + readonly result: string; + readonly threwException: boolean; + readonly durationMs: number; + + constructor(args: { + id: string; + recipient: string; + name: string; + parameters: string; + result: string; + threwException: boolean; + durationMs: number; + }) { + this.id = args.id; + this.recipient = args.recipient; + this.name = args.name; + this.parameters = args.parameters; + this.result = args.result; + this.threwException = args.threwException; + this.durationMs = args.durationMs; + } + + encode(): string { + return JSON.stringify({ + type: 'tool_call', + id: this.id, + data: { + to: this.recipient, + name: this.name, + parameters: this.parameters, + result: this.result, + threw_exception: this.threwException, + duration_ms: this.durationMs, + }, + }); + } +} + +// ============================================================================= +// TextMessage +// ============================================================================= + export class TextMessage { readonly id: string; readonly sender: string; @@ -54,6 +105,38 @@ export class TextMessage { return this.attachment !== null && this.attachment !== ''; } + /** + * Factory for a successful tool call record. + * Stamps the correct message ID and recipient so the platform + * can correlate this tool call with the originating message. + */ + toolCall(name: string, parameters: string, result: string, durationMs: number): ToolCall { + return new ToolCall({ + id: this.id, + recipient: this.sender, + name, + parameters, + result, + threwException: false, + durationMs, + }); + } + + /** + * Factory for a failed tool call record. + */ + toolCallException(name: string, parameters: string, err: Error, durationMs: number): ToolCall { + return new ToolCall({ + id: this.id, + recipient: this.sender, + name, + parameters, + result: err.message, + threwException: true, + durationMs, + }); + } + static decode(json: Record): TextMessage { const data = json.data as Record; const recipient = data.to as string; From e78fa55e17ca8bd97d2fb272ffd40f7013e0046d Mon Sep 17 00:00:00 2001 From: Gagan Singh Date: Wed, 11 Mar 2026 07:39:05 -0700 Subject: [PATCH 2/7] fixed tests --- src/client.ts | 4 ++-- src/index.ts | 6 +++--- src/langchain.ts | 6 +----- tests/integration.test.ts | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/client.ts b/src/client.ts index 54275c9..9cb74ba 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,5 +1,6 @@ import { randomUUID } from 'node:crypto'; import WebSocket from 'ws'; +import type { Annotater } from './annotate.js'; import { AlreadyClosedError, AlreadyConnectedError, @@ -9,8 +10,7 @@ import { PlatformError, TimeoutError, } from './errors.js'; -import type { Annotater } from './annotate.js'; -import { PresenceMessage, TextMessage, ToolCall } from './message.js'; +import { PresenceMessage, TextMessage, type ToolCall } from './message.js'; import { ConnectionStatus, PresenceStatus } from './presence.js'; export interface LLPClientConfig { diff --git a/src/index.ts b/src/index.ts index 493d9a0..b16cc47 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,6 @@ +// Annotater interface +export type { Annotater } from './annotate.js'; + // Core client export { LLPClient, @@ -19,9 +22,6 @@ export { TimeoutError, } from './errors.js'; -// Annotater interface -export type { Annotater } from './annotate.js'; - // Message types export { PresenceMessage, TextMessage, ToolCall } from './message.js'; diff --git a/src/langchain.ts b/src/langchain.ts index 30da732..cef68e5 100644 --- a/src/langchain.ts +++ b/src/langchain.ts @@ -49,11 +49,7 @@ export class LLPToolCallMiddleware { private readonly annotater: Annotater, ) {} - handleToolStart( - tool: ToolInfo | null | undefined, - input: unknown, - runId: string, - ): void { + handleToolStart(tool: ToolInfo | null | undefined, input: unknown, runId: string): void { const name = tool?.name ?? 'unknown'; const parameters = serialize(input); this.pending.set(runId, { name, parameters, startMs: Date.now() }); diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 6459c78..a9579c7 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -113,7 +113,7 @@ describe('LLPClient Integration Tests', () => { it('should handle incoming messages and send replies', async () => { const replies: TextMessage[] = []; - client.onMessage(async (msg: TextMessage) => { + client.onMessage(async (_annotater, msg: TextMessage) => { const reply = msg.reply(`Echo: ${msg.prompt}`); replies.push(reply); return reply; From 50cfeff0854f5c53038a12b39a1765cdecd1e9b5 Mon Sep 17 00:00:00 2001 From: Gagan Singh Date: Wed, 11 Mar 2026 09:55:27 -0700 Subject: [PATCH 3/7] another update for middleware --- README.md | 37 ++++++++++++++++++- examples/simple-agent.ts | 2 +- package.json | 8 +++- src/langchain.ts | 80 ++++++++++++++++++++++++++++++++++++++-- tests/client.test.ts | 16 ++++---- tests/handler.test.ts | 13 +++++-- tests/langchain.test.ts | 72 ++++++++++++++++++++++++++++++++++++ 7 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 tests/langchain.test.ts diff --git a/README.md b/README.md index 14c09f8..db859b3 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ async function main() { const client = new LLPClient('my-agent', process.env.LLP_API_KEY ?? ''); // Define a callback handler for processing messages - client.onMessage(async (msg) => { + client.onMessage(async (_annotater, msg) => { // Process the prompt with your agent. // Replace this with your own processing logic. const response = msg.prompt; @@ -43,6 +43,41 @@ async function main() { main(); ``` +## LangChain Tool Call Capture + +If your LangChain agent uses tools, the SDK can automatically annotate those +tool calls back to LLP. + +```typescript +import { LLPClient } from 'llpsdk'; +import { createLLPToolMiddleware } from 'llpsdk/langchain'; +import { createAgent, tool } from 'langchain'; +import * as z from 'zod'; + +const weatherTool = tool( + async ({ city }: { city: string }) => `Weather for ${city}: sunny`, + { + name: 'weather', + description: 'Look up the weather for a city', + schema: z.object({ city: z.string() }), + }, +); + +client.onMessage(async (annotater, msg) => { + const agent = createAgent({ + model, + tools: [weatherTool], + middleware: [createLLPToolMiddleware(msg, annotater)], + }); + + const result = await agent.invoke({ + messages: [{ role: 'user', content: msg.prompt }], + }); + + return msg.reply(String(result.messages.at(-1)?.content ?? '')); +}); +``` + ## Development ```bash diff --git a/examples/simple-agent.ts b/examples/simple-agent.ts index 89cbfc9..5ad4168 100644 --- a/examples/simple-agent.ts +++ b/examples/simple-agent.ts @@ -6,7 +6,7 @@ async function main() { const client = new LLPClient('simple-agent', process.env.LLP_API_KEY || ''); // Register handlers - client.onMessage(async (msg) => { + client.onMessage(async (_annotater, msg) => { // process msg.prompt with your agent return msg.reply('hello from TypeScript!'); }); diff --git a/package.json b/package.json index d007e99..41aad6e 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,15 @@ } }, "peerDependencies": { - "@langchain/core": ">=0.3.0" + "@langchain/core": ">=0.3.0", + "langchain": ">=1.0.0" }, "peerDependenciesMeta": { "@langchain/core": { "optional": true + }, + "langchain": { + "optional": true } }, "files": [ @@ -59,10 +63,12 @@ "ws": "^8.18.0" }, "devDependencies": { + "@langchain/core": "^1.0.4", "@biomejs/biome": "^2.3.12", "@types/node": "^20.0.0", "@types/ws": "^8.5.0", "dotenv": "^17.2.3", + "langchain": "^1.0.6", "tsx": "^4.19.0", "typescript": "^5.6.0", "vitest": "^2.1.0" diff --git a/src/langchain.ts b/src/langchain.ts index cef68e5..0161f5f 100644 --- a/src/langchain.ts +++ b/src/langchain.ts @@ -2,11 +2,12 @@ * LangChain integration for the LLP SDK. * * Import via the sub-path export: - * import { LLPToolCallMiddleware } from 'llpsdk/langchain'; + * import { createLLPToolMiddleware } from 'llpsdk/langchain'; * - * Requires @langchain/core as a peer dependency. + * Requires `langchain` as a peer dependency. */ +import { createMiddleware } from 'langchain'; import type { Annotater } from './annotate.js'; import type { TextMessage } from './message.js'; @@ -16,6 +17,22 @@ interface ToolInfo { name?: string; } +interface ToolCallRequest { + toolCall: { + name: string; + args: unknown; + }; +} + +type ToolCallHandler = (request: ToolCallRequest) => Promise; + +export interface LLPToolMiddlewareOptions { + name?: string; + onAnnotationError?: (error: unknown) => void; + serializeArgs?: (value: unknown) => string; + serializeResult?: (value: unknown) => string; +} + function serialize(value: unknown): string { try { return typeof value === 'string' ? value : JSON.stringify(value); @@ -25,7 +42,64 @@ function serialize(value: unknown): string { } /** - * LangChain callback handler that captures tool calls and reports them + * LangChain v1 middleware helper that captures tool calls and reports them + * to the LLP platform via `annotater.annotateToolCall()`. + * + * Instantiate once per inbound message and pass it to `createAgent()`: + * + * ```ts + * client.onMessage(async (annotater, msg) => { + * const agent = createAgent({ + * model, + * tools, + * middleware: [createLLPToolMiddleware(msg, annotater)], + * }); + * const result = await agent.invoke({ messages }); + * return msg.reply(String(result.messages.at(-1)?.content ?? '')); + * }); + * ``` + */ +export function createLLPToolMiddleware( + msg: TextMessage, + annotater: Annotater, + options: LLPToolMiddlewareOptions = {}, +) { + const serializeArgs = options.serializeArgs ?? serialize; + const serializeResult = options.serializeResult ?? serialize; + const onAnnotationError = + options.onAnnotationError ?? + ((error: unknown) => console.warn('[LLP] Failed to annotate tool call:', error)); + + return createMiddleware({ + name: options.name ?? 'LLPToolCallMiddleware', + wrapToolCall: async (request: ToolCallRequest, handler: ToolCallHandler) => { + const startMs = Date.now(); + const toolName = request.toolCall.name; + const parameters = serializeArgs(request.toolCall.args); + + try { + const result = await handler(request); + await annotater + .annotateToolCall( + msg.toolCall(toolName, parameters, serializeResult(result), Date.now() - startMs), + ) + .catch(onAnnotationError); + return result; + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + await annotater + .annotateToolCall( + msg.toolCallException(toolName, parameters, err, Date.now() - startMs), + ) + .catch(onAnnotationError); + throw error; + } + }, + }); +} + +/** + * Legacy LangChain callback handler that captures tool calls and reports them * to the LLP platform via `annotater.annotateToolCall()`. * * Instantiate once per inbound message and pass to `chain.invoke()`: diff --git a/tests/client.test.ts b/tests/client.test.ts index 461c6fc..44ca3e7 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -209,14 +209,14 @@ describe('LLPClient', () => { session_id: 'session-123', }, }); - messageHandler?.(Buffer.from(authResponse)); - await connectPromise; - const throwError = () => - client.onMessage(async (msg: TextMessage) => { - return msg; - }); - expect(throwError).toThrow(/before connecting/); - }); + messageHandler?.(Buffer.from(authResponse)); + await connectPromise; + const throwError = () => + client.onMessage(async (_annotater, msg: TextMessage) => { + return msg; + }); + expect(throwError).toThrow(/before connecting/); + }); }); describe('onPresence', () => { diff --git a/tests/handler.test.ts b/tests/handler.test.ts index 38845a4..6cfa5c8 100644 --- a/tests/handler.test.ts +++ b/tests/handler.test.ts @@ -1,30 +1,35 @@ import { describe, expect, it } from 'vitest'; +import type { Annotater } from '../src/annotate.js'; import { PresenceMessage, TextMessage } from '../src/message.js'; import { PresenceStatus } from '../src/presence.js'; describe('Message Handlers', () => { describe('MessageHandler type', () => { + const annotater: Annotater = { + annotateToolCall: async () => {}, + }; + it('should accept async function that returns TextMessage', async () => { - const handler = async (msg: TextMessage): Promise => { + const handler = async (_annotater: Annotater, msg: TextMessage): Promise => { return msg.reply('Response'); }; const input = new TextMessage('bob', 'Hello', null, 'msg-1', 'alice'); - const result = await handler(input); + const result = await handler(annotater, input); expect(result.prompt).toBe('Response'); expect(result.recipient).toBe('alice'); }); it('should allow handler to process message content', async () => { - const handler = async (msg: TextMessage): Promise => { + const handler = async (_annotater: Annotater, msg: TextMessage): Promise => { const upperPrompt = msg.prompt.toUpperCase(); return msg.reply(`Echo: ${upperPrompt}`); }; const input = new TextMessage('bob', 'hello world'); - const result = await handler(input); + const result = await handler(annotater, input); expect(result.prompt).toBe('Echo: HELLO WORLD'); }); }); diff --git a/tests/langchain.test.ts b/tests/langchain.test.ts new file mode 100644 index 0000000..8c49e54 --- /dev/null +++ b/tests/langchain.test.ts @@ -0,0 +1,72 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Annotater } from '../src/annotate.js'; +import { TextMessage } from '../src/message.js'; + +const createMiddlewareMock = vi.fn((config) => config); + +vi.mock('langchain', () => ({ + createMiddleware: createMiddlewareMock, +})); + +describe('LangChain integration', () => { + beforeEach(() => { + createMiddlewareMock.mockClear(); + }); + + it('creates middleware that annotates successful tool calls', async () => { + const { createLLPToolMiddleware } = await import('../src/langchain.js'); + const annotateToolCall = vi.fn().mockResolvedValue(undefined); + const annotater: Annotater = { annotateToolCall }; + const msg = new TextMessage('financial-advisor', 'Summarize the invoice', undefined, 'msg-123', 'user-1'); + + const middleware = createLLPToolMiddleware(msg, annotater); + const result = await middleware.wrapToolCall( + { + toolCall: { + name: 'load_invoice_pdf', + args: { url: 'https://example.com/invoice.pdf' }, + }, + }, + async () => 'Invoice total: $50', + ); + + expect(createMiddlewareMock).toHaveBeenCalledTimes(1); + expect(result).toBe('Invoice total: $50'); + expect(annotateToolCall).toHaveBeenCalledTimes(1); + + const toolCall = annotateToolCall.mock.calls[0][0]; + expect(toolCall.name).toBe('load_invoice_pdf'); + expect(toolCall.parameters).toContain('invoice.pdf'); + expect(toolCall.result).toBe('Invoice total: $50'); + expect(toolCall.threwException).toBe(false); + }); + + it('annotates failed tool calls and rethrows', async () => { + const { createLLPToolMiddleware } = await import('../src/langchain.js'); + const annotateToolCall = vi.fn().mockResolvedValue(undefined); + const annotater: Annotater = { annotateToolCall }; + const msg = new TextMessage('financial-advisor', 'Summarize the invoice', undefined, 'msg-123', 'user-1'); + const middleware = createLLPToolMiddleware(msg, annotater); + const error = new Error('network down'); + + await expect( + middleware.wrapToolCall( + { + toolCall: { + name: 'load_invoice_pdf', + args: { url: 'https://example.com/invoice.pdf' }, + }, + }, + async () => { + throw error; + }, + ), + ).rejects.toThrow('network down'); + + expect(annotateToolCall).toHaveBeenCalledTimes(1); + const toolCall = annotateToolCall.mock.calls[0][0]; + expect(toolCall.name).toBe('load_invoice_pdf'); + expect(toolCall.result).toBe('network down'); + expect(toolCall.threwException).toBe(true); + }); +}); From fb0112865610e23c56343f05ac22b2157aceda5f Mon Sep 17 00:00:00 2001 From: Gagan Singh Date: Thu, 12 Mar 2026 01:41:05 -0700 Subject: [PATCH 4/7] more middleware related updates --- README.md | 47 +- examples/simple-agent.ts | 2 +- package-lock.json | 1978 ++++++++++++++++++++++++++++++++++++- package.json | 5 +- src/client.ts | 275 +++++- src/context.ts | 31 + src/index.ts | 13 +- src/langchain.ts | 174 ++-- src/session.ts | 45 + tests/client.test.ts | 20 +- tests/handler.test.ts | 36 +- tests/integration.test.ts | 96 +- tests/langchain.test.ts | 41 +- 13 files changed, 2571 insertions(+), 192 deletions(-) create mode 100644 src/context.ts create mode 100644 src/session.ts diff --git a/README.md b/README.md index db859b3..0692c47 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ async function main() { const client = new LLPClient('my-agent', process.env.LLP_API_KEY ?? ''); // Define a callback handler for processing messages - client.onMessage(async (_annotater, msg) => { + client.onMessage(async (_session, msg) => { // Process the prompt with your agent. // Replace this with your own processing logic. const response = msg.prompt; @@ -63,21 +63,48 @@ const weatherTool = tool( }, ); -client.onMessage(async (annotater, msg) => { - const agent = createAgent({ - model, - tools: [weatherTool], - middleware: [createLLPToolMiddleware(msg, annotater)], - }); +const client = new LLPClient>('my-agent', process.env.LLP_API_KEY ?? ''); - const result = await agent.invoke({ - messages: [{ role: 'user', content: msg.prompt }], - }); +client.onStart((session, presence) => { + if (presence.status === 'available') { + return createAgent({ + model, + tools: [weatherTool], + middleware: [createLLPToolMiddleware()], + }); + } +}); + +client.onMessage(async (session, msg) => { + const agent = session.data; + if (!agent) { + throw new Error(`No agent initialized for session ${session.id}`); + } + + const result = await agent.invoke( + { + messages: [{ role: 'user', content: msg.prompt }], + }, + { + context: { + llpMessage: msg, + llpClient: session, + }, + }, + ); return msg.reply(String(result.messages.at(-1)?.content ?? '')); }); ``` +The middleware requires LLP runtime context with: + +- `llpMessage`: the inbound `TextMessage` +- `llpClient`: the session or client object used to send tool-call annotations + +`onStart()` can return a typed session value that the SDK stores on `session.data`. +`onPresence()` remains available as a compatibility alias, but `onStart()` is the preferred API. + ## Development ```bash diff --git a/examples/simple-agent.ts b/examples/simple-agent.ts index 5ad4168..d9830a4 100644 --- a/examples/simple-agent.ts +++ b/examples/simple-agent.ts @@ -6,7 +6,7 @@ async function main() { const client = new LLPClient('simple-agent', process.env.LLP_API_KEY || ''); // Register handlers - client.onMessage(async (_annotater, msg) => { + client.onMessage(async (_session, msg) => { // process msg.prompt with your agent return msg.reply('hello from TypeScript!'); }); diff --git a/package-lock.json b/package-lock.json index 8a9d5a8..b5daebb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "llpsdk", "version": "0.2.0", - "lockfileVersion": 3, + "lockfileVersion": 2, "requires": true, "packages": { "": { @@ -9,13 +9,16 @@ "version": "0.2.0", "license": "MIT", "dependencies": { - "ws": "^8.18.0" + "ws": "^8.18.0", + "zod": "^3.25.76" }, "devDependencies": { "@biomejs/biome": "^2.3.12", + "@langchain/core": "^1.0.4", "@types/node": "^20.0.0", "@types/ws": "^8.5.0", "dotenv": "^17.2.3", + "langchain": "^1.0.6", "tsx": "^4.19.0", "typescript": "^5.6.0", "vitest": "^2.1.0" @@ -24,11 +27,15 @@ "node": ">=18.0.0" }, "peerDependencies": { - "@langchain/core": ">=0.3.0" + "@langchain/core": ">=0.3.0", + "langchain": ">=1.0.0" }, "peerDependenciesMeta": { "@langchain/core": { "optional": true + }, + "langchain": { + "optional": true } } }, @@ -38,6 +45,16 @@ "integrity": "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA==", "dev": true, "license": "MIT OR Apache-2.0", + "dependencies": { + "@biomejs/cli-darwin-arm64": "2.3.12", + "@biomejs/cli-darwin-x64": "2.3.12", + "@biomejs/cli-linux-arm64": "2.3.12", + "@biomejs/cli-linux-arm64-musl": "2.3.12", + "@biomejs/cli-linux-x64": "2.3.12", + "@biomejs/cli-linux-x64-musl": "2.3.12", + "@biomejs/cli-win32-arm64": "2.3.12", + "@biomejs/cli-win32-x64": "2.3.12" + }, "bin": { "biome": "bin/biome" }, @@ -195,6 +212,12 @@ "node": ">=14.21.3" } }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "dev": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -644,6 +667,181 @@ "dev": true, "license": "MIT" }, + "node_modules/@langchain/core": { + "version": "1.1.32", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.32.tgz", + "integrity": "sha512-ZZNiER5tceFXqZOghfrxNHzM60gcQL5XK/8Ow5+o4OuKHrP1p/RUQBDM9Y1nddi/VmKQj+ncaXXM5KovXTEGGQ==", + "dev": true, + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "@standard-schema/spec": "^1.1.0", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": ">=0.5.0 <1.0.0", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "uuid": "^11.1.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@langchain/langgraph": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.2.2.tgz", + "integrity": "sha512-1F94azb3b3TpHi5Jxa7gFvAYXuSIsEobEXk3/PD7+gkOobIC5Jty3+/ATSkH7joUo0bXCF/rgMOjRGusi6YvSQ==", + "dev": true, + "dependencies": { + "@langchain/langgraph-checkpoint": "^1.0.0", + "@langchain/langgraph-sdk": "~1.7.0", + "@standard-schema/spec": "1.1.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.1.16", + "zod": "^3.25.32 || ^4.2.0", + "zod-to-json-schema": "^3.x" + }, + "peerDependenciesMeta": { + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-checkpoint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz", + "integrity": "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==", + "dev": true, + "dependencies": { + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.0.1" + } + }, + "node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/langgraph-sdk": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.7.2.tgz", + "integrity": "sha512-8ad5OTwqc15J/DxLNJYLn3IC2mpfow09nxJdszxhwm3KgsolGZIUV6g7m67C2p4j3cbQZD5USHt3hKEL0ahqoA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15", + "p-queue": "^9.0.1", + "p-retry": "^7.1.1", + "uuid": "^13.0.0" + }, + "peerDependencies": { + "@angular/core": "^18.0.0 || ^19.0.0 || ^20.0.0", + "@langchain/core": "^1.1.16", + "react": "^18 || ^19", + "react-dom": "^18 || ^19", + "svelte": "^4.0.0 || ^5.0.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@langchain/core": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true + }, + "node_modules/@langchain/langgraph-sdk/node_modules/p-queue": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz", + "integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==", + "dev": true, + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "dev": true, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/@langchain/langgraph/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.56.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", @@ -994,6 +1192,12 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1001,6 +1205,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, "node_modules/@types/node": { "version": "20.19.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", @@ -1011,6 +1221,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -1134,6 +1350,18 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1144,6 +1372,26 @@ "node": ">=12" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1154,6 +1402,18 @@ "node": ">=8" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -1171,6 +1431,18 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -1181,6 +1453,15 @@ "node": ">= 16" } }, + "node_modules/console-table-printer": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz", + "integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==", + "dev": true, + "dependencies": { + "simple-wcswidth": "^1.1.2" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1199,6 +1480,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -1236,6 +1526,34 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "dependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + }, "bin": { "esbuild": "bin/esbuild" }, @@ -1281,6 +1599,12 @@ "@types/estree": "^1.0.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -1319,6 +1643,93 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/is-network-error": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", + "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "dev": true, + "dependencies": { + "base64-js": "^1.5.1" + } + }, + "node_modules/langchain": { + "version": "1.2.31", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-1.2.31.tgz", + "integrity": "sha512-dKafIQKyNUoMlZX7NaEaDt9pqofZbsF2UoJkPrybC99vV9QbUEiLKT0yGdixA3qFHXmsFdJWpu4n4a+B87Q2Zw==", + "dev": true, + "dependencies": { + "@langchain/langgraph": "^1.1.2", + "@langchain/langgraph-checkpoint": "^1.0.0", + "langsmith": ">=0.5.0 <1.0.0", + "uuid": "^11.1.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.1.32" + } + }, + "node_modules/langsmith": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.5.9.tgz", + "integrity": "sha512-mT88HlYTz7IZzVJP/OM7D6PAR3kzcbPGjYDJpx64ua9WiJXFqQx6YKewxIIxkVSZCb9I/9TiYvPYfZ4rawOdEg==", + "dev": true, + "dependencies": { + "@types/uuid": "^10.0.0", + "chalk": "^5.6.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + } + } + }, + "node_modules/langsmith/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -1343,6 +1754,15 @@ "dev": true, "license": "MIT" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1362,6 +1782,58 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "dev": true, + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -1432,16 +1904,6 @@ "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", @@ -1467,16 +1929,70 @@ "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", + "@types/estree": "1.0.8", "fsevents": "~2.3.2" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, "license": "ISC" }, + "node_modules/simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "dev": true + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1553,6 +2069,7 @@ "license": "MIT", "dependencies": { "esbuild": "~0.27.0", + "fsevents": "~2.3.3", "get-tsconfig": "^4.7.5" }, "bin": { @@ -1586,6 +2103,19 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -1594,6 +2124,7 @@ "license": "MIT", "dependencies": { "esbuild": "^0.21.3", + "fsevents": "~2.3.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, @@ -2067,6 +2598,31 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "dependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + }, "bin": { "esbuild": "bin/esbuild" }, @@ -2202,6 +2758,1388 @@ "optional": true } } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + }, + "dependencies": { + "@biomejs/biome": { + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.12.tgz", + "integrity": "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA==", + "dev": true, + "requires": { + "@biomejs/cli-darwin-arm64": "2.3.12", + "@biomejs/cli-darwin-x64": "2.3.12", + "@biomejs/cli-linux-arm64": "2.3.12", + "@biomejs/cli-linux-arm64-musl": "2.3.12", + "@biomejs/cli-linux-x64": "2.3.12", + "@biomejs/cli-linux-x64-musl": "2.3.12", + "@biomejs/cli-win32-arm64": "2.3.12", + "@biomejs/cli-win32-x64": "2.3.12" + } + }, + "@biomejs/cli-darwin-arm64": { + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.12.tgz", + "integrity": "sha512-cO6fn+KiMBemva6EARDLQBxeyvLzgidaFRJi8G7OeRqz54kWK0E+uSjgFaiHlc3DZYoa0+1UFE8mDxozpc9ieg==", + "dev": true, + "optional": true + }, + "@biomejs/cli-darwin-x64": { + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.12.tgz", + "integrity": "sha512-/fiF/qmudKwSdvmSrSe/gOTkW77mHHkH8Iy7YC2rmpLuk27kbaUOPa7kPiH5l+3lJzTUfU/t6x1OuIq/7SGtxg==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-arm64": { + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.12.tgz", + "integrity": "sha512-nbOsuQROa3DLla5vvsTZg+T5WVPGi9/vYxETm9BOuLHBJN3oWQIg3MIkE2OfL18df1ZtNkqXkH6Yg9mdTPem7A==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-arm64-musl": { + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.12.tgz", + "integrity": "sha512-aqkeSf7IH+wkzFpKeDVPSXy9uDjxtLpYA6yzkYsY+tVjwFFirSuajHDI3ul8en90XNs1NA0n8kgBrjwRi5JeyA==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-x64": { + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.12.tgz", + "integrity": "sha512-CQtqrJ+qEEI8tgRSTjjzk6wJAwfH3wQlkIGsM5dlecfRZaoT+XCms/mf7G4kWNexrke6mnkRzNy6w8ebV177ow==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-x64-musl": { + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.12.tgz", + "integrity": "sha512-kVGWtupRRsOjvw47YFkk5mLiAdpCPMWBo1jOwAzh+juDpUb2sWarIp+iq+CPL1Wt0LLZnYtP7hH5kD6fskcxmg==", + "dev": true, + "optional": true + }, + "@biomejs/cli-win32-arm64": { + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.12.tgz", + "integrity": "sha512-Re4I7UnOoyE4kHMqpgtG6UvSBGBbbtvsOvBROgCCoH7EgANN6plSQhvo2W7OCITvTp7gD6oZOyZy72lUdXjqZg==", + "dev": true, + "optional": true + }, + "@biomejs/cli-win32-x64": { + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.12.tgz", + "integrity": "sha512-qqGVWqNNek0KikwPZlOIoxtXgsNGsX+rgdEzgw82Re8nF02W+E2WokaQhpF5TdBh/D/RQ3TLppH+otp6ztN0lw==", + "dev": true, + "optional": true + }, + "@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "dev": true + }, + "@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "dev": true, + "optional": true + }, + "@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "dev": true, + "optional": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "@langchain/core": { + "version": "1.1.32", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.32.tgz", + "integrity": "sha512-ZZNiER5tceFXqZOghfrxNHzM60gcQL5XK/8Ow5+o4OuKHrP1p/RUQBDM9Y1nddi/VmKQj+ncaXXM5KovXTEGGQ==", + "dev": true, + "requires": { + "@cfworker/json-schema": "^4.0.2", + "@standard-schema/spec": "^1.1.0", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": ">=0.5.0 <1.0.0", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "uuid": "^11.1.0", + "zod": "^3.25.76 || ^4" + } + }, + "@langchain/langgraph": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.2.2.tgz", + "integrity": "sha512-1F94azb3b3TpHi5Jxa7gFvAYXuSIsEobEXk3/PD7+gkOobIC5Jty3+/ATSkH7joUo0bXCF/rgMOjRGusi6YvSQ==", + "dev": true, + "requires": { + "@langchain/langgraph-checkpoint": "^1.0.0", + "@langchain/langgraph-sdk": "~1.7.0", + "@standard-schema/spec": "1.1.0", + "uuid": "^10.0.0" + }, + "dependencies": { + "uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true + } + } + }, + "@langchain/langgraph-checkpoint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz", + "integrity": "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==", + "dev": true, + "requires": { + "uuid": "^10.0.0" + }, + "dependencies": { + "uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true + } + } + }, + "@langchain/langgraph-sdk": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.7.2.tgz", + "integrity": "sha512-8ad5OTwqc15J/DxLNJYLn3IC2mpfow09nxJdszxhwm3KgsolGZIUV6g7m67C2p4j3cbQZD5USHt3hKEL0ahqoA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.15", + "p-queue": "^9.0.1", + "p-retry": "^7.1.1", + "uuid": "^13.0.0" + }, + "dependencies": { + "eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true + }, + "p-queue": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz", + "integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==", + "dev": true, + "requires": { + "eventemitter3": "^5.0.1", + "p-timeout": "^7.0.0" + } + }, + "p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "dev": true + }, + "uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "dev": true + } + } + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "dev": true, + "optional": true + }, + "@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true + }, + "@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "requires": { + "undici-types": "~6.21.0" + } + }, + "@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, + "@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "requires": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "requires": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + } + }, + "@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "requires": { + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "requires": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + } + }, + "@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "requires": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + } + }, + "@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "requires": { + "tinyspy": "^3.0.2" + } + }, + "@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "requires": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + } + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "requires": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + } + }, + "chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true + }, + "check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true + }, + "console-table-printer": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz", + "integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==", + "dev": true, + "requires": { + "simple-wcswidth": "^1.1.2" + } + }, + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true + }, + "dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "dev": true + }, + "es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0" + } + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "requires": { + "resolve-pkg-maps": "^1.0.0" + } + }, + "is-network-error": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", + "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", + "dev": true + }, + "js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "dev": true, + "requires": { + "base64-js": "^1.5.1" + } + }, + "langchain": { + "version": "1.2.31", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-1.2.31.tgz", + "integrity": "sha512-dKafIQKyNUoMlZX7NaEaDt9pqofZbsF2UoJkPrybC99vV9QbUEiLKT0yGdixA3qFHXmsFdJWpu4n4a+B87Q2Zw==", + "dev": true, + "requires": { + "@langchain/langgraph": "^1.1.2", + "@langchain/langgraph-checkpoint": "^1.0.0", + "langsmith": ">=0.5.0 <1.0.0", + "uuid": "^11.1.0", + "zod": "^3.25.76 || ^4" + } + }, + "langsmith": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.5.9.tgz", + "integrity": "sha512-mT88HlYTz7IZzVJP/OM7D6PAR3kzcbPGjYDJpx64ua9WiJXFqQx6YKewxIIxkVSZCb9I/9TiYvPYfZ4rawOdEg==", + "dev": true, + "requires": { + "@types/uuid": "^10.0.0", + "chalk": "^5.6.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "dependencies": { + "uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true + } + } + }, + "loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, + "magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true + }, + "nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true + }, + "p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + } + }, + "p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "dev": true, + "requires": { + "is-network-error": "^1.1.0" + } + }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, + "pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "requires": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true + }, + "rollup": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "@types/estree": "1.0.8", + "fsevents": "~2.3.2" + } + }, + "semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true + }, + "siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "dev": true + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true + }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, + "tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true + }, + "tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true + }, + "tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true + }, + "tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "requires": { + "esbuild": "~0.27.0", + "fsevents": "~2.3.3", + "get-tsconfig": "^4.7.5" + } + }, + "typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "dev": true + }, + "vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "requires": { + "esbuild": "^0.21.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "dependencies": { + "@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "dev": true, + "optional": true + }, + "esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + } + } + }, + "vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "requires": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + } + }, + "vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "requires": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + } + }, + "why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "requires": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + } + }, + "ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "requires": {} + }, + "zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" } } } diff --git a/package.json b/package.json index 41aad6e..e5c0ef3 100644 --- a/package.json +++ b/package.json @@ -60,11 +60,12 @@ "example": "tsx examples/simple-agent.ts" }, "dependencies": { - "ws": "^8.18.0" + "ws": "^8.18.0", + "zod": "^3.25.76" }, "devDependencies": { - "@langchain/core": "^1.0.4", "@biomejs/biome": "^2.3.12", + "@langchain/core": "^1.0.4", "@types/node": "^20.0.0", "@types/ws": "^8.5.0", "dotenv": "^17.2.3", diff --git a/src/client.ts b/src/client.ts index 9cb74ba..cd0b645 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,6 +1,7 @@ import { randomUUID } from 'node:crypto'; import WebSocket from 'ws'; import type { Annotater } from './annotate.js'; +import { runWithLLPContext } from './context.js'; import { AlreadyClosedError, AlreadyConnectedError, @@ -12,6 +13,7 @@ import { } from './errors.js'; import { PresenceMessage, TextMessage, type ToolCall } from './message.js'; import { ConnectionStatus, PresenceStatus } from './presence.js'; +import { LLPSession } from './session.js'; export interface LLPClientConfig { readonly url?: string; // Default: wss://llphq.com/agent/websocket @@ -20,10 +22,46 @@ export interface LLPClientConfig { readonly maxQueueSize?: number; // Default: 32 } -export type MessageHandler = (annotater: Annotater, msg: TextMessage) => Promise; -export type PresenceHandler = (msg: PresenceMessage) => void | Promise; +// --------------------------------------------------------------------------- +// Legacy handler types (backward compat) +// --------------------------------------------------------------------------- + +export type MessageHandler = ( + session: LLPSession, + msg: TextMessage, +) => Promise; +export type StartHandler = ( + session: LLPSession, + msg: PresenceMessage, +) => TSessionData | undefined | Promise; +export type PresenceHandler = StartHandler; + +// --------------------------------------------------------------------------- +// New chainable API types +// --------------------------------------------------------------------------- + +export type SessionFactory = () => T | Promise; +export type TypedMessageHandler = (data: T, msg: TextMessage) => Promise; +export type StopHandler = (data: T) => void | Promise; + +/** + * Typed interface returned by `onStart()` for chaining. + * The generic `T` is inferred from the factory return type. + */ +export interface TypedLLPClient { + onMessage(handler: TypedMessageHandler): TypedLLPClient; + onStop(handler: StopHandler): TypedLLPClient; + connect(timeout?: number): Promise; + close(): Promise; + getStatus(): ConnectionStatus; + getSessionId(): string | null; + getPresence(): PresenceStatus; + sendMessage(msg: TextMessage, timeout?: number): Promise; + sendAsyncMessage(msg: TextMessage): Promise; + annotateToolCall(toolCall: ToolCall): Promise; +} -export class LLPClient implements Annotater { +export class LLPClient implements Annotater { private ws: WebSocket | null = null; private status: ConnectionStatus = ConnectionStatus.Disconnected; private sessionId: string | null = null; @@ -37,9 +75,16 @@ export class LLPClient implements Annotater { reject: (err: Error) => void; } >(); + private readonly sessions = new Map>(); + + // Legacy handlers + private messageHandler: MessageHandler | null = null; + private startHandler: StartHandler | null = null; - private messageHandler: MessageHandler | null = null; - private presenceHandler: PresenceHandler | null = null; + // New API handlers + private startFactory: SessionFactory | null = null; + private typedMessageHandler: TypedMessageHandler | null = null; + private stopHandler: StopHandler | null = null; private authResolve: (() => void) | null = null; private authReject: ((err: Error) => void) | null = null; @@ -52,6 +97,90 @@ export class LLPClient implements Annotater { private readonly config: LLPClientConfig = {}, ) {} + // ------------------------------------------------------------------- + // New chainable API + // ------------------------------------------------------------------- + + /** + * Register a factory that creates session data for each new user session. + * The SDK calls this automatically when a user becomes available. + * + * Returns a typed client for chaining — the generic is inferred from + * the factory return type. + * + * ```ts + * new LLPClient(name, key, config) + * .onStart(() => createAgent({ model, tools, middleware: [LLPAnnotationMiddleware] })) + * .onMessage(async (agent, msg) => { + * const result = await agent.invoke({ messages: [new HumanMessage(msg.prompt)] }); + * return msg.reply(extractText(result)); + * }) + * .connect(); + * ``` + */ + onStart(factory: SessionFactory): TypedLLPClient; + /** @deprecated Use the factory overload: `onStart(() => createAgent(...))` */ + onStart(handler: StartHandler): void; + onStart( + factoryOrHandler: SessionFactory | StartHandler, + ): TypedLLPClient | undefined { + if (this.status !== ConnectionStatus.Disconnected && this.status !== ConnectionStatus.Closed) { + throw new AlreadyConnectedError('Must set onStart callback before connecting'); + } + + // Distinguish by arity: factory is 0-arg, legacy handler is 2-arg + if (factoryOrHandler.length === 0) { + this.startFactory = factoryOrHandler as SessionFactory; + return this as unknown as TypedLLPClient; + } + + this.startHandler = factoryOrHandler as StartHandler; + return undefined; + } + + /** + * Register a message handler (new API — called via chaining after onStart). + * The first argument is the session data created by the onStart factory. + */ + onMessage(handler: TypedMessageHandler): TypedLLPClient; + /** @deprecated Use the new API: `onStart(...).onMessage((data, msg) => ...)` */ + onMessage(handler: MessageHandler): void; + onMessage( + handler: TypedMessageHandler | MessageHandler, + ): TypedLLPClient | undefined { + if (this.status !== ConnectionStatus.Disconnected && this.status !== ConnectionStatus.Closed) { + throw new AlreadyConnectedError('Must set onMessage callback before connecting'); + } + + // If startFactory is set, we're in the new API path + if (this.startFactory) { + this.typedMessageHandler = handler as TypedMessageHandler; + return this as unknown as TypedLLPClient; + } + + this.messageHandler = handler as MessageHandler; + return undefined; + } + + /** + * Register a cleanup handler called when a user session ends. + */ + onStop(handler: StopHandler): TypedLLPClient { + if (this.status !== ConnectionStatus.Disconnected && this.status !== ConnectionStatus.Closed) { + throw new AlreadyConnectedError('Must set onStop callback before connecting'); + } + this.stopHandler = handler as StopHandler; + return this as unknown as TypedLLPClient; + } + + onPresence(handler: PresenceHandler): void { + this.onStart(handler); + } + + // ------------------------------------------------------------------- + // Connection lifecycle + // ------------------------------------------------------------------- + async connect(timeout?: number): Promise { const timeoutMs = timeout ?? this.config.connectTimeout ?? 10000; @@ -84,7 +213,7 @@ export class LLPClient implements Annotater { reject(err); }; - this.ws.on('message', (data) => this.handleMessage(data)); + this.ws.on('message', (data) => this.handleRawMessage(data)); this.ws.on('close', () => this.handleDisconnect()); this.ws.on('error', (err) => this.handleError(err)); }); @@ -114,6 +243,10 @@ export class LLPClient implements Annotater { }); } + // ------------------------------------------------------------------- + // Messaging + // ------------------------------------------------------------------- + async sendMessage(msg: TextMessage, timeout?: number): Promise { if (this.status !== ConnectionStatus.Authenticated) { throw new NotAuthenticatedError('Must be authenticated to send messages'); @@ -157,19 +290,9 @@ export class LLPClient implements Annotater { this.enqueue(msg.encode()); } - onMessage(handler: MessageHandler): void { - if (this.status !== ConnectionStatus.Disconnected && this.status !== ConnectionStatus.Closed) { - throw new AlreadyConnectedError('Must set onMessage callback before connecting'); - } - this.messageHandler = handler; - } - - onPresence(handler: PresenceHandler): void { - if (this.status !== ConnectionStatus.Disconnected && this.status !== ConnectionStatus.Closed) { - throw new AlreadyConnectedError('Must set onMessage callback before connecting'); - } - this.presenceHandler = handler; - } + // ------------------------------------------------------------------- + // Getters + // ------------------------------------------------------------------- getStatus(): ConnectionStatus { return this.status; @@ -183,6 +306,10 @@ export class LLPClient implements Annotater { return this.presence; } + // ------------------------------------------------------------------- + // Private — WebSocket plumbing + // ------------------------------------------------------------------- + private authenticate(): void { const authMessage = { type: 'authenticate', @@ -223,7 +350,11 @@ export class LLPClient implements Annotater { } } - private handleMessage(data: WebSocket.RawData): void { + // ------------------------------------------------------------------- + // Private — message dispatch + // ------------------------------------------------------------------- + + private handleRawMessage(data: WebSocket.RawData): void { try { const text = data.toString(); const json = JSON.parse(text) as Record; @@ -272,10 +403,31 @@ export class LLPClient implements Annotater { return; } - // Otherwise, call the message handler + const session = this.getOrCreateSession(msg.sender); + + // New API path: typed message handler with AsyncLocalStorage context + if (this.typedMessageHandler) { + try { + const data = session.data; + if (!data) { + console.error(`No session data initialized for sender ${session.id}`); + return; + } + const handler = this.typedMessageHandler; + const reply = await runWithLLPContext({ llpMessage: msg, llpClient: session }, () => + handler(data, msg), + ); + await this.sendAsyncMessage(reply); + } catch (err) { + console.error('Error in message handler:', err); + } + return; + } + + // Legacy API path if (this.messageHandler) { try { - const reply = await this.messageHandler(this, msg); + const reply = await this.messageHandler(session, msg); await this.sendAsyncMessage(reply); } catch (err) { console.error('Error in message handler:', err); @@ -285,10 +437,66 @@ export class LLPClient implements Annotater { private handlePresenceMessage(json: Record): void { const presence = PresenceMessage.decode(json); - if (this.presenceHandler) { - Promise.resolve(this.presenceHandler(presence)).catch((err) => { - console.error('Error in presence handler:', err); - }); + const session = this.getOrCreateSession(presence.sender); + + // New API path: factory + stop handler + if (this.startFactory) { + if (presence.status === PresenceStatus.Available) { + session.clearData(); + Promise.resolve(this.startFactory()) + .then((data) => { + if (data !== undefined) { + session.setData(data as TSessionData); + } + }) + .catch((err) => { + console.error('Error in onStart factory:', err); + }); + } else if (presence.status === PresenceStatus.Unavailable) { + const data = session.data; + if (data && this.stopHandler) { + Promise.resolve(this.stopHandler(data)) + .catch((err) => { + console.error('Error in onStop handler:', err); + }) + .finally(() => { + session.clear(); + this.sessions.delete(presence.sender); + }); + } else { + session.clear(); + this.sessions.delete(presence.sender); + } + } + return; + } + + // Legacy API path + if (this.startHandler) { + Promise.resolve(this.startHandler(session, presence)) + .then((data) => { + if (presence.status === PresenceStatus.Available) { + session.clearData(); + if (data !== undefined) { + session.setData(data); + } + } + }) + .catch((err) => { + console.error('Error in start handler:', err); + }) + .finally(() => { + if (presence.status === PresenceStatus.Unavailable) { + session.clear(); + this.sessions.delete(presence.sender); + } + }); + return; + } + + if (presence.status === PresenceStatus.Unavailable) { + session.clear(); + this.sessions.delete(presence.sender); } } @@ -333,6 +541,10 @@ export class LLPClient implements Annotater { pending.reject(new NotConnectedError('Disconnected from server')); } this.pending.clear(); + for (const session of this.sessions.values()) { + session.clear(); + } + this.sessions.clear(); } } @@ -345,4 +557,15 @@ export class LLPClient implements Annotater { this.authResolve = null; } } + + private getOrCreateSession(id: string): LLPSession { + const existing = this.sessions.get(id); + if (existing) { + return existing; + } + + const session = new LLPSession(id, this); + this.sessions.set(id, session); + return session; + } } diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 0000000..07e3c34 --- /dev/null +++ b/src/context.ts @@ -0,0 +1,31 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import type { Annotater } from './annotate.js'; +import type { TextMessage } from './message.js'; + +export interface LLPContext { + readonly llpMessage: TextMessage; + readonly llpClient: Annotater; +} + +const storage = new AsyncLocalStorage(); + +/** + * Runs the given function within an LLP async context. + * The middleware reads from this context automatically — + * no need for developers to pass context manually. + */ +export function runWithLLPContext(ctx: LLPContext, fn: () => T): T { + return storage.run(ctx, fn); +} + +/** + * Retrieves the current LLP context from AsyncLocalStorage. + * Throws if called outside of a `runWithLLPContext` scope. + */ +export function getLLPContext(): LLPContext { + const ctx = storage.getStore(); + if (!ctx) { + throw new Error('LLP context not available — this must be called inside an onMessage handler'); + } + return ctx; +} diff --git a/src/index.ts b/src/index.ts index b16cc47..e80b006 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,20 @@ // Annotater interface export type { Annotater } from './annotate.js'; - // Core client export { LLPClient, type LLPClientConfig, + // Legacy handler types (backward compat) type MessageHandler, type PresenceHandler, + type SessionFactory, + type StartHandler, + type StopHandler, + type TypedLLPClient, + type TypedMessageHandler, } from './client.js'; - +// Async context for middleware +export { getLLPContext, type LLPContext, runWithLLPContext } from './context.js'; // Error types and codes export { AlreadyClosedError, @@ -21,10 +27,8 @@ export { TextMessageReplyError, TimeoutError, } from './errors.js'; - // Message types export { PresenceMessage, TextMessage, ToolCall } from './message.js'; - // Status enums export { ConnectionStatus, @@ -32,3 +36,4 @@ export { PresenceStatus, type PresenceStatus as PresenceStatusType, } from './presence.js'; +export { LLPSession, LLPSession as Session } from './session.js'; diff --git a/src/langchain.ts b/src/langchain.ts index 0161f5f..4dae592 100644 --- a/src/langchain.ts +++ b/src/langchain.ts @@ -2,37 +2,28 @@ * LangChain integration for the LLP SDK. * * Import via the sub-path export: - * import { createLLPToolMiddleware } from 'llpsdk/langchain'; + * import { LLPAnnotationMiddleware } from 'llpsdk/langchain'; * * Requires `langchain` as a peer dependency. */ import { createMiddleware } from 'langchain'; +import { z } from 'zod'; import type { Annotater } from './annotate.js'; +import { getLLPContext } from './context.js'; import type { TextMessage } from './message.js'; -// LangChain calls these methods by name at runtime. -// We type only what we need from @langchain/core to stay dependency-light. -interface ToolInfo { - name?: string; -} - -interface ToolCallRequest { - toolCall: { - name: string; - args: unknown; - }; -} - -type ToolCallHandler = (request: ToolCallRequest) => Promise; - export interface LLPToolMiddlewareOptions { - name?: string; onAnnotationError?: (error: unknown) => void; serializeArgs?: (value: unknown) => string; serializeResult?: (value: unknown) => string; } +export const llpMiddlewareContextSchema = z.object({ + llpMessage: z.custom(), + llpClient: z.custom(), +}); + function serialize(value: unknown): string { try { return typeof value === 'string' ? value : JSON.stringify(value); @@ -43,27 +34,31 @@ function serialize(value: unknown): string { /** * LangChain v1 middleware helper that captures tool calls and reports them - * to the LLP platform via `annotater.annotateToolCall()`. + * to the LLP platform via `llpClient.annotateToolCall()`. * - * Instantiate once per inbound message and pass it to `createAgent()`: + * Pass the LLP runtime context when invoking the agent: * * ```ts - * client.onMessage(async (annotater, msg) => { - * const agent = createAgent({ - * model, - * tools, - * middleware: [createLLPToolMiddleware(msg, annotater)], - * }); - * const result = await agent.invoke({ messages }); + * const agent = createAgent({ + * model, + * tools, + * middleware: [createLLPToolMiddleware()], + * }); + * + * client.onMessage(async (session, msg) => { + * const result = await agent.invoke( + * { messages }, + * { context: { llpMessage: msg, llpClient: session } }, + * ); * return msg.reply(String(result.messages.at(-1)?.content ?? '')); * }); * ``` */ -export function createLLPToolMiddleware( - msg: TextMessage, - annotater: Annotater, - options: LLPToolMiddlewareOptions = {}, -) { +/** + * @deprecated Use `LLPAnnotationMiddleware` instead. This older API requires + * manually passing `{ context: { llpMessage, llpClient } }` on every invoke call. + */ +export function createLLPToolMiddleware(options: LLPToolMiddlewareOptions = {}) { const serializeArgs = options.serializeArgs ?? serialize; const serializeResult = options.serializeResult ?? serialize; const onAnnotationError = @@ -71,25 +66,32 @@ export function createLLPToolMiddleware( ((error: unknown) => console.warn('[LLP] Failed to annotate tool call:', error)); return createMiddleware({ - name: options.name ?? 'LLPToolCallMiddleware', - wrapToolCall: async (request: ToolCallRequest, handler: ToolCallHandler) => { + name: 'LLPToolCallMiddleware', + contextSchema: llpMiddlewareContextSchema, + wrapToolCall: async (request, handler) => { const startMs = Date.now(); + const { llpMessage, llpClient } = request.runtime.context; const toolName = request.toolCall.name; const parameters = serializeArgs(request.toolCall.args); try { const result = await handler(request); - await annotater + await llpClient .annotateToolCall( - msg.toolCall(toolName, parameters, serializeResult(result), Date.now() - startMs), + llpMessage.toolCall( + toolName, + parameters, + serializeResult(result), + Date.now() - startMs, + ), ) .catch(onAnnotationError); return result; } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); - await annotater + await llpClient .annotateToolCall( - msg.toolCallException(toolName, parameters, err, Date.now() - startMs), + llpMessage.toolCallException(toolName, parameters, err, Date.now() - startMs), ) .catch(onAnnotationError); throw error; @@ -98,59 +100,55 @@ export function createLLPToolMiddleware( }); } +// --------------------------------------------------------------------------- +// New API — reads context from AsyncLocalStorage automatically +// --------------------------------------------------------------------------- + /** - * Legacy LangChain callback handler that captures tool calls and reports them - * to the LLP platform via `annotater.annotateToolCall()`. - * - * Instantiate once per inbound message and pass to `chain.invoke()`: + * LangChain middleware that captures tool calls and reports them to the + * LLP platform. Context is read from AsyncLocalStorage automatically — + * no manual context passing needed. * * ```ts - * client.onMessage(async (annotater, msg) => { - * const middleware = new LLPToolCallMiddleware(msg, annotater); - * const result = await chain.invoke(input, { callbacks: [middleware] }); - * return msg.reply(result); - * }); + * new LLPClient(name, key, config) + * .onStart(() => createAgent({ + * model, + * tools, + * middleware: [LLPAnnotationMiddleware], + * })) + * .onMessage(async (agent, msg) => { + * const result = await agent.invoke({ messages: [...] }); + * return msg.reply(extractText(result)); + * }) + * .connect(); * ``` */ -export class LLPToolCallMiddleware { - private readonly pending = new Map< - string, - { name: string; parameters: string; startMs: number } - >(); - - constructor( - private readonly msg: TextMessage, - private readonly annotater: Annotater, - ) {} - - handleToolStart(tool: ToolInfo | null | undefined, input: unknown, runId: string): void { - const name = tool?.name ?? 'unknown'; - const parameters = serialize(input); - this.pending.set(runId, { name, parameters, startMs: Date.now() }); - } - - handleToolEnd(output: unknown, runId: string): void { - const entry = this.pending.get(runId); - if (!entry) return; - this.pending.delete(runId); - - const durationMs = Date.now() - entry.startMs; - const tc = this.msg.toolCall(entry.name, entry.parameters, serialize(output), durationMs); - this.annotater - .annotateToolCall(tc) - .catch((err) => console.warn('[LLP] Failed to annotate tool call:', err)); - } - - handleToolError(err: unknown, runId: string): void { - const entry = this.pending.get(runId); - if (!entry) return; - this.pending.delete(runId); - - const durationMs = Date.now() - entry.startMs; - const error = err instanceof Error ? err : new Error(String(err)); - const tc = this.msg.toolCallException(entry.name, entry.parameters, error, durationMs); - this.annotater - .annotateToolCall(tc) - .catch((e) => console.warn('[LLP] Failed to annotate tool call exception:', e)); - } -} +export const LLPAnnotationMiddleware = createMiddleware({ + name: 'LLPAnnotationMiddleware', + wrapToolCall: async (request, handler) => { + const startMs = Date.now(); + const { llpMessage, llpClient } = getLLPContext(); + const toolName = request.toolCall.name; + const parameters = serialize(request.toolCall.args); + + try { + const result = await handler(request); + await llpClient + .annotateToolCall( + llpMessage.toolCall(toolName, parameters, serialize(result), Date.now() - startMs), + ) + .catch((error: unknown) => console.warn('[LLP] Failed to annotate tool call:', error)); + return result; + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + await llpClient + .annotateToolCall( + llpMessage.toolCallException(toolName, parameters, err, Date.now() - startMs), + ) + .catch((annotateErr: unknown) => + console.warn('[LLP] Failed to annotate tool call:', annotateErr), + ); + throw error; + } + }, +}); diff --git a/src/session.ts b/src/session.ts new file mode 100644 index 0000000..744119c --- /dev/null +++ b/src/session.ts @@ -0,0 +1,45 @@ +import type { Annotater } from './annotate.js'; +import type { ToolCall } from './message.js'; + +export class LLPSession implements Annotater { + private readonly values = new Map(); + private sessionData: TData | undefined; + + constructor( + public readonly id: string, + private readonly annotater: Annotater, + ) {} + + async annotateToolCall(toolCall: ToolCall): Promise { + await this.annotater.annotateToolCall(toolCall); + } + + get data(): TData | undefined { + return this.sessionData; + } + + setData(value: TData): void { + this.sessionData = value; + } + + clearData(): void { + this.sessionData = undefined; + } + + get(key: string): T | undefined { + return this.values.get(key) as T | undefined; + } + + set(key: string, value: T): void { + this.values.set(key, value); + } + + delete(key: string): boolean { + return this.values.delete(key); + } + + clear(): void { + this.sessionData = undefined; + this.values.clear(); + } +} diff --git a/tests/client.test.ts b/tests/client.test.ts index 44ca3e7..9125c5f 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -209,17 +209,17 @@ describe('LLPClient', () => { session_id: 'session-123', }, }); - messageHandler?.(Buffer.from(authResponse)); - await connectPromise; - const throwError = () => - client.onMessage(async (_annotater, msg: TextMessage) => { - return msg; - }); - expect(throwError).toThrow(/before connecting/); - }); + messageHandler?.(Buffer.from(authResponse)); + await connectPromise; + const throwError = () => + client.onMessage(async (_session, msg: TextMessage) => { + return msg; + }); + expect(throwError).toThrow(/before connecting/); + }); }); - describe('onPresence', () => { + describe('onStart', () => { it('should throw if client is connected', async () => { const connectPromise = client.connect(); const openHandler = mockWs.on.mock.calls.find((call) => call[0] === 'open')?.[1]; @@ -235,7 +235,7 @@ describe('LLPClient', () => { }); messageHandler?.(Buffer.from(authResponse)); await connectPromise; - const throwError = () => client.onPresence(async (_msg: PresenceMessage) => {}); + const throwError = () => client.onStart(async (_session, _msg: PresenceMessage) => {}); expect(throwError).toThrow(/before connecting/); }); }); diff --git a/tests/handler.test.ts b/tests/handler.test.ts index 6cfa5c8..33f3a88 100644 --- a/tests/handler.test.ts +++ b/tests/handler.test.ts @@ -1,70 +1,76 @@ import { describe, expect, it } from 'vitest'; -import type { Annotater } from '../src/annotate.js'; import { PresenceMessage, TextMessage } from '../src/message.js'; import { PresenceStatus } from '../src/presence.js'; +import { LLPSession } from '../src/session.js'; describe('Message Handlers', () => { describe('MessageHandler type', () => { - const annotater: Annotater = { + const session = new LLPSession('alice', { annotateToolCall: async () => {}, - }; + }); it('should accept async function that returns TextMessage', async () => { - const handler = async (_annotater: Annotater, msg: TextMessage): Promise => { + const handler = async ( + _session: LLPSession, + msg: TextMessage, + ): Promise => { return msg.reply('Response'); }; const input = new TextMessage('bob', 'Hello', null, 'msg-1', 'alice'); - const result = await handler(annotater, input); + const result = await handler(session, input); expect(result.prompt).toBe('Response'); expect(result.recipient).toBe('alice'); }); it('should allow handler to process message content', async () => { - const handler = async (_annotater: Annotater, msg: TextMessage): Promise => { + const handler = async ( + _session: LLPSession, + msg: TextMessage, + ): Promise => { const upperPrompt = msg.prompt.toUpperCase(); return msg.reply(`Echo: ${upperPrompt}`); }; const input = new TextMessage('bob', 'hello world'); - const result = await handler(annotater, input); + const result = await handler(session, input); expect(result.prompt).toBe('Echo: HELLO WORLD'); }); }); describe('PresenceHandler type', () => { it('should accept void function', () => { - const handler = (msg: PresenceMessage): void => { + const handler = (_session: LLPSession, msg: PresenceMessage): void => { expect(msg.sender).toBeDefined(); }; const presence = new PresenceMessage(PresenceStatus.Available, 'alice'); - handler(presence); + handler(new LLPSession('alice', { annotateToolCall: async () => {} }), presence); }); it('should accept async void function', async () => { - const handler = async (msg: PresenceMessage): Promise => { + const handler = async (_session: LLPSession, msg: PresenceMessage): Promise => { await new Promise((resolve) => setTimeout(resolve, 1)); expect(msg.status).toBe(PresenceStatus.Unavailable); }; const presence = new PresenceMessage(PresenceStatus.Unavailable, 'bob'); - await handler(presence); + await handler(new LLPSession('bob', { annotateToolCall: async () => {} }), presence); }); it('should allow side effects in handler', async () => { const log: string[] = []; - const handler = (msg: PresenceMessage): void => { - log.push(`${msg.sender}: ${msg.status}`); + const handler = (session: LLPSession, msg: PresenceMessage): void => { + log.push(`${session.id}: ${msg.status}`); }; const p1 = new PresenceMessage(PresenceStatus.Available, 'alice'); const p2 = new PresenceMessage(PresenceStatus.Unavailable, 'bob'); - handler(p1); - handler(p2); + handler(new LLPSession('alice', { annotateToolCall: async () => {} }), p1); + handler(new LLPSession('bob', { annotateToolCall: async () => {} }), p2); expect(log).toEqual(['alice: available', 'bob: unavailable']); }); diff --git a/tests/integration.test.ts b/tests/integration.test.ts index a9579c7..ab55cae 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -26,7 +26,7 @@ describe('LLPClient Integration Tests', () => { close: ReturnType; readyState: number; }; - let client: LLPClient; + let client: LLPClient; beforeEach(async () => { vi.clearAllMocks(); @@ -49,7 +49,7 @@ describe('LLPClient Integration Tests', () => { return mockWs as unknown as WebSocket; }); - client = new LLPClient('test-agent', 'test-key'); + client = new LLPClient('test-agent', 'test-key'); }); async function connectClient(): Promise { @@ -113,7 +113,8 @@ describe('LLPClient Integration Tests', () => { it('should handle incoming messages and send replies', async () => { const replies: TextMessage[] = []; - client.onMessage(async (_annotater, msg: TextMessage) => { + client.onMessage(async (session, msg: TextMessage) => { + expect(session.id).toBe('alice'); const reply = msg.reply(`Echo: ${msg.prompt}`); replies.push(reply); return reply; @@ -123,7 +124,13 @@ describe('LLPClient Integration Tests', () => { const messageHandler = mockWs.on.mock.calls.find((call) => call[0] === 'message')?.[1]; // Simulate incoming message - const incomingMsg = new TextMessage('test-agent', 'Hello!', 'msg-incoming', 'alice'); + const incomingMsg = new TextMessage( + 'test-agent', + 'Hello!', + undefined, + 'msg-incoming', + 'alice', + ); messageHandler?.(Buffer.from(incomingMsg.encode())); @@ -147,9 +154,9 @@ describe('LLPClient Integration Tests', () => { describe('Presence handling', () => { it('should receive and process presence updates', async () => { - const presenceUpdates: PresenceMessage[] = []; - client.onPresence((msg: PresenceMessage) => { - presenceUpdates.push(msg); + const presenceUpdates: Array<{ sessionId: string; msg: PresenceMessage }> = []; + client.onStart((session, msg: PresenceMessage) => { + presenceUpdates.push({ sessionId: session.id, msg }); }); await connectClient(); @@ -172,15 +179,17 @@ describe('LLPClient Integration Tests', () => { await new Promise((resolve) => setTimeout(resolve, 10)); expect(presenceUpdates).toHaveLength(1); - expect(presenceUpdates[0]?.sender).toBe('alice'); - expect(presenceUpdates[0]?.status).toBe(PresenceStatus.Available); + expect(presenceUpdates[0]?.sessionId).toBe('alice'); + expect(presenceUpdates[0]?.msg.sender).toBe('alice'); + expect(presenceUpdates[0]?.msg.status).toBe(PresenceStatus.Available); }); it('should handle async presence handlers', async () => { let handlerCalled = false; - client.onPresence(async (msg: PresenceMessage) => { + client.onStart(async (session, msg: PresenceMessage) => { await new Promise((resolve) => setTimeout(resolve, 5)); handlerCalled = true; + expect(session.id).toBe('bob'); expect(msg.sender).toBe('bob'); }); @@ -204,6 +213,73 @@ describe('LLPClient Integration Tests', () => { expect(handlerCalled).toBe(true); }); + + it('should isolate session state by sender and clear it on unavailable', async () => { + const seenValues: Array = []; + + client.onStart((_session, msg: PresenceMessage) => { + if (msg.status === PresenceStatus.Available) { + return `agent-for-${msg.sender}`; + } + }); + + client.onMessage(async (session, msg: TextMessage) => { + seenValues.push(session.data); + return msg.reply(`Ack: ${msg.prompt}`); + }); + + await connectClient(); + + const messageHandler = mockWs.on.mock.calls.find((call) => call[0] === 'message')?.[1]; + + const alicePresence = JSON.stringify({ + type: 'presence', + id: 'pres-alice', + from: 'alice', + data: { status: 'available' }, + }); + const bobPresence = JSON.stringify({ + type: 'presence', + id: 'pres-bob', + from: 'bob', + data: { status: 'available' }, + }); + messageHandler?.(Buffer.from(alicePresence)); + messageHandler?.(Buffer.from(bobPresence)); + + // Wait for presence handler microtasks (setData) to resolve + await new Promise((resolve) => setTimeout(resolve, 10)); + + const aliceMsg = new TextMessage('test-agent', 'hello', undefined, 'msg-alice', 'alice'); + const bobMsg = new TextMessage('test-agent', 'hi', undefined, 'msg-bob', 'bob'); + messageHandler?.(Buffer.from(aliceMsg.encode())); + messageHandler?.(Buffer.from(bobMsg.encode())); + + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(seenValues).toEqual(['agent-for-alice', 'agent-for-bob']); + + const aliceUnavailable = JSON.stringify({ + type: 'presence', + id: 'pres-alice-off', + from: 'alice', + data: { status: 'unavailable' }, + }); + messageHandler?.(Buffer.from(aliceUnavailable)); + await new Promise((resolve) => setTimeout(resolve, 20)); + + const aliceMsgAfter = new TextMessage( + 'test-agent', + 'hello again', + undefined, + 'msg-alice-2', + 'alice', + ); + messageHandler?.(Buffer.from(aliceMsgAfter.encode())); + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(seenValues).toEqual(['agent-for-alice', 'agent-for-bob', undefined]); + }); }); describe('Error handling', () => { diff --git a/tests/langchain.test.ts b/tests/langchain.test.ts index 8c49e54..d0298e9 100644 --- a/tests/langchain.test.ts +++ b/tests/langchain.test.ts @@ -14,23 +14,40 @@ describe('LangChain integration', () => { }); it('creates middleware that annotates successful tool calls', async () => { - const { createLLPToolMiddleware } = await import('../src/langchain.js'); + const { createLLPToolMiddleware, llpMiddlewareContextSchema } = await import( + '../src/langchain.js' + ); const annotateToolCall = vi.fn().mockResolvedValue(undefined); const annotater: Annotater = { annotateToolCall }; - const msg = new TextMessage('financial-advisor', 'Summarize the invoice', undefined, 'msg-123', 'user-1'); + const msg = new TextMessage( + 'financial-advisor', + 'Summarize the invoice', + undefined, + 'msg-123', + 'user-1', + ); - const middleware = createLLPToolMiddleware(msg, annotater); + const callsBefore = createMiddlewareMock.mock.calls.length; + const middleware = createLLPToolMiddleware(); const result = await middleware.wrapToolCall( { toolCall: { name: 'load_invoice_pdf', args: { url: 'https://example.com/invoice.pdf' }, }, + runtime: { + context: { + llpMessage: msg, + llpClient: annotater, + }, + }, }, async () => 'Invoice total: $50', ); - expect(createMiddlewareMock).toHaveBeenCalledTimes(1); + expect(createMiddlewareMock).toHaveBeenCalledTimes(callsBefore + 1); + expect(middleware.name).toBe('LLPToolCallMiddleware'); + expect(middleware.contextSchema).toBe(llpMiddlewareContextSchema); expect(result).toBe('Invoice total: $50'); expect(annotateToolCall).toHaveBeenCalledTimes(1); @@ -45,8 +62,14 @@ describe('LangChain integration', () => { const { createLLPToolMiddleware } = await import('../src/langchain.js'); const annotateToolCall = vi.fn().mockResolvedValue(undefined); const annotater: Annotater = { annotateToolCall }; - const msg = new TextMessage('financial-advisor', 'Summarize the invoice', undefined, 'msg-123', 'user-1'); - const middleware = createLLPToolMiddleware(msg, annotater); + const msg = new TextMessage( + 'financial-advisor', + 'Summarize the invoice', + undefined, + 'msg-123', + 'user-1', + ); + const middleware = createLLPToolMiddleware(); const error = new Error('network down'); await expect( @@ -56,6 +79,12 @@ describe('LangChain integration', () => { name: 'load_invoice_pdf', args: { url: 'https://example.com/invoice.pdf' }, }, + runtime: { + context: { + llpMessage: msg, + llpClient: annotater, + }, + }, }, async () => { throw error; From 809bd5b73a1faec37d6136f372bda9825252339e Mon Sep 17 00:00:00 2001 From: Gagan Singh Date: Thu, 12 Mar 2026 10:56:48 -0700 Subject: [PATCH 5/7] sdk updates --- package-lock.json | 93 +---------------------------------------------- src/client.ts | 59 +++++++++++++++--------------- src/index.ts | 4 +- src/langchain.ts | 79 ++++++---------------------------------- src/session.ts | 33 +++++++++++------ 5 files changed, 65 insertions(+), 203 deletions(-) diff --git a/package-lock.json b/package-lock.json index b5daebb..48cb73c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,16 +45,6 @@ "integrity": "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA==", "dev": true, "license": "MIT OR Apache-2.0", - "dependencies": { - "@biomejs/cli-darwin-arm64": "2.3.12", - "@biomejs/cli-darwin-x64": "2.3.12", - "@biomejs/cli-linux-arm64": "2.3.12", - "@biomejs/cli-linux-arm64-musl": "2.3.12", - "@biomejs/cli-linux-x64": "2.3.12", - "@biomejs/cli-linux-x64-musl": "2.3.12", - "@biomejs/cli-win32-arm64": "2.3.12", - "@biomejs/cli-win32-x64": "2.3.12" - }, "bin": { "biome": "bin/biome" }, @@ -1526,34 +1516,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "dependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - }, "bin": { "esbuild": "bin/esbuild" }, @@ -1904,33 +1866,7 @@ "dev": true, "license": "MIT", "dependencies": { - "@rollup/rollup-android-arm-eabi": "4.56.0", - "@rollup/rollup-android-arm64": "4.56.0", - "@rollup/rollup-darwin-arm64": "4.56.0", - "@rollup/rollup-darwin-x64": "4.56.0", - "@rollup/rollup-freebsd-arm64": "4.56.0", - "@rollup/rollup-freebsd-x64": "4.56.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", - "@rollup/rollup-linux-arm-musleabihf": "4.56.0", - "@rollup/rollup-linux-arm64-gnu": "4.56.0", - "@rollup/rollup-linux-arm64-musl": "4.56.0", - "@rollup/rollup-linux-loong64-gnu": "4.56.0", - "@rollup/rollup-linux-loong64-musl": "4.56.0", - "@rollup/rollup-linux-ppc64-gnu": "4.56.0", - "@rollup/rollup-linux-ppc64-musl": "4.56.0", - "@rollup/rollup-linux-riscv64-gnu": "4.56.0", - "@rollup/rollup-linux-riscv64-musl": "4.56.0", - "@rollup/rollup-linux-s390x-gnu": "4.56.0", - "@rollup/rollup-linux-x64-gnu": "4.56.0", - "@rollup/rollup-linux-x64-musl": "4.56.0", - "@rollup/rollup-openbsd-x64": "4.56.0", - "@rollup/rollup-openharmony-arm64": "4.56.0", - "@rollup/rollup-win32-arm64-msvc": "4.56.0", - "@rollup/rollup-win32-ia32-msvc": "4.56.0", - "@rollup/rollup-win32-x64-gnu": "4.56.0", - "@rollup/rollup-win32-x64-msvc": "4.56.0", - "@types/estree": "1.0.8", - "fsevents": "~2.3.2" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -2069,7 +2005,6 @@ "license": "MIT", "dependencies": { "esbuild": "~0.27.0", - "fsevents": "~2.3.3", "get-tsconfig": "^4.7.5" }, "bin": { @@ -2124,7 +2059,6 @@ "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "fsevents": "~2.3.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, @@ -2598,31 +2532,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "dependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - }, "bin": { "esbuild": "bin/esbuild" }, diff --git a/src/client.ts b/src/client.ts index cd0b645..bf5f1b2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,7 +1,6 @@ import { randomUUID } from 'node:crypto'; import WebSocket from 'ws'; import type { Annotater } from './annotate.js'; -import { runWithLLPContext } from './context.js'; import { AlreadyClosedError, AlreadyConnectedError, @@ -40,8 +39,8 @@ export type PresenceHandler = StartHandler // New chainable API types // --------------------------------------------------------------------------- -export type SessionFactory = () => T | Promise; -export type TypedMessageHandler = (data: T, msg: TextMessage) => Promise; +export type SessionCreator = () => T | Promise; +export type TypedMessageHandler = (data: T, msg: TextMessage, annotater: Annotater) => Promise; export type StopHandler = (data: T) => void | Promise; /** @@ -82,7 +81,7 @@ export class LLPClient implements Annotater { private startHandler: StartHandler | null = null; // New API handlers - private startFactory: SessionFactory | null = null; + private sessionCreator: SessionCreator | null = null; private typedMessageHandler: TypedMessageHandler | null = null; private stopHandler: StopHandler | null = null; @@ -102,15 +101,12 @@ export class LLPClient implements Annotater { // ------------------------------------------------------------------- /** - * Register a factory that creates session data for each new user session. + * Register a callback that creates session data for each new user session. * The SDK calls this automatically when a user becomes available. * - * Returns a typed client for chaining — the generic is inferred from - * the factory return type. - * * ```ts * new LLPClient(name, key, config) - * .onStart(() => createAgent({ model, tools, middleware: [LLPAnnotationMiddleware] })) + * .onStart(() => createAgent({ model, tools, middleware: [createLLPToolMiddleware()] })) * .onMessage(async (agent, msg) => { * const result = await agent.invoke({ messages: [new HumanMessage(msg.prompt)] }); * return msg.reply(extractText(result)); @@ -118,29 +114,29 @@ export class LLPClient implements Annotater { * .connect(); * ``` */ - onStart(factory: SessionFactory): TypedLLPClient; - /** @deprecated Use the factory overload: `onStart(() => createAgent(...))` */ + onStart(creator: SessionCreator): TypedLLPClient; + /** @deprecated Use the new overload: `onStart(() => createAgent(...))` */ onStart(handler: StartHandler): void; onStart( - factoryOrHandler: SessionFactory | StartHandler, + creatorOrHandler: SessionCreator | StartHandler, ): TypedLLPClient | undefined { if (this.status !== ConnectionStatus.Disconnected && this.status !== ConnectionStatus.Closed) { throw new AlreadyConnectedError('Must set onStart callback before connecting'); } - // Distinguish by arity: factory is 0-arg, legacy handler is 2-arg - if (factoryOrHandler.length === 0) { - this.startFactory = factoryOrHandler as SessionFactory; + // Distinguish by arity: creator is 0-arg, legacy handler is 2-arg + if (creatorOrHandler.length === 0) { + this.sessionCreator = creatorOrHandler as SessionCreator; return this as unknown as TypedLLPClient; } - this.startHandler = factoryOrHandler as StartHandler; + this.startHandler = creatorOrHandler as StartHandler; return undefined; } /** * Register a message handler (new API — called via chaining after onStart). - * The first argument is the session data created by the onStart factory. + * The first argument is the session data created by onStart. */ onMessage(handler: TypedMessageHandler): TypedLLPClient; /** @deprecated Use the new API: `onStart(...).onMessage((data, msg) => ...)` */ @@ -152,8 +148,8 @@ export class LLPClient implements Annotater { throw new AlreadyConnectedError('Must set onMessage callback before connecting'); } - // If startFactory is set, we're in the new API path - if (this.startFactory) { + // If sessionCreator is set, we're in the new API path + if (this.sessionCreator) { this.typedMessageHandler = handler as TypedMessageHandler; return this as unknown as TypedLLPClient; } @@ -405,18 +401,17 @@ export class LLPClient implements Annotater { const session = this.getOrCreateSession(msg.sender); - // New API path: typed message handler with AsyncLocalStorage context + // New API path: typed message handler if (this.typedMessageHandler) { try { + console.log(`[message] received from sender=${msg.sender} id=${msg.id}`); + await session.waitForData(); const data = session.data; if (!data) { - console.error(`No session data initialized for sender ${session.id}`); + console.error(`[message] no session data for sender=${msg.sender} — was presence received?`); return; } - const handler = this.typedMessageHandler; - const reply = await runWithLLPContext({ llpMessage: msg, llpClient: session }, () => - handler(data, msg), - ); + const reply = await this.typedMessageHandler(data, msg, session); await this.sendAsyncMessage(reply); } catch (err) { console.error('Error in message handler:', err); @@ -437,20 +432,26 @@ export class LLPClient implements Annotater { private handlePresenceMessage(json: Record): void { const presence = PresenceMessage.decode(json); + console.log(`[presence] sender=${presence.sender} status=${presence.status}`); const session = this.getOrCreateSession(presence.sender); - // New API path: factory + stop handler - if (this.startFactory) { + // New API path: session creator + stop handler + if (this.sessionCreator) { if (presence.status === PresenceStatus.Available) { session.clearData(); - Promise.resolve(this.startFactory()) + Promise.resolve(this.sessionCreator()) .then((data) => { if (data !== undefined) { session.setData(data as TSessionData); + console.log(`[presence] session ready for sender=${presence.sender}`); + } else { + console.warn(`[presence] onStart returned undefined for sender=${presence.sender}`); + session.failInit(new Error('onStart returned undefined')); } }) .catch((err) => { - console.error('Error in onStart factory:', err); + console.error(`[presence] onStart failed for sender=${presence.sender}:`, err); + session.failInit(err instanceof Error ? err : new Error(String(err))); }); } else if (presence.status === PresenceStatus.Unavailable) { const data = session.data; diff --git a/src/index.ts b/src/index.ts index e80b006..33918a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,14 +7,12 @@ export { // Legacy handler types (backward compat) type MessageHandler, type PresenceHandler, - type SessionFactory, + type SessionCreator, type StartHandler, type StopHandler, type TypedLLPClient, type TypedMessageHandler, } from './client.js'; -// Async context for middleware -export { getLLPContext, type LLPContext, runWithLLPContext } from './context.js'; // Error types and codes export { AlreadyClosedError, diff --git a/src/langchain.ts b/src/langchain.ts index 4dae592..0471882 100644 --- a/src/langchain.ts +++ b/src/langchain.ts @@ -2,7 +2,7 @@ * LangChain integration for the LLP SDK. * * Import via the sub-path export: - * import { LLPAnnotationMiddleware } from 'llpsdk/langchain'; + * import { createLLPToolMiddleware } from 'llpsdk/langchain'; * * Requires `langchain` as a peer dependency. */ @@ -10,7 +10,6 @@ import { createMiddleware } from 'langchain'; import { z } from 'zod'; import type { Annotater } from './annotate.js'; -import { getLLPContext } from './context.js'; import type { TextMessage } from './message.js'; export interface LLPToolMiddlewareOptions { @@ -33,7 +32,7 @@ function serialize(value: unknown): string { } /** - * LangChain v1 middleware helper that captures tool calls and reports them + * LangChain middleware that captures tool calls and reports them * to the LLP platform via `llpClient.annotateToolCall()`. * * Pass the LLP runtime context when invoking the agent: @@ -45,19 +44,18 @@ function serialize(value: unknown): string { * middleware: [createLLPToolMiddleware()], * }); * - * client.onMessage(async (session, msg) => { - * const result = await agent.invoke( - * { messages }, - * { context: { llpMessage: msg, llpClient: session } }, - * ); - * return msg.reply(String(result.messages.at(-1)?.content ?? '')); - * }); + * client + * .onStart(() => agent) + * .onMessage(async (agent, msg) => { + * const result = await agent.invoke( + * { messages }, + * { context: { llpMessage: msg, llpClient: session } }, + * ); + * return msg.reply(String(result.messages.at(-1)?.content ?? '')); + * }) + * .connect(); * ``` */ -/** - * @deprecated Use `LLPAnnotationMiddleware` instead. This older API requires - * manually passing `{ context: { llpMessage, llpClient } }` on every invoke call. - */ export function createLLPToolMiddleware(options: LLPToolMiddlewareOptions = {}) { const serializeArgs = options.serializeArgs ?? serialize; const serializeResult = options.serializeResult ?? serialize; @@ -99,56 +97,3 @@ export function createLLPToolMiddleware(options: LLPToolMiddlewareOptions = {}) }, }); } - -// --------------------------------------------------------------------------- -// New API — reads context from AsyncLocalStorage automatically -// --------------------------------------------------------------------------- - -/** - * LangChain middleware that captures tool calls and reports them to the - * LLP platform. Context is read from AsyncLocalStorage automatically — - * no manual context passing needed. - * - * ```ts - * new LLPClient(name, key, config) - * .onStart(() => createAgent({ - * model, - * tools, - * middleware: [LLPAnnotationMiddleware], - * })) - * .onMessage(async (agent, msg) => { - * const result = await agent.invoke({ messages: [...] }); - * return msg.reply(extractText(result)); - * }) - * .connect(); - * ``` - */ -export const LLPAnnotationMiddleware = createMiddleware({ - name: 'LLPAnnotationMiddleware', - wrapToolCall: async (request, handler) => { - const startMs = Date.now(); - const { llpMessage, llpClient } = getLLPContext(); - const toolName = request.toolCall.name; - const parameters = serialize(request.toolCall.args); - - try { - const result = await handler(request); - await llpClient - .annotateToolCall( - llpMessage.toolCall(toolName, parameters, serialize(result), Date.now() - startMs), - ) - .catch((error: unknown) => console.warn('[LLP] Failed to annotate tool call:', error)); - return result; - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - await llpClient - .annotateToolCall( - llpMessage.toolCallException(toolName, parameters, err, Date.now() - startMs), - ) - .catch((annotateErr: unknown) => - console.warn('[LLP] Failed to annotate tool call:', annotateErr), - ); - throw error; - } - }, -}); diff --git a/src/session.ts b/src/session.ts index 744119c..916c463 100644 --- a/src/session.ts +++ b/src/session.ts @@ -2,8 +2,8 @@ import type { Annotater } from './annotate.js'; import type { ToolCall } from './message.js'; export class LLPSession implements Annotater { - private readonly values = new Map(); private sessionData: TData | undefined; + private dataReady: { promise: Promise; resolve: () => void; reject: (err: Error) => void } | null = null; constructor( public readonly id: string, @@ -20,26 +20,35 @@ export class LLPSession implements Annotater { setData(value: TData): void { this.sessionData = value; + this.dataReady?.resolve(); + this.dataReady = null; } - clearData(): void { - this.sessionData = undefined; - } - - get(key: string): T | undefined { - return this.values.get(key) as T | undefined; + failInit(err: Error): void { + this.dataReady?.reject(err); + this.dataReady = null; } - set(key: string, value: T): void { - this.values.set(key, value); + clearData(): void { + this.sessionData = undefined; } - delete(key: string): boolean { - return this.values.delete(key); + async waitForData(): Promise { + if (this.sessionData !== undefined) return; + if (!this.dataReady) { + let resolve!: () => void; + let reject!: (err: Error) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + this.dataReady = { promise, resolve, reject }; + } + await this.dataReady.promise; } clear(): void { this.sessionData = undefined; - this.values.clear(); + this.dataReady = null; } } From 953b59bdf4c0514f56f02cc073e3289061d3becf Mon Sep 17 00:00:00 2001 From: Gagan Singh Date: Thu, 12 Mar 2026 20:40:29 -0700 Subject: [PATCH 6/7] formatting fixes --- src/client.ts | 10 ++++++++-- src/session.ts | 6 +++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index bf5f1b2..178d601 100644 --- a/src/client.ts +++ b/src/client.ts @@ -40,7 +40,11 @@ export type PresenceHandler = StartHandler // --------------------------------------------------------------------------- export type SessionCreator = () => T | Promise; -export type TypedMessageHandler = (data: T, msg: TextMessage, annotater: Annotater) => Promise; +export type TypedMessageHandler = ( + data: T, + msg: TextMessage, + annotater: Annotater, +) => Promise; export type StopHandler = (data: T) => void | Promise; /** @@ -408,7 +412,9 @@ export class LLPClient implements Annotater { await session.waitForData(); const data = session.data; if (!data) { - console.error(`[message] no session data for sender=${msg.sender} — was presence received?`); + console.error( + `[message] no session data for sender=${msg.sender} — was presence received?`, + ); return; } const reply = await this.typedMessageHandler(data, msg, session); diff --git a/src/session.ts b/src/session.ts index 916c463..8e29539 100644 --- a/src/session.ts +++ b/src/session.ts @@ -3,7 +3,11 @@ import type { ToolCall } from './message.js'; export class LLPSession implements Annotater { private sessionData: TData | undefined; - private dataReady: { promise: Promise; resolve: () => void; reject: (err: Error) => void } | null = null; + private dataReady: { + promise: Promise; + resolve: () => void; + reject: (err: Error) => void; + } | null = null; constructor( public readonly id: string, From b1fd3c7a29acb18ff97fe318c528bdc9fe9c25e5 Mon Sep 17 00:00:00 2001 From: Michael Slezak Date: Fri, 13 Mar 2026 17:52:07 -0600 Subject: [PATCH 7/7] move ToolCall into its own file. remove unused context --- src/annotate.ts | 2 +- src/client.ts | 3 ++- src/context.ts | 31 ----------------------------- src/index.ts | 3 ++- src/message.ts | 52 +----------------------------------------------- src/session.ts | 2 +- src/tool_call.ts | 42 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 49 insertions(+), 86 deletions(-) delete mode 100644 src/context.ts create mode 100644 src/tool_call.ts diff --git a/src/annotate.ts b/src/annotate.ts index 60f5fb7..74e10d4 100644 --- a/src/annotate.ts +++ b/src/annotate.ts @@ -1,4 +1,4 @@ -import type { ToolCall } from './message.js'; +import type { ToolCall } from './tool_call.js'; export interface Annotater { annotateToolCall(toolCall: ToolCall): Promise; diff --git a/src/client.ts b/src/client.ts index 178d601..cb35727 100644 --- a/src/client.ts +++ b/src/client.ts @@ -10,9 +10,10 @@ import { PlatformError, TimeoutError, } from './errors.js'; -import { PresenceMessage, TextMessage, type ToolCall } from './message.js'; +import { PresenceMessage, TextMessage } from './message.js'; import { ConnectionStatus, PresenceStatus } from './presence.js'; import { LLPSession } from './session.js'; +import type { ToolCall } from './tool_call.js'; export interface LLPClientConfig { readonly url?: string; // Default: wss://llphq.com/agent/websocket diff --git a/src/context.ts b/src/context.ts deleted file mode 100644 index 07e3c34..0000000 --- a/src/context.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { AsyncLocalStorage } from 'node:async_hooks'; -import type { Annotater } from './annotate.js'; -import type { TextMessage } from './message.js'; - -export interface LLPContext { - readonly llpMessage: TextMessage; - readonly llpClient: Annotater; -} - -const storage = new AsyncLocalStorage(); - -/** - * Runs the given function within an LLP async context. - * The middleware reads from this context automatically — - * no need for developers to pass context manually. - */ -export function runWithLLPContext(ctx: LLPContext, fn: () => T): T { - return storage.run(ctx, fn); -} - -/** - * Retrieves the current LLP context from AsyncLocalStorage. - * Throws if called outside of a `runWithLLPContext` scope. - */ -export function getLLPContext(): LLPContext { - const ctx = storage.getStore(); - if (!ctx) { - throw new Error('LLP context not available — this must be called inside an onMessage handler'); - } - return ctx; -} diff --git a/src/index.ts b/src/index.ts index 33918a4..05bd053 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,7 @@ export { TimeoutError, } from './errors.js'; // Message types -export { PresenceMessage, TextMessage, ToolCall } from './message.js'; +export { PresenceMessage, TextMessage } from './message.js'; // Status enums export { ConnectionStatus, @@ -35,3 +35,4 @@ export { type PresenceStatus as PresenceStatusType, } from './presence.js'; export { LLPSession, LLPSession as Session } from './session.js'; +export { ToolCall } from './tool_call.js'; diff --git a/src/message.ts b/src/message.ts index 19ea2c7..94ce4bf 100644 --- a/src/message.ts +++ b/src/message.ts @@ -1,57 +1,7 @@ import { randomUUID } from 'node:crypto'; import { TextMessageEmptyError, TextMessageReplyError } from './errors.js'; import type { PresenceStatus } from './presence.js'; - -// ============================================================================= -// ToolCall -// ============================================================================= - -export class ToolCall { - readonly id: string; - readonly recipient: string; - readonly name: string; - readonly parameters: string; - readonly result: string; - readonly threwException: boolean; - readonly durationMs: number; - - constructor(args: { - id: string; - recipient: string; - name: string; - parameters: string; - result: string; - threwException: boolean; - durationMs: number; - }) { - this.id = args.id; - this.recipient = args.recipient; - this.name = args.name; - this.parameters = args.parameters; - this.result = args.result; - this.threwException = args.threwException; - this.durationMs = args.durationMs; - } - - encode(): string { - return JSON.stringify({ - type: 'tool_call', - id: this.id, - data: { - to: this.recipient, - name: this.name, - parameters: this.parameters, - result: this.result, - threw_exception: this.threwException, - duration_ms: this.durationMs, - }, - }); - } -} - -// ============================================================================= -// TextMessage -// ============================================================================= +import { ToolCall } from './tool_call.js'; export class TextMessage { readonly id: string; diff --git a/src/session.ts b/src/session.ts index 8e29539..481e4b0 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,5 +1,5 @@ import type { Annotater } from './annotate.js'; -import type { ToolCall } from './message.js'; +import type { ToolCall } from './tool_call.js'; export class LLPSession implements Annotater { private sessionData: TData | undefined; diff --git a/src/tool_call.ts b/src/tool_call.ts new file mode 100644 index 0000000..b9e1d84 --- /dev/null +++ b/src/tool_call.ts @@ -0,0 +1,42 @@ +export class ToolCall { + readonly id: string; + readonly recipient: string; + readonly name: string; + readonly parameters: string; + readonly result: string; + readonly threwException: boolean; + readonly durationMs: number; + + constructor(args: { + id: string; + recipient: string; + name: string; + parameters: string; + result: string; + threwException: boolean; + durationMs: number; + }) { + this.id = args.id; + this.recipient = args.recipient; + this.name = args.name; + this.parameters = args.parameters; + this.result = args.result; + this.threwException = args.threwException; + this.durationMs = args.durationMs; + } + + encode(): string { + return JSON.stringify({ + type: 'tool_call', + id: this.id, + data: { + to: this.recipient, + name: this.name, + parameters: this.parameters, + result: this.result, + threw_exception: this.threwException, + duration_ms: this.durationMs, + }, + }); + } +}