diff --git a/docs/concepts/events.mdx b/docs/concepts/events.mdx index bfc0ffebd..f7b88b9a9 100644 --- a/docs/concepts/events.mdx +++ b/docs/concepts/events.mdx @@ -95,12 +95,14 @@ The `RunFinished` event indicates that an agent has successfully completed all its work for the current run. Upon receiving this event, frontends should finalize any UI states that were waiting on the agent's completion. This event marks a clean termination point and indicates that no further processing will -occur in this run unless explicitly requested. +occur in this run unless explicitly requested. The optional `result` field can +contain any output data produced by the agent run. | Property | Description | | ---------- | ----------------------------- | | `threadId` | ID of the conversation thread | | `runId` | ID of the agent run | +| `result` | Optional result data from run | ### RunError diff --git a/docs/docs.json b/docs/docs.json index 7979094a7..5d4d1d435 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -68,7 +68,12 @@ }, { "group": "@ag-ui/client", - "pages": ["sdk/js/client/overview"] + "pages": [ + "sdk/js/client/overview", + "sdk/js/client/abstract-agent", + "sdk/js/client/http-agent", + "sdk/js/client/subscriber" + ] }, "sdk/js/encoder", "sdk/js/proto" diff --git a/docs/sdk/js/client/abstract-agent.mdx b/docs/sdk/js/client/abstract-agent.mdx index 2166ae513..3738b0aae 100644 --- a/docs/sdk/js/client/abstract-agent.mdx +++ b/docs/sdk/js/client/abstract-agent.mdx @@ -53,10 +53,10 @@ class MyAgent extends AbstractAgent { ### runAgent() -The primary method for executing an agent and receiving the event stream. +The primary method for executing an agent and processing the result. ```typescript -runAgent(parameters?: RunAgentParameters): Observable +runAgent(parameters?: RunAgentParameters, subscriber?: AgentSubscriber): Promise ``` #### Parameters @@ -70,6 +70,31 @@ interface RunAgentParameters { } ``` +The optional `subscriber` parameter allows you to provide an +[AgentSubscriber](/sdk/js/client/subscriber) for handling events during this +specific run. + +#### Return Value + +```typescript +interface RunAgentResult { + result: any // The final result returned by the agent + newMessages: Message[] // New messages added during this run +} +``` + +### subscribe() + +Adds an [AgentSubscriber](/sdk/js/client/subscriber) to handle events across +multiple agent runs. + +```typescript +subscribe(subscriber: AgentSubscriber): { unsubscribe: () => void } +``` + +Returns an object with an `unsubscribe()` method to remove the subscriber when +no longer needed. + ### abortRun() Cancels the current agent execution. diff --git a/docs/sdk/js/client/http-agent.mdx b/docs/sdk/js/client/http-agent.mdx index 0d98e8a24..ba69cc010 100644 --- a/docs/sdk/js/client/http-agent.mdx +++ b/docs/sdk/js/client/http-agent.mdx @@ -39,21 +39,40 @@ const agent = new HttpAgent({ ### runAgent() -Executes the agent by making an HTTP request to the configured endpoint. Returns -an observable event stream. +Executes the agent by making an HTTP request to the configured endpoint. ```typescript -runAgent(parameters?: RunHttpAgentConfig): Observable +runAgent(parameters?: RunAgentParameters, subscriber?: AgentSubscriber): Promise ``` #### Parameters +The `parameters` argument follows the standard `RunAgentParameters` interface. +The optional `subscriber` parameter allows you to provide an +[AgentSubscriber](/sdk/js/client/subscriber) for handling events during this +specific run. + +#### Return Value + ```typescript -interface RunHttpAgentConfig extends RunAgentParameters { - abortController?: AbortController // For canceling the HTTP request +interface RunAgentResult { + result: any // The final result returned by the agent + newMessages: Message[] // New messages added during this run } ``` +### subscribe() + +Adds an [AgentSubscriber](/sdk/js/client/subscriber) to handle events across +multiple agent runs. + +```typescript +subscribe(subscriber: AgentSubscriber): { unsubscribe: () => void } +``` + +Returns an object with an `unsubscribe()` method to remove the subscriber when +no longer needed. + ### abortRun() Cancels the current HTTP request using the AbortController. diff --git a/docs/sdk/js/client/overview.mdx b/docs/sdk/js/client/overview.mdx index bf34d9ffd..0bfcf085f 100644 --- a/docs/sdk/js/client/overview.mdx +++ b/docs/sdk/js/client/overview.mdx @@ -60,3 +60,28 @@ Concrete implementation for HTTP-based agent connectivity: Ready-to-use HTTP implementation for agent connectivity, using a highly efficient event encoding format + +## AgentSubscriber + +Event-driven subscriber system for handling agent lifecycle events and state +mutations during agent execution: + +- [Event Handlers](/sdk/js/client/subscriber#event-handlers) - Lifecycle, + message, tool call, and state events +- [State Management](/sdk/js/client/subscriber#state-management) - Mutation + control and propagation handling +- [Usage Examples](/sdk/js/client/subscriber#usage-examples) - Logging, + persistence, and error handling patterns +- [Integration](/sdk/js/client/subscriber#integration-with-agents) - Registering + and using subscribers with agents + + + Comprehensive event system for reactive agent interaction and middleware-style + functionality + diff --git a/docs/sdk/js/client/subscriber.mdx b/docs/sdk/js/client/subscriber.mdx new file mode 100644 index 000000000..ee010488f --- /dev/null +++ b/docs/sdk/js/client/subscriber.mdx @@ -0,0 +1,395 @@ +--- +title: "AgentSubscriber" +description: + "Event-driven subscriber system for agent lifecycle and event handling" +--- + +# AgentSubscriber + +The `AgentSubscriber` interface provides a comprehensive event-driven system for +handling agent lifecycle events, message updates, and state mutations during +agent execution. It allows you to hook into various stages of the agent's +operation and modify its behavior. + +```typescript +import { AgentSubscriber } from "@ag-ui/client" +``` + +## Overview + +`AgentSubscriber` defines a collection of optional event handlers and lifecycle +hooks that can respond to different stages of agent execution. All methods in +the interface are optional, allowing you to implement only the events you need +to handle. + +All subscriber methods can be either synchronous or asynchronous - if they +return a Promise, the agent will await their completion before proceeding. + +## Adding Subscribers to Agents + +Subscribers can be added to agents in two ways: + +### Permanent Subscription + +Use the `subscribe()` method to add a subscriber that will persist across +multiple agent runs: + +```typescript +const agent = new HttpAgent({ url: "https://api.example.com/agent" }) + +const subscriber: AgentSubscriber = { + onTextMessageContentEvent: ({ textMessageBuffer }) => { + console.log("Streaming text:", textMessageBuffer) + }, +} + +// Add permanent subscriber +const subscription = agent.subscribe(subscriber) + +// Later, remove the subscriber if needed +subscription.unsubscribe() +``` + +### Temporary Subscription + +Pass a subscriber directly to `runAgent()` for one-time use: + +```typescript +const temporarySubscriber: AgentSubscriber = { + onRunFinishedEvent: ({ result }) => { + console.log("Run completed with result:", result) + }, +} + +// Use subscriber for this run only +await agent.runAgent({ tools: [myTool] }, temporarySubscriber) +``` + +## Core Interfaces + +### AgentSubscriber + +The main interface that defines all available event handlers and lifecycle +hooks. All methods in the interface are optional, allowing you to implement only +the events you need to handle. + +### AgentStateMutation + +Event handlers can return an `AgentStateMutation` object to modify the agent's +state and control event processing: + +```typescript +interface AgentStateMutation { + messages?: Message[] // Update the message history + state?: State // Update the agent state + stopPropagation?: boolean // Prevent further subscribers from processing this event +} +``` + +When a subscriber returns a mutation: + +- **messages**: Replaces the current message history +- **state**: Replaces the current agent state +- **stopPropagation**: If `true`, prevents subsequent subscribers from handling + the event (useful for overriding default behavior) + +### AgentSubscriberParams + +Common parameters passed to most subscriber methods: + +```typescript +interface AgentSubscriberParams { + messages: Message[] // Current message history + state: State // Current agent state + agent: AbstractAgent // The agent instance + input: RunAgentInput // The original input parameters +} +``` + +## Event Handlers + +### Lifecycle Events + +#### onRunInitialized() + +Called when the agent run is first initialized, before any processing begins. + +```typescript +onRunInitialized?(params: AgentSubscriberParams): MaybePromise | void> +``` + +#### onRunFailed() + +Called when the agent run encounters an error. + +```typescript +onRunFailed?(params: { error: Error } & AgentSubscriberParams): MaybePromise | void> +``` + +#### onRunFinalized() + +Called when the agent run completes, regardless of success or failure. + +```typescript +onRunFinalized?(params: AgentSubscriberParams): MaybePromise | void> +``` + +### Event Handlers + +#### onEvent() + +General event handler that receives all events during agent execution. + +```typescript +onEvent?(params: { event: BaseEvent } & AgentSubscriberParams): MaybePromise +``` + +#### onRunStartedEvent() + +Triggered when an agent run begins execution. + +```typescript +onRunStartedEvent?(params: { event: RunStartedEvent } & AgentSubscriberParams): MaybePromise +``` + +#### onRunFinishedEvent() + +Called when an agent run completes successfully. + +```typescript +onRunFinishedEvent?(params: { event: RunFinishedEvent; result?: any } & AgentSubscriberParams): MaybePromise +``` + +#### onRunErrorEvent() + +Triggered when an agent run encounters an error. + +```typescript +onRunErrorEvent?(params: { event: RunErrorEvent } & AgentSubscriberParams): MaybePromise +``` + +#### onStepStartedEvent() + +Called when a step within an agent run begins. + +```typescript +onStepStartedEvent?(params: { event: StepStartedEvent } & AgentSubscriberParams): MaybePromise +``` + +#### onStepFinishedEvent() + +Triggered when a step within an agent run completes. + +```typescript +onStepFinishedEvent?(params: { event: StepFinishedEvent } & AgentSubscriberParams): MaybePromise +``` + +### Message Events + +#### onTextMessageStartEvent() + +Triggered when a text message starts being generated. + +```typescript +onTextMessageStartEvent?(params: { event: TextMessageStartEvent } & AgentSubscriberParams): MaybePromise +``` + +#### onTextMessageContentEvent() + +Called for each chunk of text content as it's generated. + +```typescript +onTextMessageContentEvent?(params: { event: TextMessageContentEvent; textMessageBuffer: string } & AgentSubscriberParams): MaybePromise +``` + +#### onTextMessageEndEvent() + +Called when a text message generation is complete. + +```typescript +onTextMessageEndEvent?(params: { event: TextMessageEndEvent; textMessageBuffer: string } & AgentSubscriberParams): MaybePromise +``` + +### Tool Call Events + +#### onToolCallStartEvent() + +Triggered when a tool call begins. + +```typescript +onToolCallStartEvent?(params: { event: ToolCallStartEvent } & AgentSubscriberParams): MaybePromise +``` + +#### onToolCallArgsEvent() + +Called as tool call arguments are being parsed, providing both raw and parsed +argument data. + +```typescript +onToolCallArgsEvent?(params: { event: ToolCallArgsEvent; toolCallBuffer: string; toolCallName: string; partialToolCallArgs: Record } & AgentSubscriberParams): MaybePromise +``` + +#### onToolCallEndEvent() + +Called when a tool call is complete with final arguments. + +```typescript +onToolCallEndEvent?(params: { event: ToolCallEndEvent; toolCallName: string; toolCallArgs: Record } & AgentSubscriberParams): MaybePromise +``` + +#### onToolCallResultEvent() + +Triggered when a tool call result is received. + +```typescript +onToolCallResultEvent?(params: { event: ToolCallResultEvent } & AgentSubscriberParams): MaybePromise +``` + +### State Events + +#### onStateSnapshotEvent() + +Called when a complete state snapshot is provided. + +```typescript +onStateSnapshotEvent?(params: { event: StateSnapshotEvent } & AgentSubscriberParams): MaybePromise +``` + +#### onStateDeltaEvent() + +Triggered when partial state changes are applied. + +```typescript +onStateDeltaEvent?(params: { event: StateDeltaEvent } & AgentSubscriberParams): MaybePromise +``` + +#### onMessagesSnapshotEvent() + +Called when a complete message history snapshot is provided. + +```typescript +onMessagesSnapshotEvent?(params: { event: MessagesSnapshotEvent } & AgentSubscriberParams): MaybePromise +``` + +#### onRawEvent() + +Handler for raw, unprocessed events. + +```typescript +onRawEvent?(params: { event: RawEvent } & AgentSubscriberParams): MaybePromise +``` + +#### onCustomEvent() + +Handler for custom application-specific events. + +```typescript +onCustomEvent?(params: { event: CustomEvent } & AgentSubscriberParams): MaybePromise +``` + +### State Change Handlers + +#### onMessagesChanged() + +Called when the agent's message history is updated. + +```typescript +onMessagesChanged?(params: Omit & { input?: RunAgentInput }): MaybePromise +``` + +#### onStateChanged() + +Triggered when the agent's state is modified. + +```typescript +onStateChanged?(params: Omit & { input?: RunAgentInput }): MaybePromise +``` + +#### onNewMessage() + +Called when a new message is added to the conversation. + +```typescript +onNewMessage?(params: { message: Message } & Omit & { input?: RunAgentInput }): MaybePromise +``` + +#### onNewToolCall() + +Triggered when a new tool call is added to a message. + +```typescript +onNewToolCall?(params: { toolCall: ToolCall } & Omit & { input?: RunAgentInput }): MaybePromise +``` + +## Async Support + +All subscriber methods support both synchronous and asynchronous execution: + +```typescript +const subscriber: AgentSubscriber = { + // Synchronous handler + onTextMessageContentEvent: ({ textMessageBuffer }) => { + updateUI(textMessageBuffer) + }, + + // Asynchronous handler + onStateChanged: async ({ state }) => { + await saveStateToDatabase(state) + }, + + // Async handler with mutation + onRunInitialized: async ({ messages, state }) => { + const enrichedState = await loadUserPreferences() + return { + state: { ...state, ...enrichedState }, + } + }, +} +``` + +## Multiple Subscribers + +Agents can have multiple subscribers, which are processed in the order they were +added: + +```typescript +// First subscriber modifies state +const stateEnricher: AgentSubscriber = { + onRunInitialized: ({ state }) => ({ + state: { ...state, timestamp: new Date().toISOString() }, + }), +} + +// Second subscriber sees the modified state +const logger: AgentSubscriber = { + onRunInitialized: ({ state }) => { + console.log("State after enrichment:", state) + }, +} + +agent.subscribe(stateEnricher) +agent.subscribe(logger) +``` + +## Integration with Agents + +Basic usage pattern: + +```typescript +const agent = new HttpAgent({ url: "https://api.example.com/agent" }) + +// Add persistent subscriber +agent.subscribe({ + onTextMessageContentEvent: ({ textMessageBuffer }) => { + updateStreamingUI(textMessageBuffer) + }, + onRunFinishedEvent: ({ result }) => { + displayFinalResult(result) + }, +}) + +// Run agent (subscriber will be called automatically) +const result = await agent.runAgent({ + tools: [myTool], +}) +``` diff --git a/docs/sdk/js/core/events.mdx b/docs/sdk/js/core/events.mdx index 55219bf35..ea119090c 100644 --- a/docs/sdk/js/core/events.mdx +++ b/docs/sdk/js/core/events.mdx @@ -85,13 +85,15 @@ type RunFinishedEvent = BaseEvent & { type: EventType.RUN_FINISHED threadId: string runId: string + result?: any } ``` -| Property | Type | Description | -| ---------- | -------- | ----------------------------- | -| `threadId` | `string` | ID of the conversation thread | -| `runId` | `string` | ID of the agent run | +| Property | Type | Description | +| ---------- | ---------------- | ------------------------------ | +| `threadId` | `string` | ID of the conversation thread | +| `runId` | `string` | ID of the agent run | +| `result` | `any` (optional) | Result data from the agent run | ### RunErrorEvent diff --git a/docs/sdk/python/core/events.mdx b/docs/sdk/python/core/events.mdx index 6b4e94887..6d5cdc934 100644 --- a/docs/sdk/python/core/events.mdx +++ b/docs/sdk/python/core/events.mdx @@ -91,12 +91,14 @@ class RunFinishedEvent(BaseEvent): type: Literal[EventType.RUN_FINISHED] thread_id: str run_id: str + result: Optional[Any] = None ``` -| Property | Type | Description | -| ----------- | ----- | ----------------------------- | -| `thread_id` | `str` | ID of the conversation thread | -| `run_id` | `str` | ID of the agent run | +| Property | Type | Description | +| ----------- | --------------- | ------------------------------ | +| `thread_id` | `str` | ID of the conversation thread | +| `run_id` | `str` | ID of the agent run | +| `result` | `Optional[Any]` | Result data from the agent run | ### RunErrorEvent diff --git a/python-sdk/ag_ui/core/events.py b/python-sdk/ag_ui/core/events.py index 02d0f0fbf..56c865481 100644 --- a/python-sdk/ag_ui/core/events.py +++ b/python-sdk/ag_ui/core/events.py @@ -226,6 +226,7 @@ class RunFinishedEvent(BaseEvent): type: Literal[EventType.RUN_FINISHED] thread_id: str run_id: str + result: Optional[Any] = None class RunErrorEvent(BaseEvent): diff --git a/typescript-sdk/apps/client-cli/.gitignore b/typescript-sdk/apps/client-cli/.gitignore new file mode 100644 index 000000000..87b89f7f7 --- /dev/null +++ b/typescript-sdk/apps/client-cli/.gitignore @@ -0,0 +1,2 @@ +dist/ +mastra.db* \ No newline at end of file diff --git a/typescript-sdk/apps/client-cli/README.md b/typescript-sdk/apps/client-cli/README.md new file mode 100644 index 000000000..cbd466ee8 --- /dev/null +++ b/typescript-sdk/apps/client-cli/README.md @@ -0,0 +1 @@ +# AG-UI CLI diff --git a/typescript-sdk/apps/client-cli/package.json b/typescript-sdk/apps/client-cli/package.json new file mode 100644 index 000000000..b1253fb7f --- /dev/null +++ b/typescript-sdk/apps/client-cli/package.json @@ -0,0 +1,29 @@ +{ + "name": "client-cli", + "version": "0.1.0", + "private": true, + "scripts": { + "start": "tsx src/index.ts", + "dev": "tsx --watch src/index.ts", + "build": "tsc", + "clean": "rm -rf dist" + }, + "dependencies": { + "@ag-ui/client": "workspace:*", + "@ag-ui/core": "workspace:*", + "@ag-ui/mastra": "workspace:*", + "@ai-sdk/openai": "^1.3.22", + "@mastra/client-js": "^0.10.9", + "@mastra/core": "^0.10.10", + "@mastra/libsql": "^0.11.0", + "@mastra/loggers": "^0.10.3", + "@mastra/memory": "^0.11.1", + "open": "^10.1.2", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/node": "^20", + "tsx": "^4.7.0", + "typescript": "^5" + } +} diff --git a/typescript-sdk/apps/client-cli/src/agent.ts b/typescript-sdk/apps/client-cli/src/agent.ts new file mode 100644 index 000000000..55020bba8 --- /dev/null +++ b/typescript-sdk/apps/client-cli/src/agent.ts @@ -0,0 +1,38 @@ +import { openai } from "@ai-sdk/openai"; +import { Agent } from "@mastra/core/agent"; +import { MastraAgent } from "@ag-ui/mastra"; +import { Memory } from "@mastra/memory"; +import { LibSQLStore } from "@mastra/libsql"; +import { weatherTool } from "./tools/weather.tool"; +import { browserTool } from "./tools/browser.tool"; + +export const agent = new MastraAgent({ + // @ts-ignore + agent: new Agent({ + name: "AG-UI Agent", + instructions: ` + You are a helpful assistant that runs a CLI application. + + When helping users get weather details for specific locations, respond: + - Always ask for a location if none is provided. + - If the location name isn’t in English, please translate it + - If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part (e.g. "New York") + - Include relevant details like humidity, wind conditions, and precipitation + - Keep responses concise but informative + + Use the weatherTool to fetch current weather data. + + When helping users browse the web, always use a full URL, for example: "https://www.google.com" + Use the browserTool to browse the web. + + `, + model: openai("gpt-4o-mini"), + tools: { weatherTool, browserTool }, + memory: new Memory({ + storage: new LibSQLStore({ + url: "file:./mastra.db", + }), + }), + }), + threadId: "1", +}); diff --git a/typescript-sdk/apps/client-cli/src/index.ts b/typescript-sdk/apps/client-cli/src/index.ts new file mode 100644 index 000000000..e2a9c60c0 --- /dev/null +++ b/typescript-sdk/apps/client-cli/src/index.ts @@ -0,0 +1,81 @@ +import * as readline from "readline"; +import { agent } from "./agent"; +import { randomUUID } from "node:crypto"; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +async function chatLoop() { + console.log("πŸ€– AG-UI chat started! Type your messages and press Enter. Press Ctrl+D to quit.\n"); + + return new Promise((resolve) => { + const promptUser = () => { + rl.question("> ", async (input) => { + if (input.trim() === "") { + promptUser(); + return; + } + console.log(""); + + rl.pause(); + + agent.messages.push({ + id: randomUUID(), + role: "user", + content: input.trim(), + }); + + try { + await agent.runAgent( + {}, + { + onTextMessageStartEvent() { + process.stdout.write("πŸ€– AG-UI assistant: "); + }, + onTextMessageContentEvent({ event }) { + process.stdout.write(event.delta); + }, + onTextMessageEndEvent() { + console.log("\n"); + }, + onToolCallStartEvent({ event }) { + console.log("πŸ”§ Tool call:", event.toolCallName); + }, + onToolCallArgsEvent({ event }) { + process.stdout.write(event.delta); + }, + onToolCallEndEvent() { + console.log(""); + }, + onToolCallResultEvent({ event }) { + if (event.content) { + console.log("πŸ” Tool call result:", event.content); + } + }, + }, + ); + } catch (error) { + console.error("❌ Error running agent:", error); + } + + rl.resume(); + promptUser(); + }); + }; + + rl.on("close", () => { + console.log("\nπŸ‘‹ Goodbye!"); + resolve(); + }); + + promptUser(); + }); +} + +async function main() { + await chatLoop(); +} + +main().catch(console.error); diff --git a/typescript-sdk/apps/client-cli/src/tools/browser.tool.ts b/typescript-sdk/apps/client-cli/src/tools/browser.tool.ts new file mode 100644 index 000000000..2277b3599 --- /dev/null +++ b/typescript-sdk/apps/client-cli/src/tools/browser.tool.ts @@ -0,0 +1,16 @@ +import { createTool } from "@mastra/core/tools"; +import { z } from "zod"; +import open from "open"; + +export const browserTool = createTool({ + id: "browser", + description: "Browse the web", + inputSchema: z.object({ + url: z.string().describe("URL to browse"), + }), + outputSchema: z.string(), + execute: async ({ context }) => { + open(context.url); + return `Browsed ${context.url}`; + }, +}); diff --git a/typescript-sdk/apps/client-cli/src/tools/weather.tool.ts b/typescript-sdk/apps/client-cli/src/tools/weather.tool.ts new file mode 100644 index 000000000..5a0e9b16c --- /dev/null +++ b/typescript-sdk/apps/client-cli/src/tools/weather.tool.ts @@ -0,0 +1,102 @@ +import { createTool } from "@mastra/core/tools"; +import { z } from "zod"; + +interface GeocodingResponse { + results: { + latitude: number; + longitude: number; + name: string; + }[]; +} +interface WeatherResponse { + current: { + time: string; + temperature_2m: number; + apparent_temperature: number; + relative_humidity_2m: number; + wind_speed_10m: number; + wind_gusts_10m: number; + weather_code: number; + }; +} + +export const weatherTool = createTool({ + id: "get-weather", + description: "Get current weather for a location", + inputSchema: z.object({ + location: z.string().describe("City name"), + }), + outputSchema: z.object({ + temperature: z.number(), + feelsLike: z.number(), + humidity: z.number(), + windSpeed: z.number(), + windGust: z.number(), + conditions: z.string(), + location: z.string(), + }), + execute: async ({ context }) => { + return await getWeather(context.location); + }, +}); + +const getWeather = async (location: string) => { + const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`; + const geocodingResponse = await fetch(geocodingUrl); + const geocodingData = (await geocodingResponse.json()) as GeocodingResponse; + + if (!geocodingData.results?.[0]) { + throw new Error(`Location '${location}' not found`); + } + + const { latitude, longitude, name } = geocodingData.results[0]; + + const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`; + + const response = await fetch(weatherUrl); + const data = (await response.json()) as WeatherResponse; + + return { + temperature: data.current.temperature_2m, + feelsLike: data.current.apparent_temperature, + humidity: data.current.relative_humidity_2m, + windSpeed: data.current.wind_speed_10m, + windGust: data.current.wind_gusts_10m, + conditions: getWeatherCondition(data.current.weather_code), + location: name, + }; +}; + +function getWeatherCondition(code: number): string { + const conditions: Record = { + 0: "Clear sky", + 1: "Mainly clear", + 2: "Partly cloudy", + 3: "Overcast", + 45: "Foggy", + 48: "Depositing rime fog", + 51: "Light drizzle", + 53: "Moderate drizzle", + 55: "Dense drizzle", + 56: "Light freezing drizzle", + 57: "Dense freezing drizzle", + 61: "Slight rain", + 63: "Moderate rain", + 65: "Heavy rain", + 66: "Light freezing rain", + 67: "Heavy freezing rain", + 71: "Slight snow fall", + 73: "Moderate snow fall", + 75: "Heavy snow fall", + 77: "Snow grains", + 80: "Slight rain showers", + 81: "Moderate rain showers", + 82: "Violent rain showers", + 85: "Slight snow showers", + 86: "Heavy snow showers", + 95: "Thunderstorm", + 96: "Thunderstorm with slight hail", + 99: "Thunderstorm with heavy hail", + }; + return conditions[code] || "Unknown"; +} diff --git a/typescript-sdk/apps/client-cli/tsconfig.json b/typescript-sdk/apps/client-cli/tsconfig.json new file mode 100644 index 000000000..3cb86c1b7 --- /dev/null +++ b/typescript-sdk/apps/client-cli/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020", "dom"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/typescript-sdk/packages/client/src/agent/__tests__/agent-mutations.test.ts b/typescript-sdk/packages/client/src/agent/__tests__/agent-mutations.test.ts new file mode 100644 index 000000000..c5a2b7a5d --- /dev/null +++ b/typescript-sdk/packages/client/src/agent/__tests__/agent-mutations.test.ts @@ -0,0 +1,493 @@ +import { AbstractAgent } from "../agent"; +import { AgentSubscriber } from "../subscriber"; +import { + BaseEvent, + EventType, + Message, + RunAgentInput, + State, + ToolCall, + AssistantMessage, +} from "@ag-ui/core"; +import { Observable, of } from "rxjs"; + +// Mock uuid module +jest.mock("uuid", () => ({ + v4: jest.fn().mockReturnValue("mock-uuid"), +})); + +// Mock utils +jest.mock("@/utils", () => ({ + structuredClone_: (obj: any) => { + if (obj === undefined) return undefined; + const jsonString = JSON.stringify(obj); + if (jsonString === undefined || jsonString === "undefined") return undefined; + return JSON.parse(jsonString); + }, +})); + +// Helper function to wait for async notifications to complete +const waitForAsyncNotifications = async () => { + // Wait for the next tick of the event loop to ensure async operations complete + await new Promise((resolve) => setImmediate(resolve)); +}; + +// Mock the verify and chunks modules +jest.mock("@/verify", () => ({ + verifyEvents: jest.fn(() => (source$: Observable) => source$), +})); + +jest.mock("@/chunks", () => ({ + transformChunks: jest.fn(() => (source$: Observable) => source$), +})); + +// Create a test agent implementation +class TestAgent extends AbstractAgent { + protected run(input: RunAgentInput): Observable { + return of(); + } +} + +describe("Agent Mutations", () => { + let agent: TestAgent; + let mockSubscriber: AgentSubscriber; + + beforeEach(() => { + jest.clearAllMocks(); + + agent = new TestAgent({ + threadId: "test-thread", + initialMessages: [ + { + id: "initial-msg", + role: "user", + content: "Initial message", + }, + ], + initialState: { counter: 0 }, + }); + + mockSubscriber = { + onMessagesChanged: jest.fn(), + onStateChanged: jest.fn(), + onNewMessage: jest.fn(), + onNewToolCall: jest.fn(), + }; + + agent.subscribe(mockSubscriber); + }); + + describe("addMessage", () => { + it("should add a user message and fire appropriate events", async () => { + const userMessage: Message = { + id: "user-msg-1", + role: "user", + content: "Hello world", + }; + + agent.addMessage(userMessage); + + // Message should be added immediately + expect(agent.messages).toHaveLength(2); + expect(agent.messages[1]).toBe(userMessage); + + // Wait for async notifications + await waitForAsyncNotifications(); + + // Should fire onNewMessage and onMessagesChanged + expect(mockSubscriber.onNewMessage).toHaveBeenCalledWith({ + message: userMessage, + messages: agent.messages, + state: agent.state, + agent, + }); + + expect(mockSubscriber.onMessagesChanged).toHaveBeenCalledWith({ + messages: agent.messages, + state: agent.state, + agent, + }); + + // Should NOT fire onNewToolCall for user messages + expect(mockSubscriber.onNewToolCall).not.toHaveBeenCalled(); + }); + + it("should add an assistant message without tool calls", async () => { + const assistantMessage: Message = { + id: "assistant-msg-1", + role: "assistant", + content: "How can I help you?", + }; + + agent.addMessage(assistantMessage); + + // Wait for async notifications + await waitForAsyncNotifications(); + + expect(mockSubscriber.onNewMessage).toHaveBeenCalledWith({ + message: assistantMessage, + messages: agent.messages, + state: agent.state, + agent, + }); + + expect(mockSubscriber.onMessagesChanged).toHaveBeenCalledWith({ + messages: agent.messages, + state: agent.state, + agent, + }); + + // Should NOT fire onNewToolCall when no tool calls + expect(mockSubscriber.onNewToolCall).not.toHaveBeenCalled(); + }); + + it("should add an assistant message with tool calls and fire onNewToolCall", async () => { + const toolCalls: ToolCall[] = [ + { + id: "call-1", + type: "function", + function: { + name: "get_weather", + arguments: '{"location": "New York"}', + }, + }, + { + id: "call-2", + type: "function", + function: { + name: "search_web", + arguments: '{"query": "latest news"}', + }, + }, + ]; + + const assistantMessage: Message = { + id: "assistant-msg-2", + role: "assistant", + content: "Let me help you with that.", + toolCalls, + }; + + agent.addMessage(assistantMessage); + + // Wait for async notifications + await waitForAsyncNotifications(); + + expect(mockSubscriber.onNewMessage).toHaveBeenCalledWith({ + message: assistantMessage, + messages: agent.messages, + state: agent.state, + agent, + }); + + // Should fire onNewToolCall for each tool call + expect(mockSubscriber.onNewToolCall).toHaveBeenCalledTimes(2); + + expect(mockSubscriber.onNewToolCall).toHaveBeenNthCalledWith(1, { + toolCall: toolCalls[0], + messages: agent.messages, + state: agent.state, + agent, + }); + + expect(mockSubscriber.onNewToolCall).toHaveBeenNthCalledWith(2, { + toolCall: toolCalls[1], + messages: agent.messages, + state: agent.state, + agent, + }); + + expect(mockSubscriber.onMessagesChanged).toHaveBeenCalledWith({ + messages: agent.messages, + state: agent.state, + agent, + }); + }); + }); + + describe("addMessages", () => { + it("should add multiple messages and fire events correctly", async () => { + const messages: Message[] = [ + { + id: "msg-1", + role: "user", + content: "First message", + }, + { + id: "msg-2", + role: "assistant", + content: "Second message", + toolCalls: [ + { + id: "call-1", + type: "function", + function: { + name: "test_tool", + arguments: '{"param": "value"}', + }, + }, + ], + }, + { + id: "msg-3", + role: "user", + content: "Third message", + }, + ]; + + const initialLength = agent.messages.length; + agent.addMessages(messages); + + // Messages should be added immediately + expect(agent.messages).toHaveLength(initialLength + 3); + + // Wait for async notifications + await waitForAsyncNotifications(); + + // Should fire onNewMessage for each message + expect(mockSubscriber.onNewMessage).toHaveBeenCalledTimes(3); + + // Should fire onNewToolCall only for the assistant message with tool calls + expect(mockSubscriber.onNewToolCall).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onNewToolCall).toHaveBeenCalledWith({ + toolCall: (messages[1] as AssistantMessage).toolCalls![0], + messages: agent.messages, + state: agent.state, + agent, + }); + + // Should fire onMessagesChanged only once at the end + expect(mockSubscriber.onMessagesChanged).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onMessagesChanged).toHaveBeenCalledWith({ + messages: agent.messages, + state: agent.state, + agent, + }); + }); + + it("should handle empty array gracefully", async () => { + const initialLength = agent.messages.length; + agent.addMessages([]); + + expect(agent.messages).toHaveLength(initialLength); + + // Wait for async notifications + await waitForAsyncNotifications(); + + // Should still fire onMessagesChanged even for empty array + expect(mockSubscriber.onMessagesChanged).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onNewMessage).not.toHaveBeenCalled(); + expect(mockSubscriber.onNewToolCall).not.toHaveBeenCalled(); + }); + }); + + describe("setMessages", () => { + it("should replace messages and fire onMessagesChanged only", async () => { + const newMessages: Message[] = [ + { + id: "new-msg-1", + role: "user", + content: "New conversation start", + }, + { + id: "new-msg-2", + role: "assistant", + content: "Assistant response", + toolCalls: [ + { + id: "call-1", + type: "function", + function: { + name: "some_tool", + arguments: "{}", + }, + }, + ], + }, + ]; + + const originalMessage = agent.messages[0]; + agent.setMessages(newMessages); + + // Messages should be replaced immediately + expect(agent.messages).toHaveLength(2); + expect(agent.messages).not.toContain(originalMessage); // Original message should be gone + expect(agent.messages[0]).toEqual(newMessages[0]); + expect(agent.messages[1]).toEqual(newMessages[1]); + + // Wait for async notifications + await waitForAsyncNotifications(); + + // Should ONLY fire onMessagesChanged + expect(mockSubscriber.onMessagesChanged).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onMessagesChanged).toHaveBeenCalledWith({ + messages: agent.messages, + state: agent.state, + agent, + }); + + // Should NOT fire onNewMessage or onNewToolCall + expect(mockSubscriber.onNewMessage).not.toHaveBeenCalled(); + expect(mockSubscriber.onNewToolCall).not.toHaveBeenCalled(); + }); + + it("should handle empty messages array", async () => { + agent.setMessages([]); + + expect(agent.messages).toHaveLength(0); + + // Wait for async notifications + await waitForAsyncNotifications(); + + expect(mockSubscriber.onMessagesChanged).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onNewMessage).not.toHaveBeenCalled(); + expect(mockSubscriber.onNewToolCall).not.toHaveBeenCalled(); + }); + }); + + describe("setState", () => { + it("should replace state and fire onStateChanged only", async () => { + const newState: State = { + counter: 100, + isActive: true, + data: { key: "value" }, + }; + + agent.setState(newState); + + // State should be replaced immediately + expect(agent.state).toEqual(newState); + expect(agent.state).not.toBe(newState); // Should be a clone + + // Wait for async notifications + await waitForAsyncNotifications(); + + // Should ONLY fire onStateChanged + expect(mockSubscriber.onStateChanged).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onStateChanged).toHaveBeenCalledWith({ + messages: agent.messages, + state: agent.state, + agent, + }); + + // Should NOT fire other events + expect(mockSubscriber.onMessagesChanged).not.toHaveBeenCalled(); + expect(mockSubscriber.onNewMessage).not.toHaveBeenCalled(); + expect(mockSubscriber.onNewToolCall).not.toHaveBeenCalled(); + }); + + it("should handle empty state object", async () => { + agent.setState({}); + + expect(agent.state).toEqual({}); + + // Wait for async notifications + await waitForAsyncNotifications(); + + expect(mockSubscriber.onStateChanged).toHaveBeenCalledTimes(1); + }); + }); + + describe("execution order", () => { + it("should execute subscriber notifications in registration order", async () => { + const callOrder: string[] = []; + + const firstSubscriber: AgentSubscriber = { + onNewMessage: jest.fn().mockImplementation(() => { + callOrder.push("first-newMessage"); + }), + onMessagesChanged: jest.fn().mockImplementation(() => { + callOrder.push("first-messagesChanged"); + }), + }; + + const secondSubscriber: AgentSubscriber = { + onNewMessage: jest.fn().mockImplementation(() => { + callOrder.push("second-newMessage"); + }), + onMessagesChanged: jest.fn().mockImplementation(() => { + callOrder.push("second-messagesChanged"); + }), + }; + + // Clear the default subscriber and add our test subscribers + agent.subscribers = []; + agent.subscribe(firstSubscriber); + agent.subscribe(secondSubscriber); + + const message: Message = { + id: "test-msg", + role: "user", + content: "Test message", + }; + + agent.addMessage(message); + + // Wait for all async operations to complete by polling until all calls are made + while (callOrder.length < 4) { + await waitForAsyncNotifications(); + } + + // Verify sequential execution order + expect(callOrder).toEqual([ + "first-newMessage", + "second-newMessage", + "first-messagesChanged", + "second-messagesChanged", + ]); + }); + }); + + describe("multiple subscribers", () => { + it("should notify all subscribers for each event", async () => { + const subscriber2: AgentSubscriber = { + onNewMessage: jest.fn(), + onMessagesChanged: jest.fn(), + onNewToolCall: jest.fn(), + }; + + const subscriber3: AgentSubscriber = { + onNewMessage: jest.fn(), + onMessagesChanged: jest.fn(), + }; + + agent.subscribe(subscriber2); + agent.subscribe(subscriber3); + + const message: Message = { + id: "test-msg", + role: "assistant", + content: "Test", + toolCalls: [ + { + id: "call-1", + type: "function", + function: { name: "test", arguments: "{}" }, + }, + ], + }; + + agent.addMessage(message); + + // Wait for async notifications + await waitForAsyncNotifications(); + + // All subscribers should receive notifications + [mockSubscriber, subscriber2, subscriber3].forEach((sub) => { + expect(sub.onNewMessage).toHaveBeenCalledWith( + expect.objectContaining({ + message, + agent, + }), + ); + expect(sub.onMessagesChanged).toHaveBeenCalled(); + }); + + // Only subscribers with onNewToolCall should receive tool call events + expect(mockSubscriber.onNewToolCall).toHaveBeenCalled(); + expect(subscriber2.onNewToolCall).toHaveBeenCalled(); + // subscriber3 doesn't have onNewToolCall method, so it shouldn't be called + expect(subscriber3.onNewToolCall).toBeUndefined(); + }); + }); +}); diff --git a/typescript-sdk/packages/client/src/agent/__tests__/agent-result.test.ts b/typescript-sdk/packages/client/src/agent/__tests__/agent-result.test.ts new file mode 100644 index 000000000..38a51c0a9 --- /dev/null +++ b/typescript-sdk/packages/client/src/agent/__tests__/agent-result.test.ts @@ -0,0 +1,526 @@ +import { AbstractAgent } from "../agent"; +import { AgentSubscriber } from "../subscriber"; +import { + BaseEvent, + EventType, + Message, + RunAgentInput, + State, + MessagesSnapshotEvent, + RunFinishedEvent, + RunStartedEvent, +} from "@ag-ui/core"; +import { Observable, of } from "rxjs"; + +// Mock uuid module +jest.mock("uuid", () => ({ + v4: jest.fn().mockReturnValue("mock-uuid"), +})); + +// Mock utils +jest.mock("@/utils", () => ({ + structuredClone_: (obj: any) => { + if (obj === undefined) return undefined; + const jsonString = JSON.stringify(obj); + if (jsonString === undefined || jsonString === "undefined") return undefined; + return JSON.parse(jsonString); + }, +})); + +// Mock the verify and chunks modules +jest.mock("@/verify", () => ({ + verifyEvents: jest.fn(() => (source$: Observable) => source$), +})); + +jest.mock("@/chunks", () => ({ + transformChunks: jest.fn(() => (source$: Observable) => source$), +})); + +// Helper function to wait for async notifications to complete +const waitForAsyncNotifications = async () => { + await new Promise((resolve) => setImmediate(resolve)); +}; + +// Create a test agent implementation that can emit specific events +class TestAgent extends AbstractAgent { + private eventsToEmit: BaseEvent[] = []; + + setEventsToEmit(events: BaseEvent[]) { + this.eventsToEmit = events; + } + + protected run(input: RunAgentInput): Observable { + return of(...this.eventsToEmit); + } +} + +describe("Agent Result", () => { + let agent: TestAgent; + + beforeEach(() => { + jest.clearAllMocks(); + + agent = new TestAgent({ + threadId: "test-thread", + initialMessages: [ + { + id: "existing-msg-1", + role: "user", + content: "Existing message 1", + }, + { + id: "existing-msg-2", + role: "assistant", + content: "Existing message 2", + }, + ], + initialState: { counter: 0 }, + }); + }); + + describe("result handling", () => { + it("should return undefined result when no result is set", async () => { + agent.setEventsToEmit([ + { + type: EventType.RUN_STARTED, + threadId: "test-thread", + runId: "test-run", + } as RunStartedEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.result).toBeUndefined(); + expect(result.newMessages).toEqual([]); + }); + + it("should return result set by onRunFinishedEvent", async () => { + const expectedResult = { success: true, data: "test-data", count: 42 }; + + agent.setEventsToEmit([ + { + type: EventType.RUN_STARTED, + threadId: "test-thread", + runId: "test-run", + } as RunStartedEvent, + { + type: EventType.RUN_FINISHED, + threadId: "test-thread", + runId: "test-run", + result: expectedResult, + } as RunFinishedEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.result).toEqual(expectedResult); + expect(result.newMessages).toEqual([]); + }); + + it("should handle string result", async () => { + const expectedResult = "Simple string result"; + + agent.setEventsToEmit([ + { + type: EventType.RUN_FINISHED, + threadId: "test-thread", + runId: "test-run", + result: expectedResult, + } as RunFinishedEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.result).toBe(expectedResult); + }); + + it("should handle null result", async () => { + agent.setEventsToEmit([ + { + type: EventType.RUN_FINISHED, + threadId: "test-thread", + runId: "test-run", + result: null, + } as RunFinishedEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.result).toBeNull(); + }); + }); + + describe("newMessages tracking", () => { + it("should track new messages added during run", async () => { + const newMessages: Message[] = [ + { + id: "new-msg-1", + role: "user", + content: "New message 1", + }, + { + id: "new-msg-2", + role: "assistant", + content: "New message 2", + }, + ]; + + const allMessages = [...agent.messages, ...newMessages]; + + agent.setEventsToEmit([ + { + type: EventType.MESSAGES_SNAPSHOT, + messages: allMessages, + } as MessagesSnapshotEvent, + { + type: EventType.RUN_FINISHED, + threadId: "test-thread", + runId: "test-run", + result: "success", + } as RunFinishedEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.result).toBe("success"); + expect(result.newMessages).toEqual(newMessages); + expect(agent.messages).toEqual(allMessages); + }); + + it("should not include existing messages in newMessages", async () => { + const newMessage: Message = { + id: "new-msg-only", + role: "assistant", + content: "Only this is new", + }; + + // Include existing messages plus new one + const allMessages = [...agent.messages, newMessage]; + + agent.setEventsToEmit([ + { + type: EventType.MESSAGES_SNAPSHOT, + messages: allMessages, + } as MessagesSnapshotEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.newMessages).toEqual([newMessage]); + expect(result.newMessages).toHaveLength(1); + expect(agent.messages).toEqual(allMessages); + }); + + it("should handle no new messages", async () => { + // Keep same messages as initial + agent.setEventsToEmit([ + { + type: EventType.MESSAGES_SNAPSHOT, + messages: agent.messages, + } as MessagesSnapshotEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.newMessages).toEqual([]); + expect(agent.messages).toHaveLength(2); // Original messages + }); + + it("should handle multiple new messages with tool calls", async () => { + const newMessages: Message[] = [ + { + id: "new-msg-user", + role: "user", + content: "User query", + }, + { + id: "new-msg-assistant", + role: "assistant", + content: "Let me help you", + toolCalls: [ + { + id: "call-1", + type: "function", + function: { + name: "search_tool", + arguments: '{"query": "test"}', + }, + }, + ], + }, + { + id: "new-msg-tool", + role: "tool", + content: "Tool result", + toolCallId: "call-1", + }, + { + id: "new-msg-final", + role: "assistant", + content: "Here's the answer", + }, + ]; + + const allMessages = [...agent.messages, ...newMessages]; + + agent.setEventsToEmit([ + { + type: EventType.MESSAGES_SNAPSHOT, + messages: allMessages, + } as MessagesSnapshotEvent, + { + type: EventType.RUN_FINISHED, + threadId: "test-thread", + runId: "test-run", + result: { toolsUsed: 1, messagesAdded: 4 }, + } as RunFinishedEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.newMessages).toEqual(newMessages); + expect(result.newMessages).toHaveLength(4); + expect(result.result).toEqual({ toolsUsed: 1, messagesAdded: 4 }); + }); + + it("should preserve message order", async () => { + const newMessages: Message[] = [ + { id: "new-1", role: "user", content: "First new" }, + { id: "new-2", role: "assistant", content: "Second new" }, + { id: "new-3", role: "user", content: "Third new" }, + ]; + + const allMessages = [...agent.messages, ...newMessages]; + + agent.setEventsToEmit([ + { + type: EventType.MESSAGES_SNAPSHOT, + messages: allMessages, + } as MessagesSnapshotEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.newMessages).toEqual(newMessages); + // Verify order is preserved + expect(result.newMessages[0].id).toBe("new-1"); + expect(result.newMessages[1].id).toBe("new-2"); + expect(result.newMessages[2].id).toBe("new-3"); + }); + }); + + describe("combined result and newMessages", () => { + it("should return both result and newMessages correctly", async () => { + const newMessages: Message[] = [ + { + id: "conversation-msg", + role: "assistant", + content: "Here's what I found", + }, + ]; + + const expectedResult = { + status: "completed", + messagesGenerated: 1, + processingTime: 1500, + }; + + const allMessages = [...agent.messages, ...newMessages]; + + agent.setEventsToEmit([ + { + type: EventType.MESSAGES_SNAPSHOT, + messages: allMessages, + } as MessagesSnapshotEvent, + { + type: EventType.RUN_FINISHED, + threadId: "test-thread", + runId: "test-run", + result: expectedResult, + } as RunFinishedEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.result).toEqual(expectedResult); + expect(result.newMessages).toEqual(newMessages); + expect(result.newMessages).toHaveLength(1); + }); + + it("should handle empty newMessages with valid result", async () => { + const expectedResult = { error: false, processed: true }; + + agent.setEventsToEmit([ + { + type: EventType.RUN_FINISHED, + threadId: "test-thread", + runId: "test-run", + result: expectedResult, + } as RunFinishedEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.result).toEqual(expectedResult); + expect(result.newMessages).toEqual([]); + }); + }); + + describe("subscriber notifications integration", () => { + it("should track newMessages without interfering with existing event processing", async () => { + const mockSubscriber: AgentSubscriber = { + onNewMessage: jest.fn(), + onMessagesChanged: jest.fn(), + onNewToolCall: jest.fn(), + }; + + agent.subscribe(mockSubscriber); + + const newMessages: Message[] = [ + { + id: "new-msg-1", + role: "user", + content: "New user message", + }, + { + id: "new-msg-2", + role: "assistant", + content: "New assistant message", + toolCalls: [ + { + id: "call-1", + type: "function", + function: { name: "test_tool", arguments: "{}" }, + }, + ], + }, + ]; + + const allMessages = [...agent.messages, ...newMessages]; + + agent.setEventsToEmit([ + { + type: EventType.MESSAGES_SNAPSHOT, + messages: allMessages, + } as MessagesSnapshotEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.newMessages).toEqual(newMessages); + + // Note: Subscriber notifications are handled by the existing event processing pipeline + // The newMessages tracking is separate from subscriber notification logic + }); + + it("should return empty newMessages when no messages are added", async () => { + const mockSubscriber: AgentSubscriber = { + onNewMessage: jest.fn(), + onMessagesChanged: jest.fn(), + onNewToolCall: jest.fn(), + }; + + agent.subscribe(mockSubscriber); + + agent.setEventsToEmit([ + { + type: EventType.RUN_FINISHED, + threadId: "test-thread", + runId: "test-run", + result: "no new messages", + } as RunFinishedEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.newMessages).toEqual([]); + + // Should not fire any new message events since no messages were added + expect(mockSubscriber.onNewMessage).not.toHaveBeenCalled(); + expect(mockSubscriber.onNewToolCall).not.toHaveBeenCalled(); + expect(mockSubscriber.onMessagesChanged).not.toHaveBeenCalled(); + }); + }); + + describe("edge cases", () => { + it("should handle agent with no initial messages", async () => { + const emptyAgent = new TestAgent({ + threadId: "empty-thread", + initialMessages: [], + }); + + const newMessages: Message[] = [{ id: "first-ever", role: "user", content: "First message" }]; + + emptyAgent.setEventsToEmit([ + { + type: EventType.MESSAGES_SNAPSHOT, + messages: newMessages, + } as MessagesSnapshotEvent, + ]); + + const result = await emptyAgent.runAgent(); + + expect(result.newMessages).toEqual(newMessages); + expect(emptyAgent.messages).toEqual(newMessages); + }); + + it("should handle messages with duplicate IDs correctly", async () => { + // This tests that we're using Set correctly for ID tracking + const messageWithSameId: Message = { + id: "existing-msg-1", // Same ID as existing message + role: "user", + content: "Updated content", + }; + + const allMessages = [...agent.messages, messageWithSameId]; + + agent.setEventsToEmit([ + { + type: EventType.MESSAGES_SNAPSHOT, + messages: allMessages, + } as MessagesSnapshotEvent, + ]); + + const result = await agent.runAgent(); + + // Should not include the duplicate ID in newMessages + expect(result.newMessages).toEqual([]); + expect(agent.messages).toEqual(allMessages); + }); + + it("should handle complex result objects", async () => { + const complexResult = { + metadata: { + timestamp: new Date().toISOString(), + version: "1.0.0", + }, + data: { + results: [1, 2, 3], + nested: { + deep: { + value: "test", + }, + }, + }, + stats: { + processingTime: 1000, + tokensUsed: 150, + }, + }; + + agent.setEventsToEmit([ + { + type: EventType.RUN_FINISHED, + threadId: "test-thread", + runId: "test-run", + result: complexResult, + } as RunFinishedEvent, + ]); + + const result = await agent.runAgent(); + + expect(result.result).toEqual(complexResult); + expect(result.result).toMatchObject(complexResult); + }); + }); +}); diff --git a/typescript-sdk/packages/client/src/agent/__tests__/subscriber.test.ts b/typescript-sdk/packages/client/src/agent/__tests__/subscriber.test.ts new file mode 100644 index 000000000..1333ee78b --- /dev/null +++ b/typescript-sdk/packages/client/src/agent/__tests__/subscriber.test.ts @@ -0,0 +1,1291 @@ +import { AbstractAgent } from "../agent"; +import { AgentSubscriber } from "../subscriber"; +import { + BaseEvent, + EventType, + Message, + RunAgentInput, + TextMessageStartEvent, + TextMessageContentEvent, + TextMessageEndEvent, + StateSnapshotEvent, + RunStartedEvent, + RunFinishedEvent, + ToolCallStartEvent, + ToolCallArgsEvent, + ToolCallEndEvent, + ToolCallResultEvent, + CustomEvent, + StepStartedEvent, + StepFinishedEvent, +} from "@ag-ui/core"; +import { Observable, of, throwError, from } from "rxjs"; +import { mergeMap } from "rxjs/operators"; + +// Mock uuid module +jest.mock("uuid", () => ({ + v4: jest.fn().mockReturnValue("mock-uuid"), +})); + +// Mock utils with handling for undefined values +jest.mock("@/utils", () => ({ + structuredClone_: (obj: any) => { + if (obj === undefined) return undefined; + const jsonString = JSON.stringify(obj); + if (jsonString === undefined || jsonString === "undefined") return undefined; + return JSON.parse(jsonString); + }, +})); + +// Mock the verify modules but NOT apply - we want to test against real defaultApplyEvents +jest.mock("@/verify", () => ({ + verifyEvents: jest.fn(() => (source$: Observable) => source$), +})); + +jest.mock("@/chunks", () => ({ + transformChunks: jest.fn(() => (source$: Observable) => source$), +})); + +// Create a test agent implementation +class TestAgent extends AbstractAgent { + private eventsToEmit: BaseEvent[] = []; + + setEventsToEmit(events: BaseEvent[]) { + this.eventsToEmit = events; + } + + protected run(input: RunAgentInput): Observable { + return of(...this.eventsToEmit); + } +} + +describe("AgentSubscriber", () => { + let agent: TestAgent; + let mockSubscriber: AgentSubscriber; + + beforeEach(() => { + jest.clearAllMocks(); + + agent = new TestAgent({ + threadId: "test-thread", + initialMessages: [ + { + id: "msg-1", + role: "user", + content: "Hello", + }, + ], + initialState: { counter: 0 }, + }); + + mockSubscriber = { + onEvent: jest.fn(), + onRunStartedEvent: jest.fn(), + onRunFinishedEvent: jest.fn(), + onTextMessageStartEvent: jest.fn(), + onTextMessageContentEvent: jest.fn(), + onTextMessageEndEvent: jest.fn(), + onToolCallStartEvent: jest.fn(), + onToolCallArgsEvent: jest.fn(), + onToolCallEndEvent: jest.fn(), + onToolCallResultEvent: jest.fn(), + onCustomEvent: jest.fn(), + onStateSnapshotEvent: jest.fn(), + onMessagesChanged: jest.fn(), + onStateChanged: jest.fn(), + onNewMessage: jest.fn(), + onNewToolCall: jest.fn(), + onRunInitialized: jest.fn(), + onRunFailed: jest.fn(), + onRunFinalized: jest.fn(), + }; + }); + + describe("subscribe/unsubscribe functionality", () => { + it("should allow subscribing and unsubscribing", () => { + // Initially no subscribers + expect(agent.subscribers).toHaveLength(0); + + // Subscribe + const subscription = agent.subscribe(mockSubscriber); + expect(agent.subscribers).toHaveLength(1); + expect(agent.subscribers[0]).toBe(mockSubscriber); + + // Unsubscribe + subscription.unsubscribe(); + expect(agent.subscribers).toHaveLength(0); + }); + + it("should support multiple subscribers", () => { + const subscriber2: AgentSubscriber = { + onEvent: jest.fn(), + }; + + agent.subscribe(mockSubscriber); + agent.subscribe(subscriber2); + + expect(agent.subscribers).toHaveLength(2); + expect(agent.subscribers[0]).toBe(mockSubscriber); + expect(agent.subscribers[1]).toBe(subscriber2); + }); + + it("should only remove the specific subscriber on unsubscribe", () => { + const subscriber2: AgentSubscriber = { + onEvent: jest.fn(), + }; + + const subscription1 = agent.subscribe(mockSubscriber); + const subscription2 = agent.subscribe(subscriber2); + + expect(agent.subscribers).toHaveLength(2); + + subscription1.unsubscribe(); + expect(agent.subscribers).toHaveLength(1); + expect(agent.subscribers[0]).toBe(subscriber2); + + subscription2.unsubscribe(); + expect(agent.subscribers).toHaveLength(0); + }); + }); + + describe("temporary subscribers via runAgent", () => { + it("should accept a temporary subscriber via runAgent parameter", async () => { + const temporarySubscriber: AgentSubscriber = { + onRunStartedEvent: jest.fn(), + onRunFinishedEvent: jest.fn(), + }; + + const runStartedEvent: RunStartedEvent = { + type: EventType.RUN_STARTED, + threadId: "test-thread", + runId: "test-run", + }; + + const runFinishedEvent: RunFinishedEvent = { + type: EventType.RUN_FINISHED, + threadId: "test-thread", + runId: "test-run", + result: "test-result", + }; + + agent.setEventsToEmit([runStartedEvent, runFinishedEvent]); + + await agent.runAgent({}, temporarySubscriber); + + // The temporary subscriber should have been called + expect(temporarySubscriber.onRunStartedEvent).toHaveBeenCalledWith( + expect.objectContaining({ + event: runStartedEvent, + messages: agent.messages, + state: agent.state, + agent, + }), + ); + + expect(temporarySubscriber.onRunFinishedEvent).toHaveBeenCalledWith( + expect.objectContaining({ + event: runFinishedEvent, + result: "test-result", + messages: agent.messages, + state: agent.state, + agent, + }), + ); + }); + + it("should combine permanent and temporary subscribers", async () => { + const permanentSubscriber: AgentSubscriber = { + onRunStartedEvent: jest.fn(), + }; + + const temporarySubscriber: AgentSubscriber = { + onRunStartedEvent: jest.fn(), + }; + + agent.subscribe(permanentSubscriber); + + const runStartedEvent: RunStartedEvent = { + type: EventType.RUN_STARTED, + threadId: "test-thread", + runId: "test-run", + }; + + agent.setEventsToEmit([runStartedEvent]); + + await agent.runAgent({}, temporarySubscriber); + + // Both subscribers should have been called + expect(permanentSubscriber.onRunStartedEvent).toHaveBeenCalled(); + expect(temporarySubscriber.onRunStartedEvent).toHaveBeenCalled(); + }); + }); + + describe("mutation capabilities", () => { + it("should allow subscribers to mutate messages", async () => { + const newMessage: Message = { + id: "new-msg", + role: "assistant", + content: "I was added by subscriber", + }; + + const mutatingSubscriber: AgentSubscriber = { + onRunInitialized: jest.fn().mockReturnValue({ + messages: [...agent.messages, newMessage], + }), + onMessagesChanged: jest.fn(), + }; + + // Emit a dummy event to avoid EmptyError + agent.setEventsToEmit([ + { + type: EventType.RUN_STARTED, + threadId: "test", + runId: "test", + } as RunStartedEvent, + ]); + + await agent.runAgent({}, mutatingSubscriber); + + // Verify the subscriber was called with the initial messages + expect(mutatingSubscriber.onRunInitialized).toHaveBeenCalledWith( + expect.objectContaining({ + messages: [ + { + id: "msg-1", + role: "user", + content: "Hello", + }, + ], + }), + ); + + // Verify the agent's messages were updated + expect(agent.messages).toHaveLength(2); + expect(agent.messages[1]).toEqual(newMessage); + + // Verify onMessagesChanged was called + expect(mutatingSubscriber.onMessagesChanged).toHaveBeenCalledWith( + expect.objectContaining({ + messages: agent.messages, + }), + ); + }); + + it("should allow subscribers to mutate state", async () => { + const mutatingSubscriber: AgentSubscriber = { + onRunInitialized: jest.fn().mockReturnValue({ + state: { counter: 42, newField: "added" }, + }), + onStateChanged: jest.fn(), + }; + + // Emit a dummy event to avoid EmptyError + agent.setEventsToEmit([ + { + type: EventType.RUN_STARTED, + threadId: "test", + runId: "test", + } as RunStartedEvent, + ]); + + await agent.runAgent({}, mutatingSubscriber); + + // Verify the subscriber was called with the initial state + expect(mutatingSubscriber.onRunInitialized).toHaveBeenCalledWith( + expect.objectContaining({ + state: { counter: 0 }, + }), + ); + + // Verify the agent's state was updated + expect(agent.state).toEqual({ counter: 42, newField: "added" }); + + // Verify onStateChanged was called + expect(mutatingSubscriber.onStateChanged).toHaveBeenCalledWith( + expect.objectContaining({ + state: agent.state, + }), + ); + }); + + it("should allow mutations in event handlers", async () => { + const stateEvent: StateSnapshotEvent = { + type: EventType.STATE_SNAPSHOT, + snapshot: { newCounter: 100 }, + }; + + const mutatingSubscriber: AgentSubscriber = { + onStateSnapshotEvent: jest.fn().mockReturnValue({ + state: { modifiedBySubscriber: true }, + stopPropagation: true, // Prevent the event from applying its snapshot + }), + onStateChanged: jest.fn(), + }; + + agent.setEventsToEmit([stateEvent]); + + await agent.runAgent({}, mutatingSubscriber); + + expect(mutatingSubscriber.onStateSnapshotEvent).toHaveBeenCalledWith( + expect.objectContaining({ + event: stateEvent, + }), + ); + + // State should be updated by the subscriber + expect(agent.state).toEqual({ modifiedBySubscriber: true }); + expect(mutatingSubscriber.onStateChanged).toHaveBeenCalled(); + }); + }); + + describe("stopPropagation functionality", () => { + it("should stop propagation to subsequent subscribers when stopPropagation is true", async () => { + const firstSubscriber: AgentSubscriber = { + onRunInitialized: jest.fn().mockReturnValue({ + stopPropagation: true, + }), + }; + + const secondSubscriber: AgentSubscriber = { + onRunInitialized: jest.fn(), + }; + + agent.subscribe(firstSubscriber); + agent.subscribe(secondSubscriber); + + // Emit a dummy event to avoid EmptyError + agent.setEventsToEmit([ + { + type: EventType.RUN_STARTED, + threadId: "test", + runId: "test", + } as RunStartedEvent, + ]); + + await agent.runAgent({}); + + // First subscriber should be called + expect(firstSubscriber.onRunInitialized).toHaveBeenCalled(); + + // Second subscriber should NOT be called due to stopPropagation + expect(secondSubscriber.onRunInitialized).not.toHaveBeenCalled(); + }); + + it("should continue to next subscriber when stopPropagation is false", async () => { + const firstSubscriber: AgentSubscriber = { + onRunInitialized: jest.fn().mockReturnValue({ + stopPropagation: false, + }), + }; + + const secondSubscriber: AgentSubscriber = { + onRunInitialized: jest.fn(), + }; + + agent.subscribe(firstSubscriber); + agent.subscribe(secondSubscriber); + + agent.setEventsToEmit([ + { type: EventType.RUN_STARTED, threadId: "test", runId: "test" } as RunStartedEvent, + ]); + + await agent.runAgent({}); + + // Both subscribers should be called + expect(firstSubscriber.onRunInitialized).toHaveBeenCalled(); + expect(secondSubscriber.onRunInitialized).toHaveBeenCalled(); + }); + + it("should continue to next subscriber when stopPropagation is undefined", async () => { + const firstSubscriber: AgentSubscriber = { + onRunInitialized: jest.fn().mockReturnValue({}), // No stopPropagation field + }; + + const secondSubscriber: AgentSubscriber = { + onRunInitialized: jest.fn(), + }; + + agent.subscribe(firstSubscriber); + agent.subscribe(secondSubscriber); + + agent.setEventsToEmit([ + { type: EventType.RUN_STARTED, threadId: "test", runId: "test" } as RunStartedEvent, + ]); + + await agent.runAgent({}); + + // Both subscribers should be called + expect(firstSubscriber.onRunInitialized).toHaveBeenCalled(); + expect(secondSubscriber.onRunInitialized).toHaveBeenCalled(); + }); + + it("should stop default behavior on error when stopPropagation is true", async () => { + const errorHandlingSubscriber: AgentSubscriber = { + onRunFailed: jest.fn().mockReturnValue({ + stopPropagation: true, + }), + }; + + // Create an agent that throws an error + class ErrorAgent extends AbstractAgent { + protected run(input: RunAgentInput): Observable { + return from([ + { + type: EventType.RUN_STARTED, + threadId: input.threadId, + runId: input.runId, + } as RunStartedEvent, + ]).pipe(mergeMap(() => throwError(() => new Error("Test error")))); + } + } + + const errorAgent = new ErrorAgent(); + errorAgent.subscribe(errorHandlingSubscriber); + + // Mock console.error to check if it's called + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); + + // This should not throw because the subscriber handles the error + await expect(errorAgent.runAgent({})).resolves.toBeDefined(); + + expect(errorHandlingSubscriber.onRunFailed).toHaveBeenCalledWith( + expect.objectContaining({ + error: expect.any(Error), + }), + ); + + // Console.error should NOT be called because subscriber handled the error + expect(consoleErrorSpy).not.toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); + + it("should allow default error behavior when stopPropagation is false", async () => { + const errorHandlingSubscriber: AgentSubscriber = { + onRunFailed: jest.fn().mockReturnValue({ + stopPropagation: false, + }), + }; + + // Create an agent that throws an error + class ErrorAgent extends AbstractAgent { + protected run(input: RunAgentInput): Observable { + return from([ + { + type: EventType.RUN_STARTED, + threadId: input.threadId, + runId: input.runId, + } as RunStartedEvent, + ]).pipe(mergeMap(() => throwError(() => new Error("Test error")))); + } + } + + const errorAgent = new ErrorAgent(); + errorAgent.subscribe(errorHandlingSubscriber); + + // Mock console.error to check if it's called + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); + + // This should throw because the subscriber doesn't stop propagation + await expect(errorAgent.runAgent({})).rejects.toThrow("Test error"); + + expect(errorHandlingSubscriber.onRunFailed).toHaveBeenCalled(); + + // Console.error should be called because error propagated + expect(consoleErrorSpy).toHaveBeenCalledWith("Agent execution failed:", expect.any(Error)); + + consoleErrorSpy.mockRestore(); + }); + }); + + describe("subscriber order and chaining", () => { + it("should call subscribers in the order they were added", async () => { + const callOrder: string[] = []; + + const subscriber1: AgentSubscriber = { + onRunInitialized: jest.fn().mockImplementation(() => { + callOrder.push("subscriber1"); + }), + }; + + const subscriber2: AgentSubscriber = { + onRunInitialized: jest.fn().mockImplementation(() => { + callOrder.push("subscriber2"); + }), + }; + + const subscriber3: AgentSubscriber = { + onRunInitialized: jest.fn().mockImplementation(() => { + callOrder.push("subscriber3"); + }), + }; + + agent.subscribe(subscriber1); + agent.subscribe(subscriber2); + agent.subscribe(subscriber3); + + agent.setEventsToEmit([ + { type: EventType.RUN_STARTED, threadId: "test", runId: "test" } as RunStartedEvent, + ]); + + await agent.runAgent({}); + + expect(callOrder).toEqual(["subscriber1", "subscriber2", "subscriber3"]); + }); + + it("should pass mutations from one subscriber to the next", async () => { + const subscriber1: AgentSubscriber = { + onRunInitialized: jest.fn().mockReturnValue({ + state: { step: 1 }, + }), + }; + + const subscriber2: AgentSubscriber = { + onRunInitialized: jest.fn().mockImplementation((params) => { + // Should receive the state modified by subscriber1 + expect(params.state).toEqual({ step: 1 }); + return { + state: { step: 2 }, + }; + }), + }; + + const subscriber3: AgentSubscriber = { + onRunInitialized: jest.fn().mockImplementation((params) => { + // Should receive the state modified by subscriber2 + expect(params.state).toEqual({ step: 2 }); + return { + state: { step: 3 }, + }; + }), + }; + + agent.subscribe(subscriber1); + agent.subscribe(subscriber2); + agent.subscribe(subscriber3); + + agent.setEventsToEmit([ + { type: EventType.RUN_STARTED, threadId: "test", runId: "test" } as RunStartedEvent, + ]); + + await agent.runAgent({}); + + // Final state should reflect all mutations + expect(agent.state).toEqual({ step: 3 }); + + expect(subscriber1.onRunInitialized).toHaveBeenCalledWith( + expect.objectContaining({ + state: { counter: 0 }, // Original state + }), + ); + + expect(subscriber2.onRunInitialized).toHaveBeenCalledWith( + expect.objectContaining({ + state: { step: 1 }, // Modified by subscriber1 + }), + ); + + expect(subscriber3.onRunInitialized).toHaveBeenCalledWith( + expect.objectContaining({ + state: { step: 2 }, // Modified by subscriber2 + }), + ); + }); + }); + + describe("event-specific callbacks", () => { + it("should call specific event callbacks with correct parameters", async () => { + const textStartEvent: TextMessageStartEvent = { + type: EventType.TEXT_MESSAGE_START, + messageId: "test-msg", + role: "assistant", + }; + + const textContentEvent: TextMessageContentEvent = { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "test-msg", + delta: "Hello", + }; + + const specificSubscriber: AgentSubscriber = { + onTextMessageStartEvent: jest.fn(), + onTextMessageContentEvent: jest.fn(), + }; + + agent.subscribe(specificSubscriber); + agent.setEventsToEmit([textStartEvent, textContentEvent]); + + await agent.runAgent({}); + + expect(specificSubscriber.onTextMessageStartEvent).toHaveBeenCalledWith( + expect.objectContaining({ + event: textStartEvent, + messages: [{ content: "Hello", id: "msg-1", role: "user" }], // Pre-mutation state + state: { counter: 0 }, // Pre-mutation state + agent, + }), + ); + + expect(specificSubscriber.onTextMessageContentEvent).toHaveBeenCalledWith( + expect.objectContaining({ + event: textContentEvent, + textMessageBuffer: "", // Empty - buffer before current delta is applied + messages: expect.arrayContaining([ + expect.objectContaining({ content: "Hello", id: "msg-1", role: "user" }), + expect.objectContaining({ content: "", id: "test-msg", role: "assistant" }), // Message before delta applied + ]), + state: { counter: 0 }, + agent, + }), + ); + }); + + it("should call generic onEvent callback for all events", async () => { + const events: BaseEvent[] = [ + { + type: EventType.TEXT_MESSAGE_START, + messageId: "test-msg", + role: "assistant", + } as TextMessageStartEvent, + { + type: EventType.STATE_SNAPSHOT, + snapshot: { test: true }, + } as StateSnapshotEvent, + ]; + + const genericSubscriber: AgentSubscriber = { + onEvent: jest.fn(), + }; + + agent.subscribe(genericSubscriber); + agent.setEventsToEmit(events); + + await agent.runAgent({}); + + expect(genericSubscriber.onEvent).toHaveBeenCalledTimes(2); + expect(genericSubscriber.onEvent).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + event: events[0], + }), + ); + expect(genericSubscriber.onEvent).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + event: events[1], + }), + ); + }); + }); + + describe("lifecycle callbacks", () => { + it("should call lifecycle callbacks in correct order", async () => { + const callOrder: string[] = []; + + const lifecycleSubscriber: AgentSubscriber = { + onRunInitialized: jest.fn().mockImplementation(() => { + callOrder.push("initialized"); + }), + onRunFinalized: jest.fn().mockImplementation(() => { + callOrder.push("finalized"); + }), + }; + + agent.subscribe(lifecycleSubscriber); + agent.setEventsToEmit([ + { type: EventType.RUN_STARTED, threadId: "test", runId: "test" } as RunStartedEvent, + ]); + + await agent.runAgent({}); + + expect(callOrder).toEqual(["initialized", "finalized"]); + }); + + it("should call onRunFinalized even after errors", async () => { + const lifecycleSubscriber: AgentSubscriber = { + onRunFailed: jest.fn().mockReturnValue({ + stopPropagation: true, // Handle the error + }), + onRunFinalized: jest.fn(), + }; + + // Create an agent that throws an error + class ErrorAgent extends AbstractAgent { + protected run(input: RunAgentInput): Observable { + return from([ + { + type: EventType.RUN_STARTED, + threadId: input.threadId, + runId: input.runId, + } as RunStartedEvent, + ]).pipe(mergeMap(() => throwError(() => new Error("Test error")))); + } + } + + const errorAgent = new ErrorAgent(); + errorAgent.subscribe(lifecycleSubscriber); + + await errorAgent.runAgent({}); + + expect(lifecycleSubscriber.onRunFailed).toHaveBeenCalled(); + expect(lifecycleSubscriber.onRunFinalized).toHaveBeenCalled(); + }); + }); + + describe("Tool Call Tests", () => { + test("should handle tool call events with proper buffer accumulation", async () => { + // Create agent that emits tool call sequence + const toolCallAgent = new TestAgent(); + toolCallAgent.subscribe(mockSubscriber); + toolCallAgent.setEventsToEmit([ + { + type: EventType.TOOL_CALL_START, + toolCallId: "call-123", + toolCallName: "search", + } as ToolCallStartEvent, + { + type: EventType.TOOL_CALL_ARGS, + toolCallId: "call-123", + delta: '{"query": "te', + } as ToolCallArgsEvent, + { + type: EventType.TOOL_CALL_ARGS, + toolCallId: "call-123", + delta: 'st"}', + } as ToolCallArgsEvent, + { + type: EventType.TOOL_CALL_END, + toolCallId: "call-123", + } as ToolCallEndEvent, + ]); + + await toolCallAgent.runAgent({}); + + // Verify tool call events were called + expect(mockSubscriber.onToolCallStartEvent).toHaveBeenCalledWith( + expect.objectContaining({ + event: expect.objectContaining({ + type: EventType.TOOL_CALL_START, + toolCallId: "call-123", + toolCallName: "search", + }), + messages: [], + state: {}, + agent: toolCallAgent, + }), + ); + + // Check buffer accumulation + expect(mockSubscriber.onToolCallArgsEvent).toHaveBeenCalledTimes(2); + + // First call should have empty buffer (before first delta applied) + expect(mockSubscriber.onToolCallArgsEvent).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + toolCallBuffer: "", + toolCallName: "search", + partialToolCallArgs: "", // Empty string when buffer is empty + }), + ); + + // Second call should have partial buffer (before second delta applied) + expect(mockSubscriber.onToolCallArgsEvent).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + toolCallBuffer: '{"query": "te', + toolCallName: "search", + partialToolCallArgs: '{"query": "te"}', // untruncateJson returns truncated JSON string + }), + ); + + expect(mockSubscriber.onToolCallEndEvent).toHaveBeenCalledWith( + expect.objectContaining({ + toolCallName: "search", + toolCallArgs: { query: "test" }, + }), + ); + + expect(mockSubscriber.onNewToolCall).toHaveBeenCalledWith( + expect.objectContaining({ + toolCall: { + id: "call-123", + type: "function", + function: { + name: "search", + arguments: '{"query": "test"}', + }, + }, + }), + ); + }); + }); + + describe("Buffer Accumulation Tests", () => { + test("should properly accumulate text message buffer", async () => { + const textAgent = new TestAgent(); + textAgent.subscribe(mockSubscriber); + textAgent.setEventsToEmit([ + { + type: EventType.TEXT_MESSAGE_START, + messageId: "msg-1", + role: "assistant", + } as TextMessageStartEvent, + { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "msg-1", + delta: "Hello", + } as TextMessageContentEvent, + { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "msg-1", + delta: " ", + } as TextMessageContentEvent, + { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "msg-1", + delta: "World", + } as TextMessageContentEvent, + { + type: EventType.TEXT_MESSAGE_END, + messageId: "msg-1", + } as TextMessageEndEvent, + ]); + + await textAgent.runAgent({}); + + // Verify buffer accumulation + expect(mockSubscriber.onTextMessageContentEvent).toHaveBeenCalledTimes(3); + + expect(mockSubscriber.onTextMessageContentEvent).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + textMessageBuffer: "", // First event: no content accumulated yet + }), + ); + + expect(mockSubscriber.onTextMessageContentEvent).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + textMessageBuffer: "Hello", // Second event: content from first event + }), + ); + + expect(mockSubscriber.onTextMessageContentEvent).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + textMessageBuffer: "Hello ", // Third event: content from first + second events + }), + ); + + expect(mockSubscriber.onTextMessageEndEvent).toHaveBeenCalledWith( + expect.objectContaining({ + textMessageBuffer: "Hello World", + }), + ); + }); + + test("should reset text buffer on new message", async () => { + const multiMessageAgent = new TestAgent(); + multiMessageAgent.subscribe(mockSubscriber); + multiMessageAgent.setEventsToEmit([ + { + type: EventType.TEXT_MESSAGE_START, + messageId: "msg-1", + } as TextMessageStartEvent, + { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "msg-1", + delta: "First", + } as TextMessageContentEvent, + { + type: EventType.TEXT_MESSAGE_END, + messageId: "msg-1", + } as TextMessageEndEvent, + { + type: EventType.TEXT_MESSAGE_START, + messageId: "msg-2", + } as TextMessageStartEvent, + { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "msg-2", + delta: "Second", + } as TextMessageContentEvent, + { + type: EventType.TEXT_MESSAGE_END, + messageId: "msg-2", + } as TextMessageEndEvent, + ]); + + await multiMessageAgent.runAgent({}); + + // Check first message + expect(mockSubscriber.onTextMessageContentEvent).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + textMessageBuffer: "", // First message, first content: no content accumulated yet + }), + ); + + // Check second message (buffer should reset) + expect(mockSubscriber.onTextMessageContentEvent).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + textMessageBuffer: "", // Second message, first content: buffer reset, no content accumulated yet + }), + ); + }); + }); + + describe("Message and Tool Call Lifecycle Tests", () => { + test("should call onNewMessage after text message completion", async () => { + const textAgent = new TestAgent(); + textAgent.subscribe(mockSubscriber); + textAgent.setEventsToEmit([ + { + type: EventType.TEXT_MESSAGE_START, + messageId: "msg-1", + role: "assistant", + } as TextMessageStartEvent, + { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "msg-1", + delta: "Test message", + } as TextMessageContentEvent, + { + type: EventType.TEXT_MESSAGE_END, + messageId: "msg-1", + } as TextMessageEndEvent, + ]); + + await textAgent.runAgent({}); + + expect(mockSubscriber.onNewMessage).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.objectContaining({ + id: "msg-1", + role: "assistant", + content: "Test message", + }), + }), + ); + }); + + test("should call onNewToolCall after tool call completion", async () => { + const toolCallAgent = new TestAgent(); + toolCallAgent.subscribe(mockSubscriber); + toolCallAgent.setEventsToEmit([ + { + type: EventType.TOOL_CALL_START, + toolCallId: "call-123", + toolCallName: "search", + } as ToolCallStartEvent, + { + type: EventType.TOOL_CALL_ARGS, + toolCallId: "call-123", + delta: '{"query": "test"}', + } as ToolCallArgsEvent, + { + type: EventType.TOOL_CALL_END, + toolCallId: "call-123", + } as ToolCallEndEvent, + ]); + + await toolCallAgent.runAgent({}); + + expect(mockSubscriber.onNewToolCall).toHaveBeenCalledWith( + expect.objectContaining({ + toolCall: { + id: "call-123", + type: "function", + function: { + name: "search", + arguments: '{"query": "test"}', + }, + }, + }), + ); + }); + }); + + describe("Custom Event Tests", () => { + test("should handle custom events", async () => { + const customAgent = new TestAgent(); + customAgent.subscribe(mockSubscriber); + customAgent.setEventsToEmit([ + { + type: EventType.CUSTOM, + name: "user_interaction", + data: { action: "click", target: "button" }, + } as CustomEvent, + ]); + + await customAgent.runAgent({}); + + expect(mockSubscriber.onCustomEvent).toHaveBeenCalledWith( + expect.objectContaining({ + event: expect.objectContaining({ + type: EventType.CUSTOM, + name: "user_interaction", + data: { action: "click", target: "button" }, + }), + messages: [], + state: {}, + agent: customAgent, + }), + ); + }); + }); + + describe("Subscriber Error Handling", () => { + test("should handle errors in subscriber callbacks gracefully", async () => { + const errorSubscriber = { + onEvent: jest.fn().mockImplementation(() => { + // Return stopPropagation to handle the error gracefully + throw new Error("Subscriber error"); + }), + onTextMessageStartEvent: jest.fn().mockImplementation(() => { + throw new Error("Sync subscriber error"); + }), + }; + + // Add a working subscriber to ensure others still work + const workingSubscriber = { + onEvent: jest.fn(), + onTextMessageStartEvent: jest.fn(), + }; + + const testAgent = new TestAgent(); + testAgent.subscribe(errorSubscriber); + testAgent.subscribe(workingSubscriber); + testAgent.setEventsToEmit([ + { + type: EventType.TEXT_MESSAGE_START, + messageId: "msg-1", + } as TextMessageStartEvent, + ]); + + // Should not throw despite subscriber errors + await expect(testAgent.runAgent({})).resolves.toBeDefined(); + + expect(errorSubscriber.onEvent).toHaveBeenCalled(); + expect(errorSubscriber.onTextMessageStartEvent).toHaveBeenCalled(); + expect(workingSubscriber.onEvent).toHaveBeenCalled(); + expect(workingSubscriber.onTextMessageStartEvent).toHaveBeenCalled(); + }); + + test("should continue processing other subscribers when one fails", async () => { + const errorSubscriber = { + onTextMessageStartEvent: jest.fn().mockImplementation(() => { + throw new Error("First subscriber error"); + }), + }; + + const workingSubscriber = { + onTextMessageStartEvent: jest.fn().mockResolvedValue(undefined), + }; + + const testAgent = new TestAgent(); + testAgent.subscribe(errorSubscriber); + testAgent.subscribe(workingSubscriber); + testAgent.setEventsToEmit([ + { + type: EventType.TEXT_MESSAGE_START, + messageId: "msg-1", + } as TextMessageStartEvent, + ]); + + await testAgent.runAgent({}); + + expect(errorSubscriber.onTextMessageStartEvent).toHaveBeenCalled(); + expect(workingSubscriber.onTextMessageStartEvent).toHaveBeenCalled(); + }); + }); + + describe("Realistic Event Sequences", () => { + test("should handle a realistic conversation with mixed events", async () => { + const realisticAgent = new TestAgent(); + realisticAgent.subscribe(mockSubscriber); + realisticAgent.setEventsToEmit([ + { + type: EventType.RUN_STARTED, + runId: "run-123", + } as RunStartedEvent, + { + type: EventType.TEXT_MESSAGE_START, + messageId: "msg-1", + role: "assistant", + } as TextMessageStartEvent, + { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "msg-1", + delta: "Let me search for that information.", + } as TextMessageContentEvent, + { + type: EventType.TEXT_MESSAGE_END, + messageId: "msg-1", + } as TextMessageEndEvent, + { + type: EventType.TOOL_CALL_START, + toolCallId: "call-1", + toolCallName: "search", + } as ToolCallStartEvent, + { + type: EventType.TOOL_CALL_ARGS, + toolCallId: "call-1", + delta: '{"query": "weather today"}', + } as ToolCallArgsEvent, + { + type: EventType.TOOL_CALL_END, + toolCallId: "call-1", + } as ToolCallEndEvent, + { + type: EventType.TOOL_CALL_RESULT, + toolCallId: "call-1", + content: "Sunny, 75Β°F", + messageId: "result-1", + } as ToolCallResultEvent, + { + type: EventType.TEXT_MESSAGE_START, + messageId: "msg-2", + role: "assistant", + } as TextMessageStartEvent, + { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "msg-2", + delta: "The weather today is sunny and 75Β°F.", + } as TextMessageContentEvent, + { + type: EventType.TEXT_MESSAGE_END, + messageId: "msg-2", + } as TextMessageEndEvent, + { + type: EventType.STATE_SNAPSHOT, + state: { weather: "sunny" }, + } as StateSnapshotEvent, + { + type: EventType.RUN_FINISHED, + runId: "run-123", + result: "success", + } as RunFinishedEvent, + ]); + + await realisticAgent.runAgent({}); + + // Verify complete sequence was processed + expect(mockSubscriber.onRunStartedEvent).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onTextMessageStartEvent).toHaveBeenCalledTimes(2); + expect(mockSubscriber.onTextMessageEndEvent).toHaveBeenCalledTimes(2); + expect(mockSubscriber.onToolCallStartEvent).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onToolCallEndEvent).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onToolCallResultEvent).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onStateSnapshotEvent).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onRunFinishedEvent).toHaveBeenCalledTimes(1); + expect(mockSubscriber.onNewMessage).toHaveBeenCalledTimes(3); // 2 TEXT_MESSAGE_END + 1 TOOL_CALL_RESULT + expect(mockSubscriber.onNewToolCall).toHaveBeenCalledTimes(1); + }); + }); + + describe("Advanced Mutation Tests", () => { + test("should handle mutations with stopPropagation in tool call events", async () => { + const mutatingSubscriber = { + onToolCallStartEvent: jest.fn().mockResolvedValue({ + state: { toolCallBlocked: true }, + stopPropagation: true, + }), + }; + + const secondSubscriber = { + onToolCallStartEvent: jest.fn(), + }; + + const toolCallAgent = new TestAgent(); + toolCallAgent.subscribe(mutatingSubscriber); + toolCallAgent.subscribe(secondSubscriber); + toolCallAgent.setEventsToEmit([ + { + type: EventType.TOOL_CALL_START, + toolCallId: "call-123", + toolCallName: "search", + } as ToolCallStartEvent, + ]); + + await toolCallAgent.runAgent({}); + + expect(mutatingSubscriber.onToolCallStartEvent).toHaveBeenCalled(); + expect(secondSubscriber.onToolCallStartEvent).not.toHaveBeenCalled(); + }); + + test("should accumulate mutations across multiple event types", async () => { + let messageCount = 0; + let stateUpdates = 0; + + const trackingSubscriber = { + onTextMessageStartEvent: jest.fn().mockImplementation(() => { + messageCount++; + return { state: { messageCount } }; + }), + onToolCallStartEvent: jest.fn().mockImplementation(() => { + stateUpdates++; + return { state: { stateUpdates } }; + }), + }; + + const mixedAgent = new TestAgent(); + mixedAgent.subscribe(mockSubscriber); + mixedAgent.subscribe(trackingSubscriber); + mixedAgent.setEventsToEmit([ + { + type: EventType.TEXT_MESSAGE_START, + messageId: "msg-1", + } as TextMessageStartEvent, + { + type: EventType.TOOL_CALL_START, + toolCallId: "call-1", + toolCallName: "search", + } as ToolCallStartEvent, + { + type: EventType.TEXT_MESSAGE_START, + messageId: "msg-2", + } as TextMessageStartEvent, + ]); + + await mixedAgent.runAgent({}); + + expect(trackingSubscriber.onTextMessageStartEvent).toHaveBeenCalledTimes(2); + expect(trackingSubscriber.onToolCallStartEvent).toHaveBeenCalledTimes(1); + }); + }); + + describe("EmptyError Bug Reproduction", () => { + test("should demonstrate EmptyError with STEP_STARTED/STEP_FINISHED events that cause no mutations", async () => { + const emptyAgent = new TestAgent(); + + // No subscribers that return mutations + emptyAgent.setEventsToEmit([ + { + type: EventType.RUN_STARTED, + runId: "run-123", + } as RunStartedEvent, + { + type: EventType.STEP_STARTED, + stepName: "step-1", + } as StepStartedEvent, + { + type: EventType.STEP_FINISHED, + stepName: "step-1", + } as StepFinishedEvent, + { + type: EventType.RUN_FINISHED, + runId: "run-123", + } as RunFinishedEvent, + ]); + + // This should throw EmptyError because: + // 1. STEP_STARTED and STEP_FINISHED have no default behavior (don't modify messages/state) + // 2. No subscribers return mutations + // 3. ALL calls to emitUpdates() return EMPTY + // 4. Observable completes without emitting anything + await expect(emptyAgent.runAgent({})); + }); + }); +}); diff --git a/typescript-sdk/packages/client/src/agent/agent.ts b/typescript-sdk/packages/client/src/agent/agent.ts index 3f6dcdf21..f596a625c 100644 --- a/typescript-sdk/packages/client/src/agent/agent.ts +++ b/typescript-sdk/packages/client/src/agent/agent.ts @@ -1,17 +1,23 @@ import { defaultApplyEvents } from "@/apply/default"; -import { Message, State, RunAgentInput, RunAgent, ApplyEvents, BaseEvent } from "@ag-ui/core"; +import { Message, State, RunAgentInput, BaseEvent, ToolCall, AssistantMessage } from "@ag-ui/core"; import { AgentConfig, RunAgentParameters } from "./types"; import { v4 as uuidv4 } from "uuid"; import { structuredClone_ } from "@/utils"; import { catchError, map, tap } from "rxjs/operators"; import { finalize } from "rxjs/operators"; -import { throwError, pipe, Observable } from "rxjs"; +import { pipe, Observable, from, of } from "rxjs"; import { verifyEvents } from "@/verify"; import { convertToLegacyEvents } from "@/legacy/convert"; import { LegacyRuntimeProtocolEvent } from "@/legacy/types"; -import { lastValueFrom, of } from "rxjs"; +import { lastValueFrom } from "rxjs"; import { transformChunks } from "@/chunks"; +import { AgentStateMutation, AgentSubscriber, runSubscribersWithMutation } from "./subscriber"; + +export interface RunAgentResult { + result: any; + newMessages: Message[]; +} export abstract class AbstractAgent { public agentId?: string; @@ -20,6 +26,7 @@ export abstract class AbstractAgent { public messages: Message[]; public state: State; public debug: boolean = false; + public subscribers: AgentSubscriber[] = []; constructor({ agentId, @@ -37,47 +44,98 @@ export abstract class AbstractAgent { this.debug = debug ?? false; } - protected abstract run(...args: Parameters): ReturnType; + public subscribe(subscriber: AgentSubscriber) { + this.subscribers.push(subscriber); + return { + unsubscribe: () => { + this.subscribers = this.subscribers.filter((s) => s !== subscriber); + }, + }; + } + + protected abstract run(input: RunAgentInput): Observable; - public async runAgent(parameters?: RunAgentParameters): Promise { + public async runAgent( + parameters?: RunAgentParameters, + subscriber?: AgentSubscriber, + ): Promise { this.agentId = this.agentId ?? uuidv4(); const input = this.prepareRunAgentInput(parameters); + let result: any = undefined; + const currentMessageIds = new Set(this.messages.map((message) => message.id)); + + const subscribers: AgentSubscriber[] = [ + { + onRunFinishedEvent: (params) => { + result = params.result; + }, + }, + ...this.subscribers, + subscriber ?? {}, + ]; + + await this.onInitialize(input, subscribers); const pipeline = pipe( () => this.run(input), transformChunks(this.debug), verifyEvents(this.debug), - (source$) => this.apply(input, source$), - (source$) => this.processApplyEvents(input, source$), + (source$) => this.apply(input, source$, subscribers), + (source$) => this.processApplyEvents(input, source$, subscribers), catchError((error) => { - this.onError(error); - return throwError(() => error); + return this.onError(input, error, subscribers); }), finalize(() => { - this.onFinalize(); + void this.onFinalize(input, subscribers); }), ); - return lastValueFrom(pipeline(of(null))).then(() => {}); + return lastValueFrom(pipeline(of(null))).then(() => { + const newMessages = structuredClone_(this.messages).filter( + (message: Message) => !currentMessageIds.has(message.id), + ); + return { result, newMessages }; + }); } public abortRun() {} - protected apply(...args: Parameters): ReturnType { - return defaultApplyEvents(...args); + protected apply( + input: RunAgentInput, + events$: Observable, + subscribers: AgentSubscriber[], + ): Observable { + return defaultApplyEvents(input, events$, this, subscribers); } protected processApplyEvents( input: RunAgentInput, - events$: ReturnType, - ): ReturnType { + events$: Observable, + subscribers: AgentSubscriber[], + ): Observable { return events$.pipe( tap((event) => { if (event.messages) { this.messages = event.messages; + subscribers.forEach((subscriber) => { + subscriber.onMessagesChanged?.({ + messages: this.messages, + state: this.state, + agent: this, + input, + }); + }); } if (event.state) { this.state = event.state; + subscribers.forEach((subscriber) => { + subscriber.onStateChanged?.({ + state: this.state, + messages: this.messages, + agent: this, + input, + }); + }); } }), ); @@ -95,11 +153,130 @@ export abstract class AbstractAgent { }; } - protected onError(error: Error) { - console.error("Agent execution failed:", error); + protected async onInitialize(input: RunAgentInput, subscribers: AgentSubscriber[]) { + const onRunInitializedMutation = await runSubscribersWithMutation( + subscribers, + this.messages, + this.state, + (subscriber, messages, state) => + subscriber.onRunInitialized?.({ messages, state, agent: this, input }), + ); + if ( + onRunInitializedMutation.messages !== undefined || + onRunInitializedMutation.state !== undefined + ) { + if (onRunInitializedMutation.messages) { + this.messages = onRunInitializedMutation.messages; + input.messages = onRunInitializedMutation.messages; + subscribers.forEach((subscriber) => { + subscriber.onMessagesChanged?.({ + messages: this.messages, + state: this.state, + agent: this, + input, + }); + }); + } + if (onRunInitializedMutation.state) { + this.state = onRunInitializedMutation.state; + input.state = onRunInitializedMutation.state; + subscribers.forEach((subscriber) => { + subscriber.onStateChanged?.({ + state: this.state, + messages: this.messages, + agent: this, + input, + }); + }); + } + } + } + + protected onError(input: RunAgentInput, error: Error, subscribers: AgentSubscriber[]) { + return from( + runSubscribersWithMutation( + subscribers, + this.messages, + this.state, + (subscriber, messages, state) => + subscriber.onRunFailed?.({ error, messages, state, agent: this, input }), + ), + ).pipe( + map((onRunFailedMutation) => { + const mutation = onRunFailedMutation as AgentStateMutation; + if (mutation.messages !== undefined || mutation.state !== undefined) { + if (mutation.messages !== undefined) { + this.messages = mutation.messages; + subscribers.forEach((subscriber) => { + subscriber.onMessagesChanged?.({ + messages: this.messages, + state: this.state, + agent: this, + input, + }); + }); + } + if (mutation.state !== undefined) { + this.state = mutation.state; + subscribers.forEach((subscriber) => { + subscriber.onStateChanged?.({ + state: this.state, + messages: this.messages, + agent: this, + input, + }); + }); + } + } + + if (mutation.stopPropagation !== true) { + console.error("Agent execution failed:", error); + throw error; + } + + // Return an empty mutation instead of null to prevent EmptyError + return {} as AgentStateMutation; + }), + ); } - protected onFinalize() {} + protected async onFinalize(input: RunAgentInput, subscribers: AgentSubscriber[]) { + const onRunFinalizedMutation = await runSubscribersWithMutation( + subscribers, + this.messages, + this.state, + (subscriber, messages, state) => + subscriber.onRunFinalized?.({ messages, state, agent: this, input }), + ); + + if ( + onRunFinalizedMutation.messages !== undefined || + onRunFinalizedMutation.state !== undefined + ) { + if (onRunFinalizedMutation.messages !== undefined) { + this.messages = onRunFinalizedMutation.messages; + subscribers.forEach((subscriber) => { + subscriber.onMessagesChanged?.({ + messages: this.messages, + state: this.state, + agent: this, + input, + }); + }); + } + if (onRunFinalizedMutation.state !== undefined) { + this.state = onRunFinalizedMutation.state; + subscribers.forEach((subscriber) => { + subscriber.onStateChanged?.({ + state: this.state, + messages: this.messages, + agent: this, + input, + }); + }); + } + } + } public clone() { const cloned = Object.create(Object.getPrototypeOf(this)); @@ -114,6 +291,125 @@ export abstract class AbstractAgent { return cloned; } + public addMessage(message: Message) { + // Add message to the messages array + this.messages.push(message); + + // Notify subscribers sequentially in the background + (async () => { + // Fire onNewMessage sequentially + for (const subscriber of this.subscribers) { + await subscriber.onNewMessage?.({ + message, + messages: this.messages, + state: this.state, + agent: this, + }); + } + + // Fire onNewToolCall if the message is from assistant and contains tool calls + if (message.role === "assistant" && message.toolCalls) { + for (const toolCall of message.toolCalls) { + for (const subscriber of this.subscribers) { + await subscriber.onNewToolCall?.({ + toolCall, + messages: this.messages, + state: this.state, + agent: this, + }); + } + } + } + + // Fire onMessagesChanged sequentially + for (const subscriber of this.subscribers) { + await subscriber.onMessagesChanged?.({ + messages: this.messages, + state: this.state, + agent: this, + }); + } + })(); + } + + public addMessages(messages: Message[]) { + // Add all messages to the messages array + this.messages.push(...messages); + + // Notify subscribers sequentially in the background + (async () => { + // Fire onNewMessage and onNewToolCall for each message sequentially + for (const message of messages) { + // Fire onNewMessage sequentially + for (const subscriber of this.subscribers) { + await subscriber.onNewMessage?.({ + message, + messages: this.messages, + state: this.state, + agent: this, + }); + } + + // Fire onNewToolCall if the message is from assistant and contains tool calls + if (message.role === "assistant" && message.toolCalls) { + for (const toolCall of message.toolCalls) { + for (const subscriber of this.subscribers) { + await subscriber.onNewToolCall?.({ + toolCall, + messages: this.messages, + state: this.state, + agent: this, + }); + } + } + } + } + + // Fire onMessagesChanged once at the end sequentially + for (const subscriber of this.subscribers) { + await subscriber.onMessagesChanged?.({ + messages: this.messages, + state: this.state, + agent: this, + }); + } + })(); + } + + public setMessages(messages: Message[]) { + // Replace the entire messages array + this.messages = structuredClone_(messages); + + // Notify subscribers sequentially in the background + (async () => { + // Fire onMessagesChanged sequentially + for (const subscriber of this.subscribers) { + await subscriber.onMessagesChanged?.({ + messages: this.messages, + state: this.state, + agent: this, + }); + } + })(); + } + + public setState(state: State) { + // Replace the entire state + this.state = structuredClone_(state); + + // Notify subscribers sequentially in the background + (async () => { + // Fire onStateChanged sequentially + for (const subscriber of this.subscribers) { + await subscriber.onStateChanged?.({ + messages: this.messages, + state: this.state, + agent: this, + }); + } + })(); + } + public legacy_to_be_removed_runAgentBridged( config?: RunAgentParameters, ): Observable { diff --git a/typescript-sdk/packages/client/src/agent/http.ts b/typescript-sdk/packages/client/src/agent/http.ts index 3d8a1a93d..aba0b50cc 100644 --- a/typescript-sdk/packages/client/src/agent/http.ts +++ b/typescript-sdk/packages/client/src/agent/http.ts @@ -1,7 +1,7 @@ import { AbstractAgent } from "./agent"; -import { runHttpRequest, HttpEvent } from "@/run/http-request"; +import { runHttpRequest } from "@/run/http-request"; import { HttpAgentConfig, RunAgentParameters } from "./types"; -import { RunAgent, RunAgentInput, BaseEvent } from "@ag-ui/core"; +import { RunAgentInput, BaseEvent } from "@ag-ui/core"; import { structuredClone_ } from "@/utils"; import { transformHttpEventStream } from "@/transform/http"; import { Observable } from "rxjs"; diff --git a/typescript-sdk/packages/client/src/agent/subscriber.ts b/typescript-sdk/packages/client/src/agent/subscriber.ts new file mode 100644 index 000000000..9f772839c --- /dev/null +++ b/typescript-sdk/packages/client/src/agent/subscriber.ts @@ -0,0 +1,215 @@ +import { + BaseEvent, + Message, + RunAgentInput, + RunErrorEvent, + RunFinishedEvent, + RunStartedEvent, + State, + StateDeltaEvent, + StateSnapshotEvent, + StepFinishedEvent, + StepStartedEvent, + TextMessageContentEvent, + TextMessageEndEvent, + TextMessageStartEvent, + ToolCallArgsEvent, + ToolCallEndEvent, + ToolCallResultEvent, + ToolCallStartEvent, + MessagesSnapshotEvent, + RawEvent, + CustomEvent, + ToolCall, +} from "@ag-ui/core"; +import { AbstractAgent } from "./agent"; +import { structuredClone_ } from "@/utils"; + +export interface AgentStateMutation { + messages?: Message[]; + state?: State; + stopPropagation?: boolean; +} + +export interface AgentSubscriberParams { + messages: Message[]; + state: State; + agent: AbstractAgent; + input: RunAgentInput; +} + +// Utility type to allow callbacks to be implemented either synchronously or asynchronously. +export type MaybePromise = T | Promise; + +export interface AgentSubscriber { + // Request lifecycle + onRunInitialized?( + params: AgentSubscriberParams, + ): MaybePromise | void>; + onRunFailed?( + params: { error: Error } & AgentSubscriberParams, + ): MaybePromise | void>; + onRunFinalized?( + params: AgentSubscriberParams, + ): MaybePromise | void>; + + // Events + onEvent?( + params: { event: BaseEvent } & AgentSubscriberParams, + ): MaybePromise; + + onRunStartedEvent?( + params: { event: RunStartedEvent } & AgentSubscriberParams, + ): MaybePromise; + onRunFinishedEvent?( + params: { event: RunFinishedEvent; result?: any } & AgentSubscriberParams, + ): MaybePromise; + onRunErrorEvent?( + params: { event: RunErrorEvent } & AgentSubscriberParams, + ): MaybePromise; + + onStepStartedEvent?( + params: { event: StepStartedEvent } & AgentSubscriberParams, + ): MaybePromise; + onStepFinishedEvent?( + params: { event: StepFinishedEvent } & AgentSubscriberParams, + ): MaybePromise; + + onTextMessageStartEvent?( + params: { event: TextMessageStartEvent } & AgentSubscriberParams, + ): MaybePromise; + onTextMessageContentEvent?( + params: { + event: TextMessageContentEvent; + textMessageBuffer: string; + } & AgentSubscriberParams, + ): MaybePromise; + onTextMessageEndEvent?( + params: { event: TextMessageEndEvent; textMessageBuffer: string } & AgentSubscriberParams, + ): MaybePromise; + + onToolCallStartEvent?( + params: { event: ToolCallStartEvent } & AgentSubscriberParams, + ): MaybePromise; + onToolCallArgsEvent?( + params: { + event: ToolCallArgsEvent; + toolCallBuffer: string; + toolCallName: string; + partialToolCallArgs: Record; + } & AgentSubscriberParams, + ): MaybePromise; + onToolCallEndEvent?( + params: { + event: ToolCallEndEvent; + toolCallName: string; + toolCallArgs: Record; + } & AgentSubscriberParams, + ): MaybePromise; + + onToolCallResultEvent?( + params: { event: ToolCallResultEvent } & AgentSubscriberParams, + ): MaybePromise; + + onStateSnapshotEvent?( + params: { event: StateSnapshotEvent } & AgentSubscriberParams, + ): MaybePromise; + + onStateDeltaEvent?( + params: { event: StateDeltaEvent } & AgentSubscriberParams, + ): MaybePromise; + + onMessagesSnapshotEvent?( + params: { event: MessagesSnapshotEvent } & AgentSubscriberParams, + ): MaybePromise; + + onRawEvent?( + params: { event: RawEvent } & AgentSubscriberParams, + ): MaybePromise; + + onCustomEvent?( + params: { event: CustomEvent } & AgentSubscriberParams, + ): MaybePromise; + + // State changes + onMessagesChanged?( + params: Omit & { input?: RunAgentInput }, + ): MaybePromise; + onStateChanged?( + params: Omit & { input?: RunAgentInput }, + ): MaybePromise; + onNewMessage?( + params: { message: Message } & Omit & { + input?: RunAgentInput; + }, + ): MaybePromise; + onNewToolCall?( + params: { toolCall: ToolCall } & Omit & { + input?: RunAgentInput; + }, + ): MaybePromise; +} + +export async function runSubscribersWithMutation( + subscribers: AgentSubscriber[], + initialMessages: Message[], + initialState: State, + executor: ( + subscriber: AgentSubscriber, + messages: Message[], + state: State, + ) => MaybePromise, +): Promise { + let messages: Message[] = initialMessages; + let state: State = initialState; + + let stopPropagation: boolean | undefined = undefined; + + for (const subscriber of subscribers) { + try { + const mutation = await executor( + subscriber, + structuredClone_(messages), + structuredClone_(state), + ); + + if (mutation === undefined) { + // Nothing returned – keep going + continue; + } + + // Merge messages/state so next subscriber sees latest view + if (mutation.messages !== undefined) { + messages = mutation.messages; + } + + if (mutation.state !== undefined) { + state = mutation.state; + } + + stopPropagation = mutation.stopPropagation; + + if (stopPropagation === true) { + break; + } + } catch (error) { + // Log subscriber errors but continue processing (silence during tests) + const isTestEnvironment = + process.env.NODE_ENV === "test" || + process.env.JEST_WORKER_ID !== undefined || + typeof jest !== "undefined"; + + if (!isTestEnvironment) { + console.error("Subscriber error:", error); + } + // Continue to next subscriber unless we want to stop propagation + continue; + } + } + + return { + ...(JSON.stringify(messages) !== JSON.stringify(initialMessages) ? { messages } : {}), + ...(JSON.stringify(state) !== JSON.stringify(initialState) ? { state } : {}), + ...(stopPropagation !== undefined ? { stopPropagation } : {}), + }; +} diff --git a/typescript-sdk/packages/client/src/apply/__tests__/default.state.test.ts b/typescript-sdk/packages/client/src/apply/__tests__/default.state.test.ts index b0b942d7c..71e64efc1 100644 --- a/typescript-sdk/packages/client/src/apply/__tests__/default.state.test.ts +++ b/typescript-sdk/packages/client/src/apply/__tests__/default.state.test.ts @@ -1,23 +1,10 @@ +import { AbstractAgent } from "@/agent"; import { defaultApplyEvents } from "../default"; -import { EventType, StateDeltaEvent, AgentState } from "@ag-ui/core"; +import { EventType, StateDeltaEvent } from "@ag-ui/core"; import { of } from "rxjs"; +import { AgentStateMutation } from "@/agent/subscriber"; -// Define the exact type expected by defaultApplyEvents -interface RunAgentInput { - threadId: string; - runId: string; - messages: { - id: string; - role: "developer" | "system" | "assistant" | "user"; - content?: string; - name?: string; - toolCalls?: any[]; - }[]; - tools: any[]; - context: any[]; - state?: any; - forwardedProps?: any; -} +const FAKE_AGENT = null as unknown as AbstractAgent; describe("defaultApplyEvents - State Patching", () => { it("should apply state delta patch correctly", (done) => { @@ -43,9 +30,9 @@ describe("defaultApplyEvents - State Patching", () => { const events$ = of(stateDelta); - const result$ = defaultApplyEvents(initialState, events$); + const result$ = defaultApplyEvents(initialState, events$, FAKE_AGENT, []); - result$.subscribe((update: AgentState) => { + result$.subscribe((update: AgentStateMutation) => { expect(update.state).toEqual({ count: 1, text: "world", @@ -78,9 +65,9 @@ describe("defaultApplyEvents - State Patching", () => { const events$ = of(stateDelta); // Cast to any to bypass strict type checking - const result$ = defaultApplyEvents(initialState as any, events$); + const result$ = defaultApplyEvents(initialState as any, events$, FAKE_AGENT, []); - result$.subscribe((update: AgentState) => { + result$.subscribe((update: AgentStateMutation) => { expect(update.state).toEqual({ user: { name: "John", @@ -115,9 +102,9 @@ describe("defaultApplyEvents - State Patching", () => { const events$ = of(stateDelta); // Cast to any to bypass strict type checking - const result$ = defaultApplyEvents(initialState as any, events$); + const result$ = defaultApplyEvents(initialState as any, events$, FAKE_AGENT, []); - result$.subscribe((update: AgentState) => { + result$.subscribe((update: AgentStateMutation) => { expect(update.state).toEqual({ items: ["x", "b", "c", "d"], }); @@ -150,10 +137,10 @@ describe("defaultApplyEvents - State Patching", () => { const events$ = of(...stateDeltas); // Cast to any to bypass strict type checking - const result$ = defaultApplyEvents(initialState as any, events$); + const result$ = defaultApplyEvents(initialState as any, events$, FAKE_AGENT, []); let updateCount = 0; - result$.subscribe((update: AgentState) => { + result$.subscribe((update: AgentStateMutation) => { updateCount++; if (updateCount === 2) { expect(update.state).toEqual({ @@ -189,11 +176,11 @@ describe("defaultApplyEvents - State Patching", () => { const events$ = of(stateDelta); // Cast to any to bypass strict type checking - const result$ = defaultApplyEvents(initialState as any, events$); + const result$ = defaultApplyEvents(initialState as any, events$, FAKE_AGENT, []); let updateCount = 0; result$.subscribe({ - next: (update: AgentState) => { + next: (update: AgentStateMutation) => { updateCount++; }, complete: () => { diff --git a/typescript-sdk/packages/client/src/apply/__tests__/default.text-message.test.ts b/typescript-sdk/packages/client/src/apply/__tests__/default.text-message.test.ts index 9c254a5eb..581f418df 100644 --- a/typescript-sdk/packages/client/src/apply/__tests__/default.text-message.test.ts +++ b/typescript-sdk/packages/client/src/apply/__tests__/default.text-message.test.ts @@ -4,7 +4,6 @@ import { firstValueFrom } from "rxjs"; import { BaseEvent, EventType, - AgentState, RunStartedEvent, TextMessageStartEvent, TextMessageContentEvent, @@ -12,6 +11,9 @@ import { RunAgentInput, } from "@ag-ui/core"; import { defaultApplyEvents } from "../default"; +import { AbstractAgent } from "@/agent"; + +const FAKE_AGENT = null as unknown as AbstractAgent; describe("defaultApplyEvents with text messages", () => { it("should handle text message events correctly", async () => { @@ -27,7 +29,7 @@ describe("defaultApplyEvents with text messages", () => { }; // Create the observable stream - const result$ = defaultApplyEvents(initialState, events$); + const result$ = defaultApplyEvents(initialState, events$, FAKE_AGENT, []); // Collect all emitted state updates in an array const stateUpdatesPromise = firstValueFrom(result$.pipe(toArray())); @@ -100,7 +102,7 @@ describe("defaultApplyEvents with text messages", () => { }; // Create the observable stream - const result$ = defaultApplyEvents(initialState, events$); + const result$ = defaultApplyEvents(initialState, events$, FAKE_AGENT, []); // Collect all emitted state updates in an array const stateUpdatesPromise = firstValueFrom(result$.pipe(toArray())); diff --git a/typescript-sdk/packages/client/src/apply/__tests__/default.tool-calls.test.ts b/typescript-sdk/packages/client/src/apply/__tests__/default.tool-calls.test.ts index dd399d23e..bcce0ab40 100644 --- a/typescript-sdk/packages/client/src/apply/__tests__/default.tool-calls.test.ts +++ b/typescript-sdk/packages/client/src/apply/__tests__/default.tool-calls.test.ts @@ -4,25 +4,36 @@ import { firstValueFrom } from "rxjs"; import { BaseEvent, EventType, - AgentState, RunStartedEvent, ToolCallStartEvent, ToolCallArgsEvent, ToolCallEndEvent, + RunAgentInput, + AssistantMessage, } from "@ag-ui/core"; import { defaultApplyEvents } from "../default"; +import { AbstractAgent } from "@/agent"; + +const FAKE_AGENT = null as unknown as AbstractAgent; describe("defaultApplyEvents with tool calls", () => { it("should handle a single tool call correctly", async () => { // Create a subject and state for events const events$ = new Subject(); - const initialState: AgentState = { + const initialState = { messages: [], - state: {}, + state: { + count: 0, + text: "hello", + }, + threadId: "test-thread", + runId: "test-run", + tools: [], + context: [], }; // Create the observable stream - const result$ = defaultApplyEvents(initialState, events$); + const result$ = defaultApplyEvents(initialState, events$, FAKE_AGENT, []); // Collect all emitted state updates in an array const stateUpdatesPromise = firstValueFrom(result$.pipe(toArray())); @@ -70,36 +81,46 @@ describe("defaultApplyEvents with tool calls", () => { expect(stateUpdates.length).toBe(4); // First update: tool call created - expect(stateUpdates[0].messages.length).toBe(1); - expect(stateUpdates[0].messages[0]?.toolCalls?.length).toBe(1); - expect(stateUpdates[0].messages[0]?.toolCalls?.[0]?.id).toBe("tool1"); - expect(stateUpdates[0].messages[0]?.toolCalls?.[0]?.function?.name).toBe("search"); - expect(stateUpdates[0].messages[0]?.toolCalls?.[0]?.function?.arguments).toBe(""); + expect(stateUpdates[0].messages?.length).toBe(1); + expect((stateUpdates[0].messages?.[0] as AssistantMessage).toolCalls?.length).toBe(1); + expect((stateUpdates[0].messages?.[0] as AssistantMessage).toolCalls?.[0]?.id).toBe("tool1"); + expect((stateUpdates[0].messages?.[0] as AssistantMessage).toolCalls?.[0]?.function?.name).toBe( + "search", + ); + expect( + (stateUpdates[0].messages?.[0] as AssistantMessage).toolCalls?.[0]?.function?.arguments, + ).toBe(""); // Second update: first args chunk added - expect(stateUpdates[1].messages[0]?.toolCalls?.[0]?.function?.arguments).toBe('{"query": "'); + expect( + (stateUpdates[1].messages?.[0] as AssistantMessage).toolCalls?.[0]?.function?.arguments, + ).toBe('{"query": "'); // Third update: second args chunk appended - expect(stateUpdates[2].messages[0]?.toolCalls?.[0]?.function?.arguments).toBe( - '{"query": "test search', - ); + expect( + (stateUpdates[2].messages?.[0] as AssistantMessage).toolCalls?.[0]?.function?.arguments, + ).toBe('{"query": "test search'); // Fourth update: third args chunk appended - expect(stateUpdates[3].messages[0]?.toolCalls?.[0]?.function?.arguments).toBe( - '{"query": "test search"}', - ); + expect( + (stateUpdates[3].messages?.[0] as AssistantMessage).toolCalls?.[0]?.function?.arguments, + ).toBe('{"query": "test search"}'); }); it("should handle multiple tool calls correctly", async () => { // Create a subject and state for events const events$ = new Subject(); - const initialState: AgentState = { + const initialState: RunAgentInput = { messages: [], state: {}, + threadId: "test-thread", + runId: "test-run", + tools: [], + context: [], }; // Create the observable stream - const result$ = defaultApplyEvents(initialState, events$); + const result$ = defaultApplyEvents(initialState, events$, FAKE_AGENT, []); // Collect all emitted state updates in an array const stateUpdatesPromise = firstValueFrom(result$.pipe(toArray())); @@ -157,19 +178,25 @@ describe("defaultApplyEvents with tool calls", () => { // Check last state update for the correct tool calls const finalState = stateUpdates[stateUpdates.length - 1]; - expect(finalState.messages.length).toBe(2); + expect(finalState.messages?.length).toBe(2); // First message should have first tool call - expect(finalState.messages[0]?.toolCalls?.length).toBe(1); - expect(finalState.messages[0]?.toolCalls?.[0]?.id).toBe("tool1"); - expect(finalState.messages[0]?.toolCalls?.[0]?.function?.name).toBe("search"); - expect(finalState.messages[0]?.toolCalls?.[0]?.function?.arguments).toBe('{"query":"test"}'); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.length).toBe(1); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.[0]?.id).toBe("tool1"); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.[0]?.function?.name).toBe( + "search", + ); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.[0]?.function?.arguments).toBe( + '{"query":"test"}', + ); // Second message should have second tool call - expect(finalState.messages[1]?.toolCalls?.length).toBe(1); - expect(finalState.messages[1]?.toolCalls?.[0]?.id).toBe("tool2"); - expect(finalState.messages[1]?.toolCalls?.[0]?.function?.name).toBe("calculate"); - expect(finalState.messages[1]?.toolCalls?.[0]?.function?.arguments).toBe( + expect((finalState.messages?.[1] as AssistantMessage).toolCalls?.length).toBe(1); + expect((finalState.messages?.[1] as AssistantMessage).toolCalls?.[0]?.id).toBe("tool2"); + expect((finalState.messages?.[1] as AssistantMessage).toolCalls?.[0]?.function?.name).toBe( + "calculate", + ); + expect((finalState.messages?.[1] as AssistantMessage).toolCalls?.[0]?.function?.arguments).toBe( '{"expression":"1+1"}', ); }); @@ -180,7 +207,7 @@ describe("defaultApplyEvents with tool calls", () => { // Create initial state with an existing message const parentMessageId = "existing_message"; - const initialState: AgentState = { + const initialState: RunAgentInput = { messages: [ { id: parentMessageId, @@ -190,10 +217,14 @@ describe("defaultApplyEvents with tool calls", () => { }, ], state: {}, + threadId: "test-thread", + runId: "test-run", + tools: [], + context: [], }; // Create the observable stream - const result$ = defaultApplyEvents(initialState, events$); + const result$ = defaultApplyEvents(initialState, events$, FAKE_AGENT, []); // Collect all emitted state updates in an array const stateUpdatesPromise = firstValueFrom(result$.pipe(toArray())); @@ -230,25 +261,33 @@ describe("defaultApplyEvents with tool calls", () => { // Check that the tool call was added to the existing message const finalState = stateUpdates[stateUpdates.length - 1]; - expect(finalState.messages.length).toBe(1); - expect(finalState.messages[0].id).toBe(parentMessageId); - expect(finalState.messages[0].content).toBe("I'll help you with that."); - expect(finalState.messages[0]?.toolCalls?.length).toBe(1); - expect(finalState.messages[0]?.toolCalls?.[0]?.id).toBe("tool1"); - expect(finalState.messages[0]?.toolCalls?.[0]?.function?.name).toBe("search"); - expect(finalState.messages[0]?.toolCalls?.[0]?.function?.arguments).toBe('{"query":"test"}'); + expect(finalState.messages?.length).toBe(1); + expect(finalState.messages?.[0]?.id).toBe(parentMessageId); + expect(finalState.messages?.[0]?.content).toBe("I'll help you with that."); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.length).toBe(1); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.[0]?.id).toBe("tool1"); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.[0]?.function?.name).toBe( + "search", + ); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.[0]?.function?.arguments).toBe( + '{"query":"test"}', + ); }); it("should handle errors and partial updates correctly", async () => { // Create a subject and state for events const events$ = new Subject(); - const initialState: AgentState = { + const initialState: RunAgentInput = { messages: [], state: {}, + threadId: "test-thread", + runId: "test-run", + tools: [], + context: [], }; // Create the observable stream - const result$ = defaultApplyEvents(initialState, events$); + const result$ = defaultApplyEvents(initialState, events$, FAKE_AGENT, []); // Collect all emitted state updates in an array const stateUpdatesPromise = firstValueFrom(result$.pipe(toArray())); @@ -289,19 +328,25 @@ describe("defaultApplyEvents with tool calls", () => { // Check the final JSON (should be valid now) const finalState = stateUpdates[stateUpdates.length - 1]; - expect(finalState.messages[0]?.toolCalls?.[0]?.function?.arguments).toBe('{"query:"test"}'); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.[0]?.function?.arguments).toBe( + '{"query:"test"}', + ); }); it("should handle advanced scenarios with multiple tools and text messages", async () => { // Create a subject and state for events const events$ = new Subject(); - const initialState: AgentState = { + const initialState: RunAgentInput = { messages: [], state: {}, + threadId: "test-thread", + runId: "test-run", + tools: [], + context: [], }; // Create the observable stream - const result$ = defaultApplyEvents(initialState, events$); + const result$ = defaultApplyEvents(initialState, events$, FAKE_AGENT, []); // Collect all emitted state updates in an array const stateUpdatesPromise = firstValueFrom(result$.pipe(toArray())); @@ -355,16 +400,20 @@ describe("defaultApplyEvents with tool calls", () => { // Check the final state for both tool calls const finalState = stateUpdates[stateUpdates.length - 1]; - expect(finalState.messages.length).toBe(2); + expect(finalState.messages?.length).toBe(2); // Verify first tool call - expect(finalState.messages[0]?.toolCalls?.length).toBe(1); - expect(finalState.messages[0]?.toolCalls?.[0]?.id).toBe("tool1"); - expect(finalState.messages[0]?.toolCalls?.[0]?.function?.name).toBe("search"); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.length).toBe(1); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.[0]?.id).toBe("tool1"); + expect((finalState.messages?.[0] as AssistantMessage).toolCalls?.[0]?.function?.name).toBe( + "search", + ); // Verify second tool call - expect(finalState.messages[1]?.toolCalls?.length).toBe(1); - expect(finalState.messages[1]?.toolCalls?.[0]?.id).toBe("tool2"); - expect(finalState.messages[1]?.toolCalls?.[0]?.function?.name).toBe("calculate"); + expect((finalState.messages?.[1] as AssistantMessage).toolCalls?.length).toBe(1); + expect((finalState.messages?.[1] as AssistantMessage).toolCalls?.[0]?.id).toBe("tool2"); + expect((finalState.messages?.[1] as AssistantMessage).toolCalls?.[0]?.function?.name).toBe( + "calculate", + ); }); }); diff --git a/typescript-sdk/packages/client/src/apply/default.ts b/typescript-sdk/packages/client/src/apply/default.ts index 83e302a42..0e9deb04c 100644 --- a/typescript-sdk/packages/client/src/apply/default.ts +++ b/typescript-sdk/packages/client/src/apply/default.ts @@ -1,5 +1,4 @@ import { - ApplyEvents, EventType, TextMessageStartEvent, TextMessageContentEvent, @@ -14,233 +13,596 @@ import { AssistantMessage, ToolCallResultEvent, ToolMessage, + RunAgentInput, + TextMessageEndEvent, + ToolCallEndEvent, + RawEvent, + RunStartedEvent, + RunFinishedEvent, + RunErrorEvent, + StepStartedEvent, + StepFinishedEvent, } from "@ag-ui/core"; -import { mergeMap } from "rxjs/operators"; +import { mergeMap, mergeAll, defaultIfEmpty, concatMap } from "rxjs/operators"; +import { of, EMPTY } from "rxjs"; import { structuredClone_ } from "../utils"; import { applyPatch } from "fast-json-patch"; -import untruncateJson from "untruncate-json"; -import { AgentState } from "@ag-ui/core"; +import { + AgentStateMutation, + AgentSubscriber, + runSubscribersWithMutation, +} from "@/agent/subscriber"; import { Observable } from "rxjs"; +import { AbstractAgent } from "@/agent/agent"; +import untruncateJson from "untruncate-json"; -interface PredictStateValue { - state_key: string; - tool: string; - tool_argument: string; -} - -export const defaultApplyEvents = (...args: Parameters): ReturnType => { - const [input, events$] = args; - +export const defaultApplyEvents = ( + input: RunAgentInput, + events$: Observable, + agent: AbstractAgent, + subscribers: AgentSubscriber[], +): Observable => { let messages = structuredClone_(input.messages); let state = structuredClone_(input.state); - let predictState: PredictStateValue[] | undefined; - - // Helper function to emit state updates with proper cloning - const emitUpdate = (agentState: AgentState) => [structuredClone_(agentState)]; - - const emitNoUpdate = () => []; + let currentMutation: AgentStateMutation = {}; + + const applyMutation = (mutation: AgentStateMutation) => { + if (mutation.messages !== undefined) { + messages = mutation.messages; + currentMutation.messages = mutation.messages; + } + if (mutation.state !== undefined) { + state = mutation.state; + currentMutation.state = mutation.state; + } + }; + + const emitUpdates = () => { + const result = structuredClone_(currentMutation) as AgentStateMutation; + currentMutation = {}; + if (result.messages !== undefined || result.state !== undefined) { + return of(result); + } + return EMPTY; + }; return events$.pipe( - mergeMap((event) => { + concatMap(async (event) => { + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onEvent?.({ event, agent, input, messages, state }), + ); + applyMutation(mutation); + + if (mutation.stopPropagation === true) { + return emitUpdates(); + } + switch (event.type) { case EventType.TEXT_MESSAGE_START: { - const { messageId, role } = event as TextMessageStartEvent; - - // Create a new message using properties from the event - const newMessage: Message = { - id: messageId, - role: role, - content: "", - }; - - // Add the new message to the messages array - messages.push(newMessage); + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onTextMessageStartEvent?.({ + event: event as TextMessageStartEvent, + messages, + state, + agent, + input, + }), + ); + applyMutation(mutation); + + if (mutation.stopPropagation !== true) { + const { messageId, role } = event as TextMessageStartEvent; + + // Create a new message using properties from the event + const newMessage: Message = { + id: messageId, + role: role, + content: "", + }; - return emitUpdate({ messages }); + // Add the new message to the messages array + messages.push(newMessage); + applyMutation({ messages }); + } + return emitUpdates(); } case EventType.TEXT_MESSAGE_CONTENT: { - const { delta } = event as TextMessageContentEvent; - - // Get the last message and append the content - const lastMessage = messages[messages.length - 1]; - lastMessage.content = lastMessage.content! + delta; + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onTextMessageContentEvent?.({ + event: event as TextMessageContentEvent, + messages, + state, + agent, + input, + textMessageBuffer: messages[messages.length - 1].content ?? "", + }), + ); + applyMutation(mutation); + + if (mutation.stopPropagation !== true) { + const { delta } = event as TextMessageContentEvent; + + // Get the last message and append the content + const lastMessage = messages[messages.length - 1]; + lastMessage.content = lastMessage.content! + delta; + applyMutation({ messages }); + } - return emitUpdate({ messages }); + return emitUpdates(); } case EventType.TEXT_MESSAGE_END: { - return emitNoUpdate(); + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onTextMessageEndEvent?.({ + event: event as TextMessageEndEvent, + messages, + state, + agent, + input, + textMessageBuffer: messages[messages.length - 1].content ?? "", + }), + ); + applyMutation(mutation); + + await Promise.all( + subscribers.map((subscriber) => { + subscriber.onNewMessage?.({ + message: messages[messages.length - 1], + messages, + state, + agent, + input, + }); + }), + ); + + return emitUpdates(); } case EventType.TOOL_CALL_START: { - const { toolCallId, toolCallName, parentMessageId } = event as ToolCallStartEvent; - - let targetMessage: AssistantMessage; - - // Use last message if parentMessageId exists, we have messages, and the parentMessageId matches the last message's id - if ( - parentMessageId && - messages.length > 0 && - messages[messages.length - 1].id === parentMessageId - ) { - targetMessage = messages[messages.length - 1]; - } else { - // Create a new message otherwise - targetMessage = { - id: parentMessageId || toolCallId, - role: "assistant", - toolCalls: [], - }; - messages.push(targetMessage); - } + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onToolCallStartEvent?.({ + event: event as ToolCallStartEvent, + messages, + state, + agent, + input, + }), + ); + applyMutation(mutation); + + if (mutation.stopPropagation !== true) { + const { toolCallId, toolCallName, parentMessageId } = event as ToolCallStartEvent; + + let targetMessage: AssistantMessage; + + // Use last message if parentMessageId exists, we have messages, and the parentMessageId matches the last message's id + if ( + parentMessageId && + messages.length > 0 && + messages[messages.length - 1].id === parentMessageId + ) { + targetMessage = messages[messages.length - 1] as AssistantMessage; + } else { + // Create a new message otherwise + targetMessage = { + id: parentMessageId || toolCallId, + role: "assistant", + toolCalls: [], + }; + messages.push(targetMessage); + } - targetMessage.toolCalls ??= []; + targetMessage.toolCalls ??= []; - // Add the new tool call - targetMessage.toolCalls.push({ - id: toolCallId, - type: "function", - function: { - name: toolCallName, - arguments: "", - }, - }); + // Add the new tool call + targetMessage.toolCalls.push({ + id: toolCallId, + type: "function", + function: { + name: toolCallName, + arguments: "", + }, + }); + + applyMutation({ messages }); + } - return emitUpdate({ messages }); + return emitUpdates(); } case EventType.TOOL_CALL_ARGS: { - const { delta } = event as ToolCallArgsEvent; + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => { + const toolCalls = + (messages[messages.length - 1] as AssistantMessage)?.toolCalls ?? []; + const toolCallBuffer = + toolCalls.length > 0 ? toolCalls[toolCalls.length - 1].function.arguments : ""; + const toolCallName = + toolCalls.length > 0 ? toolCalls[toolCalls.length - 1].function.name : ""; + let partialToolCallArgs = {}; + try { + // Parse from toolCallBuffer only (before current delta is applied) + partialToolCallArgs = untruncateJson(toolCallBuffer); + } catch (error) {} + + return subscriber.onToolCallArgsEvent?.({ + event: event as ToolCallArgsEvent, + messages, + state, + agent, + input, + toolCallBuffer, + toolCallName, + partialToolCallArgs, + }); + }, + ); + applyMutation(mutation); - // Get the last message - const lastMessage = messages[messages.length - 1]; + if (mutation.stopPropagation !== true) { + const { delta } = event as ToolCallArgsEvent; - // Get the last tool call - const lastToolCall = lastMessage.toolCalls[lastMessage.toolCalls.length - 1]; + // Get the last message + const lastMessage = messages[messages.length - 1] as AssistantMessage; - // Append the arguments - lastToolCall.function.arguments += delta; + // Get the last tool call + const lastToolCall = lastMessage.toolCalls![lastMessage.toolCalls!.length - 1]; - if (predictState) { - const config = predictState.find((p) => p.tool === lastToolCall.function.name); - if (config) { - try { - const lastToolCallArguments = JSON.parse( - untruncateJson(lastToolCall.function.arguments), - ); - if (config.tool_argument && config.tool_argument in lastToolCallArguments) { - state = { - ...state, - [config.state_key]: lastToolCallArguments[config.tool_argument], - }; - return emitUpdate({ messages, state }); - } else { - state = { - ...state, - [config.state_key]: lastToolCallArguments, - }; - return emitUpdate({ messages, state }); - } - } catch (_) {} - } + // Append the arguments + lastToolCall.function.arguments += delta; + + applyMutation({ messages }); } - return emitUpdate({ messages }); + return emitUpdates(); } case EventType.TOOL_CALL_END: { - return emitNoUpdate(); + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => { + const toolCalls = + (messages[messages.length - 1] as AssistantMessage)?.toolCalls ?? []; + const toolCallArgsString = + toolCalls.length > 0 ? toolCalls[toolCalls.length - 1].function.arguments : ""; + const toolCallName = + toolCalls.length > 0 ? toolCalls[toolCalls.length - 1].function.name : ""; + let toolCallArgs = {}; + try { + toolCallArgs = JSON.parse(toolCallArgsString); + } catch (error) {} + return subscriber.onToolCallEndEvent?.({ + event: event as ToolCallEndEvent, + messages, + state, + agent, + input, + toolCallName, + toolCallArgs, + }); + }, + ); + applyMutation(mutation); + + await Promise.all( + subscribers.map((subscriber) => { + subscriber.onNewToolCall?.({ + toolCall: (messages[messages.length - 1] as AssistantMessage).toolCalls![ + (messages[messages.length - 1] as AssistantMessage).toolCalls!.length - 1 + ], + messages, + state, + agent, + input, + }); + }), + ); + + return emitUpdates(); } case EventType.TOOL_CALL_RESULT: { - const { messageId, toolCallId, content, role } = event as ToolCallResultEvent; + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onToolCallResultEvent?.({ + event: event as ToolCallResultEvent, + messages, + state, + agent, + input, + }), + ); + + applyMutation(mutation); + + if (mutation.stopPropagation !== true) { + const { messageId, toolCallId, content, role } = event as ToolCallResultEvent; + + const toolMessage: ToolMessage = { + id: messageId, + toolCallId, + role: role || "tool", + content: content, + }; - const toolMessage: ToolMessage = { - id: messageId, - toolCallId, - role: role || "tool", - content: content, - }; + messages.push(toolMessage); + + await Promise.all( + subscribers.map((subscriber) => { + subscriber.onNewMessage?.({ + message: toolMessage, + messages, + state, + agent, + input, + }); + }), + ); - messages.push(toolMessage); + applyMutation({ messages }); + } - return emitNoUpdate(); + return emitUpdates(); } case EventType.STATE_SNAPSHOT: { - const { snapshot } = event as StateSnapshotEvent; - - // Replace state with the literal snapshot - state = snapshot; + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onStateSnapshotEvent?.({ + event: event as StateSnapshotEvent, + messages, + state, + agent, + input, + }), + ); + applyMutation(mutation); + + if (mutation.stopPropagation !== true) { + const { snapshot } = event as StateSnapshotEvent; + + // Replace state with the literal snapshot + state = snapshot; + + applyMutation({ state }); + } - return emitUpdate({ state }); + return emitUpdates(); } case EventType.STATE_DELTA: { - const { delta } = event as StateDeltaEvent; - - try { - // Apply the JSON Patch operations to the current state without mutating the original - const result = applyPatch(state, delta, true, false); - state = result.newDocument; - return emitUpdate({ state }); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.warn( - `Failed to apply state patch:\n` + - `Current state: ${JSON.stringify(state, null, 2)}\n` + - `Patch operations: ${JSON.stringify(delta, null, 2)}\n` + - `Error: ${errorMessage}`, - ); - return emitNoUpdate(); + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onStateDeltaEvent?.({ + event: event as StateDeltaEvent, + messages, + state, + agent, + input, + }), + ); + applyMutation(mutation); + + if (mutation.stopPropagation !== true) { + const { delta } = event as StateDeltaEvent; + + try { + // Apply the JSON Patch operations to the current state without mutating the original + const result = applyPatch(state, delta, true, false); + state = result.newDocument; + applyMutation({ state }); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.warn( + `Failed to apply state patch:\n` + + `Current state: ${JSON.stringify(state, null, 2)}\n` + + `Patch operations: ${JSON.stringify(delta, null, 2)}\n` + + `Error: ${errorMessage}`, + ); + // If patch failed, only emit updates if there were subscriber mutations + // This prevents emitting updates when both patch fails AND no subscriber mutations + } } + + return emitUpdates(); } case EventType.MESSAGES_SNAPSHOT: { - const { messages: newMessages } = event as MessagesSnapshotEvent; - - // Replace messages with the snapshot - messages = newMessages; + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onMessagesSnapshotEvent?.({ + event: event as MessagesSnapshotEvent, + messages, + state, + agent, + input, + }), + ); + applyMutation(mutation); + + if (mutation.stopPropagation !== true) { + const { messages: newMessages } = event as MessagesSnapshotEvent; + + // Replace messages with the snapshot + messages = newMessages; + + applyMutation({ messages }); + } - return emitUpdate({ messages }); + return emitUpdates(); } case EventType.RAW: { - return emitNoUpdate(); + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onRawEvent?.({ + event: event as RawEvent, + messages, + state, + agent, + input, + }), + ); + applyMutation(mutation); + + return emitUpdates(); } case EventType.CUSTOM: { - const customEvent = event as CustomEvent; - - if (customEvent.name === "PredictState") { - predictState = customEvent.value as PredictStateValue[]; - return emitNoUpdate(); - } - - return emitNoUpdate(); + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onCustomEvent?.({ + event: event as CustomEvent, + messages, + state, + agent, + input, + }), + ); + applyMutation(mutation); + + return emitUpdates(); } case EventType.RUN_STARTED: { - return emitNoUpdate(); + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onRunStartedEvent?.({ + event: event as RunStartedEvent, + messages, + state, + agent, + input, + }), + ); + applyMutation(mutation); + + return emitUpdates(); } case EventType.RUN_FINISHED: { - return emitNoUpdate(); + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onRunFinishedEvent?.({ + event: event as RunFinishedEvent, + messages, + state, + agent, + input, + result: (event as RunFinishedEvent).result, + }), + ); + applyMutation(mutation); + + return emitUpdates(); } case EventType.RUN_ERROR: { - return emitNoUpdate(); + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onRunErrorEvent?.({ + event: event as RunErrorEvent, + messages, + state, + agent, + input, + }), + ); + applyMutation(mutation); + + return emitUpdates(); } case EventType.STEP_STARTED: { - return emitNoUpdate(); + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onStepStartedEvent?.({ + event: event as StepStartedEvent, + messages, + state, + agent, + input, + }), + ); + applyMutation(mutation); + + return emitUpdates(); } case EventType.STEP_FINISHED: { - // reset predictive state after step is finished - predictState = undefined; - return emitNoUpdate(); + const mutation = await runSubscribersWithMutation( + subscribers, + messages, + state, + (subscriber, messages, state) => + subscriber.onStepFinishedEvent?.({ + event: event as StepFinishedEvent, + messages, + state, + agent, + input, + }), + ); + applyMutation(mutation); + + return emitUpdates(); } case EventType.TEXT_MESSAGE_CHUNK: { @@ -252,30 +614,34 @@ export const defaultApplyEvents = (...args: Parameters): ReturnType } case EventType.THINKING_START: { - return emitNoUpdate(); + return emitUpdates(); } case EventType.THINKING_END: { - return emitNoUpdate(); + return emitUpdates(); } case EventType.THINKING_TEXT_MESSAGE_START: { - return emitNoUpdate(); + return emitUpdates(); } case EventType.THINKING_TEXT_MESSAGE_CONTENT: { - return emitNoUpdate(); + return emitUpdates(); } case EventType.THINKING_TEXT_MESSAGE_END: { - return emitNoUpdate(); + return emitUpdates(); } } // This makes TypeScript check that the switch is exhaustive // If a new EventType is added, this will cause a compile error const _exhaustiveCheck: never = event.type; - return emitNoUpdate(); + return emitUpdates(); }), + mergeAll(), + // Only use defaultIfEmpty when there are subscribers to avoid emitting empty updates + // when patches fail and there are no subscribers (like in state patching test) + subscribers.length > 0 ? defaultIfEmpty({} as AgentStateMutation) : (stream: any) => stream, ); }; diff --git a/typescript-sdk/packages/client/src/utils.ts b/typescript-sdk/packages/client/src/utils.ts index 675e9f706..f129f3493 100644 --- a/typescript-sdk/packages/client/src/utils.ts +++ b/typescript-sdk/packages/client/src/utils.ts @@ -1,4 +1,4 @@ -export const structuredClone_ = (obj: any) => { +export const structuredClone_ = (obj: T): T => { if (typeof structuredClone === "function") { return structuredClone(obj); } @@ -6,6 +6,6 @@ export const structuredClone_ = (obj: any) => { try { return JSON.parse(JSON.stringify(obj)); } catch (err) { - return { ...obj }; + return { ...obj } as T; } }; diff --git a/typescript-sdk/packages/client/tsconfig.json b/typescript-sdk/packages/client/tsconfig.json index d12ec063d..9ff3dde94 100644 --- a/typescript-sdk/packages/client/tsconfig.json +++ b/typescript-sdk/packages/client/tsconfig.json @@ -19,6 +19,6 @@ }, "stripInternal": true }, - "include": ["src"], + "include": ["src", "../core/src/subscriber.ts"], "exclude": ["node_modules", "dist"] } diff --git a/typescript-sdk/packages/core/src/events.ts b/typescript-sdk/packages/core/src/events.ts index b8d9ff26d..e64c62518 100644 --- a/typescript-sdk/packages/core/src/events.ts +++ b/typescript-sdk/packages/core/src/events.ts @@ -34,34 +34,6 @@ const BaseEventSchema = z.object({ rawEvent: z.any().optional(), }); -export const RunStartedSchema = BaseEventSchema.extend({ - type: z.literal(EventType.RUN_STARTED), - threadId: z.string(), - runId: z.string(), -}); - -export const RunFinishedSchema = BaseEventSchema.extend({ - type: z.literal(EventType.RUN_FINISHED), - threadId: z.string(), - runId: z.string(), -}); - -export const RunErrorSchema = BaseEventSchema.extend({ - type: z.literal(EventType.RUN_ERROR), - message: z.string(), - code: z.string().optional(), -}); - -export const StepStartedSchema = BaseEventSchema.extend({ - type: z.literal(EventType.STEP_STARTED), - stepName: z.string(), -}); - -export const StepFinishedSchema = BaseEventSchema.extend({ - type: z.literal(EventType.STEP_FINISHED), - stepName: z.string(), -}); - export const TextMessageStartEventSchema = BaseEventSchema.extend({ type: z.literal(EventType.TEXT_MESSAGE_START), messageId: z.string(), @@ -181,6 +153,7 @@ export const RunFinishedEventSchema = BaseEventSchema.extend({ type: z.literal(EventType.RUN_FINISHED), threadId: z.string(), runId: z.string(), + result: z.any().optional(), }); export const RunErrorEventSchema = BaseEventSchema.extend({ diff --git a/typescript-sdk/packages/core/src/index.ts b/typescript-sdk/packages/core/src/index.ts index 8cd919676..9d40a8f6e 100644 --- a/typescript-sdk/packages/core/src/index.ts +++ b/typescript-sdk/packages/core/src/index.ts @@ -3,6 +3,3 @@ export * from "./types"; // Export all event-related types and schemas export * from "./events"; - -// Export all stream-related types and schemas -export * from "./stream"; diff --git a/typescript-sdk/packages/core/src/stream.ts b/typescript-sdk/packages/core/src/stream.ts deleted file mode 100644 index 0f4071e24..000000000 --- a/typescript-sdk/packages/core/src/stream.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Observable } from "rxjs"; -import { Message, State, RunAgentInput } from "./types"; -import { BaseEvent } from "./events"; - -/** - * Function type for agent runners that process input and return a stream of results. - */ -export type RunAgent = (input: RunAgentInput) => Observable; - -/** - * The transformed state of an agent. - */ -export interface AgentState { - messages?: Message[]; - state?: State; -} - -/** - * Maps a stream of BaseEvent objects to a stream of AgentState objects. - * @returns A function that transforms an Observable into an Observable - */ -export type ApplyEvents = ( - input: RunAgentInput, - events$: Observable, -) => Observable; diff --git a/typescript-sdk/packages/proto/src/proto/events.proto b/typescript-sdk/packages/proto/src/proto/events.proto index de35e0a1c..7b360726b 100644 --- a/typescript-sdk/packages/proto/src/proto/events.proto +++ b/typescript-sdk/packages/proto/src/proto/events.proto @@ -103,6 +103,7 @@ message RunFinishedEvent { BaseEvent base_event = 1; string thread_id = 2; string run_id = 3; + optional google.protobuf.Value result = 4; } message RunErrorEvent { diff --git a/typescript-sdk/pnpm-lock.yaml b/typescript-sdk/pnpm-lock.yaml index fe01cac6a..53bba0293 100644 --- a/typescript-sdk/pnpm-lock.yaml +++ b/typescript-sdk/pnpm-lock.yaml @@ -18,6 +18,52 @@ importers: specifier: 5.8.2 version: 5.8.2 + apps/client-cli: + dependencies: + '@ag-ui/client': + specifier: workspace:* + version: link:../../packages/client + '@ag-ui/core': + specifier: workspace:* + version: link:../../packages/core + '@ag-ui/mastra': + specifier: workspace:* + version: link:../../integrations/mastra + '@ai-sdk/openai': + specifier: ^1.3.22 + version: 1.3.22(zod@3.25.71) + '@mastra/client-js': + specifier: ^0.10.9 + version: 0.10.9(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71) + '@mastra/core': + specifier: ^0.10.10 + version: 0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71) + '@mastra/libsql': + specifier: ^0.11.0 + version: 0.11.0(@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71)) + '@mastra/loggers': + specifier: ^0.10.3 + version: 0.10.3(@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71)) + '@mastra/memory': + specifier: ^0.11.1 + version: 0.11.1(@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71))(react@19.1.0) + open: + specifier: ^10.1.2 + version: 10.1.2 + zod: + specifier: ^3.22.4 + version: 3.25.71 + devDependencies: + '@types/node': + specifier: ^20 + version: 20.19.4 + tsx: + specifier: ^4.7.0 + version: 4.20.3 + typescript: + specifier: ^5 + version: 5.8.2 + apps/dojo: dependencies: '@ag-ui/agno': @@ -258,7 +304,7 @@ importers: version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.2 @@ -286,7 +332,7 @@ importers: version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.2 @@ -298,10 +344,10 @@ importers: version: link:../../packages/client '@langchain/core': specifier: ^0.3.38 - version: 0.3.56(openai@4.104.0(ws@8.18.3)(zod@3.25.67)) + version: 0.3.56(openai@4.104.0(ws@8.18.3)(zod@3.25.71)) '@langchain/langgraph-sdk': specifier: ^0.0.78 - version: 0.0.78(@langchain/core@0.3.56(openai@4.104.0(ws@8.18.3)(zod@3.25.67)))(react@19.1.0) + version: 0.0.78(@langchain/core@0.3.56(openai@4.104.0(ws@8.18.3)(zod@3.25.71)))(react@19.1.0) partial-json: specifier: ^0.1.7 version: 0.1.7 @@ -323,7 +369,7 @@ importers: version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.2 @@ -351,7 +397,7 @@ importers: version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.2 @@ -366,7 +412,7 @@ importers: version: 1.2.11(zod@3.25.67) '@copilotkit/runtime': specifier: ^1.8.13 - version: 1.8.13(@ag-ui/client@packages+client)(@ag-ui/core@0.0.30)(@ag-ui/encoder@0.0.30)(@ag-ui/proto@0.0.30)(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/credential-provider-node@3.840.0)(@browserbasehq/sdk@2.6.0)(@browserbasehq/stagehand@2.4.0(deepmerge@4.3.1)(dotenv@17.0.1)(react@19.1.0)(zod@3.25.67))(@ibm-cloud/watsonx-ai@1.6.8)(@libsql/client@0.15.9)(@smithy/eventstream-codec@4.0.4)(@smithy/protocol-http@5.1.2)(@smithy/signature-v4@5.1.2)(@smithy/util-utf8@4.0.0)(@upstash/redis@1.35.1)(axios@1.10.0)(cohere-ai@7.17.1)(fast-xml-parser@5.2.5)(google-auth-library@10.1.0)(ibm-cloud-sdk-core@5.4.0)(ignore@7.0.5)(jsonwebtoken@9.0.2)(lodash@4.17.21)(pg@8.16.3)(playwright@1.53.2)(react@19.1.0)(redis@5.5.6)(ws@8.18.3) + version: 1.8.13(@ag-ui/client@packages+client)(@ag-ui/core@0.0.31)(@ag-ui/encoder@0.0.31)(@ag-ui/proto@0.0.31)(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/credential-provider-node@3.840.0)(@browserbasehq/sdk@2.6.0)(@browserbasehq/stagehand@2.4.0(deepmerge@4.3.1)(dotenv@17.0.1)(react@19.1.0)(zod@3.25.67))(@ibm-cloud/watsonx-ai@1.6.8)(@libsql/client@0.15.9)(@smithy/eventstream-codec@4.0.4)(@smithy/protocol-http@5.1.2)(@smithy/signature-v4@5.1.2)(@smithy/util-utf8@4.0.0)(@upstash/redis@1.35.1)(axios@1.10.0)(cohere-ai@7.17.1)(fast-xml-parser@5.2.5)(google-auth-library@10.1.0)(ibm-cloud-sdk-core@5.4.0)(ignore@7.0.5)(jsonwebtoken@9.0.2)(lodash@4.17.21)(pg@8.16.3)(playwright@1.53.2)(react@19.1.0)(redis@5.5.6)(ws@8.18.3) '@mastra/client-js': specifier: ^0.10.9 version: 0.10.9(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.67) @@ -394,7 +440,7 @@ importers: version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.2 @@ -422,7 +468,7 @@ importers: version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.2 @@ -450,7 +496,7 @@ importers: version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.2 @@ -478,7 +524,7 @@ importers: version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.2 @@ -512,7 +558,7 @@ importers: version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.2 @@ -543,7 +589,7 @@ importers: version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.2 @@ -592,7 +638,7 @@ importers: version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.3.3 version: 5.8.2 @@ -611,13 +657,13 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.17.50) + version: 29.7.0(@types/node@20.19.4) ts-jest: specifier: ^29.1.2 - version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) + version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.19.4))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.8.2 version: 5.8.2 @@ -636,13 +682,13 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.17.50) + version: 29.7.0(@types/node@20.19.4) ts-jest: specifier: ^29.1.2 - version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) + version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.19.4))(typescript@5.8.2) tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.8.2 version: 5.8.2 @@ -664,16 +710,16 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.17.50) + version: 29.7.0(@types/node@20.19.4) ts-jest: specifier: ^29.1.2 - version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.17.50))(typescript@5.8.2) + version: 29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.19.4))(typescript@5.8.2) ts-proto: specifier: ^2.7.0 version: 2.7.0 tsup: specifier: ^8.0.2 - version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0) + version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0) typescript: specifier: ^5.8.2 version: 5.8.2 @@ -700,18 +746,27 @@ packages: '@ag-ui/core@0.0.30': resolution: {integrity: sha512-cBukbc2O0qMKi/BKix6Exld5zSqGKR72376KA6NZNQz/xYAiPNhmK40VX77d/hyblhtXT3BlBGrYmda9V4ETlw==} + '@ag-ui/core@0.0.31': + resolution: {integrity: sha512-Bx3/cq/so0DsyWLsu3FrJETXOuUkyy3xOzGZvzQxvIAKtPBB7cUEw7DnWvbvcoBaaTNCQXERYS75OuYGUoAeOA==} + '@ag-ui/encoder@0.0.27': resolution: {integrity: sha512-GO42BDdi9pmNsfhPlMQeSxGFfMJJg/Jvgng/N/5elHEfEOjGVtOCkDPpN4lirkuBoXEh/hW6gIgYAXDu/HuZJA==} '@ag-ui/encoder@0.0.30': resolution: {integrity: sha512-xk43F5WaEpaRg5vY0y6U/ZMAzScieSA1L0TAtVGysh91M9JS9hxuxTK2jyxh/sC3AySIjbZUQ9m69fECKloT0g==} + '@ag-ui/encoder@0.0.31': + resolution: {integrity: sha512-29mA/PA52vs57c75vtLP21zLZlxbd8yOG4QKmiTYuEqU/xr1NA4aXBgwzeIEAqX98DV/5WwJS6kVYUg+CuzyCQ==} + '@ag-ui/proto@0.0.27': resolution: {integrity: sha512-bgF2DGqU+DvcNKF3gOlT97kZmhHNB0lWfjkJQ6ONxMtmWlSVYAE97LCtdTIjXEhnHyqi3QQBQ0BEXJ74q7QMcg==} '@ag-ui/proto@0.0.30': resolution: {integrity: sha512-5yObohnpAhuzkIrcbgBuT7xrXLThuhsBl+vh85uNeUlb6CNJ7W2rdwApJGTj/3HbitK4iLq2BiY3U18Bno+qqg==} + '@ag-ui/proto@0.0.31': + resolution: {integrity: sha512-r5p92oPusQqsKgbhaFW5oRpbcr5yY6djaI+K96Y0lJppkjj4O95Zn5fRBNXKorZ9yUBDwCpCitw0qbbHFhHn6g==} + '@ai-sdk/anthropic@1.2.12': resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==} engines: {node: '>=18'} @@ -3482,24 +3537,12 @@ packages: resolution: {integrity: sha512-tJwgE6jt32bLs/9J6jhQRKU2EZnsD8qaO13aoFyXwF6s4LhpT7YFHf3Z03MqdILk6BA2BFUhoyh7k9fj9i032A==} engines: {node: ^18.19.0 || >=20.6.0} - '@opentelemetry/resource-detector-alibaba-cloud@0.31.1': - resolution: {integrity: sha512-RPitvB5oHZsECnK7xtUAFdyBXRdtJbY0eEzQPBrLMQv4l/FN4pETijqv6LcKBbn6tevaoBU2bqOGnVoL4uX4Tg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - '@opentelemetry/resource-detector-alibaba-cloud@0.31.2': resolution: {integrity: sha512-Itp6duMXkAIQzmDHIf1kc6Llj/fa0BxilaELp0K6Fp9y+b0ex9LksNAQfTDFPHNine7tFoXauvvHbJFXIB6mqw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 - '@opentelemetry/resource-detector-aws@2.1.0': - resolution: {integrity: sha512-7QG5wQXMiHseKIyU69m8vfZgLhrxFx48DdyaQEYj6GXjE/Xrv1nS3bUwhICjb6+4NorB9+1pFCvJ/4S01CCCjQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - '@opentelemetry/resource-detector-aws@2.2.0': resolution: {integrity: sha512-6k7//RWAv4U1PeZhv0Too0Sv7sp7/A6s6g9h5ZYauPcroh2t4gOmkQSspSLYCynn34YZwn3FGbuaMwTDjHEJig==} engines: {node: ^18.19.0 || >=20.6.0} @@ -3518,12 +3561,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.0.0 - '@opentelemetry/resource-detector-container@0.7.1': - resolution: {integrity: sha512-I2vXgdA8mhIlAktIp7NovicalqKPaas9APH5wQxIzMK6jPjZmwS5x0MBW+sTsaFM4pnOf/Md9enoDnnR5CLq5A==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - '@opentelemetry/resource-detector-container@0.7.2': resolution: {integrity: sha512-St3Krrbpvq7k0UoUNlm7Z4Xqf9HdS9R5yPslwl/WPaZpj/Bf/54WZTPmNQat+93Ey6PTX0ISKg26DfcjPemUhg==} engines: {node: ^18.19.0 || >=20.6.0} @@ -4938,9 +4975,6 @@ packages: '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} - '@types/node@18.19.103': - resolution: {integrity: sha512-hHTHp+sEz6SxFsp+SA+Tqrua3AbmlAw+Y//aEwdHrdZkYVRWdvWD3y5uPZ0flYOkgskaFWqZ/YGFm3FaFQ0pRw==} - '@types/node@18.19.115': resolution: {integrity: sha512-kNrFiTgG4a9JAn1LMQeLOv3MvXIPokzXziohMrMsvpYgLpdEt/mMiVYc4sGKtDfyxM5gIDF4VgrPRyCw4fHOYg==} @@ -9711,6 +9745,11 @@ packages: typescript: optional: true + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} + engines: {node: '>=18.0.0'} + hasBin: true + turbo-darwin-64@2.5.3: resolution: {integrity: sha512-YSItEVBUIvAGPUDpAB9etEmSqZI3T6BHrkBkeSErvICXn3dfqXUfeLx35LfptLDEbrzFUdwYFNmt8QXOwe9yaw==} cpu: [x64] @@ -10214,7 +10253,7 @@ snapshots: rxjs: 7.8.1 untruncate-json: 0.0.1 uuid: 11.1.0 - zod: 3.25.67 + zod: 3.25.71 '@ag-ui/client@0.0.30': dependencies: @@ -10231,13 +10270,18 @@ snapshots: '@ag-ui/core@0.0.27': dependencies: rxjs: 7.8.1 - zod: 3.25.67 + zod: 3.25.71 '@ag-ui/core@0.0.30': dependencies: rxjs: 7.8.1 zod: 3.25.71 + '@ag-ui/core@0.0.31': + dependencies: + rxjs: 7.8.1 + zod: 3.25.71 + '@ag-ui/encoder@0.0.27': dependencies: '@ag-ui/core': 0.0.27 @@ -10248,6 +10292,11 @@ snapshots: '@ag-ui/core': 0.0.30 '@ag-ui/proto': 0.0.30 + '@ag-ui/encoder@0.0.31': + dependencies: + '@ag-ui/core': 0.0.31 + '@ag-ui/proto': 0.0.31 + '@ag-ui/proto@0.0.27': dependencies: '@ag-ui/core': 0.0.27 @@ -10258,6 +10307,11 @@ snapshots: '@ag-ui/core': 0.0.30 '@bufbuild/protobuf': 2.6.0 + '@ag-ui/proto@0.0.31': + dependencies: + '@ag-ui/core': 0.0.31 + '@bufbuild/protobuf': 2.6.0 + '@ai-sdk/anthropic@1.2.12(zod@3.25.67)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -10323,6 +10377,12 @@ snapshots: '@ai-sdk/provider-utils': 2.2.8(zod@3.25.67) zod: 3.25.67 + '@ai-sdk/openai@1.3.22(zod@3.25.71)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.71) + zod: 3.25.71 + '@ai-sdk/perplexity@1.1.9(zod@3.25.67)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -10344,6 +10404,13 @@ snapshots: secure-json-parse: 2.7.0 zod: 3.25.67 + '@ai-sdk/provider-utils@2.2.8(zod@3.25.71)': + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.11 + secure-json-parse: 2.7.0 + zod: 3.25.71 + '@ai-sdk/provider@1.1.3': dependencies: json-schema: 0.4.0 @@ -10368,6 +10435,16 @@ snapshots: optionalDependencies: zod: 3.25.67 + '@ai-sdk/react@1.2.12(react@19.1.0)(zod@3.25.71)': + dependencies: + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.71) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.71) + react: 19.1.0 + swr: 2.3.3(react@19.1.0) + throttleit: 2.1.0 + optionalDependencies: + zod: 3.25.71 + '@ai-sdk/togetherai@0.2.14(zod@3.25.67)': dependencies: '@ai-sdk/openai-compatible': 0.2.14(zod@3.25.67) @@ -10381,14 +10458,21 @@ snapshots: '@ai-sdk/provider': 1.1.3 '@ai-sdk/provider-utils': 2.2.8(zod@3.25.17) zod: 3.25.17 - zod-to-json-schema: 3.24.5(zod@3.25.17) + zod-to-json-schema: 3.24.6(zod@3.25.17) '@ai-sdk/ui-utils@1.2.11(zod@3.25.67)': dependencies: '@ai-sdk/provider': 1.1.3 '@ai-sdk/provider-utils': 2.2.8(zod@3.25.67) zod: 3.25.67 - zod-to-json-schema: 3.24.5(zod@3.25.67) + zod-to-json-schema: 3.24.6(zod@3.25.67) + + '@ai-sdk/ui-utils@1.2.11(zod@3.25.71)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.71) + zod: 3.25.71 + zod-to-json-schema: 3.24.6(zod@3.25.71) '@ai-sdk/xai@1.2.16(zod@3.25.67)': dependencies: @@ -10407,7 +10491,7 @@ snapshots: '@anthropic-ai/sdk@0.27.3': dependencies: - '@types/node': 18.19.103 + '@types/node': 18.19.115 '@types/node-fetch': 2.6.12 abort-controller: 3.0.0 agentkeepalive: 4.6.0 @@ -11271,20 +11355,20 @@ snapshots: - encoding - graphql - '@copilotkit/runtime@1.8.13(@ag-ui/client@packages+client)(@ag-ui/core@0.0.30)(@ag-ui/encoder@0.0.30)(@ag-ui/proto@0.0.30)(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/credential-provider-node@3.840.0)(@browserbasehq/sdk@2.6.0)(@browserbasehq/stagehand@2.4.0(deepmerge@4.3.1)(dotenv@17.0.1)(react@19.1.0)(zod@3.25.67))(@ibm-cloud/watsonx-ai@1.6.8)(@libsql/client@0.15.9)(@smithy/eventstream-codec@4.0.4)(@smithy/protocol-http@5.1.2)(@smithy/signature-v4@5.1.2)(@smithy/util-utf8@4.0.0)(@upstash/redis@1.35.1)(axios@1.10.0)(cohere-ai@7.17.1)(fast-xml-parser@5.2.5)(google-auth-library@10.1.0)(ibm-cloud-sdk-core@5.4.0)(ignore@7.0.5)(jsonwebtoken@9.0.2)(lodash@4.17.21)(pg@8.16.3)(playwright@1.53.2)(react@19.1.0)(redis@5.5.6)(ws@8.18.3)': + '@copilotkit/runtime@1.8.13(@ag-ui/client@packages+client)(@ag-ui/core@0.0.31)(@ag-ui/encoder@0.0.31)(@ag-ui/proto@0.0.31)(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/credential-provider-node@3.840.0)(@browserbasehq/sdk@2.6.0)(@browserbasehq/stagehand@2.4.0(deepmerge@4.3.1)(dotenv@17.0.1)(react@19.1.0)(zod@3.25.67))(@ibm-cloud/watsonx-ai@1.6.8)(@libsql/client@0.15.9)(@smithy/eventstream-codec@4.0.4)(@smithy/protocol-http@5.1.2)(@smithy/signature-v4@5.1.2)(@smithy/util-utf8@4.0.0)(@upstash/redis@1.35.1)(axios@1.10.0)(cohere-ai@7.17.1)(fast-xml-parser@5.2.5)(google-auth-library@10.1.0)(ibm-cloud-sdk-core@5.4.0)(ignore@7.0.5)(jsonwebtoken@9.0.2)(lodash@4.17.21)(pg@8.16.3)(playwright@1.53.2)(react@19.1.0)(redis@5.5.6)(ws@8.18.3)': dependencies: '@ag-ui/client': link:packages/client - '@ag-ui/core': 0.0.30 - '@ag-ui/encoder': 0.0.30 - '@ag-ui/proto': 0.0.30 + '@ag-ui/core': 0.0.31 + '@ag-ui/encoder': 0.0.31 + '@ag-ui/proto': 0.0.31 '@anthropic-ai/sdk': 0.27.3 '@copilotkit/shared': 1.8.13 '@graphql-yoga/plugin-defer-stream': 3.13.4(graphql-yoga@5.13.4(graphql@16.11.0))(graphql@16.11.0) - '@langchain/community': 0.3.43(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/credential-provider-node@3.840.0)(@browserbasehq/sdk@2.6.0)(@browserbasehq/stagehand@2.4.0(deepmerge@4.3.1)(dotenv@17.0.1)(react@19.1.0)(zod@3.25.67))(@ibm-cloud/watsonx-ai@1.6.8)(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(@libsql/client@0.15.9)(@smithy/eventstream-codec@4.0.4)(@smithy/protocol-http@5.1.2)(@smithy/signature-v4@5.1.2)(@smithy/util-utf8@4.0.0)(@upstash/redis@1.35.1)(axios@1.10.0)(cohere-ai@7.17.1)(fast-xml-parser@5.2.5)(google-auth-library@10.1.0)(ibm-cloud-sdk-core@5.4.0)(ignore@7.0.5)(jsonwebtoken@9.0.2)(lodash@4.17.21)(openai@4.100.0(ws@8.18.3)(zod@3.25.67))(pg@8.16.3)(playwright@1.53.2)(redis@5.5.6)(ws@8.18.3) - '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)) - '@langchain/google-gauth': 0.1.8(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(zod@3.25.67) - '@langchain/langgraph-sdk': 0.0.70(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(react@19.1.0) - '@langchain/openai': 0.4.9(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(ws@8.18.3) + '@langchain/community': 0.3.43(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/credential-provider-node@3.840.0)(@browserbasehq/sdk@2.6.0)(@browserbasehq/stagehand@2.4.0(deepmerge@4.3.1)(dotenv@17.0.1)(react@19.1.0)(zod@3.25.67))(@ibm-cloud/watsonx-ai@1.6.8)(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(@libsql/client@0.15.9)(@smithy/eventstream-codec@4.0.4)(@smithy/protocol-http@5.1.2)(@smithy/signature-v4@5.1.2)(@smithy/util-utf8@4.0.0)(@upstash/redis@1.35.1)(axios@1.10.0)(cohere-ai@7.17.1)(fast-xml-parser@5.2.5)(google-auth-library@10.1.0)(ibm-cloud-sdk-core@5.4.0)(ignore@7.0.5)(jsonwebtoken@9.0.2)(lodash@4.17.21)(openai@4.100.0(ws@8.18.3)(zod@3.25.71))(pg@8.16.3)(playwright@1.53.2)(redis@5.5.6)(ws@8.18.3) + '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)) + '@langchain/google-gauth': 0.1.8(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(zod@3.25.71) + '@langchain/langgraph-sdk': 0.0.70(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(react@19.1.0) + '@langchain/openai': 0.4.9(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(ws@8.18.3) class-transformer: 0.5.1 class-validator: 0.14.2 express: 4.21.2 @@ -11292,7 +11376,7 @@ snapshots: graphql-scalars: 1.24.2(graphql@16.11.0) graphql-yoga: 5.13.4(graphql@16.11.0) groq-sdk: 0.5.0 - langchain: 0.3.26(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(axios@1.10.0)(openai@4.100.0(ws@8.18.3)(zod@3.25.67))(ws@8.18.3) + langchain: 0.3.26(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(axios@1.10.0)(openai@4.100.0(ws@8.18.3)(zod@3.25.71))(ws@8.18.3) openai: 4.100.0(ws@8.18.3)(zod@3.25.67) partial-json: 0.1.7 pino: 9.7.0 @@ -11300,7 +11384,7 @@ snapshots: reflect-metadata: 0.2.2 rxjs: 7.8.1 type-graphql: 2.0.0-rc.1(class-validator@0.14.2)(graphql-scalars@1.24.2(graphql@16.11.0))(graphql@16.11.0) - zod: 3.25.67 + zod: 3.25.71 transitivePeerDependencies: - '@arcjet/redact' - '@aws-crypto/sha256-js' @@ -11457,11 +11541,11 @@ snapshots: '@anthropic-ai/sdk': 0.27.3 '@copilotkit/shared': 1.8.14-next.4 '@graphql-yoga/plugin-defer-stream': 3.13.4(graphql-yoga@5.13.4(graphql@16.11.0))(graphql@16.11.0) - '@langchain/community': 0.3.43(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/credential-provider-node@3.840.0)(@browserbasehq/sdk@2.6.0)(@browserbasehq/stagehand@2.4.0(deepmerge@4.3.1)(dotenv@17.0.1)(react@19.1.0)(zod@3.25.67))(@ibm-cloud/watsonx-ai@1.6.8)(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(@libsql/client@0.15.9)(@smithy/eventstream-codec@4.0.4)(@smithy/protocol-http@5.1.2)(@smithy/signature-v4@5.1.2)(@smithy/util-utf8@4.0.0)(@upstash/redis@1.35.1)(axios@1.10.0)(cohere-ai@7.17.1)(fast-xml-parser@5.2.5)(google-auth-library@10.1.0)(ibm-cloud-sdk-core@5.4.0)(ignore@7.0.5)(jsonwebtoken@9.0.2)(lodash@4.17.21)(openai@4.100.0(ws@8.18.3)(zod@3.25.67))(pg@8.16.3)(playwright@1.53.2)(redis@5.5.6)(ws@8.18.3) - '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)) - '@langchain/google-gauth': 0.1.8(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(zod@3.25.67) - '@langchain/langgraph-sdk': 0.0.70(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(react@19.1.0) - '@langchain/openai': 0.4.9(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(ws@8.18.3) + '@langchain/community': 0.3.43(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/credential-provider-node@3.840.0)(@browserbasehq/sdk@2.6.0)(@browserbasehq/stagehand@2.4.0(deepmerge@4.3.1)(dotenv@17.0.1)(react@19.1.0)(zod@3.25.67))(@ibm-cloud/watsonx-ai@1.6.8)(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(@libsql/client@0.15.9)(@smithy/eventstream-codec@4.0.4)(@smithy/protocol-http@5.1.2)(@smithy/signature-v4@5.1.2)(@smithy/util-utf8@4.0.0)(@upstash/redis@1.35.1)(axios@1.10.0)(cohere-ai@7.17.1)(fast-xml-parser@5.2.5)(google-auth-library@10.1.0)(ibm-cloud-sdk-core@5.4.0)(ignore@7.0.5)(jsonwebtoken@9.0.2)(lodash@4.17.21)(openai@4.100.0(ws@8.18.3)(zod@3.25.71))(pg@8.16.3)(playwright@1.53.2)(redis@5.5.6)(ws@8.18.3) + '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)) + '@langchain/google-gauth': 0.1.8(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(zod@3.25.71) + '@langchain/langgraph-sdk': 0.0.70(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(react@19.1.0) + '@langchain/openai': 0.4.9(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(ws@8.18.3) class-transformer: 0.5.1 class-validator: 0.14.2 express: 4.21.2 @@ -11469,15 +11553,15 @@ snapshots: graphql-scalars: 1.24.2(graphql@16.11.0) graphql-yoga: 5.13.4(graphql@16.11.0) groq-sdk: 0.5.0 - langchain: 0.3.26(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(axios@1.10.0)(openai@4.100.0(ws@8.18.3)(zod@3.25.67))(ws@8.18.3) - openai: 4.100.0(ws@8.18.3)(zod@3.25.67) + langchain: 0.3.26(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(axios@1.10.0)(openai@4.100.0(ws@8.18.3)(zod@3.25.71))(ws@8.18.3) + openai: 4.100.0(ws@8.18.3)(zod@3.25.71) partial-json: 0.1.7 pino: 9.7.0 pino-pretty: 11.3.0 reflect-metadata: 0.2.2 rxjs: 7.8.1 type-graphql: 2.0.0-rc.1(class-validator@0.14.2)(graphql-scalars@1.24.2(graphql@16.11.0))(graphql@16.11.0) - zod: 3.25.67 + zod: 3.25.71 transitivePeerDependencies: - '@arcjet/redact' - '@aws-crypto/sha256-js' @@ -11631,8 +11715,8 @@ snapshots: chalk: 4.1.2 graphql: 16.11.0 uuid: 10.0.0 - zod: 3.25.67 - zod-to-json-schema: 3.24.5(zod@3.25.67) + zod: 3.25.71 + zod-to-json-schema: 3.24.5(zod@3.25.71) transitivePeerDependencies: - encoding @@ -11642,8 +11726,8 @@ snapshots: chalk: 4.1.2 graphql: 16.11.0 uuid: 10.0.0 - zod: 3.25.67 - zod-to-json-schema: 3.24.5(zod@3.25.67) + zod: 3.25.71 + zod-to-json-schema: 3.24.5(zod@3.25.71) transitivePeerDependencies: - encoding @@ -12229,7 +12313,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.50 + '@types/node': 20.19.4 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -12242,14 +12326,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.50 + '@types/node': 20.19.4 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.50) + jest-config: 29.7.0(@types/node@20.19.4) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -12274,7 +12358,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.50 + '@types/node': 20.19.4 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -12292,7 +12376,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.17.50 + '@types/node': 20.19.4 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -12320,7 +12404,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.17.50 + '@types/node': 20.19.4 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -12416,7 +12500,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.17.50 + '@types/node': 20.19.4 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -12470,23 +12554,23 @@ snapshots: '@jsdevtools/ono@7.1.3': {} - '@langchain/community@0.3.43(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/credential-provider-node@3.840.0)(@browserbasehq/sdk@2.6.0)(@browserbasehq/stagehand@2.4.0(deepmerge@4.3.1)(dotenv@17.0.1)(react@19.1.0)(zod@3.25.67))(@ibm-cloud/watsonx-ai@1.6.8)(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(@libsql/client@0.15.9)(@smithy/eventstream-codec@4.0.4)(@smithy/protocol-http@5.1.2)(@smithy/signature-v4@5.1.2)(@smithy/util-utf8@4.0.0)(@upstash/redis@1.35.1)(axios@1.10.0)(cohere-ai@7.17.1)(fast-xml-parser@5.2.5)(google-auth-library@10.1.0)(ibm-cloud-sdk-core@5.4.0)(ignore@7.0.5)(jsonwebtoken@9.0.2)(lodash@4.17.21)(openai@4.100.0(ws@8.18.3)(zod@3.25.67))(pg@8.16.3)(playwright@1.53.2)(redis@5.5.6)(ws@8.18.3)': + '@langchain/community@0.3.43(@aws-crypto/sha256-js@5.2.0)(@aws-sdk/credential-provider-node@3.840.0)(@browserbasehq/sdk@2.6.0)(@browserbasehq/stagehand@2.4.0(deepmerge@4.3.1)(dotenv@17.0.1)(react@19.1.0)(zod@3.25.67))(@ibm-cloud/watsonx-ai@1.6.8)(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(@libsql/client@0.15.9)(@smithy/eventstream-codec@4.0.4)(@smithy/protocol-http@5.1.2)(@smithy/signature-v4@5.1.2)(@smithy/util-utf8@4.0.0)(@upstash/redis@1.35.1)(axios@1.10.0)(cohere-ai@7.17.1)(fast-xml-parser@5.2.5)(google-auth-library@10.1.0)(ibm-cloud-sdk-core@5.4.0)(ignore@7.0.5)(jsonwebtoken@9.0.2)(lodash@4.17.21)(openai@4.100.0(ws@8.18.3)(zod@3.25.71))(pg@8.16.3)(playwright@1.53.2)(redis@5.5.6)(ws@8.18.3)': dependencies: '@browserbasehq/stagehand': 2.4.0(deepmerge@4.3.1)(dotenv@17.0.1)(react@19.1.0)(zod@3.25.67) '@ibm-cloud/watsonx-ai': 1.6.8 - '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)) - '@langchain/openai': 0.4.9(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(ws@8.18.3) + '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)) + '@langchain/openai': 0.4.9(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(ws@8.18.3) binary-extensions: 2.3.0 expr-eval: 2.0.2 flat: 5.0.2 ibm-cloud-sdk-core: 5.4.0 js-yaml: 4.1.0 - langchain: 0.3.26(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(axios@1.10.0)(openai@4.100.0(ws@8.18.3)(zod@3.25.67))(ws@8.18.3) - langsmith: 0.3.29(openai@4.100.0(ws@8.18.3)(zod@3.25.67)) - openai: 4.100.0(ws@8.18.3)(zod@3.25.67) + langchain: 0.3.26(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(axios@1.10.0)(openai@4.100.0(ws@8.18.3)(zod@3.25.71))(ws@8.18.3) + langsmith: 0.3.29(openai@4.100.0(ws@8.18.3)(zod@3.25.71)) + openai: 4.100.0(ws@8.18.3)(zod@3.25.71) uuid: 10.0.0 - zod: 3.25.67 - zod-to-json-schema: 3.24.5(zod@3.25.67) + zod: 3.25.71 + zod-to-json-schema: 3.24.5(zod@3.25.71) optionalDependencies: '@aws-crypto/sha256-js': 5.2.0 '@aws-sdk/credential-provider-node': 3.840.0 @@ -12525,7 +12609,7 @@ snapshots: - handlebars - peggy - '@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67))': + '@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71))': dependencies: '@cfworker/json-schema': 4.1.1 ansi-styles: 5.2.0 @@ -12537,80 +12621,80 @@ snapshots: p-queue: 6.6.2 p-retry: 4.6.2 uuid: 10.0.0 - zod: 3.25.67 - zod-to-json-schema: 3.24.5(zod@3.25.67) + zod: 3.25.71 + zod-to-json-schema: 3.24.5(zod@3.25.71) transitivePeerDependencies: - openai - '@langchain/core@0.3.56(openai@4.104.0(ws@8.18.3)(zod@3.25.67))': + '@langchain/core@0.3.56(openai@4.104.0(ws@8.18.3)(zod@3.25.71))': dependencies: '@cfworker/json-schema': 4.1.1 ansi-styles: 5.2.0 camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.20 - langsmith: 0.3.29(openai@4.104.0(ws@8.18.3)(zod@3.25.67)) + langsmith: 0.3.29(openai@4.104.0(ws@8.18.3)(zod@3.25.71)) mustache: 4.2.0 p-queue: 6.6.2 p-retry: 4.6.2 uuid: 10.0.0 - zod: 3.25.67 - zod-to-json-schema: 3.24.5(zod@3.25.67) + zod: 3.25.71 + zod-to-json-schema: 3.24.5(zod@3.25.71) transitivePeerDependencies: - openai - '@langchain/google-common@0.1.8(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(zod@3.25.67)': + '@langchain/google-common@0.1.8(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(zod@3.25.71)': dependencies: - '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)) + '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)) uuid: 10.0.0 - zod-to-json-schema: 3.24.5(zod@3.25.67) + zod-to-json-schema: 3.24.5(zod@3.25.71) transitivePeerDependencies: - zod - '@langchain/google-gauth@0.1.8(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(zod@3.25.67)': + '@langchain/google-gauth@0.1.8(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(zod@3.25.71)': dependencies: - '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)) - '@langchain/google-common': 0.1.8(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(zod@3.25.67) + '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)) + '@langchain/google-common': 0.1.8(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(zod@3.25.71) google-auth-library: 8.9.0 transitivePeerDependencies: - encoding - supports-color - zod - '@langchain/langgraph-sdk@0.0.70(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(react@19.1.0)': + '@langchain/langgraph-sdk@0.0.70(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(react@19.1.0)': dependencies: '@types/json-schema': 7.0.15 p-queue: 6.6.2 p-retry: 4.6.2 uuid: 9.0.1 optionalDependencies: - '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)) + '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)) react: 19.1.0 - '@langchain/langgraph-sdk@0.0.78(@langchain/core@0.3.56(openai@4.104.0(ws@8.18.3)(zod@3.25.67)))(react@19.1.0)': + '@langchain/langgraph-sdk@0.0.78(@langchain/core@0.3.56(openai@4.104.0(ws@8.18.3)(zod@3.25.71)))(react@19.1.0)': dependencies: '@types/json-schema': 7.0.15 p-queue: 6.6.2 p-retry: 4.6.2 uuid: 9.0.1 optionalDependencies: - '@langchain/core': 0.3.56(openai@4.104.0(ws@8.18.3)(zod@3.25.67)) + '@langchain/core': 0.3.56(openai@4.104.0(ws@8.18.3)(zod@3.25.71)) react: 19.1.0 - '@langchain/openai@0.4.9(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(ws@8.18.3)': + '@langchain/openai@0.4.9(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(ws@8.18.3)': dependencies: - '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)) + '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)) js-tiktoken: 1.0.20 - openai: 4.100.0(ws@8.18.3)(zod@3.25.67) - zod: 3.25.67 - zod-to-json-schema: 3.24.5(zod@3.25.67) + openai: 4.100.0(ws@8.18.3)(zod@3.25.71) + zod: 3.25.71 + zod-to-json-schema: 3.24.5(zod@3.25.71) transitivePeerDependencies: - encoding - ws - '@langchain/textsplitters@0.1.0(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))': + '@langchain/textsplitters@0.1.0(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))': dependencies: - '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)) + '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)) js-tiktoken: 1.0.20 '@libsql/client@0.15.9': @@ -12708,6 +12792,33 @@ snapshots: - valibot - zod-openapi + '@mastra/client-js@0.10.9(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71)': + dependencies: + '@ag-ui/client': 0.0.27 + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.71) + '@mastra/core': 0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71) + json-schema: 0.4.0 + rxjs: 7.8.1 + zod: 3.25.71 + zod-to-json-schema: 3.24.6(zod@3.25.71) + transitivePeerDependencies: + - '@hono/arktype-validator' + - '@hono/effect-validator' + - '@hono/typebox-validator' + - '@hono/valibot-validator' + - '@hono/zod-validator' + - '@sinclair/typebox' + - '@valibot/to-json-schema' + - arktype + - aws-crt + - effect + - encoding + - openapi-types + - react + - supports-color + - valibot + - zod-openapi + '@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.67)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -12761,6 +12872,59 @@ snapshots: - valibot - zod-openapi + '@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.71) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.71) + '@mastra/schema-compat': 0.10.3(ai@4.3.16(react@19.1.0)(zod@3.25.71))(zod@3.25.71) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/auto-instrumentations-node': 0.59.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.201.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.201.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.201.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.201.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.201.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.34.0 + '@sindresorhus/slugify': 2.2.1 + ai: 4.3.16(react@19.1.0)(zod@3.25.71) + cohere-ai: 7.17.1 + date-fns: 3.6.0 + dotenv: 16.6.1 + hono: 4.8.3 + hono-openapi: 0.4.8(@sinclair/typebox@0.34.37)(hono@4.8.3)(openapi-types@12.1.3)(zod@3.25.71) + json-schema: 0.4.0 + json-schema-to-zod: 2.6.1 + pino: 9.7.0 + pino-pretty: 13.0.0 + radash: 12.1.1 + sift: 17.1.3 + xstate: 5.20.0 + zod: 3.25.71 + zod-to-json-schema: 3.24.6(zod@3.25.71) + transitivePeerDependencies: + - '@hono/arktype-validator' + - '@hono/effect-validator' + - '@hono/typebox-validator' + - '@hono/valibot-validator' + - '@hono/zod-validator' + - '@sinclair/typebox' + - '@valibot/to-json-schema' + - arktype + - aws-crt + - effect + - encoding + - openapi-types + - react + - supports-color + - valibot + - zod-openapi + '@mastra/deployer@0.10.10(@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.67))(typescript@5.8.2)': dependencies: '@babel/core': 7.28.0 @@ -12800,12 +12964,26 @@ snapshots: - bufferutil - utf-8-validate + '@mastra/libsql@0.11.0(@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71))': + dependencies: + '@libsql/client': 0.15.9 + '@mastra/core': 0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@mastra/loggers@0.10.3(@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.67))': dependencies: '@mastra/core': 0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.67) pino: 9.7.0 pino-pretty: 13.0.0 + '@mastra/loggers@0.10.3(@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71))': + dependencies: + '@mastra/core': 0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71) + pino: 9.7.0 + pino-pretty: 13.0.0 + '@mastra/mcp@0.10.5(@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.67))(zod@3.25.71)': dependencies: '@mastra/core': 0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.67) @@ -12823,15 +13001,32 @@ snapshots: dependencies: '@mastra/core': 0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.67) '@upstash/redis': 1.35.1 - ai: 4.3.16(react@19.1.0)(zod@3.25.67) + ai: 4.3.16(react@19.1.0)(zod@3.25.71) js-tiktoken: 1.0.20 pg: 8.16.3 pg-pool: 3.10.1(pg@8.16.3) postgres: 3.4.7 redis: 4.7.1 xxhash-wasm: 1.1.0 - zod: 3.25.67 - zod-to-json-schema: 3.24.6(zod@3.25.67) + zod: 3.25.71 + zod-to-json-schema: 3.24.6(zod@3.25.71) + transitivePeerDependencies: + - pg-native + - react + + '@mastra/memory@0.11.1(@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71))(react@19.1.0)': + dependencies: + '@mastra/core': 0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.71) + '@upstash/redis': 1.35.1 + ai: 4.3.16(react@19.1.0)(zod@3.25.71) + js-tiktoken: 1.0.20 + pg: 8.16.3 + pg-pool: 3.10.1(pg@8.16.3) + postgres: 3.4.7 + redis: 4.7.1 + xxhash-wasm: 1.1.0 + zod: 3.25.71 + zod-to-json-schema: 3.24.6(zod@3.25.71) transitivePeerDependencies: - pg-native - react @@ -12844,6 +13039,14 @@ snapshots: zod-from-json-schema: 0.0.5 zod-to-json-schema: 3.24.6(zod@3.25.67) + '@mastra/schema-compat@0.10.3(ai@4.3.16(react@19.1.0)(zod@3.25.71))(zod@3.25.71)': + dependencies: + ai: 4.3.16(react@19.1.0)(zod@3.25.71) + json-schema: 0.4.0 + zod: 3.25.71 + zod-from-json-schema: 0.0.5 + zod-to-json-schema: 3.24.6(zod@3.25.71) + '@mastra/server@0.10.10(@mastra/core@0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.67))(zod@3.25.71)': dependencies: '@mastra/core': 0.10.10(@sinclair/typebox@0.34.37)(openapi-types@12.1.3)(react@19.1.0)(zod@3.25.67) @@ -13041,10 +13244,10 @@ snapshots: '@opentelemetry/instrumentation-tedious': 0.20.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-undici': 0.12.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-winston': 0.46.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-alibaba-cloud': 0.31.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-aws': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-alibaba-cloud': 0.31.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-aws': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/resource-detector-azure': 0.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resource-detector-container': 0.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-container': 0.7.2(@opentelemetry/api@1.9.0) '@opentelemetry/resource-detector-gcp': 0.35.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': 0.201.1(@opentelemetry/api@1.9.0) @@ -14110,13 +14313,6 @@ snapshots: '@opentelemetry/redis-common@0.37.0': {} - '@opentelemetry/resource-detector-alibaba-cloud@0.31.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 - '@opentelemetry/resource-detector-alibaba-cloud@0.31.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -14124,13 +14320,6 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.34.0 - '@opentelemetry/resource-detector-aws@2.1.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 - '@opentelemetry/resource-detector-aws@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -14152,13 +14341,6 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.34.0 - '@opentelemetry/resource-detector-container@0.7.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.34.0 - '@opentelemetry/resource-detector-container@0.7.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -14888,7 +15070,7 @@ snapshots: node-fetch: 3.3.2 ora: 6.3.1 prompts: 2.4.2 - zod: 3.25.67 + zod: 3.25.71 '@shikijs/core@1.29.2': dependencies: @@ -15585,11 +15767,11 @@ snapshots: '@types/bunyan@1.8.11': dependencies: - '@types/node': 20.17.50 + '@types/node': 20.19.4 '@types/connect@3.4.38': dependencies: - '@types/node': 20.17.50 + '@types/node': 20.19.4 '@types/debug@4.1.12': dependencies: @@ -15621,7 +15803,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 20.17.50 + '@types/node': 20.19.4 '@types/hast@2.3.10': dependencies: @@ -15678,23 +15860,19 @@ snapshots: '@types/memcached@2.2.10': dependencies: - '@types/node': 20.17.50 + '@types/node': 20.19.4 '@types/ms@2.1.0': {} '@types/mysql@2.15.26': dependencies: - '@types/node': 20.17.50 + '@types/node': 20.19.4 '@types/node-fetch@2.6.12': dependencies: - '@types/node': 20.17.50 + '@types/node': 20.19.4 form-data: 4.0.2 - '@types/node@18.19.103': - dependencies: - undici-types: 5.26.5 - '@types/node@18.19.115': dependencies: undici-types: 5.26.5 @@ -15709,7 +15887,7 @@ snapshots: '@types/oracledb@6.5.2': dependencies: - '@types/node': 20.17.50 + '@types/node': 20.19.4 '@types/pg-pool@2.0.6': dependencies: @@ -15723,7 +15901,7 @@ snapshots: '@types/pg@8.6.1': dependencies: - '@types/node': 20.17.50 + '@types/node': 20.19.4 pg-protocol: 1.10.0 pg-types: 2.2.0 @@ -15753,11 +15931,11 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 20.17.50 + '@types/node': 20.19.4 '@types/through@0.0.33': dependencies: - '@types/node': 20.17.50 + '@types/node': 20.19.4 '@types/tough-cookie@4.0.5': {} @@ -16121,6 +16299,18 @@ snapshots: optionalDependencies: react: 19.1.0 + ai@4.3.16(react@19.1.0)(zod@3.25.71): + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.71) + '@ai-sdk/react': 1.2.12(react@19.1.0)(zod@3.25.71) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.71) + '@opentelemetry/api': 1.9.0 + jsondiffpatch: 0.6.0 + zod: 3.25.71 + optionalDependencies: + react: 19.1.0 + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -16730,6 +16920,21 @@ snapshots: - supports-color - ts-node + create-jest@29.7.0(@types/node@20.19.4): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.19.4) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + crelt@1.0.6: {} cross-inspect@1.0.1: @@ -17977,7 +18182,7 @@ snapshots: groq-sdk@0.5.0: dependencies: - '@types/node': 18.19.103 + '@types/node': 18.19.115 '@types/node-fetch': 2.6.12 abort-controller: 3.0.0 agentkeepalive: 4.6.0 @@ -18170,6 +18375,15 @@ snapshots: hono: 4.8.3 zod: 3.25.67 + hono-openapi@0.4.8(@sinclair/typebox@0.34.37)(hono@4.8.3)(openapi-types@12.1.3)(zod@3.25.71): + dependencies: + json-schema-walker: 2.0.0 + openapi-types: 12.1.3 + optionalDependencies: + '@sinclair/typebox': 0.34.37 + hono: 4.8.3 + zod: 3.25.71 + hono@4.8.3: {} html-escaper@2.0.2: {} @@ -18563,7 +18777,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.50 + '@types/node': 20.19.4 chalk: 4.1.2 co: 4.6.0 dedent: 1.6.0 @@ -18602,6 +18816,25 @@ snapshots: - supports-color - ts-node + jest-cli@29.7.0(@types/node@20.19.4): + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.19.4) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.19.4) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-config@29.7.0(@types/node@20.17.50): dependencies: '@babel/core': 7.27.1 @@ -18632,6 +18865,36 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@20.19.4): + dependencies: + '@babel/core': 7.27.1 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.27.1) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.19.4 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-diff@29.7.0: dependencies: chalk: 4.1.2 @@ -18656,7 +18919,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.50 + '@types/node': 20.19.4 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -18666,7 +18929,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.17.50 + '@types/node': 20.19.4 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -18721,7 +18984,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.50 + '@types/node': 20.19.4 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -18759,7 +19022,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.50 + '@types/node': 20.19.4 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -18787,7 +19050,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.50 + '@types/node': 20.19.4 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.2 @@ -18833,7 +19096,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.17.50 + '@types/node': 20.19.4 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -18862,7 +19125,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.17.50 + '@types/node': 20.19.4 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -18878,7 +19141,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 20.17.50 + '@types/node': 20.19.4 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -18904,6 +19167,18 @@ snapshots: - supports-color - ts-node + jest@29.7.0(@types/node@20.19.4): + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.19.4) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jiti@2.4.2: {} jose@5.10.0: {} @@ -19029,21 +19304,21 @@ snapshots: kleur@4.1.5: {} - langchain@0.3.26(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(axios@1.10.0)(openai@4.100.0(ws@8.18.3)(zod@3.25.67))(ws@8.18.3): + langchain@0.3.26(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(axios@1.10.0)(openai@4.100.0(ws@8.18.3)(zod@3.25.71))(ws@8.18.3): dependencies: - '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)) - '@langchain/openai': 0.4.9(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67)))(ws@8.18.3) - '@langchain/textsplitters': 0.1.0(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.67))) + '@langchain/core': 0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)) + '@langchain/openai': 0.4.9(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71)))(ws@8.18.3) + '@langchain/textsplitters': 0.1.0(@langchain/core@0.3.56(openai@4.100.0(ws@8.18.3)(zod@3.25.71))) js-tiktoken: 1.0.20 js-yaml: 4.1.0 jsonpointer: 5.0.1 - langsmith: 0.3.29(openai@4.100.0(ws@8.18.3)(zod@3.25.67)) + langsmith: 0.3.29(openai@4.100.0(ws@8.18.3)(zod@3.25.71)) openapi-types: 12.1.3 p-retry: 4.6.2 uuid: 10.0.0 yaml: 2.8.0 - zod: 3.25.67 - zod-to-json-schema: 3.24.5(zod@3.25.67) + zod: 3.25.71 + zod-to-json-schema: 3.24.5(zod@3.25.71) optionalDependencies: axios: 1.10.0(debug@4.4.1) transitivePeerDependencies: @@ -19063,7 +19338,7 @@ snapshots: optionalDependencies: openai: 4.100.0(ws@8.18.3)(zod@3.25.67) - langsmith@0.3.29(openai@4.104.0(ws@8.18.3)(zod@3.25.67)): + langsmith@0.3.29(openai@4.100.0(ws@8.18.3)(zod@3.25.71)): dependencies: '@types/uuid': 10.0.0 chalk: 4.1.2 @@ -19073,7 +19348,19 @@ snapshots: semver: 7.7.2 uuid: 10.0.0 optionalDependencies: - openai: 4.104.0(ws@8.18.3)(zod@3.25.67) + openai: 4.100.0(ws@8.18.3)(zod@3.25.71) + + langsmith@0.3.29(openai@4.104.0(ws@8.18.3)(zod@3.25.71)): + dependencies: + '@types/uuid': 10.0.0 + chalk: 4.1.2 + console-table-printer: 2.12.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + semver: 7.7.2 + uuid: 10.0.0 + optionalDependencies: + openai: 4.104.0(ws@8.18.3)(zod@3.25.71) language-subtag-registry@0.3.23: {} @@ -20186,7 +20473,7 @@ snapshots: openai@4.100.0(ws@8.18.3)(zod@3.25.67): dependencies: - '@types/node': 18.19.103 + '@types/node': 18.19.115 '@types/node-fetch': 2.6.12 abort-controller: 3.0.0 agentkeepalive: 4.6.0 @@ -20199,6 +20486,21 @@ snapshots: transitivePeerDependencies: - encoding + openai@4.100.0(ws@8.18.3)(zod@3.25.71): + dependencies: + '@types/node': 18.19.115 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + ws: 8.18.3 + zod: 3.25.71 + transitivePeerDependencies: + - encoding + openai@4.104.0(ws@8.18.3)(zod@3.25.67): dependencies: '@types/node': 18.19.115 @@ -20214,6 +20516,22 @@ snapshots: transitivePeerDependencies: - encoding + openai@4.104.0(ws@8.18.3)(zod@3.25.71): + dependencies: + '@types/node': 18.19.115 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + ws: 8.18.3 + zod: 3.25.71 + transitivePeerDependencies: + - encoding + optional: true + openapi-types@12.1.3: {} optionator@0.9.4: @@ -20480,12 +20798,13 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.6)(yaml@2.8.0): + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(yaml@2.8.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.4.2 postcss: 8.5.6 + tsx: 4.20.3 yaml: 2.8.0 postcss-selector-parser@6.0.10: @@ -20708,7 +21027,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.17.50 + '@types/node': 20.19.4 long: 5.3.2 proxy-addr@2.0.7: @@ -21791,6 +22110,27 @@ snapshots: babel-jest: 30.0.4(@babel/core@7.27.1) esbuild: 0.25.4 + ts-jest@29.3.4(@babel/core@7.27.1)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.27.1))(esbuild@0.25.4)(jest@29.7.0(@types/node@20.19.4))(typescript@5.8.2): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@20.19.4) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.2 + type-fest: 4.41.0 + typescript: 5.8.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.27.1 + '@jest/transform': 30.0.4 + '@jest/types': 30.0.1 + babel-jest: 30.0.4(@babel/core@7.27.1) + esbuild: 0.25.4 + ts-poet@6.11.0: dependencies: dprint-node: 1.0.8 @@ -21817,7 +22157,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.0(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.2)(yaml@2.8.0): + tsup@8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.2)(yaml@2.8.0): dependencies: bundle-require: 5.1.0(esbuild@0.25.4) cac: 6.7.14 @@ -21828,7 +22168,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.6)(yaml@2.8.0) + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(yaml@2.8.0) resolve-from: 5.0.0 rollup: 4.41.0 source-map: 0.8.0-beta.0 @@ -21845,6 +22185,13 @@ snapshots: - tsx - yaml + tsx@4.20.3: + dependencies: + esbuild: 0.25.5 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + turbo-darwin-64@2.5.3: optional: true @@ -21885,7 +22232,7 @@ snapshots: type-graphql@2.0.0-rc.1(class-validator@0.14.2)(graphql-scalars@1.24.2(graphql@16.11.0))(graphql@16.11.0): dependencies: '@graphql-yoga/subscription': 5.0.5 - '@types/node': 20.17.50 + '@types/node': 20.19.4 '@types/semver': 7.7.0 graphql: 16.11.0 graphql-query-complexity: 0.12.0(graphql@16.11.0) @@ -22388,15 +22735,15 @@ snapshots: zod-from-json-schema@0.0.5: dependencies: - zod: 3.25.67 + zod: 3.25.71 - zod-to-json-schema@3.24.5(zod@3.25.17): + zod-to-json-schema@3.24.5(zod@3.25.71): dependencies: - zod: 3.25.17 + zod: 3.25.71 - zod-to-json-schema@3.24.5(zod@3.25.67): + zod-to-json-schema@3.24.6(zod@3.25.17): dependencies: - zod: 3.25.67 + zod: 3.25.17 zod-to-json-schema@3.24.6(zod@3.25.67): dependencies: