diff --git a/packages/tool-registry/README.md b/packages/tool-registry/README.md new file mode 100644 index 000000000..db44c031c --- /dev/null +++ b/packages/tool-registry/README.md @@ -0,0 +1,247 @@ +# @temporalio/tool-registry + +LLM tool-calling primitives for Temporal activities — define tools once, use with +Anthropic or OpenAI. + +## Before you start + +A Temporal Activity is a function that Temporal monitors and retries automatically on failure. Temporal streams progress between retries via heartbeats — that's the mechanism `agenticSession` uses to resume a crashed LLM conversation mid-turn. + +`runToolLoop` works standalone in any async function — no Temporal server needed. Add `agenticSession` only when you need crash-safe resume inside a Temporal activity. + +`agenticSession` requires a running Temporal worker — it reads and writes heartbeat state from the active activity context. Use `runToolLoop` standalone for scripts, one-off jobs, or any code that runs outside a Temporal worker. + +New to Temporal? → https://docs.temporal.io/develop + +## Install + +```bash +npm install @temporalio/tool-registry @anthropic-ai/sdk # Anthropic +npm install @temporalio/tool-registry openai # OpenAI +``` + +## Quickstart + +Tool definitions use [JSON Schema](https://json-schema.org/understanding-json-schema/) for `input_schema`. The quickstart uses a single string field; for richer schemas refer to the JSON Schema docs. + +```typescript +import { ToolRegistry, runToolLoop } from '@temporalio/tool-registry'; + +export async function analyzeCode(prompt: string): Promise { + const results: string[] = []; + const tools = new ToolRegistry(); + + tools.define( + { + name: 'flag_issue', + description: 'Flag a problem found in the analysis', + input_schema: { + type: 'object', + properties: { description: { type: 'string' } }, + required: ['description'], + }, + }, + (inp: Record) => { + results.push(inp['description'] as string); + return 'recorded'; // this string is sent back to the LLM as the tool result + } + ); + + await runToolLoop({ + provider: 'anthropic', // reads ANTHROPIC_API_KEY from environment; or use 'openai' + system: 'You are a code reviewer. Call flag_issue for each problem you find.', + prompt, + tools, + }); + + return results; +} +``` + +## Feature matrix + +| Feature | `@temporalio/tool-registry` | `@temporalio/ai-sdk` | +|---|---|---| +| Anthropic (claude-*) | ✓ | ✗ | +| OpenAI (gpt-*) | ✓ | ✓ (via AI SDK) | +| MCP tool wrapping | ✓ | ✓ | +| Crash-safe heartbeat resume | ✓ (via `agenticSession`) | ✗ | +| AI SDK provider abstraction | ✗ | ✓ | + +Use `@temporalio/ai-sdk` when you are already using the Vercel AI SDK and want each model call to be a separately observable, retryable Temporal activity. +Use `@temporalio/tool-registry` for direct Anthropic support, crash-safe sessions that survive server-side session expiry, or when you need the same implementation pattern across all six Temporal SDKs (Go, Java, Ruby, .NET have no framework-level integrations). + +## Sandbox configuration + +You need this if you register both workflows and activities on the same `Worker` instance. If your activities run on a dedicated worker (no `workflowsPath`), skip this section. + +The Temporal workflow bundler excludes third-party packages. Use `ToolRegistryPlugin` +so that activities using LLM libraries can run on the same worker as bundled workflows: + +```typescript +import { Worker } from '@temporalio/worker'; +import { ToolRegistryPlugin } from '@temporalio/tool-registry'; + +const worker = await Worker.create({ + connection, + namespace: 'default', + taskQueue: 'my-queue', + plugins: [new ToolRegistryPlugin({ provider: 'anthropic' })], + workflowsPath: require.resolve('./workflows'), + activities, +}); +``` + +## MCP integration + +MCP tool wrapping is supported via `ToolRegistry.fromMcpTools()`. See the MCP integration guide for a complete example including server setup. + +### Selecting a model + +The default model is `"claude-sonnet-4-6"` (Anthropic) or `"gpt-4o"` (OpenAI). Pass `model` to `runToolLoop`: + +```typescript +await runToolLoop({ + provider: 'anthropic', + model: 'claude-3-5-sonnet-20241022', + system: '...', + prompt, + tools, +}); +``` + +Model IDs are defined by the provider — see Anthropic or OpenAI docs for current names. + +### OpenAI + +```typescript +await runToolLoop({ + provider: 'openai', // reads OPENAI_API_KEY from environment + system: '...', + prompt, + tools, +}); +``` + +## Crash-safe agentic sessions + +For multi-turn LLM conversations that must survive activity retries, use +`agenticSession`. It saves conversation history via `activity.heartbeat()` on every +turn and restores it automatically on retry. + +```typescript +export async function longAnalysis(prompt: string): Promise { + let results: object[] = []; + await agenticSession(async (session) => { + const tools = new ToolRegistry(); + tools.define( + { name: 'flag', description: '...', input_schema: { type: 'object' } }, + (inp: Record) => { + session.results.push(inp); + return 'ok'; // this string is sent back to the LLM as the tool result + } + ); + await session.runToolLoop({ + registry: tools, + provider: 'anthropic', // reads ANTHROPIC_API_KEY from environment + system: '...', + prompt, + }); + results = session.results; // capture after loop completes + }); + return results; +} +``` + +## Testing without an API key + +Use `MockProvider` and `ResponseBuilder` to test tool-calling logic without hitting a live API: + +```typescript +import { ToolRegistry } from '@temporalio/tool-registry'; +import { MockProvider, ResponseBuilder } from '@temporalio/tool-registry/testing'; + +const tools = new ToolRegistry(); +tools.define( + { name: 'flag', description: 'd', input_schema: { type: 'object' } }, + (inp: Record) => 'ok' // this string is sent back to the LLM as the tool result +); + +const provider = new MockProvider([ + ResponseBuilder.toolCall('flag', { description: 'stale API' }), + ResponseBuilder.done('done'), +]); +const messages = [{ role: 'user', content: 'analyze' }]; +await provider.runLoop(messages, tools); +assert(messages.length > 2); +``` + +## Integration testing with real providers + +To run the integration tests against live Anthropic and OpenAI APIs: + +```bash +RUN_INTEGRATION_TESTS=1 \ + ANTHROPIC_API_KEY=sk-ant-... \ + OPENAI_API_KEY=sk-proj-... \ + npm test +``` + +Tests skip automatically when `RUN_INTEGRATION_TESTS` is unset. Real API calls +incur billing — expect a few cents per full test run. + +## Storing application results + +`session.results` accumulates application-level results during the tool loop. +Elements are serialized to JSON inside each heartbeat checkpoint — they must be +plain objects with JSON-serializable values. A non-serializable value raises +a non-retryable `ApplicationFailure` at heartbeat time rather than silently losing +data on the next retry. + +### Storing typed results + +Convert your domain type to a plain object at the tool-call site and back after +the session: + +```typescript +interface Finding { type: string; file: string; } + +// Inside tool handler: +session.results.push({ type: 'smell', file: 'foo.ts' } satisfies Finding); + +// After session: +const findings = session.results as Finding[]; +``` + +## Per-turn LLM timeout + +Individual LLM calls inside the tool loop are unbounded by default. A hung HTTP +connection holds the activity open until Temporal's `ScheduleToCloseTimeout` +fires — potentially many minutes. Set a per-turn timeout on the provider client: + +```typescript +import Anthropic from '@anthropic-ai/sdk'; +const client = new Anthropic({ apiKey: '...', timeout: 30_000 }); // ms +await session.runToolLoop({ ..., client }); +``` + +Recommended timeouts: + +| Model type | Recommended | +|---|---| +| Standard (Claude 3.x, GPT-4o) | 30 s | +| Reasoning (o1, o3, extended thinking) | 300 s | + +### Activity-level timeout + +Set `scheduleToCloseTimeout` on the activity options to bound the entire conversation: + +```typescript +await workflow.executeActivity(longAnalysis, prompt, { + scheduleToCloseTimeout: '10m', +}); +``` + +The per-turn client timeout and `scheduleToCloseTimeout` are complementary: +- Per-turn timeout fires if one LLM call hangs (protects against a single stuck turn) +- `scheduleToCloseTimeout` bounds the entire conversation including all retries (protects against runaway multi-turn loops) diff --git a/packages/tool-registry/package.json b/packages/tool-registry/package.json new file mode 100644 index 000000000..17492f7ad --- /dev/null +++ b/packages/tool-registry/package.json @@ -0,0 +1,66 @@ +{ + "name": "@temporalio/tool-registry", + "version": "1.15.0", + "description": "LLM tool-calling primitives for Temporal activities — define tools once, use with Anthropic or OpenAI", + "main": "lib/index.js", + "types": "./lib/index.d.ts", + "keywords": [ + "temporal", + "workflow", + "llm", + "anthropic", + "openai", + "tool-use", + "agentic" + ], + "author": "Temporal Technologies Inc. ", + "license": "MIT", + "dependencies": { + "@temporalio/activity": "workspace:*", + "@temporalio/common": "workspace:*", + "@temporalio/plugin": "workspace:*" + }, + "peerDependencies": { + "@anthropic-ai/sdk": ">=0.40.0", + "openai": ">=4.0.0" + }, + "peerDependenciesMeta": { + "@anthropic-ai/sdk": { + "optional": true + }, + "openai": { + "optional": true + } + }, + "devDependencies": { + "@anthropic-ai/sdk": "^0.40.0", + "@types/mocha": "^10.0.10", + "@types/node": "^20.10.8", + "mocha": "^10.0.0", + "ts-node": "^10.9.0", + "typescript": "^5.0.0" + }, + "scripts": { + "build": "tsc --build", + "test": "TS_NODE_PROJECT=tsconfig.test.json mocha --require ts-node/register --extension ts 'src/**/*.test.ts'" + }, + "engines": { + "node": ">= 20.0.0" + }, + "bugs": { + "url": "https://github.com/temporalio/sdk-typescript/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/temporalio/sdk-typescript.git", + "directory": "packages/tool-registry" + }, + "homepage": "https://github.com/temporalio/sdk-typescript/tree/main/packages/tool-registry", + "publishConfig": { + "access": "public" + }, + "files": [ + "src", + "lib" + ] +} diff --git a/packages/tool-registry/src/index.ts b/packages/tool-registry/src/index.ts new file mode 100644 index 000000000..f05bb3f48 --- /dev/null +++ b/packages/tool-registry/src/index.ts @@ -0,0 +1,43 @@ +/** + * @temporalio/tool-registry — LLM tool-calling primitives for Temporal activities. + * + * Define tools once with {@link ToolRegistry}, export provider-specific schemas + * for Anthropic or OpenAI, and run complete multi-turn tool-calling conversations + * with {@link runToolLoop}. + * + * For crash-safe multi-turn sessions that survive activity retries, use + * {@link agenticSession} (exported from `./session`). + * + * @example + * ```typescript + * import { ToolRegistry, runToolLoop } from '@temporalio/tool-registry'; + * + * const tools = new ToolRegistry(); + * const issues: string[] = []; + * + * tools.define( + * { name: 'flag', description: 'Flag an issue', input_schema: { ... } }, + * (inp) => { issues.push(inp.description as string); return 'recorded'; } + * ); + * + * await runToolLoop({ + * provider: 'anthropic', + * system: 'You are a code reviewer...', + * prompt: 'Review this code...', + * tools, + * }); + * ``` + */ + +export { ToolRegistry } from './registry'; +export type { ToolDefinition, ToolHandler, McpToolDescriptor } from './registry'; +export { AnthropicProvider, OpenAIProvider, runToolLoop } from './providers'; +export type { Message, RunToolLoopOptions as ProviderRunLoopOptions } from './providers'; +export { ToolRegistryPlugin } from './plugin'; +export type { ToolRegistryPluginOptions } from './plugin'; +export { registryFromMcpTools } from './mcp'; +export type { McpTool } from './mcp'; +export { AgenticSession, agenticSession } from './session'; +export type { RunToolLoopOptions } from './session'; +export { ResponseBuilder, MockProvider, FakeToolRegistry, MockAgenticSession, CrashAfterTurns } from './testing'; +export type { MockResponse } from './testing'; diff --git a/packages/tool-registry/src/mcp.ts b/packages/tool-registry/src/mcp.ts new file mode 100644 index 000000000..0501c1186 --- /dev/null +++ b/packages/tool-registry/src/mcp.ts @@ -0,0 +1,36 @@ +/** + * MCP (Model Context Protocol) integration for ToolRegistry. + * + * Provides utilities for wrapping MCP tool definitions in a {@link ToolRegistry}, + * enabling use of MCP-sourced tools with Anthropic or OpenAI providers. + */ + +import { ToolRegistry } from './registry'; + +/** MCP-compatible tool descriptor. */ +export interface McpTool { + name: string; + description?: string; + inputSchema?: Record; +} + +/** + * Build a {@link ToolRegistry} from a list of MCP tool descriptors. + * + * Each MCP tool is registered with a no-op handler (returns empty string). + * Replace handlers by calling {@link ToolRegistry.define} with the same name + * after construction. + * + * @example + * ```typescript + * const registry = registryFromMcpTools(mcpServer.listTools()); + * // Override the handler for a specific tool: + * registry.define( + * { name: 'read_file', description: '...', input_schema: { ... } }, + * (inp) => mcpServer.callTool('read_file', inp) + * ); + * ``` + */ +export function registryFromMcpTools(tools: McpTool[]): ToolRegistry { + return ToolRegistry.fromMcpTools(tools); +} diff --git a/packages/tool-registry/src/plugin.ts b/packages/tool-registry/src/plugin.ts new file mode 100644 index 000000000..1cd9de078 --- /dev/null +++ b/packages/tool-registry/src/plugin.ts @@ -0,0 +1,81 @@ +/** + * ToolRegistryPlugin — Temporal plugin for LLM tool-calling activities. + * + * Configures the worker's workflow bundle to pass through `anthropic` and + * `openai` imports, which are otherwise bundled out of the Temporal workflow + * sandbox. Apply this plugin when using {@link runToolLoop} or + * {@link AgenticSession} in activities on the same worker as sandboxed + * workflows. + * + * @example + * ```typescript + * import { Worker } from '@temporalio/worker'; + * import { ToolRegistryPlugin } from '@temporalio/tool-registry'; + * + * const worker = await Worker.create({ + * connection, + * namespace: 'default', + * taskQueue: 'my-queue', + * plugins: [new ToolRegistryPlugin({ provider: 'anthropic' })], + * workflowsPath: require.resolve('./workflows'), + * activities, + * }); + * ``` + */ + +import { SimplePlugin } from '@temporalio/plugin'; + +/** Options for {@link ToolRegistryPlugin}. */ +export interface ToolRegistryPluginOptions { + /** + * LLM provider to configure sandbox passthrough for. + * - `"anthropic"`: pass through `@anthropic-ai/sdk` + * - `"openai"`: pass through `openai` + * - `"both"`: pass through both + * @defaultValue `"anthropic"` + */ + provider?: 'anthropic' | 'openai' | 'both'; + + /** + * Default Anthropic model name surfaced to activities. + * @defaultValue `"claude-sonnet-4-6"` + */ + anthropicModel?: string; + + /** + * Default OpenAI model name surfaced to activities. + * @defaultValue `"gpt-4o"` + */ + openaiModel?: string; +} + +/** + * Temporal plugin that configures sandbox passthrough for LLM imports. + * + * The Temporal workflow bundler excludes third-party packages like + * `@anthropic-ai/sdk` and `openai` from the workflow sandbox. This plugin + * adds the necessary `ignoreModules` entries so that activities using those + * libraries can be registered on the same worker as bundled workflows. + */ +export class ToolRegistryPlugin extends SimplePlugin { + constructor(options: ToolRegistryPluginOptions = {}) { + const { provider = 'anthropic' } = options; + + const ignoreModules: string[] = []; + if (provider === 'anthropic' || provider === 'both') { + ignoreModules.push('@anthropic-ai/sdk'); + } + if (provider === 'openai' || provider === 'both') { + ignoreModules.push('openai'); + } + + super({ + name: 'ToolRegistryPlugin', + ...(ignoreModules.length > 0 + ? { + bundlerOptions: { ignoreModules }, + } + : {}), + }); + } +} diff --git a/packages/tool-registry/src/providers.test.ts b/packages/tool-registry/src/providers.test.ts new file mode 100644 index 000000000..5f3c14dbc --- /dev/null +++ b/packages/tool-registry/src/providers.test.ts @@ -0,0 +1,305 @@ +/** + * Unit tests for provider implementations and runToolLoop. + * Runs without an API key — LLM calls are mocked. + */ + +import assert from 'assert'; +import { ToolRegistry } from './registry'; +import { AnthropicProvider, OpenAIProvider, runToolLoop } from './providers'; + +// ── Mock client helpers ──────────────────────────────────────────────────────── + +function makeAnthropicClient(responses: Array<{ content: unknown[]; stop_reason: string }>) { + let idx = 0; + return { + messages: { + create: async (_opts: unknown) => { + if (idx >= responses.length) return { content: [], stop_reason: 'end_turn' }; + return responses[idx++]; + }, + }, + }; +} + +function makeOpenAIClient( + responses: Array<{ + choices: Array<{ + message: { content: string | null; tool_calls?: Array<{ id: string; function: { name: string; arguments: string } }> }; + finish_reason: string; + }>; + }> +) { + let idx = 0; + return { + chat: { + completions: { + create: async (_opts: unknown) => { + if (idx >= responses.length) return { choices: [{ message: { content: 'done' }, finish_reason: 'stop' }] }; + return responses[idx++]; + }, + }, + }, + }; +} + +// ── AnthropicProvider tests ──────────────────────────────────────────────────── + +describe('AnthropicProvider', () => { + it('runs to completion when model returns end_turn immediately', async () => { + const registry = new ToolRegistry(); + const client = makeAnthropicClient([{ content: [{ type: 'text', text: 'done' }], stop_reason: 'end_turn' }]); + const provider = new AnthropicProvider(registry, 'system', { client }); + + const messages = [{ role: 'user', content: 'go' }]; + await provider.runLoop(messages); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[1]['role'], 'assistant'); + }); + + it('dispatches tool calls and continues', async () => { + const collected: string[] = []; + const registry = new ToolRegistry(); + registry.define({ name: 'flag', description: 'd', input_schema: {} }, (inp) => { + collected.push(inp['value'] as string); + return 'recorded'; + }); + + const client = makeAnthropicClient([ + { + content: [{ type: 'tool_use', id: 'c1', name: 'flag', input: { value: 'hello' } }], + stop_reason: 'tool_use', + }, + { content: [{ type: 'text', text: 'done' }], stop_reason: 'end_turn' }, + ]); + const provider = new AnthropicProvider(registry, 'sys', { client }); + + const messages = [{ role: 'user', content: 'go' }]; + await provider.runLoop(messages); + + assert.deepStrictEqual(collected, ['hello']); + // messages: user + assistant(tool_use) + user(tool_result) + assistant(done) + assert.strictEqual(messages.length, 4); + }); +}); + +// ── OpenAIProvider tests ──────────────────────────────────────────────────────── + +describe('OpenAIProvider', () => { + it('runs to completion on stop immediately', async () => { + const registry = new ToolRegistry(); + const client = makeOpenAIClient([ + { choices: [{ message: { content: 'done' }, finish_reason: 'stop' }] }, + ]); + const provider = new OpenAIProvider(registry, 'system', { client }); + + const messages = [{ role: 'user', content: 'go' }]; + await provider.runLoop(messages); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[1]['role'], 'assistant'); + }); + + it('dispatches function calls and continues', async () => { + const collected: string[] = []; + const registry = new ToolRegistry(); + registry.define({ name: 'record', description: 'd', input_schema: {} }, (inp) => { + collected.push(inp['val'] as string); + return 'ok'; + }); + + const client = makeOpenAIClient([ + { + choices: [ + { + message: { + content: null, + tool_calls: [{ id: 'tc1', function: { name: 'record', arguments: '{"val":"world"}' } }], + }, + finish_reason: 'tool_calls', + }, + ], + }, + { choices: [{ message: { content: 'done' }, finish_reason: 'stop' }] }, + ]); + const provider = new OpenAIProvider(registry, 'sys', { client }); + + const messages = [{ role: 'user', content: 'go' }]; + await provider.runLoop(messages); + + assert.deepStrictEqual(collected, ['world']); + assert.strictEqual(messages.length, 4); // user + assistant(fn) + tool + assistant(done) + }); +}); + +// ── runToolLoop tests ────────────────────────────────────────────────────────── + +describe('runToolLoop', () => { + it('throws for unknown provider', async () => { + await assert.rejects( + () => + runToolLoop({ + provider: 'gemini', + system: 's', + prompt: 'p', + tools: new ToolRegistry(), + }), + /gemini/ + ); + }); + + it('prepends prompt as first user message when messages is empty', async () => { + const client = makeAnthropicClient([{ content: [], stop_reason: 'end_turn' }]); + const messages = await runToolLoop({ + provider: 'anthropic', + system: 'sys', + prompt: 'my prompt', + tools: new ToolRegistry(), + client, + }); + assert.strictEqual((messages[0] as Record)['content'], 'my prompt'); + }); + + it('uses existing messages when provided', async () => { + const client = makeAnthropicClient([{ content: [], stop_reason: 'end_turn' }]); + const existing = [{ role: 'user', content: 'original' }]; + const messages = await runToolLoop({ + provider: 'anthropic', + system: 'sys', + prompt: 'ignored', + tools: new ToolRegistry(), + messages: [...existing], + client, + }); + assert.strictEqual((messages[0] as Record)['content'], 'original'); + }); +}); + +// ── is_error / handler error tests ──────────────────────────────────────────── + +describe('AnthropicProvider handler errors', () => { + it('catches handler exception, sets is_error:true, does not crash', async () => { + const registry = new ToolRegistry(); + registry.define({ name: 'boom', description: 'd', input_schema: {} }, () => { + throw new Error('intentional failure'); + }); + + const client = makeAnthropicClient([ + { content: [{ type: 'tool_use', id: 'c1', name: 'boom', input: {} }], stop_reason: 'tool_use' }, + { content: [{ type: 'text', text: 'done' }], stop_reason: 'end_turn' }, + ]); + const provider = new AnthropicProvider(registry, 'sys', { client }); + + const messages = [{ role: 'user', content: 'go' }]; + // Must not throw even though handler errors. + await provider.runLoop(messages); + + // messages: user, assistant(tool_use), user(tool_result), assistant(done) + assert.strictEqual(messages.length, 4); + const toolResultWrapper = messages[2] as Record; + const toolResults = toolResultWrapper['content'] as Array>; + assert.strictEqual(toolResults[0]['type'], 'tool_result'); + assert.strictEqual(toolResults[0]['is_error'], true); + assert.ok((toolResults[0]['content'] as string).includes('intentional failure')); + }); + + it('does not set is_error on successful handler', async () => { + const registry = new ToolRegistry(); + registry.define({ name: 'ok', description: 'd', input_schema: {} }, () => 'result'); + + const client = makeAnthropicClient([ + { content: [{ type: 'tool_use', id: 'c1', name: 'ok', input: {} }], stop_reason: 'tool_use' }, + { content: [{ type: 'text', text: 'done' }], stop_reason: 'end_turn' }, + ]); + const provider = new AnthropicProvider(registry, 'sys', { client }); + + const messages = [{ role: 'user', content: 'go' }]; + await provider.runLoop(messages); + + const toolResultWrapper = messages[2] as Record; + const toolResults = toolResultWrapper['content'] as Array>; + assert.ok(!('is_error' in toolResults[0]), 'is_error should not be present on success'); + }); +}); + +describe('OpenAIProvider handler errors', () => { + it('catches handler exception, does not crash', async () => { + const registry = new ToolRegistry(); + registry.define({ name: 'boom', description: 'd', input_schema: {} }, () => { + throw new Error('openai error test'); + }); + + const client = makeOpenAIClient([ + { + choices: [{ + message: { content: null, tool_calls: [{ id: 'tc1', function: { name: 'boom', arguments: '{}' } }] }, + finish_reason: 'tool_calls', + }], + }, + { choices: [{ message: { content: 'done' }, finish_reason: 'stop' }] }, + ]); + const provider = new OpenAIProvider(registry, 'sys', { client }); + + const messages = [{ role: 'user', content: 'go' }]; + await provider.runLoop(messages); + + // Tool result message should contain the error string. + const toolMsg = messages[2] as Record; + assert.ok((toolMsg['content'] as string).includes('openai error test')); + }); +}); + +// ── Integration tests (skipped unless RUN_INTEGRATION_TESTS is set) ──────────── + +describe('integration', function () { + before(function () { + if (!process.env['RUN_INTEGRATION_TESTS']) this.skip(); + }); + + function makeRecordRegistry(): { registry: ToolRegistry; collected: string[] } { + const collected: string[] = []; + const registry = new ToolRegistry(); + registry.define( + { + name: 'record', + description: 'Record a value', + input_schema: { + type: 'object', + properties: { value: { type: 'string' } }, + required: ['value'], + }, + }, + (inp) => { + collected.push(inp['value'] as string); + return 'recorded'; + } + ); + return { registry, collected }; + } + + it('Anthropic: dispatches record tool with real API call', async function () { + this.timeout(30000); + assert.ok(process.env['ANTHROPIC_API_KEY'], 'ANTHROPIC_API_KEY required'); + const { registry, collected } = makeRecordRegistry(); + await runToolLoop({ + provider: 'anthropic', + system: "You must call record() exactly once with value='hello'.", + prompt: "Please call the record tool with value='hello'.", + tools: registry, + }); + assert.ok(collected.includes('hello'), `expected 'hello' in ${JSON.stringify(collected)}`); + }); + + it('OpenAI: dispatches record tool with real API call', async function () { + this.timeout(30000); + assert.ok(process.env['OPENAI_API_KEY'], 'OPENAI_API_KEY required'); + const { registry, collected } = makeRecordRegistry(); + await runToolLoop({ + provider: 'openai', + system: "You must call record() exactly once with value='hello'.", + prompt: "Please call the record tool with value='hello'.", + tools: registry, + }); + assert.ok(collected.includes('hello'), `expected 'hello' in ${JSON.stringify(collected)}`); + }); +}); diff --git a/packages/tool-registry/src/providers.ts b/packages/tool-registry/src/providers.ts new file mode 100644 index 000000000..8a358a04a --- /dev/null +++ b/packages/tool-registry/src/providers.ts @@ -0,0 +1,244 @@ +/** + * LLM provider implementations for ToolRegistry. + * + * Provides {@link AnthropicProvider} and {@link OpenAIProvider}, each + * implementing a complete multi-turn tool-calling loop. The top-level + * {@link runToolLoop} function constructs the appropriate provider and runs + * the loop. + * + * Both providers follow the same protocol: + * 1. Send messages + tool definitions to the model. + * 2. If the model returns tool_use / function blocks, dispatch each tool call + * via {@link ToolRegistry.dispatch} and append the result. + * 3. Repeat until the model stops requesting tool calls. + */ + +import type { ToolRegistry } from './registry'; + +/** A single message in the conversation history. */ +export type Message = Record; + +/** Options for {@link runToolLoop}. */ +export interface RunToolLoopOptions { + /** LLM provider: "anthropic" or "openai". */ + provider: string; + /** System prompt. */ + system: string; + /** Initial user message. */ + prompt: string; + /** Registered tool handlers. */ + tools: ToolRegistry; + /** Existing message history to continue from. Prompt is ignored if provided. */ + messages?: Message[]; + /** Model name override. */ + model?: string; + /** Pre-constructed LLM client (useful in tests). */ + client?: unknown; +} + +/** + * Anthropic multi-turn tool-calling loop. + * + * @experimental Provider classes are internal implementation details. + * Prefer {@link runToolLoop} or {@link AgenticSession.runToolLoop} as the + * public entry point. + */ +export class AnthropicProvider { + private readonly _client: { + messages: { + create: (opts: Record) => Promise>; + }; + }; + + constructor( + private readonly _registry: ToolRegistry, + private readonly _system: string, + options: { model?: string; client?: unknown } = {} + ) { + this._model = options.model ?? 'claude-sonnet-4-6'; + if (options.client != null) { + this._client = options.client as typeof this._client; + } else { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const Anthropic = require('@anthropic-ai/sdk'); + this._client = new Anthropic.default({ apiKey: process.env['ANTHROPIC_API_KEY'] }); + } + } + + private readonly _model: string; + + /** + * Execute one turn of the conversation. + * + * Appends the assistant response (and any tool results) to `messages` in-place. + * + * @returns `true` when the loop should stop, `false` to continue. + */ + async runTurn(messages: Message[]): Promise { + const response = (await this._client.messages.create({ + model: this._model, + max_tokens: 4096, + system: this._system, + tools: this._registry.toAnthropic(), + messages, + })) as { + content: Array<{ type: string; id?: string; name?: string; input?: Record }>; + stop_reason: string; + }; + + // Convert content blocks to plain objects (Anthropic SDK returns Pydantic-style objects) + const assistantContent = response.content.map((block) => ({ ...block })); + messages.push({ role: 'assistant', content: assistantContent }); + + const toolCalls = assistantContent.filter((b) => b['type'] === 'tool_use'); + if (toolCalls.length === 0 || response.stop_reason === 'end_turn') { + return true; + } + + const toolResults = await Promise.all(toolCalls.map(async (call) => { + let content: string; + let isError = false; + try { + content = await this._registry.dispatch(call['name'] as string, (call['input'] ?? {}) as Record); + } catch (e) { + content = `error: ${e instanceof Error ? e.message : String(e)}`; + isError = true; + } + const result: Record = { type: 'tool_result', tool_use_id: call['id'], content }; + if (isError) result['is_error'] = true; + return result; + })); + messages.push({ role: 'user', content: toolResults }); + return false; + } + + /** Run turns until the model stops requesting tools. */ + async runLoop(messages: Message[]): Promise { + while (!(await this.runTurn(messages))) { + // continue + } + } +} + +/** + * OpenAI multi-turn function-calling loop. + * + * @experimental Provider classes are internal implementation details. + * Prefer {@link runToolLoop} as the public entry point. + */ +export class OpenAIProvider { + private readonly _client: { + chat: { + completions: { + create: (opts: Record) => Promise>; + }; + }; + }; + + constructor( + private readonly _registry: ToolRegistry, + private readonly _system: string, + options: { model?: string; client?: unknown } = {} + ) { + this._model = options.model ?? 'gpt-4o'; + if (options.client != null) { + this._client = options.client as typeof this._client; + } else { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const OpenAI = require('openai'); + this._client = new OpenAI.default({ apiKey: process.env['OPENAI_API_KEY'] }); + } + } + + private readonly _model: string; + + /** + * Execute one turn of the conversation. + * + * @returns `true` when the loop should stop, `false` to continue. + */ + async runTurn(messages: Message[]): Promise { + const fullMessages = [{ role: 'system', content: this._system }, ...messages]; + const response = (await this._client.chat.completions.create({ + model: this._model, + tools: this._registry.toOpenAI(), + messages: fullMessages, + })) as { + choices: Array<{ + message: { + content: string | null; + tool_calls?: Array<{ id: string; function: { name: string; arguments: string } }>; + }; + finish_reason: string; + }>; + }; + + const choice = response.choices[0]; + if (choice == null) return true; + + const msg: Message = { role: 'assistant', content: choice.message.content }; + if (choice.message.tool_calls != null) { + msg['tool_calls'] = choice.message.tool_calls.map((tc) => ({ + id: tc.id, + type: 'function', + function: { name: tc.function.name, arguments: tc.function.arguments }, + })); + } + messages.push(msg); + + if (!choice.message.tool_calls?.length || ['stop', 'length'].includes(choice.finish_reason)) { + return true; + } + + for (const tc of choice.message.tool_calls) { + const args = JSON.parse(tc.function.arguments || '{}') as Record; + let result: string; + try { + result = await this._registry.dispatch(tc.function.name, args); + } catch (e) { + result = `error: ${e instanceof Error ? e.message : String(e)}`; + } + messages.push({ role: 'tool', tool_call_id: tc.id, content: result }); + } + return false; + } + + /** Run turns until the model stops calling functions. */ + async runLoop(messages: Message[]): Promise { + while (!(await this.runTurn(messages))) { + // continue + } + } +} + +/** + * Run a complete multi-turn LLM tool-calling loop. + * + * This is the primary entry point for simple (non-resumable) tool loops. + * For resumable agentic sessions with crash-safe heartbeating, use + * {@link AgenticSession} via {@link agenticSession}. + * + * @returns The final messages list with the complete conversation history. + * @throws {Error} If `provider` is not "anthropic" or "openai". + */ +export async function runToolLoop(options: RunToolLoopOptions): Promise { + const { provider, system, prompt, tools, model, client } = options; + const messages: Message[] = options.messages ?? [{ role: 'user', content: prompt }]; + if (messages.length === 0) { + messages.push({ role: 'user', content: prompt }); + } + + const providerOpts = { model, client }; + + if (provider === 'anthropic') { + const p = new AnthropicProvider(tools, system, providerOpts); + await p.runLoop(messages); + } else if (provider === 'openai') { + const p = new OpenAIProvider(tools, system, providerOpts); + await p.runLoop(messages); + } else { + throw new Error(`Unknown provider ${JSON.stringify(provider)}. Use 'anthropic' or 'openai'.`); + } + + return messages; +} diff --git a/packages/tool-registry/src/registry.test.ts b/packages/tool-registry/src/registry.test.ts new file mode 100644 index 000000000..68aa5803b --- /dev/null +++ b/packages/tool-registry/src/registry.test.ts @@ -0,0 +1,82 @@ +/** + * Unit tests for ToolRegistry. + * Runs without an API key or Temporal server. + */ + +import assert from 'assert'; +import { ToolRegistry } from './registry'; + +describe('ToolRegistry', () => { + it('dispatches to the registered handler', async () => { + const registry = new ToolRegistry(); + registry.define( + { name: 'greet', description: 'd', input_schema: {} }, + (inp) => `hello ${inp['name']}` + ); + assert.strictEqual(await registry.dispatch('greet', { name: 'world' }), 'hello world'); + }); + + it('rejects for unknown tool names', async () => { + const registry = new ToolRegistry(); + await assert.rejects(() => registry.dispatch('missing', {}), /Unknown tool/); + }); + + it('dispatches async handlers and awaits the result', async () => { + const registry = new ToolRegistry(); + registry.define( + { name: 'async_greet', description: 'd', input_schema: {} }, + async (inp) => `async hello ${inp['name']}` + ); + assert.strictEqual(await registry.dispatch('async_greet', { name: 'world' }), 'async hello world'); + }); + + it('exports definitions in OpenAI format', () => { + const registry = new ToolRegistry(); + registry.define( + { + name: 'my_tool', + description: 'does something', + input_schema: { type: 'object', properties: { x: { type: 'string' } } }, + }, + () => 'ok' + ); + const result = registry.toOpenAI(); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].type, 'function'); + assert.strictEqual(result[0].function.name, 'my_tool'); + assert.strictEqual(result[0].function.description, 'does something'); + assert.deepStrictEqual(result[0].function.parameters, { + type: 'object', + properties: { x: { type: 'string' } }, + }); + }); + + it('toAnthropic returns definitions unchanged', () => { + const defn = { name: 't', description: 'd', input_schema: {} }; + const registry = new ToolRegistry(); + registry.define(defn, () => 'ok'); + assert.deepStrictEqual(registry.toAnthropic(), [defn]); + }); + + it('exports multiple tools', () => { + const registry = new ToolRegistry(); + registry.define({ name: 'alpha', description: 'a', input_schema: {} }, () => 'a'); + registry.define({ name: 'beta', description: 'b', input_schema: {} }, () => 'b'); + const result = registry.toOpenAI(); + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].function.name, 'alpha'); + assert.strictEqual(result[1].function.name, 'beta'); + }); + + it('fromMcpTools wraps MCP tool descriptors', async () => { + const mcpTools = [ + { name: 'search', description: 'Search', inputSchema: { type: 'object', properties: {} } }, + { name: 'read', description: 'Read', inputSchema: { type: 'object', properties: {} } }, + ]; + const registry = ToolRegistry.fromMcpTools(mcpTools); + const names = registry.toAnthropic().map((d) => d.name); + assert.deepStrictEqual(names, ['search', 'read']); + // No-op handlers return empty string + assert.strictEqual(await registry.dispatch('search', {}), ''); + }); +}); diff --git a/packages/tool-registry/src/registry.ts b/packages/tool-registry/src/registry.ts new file mode 100644 index 000000000..703ce4766 --- /dev/null +++ b/packages/tool-registry/src/registry.ts @@ -0,0 +1,141 @@ +/** + * ToolRegistry — define LLM tools once, export provider-specific schemas. + * + * A {@link ToolRegistry} stores a mapping from tool name to its definition (in + * Anthropic's `tool_use` format) and a callable handler. The same registry can + * be converted to Anthropic-format or OpenAI-format schemas for use with either + * provider's client library, and dispatches incoming tool calls to the registered + * handler. + * + * @example + * ```typescript + * const tools = new ToolRegistry(); + * tools.define( + * { name: 'flag_issue', description: '...', input_schema: { type: 'object', properties: { ... } } }, + * (inp) => { issues.push(inp.description); return 'recorded'; } + * ); + * + * // Use with Anthropic + * client.messages.create({ tools: tools.toAnthropic(), ... }); + * + * // Use with OpenAI + * client.chat.completions.create({ tools: tools.toOpenAI(), ... }); + * + * // Dispatch a tool call returned by the model + * const result = tools.dispatch(toolName, toolInput); + * ``` + */ + +/** Anthropic-format tool definition. */ +export interface ToolDefinition { + name: string; + description: string; + input_schema: Record; +} + +/** Handler function type: receives parsed tool input, returns a string result or a Promise of one. */ +export type ToolHandler = (input: Record) => string | Promise; + +/** MCP-compatible tool descriptor (subset of mcp.Tool). */ +export interface McpToolDescriptor { + name: string; + description?: string; + inputSchema?: Record; +} + +/** + * Registry mapping tool names to definitions and handlers. + * + * Tools are registered in Anthropic's `tool_use` JSON format. The registry + * can export the same tools for Anthropic or OpenAI providers, and dispatch + * incoming tool calls to the appropriate handler. + */ +export class ToolRegistry { + private readonly _definitions: ToolDefinition[] = []; + private readonly _handlers = new Map(); + + // ── Registration ──────────────────────────────────────────────────────────── + + /** + * Register a tool definition and its handler. + * + * @param definition - Tool definition in Anthropic `tool_use` format. + * @param handler - Function called when the model invokes this tool. + */ + define(definition: ToolDefinition, handler: ToolHandler): void { + this._definitions.push(definition); + this._handlers.set(definition.name, handler); + } + + /** + * Build a ToolRegistry from a list of MCP tool descriptors. + * + * Each MCP tool is wrapped with a no-op handler (returning an empty string). + * Replace handlers via {@link define} as needed. + * + * @param tools - MCP tool objects with `name`, `description`, and `inputSchema`. + */ + static fromMcpTools(tools: McpToolDescriptor[]): ToolRegistry { + const registry = new ToolRegistry(); + for (const tool of tools) { + registry.define( + { + name: tool.name, + description: tool.description ?? '', + input_schema: tool.inputSchema ?? { type: 'object', properties: {} }, + }, + () => '' + ); + } + return registry; + } + + // ── Schema export ──────────────────────────────────────────────────────────── + + /** + * Return tool definitions in Anthropic `tool_use` format. + * + * Definitions are returned exactly as registered — no conversion needed. + */ + toAnthropic(): ToolDefinition[] { + return [...this._definitions]; + } + + /** + * Return tool definitions in OpenAI function-calling format. + * + * Converts each Anthropic-format definition to the + * `{"type": "function", "function": {...}}` shape, mapping `input_schema` + * to `parameters`. + */ + toOpenAI(): Array<{ type: 'function'; function: { name: string; description: string; parameters: Record } }> { + return this._definitions.map((defn) => ({ + type: 'function' as const, + function: { + name: defn.name, + description: defn.description, + parameters: defn.input_schema, + }, + })); + } + + // ── Dispatch ───────────────────────────────────────────────────────────────── + + /** + * Call the handler registered for `name` with `input`. + * + * Supports both synchronous and asynchronous handlers. + * + * @param name - Tool name as returned by the model. + * @param input - Parsed tool input as a plain object. + * @returns Promise resolving to the string result from the handler. + * @throws Error if no handler is registered for `name`. + */ + async dispatch(name: string, input: Record): Promise { + const handler = this._handlers.get(name); + if (handler === undefined) { + throw new Error(`Unknown tool: ${JSON.stringify(name)}`); + } + return handler(input); + } +} diff --git a/packages/tool-registry/src/session.test.ts b/packages/tool-registry/src/session.test.ts new file mode 100644 index 000000000..b27ad8351 --- /dev/null +++ b/packages/tool-registry/src/session.test.ts @@ -0,0 +1,300 @@ +/** + * Unit tests for AgenticSession and agenticSession. + * Runs without an API key — heartbeat and activityInfo are mocked. + */ + +import assert from 'assert'; +import { AgenticSession, agenticSession } from './session'; +import { ToolRegistry } from './registry'; + +// ── Mock helpers ─────────────────────────────────────────────────────────────── + +/** + * Patch activityInfo and heartbeat for a test. + * Returns the mocked heartbeat call list. + */ +function mockActivity( + heartbeatDetails: unknown[] = [] +): { heartbeatCalls: string[]; restore: () => void } { + const heartbeatCalls: string[] = []; + const activityModule = require('@temporalio/activity'); + + const origInfo = activityModule.activityInfo; + const origHeartbeat = activityModule.heartbeat; + + activityModule.activityInfo = () => ({ heartbeatDetails }); + activityModule.heartbeat = (s: string) => heartbeatCalls.push(s); + + return { + heartbeatCalls, + restore: () => { + activityModule.activityInfo = origInfo; + activityModule.heartbeat = origHeartbeat; + }, + }; +} + +/** + * Build a mock Anthropic client returning scripted responses. + * + * @param responses - list of bools: true=done (end_turn, no tools), false=continue (tool_use) + */ +function makeMockAnthropicClient(responses: boolean[], toolName = 'noop') { + let idx = 0; + return { + messages: { + create: async (_opts: unknown) => { + const done = idx < responses.length ? (responses[idx++] ?? true) : true; + if (done) { + return { content: [{ type: 'text', text: 'done' }], stop_reason: 'end_turn' }; + } + return { + content: [{ type: 'tool_use', id: 'tid', name: toolName, input: {} }], + stop_reason: 'tool_use', + }; + }, + }, + }; +} + +// ── AgenticSession constructor tests ────────────────────────────────────────── + +describe('AgenticSession', () => { + it('initializes with empty messages and issues by default', () => { + const session = new AgenticSession(); + assert.deepStrictEqual(session.messages, []); + assert.deepStrictEqual(session.results, []); + }); + + it('restores messages and issues from saved state', () => { + const state = { + messages: [{ role: 'user', content: 'analyze this' }], + results: [{ type: 'missing', symbol: 'patched', description: 'removed' }], + }; + const session = new AgenticSession(state); + assert.deepStrictEqual(session.messages, state.messages); + assert.strictEqual(session.results.length, 1); + assert.strictEqual((session.results[0] as Record)['type'], 'missing'); + }); +}); + +// ── AgenticSession._checkpoint tests ────────────────────────────────────────── + +describe('AgenticSession._checkpoint', () => { + it('heartbeats JSON with messages and issues', () => { + const { heartbeatCalls, restore } = mockActivity(); + try { + const session = new AgenticSession({ + messages: [{ role: 'user', content: 'hi' }], + results: [{ type: 'deprecated', symbol: 'old', description: 'use new' }], + }); + session._checkpoint(); + } finally { + restore(); + } + + assert.strictEqual(heartbeatCalls.length, 1); + const payload = JSON.parse(heartbeatCalls[0]); + assert.strictEqual(payload.messages.length, 1); + assert.strictEqual(payload.results.length, 1); + assert.strictEqual(payload.results[0].symbol, 'old'); + }); + + it('heartbeats valid empty JSON when state is empty', () => { + const { heartbeatCalls, restore } = mockActivity(); + try { + new AgenticSession()._checkpoint(); + } finally { + restore(); + } + + const payload = JSON.parse(heartbeatCalls[0]); + assert.deepStrictEqual(payload, { version: 1, messages: [], results: [] }); + }); +}); + +// ── AgenticSession.runToolLoop tests ────────────────────────────────────────── + +describe('AgenticSession.runToolLoop', () => { + it('adds prompt as first message on fresh start', async () => { + const { restore } = mockActivity(); + const client = makeMockAnthropicClient([true]); + + try { + const session = new AgenticSession(); + await session.runToolLoop({ + registry: new ToolRegistry(), + provider: 'anthropic', + system: 'sys', + prompt: 'my prompt', + client, + }); + assert.strictEqual((session.messages[0] as Record)['content'], 'my prompt'); + } finally { + restore(); + } + }); + + it('does not add prompt when messages already present (resume)', async () => { + const existing = [ + { role: 'user', content: 'original prompt' }, + { role: 'assistant', content: [{ type: 'text', text: 'ok' }] }, + ]; + const { restore } = mockActivity(); + const client = makeMockAnthropicClient([true]); + + try { + const session = new AgenticSession({ messages: [...existing] }); + await session.runToolLoop({ + registry: new ToolRegistry(), + provider: 'anthropic', + system: 'sys', + prompt: 'ignored', + client, + }); + assert.deepStrictEqual(session.messages.slice(0, 2), existing); + } finally { + restore(); + } + }); + + it('calls _checkpoint once per turn', async () => { + const registry = new ToolRegistry(); + registry.define({ name: 'noop', description: 'd', input_schema: {} }, () => 'ok'); + + const { restore } = mockActivity(); + // 3 turns: not-done, not-done, done + const client = makeMockAnthropicClient([false, false, true], 'noop'); + let checkpointCount = 0; + + try { + const session = new AgenticSession({ messages: [{ role: 'user', content: 'go' }] }); + const orig = session._checkpoint.bind(session); + session._checkpoint = () => { checkpointCount++; orig(); }; + + await session.runToolLoop({ + registry, + provider: 'anthropic', + system: 's', + prompt: 'ignored', + client, + }); + } finally { + restore(); + } + + assert.strictEqual(checkpointCount, 3); + }); + + it('throws for unknown provider', async () => { + const { restore } = mockActivity(); + try { + const session = new AgenticSession({ messages: [{ role: 'user', content: 'x' }] }); + await assert.rejects( + () => session.runToolLoop({ + registry: new ToolRegistry(), + provider: 'gemini', + system: 's', + prompt: 'p', + }), + /gemini/ + ); + } finally { + restore(); + } + }); +}); + +// ── Checkpoint round-trip test (T6) ────────────────────────────────────────── + +describe('AgenticSession checkpoint round-trip', () => { + it('preserves tool_calls through JSON serialize/deserialize', () => { + const { heartbeatCalls, restore } = mockActivity(); + try { + const toolCallsInMemory = [ + { + id: 'call_abc', + type: 'function', + function: { name: 'my_tool', arguments: '{"x":1}' }, + }, + ]; + const session = new AgenticSession({ + messages: [{ role: 'assistant', tool_calls: toolCallsInMemory }], + results: [{ type: 'smell', file: 'foo.ts' }], + }); + session._checkpoint(); + } finally { + restore(); + } + + assert.strictEqual(heartbeatCalls.length, 1); + const restored = JSON.parse(heartbeatCalls[0]); + + assert.strictEqual(restored.messages[0].role, 'assistant'); + const toolCallsRestored = restored.messages[0].tool_calls as Record[]; + assert.strictEqual(toolCallsRestored.length, 1); + assert.strictEqual(toolCallsRestored[0].id, 'call_abc'); + assert.strictEqual( + (toolCallsRestored[0].function as Record).name, + 'my_tool' + ); + + assert.strictEqual(restored.results[0].type, 'smell'); + assert.strictEqual(restored.results[0].file, 'foo.ts'); + }); +}); + +// ── agenticSession function tests ───────────────────────────────────────────── + +describe('agenticSession', () => { + it('starts fresh with no heartbeat state', async () => { + const { restore } = mockActivity([]); + try { + await agenticSession(async (session) => { + assert.deepStrictEqual(session.messages, []); + assert.deepStrictEqual(session.results, []); + }); + } finally { + restore(); + } + }); + + it('restores from JSON checkpoint in heartbeatDetails', async () => { + const saved = JSON.stringify({ + messages: [{ role: 'user', content: 'test' }], + results: [{ type: 'deprecated', symbol: 'old_api', description: 'use new_api' }], + }); + const { restore } = mockActivity([saved]); + try { + await agenticSession(async (session) => { + assert.strictEqual(session.messages.length, 1); + assert.strictEqual(session.results.length, 1); + assert.strictEqual((session.results[0] as Record)['symbol'], 'old_api'); + }); + } finally { + restore(); + } + }); + + it('starts fresh on corrupted checkpoint', async () => { + const { restore } = mockActivity(['not valid json{{']); + try { + await agenticSession(async (session) => { + assert.deepStrictEqual(session.messages, []); + assert.deepStrictEqual(session.results, []); + }); + } finally { + restore(); + } + }); + + it('returns the callback result', async () => { + const { restore } = mockActivity([]); + try { + const result = await agenticSession(async (_session) => 'my result'); + assert.strictEqual(result, 'my result'); + } finally { + restore(); + } + }); +}); diff --git a/packages/tool-registry/src/session.ts b/packages/tool-registry/src/session.ts new file mode 100644 index 000000000..c66e5a668 --- /dev/null +++ b/packages/tool-registry/src/session.ts @@ -0,0 +1,208 @@ +/** + * agenticSession — durable multi-turn LLM activity with heartbeat checkpointing. + * + * Provides {@link agenticSession}, a function that saves conversation state via + * {@link heartbeat} from `@temporalio/activity` after each LLM turn. On activity + * retry, the session is automatically restored from the last checkpoint so the + * conversation resumes mid-turn rather than restarting from the beginning. + * + * This builds on standard Temporal APIs — `activity.heartbeat()` and + * `Context.current().info.heartbeatDetails` — and adds conversation-specific + * serialization and restore logic as a reusable primitive. + * + * TypeScript uses a callback pattern rather than Python's `async with` because + * TypeScript lacks a direct equivalent of async context managers for yielding + * mutable objects. The behaviour is identical: + * + * ``` + * Python: async with agentic_session() as session: ... + * TypeScript: await agenticSession(async (session) => { ... }) + * ``` + * + * @example + * ```typescript + * import { activityDefn } from '@temporalio/activity'; + * import { ToolRegistry, agenticSession } from '@temporalio/tool-registry'; + * + * export async function analyze(prompt: string): Promise { + * let results: object[] = []; + * await agenticSession(async (session) => { + * results = session.results; + * const tools = new ToolRegistry(); + * tools.define( + * { name: 'flag', description: '...', input_schema: { ... } }, + * (inp) => { session.results.push(inp); return 'recorded'; } + * ); + * await session.runToolLoop({ + * registry: tools, + * provider: 'anthropic', + * system: '...', + * prompt, + * }); + * }); + * return results; + * } + * ``` + */ + +import { activityInfo, heartbeat, ApplicationFailure } from '@temporalio/activity'; +import type { ToolRegistry } from './registry'; + +/** Saved checkpoint state serialized by {@link AgenticSession._checkpoint}. */ +interface CheckpointState { + version?: number; + messages: unknown[]; + results: unknown[]; +} + +/** Options for {@link AgenticSession.runToolLoop}. */ +export interface RunToolLoopOptions { + /** Tool registry with handlers for all tools the model may call. */ + registry: ToolRegistry; + /** LLM provider: `"anthropic"` or `"openai"`. */ + provider: string; + /** System prompt. */ + system: string; + /** Initial user message. Ignored on resume (messages already set). */ + prompt: string; + /** Model name override. Uses provider default if omitted. */ + model?: string; + /** Pre-constructed LLM client. Useful in tests to avoid API key requirements. */ + client?: unknown; +} + +/** + * Holds conversation state across a multi-turn LLM tool-use loop. + * + * Instances are created by {@link agenticSession} and should not normally be + * constructed directly. On activity retry, {@link agenticSession} deserializes + * the saved state from `activityInfo().heartbeatDetails` and passes it to the + * constructor. + */ +export class AgenticSession { + /** Full conversation history in provider-neutral format. */ + messages: unknown[]; + /** Application-level results accumulated during the session. */ + results: unknown[]; + + constructor(state: Partial = {}) { + this.messages = state.messages ?? []; + this.results = state.results ?? []; + } + + /** + * Run the agentic tool-use loop to completion. + * + * If {@link messages} is empty (fresh start), adds `prompt` as the first + * user message. Otherwise resumes from the existing conversation state + * (retry case). + * + * Checkpoints via {@link _checkpoint} before each LLM call. If the activity + * is cancelled due to a heartbeat timeout, the next attempt will restore from + * the last checkpoint. + * + * @throws {Error} If `provider` is not `"anthropic"` or `"openai"`. + */ + async runToolLoop(opts: RunToolLoopOptions): Promise { + const { registry, provider, system, prompt, model, client } = opts; + + if (this.messages.length === 0) { + this.messages = [{ role: 'user', content: prompt }]; + } + + const providerOpts = { model, client }; + + if (provider === 'anthropic') { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { AnthropicProvider } = require('./providers') as typeof import('./providers'); + const p = new AnthropicProvider(registry, system, providerOpts); + while (true) { + this._checkpoint(); + const done = await p.runTurn(this.messages as Array>); + if (done) break; + } + } else if (provider === 'openai') { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { OpenAIProvider } = require('./providers') as typeof import('./providers'); + const p = new OpenAIProvider(registry, system, providerOpts); + while (true) { + this._checkpoint(); + const done = await p.runTurn(this.messages as Array>); + if (done) break; + } + } else { + throw new Error(`Unknown provider ${JSON.stringify(provider)}. Use 'anthropic' or 'openai'.`); + } + } + + /** + * Heartbeat the serialized conversation state for crash-safe resume. + * + * `heartbeat()` from `@temporalio/activity` persists the payload to the + * Temporal server. On retry, `activityInfo().heartbeatDetails[0]` contains + * this JSON. {@link agenticSession} reads it on entry and restores + * messages + issues. + * + * @throws {ApplicationFailure} (non-retryable) If any result is not JSON-serializable. + * @internal + */ + _checkpoint(): void { + for (let i = 0; i < this.results.length; i++) { + try { + JSON.stringify(this.results[i]); + } catch (e) { + throw ApplicationFailure.nonRetryable( + `AgenticSession: results[${i}] is not JSON-serializable: ${e}. ` + + 'Store only plain objects with JSON-serializable values.', + ); + } + } + heartbeat(JSON.stringify({ version: 1, messages: this.messages, results: this.results })); + } +} + +/** + * Run a callback with a durable, checkpointed LLM session. + * + * On entry, restores conversation state (messages + issues) from + * `activityInfo().heartbeatDetails` if this is a retry. The session resumes + * mid-conversation instead of restarting from turn 0. + * + * @param fn - Async callback that receives the session and runs the tool loop. + * @returns The return value of `fn`. + * + * @example + * ```typescript + * const result = await agenticSession(async (session) => { + * await session.runToolLoop({ registry, provider: 'anthropic', system, prompt }); + * return session.results; + * }); + * ``` + */ +export async function agenticSession( + fn: (session: AgenticSession) => Promise +): Promise { + const details = activityInfo().heartbeatDetails as unknown[]; + let saved: Partial = {}; + + if (details && details.length > 0) { + try { + saved = JSON.parse(details[0] as string) as CheckpointState; + const v = saved.version; + if (v === undefined || v === null) { + // eslint-disable-next-line no-console + console.warn('AgenticSession: checkpoint has no version field — may be from an older release'); + } else if (v !== 1) { + // eslint-disable-next-line no-console + console.warn(`AgenticSession: checkpoint version ${v}, expected 1 — starting fresh`); + saved = {} as CheckpointState; + } + } catch (e) { + // eslint-disable-next-line no-console + console.warn(`AgenticSession: failed to decode checkpoint, starting fresh: ${e}`); + } + } + + const session = new AgenticSession(saved); + return fn(session); +} diff --git a/packages/tool-registry/src/testing.test.ts b/packages/tool-registry/src/testing.test.ts new file mode 100644 index 000000000..82b81ac8a --- /dev/null +++ b/packages/tool-registry/src/testing.test.ts @@ -0,0 +1,132 @@ +/** + * Unit tests for testing utilities. + * Runs without an API key or Temporal server. + */ + +import assert from 'assert'; +import { ToolRegistry } from './registry'; +import { + CrashAfterTurns, + FakeToolRegistry, + MockAgenticSession, + MockProvider, + ResponseBuilder, +} from './testing'; + +// ── MockProvider ─────────────────────────────────────────────────────────────── + +describe('MockProvider', () => { + it('dispatches tool calls and runs loop to completion', async () => { + const collected: string[] = []; + const registry = new ToolRegistry(); + registry.define( + { name: 'collect', description: 'd', input_schema: {} }, + (inp) => { + collected.push(inp['value'] as string); + return 'ok'; + } + ); + + const provider = new MockProvider([ + ResponseBuilder.toolCall('collect', { value: 'first' }), + ResponseBuilder.toolCall('collect', { value: 'second' }), + ResponseBuilder.done('all done'), + ]); + const messages = [{ role: 'user', content: 'go' }]; + await provider.runLoop(messages, registry); + + assert.deepStrictEqual(collected, ['first', 'second']); + }); + + it('stops on done response', async () => { + const provider = new MockProvider([ResponseBuilder.done('finished')]); + const messages = [{ role: 'user', content: 'x' }]; + await provider.runLoop(messages); + // user + assistant + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[1]['role'], 'assistant'); + }); + + it('stops cleanly when responses are exhausted', async () => { + const provider = new MockProvider([]); + const messages = [{ role: 'user', content: 'x' }]; + await provider.runLoop(messages); + assert.strictEqual(messages.length, 1); + }); +}); + +// ── FakeToolRegistry ─────────────────────────────────────────────────────────── + +describe('FakeToolRegistry', () => { + it('records all dispatch calls', () => { + const fake = new FakeToolRegistry(); + fake.define({ name: 'greet', description: 'd', input_schema: {} }, () => 'ok'); + + fake.dispatch('greet', { name: 'world' }); + fake.dispatch('greet', { name: 'temporal' }); + + assert.deepStrictEqual(fake.calls, [ + ['greet', { name: 'world' }], + ['greet', { name: 'temporal' }], + ]); + }); + + it('still dispatches to the registered handler', async () => { + const fake = new FakeToolRegistry(); + fake.define({ name: 'echo', description: 'd', input_schema: {} }, (inp) => inp['v'] as string); + assert.strictEqual(await fake.dispatch('echo', { v: 'hello' }), 'hello'); + }); +}); + +// ── CrashAfterTurns ─────────────────────────────────────────────────────────── + +describe('CrashAfterTurns', () => { + it('throws after exactly n turns', async () => { + const crasher = new CrashAfterTurns(1); + const messages = [{ role: 'user', content: 'x' }]; + await crasher.runTurn(messages, new ToolRegistry()); // first turn: fine + await assert.rejects( + () => crasher.runTurn(messages, new ToolRegistry()), + /simulated crash/ + ); + }); + + it('completes n turns before crashing', async () => { + const crasher = new CrashAfterTurns(2); + const messages = [{ role: 'user', content: 'x' }]; + await crasher.runTurn(messages, new ToolRegistry()); + await crasher.runTurn(messages, new ToolRegistry()); + await assert.rejects( + () => crasher.runTurn(messages, new ToolRegistry()), + /simulated crash/ + ); + }); +}); + +// ── MockAgenticSession ──────────────────────────────────────────────────────── + +describe('MockAgenticSession', () => { + it('returns pre-canned results without LLM calls', async () => { + const session = new MockAgenticSession([{ type: 'deprecated', symbol: 'old_fn' }]); + await session.runToolLoop({ + registry: new ToolRegistry(), + provider: 'anthropic', + system: 's', + prompt: 'p', + }); + assert.strictEqual(session.results.length, 1); + assert.strictEqual((session.results[0] as Record)['symbol'], 'old_fn'); + }); + + it('starts empty when no results provided', () => { + const session = new MockAgenticSession(); + assert.deepStrictEqual(session.results, []); + }); + + it('does not mutate the input results array', () => { + const input = [{ v: 1 }]; + const session = new MockAgenticSession(input); + (session.results as unknown[]).push({ v: 2 }); + assert.strictEqual(input.length, 1); + }); +}); diff --git a/packages/tool-registry/src/testing.ts b/packages/tool-registry/src/testing.ts new file mode 100644 index 000000000..c7b4dc992 --- /dev/null +++ b/packages/tool-registry/src/testing.ts @@ -0,0 +1,232 @@ +/** + * Testing utilities for `@temporalio/tool-registry`. + * + * Provides mock objects that allow unit tests to exercise {@link ToolRegistry}, + * {@link AgenticSession}, and {@link runToolLoop} without an API key or a + * running Temporal server. Also provides {@link MockAgenticSession} for testing + * code that uses {@link agenticSession} without any LLM calls. + * + * @example + * ```typescript + * import { ToolRegistry } from '@temporalio/tool-registry'; + * import { MockProvider, ResponseBuilder } from '@temporalio/tool-registry'; + * + * const provider = new MockProvider([ + * ResponseBuilder.toolCall('flag_issue', { description: 'wrong' }), + * ResponseBuilder.done('Analysis complete.'), + * ]); + * + * const messages = [{ role: 'user', content: 'analyze this' }]; + * await provider.runLoop(messages, registry); + * ``` + */ + +import { ToolRegistry } from './registry'; +import type { Message } from './providers'; +import type { RunToolLoopOptions } from './session'; + +/** Internal shape of a scripted mock response produced by {@link ResponseBuilder}. */ +export interface MockResponse { + /** Whether this response ends the loop. */ + _mockStop: boolean; + /** Content blocks returned as the assistant message. */ + content: Array>; +} + +/** + * Factories for scripting mock LLM turn sequences. + * + * @example + * ```typescript + * const provider = new MockProvider([ + * ResponseBuilder.toolCall('greet', { name: 'world' }), + * ResponseBuilder.done('said hello'), + * ]); + * ``` + */ +export class ResponseBuilder { + /** + * Create a mock assistant message that makes a single tool call. + * + * @param toolName - Name of the tool to call. + * @param toolInput - Input passed to the tool. + * @param callId - Optional tool-use ID. Auto-generated if omitted. + */ + static toolCall( + toolName: string, + toolInput: Record, + callId?: string + ): MockResponse { + const id = callId ?? `test_${Math.random().toString(16).slice(2, 10)}`; + return { + _mockStop: false, + content: [{ type: 'tool_use', id, name: toolName, input: toolInput }], + }; + } + + /** + * Create a mock assistant message that ends the loop. + * + * @param text - Assistant text response. + */ + static done(text = 'Done.'): MockResponse { + return { + _mockStop: true, + content: [{ type: 'text', text }], + }; + } +} + +/** + * LLM provider that returns pre-scripted responses without API calls. + * + * Responses are consumed in order. Once exhausted the loop stops. + * + * @example + * ```typescript + * const provider = new MockProvider([ + * ResponseBuilder.toolCall('greet', { name: 'world' }), + * ResponseBuilder.done('said hello'), + * ]); + * const messages = [{ role: 'user', content: 'greet world' }]; + * await provider.runLoop(messages, registry); + * ``` + */ +export class MockProvider { + private _index = 0; + + constructor(private readonly _responses: MockResponse[]) {} + + /** + * Execute one scripted turn. + * + * @returns `true` when done, `false` to continue. + */ + async runTurn(messages: Message[], registry: ToolRegistry): Promise { + if (this._index >= this._responses.length) return true; + + const response = this._responses[this._index++]; + const { _mockStop: stop, content } = response; + + messages.push({ role: 'assistant', content }); + + if (!stop) { + const toolResults: Record[] = []; + for (const block of content) { + if (block['type'] === 'tool_use') { + const result = registry.dispatch( + block['name'] as string, + (block['input'] ?? {}) as Record + ); + toolResults.push({ + type: 'tool_result', + tool_use_id: block['id'], + content: result, + }); + } + } + if (toolResults.length > 0) { + messages.push({ role: 'user', content: toolResults }); + } + } + + return stop; + } + + /** Run all scripted turns until exhausted or a done response is reached. */ + async runLoop(messages: Message[], registry?: ToolRegistry): Promise { + const reg = registry ?? new ToolRegistry(); + while (!(await this.runTurn(messages, reg))) { + // continue + } + } +} + +/** + * A {@link ToolRegistry} that records all dispatch calls. + * + * Useful for asserting which tools were called and with what inputs. + * + * @example + * ```typescript + * const fake = new FakeToolRegistry(); + * fake.define({ name: 'greet', description: 'd', input_schema: {} }, () => 'ok'); + * fake.dispatch('greet', { name: 'world' }); + * assert.deepStrictEqual(fake.calls, [['greet', { name: 'world' }]]); + * ``` + */ +export class FakeToolRegistry extends ToolRegistry { + /** All recorded dispatch calls as `[name, input]` tuples. */ + readonly calls: Array<[string, Record]> = []; + + async dispatch(name: string, input: Record): Promise { + this.calls.push([name, input]); + return super.dispatch(name, input); + } +} + +/** + * A pre-canned session that returns fixed results without LLM calls. + * + * Use this to test code that calls {@link agenticSession} and inspects + * `session.results` without needing an API key or a running server. + * + * @example + * ```typescript + * const session = new MockAgenticSession([{ type: 'deprecated', symbol: 'old_fn' }]); + * await session.runToolLoop({ registry, provider: 'anthropic', system: 's', prompt: 'p' }); + * assert.strictEqual(session.results.length, 1); + * ``` + */ +export class MockAgenticSession { + messages: unknown[] = []; + results: unknown[]; + + constructor(results: unknown[] = []) { + this.results = [...results]; + } + + /** No-op — does not call any LLM. */ + async runToolLoop(_opts: RunToolLoopOptions): Promise { + // results already set by constructor + } + + /** No-op — does not call heartbeat() in tests. */ + _checkpoint(): void { + // nothing + } +} + +/** + * Simulates an activity crash after `n` turns. + * + * Use in integration tests to verify that {@link agenticSession} correctly + * resumes from a checkpoint after a crash. + * + * @example + * ```typescript + * const crasher = new CrashAfterTurns(2); + * // First two turns complete; third throws. + * ``` + */ +export class CrashAfterTurns { + private _count = 0; + + constructor(private readonly _n: number) {} + + async runTurn(messages: Message[], _registry: ToolRegistry): Promise { + this._count++; + if (this._count > this._n) { + throw new Error(`CrashAfterTurns: simulated crash after ${this._n} turns`); + } + messages.push({ role: 'assistant', content: [{ type: 'text', text: '...' }] }); + return this._count >= this._n; + } + + async runLoop(messages: Message[], registry?: ToolRegistry): Promise { + const reg = registry ?? new ToolRegistry(); + while (!(await this.runTurn(messages, reg))) { + // continue + } + } +} diff --git a/packages/tool-registry/tsconfig.json b/packages/tool-registry/tsconfig.json new file mode 100644 index 000000000..9605f6137 --- /dev/null +++ b/packages/tool-registry/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./lib", + "rootDir": "./src", + "declaration": true, + "declarationMap": true + }, + "references": [ + { "path": "../activity" }, + { "path": "../common" }, + { "path": "../plugin" } + ], + "include": ["./src/**/*.ts"], + "exclude": ["./src/**/*.test.ts"] +} diff --git a/packages/tool-registry/tsconfig.test.json b/packages/tool-registry/tsconfig.test.json new file mode 100644 index 000000000..4f17a2939 --- /dev/null +++ b/packages/tool-registry/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["mocha", "node"], + "noUncheckedIndexedAccess": false + }, + "include": ["./src/**/*.ts"], + "exclude": [] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60ddfc954..fb8fd3940 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -697,6 +697,40 @@ importers: specifier: ^3.0.0 version: 3.0.0 + packages/tool-registry: + dependencies: + '@temporalio/activity': + specifier: workspace:* + version: link:../activity + '@temporalio/common': + specifier: workspace:* + version: link:../common + '@temporalio/plugin': + specifier: workspace:* + version: link:../plugin + openai: + specifier: '>=4.0.0' + version: 6.33.0(zod@3.25.76) + devDependencies: + '@anthropic-ai/sdk': + specifier: ^0.40.0 + version: 0.40.1(encoding@0.1.13) + '@types/mocha': + specifier: ^10.0.10 + version: 10.0.10 + '@types/node': + specifier: ^20.10.8 + version: 20.10.8 + mocha: + specifier: ^10.0.0 + version: 10.8.2 + ts-node: + specifier: ^10.9.0 + version: 10.9.2(@swc/core@1.3.102)(@types/node@20.10.8)(typescript@5.9.3) + typescript: + specifier: ^5.0.0 + version: 5.9.3 + packages/worker: dependencies: '@grpc/grpc-js': @@ -826,6 +860,9 @@ packages: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} + '@anthropic-ai/sdk@0.40.1': + resolution: {integrity: sha512-DJMWm8lTEM9Lk/MSFL+V+ugF7jKOn0M2Ujvb5fN8r2nY14aHbGPZ1k6sgjL+tpJ3VuOGJNG+4R83jEpOuYPv8w==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -909,6 +946,10 @@ packages: resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} engines: {node: '>=6.9.0'} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@cypress/request@3.0.9': resolution: {integrity: sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==} engines: {node: '>= 6'} @@ -1178,12 +1219,12 @@ packages: '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - '@jridgewell/trace-mapping@0.3.20': - resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} @@ -1474,6 +1515,18 @@ packages: '@ts-morph/common@0.12.3': resolution: {integrity: sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==} + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tsconfig/node20@20.1.9': resolution: {integrity: sha512-IjlTv1RsvnPtUcjTqtVsZExKVq+KQx4g5pCP5tI7rAs6Xesl2qFwSz/tPDBC4JajkL/MlezBu3gPUwqRHl+RIg==} @@ -1531,12 +1584,18 @@ packages: '@types/mdurl@1.0.5': resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} + '@types/mocha@10.0.10': + resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} + '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} '@types/node-fetch@2.6.10': resolution: {integrity: sha512-PPpPK6F9ALFTn59Ka3BaL+qGuipRfxNE8qVgkp0bVixeiR2c2/L+IVOiBdu9JhhT22sWnQEp6YyHGI2b2+CMcA==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@20.10.8': resolution: {integrity: sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==} @@ -1868,11 +1927,6 @@ packages: resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==} engines: {node: '>=0.4.0'} - acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -1882,6 +1936,10 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + aggregate-error@4.0.1: resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} engines: {node: '>=12'} @@ -1922,6 +1980,10 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1946,6 +2008,9 @@ packages: resolution: {integrity: sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA==} engines: {node: '>=8'} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -2098,6 +2163,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + browserify-zlib@0.1.4: resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} @@ -2164,6 +2232,10 @@ packages: resolution: {integrity: sha512-aBMbD1Xxay75ViYezwT40aQONfr+pSXTHwNKvIXhXD6+LY3F1dLIcceoC5OZKBVHbXcysz1hL9D2w0JJIMXpUw==} engines: {node: '>=12.20'} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + caniuse-lite@1.0.30001620: resolution: {integrity: sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==} @@ -2239,6 +2311,9 @@ packages: peerDependencies: typanion: '*' + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2345,6 +2420,9 @@ packages: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2410,6 +2488,10 @@ packages: supports-color: optional: true + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -2453,6 +2535,14 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + diff@5.2.2: + resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2568,10 +2658,6 @@ packages: engines: {node: '>=18'} hasBin: true - escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2836,6 +2922,10 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + flatted@3.2.9: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} @@ -2868,6 +2958,10 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3036,6 +3130,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} @@ -3076,6 +3174,9 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + hyperdyperid@1.2.0: resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} engines: {node: '>=10.18'} @@ -3244,6 +3345,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -3285,6 +3390,10 @@ packages: is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} @@ -3505,6 +3614,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} @@ -3530,6 +3643,9 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + map-age-cleaner@0.1.3: resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} engines: {node: '>=6'} @@ -3677,6 +3793,11 @@ packages: engines: {node: '>=10'} hasBin: true + mocha@10.8.2: + resolution: {integrity: sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==} + engines: {node: '>= 14.0.0'} + hasBin: true + module-details-from-path@1.0.3: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} @@ -3712,6 +3833,11 @@ packages: resolution: {integrity: sha512-IWjIExdVYlmwXuzHdY/Q3lXCv1gbqoAXPazQhy2w4Xgtgha3H0OOujEESVPQcFUFMWm+pAk2gKnb57g8S41JZg==} engines: {node: '>= 20.0.0'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0} @@ -3786,6 +3912,18 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + openai@6.33.0: + resolution: {integrity: sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optionator@0.8.3: resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} engines: {node: '>= 0.8.0'} @@ -4522,6 +4660,20 @@ packages: ts-morph@13.0.3: resolution: {integrity: sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + ts-prune@0.10.3: resolution: {integrity: sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==} hasBin: true @@ -4669,6 +4821,9 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + validate-npm-package-name@5.0.1: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4702,6 +4857,10 @@ packages: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + web-streams-polyfill@4.2.0: resolution: {integrity: sha512-0rYDzGOh9EZpig92umN5g5D/9A1Kff7k0/mzPSSCY8jEQeYkgRMoY7LhbXtUCWzLCMX0TUE9aoHkjFNB7D9pfA==} engines: {node: '>= 8'} @@ -4758,6 +4917,9 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + workerpool@6.5.1: + resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -4795,14 +4957,30 @@ packages: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -4857,7 +5035,19 @@ snapshots: '@ampproject/remapping@2.2.1': dependencies: '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.31 + + '@anthropic-ai/sdk@0.40.1(encoding@0.1.13)': + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.10 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.6.7(encoding@0.1.13) + transitivePeerDependencies: + - encoding '@babel/code-frame@7.27.1': dependencies: @@ -4880,7 +5070,7 @@ snapshots: '@babel/traverse': 7.23.7 '@babel/types': 7.27.1 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4891,7 +5081,7 @@ snapshots: dependencies: '@babel/types': 7.27.1 '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 2.5.2 '@babel/helper-compilation-targets@7.23.6': @@ -4965,7 +5155,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.5 '@babel/parser': 7.27.2 '@babel/types': 7.27.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -4975,6 +5165,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@cypress/request@3.0.9': dependencies: aws-sign2: 0.7.0 @@ -5091,7 +5285,7 @@ snapshots: '@eslint/config-array@0.21.1': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -5107,7 +5301,7 @@ snapshots: '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.14.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -5173,7 +5367,7 @@ snapshots: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.20 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.1': {} @@ -5186,12 +5380,12 @@ snapshots: '@jridgewell/sourcemap-codec@1.4.15': {} - '@jridgewell/trace-mapping@0.3.20': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping@0.3.31': + '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 @@ -5493,6 +5687,14 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + '@tsconfig/node20@20.1.9': {} '@types/async-retry@1.4.8': @@ -5557,6 +5759,8 @@ snapshots: '@types/mdurl@1.0.5': {} + '@types/mocha@10.0.10': {} + '@types/ms@0.7.34': {} '@types/node-fetch@2.6.10': @@ -5564,6 +5768,10 @@ snapshots: '@types/node': 20.10.8 form-data: 4.0.4 + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@20.10.8': dependencies: undici-types: 5.26.5 @@ -5664,7 +5872,7 @@ snapshots: '@typescript-eslint/types': 8.55.0 '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 8.55.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.2 typescript: 5.6.3 transitivePeerDependencies: @@ -5674,7 +5882,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.6.3) '@typescript-eslint/types': 8.55.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -5697,7 +5905,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) '@typescript-eslint/utils': 8.13.0(eslint@9.39.2)(typescript@5.6.3) - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) ts-api-utils: 1.4.0(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 @@ -5710,7 +5918,7 @@ snapshots: '@typescript-eslint/types': 8.55.0 '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.6.3) '@typescript-eslint/utils': 8.55.0(eslint@9.39.2)(typescript@5.6.3) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.2 ts-api-utils: 2.4.0(typescript@5.6.3) typescript: 5.6.3 @@ -5725,7 +5933,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.13.0 '@typescript-eslint/visitor-keys': 8.13.0 - debug: 4.4.1 + debug: 4.4.3(supports-color@8.1.1) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.9 @@ -5742,7 +5950,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.6.3) '@typescript-eslint/types': 8.55.0 '@typescript-eslint/visitor-keys': 8.55.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 9.0.9 semver: 7.7.3 tinyglobby: 0.2.15 @@ -5793,7 +6001,7 @@ snapshots: '@verdaccio/core': 8.0.0-next-8.27 '@verdaccio/loaders': 8.0.0-next-8.17 '@verdaccio/signature': 8.0.0-next-8.19 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) lodash: 4.17.21 verdaccio-htpasswd: 13.0.0-next-8.27 transitivePeerDependencies: @@ -5802,7 +6010,7 @@ snapshots: '@verdaccio/config@8.0.0-next-8.27': dependencies: '@verdaccio/core': 8.0.0-next-8.27 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) js-yaml: 4.1.1 lodash: 4.17.21 transitivePeerDependencies: @@ -5838,7 +6046,7 @@ snapshots: dependencies: '@verdaccio/core': 8.0.0-next-8.27 '@verdaccio/logger': 8.0.0-next-8.27 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) got-cjs: 12.5.4 handlebars: 4.7.9 transitivePeerDependencies: @@ -5847,7 +6055,7 @@ snapshots: '@verdaccio/loaders@8.0.0-next-8.17': dependencies: '@verdaccio/core': 8.0.0-next-8.27 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) lodash: 4.17.21 transitivePeerDependencies: - supports-color @@ -5870,7 +6078,7 @@ snapshots: '@verdaccio/core': 8.0.0-next-8.27 '@verdaccio/logger-prettify': 8.0.0-next-8.4 colorette: 2.0.20 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -5895,7 +6103,7 @@ snapshots: '@verdaccio/config': 8.0.0-next-8.27 '@verdaccio/core': 8.0.0-next-8.27 '@verdaccio/url': 13.0.0-next-8.27 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) express: 4.21.2 express-rate-limit: 5.5.1 lodash: 4.17.21 @@ -5909,7 +6117,7 @@ snapshots: dependencies: '@verdaccio/config': 8.0.0-next-8.27 '@verdaccio/core': 8.0.0-next-8.27 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) jsonwebtoken: 9.0.2 transitivePeerDependencies: - supports-color @@ -5920,7 +6128,7 @@ snapshots: dependencies: '@verdaccio/core': 8.0.0-next-8.27 '@verdaccio/url': 13.0.0-next-8.27 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gunzip-maybe: 1.4.2 tar-stream: 3.1.7 transitivePeerDependencies: @@ -5933,7 +6141,7 @@ snapshots: '@verdaccio/url@13.0.0-next-8.27': dependencies: '@verdaccio/core': 8.0.0-next-8.27 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) validator: 13.15.23 transitivePeerDependencies: - supports-color @@ -6051,26 +6259,24 @@ snapshots: dependencies: acorn: 8.15.0 - acorn-jsx@5.3.2(acorn@8.11.3): - dependencies: - acorn: 8.11.3 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 acorn-walk@8.3.1: {} - acorn@8.11.3: {} - acorn@8.15.0: {} agent-base@6.0.2: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + aggregate-error@4.0.1: dependencies: clean-stack: 4.2.0 @@ -6118,6 +6324,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-colors@4.1.3: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -6135,6 +6343,8 @@ snapshots: apache-md5@1.1.8: {} + arg@4.1.3: {} + arg@5.0.2: {} argparse@1.0.10: @@ -6239,7 +6449,7 @@ snapshots: common-path-prefix: 3.0.0 concordance: 5.0.4 currently-unhandled: 0.4.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) emittery: 1.0.3 figures: 5.0.0 globby: 13.2.2 @@ -6318,7 +6528,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 @@ -6341,6 +6551,8 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-stdout@1.3.1: {} + browserify-zlib@0.1.4: dependencies: pako: 0.2.9 @@ -6424,6 +6636,8 @@ snapshots: callsites@4.1.0: {} + camelcase@6.3.0: {} + caniuse-lite@1.0.30001620: {} caniuse-lite@1.0.30001769: {} @@ -6490,6 +6704,12 @@ snapshots: dependencies: typanion: 3.14.0 + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -6595,6 +6815,8 @@ snapshots: path-type: 4.0.0 yaml: 1.10.3 + create-require@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -6645,9 +6867,13 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.3: + debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@4.0.0: {} decompress-response@6.0.0: dependencies: @@ -6679,6 +6905,10 @@ snapshots: destroy@1.2.0: {} + diff@4.0.4: {} + + diff@5.2.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -6871,8 +7101,6 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 - escalade@3.1.1: {} - escalade@3.2.0: {} escape-html@1.0.3: {} @@ -7002,7 +7230,7 @@ snapshots: ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -7032,8 +7260,8 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.11.3 - acorn-jsx: 5.3.2(acorn@8.11.3) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -7121,7 +7349,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -7205,7 +7433,7 @@ snapshots: finalhandler@2.1.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -7229,6 +7457,8 @@ snapshots: flatted: 3.2.9 keyv: 4.5.4 + flat@5.0.2: {} + flatted@3.2.9: {} for-each@0.3.3: @@ -7267,6 +7497,11 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -7466,6 +7701,8 @@ snapshots: dependencies: function-bind: 1.1.2 + he@1.2.0: {} + headers-polyfill@4.0.3: {} heap-js@2.6.0: {} @@ -7508,10 +7745,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + hyperdyperid@1.2.0: {} iconv-lite@0.4.24: @@ -7664,6 +7905,8 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@2.1.0: {} + is-plain-object@5.0.0: {} is-promise@2.2.2: {} @@ -7704,6 +7947,8 @@ snapshots: is-typedarray@1.0.0: {} + is-unicode-supported@0.1.0: {} + is-unicode-supported@1.3.0: {} is-weakmap@2.0.2: {} @@ -7925,6 +8170,11 @@ snapshots: lodash@4.17.21: {} + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + long@5.2.3: {} lowdb@1.0.0: @@ -7947,6 +8197,8 @@ snapshots: lru-cache@7.18.3: {} + make-error@1.3.6: {} + map-age-cleaner@0.1.3: dependencies: p-defer: 1.0.0 @@ -8059,6 +8311,29 @@ snapshots: mkdirp@1.0.4: {} + mocha@10.8.2: + dependencies: + ansi-colors: 4.1.3 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.4.3(supports-color@8.1.1) + diff: 5.2.2 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 8.1.0 + he: 1.2.0 + js-yaml: 4.1.1 + log-symbols: 4.1.0 + minimatch: 5.1.9 + ms: 2.1.3 + serialize-javascript: 7.0.4 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.5.1 + yargs: 16.2.0 + yargs-parser: 20.2.9 + yargs-unparser: 2.0.0 + module-details-from-path@1.0.3: {} ms@2.0.0: {} @@ -8079,6 +8354,8 @@ snapshots: nexus-rpc@0.0.2: {} + node-domexception@1.0.0: {} + node-fetch@2.6.7(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -8144,6 +8421,10 @@ snapshots: dependencies: wrappy: 1.0.2 + openai@6.33.0(zod@3.25.76): + optionalDependencies: + zod: 3.25.76 + optionator@0.8.3: dependencies: deep-is: 0.1.4 @@ -8475,7 +8756,7 @@ snapshots: require-in-the-middle@7.2.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) module-details-from-path: 1.0.3 resolve: 1.22.8 transitivePeerDependencies: @@ -8517,7 +8798,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -8593,7 +8874,7 @@ snapshots: send@1.2.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -8992,6 +9273,26 @@ snapshots: '@ts-morph/common': 0.12.3 code-block-writer: 11.0.3 + ts-node@10.9.2(@swc/core@1.3.102)(@types/node@20.10.8)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.10.8 + acorn: 8.15.0 + acorn-walk: 8.3.1 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.3.102 + ts-prune@0.10.3: dependencies: commander: 6.2.1 @@ -9125,7 +9426,7 @@ snapshots: update-browserslist-db@1.0.13(browserslist@4.23.0): dependencies: browserslist: 4.23.0 - escalade: 3.1.1 + escalade: 3.2.0 picocolors: 1.1.1 update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -9151,6 +9452,8 @@ snapshots: uuid@8.3.2: {} + v8-compile-cache-lib@3.0.1: {} + validate-npm-package-name@5.0.1: {} validator@13.15.23: {} @@ -9174,7 +9477,7 @@ snapshots: '@verdaccio/file-locking': 13.0.0-next-8.6 apache-md5: 1.1.8 bcryptjs: 2.4.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.0 unix-crypt-td-js: 1.1.4 transitivePeerDependencies: @@ -9203,7 +9506,7 @@ snapshots: clipanion: 4.0.0-rc.4(typanion@3.14.0) compression: 1.8.1 cors: 2.8.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) envinfo: 7.15.0 express: 4.21.2 lodash: 4.17.21 @@ -9230,6 +9533,8 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + web-streams-polyfill@4.0.0-beta.3: {} + web-streams-polyfill@4.2.0: {} webidl-conversions@3.0.1: {} @@ -9356,6 +9661,8 @@ snapshots: wordwrap@1.0.0: {} + workerpool@6.5.1: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -9387,18 +9694,39 @@ snapshots: yaml@1.10.3: {} + yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yargs@17.7.2: dependencies: cliui: 8.0.1 - escalade: 3.1.1 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 + yn@3.1.1: {} + yocto-queue@0.1.0: {} yocto-queue@1.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 62362712b..2a8b767d5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - packages/activity - packages/ai-sdk + - packages/tool-registry - packages/client - packages/cloud - packages/common