From 1778562d3ddfadb62dcd1f62822786dbf03b5630 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 31 Mar 2026 16:40:03 -0400 Subject: [PATCH 01/34] WIP --- {examples => examples_old}/chatbot-alt.ts | 0 {examples => examples_old}/chatbot.ts | 0 {examples => examples_old}/cot.ts | 0 {examples => examples_old}/email.ts | 0 examples_old/email2.ts | 59 + {examples => examples_old}/example.ts | 0 {examples => examples_old}/executor.ts | 0 {examples => examples_old}/goal.ts | 0 {examples => examples_old}/helpers/helpers.ts | 0 {examples => examples_old}/helpers/loader.ts | 0 {examples => examples_old}/helpers/runner.ts | 0 examples_old/hono/.gitignore | 34 + examples_old/hono/README.md | 21 + examples_old/hono/package.json | 23 + examples_old/hono/pnpm-lock.yaml | 1643 ++++++++ examples_old/hono/public/.assetsignore | 1 + examples_old/hono/public/favicon.ico | Bin 0 -> 15406 bytes examples_old/hono/src/agent.ts | 90 + examples_old/hono/src/db.ts | 76 + examples_old/hono/src/events.ts | 32 + examples_old/hono/src/index.ts | 362 ++ examples_old/hono/src/machine.ts | 74 + examples_old/hono/src/style.css | 3 + examples_old/hono/src/utils.ts | 29 + examples_old/hono/tsconfig.json | 15 + examples_old/hono/vite.config.ts | 6 + examples_old/hono/wrangler.jsonc | 6 + {examples => examples_old}/joke.ts | 0 {examples => examples_old}/multi.ts | 0 {examples => examples_old}/newspaper.ts | 0 {examples => examples_old}/number.ts | 0 {examples => examples_old}/raffle.ts | 0 {examples => examples_old}/sandbox.ts | 0 {examples => examples_old}/simple.ts | 0 {examples => examples_old}/support.ts | 0 {examples => examples_old}/ticTacToe.ts | 0 {examples => examples_old}/todo.ts | 0 {examples => examples_old}/tutor.ts | 0 {examples => examples_old}/verify.ts | 0 {examples => examples_old}/weather.ts | 0 {examples => examples_old}/wiki.ts | 0 {examples => examples_old}/word.ts | 0 package.json | 93 +- pnpm-lock.yaml | 3613 +++++------------ src/adapter.ts | 8 + src/agent.test.ts | 1431 +++++-- src/agent.ts | 687 ---- src/ai-sdk/index.ts | 93 + src/classify.ts | 32 + src/decide.ts | 13 + src/decision.test.ts | 155 - src/decision.ts | 85 - src/event.ts | 107 + src/graph/index.ts | 33 + src/index.ts | 41 +- src/machine.ts | 9 + src/memory.ts | 25 - src/middleware.ts | 103 - src/mockModel.ts | 47 - src/planners/shortestPathPlanner.ts | 22 - src/planners/simplePlanner.ts | 162 - src/run.ts | 51 + src/schemas.ts | 11 - src/state.ts | 50 + src/step.ts | 167 + src/strategies/chain-of-note.ts | 106 - src/stream.ts | 29 + src/templates/defaultText.ts | 18 - src/text.ts | 148 - src/types.ts | 615 +-- src/utils.ts | 295 +- src/xstate/index.ts | 10 + tsconfig.json | 121 +- tsdown.config.ts | 13 + 74 files changed, 5841 insertions(+), 5026 deletions(-) rename {examples => examples_old}/chatbot-alt.ts (100%) rename {examples => examples_old}/chatbot.ts (100%) rename {examples => examples_old}/cot.ts (100%) rename {examples => examples_old}/email.ts (100%) create mode 100644 examples_old/email2.ts rename {examples => examples_old}/example.ts (100%) rename {examples => examples_old}/executor.ts (100%) rename {examples => examples_old}/goal.ts (100%) rename {examples => examples_old}/helpers/helpers.ts (100%) rename {examples => examples_old}/helpers/loader.ts (100%) rename {examples => examples_old}/helpers/runner.ts (100%) create mode 100644 examples_old/hono/.gitignore create mode 100644 examples_old/hono/README.md create mode 100644 examples_old/hono/package.json create mode 100644 examples_old/hono/pnpm-lock.yaml create mode 100644 examples_old/hono/public/.assetsignore create mode 100644 examples_old/hono/public/favicon.ico create mode 100644 examples_old/hono/src/agent.ts create mode 100644 examples_old/hono/src/db.ts create mode 100644 examples_old/hono/src/events.ts create mode 100644 examples_old/hono/src/index.ts create mode 100644 examples_old/hono/src/machine.ts create mode 100644 examples_old/hono/src/style.css create mode 100644 examples_old/hono/src/utils.ts create mode 100644 examples_old/hono/tsconfig.json create mode 100644 examples_old/hono/vite.config.ts create mode 100644 examples_old/hono/wrangler.jsonc rename {examples => examples_old}/joke.ts (100%) rename {examples => examples_old}/multi.ts (100%) rename {examples => examples_old}/newspaper.ts (100%) rename {examples => examples_old}/number.ts (100%) rename {examples => examples_old}/raffle.ts (100%) rename {examples => examples_old}/sandbox.ts (100%) rename {examples => examples_old}/simple.ts (100%) rename {examples => examples_old}/support.ts (100%) rename {examples => examples_old}/ticTacToe.ts (100%) rename {examples => examples_old}/todo.ts (100%) rename {examples => examples_old}/tutor.ts (100%) rename {examples => examples_old}/verify.ts (100%) rename {examples => examples_old}/weather.ts (100%) rename {examples => examples_old}/wiki.ts (100%) rename {examples => examples_old}/word.ts (100%) create mode 100644 src/adapter.ts delete mode 100644 src/agent.ts create mode 100644 src/ai-sdk/index.ts create mode 100644 src/classify.ts create mode 100644 src/decide.ts delete mode 100644 src/decision.test.ts delete mode 100644 src/decision.ts create mode 100644 src/event.ts create mode 100644 src/graph/index.ts create mode 100644 src/machine.ts delete mode 100644 src/memory.ts delete mode 100644 src/middleware.ts delete mode 100644 src/mockModel.ts delete mode 100644 src/planners/shortestPathPlanner.ts delete mode 100644 src/planners/simplePlanner.ts create mode 100644 src/run.ts delete mode 100644 src/schemas.ts create mode 100644 src/state.ts create mode 100644 src/step.ts delete mode 100644 src/strategies/chain-of-note.ts create mode 100644 src/stream.ts delete mode 100644 src/templates/defaultText.ts delete mode 100644 src/text.ts create mode 100644 src/xstate/index.ts create mode 100644 tsdown.config.ts diff --git a/examples/chatbot-alt.ts b/examples_old/chatbot-alt.ts similarity index 100% rename from examples/chatbot-alt.ts rename to examples_old/chatbot-alt.ts diff --git a/examples/chatbot.ts b/examples_old/chatbot.ts similarity index 100% rename from examples/chatbot.ts rename to examples_old/chatbot.ts diff --git a/examples/cot.ts b/examples_old/cot.ts similarity index 100% rename from examples/cot.ts rename to examples_old/cot.ts diff --git a/examples/email.ts b/examples_old/email.ts similarity index 100% rename from examples/email.ts rename to examples_old/email.ts diff --git a/examples_old/email2.ts b/examples_old/email2.ts new file mode 100644 index 0000000..31ac3f5 --- /dev/null +++ b/examples_old/email2.ts @@ -0,0 +1,59 @@ +import { setup, SnapshotFrom } from 'xstate'; +import { mapState } from '../src/mapState'; + +const machine = setup({}).createMachine({ + initial: 'checking', + states: { + checking: { + on: { + askForClarification: { + target: 'clarifying', + }, + submitEmail: { + target: 'submitting', + }, + }, + }, + clarifying: { + on: { + provideClarification: { + target: 'checking', + }, + }, + }, + submitting: { + on: { + confirm: { + target: 'done', + }, + }, + }, + done: { + type: 'final', + }, + }, +}); + +function getStuff(snapshot: SnapshotFrom) { + return mapState< + typeof snapshot, + { + goal: string; + } + >(snapshot, { + states: { + checking: { + map: () => ({ + goal: 'Respond to the email given the instructions and the provided clarifications. If not enough information is provided, ask for clarification. Otherwise, if you are absolutely sure that there is no ambiguous or missing information, create and submit a response email.', + }), + }, + submitting: { + map: () => ({ + goal: 'Create and submit an email based on the instructions.', + }), + }, + }, + }); +} + +async function main() {} diff --git a/examples/example.ts b/examples_old/example.ts similarity index 100% rename from examples/example.ts rename to examples_old/example.ts diff --git a/examples/executor.ts b/examples_old/executor.ts similarity index 100% rename from examples/executor.ts rename to examples_old/executor.ts diff --git a/examples/goal.ts b/examples_old/goal.ts similarity index 100% rename from examples/goal.ts rename to examples_old/goal.ts diff --git a/examples/helpers/helpers.ts b/examples_old/helpers/helpers.ts similarity index 100% rename from examples/helpers/helpers.ts rename to examples_old/helpers/helpers.ts diff --git a/examples/helpers/loader.ts b/examples_old/helpers/loader.ts similarity index 100% rename from examples/helpers/loader.ts rename to examples_old/helpers/loader.ts diff --git a/examples/helpers/runner.ts b/examples_old/helpers/runner.ts similarity index 100% rename from examples/helpers/runner.ts rename to examples_old/helpers/runner.ts diff --git a/examples_old/hono/.gitignore b/examples_old/hono/.gitignore new file mode 100644 index 0000000..c363919 --- /dev/null +++ b/examples_old/hono/.gitignore @@ -0,0 +1,34 @@ +# prod +dist/ +dist-server/ + +# dev +.yarn/ +!.yarn/releases +.vscode/* +!.vscode/launch.json +!.vscode/*.code-snippets +.idea/workspace.xml +.idea/usage.statistics.xml +.idea/shelf + +# deps +node_modules/ +.wrangler + +# env +.env +.env.production +.dev.vars + +# logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# misc +.DS_Store diff --git a/examples_old/hono/README.md b/examples_old/hono/README.md new file mode 100644 index 0000000..eba2b1e --- /dev/null +++ b/examples_old/hono/README.md @@ -0,0 +1,21 @@ +```txt +npm install +npm run dev +``` + +```txt +npm run deploy +``` + +[For generating/synchronizing types based on your Worker configuration run](https://developers.cloudflare.com/workers/wrangler/commands/#types): + +```txt +npm run cf-typegen +``` + +Pass the `CloudflareBindings` as generics when instantiation `Hono`: + +```ts +// src/index.ts +const app = new Hono<{ Bindings: CloudflareBindings }>() +``` diff --git a/examples_old/hono/package.json b/examples_old/hono/package.json new file mode 100644 index 0000000..8bd9148 --- /dev/null +++ b/examples_old/hono/package.json @@ -0,0 +1,23 @@ +{ + "name": "hono", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "$npm_execpath run build && vite preview", + "deploy": "$npm_execpath run build && wrangler deploy", + "cf-typegen": "wrangler types --env-interface CloudflareBindings" + }, + "dependencies": { + "@ai-sdk/openai": "^3.0.24", + "ai": "^6.0.66", + "hono": "^4.11.7", + "xstate": "^5.26.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.2.3", + "vite": "^6.3.5", + "wrangler": "^4.17.0" + } +} \ No newline at end of file diff --git a/examples_old/hono/pnpm-lock.yaml b/examples_old/hono/pnpm-lock.yaml new file mode 100644 index 0000000..77d37c1 --- /dev/null +++ b/examples_old/hono/pnpm-lock.yaml @@ -0,0 +1,1643 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ai-sdk/openai': + specifier: ^3.0.24 + version: 3.0.24(zod@3.25.76) + ai: + specifier: ^6.0.66 + version: 6.0.66(zod@3.25.76) + hono: + specifier: ^4.11.7 + version: 4.11.7 + xstate: + specifier: ^5.26.0 + version: 5.26.0 + zod: + specifier: ^3.23.0 + version: 3.25.76 + devDependencies: + '@cloudflare/vite-plugin': + specifier: ^1.2.3 + version: 1.22.1(vite@6.4.1)(workerd@1.20260128.0)(wrangler@4.61.1) + vite: + specifier: ^6.3.5 + version: 6.4.1 + wrangler: + specifier: ^4.17.0 + version: 4.61.1 + +packages: + + '@ai-sdk/gateway@3.0.31': + resolution: {integrity: sha512-WActnxPeW46XcfZWWEcJ1FytpjCtKQEo25WZVa2xZSf+u2FgSNVt/dXIvlSZetPnXo6T2P/GhFAPBULMN6siRA==, tarball: https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.31.tgz} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@3.0.24': + resolution: {integrity: sha512-f4d2z4cQpaLnCxlhL5X+/FIpA7u55eYbfCtu7hJxukav7MIQi+5uufy5OAXdCieqPnsdoiGRWaI+VTPh151mZQ==, tarball: https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.24.tgz} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.12': + resolution: {integrity: sha512-sdC3eUTa5W4r/bISlF3nxmM6zc8mV7Nj3mWI9iUO0cib70h0Zr52Tz5gGzO6HcDirbKVTR2ywmZb61MHU68prA==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.12.tgz} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.6': + resolution: {integrity: sha512-hSfoJtLtpMd7YxKM+iTqlJ0ZB+kJ83WESMiWuWrNVey3X8gg97x0OdAAaeAeclZByCX3UdPOTqhvJdK8qYA3ww==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.6.tgz} + engines: {node: '>=18'} + + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==, tarball: https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.12.0': + resolution: {integrity: sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ==, tarball: https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.12.0.tgz} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: ^1.20260115.0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/vite-plugin@1.22.1': + resolution: {integrity: sha512-RDWc6WtrdjVDfpBeO3MYcgJIbq+Phg9qBXq1Ixl00qPqM8bgKp9oPLhg8oayynQs8udNnqkV0CjfojvIhhfZWg==, tarball: https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.22.1.tgz} + peerDependencies: + vite: ^6.1.0 || ^7.0.0 + wrangler: ^4.61.1 + + '@cloudflare/workerd-darwin-64@1.20260128.0': + resolution: {integrity: sha512-XJN8zWWNG3JwAUqqwMLNKJ9fZfdlQkx/zTTHW/BB8wHat9LjKD6AzxqCu432YmfjR+NxEKCzUOxMu1YOxlVxmg==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260128.0.tgz} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260128.0': + resolution: {integrity: sha512-vKnRcmnm402GQ5DOdfT5H34qeR2m07nhnTtky8mTkNWP+7xmkz32AMdclwMmfO/iX9ncyKwSqmml2wPG32eq/w==, tarball: https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260128.0.tgz} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260128.0': + resolution: {integrity: sha512-RiaR+Qugof/c6oI5SagD2J5wJmIfI8wQWaV2Y9905Raj6sAYOFaEKfzkKnoLLLNYb4NlXicBrffJi1j7R/ypUA==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260128.0.tgz} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260128.0': + resolution: {integrity: sha512-U39U9vcXLXYDbrJ112Q7D0LDUUnM54oXfAxPgrL2goBwio7Z6RnsM25TRvm+Q06F4+FeDOC4D51JXlFHb9t1OA==, tarball: https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260128.0.tgz} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260128.0': + resolution: {integrity: sha512-fdJwSqRkJsAJFJ7+jy0th2uMO6fwaDA8Ny6+iFCssfzlNkc4dP/twXo+3F66FMLMe/6NIqjzVts0cpiv7ERYbQ==, tarball: https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260128.0.tgz} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==, tarball: https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz} + engines: {node: '>=12'} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==, tarball: https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==, tarball: https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.0': + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==, tarball: https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.0': + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.0': + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.0': + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.0': + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.0': + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.0': + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.0': + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.0': + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.0': + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.0': + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.0': + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.0': + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.0': + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.0': + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.0': + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.0': + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==, tarball: https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.0': + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==, tarball: https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.0': + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==, tarball: https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.0': + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==, tarball: https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.0': + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==, tarball: https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.0': + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==, tarball: https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.0': + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.0': + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.0': + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.0': + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==, tarball: https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==, tarball: https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==, tarball: https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==, tarball: https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==, tarball: https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==, tarball: https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==, tarball: https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==, tarball: https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==, tarball: https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==, tarball: https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==, tarball: https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==, tarball: https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==, tarball: https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==, tarball: https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==, tarball: https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==, tarball: https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==, tarball: https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==, tarball: https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, tarball: https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, tarball: https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==, tarball: https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==, tarball: https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz} + engines: {node: '>=8.0.0'} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==, tarball: https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==, tarball: https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==, tarball: https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz} + + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==, tarball: https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==, tarball: https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz} + cpu: [x64] + os: [win32] + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==, tarball: https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.14': + resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==, tarball: https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, tarball: https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz} + + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==, tarball: https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz} + engines: {node: '>= 20'} + + ai@6.0.66: + resolution: {integrity: sha512-Klnzjlc3JczRykD75t+Qn5Jt5HwUCaLlN9aZku9KrSDjhc/pab54YH0w85huue7FLPlbTVF5zaQrw3NdEwiGpA==, tarball: https://registry.npmjs.org/ai/-/ai-6.0.66.tgz} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==, tarball: https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==, tarball: https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz} + engines: {node: '>=18'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, tarball: https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz} + engines: {node: '>=8'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==, tarball: https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==, tarball: https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==, tarball: https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz} + engines: {node: '>=18'} + hasBin: true + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==, tarball: https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz} + engines: {node: '>=18.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, tarball: https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, tarball: https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hono@4.11.7: + resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==, tarball: https://registry.npmjs.org/hono/-/hono-4.11.7.tgz} + engines: {node: '>=16.9.0'} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==, tarball: https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==, tarball: https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz} + engines: {node: '>=6'} + + miniflare@4.20260128.0: + resolution: {integrity: sha512-AVCn3vDRY+YXu1sP4mRn81ssno6VUqxo29uY2QVfgxXU2TMLvhRIoGwm7RglJ3Gzfuidit5R86CMQ6AvdFTGAw==, tarball: https://registry.npmjs.org/miniflare/-/miniflare-4.20260128.0.tgz} + engines: {node: '>=18.0.0'} + hasBin: true + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==, tarball: https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, tarball: https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, tarball: https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz} + engines: {node: '>=12'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==, tarball: https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==, tarball: https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==, tarball: https://registry.npmjs.org/semver/-/semver-7.7.3.tgz} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==, tarball: https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, tarball: https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz} + engines: {node: '>=0.10.0'} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==, tarball: https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==, tarball: https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, tarball: https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz} + + undici@7.18.2: + resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==, tarball: https://registry.npmjs.org/undici/-/undici-7.18.2.tgz} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==, tarball: https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz} + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==, tarball: https://registry.npmjs.org/vite/-/vite-6.4.1.tgz} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + workerd@1.20260128.0: + resolution: {integrity: sha512-EhLJGptSGFi8AEErLiamO3PoGpbRqL+v4Ve36H2B38VxmDgFOSmDhfepBnA14sCQzGf1AEaoZX2DCwZsmO74yQ==, tarball: https://registry.npmjs.org/workerd/-/workerd-1.20260128.0.tgz} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.61.1: + resolution: {integrity: sha512-hfYQ16VLPkNi8xE1/V3052S2stM5e+vq3Idpt83sXoDC3R7R1CLgMkK6M6+Qp3G+9GVDNyHCkvohMPdfFTaD4Q==, tarball: https://registry.npmjs.org/wrangler/-/wrangler-4.61.1.tgz} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260128.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==, tarball: https://registry.npmjs.org/ws/-/ws-8.18.0.tgz} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.26.0: + resolution: {integrity: sha512-Fvi9VBoqHgsGYLU2NTag8xDTWtKqUC0+ue7EAhBNBb06wf620QEy05upBaEI1VLMzIn63zugLV8nHb69ZUWYAA==, tarball: https://registry.npmjs.org/xstate/-/xstate-5.26.0.tgz} + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==, tarball: https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==, tarball: https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==, tarball: https://registry.npmjs.org/zod/-/zod-3.25.76.tgz} + +snapshots: + + '@ai-sdk/gateway@3.0.31(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.6 + '@ai-sdk/provider-utils': 4.0.12(zod@3.25.76) + '@vercel/oidc': 3.1.0 + zod: 3.25.76 + + '@ai-sdk/openai@3.0.24(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.6 + '@ai-sdk/provider-utils': 4.0.12(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.12(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.6 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider@3.0.6': + dependencies: + json-schema: 0.4.0 + + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260128.0)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260128.0 + + '@cloudflare/vite-plugin@1.22.1(vite@6.4.1)(workerd@1.20260128.0)(wrangler@4.61.1)': + dependencies: + '@cloudflare/unenv-preset': 2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260128.0) + miniflare: 4.20260128.0 + unenv: 2.0.0-rc.24 + vite: 6.4.1 + wrangler: 4.61.1 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - workerd + + '@cloudflare/workerd-darwin-64@1.20260128.0': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260128.0': + optional: true + + '@cloudflare/workerd-linux-64@1.20260128.0': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260128.0': + optional: true + + '@cloudflare/workerd-windows-64@1.20260128.0': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.0': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.0': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.0': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.0': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.0': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.0': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.0': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.0': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.0': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.0': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.0': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.0': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.0': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.0': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.0': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.0': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.0': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.0': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.0': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.0': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.0': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.0': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.0': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.0': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.0': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.0': + optional: true + + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@opentelemetry/api@1.9.0': {} + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.14': {} + + '@standard-schema/spec@1.1.0': {} + + '@types/estree@1.0.8': {} + + '@vercel/oidc@3.1.0': {} + + ai@6.0.66(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 3.0.31(zod@3.25.76) + '@ai-sdk/provider': 3.0.6 + '@ai-sdk/provider-utils': 4.0.12(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + + blake3-wasm@2.1.5: {} + + cookie@1.1.1: {} + + detect-libc@2.1.2: {} + + error-stack-parser-es@1.0.5: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 + + eventsource-parser@3.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + hono@4.11.7: {} + + json-schema@0.4.0: {} + + kleur@4.1.5: {} + + miniflare@4.20260128.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.18.2 + workerd: 1.20260128.0 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + nanoid@3.3.11: {} + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + semver@7.7.3: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + source-map-js@1.2.1: {} + + supports-color@10.2.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tslib@2.8.1: + optional: true + + undici@7.18.2: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + vite@6.4.1: + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + + workerd@1.20260128.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260128.0 + '@cloudflare/workerd-darwin-arm64': 1.20260128.0 + '@cloudflare/workerd-linux-64': 1.20260128.0 + '@cloudflare/workerd-linux-arm64': 1.20260128.0 + '@cloudflare/workerd-windows-64': 1.20260128.0 + + wrangler@4.61.1: + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.12.0(unenv@2.0.0-rc.24)(workerd@1.20260128.0) + blake3-wasm: 2.1.5 + esbuild: 0.27.0 + miniflare: 4.20260128.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260128.0 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.18.0: {} + + xstate@5.26.0: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.14 + cookie: 1.1.1 + youch-core: 0.3.3 + + zod@3.25.76: {} diff --git a/examples_old/hono/public/.assetsignore b/examples_old/hono/public/.assetsignore new file mode 100644 index 0000000..9f1f131 --- /dev/null +++ b/examples_old/hono/public/.assetsignore @@ -0,0 +1 @@ +.vite \ No newline at end of file diff --git a/examples_old/hono/public/favicon.ico b/examples_old/hono/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..543164354afc72a8b8de19830b6b9c06af58c0ae GIT binary patch literal 15406 zcmeHOdr(x@8NZ+)h{!{Yf~ZJHzz|{A<&7-MQ{D(dFls}51Y#7yNHo!C)>p7KjWw~Y zopdtOOgl}d|MZb*XPTxn&X{D(w05SQ{*jsVF=?xvnzqD_F|p#Wzi)TXa#^^*-Cd|{ zcV~Wc?m72-kMo^#&*S^fYFd~!ON)=!n5Jrv&(^ejP190S-TBM}O?#DP7K`Wo{hId9 zB2CL=9g>j3UC!jc<~KVJRbf`VN&J(zL0p_=!=|Y;AhHnq=M>=1vQ{8(50Kjnq_hAm zLsTKYJ`s=DQk2Xq#U1NT;NkP5k-HnkBwr)h^_v8KiKD=1;B+&jcu<@&a z{Q$u7Tlu}nnTTHAS)7R1oCq&HfcHe_+7keuv35V4lhNwmJDjkTJ8`T0IiUOqE;7%r z%-pZ~1P<1mz+dW|NHIt0sm*pY9LFlZXlIB>=9yH&LCE|R`h_eIxCIcd<)BfsaIUi8 zec}`5Z!-M@@jYUPmVwy7<&5Ppdksp%ZTNNg8o&@%zO&(P#2!tfu0i@m6re96AGag( z0T=SP-q?Y~^?bY<)py~_FykhkFYlGM_{ER@X19*UI ztdcQVDtikTozRxyn&v>1SsfB9@j!e9AV{bjf9*EXqm%uS)aa&l@hC@|S@{dsVc)_U zKp=f?B+ICL%@b}~p--&AX|wVdH{z8g4IJZa$AjC=%eOyGTA?SFG~zY0@|U*YwWZvb z306I7UVg~{X}Q1j;JjJ+%QoPgd`|-I-_XH5kCCm%D_^|>Sl;FadF?bSKdlo_%AQl9 z?BLYpx4y_RL)w*{F5Fq!h81Sz-?s(T+*8WF(gpecI?hc^heGZV@83d@mg!q&WlR0E zZp3A7#ck<(7bw5vsmbLxpJsj5_0n~r!5{fTll>Sj640aZ^Ts;JJeO+#A#St2~Isl}=nEpu;W7E}T}uI_6c! z(N=wo`z{Y^j>(>LW`DIOah^ckNd)CvGpjxA9aT4ouQfRX-{+c@9jYI)Krk#IeiFK9 zwMU7NpM*vT{X!N9S>S)v0tll|YN2LDE`4?Ti8qK3h#M|HUJgFw^94S?K&;d9uuMzG zoV&spRwUE!x7!tevfz4{1>mzA^6Z@2^(`hPQHy;HxOBX>!PO~@#R4JCZ1^-JCqny1ZReIlMpkdJ12Owwi_Ltl~V)~H0eq%)+ zRe)hW|5VFD&(Nh$nhX{T4DzZDW_w<@0V_4@ep zEIuNM*9hOZ5&mz-R0hXSB;Ao%hZNo;_!WT#9?0DSUtgI&`U{Qt)+83eG3o;4d`vk( zi$f>WBAaoC&j=_;zx%7NFP+R=i-b305(){`s5cnOri+sr(B_Icu%A^b_Z58g@HfBy zi@dYK{=A3$6)0z$X+;ePlGkH9@0vi5VC%n}@r)%+BS>z-b^~=x7mNO4A3bC}_1E6M zbmDv0^OonO&HnrMeH+MlP5ZJoPt3-X${Om=yzi^K zHso>dGA#QSJ!F3~o<3&EWDQu=HJa~=cDzkFOZ?T>m+syzNatOsYjPhd%>#K}%@~Z_ zIWnWKZ(WIxE1s^j6L;9RXRCGjs0GHJJ}=ls`S)-Jib#h4sq z|K~~n!98q?uMGpqr*Fpmj4pgf^!3FW#-QZBn6F(Y%gfp)>$k+@e0aXQd=q#}GKfFk z%VM1JC}j;xymAz_xnd)x^_RW0w0#sW^3K?X%|X=M%Zf3>HpUN!Bz`y^CW=4$_J!Eq z!I;o$#_t6Z$9G`|-qUzw_6iN+LsmDITYB&Tf$<<`fYvip8%OoeyU$~ak9-3;-5APw zjJ}EK`f&_dcHvctxw^nJqW76)G8W2sR-9~2kT&Ks7OAI|ST65ne3N5mTJA_GlUR2n z*Vc#6Bv$Ofr%eCIGt6x{$H+dij<)q3FDfr>D}UBaYh!%{(q6UgLAjDMooPWoUMC>n z9nC{{C&g+Mza38Mn&hA?Uy_nz8 zfmw`QpJf?bF>LC;_5jfKrO|C@W=vV``Kmg)^yek}r+3^a-6hR(BC=#)xQFrgbX6|* z0q7&SQv4Lpv>wR1_7KO5j2&4YN}q9`cje5h!!;E{SG;B>9XrQy54M#)pPSIdCzs6NUv=G8k-`YFqfNvv~bbe>pBkrRoG^M7CbRbKF072PE-bCVwSL%3Aq#61&p zo!Pg%w@}MF1b-zk5jY2ZWIv46GppU?(&XEyJc2J&aO``*Jx+x{l^wwl@2NjBIk*2z z4Vg>0FMGF&<7O1t2A1FEe)+8JE|2Ti9fOtkfUc-J7~b?KF8O`8e+IjwgX4(*McB{( zB78!)?(#7G9q*FZW>AfQL{*(aTJw&Q)6Q4y{P^aF7qzSA?Xw2 fns@IT_Cadv^H^~AY8cWiWPy+cLKgV{w7|as=smC{ literal 0 HcmV?d00001 diff --git a/examples_old/hono/src/agent.ts b/examples_old/hono/src/agent.ts new file mode 100644 index 0000000..602c253 --- /dev/null +++ b/examples_old/hono/src/agent.ts @@ -0,0 +1,90 @@ +import { generateText, tool } from 'ai'; +import { createOpenAI } from '@ai-sdk/openai'; +import type { StateValue } from 'xstate'; +import { emailMachine } from './machine'; +import { agentEvents } from './events'; +import { getAllTransitions } from './utils'; +import { z } from 'zod'; + +type ObservedState = { + value: StateValue; + context: Record; +}; + +// States where agent makes decisions +export function requiresAgentDecision(stateValue: StateValue): boolean { + return stateValue === 'checking'; +} + +// Build AI SDK tools from Zod events, filtered by available transitions +function buildTools( + resolvedState: ReturnType +) { + const transitions = getAllTransitions(resolvedState); + const availableEventTypes = new Set(transitions.map((t) => t.eventType)); + + const tools: Record> = {}; + + for (const [eventType, schema] of Object.entries(agentEvents)) { + if (!availableEventTypes.has(eventType)) continue; + + tools[eventType] = tool({ + description: schema.description!, + inputSchema: schema, + execute: async (params) => ({ type: eventType, ...params }), + }); + } + + return tools; +} + +export async function getAgentDecision( + observedState: ObservedState, + goal: string, + apiKey: string +): Promise<{ type: string; [key: string]: unknown } | null> { + // Rehydrate state from serialized form + const resolvedState = emailMachine.resolveState(observedState); + const tools = buildTools(resolvedState); + + console.log('tools', tools); + + if (Object.keys(tools).length === 0) { + return null; + } + + const context = observedState.context as { + userRequest: string; + clarifications: string[]; + questions: string[]; + }; + + const systemPrompt = `You are an email assistant helping draft emails. + +User's request: ${context.userRequest} + +${ + context.clarifications.length > 0 + ? `Previous clarifications provided:\n${context.clarifications.join('\n')}` + : '' +} + +${goal} + +If you need more information to write a proper email (recipient, tone, specific details), ask for clarification. +If you have enough information, submit the email with recipient, subject, and body.`; + + const openai = createOpenAI({ apiKey }); + const result = await generateText({ + model: openai.chat('gpt-5-mini'), + system: systemPrompt, + messages: [{ role: 'user', content: goal }], + tools, + toolChoice: 'required', + }); + + const toolResult = result.toolResults[0]; + return ( + (toolResult?.result as { type: string; [key: string]: unknown }) ?? null + ); +} diff --git a/examples_old/hono/src/db.ts b/examples_old/hono/src/db.ts new file mode 100644 index 0000000..4aa0d30 --- /dev/null +++ b/examples_old/hono/src/db.ts @@ -0,0 +1,76 @@ +import type { StateValue } from 'xstate'; + +export interface StateEntry { + id: string; + value: StateValue; + context: Record; + event: { type: string; [key: string]: unknown } | null; + timestamp: number; +} + +export interface Session { + sessionId: string; + value: StateValue; + context: Record; + history: StateEntry[]; + createdAt: number; +} + +export interface SessionDB { + createSession(initialContext: Record): string; + getSession(sessionId: string): Session | null; + appendState( + sessionId: string, + entry: { + value: StateValue; + context: Record; + event: { type: string; [key: string]: unknown } | null; + } + ): void; +} + +// In-memory implementation +const sessions = new Map(); + +export const db: SessionDB = { + createSession(initialContext) { + const sessionId = crypto.randomUUID(); + const now = Date.now(); + const session: Session = { + sessionId, + value: 'checking', + context: initialContext, + history: [ + { + id: crypto.randomUUID(), + value: 'checking', + context: initialContext, + event: null, + timestamp: now, + }, + ], + createdAt: now, + }; + sessions.set(sessionId, session); + return sessionId; + }, + + getSession(sessionId) { + return sessions.get(sessionId) ?? null; + }, + + appendState(sessionId, entry) { + const session = sessions.get(sessionId); + if (!session) throw new Error('Session not found'); + + const stateEntry: StateEntry = { + id: crypto.randomUUID(), + timestamp: Date.now(), + ...entry, + }; + + session.history.push(stateEntry); + session.value = entry.value; + session.context = entry.context; + }, +}; diff --git a/examples_old/hono/src/events.ts b/examples_old/hono/src/events.ts new file mode 100644 index 0000000..b37788b --- /dev/null +++ b/examples_old/hono/src/events.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +// Events the agent can choose +export const agentEvents = { + askForClarification: z + .object({ + questions: z + .array(z.string()) + .describe('Questions to ask the user for clarification'), + }) + .describe('Ask the user for more information before drafting email'), + + submitEmail: z + .object({ + recipient: z.string().describe('Email recipient address'), + subject: z.string().describe('Email subject line'), + body: z.string().describe('Email body content'), + }) + .describe('Submit the final drafted email'), +}; + +// Events the user sends +export const userEvents = { + provideClarification: z.object({ + answers: z.string().describe('User answers to clarification questions'), + }), + + confirm: z.object({}).describe('Confirm and send the email'), +}; + +export type AgentEventType = keyof typeof agentEvents; +export type UserEventType = keyof typeof userEvents; diff --git a/examples_old/hono/src/index.ts b/examples_old/hono/src/index.ts new file mode 100644 index 0000000..09606ac --- /dev/null +++ b/examples_old/hono/src/index.ts @@ -0,0 +1,362 @@ +import { Hono } from 'hono'; +import { html, raw } from 'hono/html'; +import { emailMachine } from './machine'; +import { db } from './db'; +import { getAgentDecision, requiresAgentDecision } from './agent'; +import { transition } from 'xstate'; + +type Bindings = { + OPENAI_API_KEY: string; +}; + +const app = new Hono<{ Bindings: Bindings }>(); + +// GET / - Simple UI +app.get('/', async (c) => { + const sessionId = c.req.query('sessionId'); + let initialSession = null; + + if (sessionId) { + const session = db.getSession(sessionId); + if (session) { + initialSession = { + sessionId, + state: { value: session.value, context: session.context }, + }; + } + } + + return c.html(html` + + + + Email Agent + + + +

Email Agent

+ +
+ + +
+ + + + + + + `); +}); + +// POST /sessions - Start new email session +app.post('/sessions', async (c) => { + const body = await c.req.json<{ userRequest: string }>(); + + const sessionId = db.createSession({ + userRequest: body.userRequest, + recipient: '', + subject: '', + body: '', + clarifications: [], + questions: [], + }); + + const session = db.getSession(sessionId)!; + + const response: { + sessionId: string; + state: { value: unknown; context: Record }; + agentResponse?: { type: string; [key: string]: unknown }; + } = { + sessionId, + state: { value: session.value, context: session.context }, + }; + + // Initial state requires agent decision + if (requiresAgentDecision(session.value)) { + console.log('requiresAgentDecision', session.value); + const event = await getAgentDecision( + { value: session.value, context: session.context }, + 'Help the user draft and send an email based on their request.', + c.env.OPENAI_API_KEY + ); + + console.log('event', event); + + if (event) { + const resolvedState = emailMachine.resolveState({ + value: session.value, + context: session.context, + }); + const [nextState] = transition(emailMachine, resolvedState, event as any); + + console.log('nextState', nextState); + + db.appendState(sessionId, { + value: nextState.value, + context: nextState.context, + event, + }); + + response.state = { value: nextState.value, context: nextState.context }; + response.agentResponse = event; + } + } + + return c.json(response); +}); + +// POST /sessions/:id/events - Send event to session +app.post('/sessions/:id/events', async (c) => { + const sessionId = c.req.param('id'); + const session = db.getSession(sessionId); + + if (!session) { + return c.json({ error: 'Session not found' }, 404); + } + + const event = await c.req.json<{ type: string; [key: string]: unknown }>(); + + // Transition with user event + const resolvedState = emailMachine.resolveState({ + value: session.value, + context: session.context, + }); + const [nextState] = transition(emailMachine, resolvedState, event as any); + + db.appendState(sessionId, { + value: nextState.value, + context: nextState.context, + event, + }); + + const response: { + state: { value: unknown; context: Record }; + agentResponse?: { type: string; [key: string]: unknown }; + } = { + state: { value: nextState.value, context: nextState.context }, + }; + + // If new state requires agent, call LLM + if (requiresAgentDecision(nextState.value)) { + console.log('requiresAgentDecision', nextState.value); + const agentEvent = await getAgentDecision( + { value: nextState.value, context: nextState.context }, + 'Continue helping draft the email based on the clarifications provided.', + c.env.OPENAI_API_KEY + ); + + console.log('agentEvent', agentEvent); + + if (agentEvent) { + const [afterAgentState] = transition( + emailMachine, + emailMachine.resolveState({ + value: nextState.value, + context: nextState.context, + }), + agentEvent as any + ); + + console.log('afterAgentState', afterAgentState); + + db.appendState(sessionId, { + value: afterAgentState.value, + context: afterAgentState.context, + event: agentEvent, + }); + + response.state = { + value: afterAgentState.value, + context: afterAgentState.context, + }; + response.agentResponse = agentEvent; + } + } + + return c.json(response); +}); + +// GET /sessions/:id - Get current state +app.get('/sessions/:id', (c) => { + const sessionId = c.req.param('id'); + const session = db.getSession(sessionId); + + if (!session) { + return c.json({ error: 'Session not found' }, 404); + } + + return c.json({ + sessionId, + state: { value: session.value, context: session.context }, + }); +}); + +// GET /sessions/:id/history - Get full append-only history +app.get('/sessions/:id/history', (c) => { + const sessionId = c.req.param('id'); + const session = db.getSession(sessionId); + + if (!session) { + return c.json({ error: 'Session not found' }, 404); + } + + return c.json({ sessionId, history: session.history }); +}); + +export default app; diff --git a/examples_old/hono/src/machine.ts b/examples_old/hono/src/machine.ts new file mode 100644 index 0000000..bdcf230 --- /dev/null +++ b/examples_old/hono/src/machine.ts @@ -0,0 +1,74 @@ +import { setup, assign } from 'xstate'; + +export const emailMachine = setup({ + types: { + context: {} as { + userRequest: string; + recipient: string; + subject: string; + body: string; + clarifications: string[]; + questions: string[]; + }, + events: {} as + | { type: 'askForClarification'; questions: string[] } + | { type: 'provideClarification'; answers: string } + | { type: 'submitEmail'; recipient: string; subject: string; body: string } + | { type: 'confirm' }, + }, +}).createMachine({ + id: 'emailAgent', + initial: 'checking', + context: { + userRequest: '', + recipient: '', + subject: '', + body: '', + clarifications: [], + questions: [], + }, + states: { + checking: { + // Agent decides: askForClarification or submitEmail + on: { + askForClarification: { + target: 'clarifying', + actions: assign({ + questions: ({ event }) => event.questions, + }), + }, + submitEmail: { + target: 'submitting', + actions: assign({ + recipient: ({ event }) => event.recipient, + subject: ({ event }) => event.subject, + body: ({ event }) => event.body, + }), + }, + }, + }, + clarifying: { + // Wait for user clarification + on: { + provideClarification: { + target: 'checking', + actions: assign({ + clarifications: ({ context, event }) => [ + ...context.clarifications, + event.answers, + ], + }), + }, + }, + }, + submitting: { + // User confirms or edits + on: { + confirm: 'done', + }, + }, + done: { + type: 'final', + }, + }, +}); diff --git a/examples_old/hono/src/style.css b/examples_old/hono/src/style.css new file mode 100644 index 0000000..50969c8 --- /dev/null +++ b/examples_old/hono/src/style.css @@ -0,0 +1,3 @@ +h1 { + font-family: Arial, Helvetica, sans-serif; +} diff --git a/examples_old/hono/src/utils.ts b/examples_old/hono/src/utils.ts new file mode 100644 index 0000000..5d9636b --- /dev/null +++ b/examples_old/hono/src/utils.ts @@ -0,0 +1,29 @@ +import type { AnyMachineSnapshot, AnyStateNode } from 'xstate'; + +export interface TransitionData { + eventType: string; + description?: string; +} + +export function getAllTransitions(state: AnyMachineSnapshot): TransitionData[] { + const nodes = state._nodes; + const transitions = (nodes as AnyStateNode[]) + .map((node) => [...(node as AnyStateNode).transitions.values()]) + .map((nodeTransitions) => { + return nodeTransitions.map((nodeEventTransitions) => { + return nodeEventTransitions.map((transition) => ({ + eventType: transition.eventType, + description: transition.description, + })); + }); + }) + .flat(2); + + return transitions; +} + +export function randomId() { + const timestamp = Date.now().toString(36); + const random = Math.random().toString(36).substring(2, 9); + return timestamp + random; +} diff --git a/examples_old/hono/tsconfig.json b/examples_old/hono/tsconfig.json new file mode 100644 index 0000000..fe4b04f --- /dev/null +++ b/examples_old/hono/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "lib": [ + "ESNext" + ], + "types": ["vite/client"], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + }, +} \ No newline at end of file diff --git a/examples_old/hono/vite.config.ts b/examples_old/hono/vite.config.ts new file mode 100644 index 0000000..f626b72 --- /dev/null +++ b/examples_old/hono/vite.config.ts @@ -0,0 +1,6 @@ +import { cloudflare } from '@cloudflare/vite-plugin' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [cloudflare()] +}) diff --git a/examples_old/hono/wrangler.jsonc b/examples_old/hono/wrangler.jsonc new file mode 100644 index 0000000..8441ec8 --- /dev/null +++ b/examples_old/hono/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "hono", + "compatibility_date": "2025-08-03", + "main": "./src/index.ts" +} \ No newline at end of file diff --git a/examples/joke.ts b/examples_old/joke.ts similarity index 100% rename from examples/joke.ts rename to examples_old/joke.ts diff --git a/examples/multi.ts b/examples_old/multi.ts similarity index 100% rename from examples/multi.ts rename to examples_old/multi.ts diff --git a/examples/newspaper.ts b/examples_old/newspaper.ts similarity index 100% rename from examples/newspaper.ts rename to examples_old/newspaper.ts diff --git a/examples/number.ts b/examples_old/number.ts similarity index 100% rename from examples/number.ts rename to examples_old/number.ts diff --git a/examples/raffle.ts b/examples_old/raffle.ts similarity index 100% rename from examples/raffle.ts rename to examples_old/raffle.ts diff --git a/examples/sandbox.ts b/examples_old/sandbox.ts similarity index 100% rename from examples/sandbox.ts rename to examples_old/sandbox.ts diff --git a/examples/simple.ts b/examples_old/simple.ts similarity index 100% rename from examples/simple.ts rename to examples_old/simple.ts diff --git a/examples/support.ts b/examples_old/support.ts similarity index 100% rename from examples/support.ts rename to examples_old/support.ts diff --git a/examples/ticTacToe.ts b/examples_old/ticTacToe.ts similarity index 100% rename from examples/ticTacToe.ts rename to examples_old/ticTacToe.ts diff --git a/examples/todo.ts b/examples_old/todo.ts similarity index 100% rename from examples/todo.ts rename to examples_old/todo.ts diff --git a/examples/tutor.ts b/examples_old/tutor.ts similarity index 100% rename from examples/tutor.ts rename to examples_old/tutor.ts diff --git a/examples/verify.ts b/examples_old/verify.ts similarity index 100% rename from examples/verify.ts rename to examples_old/verify.ts diff --git a/examples/weather.ts b/examples_old/weather.ts similarity index 100% rename from examples/weather.ts rename to examples_old/weather.ts diff --git a/examples/wiki.ts b/examples_old/wiki.ts similarity index 100% rename from examples/wiki.ts rename to examples_old/wiki.ts diff --git a/examples/word.ts b/examples_old/word.ts similarity index 100% rename from examples/word.ts rename to examples_old/word.ts diff --git a/package.json b/package.json index 019cbb9..a4b1c0e 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,62 @@ { "name": "@statelyai/agent", - "version": "1.1.6", - "description": "Stateful agents that make decisions based on finite-state machine models", - "main": "dist/index.js", + "version": "2.0.0", + "description": "Lightweight, stateless, framework-agnostic state-machine-driven AI agents", + "type": "module", + "main": "dist/index.cjs", "module": "dist/index.mjs", - "types": "dist/index.d.ts", + "types": "dist/index.d.mts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./ai-sdk": { + "import": { + "types": "./dist/ai-sdk.d.mts", + "default": "./dist/ai-sdk.mjs" + }, + "require": { + "types": "./dist/ai-sdk.d.cts", + "default": "./dist/ai-sdk.cjs" + } + }, + "./graph": { + "import": { + "types": "./dist/graph.d.mts", + "default": "./dist/graph.mjs" + }, + "require": { + "types": "./dist/graph.d.cts", + "default": "./dist/graph.cjs" + } + }, + "./xstate": { + "import": { + "types": "./dist/xstate.d.mts", + "default": "./dist/xstate.mjs" + }, + "require": { + "types": "./dist/xstate.d.cts", + "default": "./dist/xstate.cjs" + } + } + }, + "files": [ + "dist" + ], "scripts": { - "build": "tsup src/index.ts --format cjs,esm --dts", + "build": "tsdown", "lint": "tsc --noEmit", "test": "vitest", "test:ci": "vitest --run", - "example": "ts-node examples/helpers/runner.ts", - "prepublishOnly": "tsup src/index.ts --format cjs,esm --dts", + "prepublishOnly": "tsdown", "changeset": "changeset", "release": "changeset publish", "version": "changeset version" @@ -20,37 +65,37 @@ "ai", "state machine", "agent", - "rl", - "reinforcement learning" + "statechart", + "classify", + "decide" ], "author": "David Khourshid ", "license": "MIT", "devDependencies": { - "@ai-sdk/openai": "^0.0.40", + "@ai-sdk/openai": "^3.0.25", "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.9", - "@langchain/community": "^0.0.53", - "@langchain/core": "^0.1.63", - "@langchain/openai": "^0.0.28", "@types/node": "^20.16.10", - "@types/object-hash": "^3.0.6", "dotenv": "^16.4.5", - "json-schema-to-ts": "^3.1.1", - "ts-node": "^10.9.2", - "tsup": "^8.3.0", + "tsdown": "^0.21.7", "typescript": "^5.6.2", "vitest": "^2.1.2", - "wikipedia": "^2.1.2", - "zod": "^3.23.8" + "zod": "^4.3.6" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } }, "publishConfig": { "access": "public" }, "dependencies": { - "@xstate/graph": "^2.0.1", - "ai": "^3.4.9", - "object-hash": "^3.0.0", - "xstate": "^5.18.2" + "ai": "^6.0.67", + "xstate": "^5.26.0" }, - "packageManager": "pnpm@8.11.0" + "packageManager": "pnpm@10.28.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d617c3e..86020b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,237 +10,160 @@ importers: dependencies: '@xstate/graph': specifier: ^2.0.1 - version: 2.0.1(xstate@5.18.2) + version: 2.0.1(xstate@5.26.0) ai: - specifier: ^3.4.9 - version: 3.4.9(openai@4.67.1(zod@3.23.8))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.11(typescript@5.6.2))(zod@3.23.8) - object-hash: - specifier: ^3.0.0 - version: 3.0.0 + specifier: ^6.0.67 + version: 6.0.67(zod@4.3.6) xstate: - specifier: ^5.18.2 - version: 5.18.2 + specifier: ^5.26.0 + version: 5.26.0 devDependencies: '@ai-sdk/openai': - specifier: ^0.0.40 - version: 0.0.40(zod@3.23.8) + specifier: ^3.0.25 + version: 3.0.25(zod@4.3.6) '@changesets/changelog-github': specifier: ^0.5.0 - version: 0.5.0 + version: 0.5.2 '@changesets/cli': specifier: ^2.27.9 - version: 2.27.9 - '@langchain/community': - specifier: ^0.0.53 - version: 0.0.53(openai@4.67.1(zod@3.23.8)) - '@langchain/core': - specifier: ^0.1.63 - version: 0.1.63(openai@4.67.1(zod@3.23.8)) - '@langchain/openai': - specifier: ^0.0.28 - version: 0.0.28 + version: 2.29.8(@types/node@20.19.30) '@types/node': specifier: ^20.16.10 - version: 20.16.10 - '@types/object-hash': - specifier: ^3.0.6 - version: 3.0.6 + version: 20.19.30 dotenv: specifier: ^16.4.5 - version: 16.4.5 - json-schema-to-ts: - specifier: ^3.1.1 - version: 3.1.1 - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@20.16.10)(typescript@5.6.2) - tsup: - specifier: ^8.3.0 - version: 8.3.0(postcss@8.4.47)(typescript@5.6.2) + version: 16.6.1 + tsdown: + specifier: ^0.21.7 + version: 0.21.7(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(typescript@5.9.3) typescript: specifier: ^5.6.2 - version: 5.6.2 + version: 5.9.3 vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@20.16.10) - wikipedia: - specifier: ^2.1.2 - version: 2.1.2 + version: 2.1.9(@types/node@20.19.30) zod: - specifier: ^3.23.8 - version: 3.23.8 + specifier: ^4.3.6 + version: 4.3.6 packages: - '@ai-sdk/openai@0.0.40': - resolution: {integrity: sha512-9Iq1UaBHA5ZzNv6j3govuKGXrbrjuWvZIgWNJv4xzXlDMHu9P9hnqlBr/Aiay54WwCuTVNhTzAUTfFgnTs2kbQ==, tarball: https://registry.npmjs.org/@ai-sdk/openai/-/openai-0.0.40.tgz} + '@ai-sdk/gateway@3.0.32': + resolution: {integrity: sha512-7clZRr07P9rpur39t1RrbIe7x8jmwnwUWI8tZs+BvAfX3NFgdSVGGIaT7bTz2pb08jmLXzTSDbrOTqAQ7uBkBQ==, tarball: https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.32.tgz} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@1.0.20': - resolution: {integrity: sha512-ngg/RGpnA00eNOWEtXHenpX1MsM2QshQh4QJFjUfwcqHpM5kTfG7je7Rc3HcEDP+OkRVv2GF+X4fC1Vfcnl8Ow==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.20.tgz} + '@ai-sdk/openai@3.0.25': + resolution: {integrity: sha512-DsaN46R98+D1W3lU3fKuPU3ofacboLaHlkAwxJPgJ8eup1AJHmPK1N1y10eJJbJcF6iby8Tf/vanoZxc9JPUfw==, tarball: https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.25.tgz} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true + zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@1.0.5': - resolution: {integrity: sha512-XfOawxk95X3S43arn2iQIFyWGMi0DTxsf9ETc6t7bh91RPWOOPYN1tsmS5MTKD33OGJeaDQ/gnVRzXUCRBrckQ==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.5.tgz} + '@ai-sdk/provider-utils@4.0.13': + resolution: {integrity: sha512-HHG72BN4d+OWTcq2NwTxOm/2qvk1duYsnhCDtsbYwn/h/4zeqURu1S0+Cn0nY2Ysq9a9HGKvrYuMn9bgFhR2Og==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.13.tgz} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true + zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider@0.0.14': - resolution: {integrity: sha512-gaQ5Y033nro9iX1YUjEDFDRhmMcEiCk56LJdIUbX5ozEiCNCfpiBpEqrjSp/Gp5RzBS2W0BVxfG7UGW6Ezcrzg==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.14.tgz} + '@ai-sdk/provider@3.0.7': + resolution: {integrity: sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.7.tgz} engines: {node: '>=18'} - '@ai-sdk/provider@0.0.24': - resolution: {integrity: sha512-XMsNGJdGO+L0cxhhegtqZ8+T6nn4EoShS819OvCgI2kLbYTIvk0GWFGD0AXJmxkxs3DrpsJxKAFukFR7bvTkgQ==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.24.tgz} - engines: {node: '>=18'} - - '@ai-sdk/react@0.0.62': - resolution: {integrity: sha512-1asDpxgmeHWL0/EZPCLENxfOHT+0jce0z/zasRhascodm2S6f6/KZn5doLG9jdmarcb+GjMjFmmwyOVXz3W1xg==, tarball: https://registry.npmjs.org/@ai-sdk/react/-/react-0.0.62.tgz} - engines: {node: '>=18'} - peerDependencies: - react: ^18 || ^19 - zod: ^3.0.0 - peerDependenciesMeta: - react: - optional: true - zod: - optional: true - - '@ai-sdk/solid@0.0.49': - resolution: {integrity: sha512-KnfWTt640cS1hM2fFIba8KHSPLpOIWXtEm28pNCHTvqasVKlh2y/zMQANTwE18pF2nuXL9P9F5/dKWaPsaEzQw==, tarball: https://registry.npmjs.org/@ai-sdk/solid/-/solid-0.0.49.tgz} - engines: {node: '>=18'} - peerDependencies: - solid-js: ^1.7.7 - peerDependenciesMeta: - solid-js: - optional: true - - '@ai-sdk/svelte@0.0.51': - resolution: {integrity: sha512-aIZJaIds+KpCt19yUDCRDWebzF/17GCY7gN9KkcA2QM6IKRO5UmMcqEYja0ZmwFQPm1kBZkF2njhr8VXis2mAw==, tarball: https://registry.npmjs.org/@ai-sdk/svelte/-/svelte-0.0.51.tgz} - engines: {node: '>=18'} - peerDependencies: - svelte: ^3.0.0 || ^4.0.0 - peerDependenciesMeta: - svelte: - optional: true - - '@ai-sdk/ui-utils@0.0.46': - resolution: {integrity: sha512-ZG/wneyJG+6w5Nm/hy1AKMuRgjPQToAxBsTk61c9sVPUTaxo+NNjM2MhXQMtmsja2N5evs8NmHie+ExEgpL3cA==, tarball: https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-0.0.46.tgz} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - - '@ai-sdk/vue@0.0.54': - resolution: {integrity: sha512-Ltu6gbuii8Qlp3gg7zdwdnHdS4M8nqKDij2VVO1223VOtIFwORFJzKqpfx44U11FW8z2TPVBYN+FjkyVIcN2hg==, tarball: https://registry.npmjs.org/@ai-sdk/vue/-/vue-0.0.54.tgz} - engines: {node: '>=18'} - peerDependencies: - vue: ^3.3.4 - peerDependenciesMeta: - vue: - optional: true - - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==, tarball: https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz} - engines: {node: '>=6.0.0'} + '@babel/generator@8.0.0-rc.3': + resolution: {integrity: sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==, tarball: https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.3.tgz} + engines: {node: ^20.19.0 || >=22.12.0} - '@babel/helper-string-parser@7.25.7': - resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==, tarball: https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz} - engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@8.0.0-rc.3': + resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==, tarball: https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.3.tgz} + engines: {node: ^20.19.0 || >=22.12.0} - '@babel/helper-validator-identifier@7.25.7': - resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==, tarball: https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz} - engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.0-rc.3': + resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==, tarball: https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.3.tgz} + engines: {node: ^20.19.0 || >=22.12.0} - '@babel/parser@7.25.7': - resolution: {integrity: sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==, tarball: https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz} - engines: {node: '>=6.0.0'} + '@babel/parser@8.0.0-rc.3': + resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==, tarball: https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.3.tgz} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - '@babel/runtime@7.25.7': - resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==, tarball: https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==, tarball: https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz} engines: {node: '>=6.9.0'} - '@babel/types@7.25.7': - resolution: {integrity: sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==, tarball: https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz} - engines: {node: '>=6.9.0'} + '@babel/types@8.0.0-rc.3': + resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==, tarball: https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.3.tgz} + engines: {node: ^20.19.0 || >=22.12.0} - '@changesets/apply-release-plan@7.0.5': - resolution: {integrity: sha512-1cWCk+ZshEkSVEZrm2fSj1Gz8sYvxgUL4Q78+1ZZqeqfuevPTPk033/yUZ3df8BKMohkqqHfzj0HOOrG0KtXTw==, tarball: https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.5.tgz} + '@changesets/apply-release-plan@7.0.14': + resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==, tarball: https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.14.tgz} - '@changesets/assemble-release-plan@6.0.4': - resolution: {integrity: sha512-nqICnvmrwWj4w2x0fOhVj2QEGdlUuwVAwESrUo5HLzWMI1rE5SWfsr9ln+rDqWB6RQ2ZyaMZHUcU7/IRaUJS+Q==, tarball: https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.4.tgz} + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==, tarball: https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.9.tgz} - '@changesets/changelog-git@0.2.0': - resolution: {integrity: sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==, tarball: https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.0.tgz} + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==, tarball: https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz} - '@changesets/changelog-github@0.5.0': - resolution: {integrity: sha512-zoeq2LJJVcPJcIotHRJEEA2qCqX0AQIeFE+L21L8sRLPVqDhSXY8ZWAt2sohtBpFZkBwu+LUwMSKRr2lMy3LJA==, tarball: https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.5.0.tgz} + '@changesets/changelog-github@0.5.2': + resolution: {integrity: sha512-HeGeDl8HaIGj9fQHo/tv5XKQ2SNEi9+9yl1Bss1jttPqeiASRXhfi0A2wv8yFKCp07kR1gpOI5ge6+CWNm1jPw==, tarball: https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.5.2.tgz} - '@changesets/cli@2.27.9': - resolution: {integrity: sha512-q42a/ZbDnxPpCb5Wkm6tMVIxgeI9C/bexntzTeCFBrQEdpisQqk8kCHllYZMDjYtEc1ZzumbMJAG8H0Z4rdvjg==, tarball: https://registry.npmjs.org/@changesets/cli/-/cli-2.27.9.tgz} + '@changesets/cli@2.29.8': + resolution: {integrity: sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==, tarball: https://registry.npmjs.org/@changesets/cli/-/cli-2.29.8.tgz} hasBin: true - '@changesets/config@3.0.3': - resolution: {integrity: sha512-vqgQZMyIcuIpw9nqFIpTSNyc/wgm/Lu1zKN5vECy74u95Qx/Wa9g27HdgO4NkVAaq+BGA8wUc/qvbvVNs93n6A==, tarball: https://registry.npmjs.org/@changesets/config/-/config-3.0.3.tgz} + '@changesets/config@3.1.2': + resolution: {integrity: sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==, tarball: https://registry.npmjs.org/@changesets/config/-/config-3.1.2.tgz} '@changesets/errors@0.2.0': resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==, tarball: https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz} - '@changesets/get-dependents-graph@2.1.2': - resolution: {integrity: sha512-sgcHRkiBY9i4zWYBwlVyAjEM9sAzs4wYVwJUdnbDLnVG3QwAaia1Mk5P8M7kraTOZN+vBET7n8KyB0YXCbFRLQ==, tarball: https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.2.tgz} + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==, tarball: https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.3.tgz} - '@changesets/get-github-info@0.6.0': - resolution: {integrity: sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==, tarball: https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.6.0.tgz} + '@changesets/get-github-info@0.7.0': + resolution: {integrity: sha512-+i67Bmhfj9V4KfDeS1+Tz3iF32btKZB2AAx+cYMqDSRFP7r3/ZdGbjCo+c6qkyViN9ygDuBjzageuPGJtKGe5A==, tarball: https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.7.0.tgz} - '@changesets/get-release-plan@4.0.4': - resolution: {integrity: sha512-SicG/S67JmPTrdcc9Vpu0wSQt7IiuN0dc8iR5VScnnTVPfIaLvKmEGRvIaF0kcn8u5ZqLbormZNTO77bCEvyWw==, tarball: https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.4.tgz} + '@changesets/get-release-plan@4.0.14': + resolution: {integrity: sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==, tarball: https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.14.tgz} '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==, tarball: https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz} - '@changesets/git@3.0.1': - resolution: {integrity: sha512-pdgHcYBLCPcLd82aRcuO0kxCDbw/yISlOtkmwmE8Odo1L6hSiZrBOsRl84eYG7DRCab/iHnOkWqExqc4wxk2LQ==, tarball: https://registry.npmjs.org/@changesets/git/-/git-3.0.1.tgz} + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==, tarball: https://registry.npmjs.org/@changesets/git/-/git-3.0.4.tgz} '@changesets/logger@0.1.1': resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==, tarball: https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz} - '@changesets/parse@0.4.0': - resolution: {integrity: sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==, tarball: https://registry.npmjs.org/@changesets/parse/-/parse-0.4.0.tgz} + '@changesets/parse@0.4.2': + resolution: {integrity: sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==, tarball: https://registry.npmjs.org/@changesets/parse/-/parse-0.4.2.tgz} - '@changesets/pre@2.0.1': - resolution: {integrity: sha512-vvBJ/If4jKM4tPz9JdY2kGOgWmCowUYOi5Ycv8dyLnEE8FgpYYUo1mgJZxcdtGGP3aG8rAQulGLyyXGSLkIMTQ==, tarball: https://registry.npmjs.org/@changesets/pre/-/pre-2.0.1.tgz} + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==, tarball: https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz} - '@changesets/read@0.6.1': - resolution: {integrity: sha512-jYMbyXQk3nwP25nRzQQGa1nKLY0KfoOV7VLgwucI0bUO8t8ZLCr6LZmgjXsiKuRDc+5A6doKPr9w2d+FEJ55zQ==, tarball: https://registry.npmjs.org/@changesets/read/-/read-0.6.1.tgz} + '@changesets/read@0.6.6': + resolution: {integrity: sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==, tarball: https://registry.npmjs.org/@changesets/read/-/read-0.6.6.tgz} - '@changesets/should-skip-package@0.1.1': - resolution: {integrity: sha512-H9LjLbF6mMHLtJIc/eHR9Na+MifJ3VxtgP/Y+XLn4BF7tDTEN1HNYtH6QMcjP1uxp9sjaFYmW8xqloaCi/ckTg==, tarball: https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.1.tgz} + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==, tarball: https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz} '@changesets/types@4.1.0': resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==, tarball: https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz} - '@changesets/types@6.0.0': - resolution: {integrity: sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==, tarball: https://registry.npmjs.org/@changesets/types/-/types-6.0.0.tgz} + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==, tarball: https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz} - '@changesets/write@0.3.2': - resolution: {integrity: sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw==, tarball: https://registry.npmjs.org/@changesets/write/-/write-0.3.2.tgz} + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==, tarball: https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz} - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==, tarball: https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz} - engines: {node: '>=12'} + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==, tarball: https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz} + + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==, tarball: https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==, tarball: https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz} '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==, tarball: https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz} @@ -248,750 +171,459 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.23.1': - resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==, tarball: https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.23.1': - resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.23.1': - resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [android] - '@esbuild/android-x64@0.23.1': - resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.23.1': - resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.23.1': - resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.23.1': - resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.23.1': - resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.23.1': - resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.23.1': - resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz} engines: {node: '>=12'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.23.1': - resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz} engines: {node: '>=12'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.23.1': - resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz} engines: {node: '>=12'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.23.1': - resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz} engines: {node: '>=12'} cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.23.1': - resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz} engines: {node: '>=12'} cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.23.1': - resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz} engines: {node: '>=12'} cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.23.1': - resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.23.1': - resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.23.1': - resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.23.1': - resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==, tarball: https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.23.1': - resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.23.1': - resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.23.1': - resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz} engines: {node: '>=12'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.23.1': - resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.23.1': - resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz} + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==, tarball: https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz} engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==, tarball: https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz} - engines: {node: '>=12'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==, tarball: https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, tarball: https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, tarball: https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==, tarball: https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz} - engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, tarball: https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==, tarball: https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, tarball: https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==, tarball: https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz} + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==, tarball: https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz} - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==, tarball: https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz} + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==, tarball: https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz} - '@langchain/community@0.0.53': - resolution: {integrity: sha512-iFqZPt4MRssGYsQoKSXWJQaYTZCC7WNuilp2JCCs3wKmJK3l6mR0eV+PDrnT+TaDHUVxt/b0rwgM0sOiy0j2jA==, tarball: https://registry.npmjs.org/@langchain/community/-/community-0.0.53.tgz} - engines: {node: '>=18'} + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==, tarball: https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz} peerDependencies: - '@aws-crypto/sha256-js': ^5.0.0 - '@aws-sdk/client-bedrock-agent-runtime': ^3.485.0 - '@aws-sdk/client-bedrock-runtime': ^3.422.0 - '@aws-sdk/client-dynamodb': ^3.310.0 - '@aws-sdk/client-kendra': ^3.352.0 - '@aws-sdk/client-lambda': ^3.310.0 - '@aws-sdk/client-sagemaker-runtime': ^3.310.0 - '@aws-sdk/client-sfn': ^3.310.0 - '@aws-sdk/credential-provider-node': ^3.388.0 - '@azure/search-documents': ^12.0.0 - '@clickhouse/client': ^0.2.5 - '@cloudflare/ai': '*' - '@datastax/astra-db-ts': ^1.0.0 - '@elastic/elasticsearch': ^8.4.0 - '@getmetal/metal-sdk': '*' - '@getzep/zep-js': ^0.9.0 - '@gomomento/sdk': ^1.51.1 - '@gomomento/sdk-core': ^1.51.1 - '@google-ai/generativelanguage': ^0.2.1 - '@gradientai/nodejs-sdk': ^1.2.0 - '@huggingface/inference': ^2.6.4 - '@mozilla/readability': '*' - '@neondatabase/serverless': '*' - '@opensearch-project/opensearch': '*' - '@pinecone-database/pinecone': '*' - '@planetscale/database': ^1.8.0 - '@premai/prem-sdk': ^0.3.25 - '@qdrant/js-client-rest': ^1.8.2 - '@raycast/api': ^1.55.2 - '@rockset/client': ^0.9.1 - '@smithy/eventstream-codec': ^2.0.5 - '@smithy/protocol-http': ^3.0.6 - '@smithy/signature-v4': ^2.0.10 - '@smithy/util-utf8': ^2.0.0 - '@supabase/postgrest-js': ^1.1.1 - '@supabase/supabase-js': ^2.10.0 - '@tensorflow-models/universal-sentence-encoder': '*' - '@tensorflow/tfjs-converter': '*' - '@tensorflow/tfjs-core': '*' - '@upstash/redis': ^1.20.6 - '@upstash/vector': ^1.0.7 - '@vercel/kv': ^0.2.3 - '@vercel/postgres': ^0.5.0 - '@writerai/writer-sdk': ^0.40.2 - '@xata.io/client': ^0.28.0 - '@xenova/transformers': ^2.5.4 - '@zilliz/milvus2-sdk-node': '>=2.2.7' - better-sqlite3: ^9.4.0 - cassandra-driver: ^4.7.2 - cborg: ^4.1.1 - chromadb: '*' - closevector-common: 0.1.3 - closevector-node: 0.1.6 - closevector-web: 0.1.6 - cohere-ai: '*' - convex: ^1.3.1 - couchbase: ^4.3.0 - discord.js: ^14.14.1 - dria: ^0.0.3 - duck-duck-scrape: ^2.2.5 - faiss-node: ^0.5.1 - firebase-admin: ^11.9.0 || ^12.0.0 - google-auth-library: ^8.9.0 - googleapis: ^126.0.1 - hnswlib-node: ^3.0.0 - html-to-text: ^9.0.5 - interface-datastore: ^8.2.11 - ioredis: ^5.3.2 - it-all: ^3.0.4 - jsdom: '*' - jsonwebtoken: ^9.0.2 - llmonitor: ^0.5.9 - lodash: ^4.17.21 - lunary: ^0.6.11 - mongodb: '>=5.2.0' - mysql2: ^3.3.3 - neo4j-driver: '*' - node-llama-cpp: '*' - pg: ^8.11.0 - pg-copy-streams: ^6.0.5 - pickleparser: ^0.2.1 - portkey-ai: ^0.1.11 - redis: '*' - replicate: ^0.18.0 - typeorm: ^0.3.12 - typesense: ^1.5.3 - usearch: ^1.1.1 - vectordb: ^0.1.4 - voy-search: 0.6.2 - weaviate-ts-client: '*' - web-auth-library: ^1.0.3 - ws: ^8.14.2 - peerDependenciesMeta: - '@aws-crypto/sha256-js': - optional: true - '@aws-sdk/client-bedrock-agent-runtime': - optional: true - '@aws-sdk/client-bedrock-runtime': - optional: true - '@aws-sdk/client-dynamodb': - optional: true - '@aws-sdk/client-kendra': - optional: true - '@aws-sdk/client-lambda': - optional: true - '@aws-sdk/client-sagemaker-runtime': - optional: true - '@aws-sdk/client-sfn': - optional: true - '@aws-sdk/credential-provider-node': - optional: true - '@azure/search-documents': - optional: true - '@clickhouse/client': - optional: true - '@cloudflare/ai': - optional: true - '@datastax/astra-db-ts': - optional: true - '@elastic/elasticsearch': - optional: true - '@getmetal/metal-sdk': - optional: true - '@getzep/zep-js': - optional: true - '@gomomento/sdk': - optional: true - '@gomomento/sdk-core': - optional: true - '@google-ai/generativelanguage': - optional: true - '@gradientai/nodejs-sdk': - optional: true - '@huggingface/inference': - optional: true - '@mozilla/readability': - optional: true - '@neondatabase/serverless': - optional: true - '@opensearch-project/opensearch': - optional: true - '@pinecone-database/pinecone': - optional: true - '@planetscale/database': - optional: true - '@premai/prem-sdk': - optional: true - '@qdrant/js-client-rest': - optional: true - '@raycast/api': - optional: true - '@rockset/client': - optional: true - '@smithy/eventstream-codec': - optional: true - '@smithy/protocol-http': - optional: true - '@smithy/signature-v4': - optional: true - '@smithy/util-utf8': - optional: true - '@supabase/postgrest-js': - optional: true - '@supabase/supabase-js': - optional: true - '@tensorflow-models/universal-sentence-encoder': - optional: true - '@tensorflow/tfjs-converter': - optional: true - '@tensorflow/tfjs-core': - optional: true - '@upstash/redis': - optional: true - '@upstash/vector': - optional: true - '@vercel/kv': - optional: true - '@vercel/postgres': - optional: true - '@writerai/writer-sdk': - optional: true - '@xata.io/client': - optional: true - '@xenova/transformers': - optional: true - '@zilliz/milvus2-sdk-node': - optional: true - better-sqlite3: - optional: true - cassandra-driver: - optional: true - cborg: - optional: true - chromadb: - optional: true - closevector-common: - optional: true - closevector-node: - optional: true - closevector-web: - optional: true - cohere-ai: - optional: true - convex: - optional: true - couchbase: - optional: true - discord.js: - optional: true - dria: - optional: true - duck-duck-scrape: - optional: true - faiss-node: - optional: true - firebase-admin: - optional: true - google-auth-library: - optional: true - googleapis: - optional: true - hnswlib-node: - optional: true - html-to-text: - optional: true - interface-datastore: - optional: true - ioredis: - optional: true - it-all: - optional: true - jsdom: - optional: true - jsonwebtoken: - optional: true - llmonitor: - optional: true - lodash: - optional: true - lunary: - optional: true - mongodb: - optional: true - mysql2: - optional: true - neo4j-driver: - optional: true - node-llama-cpp: - optional: true - pg: - optional: true - pg-copy-streams: - optional: true - pickleparser: - optional: true - portkey-ai: - optional: true - redis: - optional: true - replicate: - optional: true - typeorm: - optional: true - typesense: - optional: true - usearch: - optional: true - vectordb: - optional: true - voy-search: - optional: true - weaviate-ts-client: - optional: true - web-auth-library: - optional: true - ws: - optional: true + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 - '@langchain/core@0.1.63': - resolution: {integrity: sha512-+fjyYi8wy6x1P+Ee1RWfIIEyxd9Ee9jksEwvrggPwwI/p45kIDTdYTblXsM13y4mNWTiACyLSdbwnPaxxdoz+w==, tarball: https://registry.npmjs.org/@langchain/core/-/core-0.1.63.tgz} - engines: {node: '>=18'} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, tarball: https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz} + engines: {node: '>= 8'} - '@langchain/openai@0.0.28': - resolution: {integrity: sha512-2s1RA3/eAnz4ahdzsMPBna9hfAqpFNlWdHiPxVGZ5yrhXsbLWWoPcF+22LCk9t0HJKtazi2GCIWc0HVXH9Abig==, tarball: https://registry.npmjs.org/@langchain/openai/-/openai-0.0.28.tgz} - engines: {node: '>=18'} + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, tarball: https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz} + engines: {node: '>= 8'} - '@manypkg/find-root@1.1.0': - resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==, tarball: https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz} + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, tarball: https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz} + engines: {node: '>= 8'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==, tarball: https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz} + engines: {node: '>=8.0.0'} + + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==, tarball: https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==, tarball: https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==, tarball: https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==, tarball: https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==, tarball: https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==, tarball: https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] - '@manypkg/get-packages@1.1.3': - resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==, tarball: https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, tarball: https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz} - engines: {node: '>= 8'} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==, tarball: https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, tarball: https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz} - engines: {node: '>= 8'} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==, tarball: https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, tarball: https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz} - engines: {node: '>= 8'} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==, tarball: https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz} + engines: {node: '>=14.0.0'} + cpu: [wasm32] - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==, tarball: https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz} - engines: {node: '>=8.0.0'} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==, tarball: https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==, tarball: https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz} - engines: {node: '>=14'} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==, tarball: https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==, tarball: https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz} - '@rollup/rollup-android-arm-eabi@4.24.0': - resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz} + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.24.0': - resolution: {integrity: sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz} + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.24.0': - resolution: {integrity: sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz} + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.24.0': - resolution: {integrity: sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz} + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.24.0': - resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz} + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz} cpu: [arm] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.24.0': - resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz} + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz} cpu: [arm] os: [linux] + libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.24.0': - resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz} + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz} cpu: [arm64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.24.0': - resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz} + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz} cpu: [arm64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': - resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz} + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz} cpu: [ppc64] os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz} + cpu: [riscv64] + os: [linux] + libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.24.0': - resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz} + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz} cpu: [riscv64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.24.0': - resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz} + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz} cpu: [s390x] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.24.0': - resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz} + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz} cpu: [x64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.24.0': - resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz} + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz} cpu: [x64] os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==, tarball: https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==, tarball: https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz} + cpu: [arm64] + os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.24.0': - resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz} + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.24.0': - resolution: {integrity: sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz} + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.24.0': - resolution: {integrity: sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz} + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz} cpu: [x64] os: [win32] - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==, tarball: https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==, tarball: https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==, tarball: https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz} + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz} + cpu: [x64] + os: [win32] - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==, tarball: https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, tarball: https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz} - '@types/diff-match-patch@1.0.36': - resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==, tarball: https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==, tarball: https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz} - '@types/estree@1.0.6': - resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz} - '@types/node-fetch@2.6.11': - resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==, tarball: https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz} + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==, tarball: https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz} '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==, tarball: https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz} - '@types/node@18.19.54': - resolution: {integrity: sha512-+BRgt0G5gYjTvdLac9sIeE0iZcJxi4Jc4PV5EUzqi+88jmQLr+fRZdv2tCTV7IHKSGxM6SaLoOXQWWUiLUItMw==, tarball: https://registry.npmjs.org/@types/node/-/node-18.19.54.tgz} - - '@types/node@20.16.10': - resolution: {integrity: sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==, tarball: https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz} - - '@types/object-hash@3.0.6': - resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==, tarball: https://registry.npmjs.org/@types/object-hash/-/object-hash-3.0.6.tgz} - - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==, tarball: https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz} + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==, tarball: https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz} - '@types/uuid@10.0.0': - resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==, tarball: https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz} + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==, tarball: https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz} + engines: {node: '>= 20'} - '@vitest/expect@2.1.2': - resolution: {integrity: sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg==, tarball: https://registry.npmjs.org/@vitest/expect/-/expect-2.1.2.tgz} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==, tarball: https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz} - '@vitest/mocker@2.1.2': - resolution: {integrity: sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA==, tarball: https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.2.tgz} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==, tarball: https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz} peerDependencies: - '@vitest/spy': 2.1.2 - msw: ^2.3.5 + msw: ^2.4.9 vite: ^5.0.0 peerDependenciesMeta: msw: @@ -999,92 +631,31 @@ packages: vite: optional: true - '@vitest/pretty-format@2.1.2': - resolution: {integrity: sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==, tarball: https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.2.tgz} - - '@vitest/runner@2.1.2': - resolution: {integrity: sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw==, tarball: https://registry.npmjs.org/@vitest/runner/-/runner-2.1.2.tgz} - - '@vitest/snapshot@2.1.2': - resolution: {integrity: sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA==, tarball: https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.2.tgz} - - '@vitest/spy@2.1.2': - resolution: {integrity: sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A==, tarball: https://registry.npmjs.org/@vitest/spy/-/spy-2.1.2.tgz} - - '@vitest/utils@2.1.2': - resolution: {integrity: sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==, tarball: https://registry.npmjs.org/@vitest/utils/-/utils-2.1.2.tgz} - - '@vue/compiler-core@3.5.11': - resolution: {integrity: sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==, tarball: https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.11.tgz} - - '@vue/compiler-dom@3.5.11': - resolution: {integrity: sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==, tarball: https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.11.tgz} - - '@vue/compiler-sfc@3.5.11': - resolution: {integrity: sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw==, tarball: https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.11.tgz} - - '@vue/compiler-ssr@3.5.11': - resolution: {integrity: sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA==, tarball: https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.11.tgz} + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==, tarball: https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz} - '@vue/reactivity@3.5.11': - resolution: {integrity: sha512-Nqo5VZEn8MJWlCce8XoyVqHZbd5P2NH+yuAaFzuNSR96I+y1cnuUiq7xfSG+kyvLSiWmaHTKP1r3OZY4mMD50w==, tarball: https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.11.tgz} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==, tarball: https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz} - '@vue/runtime-core@3.5.11': - resolution: {integrity: sha512-7PsxFGqwfDhfhh0OcDWBG1DaIQIVOLgkwA5q6MtkPiDFjp5gohVnJEahSktwSFLq7R5PtxDKy6WKURVN1UDbzA==, tarball: https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.11.tgz} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==, tarball: https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz} - '@vue/runtime-dom@3.5.11': - resolution: {integrity: sha512-GNghjecT6IrGf0UhuYmpgaOlN7kxzQBhxWEn08c/SQDxv1yy4IXI1bn81JgEpQ4IXjRxWtPyI8x0/7TF5rPfYQ==, tarball: https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.11.tgz} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==, tarball: https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz} - '@vue/server-renderer@3.5.11': - resolution: {integrity: sha512-cVOwYBxR7Wb1B1FoxYvtjJD8X/9E5nlH4VSkJy2uMA1MzYNdzAAB//l8nrmN9py/4aP+3NjWukf9PZ3TeWULaA==, tarball: https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.11.tgz} - peerDependencies: - vue: 3.5.11 - - '@vue/shared@3.5.11': - resolution: {integrity: sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==, tarball: https://registry.npmjs.org/@vue/shared/-/shared-3.5.11.tgz} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==, tarball: https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz} '@xstate/graph@2.0.1': resolution: {integrity: sha512-WWfL97yvyVISbmetqrspd6mUn13UKoHZ+/FBSU17n+YPdMrYnKaP8UDe/HjNoZAVYsR3wuQLoitTW9cxud0DIA==, tarball: https://registry.npmjs.org/@xstate/graph/-/graph-2.0.1.tgz} peerDependencies: xstate: ^5.18.2 - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==, tarball: https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz} - engines: {node: '>=6.5'} - - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==, tarball: https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz} - engines: {node: '>=0.4.0'} - - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==, tarball: https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz} - engines: {node: '>=0.4.0'} - hasBin: true - - agentkeepalive@4.5.0: - resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==, tarball: https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz} - engines: {node: '>= 8.0.0'} - - ai@3.4.9: - resolution: {integrity: sha512-wmVzpIHNGjCEjIJ/3945a/DIkz+gwObjC767ZRgO8AmtIZMO5KqvqNr7n2KF+gQrCPCMC8fM1ICQFXSvBZnBlA==, tarball: https://registry.npmjs.org/ai/-/ai-3.4.9.tgz} + ai@6.0.67: + resolution: {integrity: sha512-xBnTcByHCj3OcG6V8G1s6zvSEqK0Bdiu+IEXYcpGrve1iGFFRgcrKeZtr/WAW/7gupnSvBbDF24BEv1OOfqi1g==, tarball: https://registry.npmjs.org/ai/-/ai-6.0.67.tgz} engines: {node: '>=18'} peerDependencies: - openai: ^4.42.0 - react: ^18 || ^19 - sswr: ^2.1.0 - svelte: ^3.0.0 || ^4.0.0 - zod: ^3.0.0 - peerDependenciesMeta: - openai: - optional: true - react: - optional: true - sswr: - optional: true - svelte: - optional: true - zod: - optional: true + zod: ^3.25.76 || ^4.1.8 ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==, tarball: https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz} @@ -1094,38 +665,15 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, tarball: https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==, tarball: https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, tarball: https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==, tarball: https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz} - engines: {node: '>=10'} - - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==, tarball: https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz} - engines: {node: '>=12'} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==, tarball: https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==, tarball: https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz} - engines: {node: '>= 8'} - - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==, tarball: https://registry.npmjs.org/arg/-/arg-4.1.3.tgz} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==, tarball: https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz} + engines: {node: '>=14'} argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==, tarball: https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz} - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==, tarball: https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz} - engines: {node: '>= 0.4'} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, tarball: https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz} array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==, tarball: https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz} @@ -1135,132 +683,53 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==, tarball: https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz} engines: {node: '>=12'} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, tarball: https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz} - - axios@1.7.7: - resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==, tarball: https://registry.npmjs.org/axios/-/axios-1.7.7.tgz} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==, tarball: https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz} - engines: {node: '>= 0.4'} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, tarball: https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, tarball: https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz} + ast-kit@3.0.0-beta.1: + resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==, tarball: https://registry.npmjs.org/ast-kit/-/ast-kit-3.0.0-beta.1.tgz} + engines: {node: '>=20.19.0'} better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==, tarball: https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz} engines: {node: '>=4'} - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==, tarball: https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz} - engines: {node: '>=8'} - - binary-search@1.3.6: - resolution: {integrity: sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==, tarball: https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==, tarball: https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz} + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==, tarball: https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, tarball: https://registry.npmjs.org/braces/-/braces-3.0.3.tgz} engines: {node: '>=8'} - bundle-require@5.0.0: - resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==, tarball: https://registry.npmjs.org/bundle-require/-/bundle-require-5.0.0.tgz} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.18' - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==, tarball: https://registry.npmjs.org/cac/-/cac-6.7.14.tgz} engines: {node: '>=8'} - camelcase@4.1.0: - resolution: {integrity: sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==, tarball: https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz} - engines: {node: '>=4'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==, tarball: https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz} - engines: {node: '>=10'} - - chai@5.1.1: - resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==, tarball: https://registry.npmjs.org/chai/-/chai-5.1.1.tgz} - engines: {node: '>=12'} + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==, tarball: https://registry.npmjs.org/cac/-/cac-7.0.0.tgz} + engines: {node: '>=20.19.0'} - chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==, tarball: https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==, tarball: https://registry.npmjs.org/chai/-/chai-5.3.3.tgz} + engines: {node: '>=18'} - chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==, tarball: https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==, tarball: https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz} - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==, tarball: https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==, tarball: https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz} engines: {node: '>= 16'} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==, tarball: https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz} - engines: {node: '>= 8.10.0'} - ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==, tarball: https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz} engines: {node: '>=8'} - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==, tarball: https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz} - - code-red@1.0.4: - resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==, tarball: https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, tarball: https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, tarball: https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz} - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, tarball: https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz} - engines: {node: '>= 0.8'} - - commander@10.0.1: - resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==, tarball: https://registry.npmjs.org/commander/-/commander-10.0.1.tgz} - engines: {node: '>=14'} - - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==, tarball: https://registry.npmjs.org/commander/-/commander-4.1.1.tgz} - engines: {node: '>= 6'} - - consola@3.2.3: - resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==, tarball: https://registry.npmjs.org/consola/-/consola-3.2.3.tgz} - engines: {node: ^14.18.0 || >=16.10.0} - - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==, tarball: https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz} - - cross-spawn@5.1.0: - resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==, tarball: https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz} - - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==, tarball: https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, tarball: https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz} engines: {node: '>= 8'} - css-tree@2.3.1: - resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==, tarball: https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==, tarball: https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz} - dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==, tarball: https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz} - debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==, tarball: https://registry.npmjs.org/debug/-/debug-4.3.7.tgz} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, tarball: https://registry.npmjs.org/debug/-/debug-4.4.3.tgz} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -1268,113 +737,83 @@ packages: supports-color: optional: true - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==, tarball: https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz} - engines: {node: '>=0.10.0'} - deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==, tarball: https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz} engines: {node: '>=6'} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, tarball: https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz} - engines: {node: '>=0.4.0'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==, tarball: https://registry.npmjs.org/defu/-/defu-6.1.4.tgz} detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==, tarball: https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz} engines: {node: '>=8'} - diff-match-patch@1.0.5: - resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==, tarball: https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz} - - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==, tarball: https://registry.npmjs.org/diff/-/diff-4.0.2.tgz} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==, tarball: https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz} engines: {node: '>=8'} - dotenv@16.4.5: - resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==, tarball: https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==, tarball: https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz} engines: {node: '>=12'} dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==, tarball: https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz} engines: {node: '>=10'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==, tarball: https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==, tarball: https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz} + dts-resolver@2.1.3: + resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==, tarball: https://registry.npmjs.org/dts-resolver/-/dts-resolver-2.1.3.tgz} + engines: {node: '>=20.19.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, tarball: https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==, tarball: https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz} + engines: {node: '>=14'} enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==, tarball: https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz} engines: {node: '>=8.6'} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==, tarball: https://registry.npmjs.org/entities/-/entities-4.5.0.tgz} - engines: {node: '>=0.12'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==, tarball: https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz} esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==, tarball: https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz} engines: {node: '>=12'} hasBin: true - esbuild@0.23.1: - resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==, tarball: https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz} - engines: {node: '>=18'} - hasBin: true - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, tarball: https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz} engines: {node: '>=4'} hasBin: true - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, tarball: https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz} - estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, tarball: https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==, tarball: https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz} - engines: {node: '>=6'} - - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==, tarball: https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz} - - eventsource-parser@1.1.2: - resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==, tarball: https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz} - engines: {node: '>=14.18'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==, tarball: https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz} + engines: {node: '>=18.0.0'} - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==, tarball: https://registry.npmjs.org/execa/-/execa-5.1.1.tgz} - engines: {node: '>=10'} - - expr-eval@2.0.2: - resolution: {integrity: sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==, tarball: https://registry.npmjs.org/expr-eval/-/expr-eval-2.0.2.tgz} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==, tarball: https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz} + engines: {node: '>=12.0.0'} extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==, tarball: https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz} - external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==, tarball: https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz} - engines: {node: '>=4'} - - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==, tarball: https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==, tarball: https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz} engines: {node: '>=8.6.0'} - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==, tarball: https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==, tarball: https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz} - fdir@6.4.0: - resolution: {integrity: sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==, tarball: https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, tarball: https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -1389,34 +828,6 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==, tarball: https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz} engines: {node: '>=8'} - flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==, tarball: https://registry.npmjs.org/flat/-/flat-5.0.2.tgz} - hasBin: true - - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==, tarball: https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - foreground-child@3.3.0: - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==, tarball: https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz} - engines: {node: '>=14'} - - form-data-encoder@1.7.2: - resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==, tarball: https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz} - - form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==, tarball: https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz} - engines: {node: '>= 6'} - - formdata-node@4.4.1: - resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==, tarball: https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz} - engines: {node: '>= 12.20'} - fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==, tarball: https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz} engines: {node: '>=6 <7 || >=8'} @@ -1430,21 +841,13 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==, tarball: https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz} - - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==, tarball: https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz} - engines: {node: '>=10'} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==, tarball: https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, tarball: https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz} engines: {node: '>= 6'} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==, tarball: https://registry.npmjs.org/glob/-/glob-10.4.5.tgz} - hasBin: true - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==, tarball: https://registry.npmjs.org/globby/-/globby-11.1.0.tgz} engines: {node: '>=10'} @@ -1452,42 +855,29 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, tarball: https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz} - human-id@1.0.2: - resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==, tarball: https://registry.npmjs.org/human-id/-/human-id-1.0.2.tgz} + hookable@6.1.0: + resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==, tarball: https://registry.npmjs.org/hookable/-/hookable-6.1.0.tgz} - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==, tarball: https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz} - engines: {node: '>=10.17.0'} - - humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==, tarball: https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz} + human-id@4.1.3: + resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==, tarball: https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz} + hasBin: true - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==, tarball: https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==, tarball: https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz} engines: {node: '>=0.10.0'} ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, tarball: https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz} engines: {node: '>= 4'} - infobox-parser@3.6.4: - resolution: {integrity: sha512-d2lTlxKZX7WsYxk9/UPt51nkmZv5tbC75SSw4hfHqZ3LpRAn6ug0oru9xI2X+S78va3aUAze3xl/UqMuwLmJUw==, tarball: https://registry.npmjs.org/infobox-parser/-/infobox-parser-3.6.4.tgz} - - is-any-array@2.0.1: - resolution: {integrity: sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==, tarball: https://registry.npmjs.org/is-any-array/-/is-any-array-2.0.1.tgz} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==, tarball: https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz} - engines: {node: '>=8'} + import-without-cache@0.2.5: + resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==, tarball: https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.2.5.tgz} + engines: {node: '>=20.19.0'} is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, tarball: https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==, tarball: https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz} - engines: {node: '>=8'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, tarball: https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz} engines: {node: '>=0.10.0'} @@ -1496,13 +886,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, tarball: https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz} engines: {node: '>=0.12.0'} - is-reference@3.0.2: - resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==, tarball: https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==, tarball: https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz} - engines: {node: '>=8'} - is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==, tarball: https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz} engines: {node: '>=4'} @@ -1514,94 +897,37 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, tarball: https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==, tarball: https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz} - - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==, tarball: https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz} - engines: {node: '>=10'} - - js-tiktoken@1.0.15: - resolution: {integrity: sha512-65ruOWWXDEZHHbAo7EjOcNxOGasQKbL4Fq3jEr2xsCqSsoOo6VVSqzWQb6PRIqypFSDcma4jO90YP0w5X8qVXQ==, tarball: https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.15.tgz} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, tarball: https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==, tarball: https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz} + hasBin: true - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==, tarball: https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==, tarball: https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz} hasBin: true - json-schema-to-ts@3.1.1: - resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==, tarball: https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz} - engines: {node: '>=16'} + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, tarball: https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz} + engines: {node: '>=6'} + hasBin: true json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==, tarball: https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz} - jsondiffpatch@0.6.0: - resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==, tarball: https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==, tarball: https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz} - langsmith@0.1.61: - resolution: {integrity: sha512-XQE4KPScwPmdaT0mWDzhNxj9gvqXUR+C7urLA0QFi27XeoQdm17eYpudenn4wxC0gIyUJutQCyuYJpfwlT5JnQ==, tarball: https://registry.npmjs.org/langsmith/-/langsmith-0.1.61.tgz} - peerDependencies: - openai: '*' - peerDependenciesMeta: - openai: - optional: true - - lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==, tarball: https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz} - engines: {node: '>=14'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==, tarball: https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz} - - load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==, tarball: https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==, tarball: https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz} - locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==, tarball: https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz} engines: {node: '>=8'} - lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==, tarball: https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==, tarball: https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz} - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==, tarball: https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz} - hasBin: true - - loupe@3.1.1: - resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==, tarball: https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, tarball: https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz} - - lru-cache@4.1.5: - resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==, tarball: https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==, tarball: https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz} - magic-string@0.30.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==, tarball: https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz} - - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==, tarball: https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz} - - mdn-data@2.0.30: - resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==, tarball: https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==, tarball: https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, tarball: https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz} merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, tarball: https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz} @@ -1611,41 +937,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, tarball: https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, tarball: https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, tarball: https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz} - engines: {node: '>= 0.6'} - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==, tarball: https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz} - engines: {node: '>=6'} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==, tarball: https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz} - engines: {node: '>=16 || 14 >=14.17'} - - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==, tarball: https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz} - engines: {node: '>=16 || 14 >=14.17'} - - ml-array-mean@1.1.6: - resolution: {integrity: sha512-MIdf7Zc8HznwIisyiJGRH9tRigg3Yf4FldW8DxKxpCCv/g5CafTw0RRu51nojVEOXuCQC7DRVVu5c7XXO/5joQ==, tarball: https://registry.npmjs.org/ml-array-mean/-/ml-array-mean-1.1.6.tgz} - - ml-array-sum@1.1.6: - resolution: {integrity: sha512-29mAh2GwH7ZmiRnup4UyibQZB9+ZLyMShvt4cH4eTK+cL2oEMIZFnSyB3SS8MlsTh6q/w/yh48KmqLxmovN4Dw==, tarball: https://registry.npmjs.org/ml-array-sum/-/ml-array-sum-1.1.6.tgz} - - ml-distance-euclidean@2.0.0: - resolution: {integrity: sha512-yC9/2o8QF0A3m/0IXqCTXCzz2pNEzvmcE/9HFKOZGnTjatvBbsn4lWYJkxENkA4Ug2fnYl7PXQxnPi21sgMy/Q==, tarball: https://registry.npmjs.org/ml-distance-euclidean/-/ml-distance-euclidean-2.0.0.tgz} - - ml-distance@4.0.1: - resolution: {integrity: sha512-feZ5ziXs01zhyFUUUeZV5hwc0f5JW0Sh0ckU1koZe/wdVkJdGxcP06KNQuF0WBTj8FttQUzcvQcpcrOp/XrlEw==, tarball: https://registry.npmjs.org/ml-distance/-/ml-distance-4.0.1.tgz} - - ml-tree-similarity@1.0.0: - resolution: {integrity: sha512-XJUyYqjSuUQkNQHMscr6tcjldsOoAekxADTplt40QKfwW6nd++1wHWV9AArl0Zvw/TIHgNaZZNvr8QGvE8wLRg==, tarball: https://registry.npmjs.org/ml-tree-similarity/-/ml-tree-similarity-1.0.0.tgz} - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==, tarball: https://registry.npmjs.org/mri/-/mri-1.2.0.tgz} engines: {node: '>=4'} @@ -1653,27 +944,11 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, tarball: https://registry.npmjs.org/ms/-/ms-2.1.3.tgz} - mustache@4.2.0: - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==, tarball: https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz} - hasBin: true - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==, tarball: https://registry.npmjs.org/mz/-/mz-2.7.0.tgz} - - nanoid@3.3.6: - resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==, tarball: https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz} - engines: {node: '>=10.5.0'} - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==, tarball: https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz} engines: {node: 4.x || >=6.0.0} @@ -1683,42 +958,8 @@ packages: encoding: optional: true - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==, tarball: https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz} - engines: {node: '>=0.10.0'} - - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==, tarball: https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz} - engines: {node: '>=8'} - - num-sort@2.1.0: - resolution: {integrity: sha512-1MQz1Ed8z2yckoBeSfkQHHO9K1yDRxxtotKSJ9yvcTUUxSvfvzEq5GwBrjjHEpMlq/k5gvXdmJ1SbYxWtpNoVg==, tarball: https://registry.npmjs.org/num-sort/-/num-sort-2.1.0.tgz} - engines: {node: '>=8'} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, tarball: https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz} - engines: {node: '>=0.10.0'} - - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==, tarball: https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz} - engines: {node: '>= 6'} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==, tarball: https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz} - engines: {node: '>=6'} - - openai@4.67.1: - resolution: {integrity: sha512-2YbRFy6qaYRJabK2zLMn4txrB2xBy0KP5g/eoqeSPTT31mIJMnkT75toagvfE555IKa2RdrzJrZwdDsUipsAMw==, tarball: https://registry.npmjs.org/openai/-/openai-4.67.1.tgz} - hasBin: true - peerDependencies: - zod: ^3.23.8 - peerDependenciesMeta: - zod: - optional: true - - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==, tarball: https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz} - engines: {node: '>=0.10.0'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==, tarball: https://registry.npmjs.org/obug/-/obug-2.1.1.tgz} outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==, tarball: https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz} @@ -1727,10 +968,6 @@ packages: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==, tarball: https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz} engines: {node: '>=8'} - p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==, tarball: https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz} - engines: {node: '>=4'} - p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==, tarball: https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz} engines: {node: '>=6'} @@ -1743,27 +980,12 @@ packages: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==, tarball: https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz} engines: {node: '>=6'} - p-queue@6.6.2: - resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==, tarball: https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz} - engines: {node: '>=8'} - - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==, tarball: https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz} - engines: {node: '>=8'} - - p-timeout@3.2.0: - resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==, tarball: https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz} - engines: {node: '>=8'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==, tarball: https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz} engines: {node: '>=6'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==, tarball: https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz} - - package-manager-detector@0.2.1: - resolution: {integrity: sha512-/hVW2fZvAdEas+wyKh0SnlZ2mx0NIa1+j11YaQkogEJkcMErbwchHCuo8z7lEtajZJQZ6rgZNVTWMVVd71Bjng==, tarball: https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.1.tgz} + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==, tarball: https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz} path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, tarball: https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz} @@ -1773,10 +995,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, tarball: https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz} engines: {node: '>=8'} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==, tarball: https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz} - engines: {node: '>=16 || 14 >=14.18'} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==, tarball: https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz} engines: {node: '>=8'} @@ -1784,52 +1002,34 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==, tarball: https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz} - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==, tarball: https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz} - engines: {node: '>= 14.16'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, tarball: https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz} - periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==, tarball: https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==, tarball: https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz} + engines: {node: '>= 14.16'} - picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==, tarball: https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, tarball: https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz} picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz} engines: {node: '>=12'} - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==, tarball: https://registry.npmjs.org/pify/-/pify-4.0.1.tgz} - engines: {node: '>=6'} - - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==, tarball: https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz} - engines: {node: '>= 6'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==, tarball: https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz} + engines: {node: '>=12'} - postcss-load-config@6.0.1: - resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==, tarball: https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - yaml: - optional: true + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==, tarball: https://registry.npmjs.org/pify/-/pify-4.0.1.tgz} + engines: {node: '>=6'} - postcss@8.4.47: - resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==, tarball: https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==, tarball: https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz} engines: {node: ^10 || ^12 || >=14} prettier@2.8.8: @@ -1837,48 +1037,56 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==, tarball: https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz} - - pseudomap@1.0.2: - resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==, tarball: https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==, tarball: https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz} - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, tarball: https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz} - engines: {node: '>=6'} + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==, tarball: https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, tarball: https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz} - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==, tarball: https://registry.npmjs.org/react/-/react-18.3.1.tgz} - engines: {node: '>=0.10.0'} - read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==, tarball: https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz} engines: {node: '>=6'} - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, tarball: https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz} - engines: {node: '>=8.10.0'} - - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==, tarball: https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==, tarball: https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz} engines: {node: '>=8'} - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==, tarball: https://registry.npmjs.org/retry/-/retry-0.13.1.tgz} - engines: {node: '>= 4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, tarball: https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz} - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==, tarball: https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==, tarball: https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.24.0: - resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==, tarball: https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz} + rolldown-plugin-dts@0.23.2: + resolution: {integrity: sha512-PbSqLawLgZBGcOGT3yqWBGn4cX+wh2nt5FuBGdcMHyOhoukmjbhYAl8NT9sE4U38Cm9tqLOIQeOrvzeayM0DLQ==, tarball: https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.23.2.tgz} + engines: {node: '>=20.19.0'} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20260325.1' + rolldown: ^1.0.0-rc.12 + typescript: ^5.0.0 || ^6.0.0 + vue-tsc: ~3.2.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==, tarball: https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==, tarball: https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1888,26 +1096,20 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, tarball: https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz} - secure-json-parse@2.7.0: - resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==, tarball: https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz} - - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==, tarball: https://registry.npmjs.org/semver/-/semver-7.6.3.tgz} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==, tarball: https://registry.npmjs.org/semver/-/semver-7.7.3.tgz} engines: {node: '>=10'} hasBin: true - shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==, tarball: https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz} - engines: {node: '>=0.10.0'} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==, tarball: https://registry.npmjs.org/semver/-/semver-7.7.4.tgz} + engines: {node: '>=10'} + hasBin: true shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, tarball: https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz} engines: {node: '>=8'} - shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==, tarball: https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz} - engines: {node: '>=0.10.0'} - shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, tarball: https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz} engines: {node: '>=8'} @@ -1915,9 +1117,6 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==, tarball: https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==, tarball: https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, tarball: https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz} engines: {node: '>=14'} @@ -1930,96 +1129,46 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, tarball: https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz} engines: {node: '>=0.10.0'} - source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==, tarball: https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz} - engines: {node: '>= 8'} - - spawndamnit@2.0.0: - resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==, tarball: https://registry.npmjs.org/spawndamnit/-/spawndamnit-2.0.0.tgz} + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==, tarball: https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz} sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==, tarball: https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz} - sswr@2.1.0: - resolution: {integrity: sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==, tarball: https://registry.npmjs.org/sswr/-/sswr-2.1.0.tgz} - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.0 - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, tarball: https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz} - std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==, tarball: https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, tarball: https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==, tarball: https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz} - engines: {node: '>=12'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==, tarball: https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz} strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, tarball: https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==, tarball: https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz} - engines: {node: '>=12'} - strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==, tarball: https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz} engines: {node: '>=4'} - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==, tarball: https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz} - engines: {node: '>=6'} - - sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==, tarball: https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - - svelte@4.2.19: - resolution: {integrity: sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==, tarball: https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz} - engines: {node: '>=16'} - - swr@2.2.5: - resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==, tarball: https://registry.npmjs.org/swr/-/swr-2.2.5.tgz} - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 - - swrev@4.0.0: - resolution: {integrity: sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==, tarball: https://registry.npmjs.org/swrev/-/swrev-4.0.0.tgz} - - swrv@1.0.4: - resolution: {integrity: sha512-zjEkcP8Ywmj+xOJW3lIT65ciY/4AL4e/Or7Gj0MzU3zBJNMdJiT8geVZhINavnlHRMMCcJLHhraLTAiDOTmQ9g==, tarball: https://registry.npmjs.org/swrv/-/swrv-1.0.4.tgz} - peerDependencies: - vue: '>=3.2.26 < 4' - term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==, tarball: https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz} engines: {node: '>=8'} - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==, tarball: https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==, tarball: https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==, tarball: https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz} - tinyexec@0.3.0: - resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==, tarball: https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==, tarball: https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz} + + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==, tarball: https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz} + engines: {node: '>=18'} - tinyglobby@0.2.9: - resolution: {integrity: sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw==, tarball: https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.9.tgz} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==, tarball: https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz} engines: {node: '>=12.0.0'} - tinypool@1.0.1: - resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==, tarball: https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==, tarball: https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz} engines: {node: ^18.0.0 || >=20.0.0} tinyrainbow@1.2.0: @@ -2030,14 +1179,6 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==, tarball: https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz} engines: {node: '>=14.0.0'} - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==, tarball: https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz} - engines: {node: '>=0.6.0'} - - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==, tarball: https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz} - engines: {node: '>=4'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, tarball: https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz} engines: {node: '>=8.0'} @@ -2045,90 +1186,73 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, tarball: https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz} - tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==, tarball: https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz} - tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==, tarball: https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz} hasBin: true - ts-algebra@2.0.0: - resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==, tarball: https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz} - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==, tarball: https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz} - - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==, tarball: https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz} + tsdown@0.21.7: + resolution: {integrity: sha512-ukKIxKQzngkWvOYJAyptudclkm4VQqbjq+9HF5K5qDO8GJsYtMh8gIRwicbnZEnvFPr6mquFwYAVZ8JKt3rY2g==, tarball: https://registry.npmjs.org/tsdown/-/tsdown-0.21.7.tgz} + engines: {node: '>=20.19.0'} hasBin: true peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.21.7 + '@tsdown/exe': 0.21.7 + '@vitejs/devtools': '*' + publint: ^0.3.0 + typescript: ^5.0.0 || ^6.0.0 + unplugin-unused: ^0.5.0 peerDependenciesMeta: - '@swc/core': + '@arethetypeswrong/core': optional: true - '@swc/wasm': + '@tsdown/css': optional: true - - tsup@8.3.0: - resolution: {integrity: sha512-ALscEeyS03IomcuNdFdc0YWGVIkwH1Ws7nfTbAPuoILvEV2hpGQAY72LIOjglGo4ShWpZfpBqP/jpQVCzqYQag==, tarball: https://registry.npmjs.org/tsup/-/tsup-8.3.0.tgz} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@microsoft/api-extractor': ^7.36.0 - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.5.0' - peerDependenciesMeta: - '@microsoft/api-extractor': + '@tsdown/exe': optional: true - '@swc/core': + '@vitejs/devtools': optional: true - postcss: + publint: optional: true typescript: optional: true + unplugin-unused: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, tarball: https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz} - typescript@5.6.2: - resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==, tarball: https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, tarball: https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz} engines: {node: '>=14.17'} hasBin: true - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz} + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==, tarball: https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz} - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==, tarball: https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz} universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==, tarball: https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz} engines: {node: '>= 4.0.0'} - use-sync-external-store@1.2.2: - resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==, tarball: https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - - uuid@10.0.0: - resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==, tarball: https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz} - hasBin: true - - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==, tarball: https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz} + unrun@0.2.34: + resolution: {integrity: sha512-LyaghRBR++r7svhDK6tnDz2XaYHWdneBOA0jbS8wnRsHerI9MFljX4fIiTgbbNbEVzZ0C9P1OjWLLe1OqoaaEw==, tarball: https://registry.npmjs.org/unrun/-/unrun-0.2.34.tgz} + engines: {node: '>=20.19.0'} hasBin: true + peerDependencies: + synckit: ^0.11.11 + peerDependenciesMeta: + synckit: + optional: true - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==, tarball: https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz} - - vite-node@2.1.2: - resolution: {integrity: sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ==, tarball: https://registry.npmjs.org/vite-node/-/vite-node-2.1.2.tgz} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==, tarball: https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite@5.4.8: - resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==, tarball: https://registry.npmjs.org/vite/-/vite-5.4.8.tgz} + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==, tarball: https://registry.npmjs.org/vite/-/vite-5.4.21.tgz} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -2158,15 +1282,15 @@ packages: terser: optional: true - vitest@2.1.2: - resolution: {integrity: sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A==, tarball: https://registry.npmjs.org/vitest/-/vitest-2.1.2.tgz} + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==, tarball: https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.2 - '@vitest/ui': 2.1.2 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -2183,34 +1307,12 @@ packages: jsdom: optional: true - vue@3.5.11: - resolution: {integrity: sha512-/8Wurrd9J3lb72FTQS7gRMNQD4nztTtKPmuDuPuhqXmmpD6+skVjAeahNpVzsuky6Sy9gy7wn8UadqPtt9SQIg==, tarball: https://registry.npmjs.org/vue/-/vue-3.5.11.tgz} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - web-streams-polyfill@4.0.0-beta.3: - resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==, tarball: https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz} - engines: {node: '>= 14'} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, tarball: https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz} - webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==, tarball: https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz} - whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, tarball: https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz} - whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==, tarball: https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz} - - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==, tarball: https://registry.npmjs.org/which/-/which-1.3.1.tgz} - hasBin: true - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, tarball: https://registry.npmjs.org/which/-/which-2.0.2.tgz} engines: {node: '>= 8'} @@ -2221,151 +1323,69 @@ packages: engines: {node: '>=8'} hasBin: true - wikipedia@2.1.2: - resolution: {integrity: sha512-RAYaMpXC9/E873RaSEtlEa8dXK4e0p5k98GKOd210MtkE5emm6fcnwD+N6ZA4cuffjDWagvhaQKtp/mGp2BOVQ==, tarball: https://registry.npmjs.org/wikipedia/-/wikipedia-2.1.2.tgz} - engines: {node: '>=10'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==, tarball: https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==, tarball: https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz} - engines: {node: '>=12'} - - xstate@5.18.2: - resolution: {integrity: sha512-hab5VOe29D0agy8/7dH1lGw+7kilRQyXwpaChoMu4fe6rDP+nsHYhDYKfS2O4iXE7myA98TW6qMEudj/8NXEkA==, tarball: https://registry.npmjs.org/xstate/-/xstate-5.18.2.tgz} + xstate@5.26.0: + resolution: {integrity: sha512-Fvi9VBoqHgsGYLU2NTag8xDTWtKqUC0+ue7EAhBNBb06wf620QEy05upBaEI1VLMzIn63zugLV8nHb69ZUWYAA==, tarball: https://registry.npmjs.org/xstate/-/xstate-5.26.0.tgz} - yallist@2.1.2: - resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==, tarball: https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz} - - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==, tarball: https://registry.npmjs.org/yn/-/yn-3.1.1.tgz} - engines: {node: '>=6'} - - zod-to-json-schema@3.23.2: - resolution: {integrity: sha512-uSt90Gzc/tUfyNqxnjlfBs8W6WSGpNBv0rVsNxP/BVSMHMKGdthPYff4xtCHYloJGM0CFxFsb3NbC0eqPhfImw==, tarball: https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.2.tgz} - peerDependencies: - zod: ^3.23.3 - - zod-to-json-schema@3.23.3: - resolution: {integrity: sha512-TYWChTxKQbRJp5ST22o/Irt9KC5nj7CdBKYB/AosCRdj/wxEMvv4NNaj9XVUHDOIp53ZxArGhnw5HMZziPFjog==, tarball: https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.3.tgz} - peerDependencies: - zod: ^3.23.3 - - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==, tarball: https://registry.npmjs.org/zod/-/zod-3.23.8.tgz} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==, tarball: https://registry.npmjs.org/zod/-/zod-4.3.6.tgz} snapshots: - '@ai-sdk/openai@0.0.40(zod@3.23.8)': - dependencies: - '@ai-sdk/provider': 0.0.14 - '@ai-sdk/provider-utils': 1.0.5(zod@3.23.8) - zod: 3.23.8 - - '@ai-sdk/provider-utils@1.0.20(zod@3.23.8)': - dependencies: - '@ai-sdk/provider': 0.0.24 - eventsource-parser: 1.1.2 - nanoid: 3.3.6 - secure-json-parse: 2.7.0 - optionalDependencies: - zod: 3.23.8 - - '@ai-sdk/provider-utils@1.0.5(zod@3.23.8)': - dependencies: - '@ai-sdk/provider': 0.0.14 - eventsource-parser: 1.1.2 - nanoid: 3.3.6 - secure-json-parse: 2.7.0 - optionalDependencies: - zod: 3.23.8 - - '@ai-sdk/provider@0.0.14': - dependencies: - json-schema: 0.4.0 - - '@ai-sdk/provider@0.0.24': - dependencies: - json-schema: 0.4.0 - - '@ai-sdk/react@0.0.62(react@18.3.1)(zod@3.23.8)': + '@ai-sdk/gateway@3.0.32(zod@4.3.6)': dependencies: - '@ai-sdk/provider-utils': 1.0.20(zod@3.23.8) - '@ai-sdk/ui-utils': 0.0.46(zod@3.23.8) - swr: 2.2.5(react@18.3.1) - optionalDependencies: - react: 18.3.1 - zod: 3.23.8 + '@ai-sdk/provider': 3.0.7 + '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) + '@vercel/oidc': 3.1.0 + zod: 4.3.6 - '@ai-sdk/solid@0.0.49(zod@3.23.8)': + '@ai-sdk/openai@3.0.25(zod@4.3.6)': dependencies: - '@ai-sdk/provider-utils': 1.0.20(zod@3.23.8) - '@ai-sdk/ui-utils': 0.0.46(zod@3.23.8) - transitivePeerDependencies: - - zod + '@ai-sdk/provider': 3.0.7 + '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) + zod: 4.3.6 - '@ai-sdk/svelte@0.0.51(svelte@4.2.19)(zod@3.23.8)': + '@ai-sdk/provider-utils@4.0.13(zod@4.3.6)': dependencies: - '@ai-sdk/provider-utils': 1.0.20(zod@3.23.8) - '@ai-sdk/ui-utils': 0.0.46(zod@3.23.8) - sswr: 2.1.0(svelte@4.2.19) - optionalDependencies: - svelte: 4.2.19 - transitivePeerDependencies: - - zod + '@ai-sdk/provider': 3.0.7 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.3.6 - '@ai-sdk/ui-utils@0.0.46(zod@3.23.8)': + '@ai-sdk/provider@3.0.7': dependencies: - '@ai-sdk/provider': 0.0.24 - '@ai-sdk/provider-utils': 1.0.20(zod@3.23.8) json-schema: 0.4.0 - secure-json-parse: 2.7.0 - zod-to-json-schema: 3.23.2(zod@3.23.8) - optionalDependencies: - zod: 3.23.8 - - '@ai-sdk/vue@0.0.54(vue@3.5.11(typescript@5.6.2))(zod@3.23.8)': - dependencies: - '@ai-sdk/provider-utils': 1.0.20(zod@3.23.8) - '@ai-sdk/ui-utils': 0.0.46(zod@3.23.8) - swrv: 1.0.4(vue@3.5.11(typescript@5.6.2)) - optionalDependencies: - vue: 3.5.11(typescript@5.6.2) - transitivePeerDependencies: - - zod - '@ampproject/remapping@2.3.0': + '@babel/generator@8.0.0-rc.3': dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 - '@babel/helper-string-parser@7.25.7': {} + '@babel/helper-string-parser@8.0.0-rc.3': {} - '@babel/helper-validator-identifier@7.25.7': {} + '@babel/helper-validator-identifier@8.0.0-rc.3': {} - '@babel/parser@7.25.7': + '@babel/parser@8.0.0-rc.3': dependencies: - '@babel/types': 7.25.7 + '@babel/types': 8.0.0-rc.3 - '@babel/runtime@7.25.7': - dependencies: - regenerator-runtime: 0.14.1 + '@babel/runtime@7.28.6': {} - '@babel/types@7.25.7': + '@babel/types@8.0.0-rc.3': dependencies: - '@babel/helper-string-parser': 7.25.7 - '@babel/helper-validator-identifier': 7.25.7 - to-fast-properties: 2.0.0 + '@babel/helper-string-parser': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 - '@changesets/apply-release-plan@7.0.5': + '@changesets/apply-release-plan@7.0.14': dependencies: - '@changesets/config': 3.0.3 + '@changesets/config': 3.1.2 '@changesets/get-version-range-type': 0.4.0 - '@changesets/git': 3.0.1 - '@changesets/should-skip-package': 0.1.1 - '@changesets/types': 6.0.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 detect-indent: 6.1.0 fs-extra: 7.0.1 @@ -2373,66 +1393,68 @@ snapshots: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 7.6.3 + semver: 7.7.3 - '@changesets/assemble-release-plan@6.0.4': + '@changesets/assemble-release-plan@6.0.9': dependencies: '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.2 - '@changesets/should-skip-package': 0.1.1 - '@changesets/types': 6.0.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 - semver: 7.6.3 + semver: 7.7.3 - '@changesets/changelog-git@0.2.0': + '@changesets/changelog-git@0.2.1': dependencies: - '@changesets/types': 6.0.0 + '@changesets/types': 6.1.0 - '@changesets/changelog-github@0.5.0': + '@changesets/changelog-github@0.5.2': dependencies: - '@changesets/get-github-info': 0.6.0 - '@changesets/types': 6.0.0 + '@changesets/get-github-info': 0.7.0 + '@changesets/types': 6.1.0 dotenv: 8.6.0 transitivePeerDependencies: - encoding - '@changesets/cli@2.27.9': + '@changesets/cli@2.29.8(@types/node@20.19.30)': dependencies: - '@changesets/apply-release-plan': 7.0.5 - '@changesets/assemble-release-plan': 6.0.4 - '@changesets/changelog-git': 0.2.0 - '@changesets/config': 3.0.3 + '@changesets/apply-release-plan': 7.0.14 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.2 '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.2 - '@changesets/get-release-plan': 4.0.4 - '@changesets/git': 3.0.1 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.14 + '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 - '@changesets/pre': 2.0.1 - '@changesets/read': 0.6.1 - '@changesets/should-skip-package': 0.1.1 - '@changesets/types': 6.0.0 - '@changesets/write': 0.3.2 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.6 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3(@types/node@20.19.30) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 ci-info: 3.9.0 enquirer: 2.4.1 - external-editor: 3.1.0 fs-extra: 7.0.1 mri: 1.2.0 p-limit: 2.3.0 - package-manager-detector: 0.2.1 - picocolors: 1.1.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 resolve-from: 5.0.0 - semver: 7.6.3 - spawndamnit: 2.0.0 + semver: 7.7.3 + spawndamnit: 3.0.1 term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' - '@changesets/config@3.0.3': + '@changesets/config@3.1.2': dependencies: '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.2 + '@changesets/get-dependents-graph': 2.1.3 '@changesets/logger': 0.1.1 - '@changesets/types': 6.0.0 + '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 micromatch: 4.0.8 @@ -2441,314 +1463,210 @@ snapshots: dependencies: extendable-error: 0.1.7 - '@changesets/get-dependents-graph@2.1.2': + '@changesets/get-dependents-graph@2.1.3': dependencies: - '@changesets/types': 6.0.0 + '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 - picocolors: 1.1.0 - semver: 7.6.3 + picocolors: 1.1.1 + semver: 7.7.3 - '@changesets/get-github-info@0.6.0': + '@changesets/get-github-info@0.7.0': dependencies: dataloader: 1.4.0 node-fetch: 2.7.0 transitivePeerDependencies: - encoding - '@changesets/get-release-plan@4.0.4': + '@changesets/get-release-plan@4.0.14': dependencies: - '@changesets/assemble-release-plan': 6.0.4 - '@changesets/config': 3.0.3 - '@changesets/pre': 2.0.1 - '@changesets/read': 0.6.1 - '@changesets/types': 6.0.0 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/config': 3.1.2 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.6 + '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 '@changesets/get-version-range-type@0.4.0': {} - '@changesets/git@3.0.1': + '@changesets/git@3.0.4': dependencies: '@changesets/errors': 0.2.0 '@manypkg/get-packages': 1.1.3 is-subdir: 1.2.0 micromatch: 4.0.8 - spawndamnit: 2.0.0 + spawndamnit: 3.0.1 '@changesets/logger@0.1.1': dependencies: - picocolors: 1.1.0 + picocolors: 1.1.1 - '@changesets/parse@0.4.0': + '@changesets/parse@0.4.2': dependencies: - '@changesets/types': 6.0.0 - js-yaml: 3.14.1 + '@changesets/types': 6.1.0 + js-yaml: 4.1.1 - '@changesets/pre@2.0.1': + '@changesets/pre@2.0.2': dependencies: '@changesets/errors': 0.2.0 - '@changesets/types': 6.0.0 + '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 - '@changesets/read@0.6.1': + '@changesets/read@0.6.6': dependencies: - '@changesets/git': 3.0.1 + '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 - '@changesets/parse': 0.4.0 - '@changesets/types': 6.0.0 + '@changesets/parse': 0.4.2 + '@changesets/types': 6.1.0 fs-extra: 7.0.1 p-filter: 2.1.0 - picocolors: 1.1.0 + picocolors: 1.1.1 - '@changesets/should-skip-package@0.1.1': + '@changesets/should-skip-package@0.1.2': dependencies: - '@changesets/types': 6.0.0 + '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 '@changesets/types@4.1.0': {} - '@changesets/types@6.0.0': {} + '@changesets/types@6.1.0': {} - '@changesets/write@0.3.2': + '@changesets/write@0.4.0': dependencies: - '@changesets/types': 6.0.0 + '@changesets/types': 6.1.0 fs-extra: 7.0.1 - human-id: 1.0.2 + human-id: 4.1.3 prettier: 2.8.8 - '@cspotcode/source-map-support@0.8.1': + '@emnapi/core@1.9.1': dependencies: - '@jridgewell/trace-mapping': 0.3.9 - - '@esbuild/aix-ppc64@0.21.5': + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.23.1': + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/android-arm64@0.21.5': + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/android-arm64@0.23.1': + '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/android-arm@0.21.5': + '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm@0.23.1': + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.23.1': - optional: true - '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.23.1': - optional: true - '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.23.1': - optional: true - '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.23.1': - optional: true - '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.23.1': - optional: true - '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.23.1': - optional: true - '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.23.1': - optional: true - '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.23.1': - optional: true - '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.23.1': - optional: true - '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.23.1': - optional: true - '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.23.1': - optional: true - '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.23.1': - optional: true - '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.23.1': - optional: true - '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/linux-x64@0.23.1': - optional: true - '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.23.1': - optional: true - - '@esbuild/openbsd-arm64@0.23.1': - optional: true - '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.23.1': - optional: true - '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.23.1': - optional: true - '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.23.1': - optional: true - '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.23.1': - optional: true - '@esbuild/win32-x64@0.21.5': optional: true - '@esbuild/win32-x64@0.23.1': - optional: true - - '@isaacs/cliui@8.0.2': + '@inquirer/external-editor@1.0.3(@types/node@20.19.30)': dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 20.19.30 - '@jridgewell/gen-mapping@0.3.5': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.9': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - '@langchain/community@0.0.53(openai@4.67.1(zod@3.23.8))': - dependencies: - '@langchain/core': 0.1.63(openai@4.67.1(zod@3.23.8)) - '@langchain/openai': 0.0.28 - expr-eval: 2.0.2 - flat: 5.0.2 - langsmith: 0.1.61(openai@4.67.1(zod@3.23.8)) - uuid: 9.0.1 - zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) - transitivePeerDependencies: - - encoding - - openai - - '@langchain/core@0.1.63(openai@4.67.1(zod@3.23.8))': - dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.15 - langsmith: 0.1.61(openai@4.67.1(zod@3.23.8)) - ml-distance: 4.0.1 - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 9.0.1 - zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) - transitivePeerDependencies: - - openai - - '@langchain/openai@0.0.28': - dependencies: - '@langchain/core': 0.1.63(openai@4.67.1(zod@3.23.8)) - js-tiktoken: 1.0.15 - openai: 4.67.1(zod@3.23.8) - zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) - transitivePeerDependencies: - - encoding + '@jridgewell/sourcemap-codec': 1.5.5 '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.25.7 + '@babel/runtime': 7.28.6 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.25.7 + '@babel/runtime': 7.28.6 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 globby: 11.1.0 read-yaml-file: 1.1.0 + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2759,421 +1677,300 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 + fastq: 1.20.1 '@opentelemetry/api@1.9.0': {} - '@pkgjs/parseargs@0.11.0': + '@oxc-project/types@0.122.0': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': optional: true - '@rollup/rollup-android-arm-eabi@4.24.0': + '@rolldown/binding-darwin-x64@1.0.0-rc.12': optional: true - '@rollup/rollup-android-arm64@4.24.0': + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': optional: true - '@rollup/rollup-darwin-arm64@4.24.0': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': optional: true - '@rollup/rollup-darwin-x64@4.24.0': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.24.0': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.24.0': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': optional: true - '@rollup/rollup-linux-arm64-gnu@4.24.0': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': optional: true - '@rollup/rollup-linux-arm64-musl@4.24.0': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.24.0': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': optional: true - '@rollup/rollup-linux-s390x-gnu@4.24.0': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true - '@rollup/rollup-linux-x64-gnu@4.24.0': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': optional: true - '@rollup/rollup-linux-x64-musl@4.24.0': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': optional: true - '@rollup/rollup-win32-arm64-msvc@4.24.0': + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@rollup/rollup-android-arm-eabi@4.57.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.24.0': + '@rollup/rollup-android-arm64@4.57.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.24.0': + '@rollup/rollup-darwin-arm64@4.57.1': optional: true - '@tsconfig/node10@1.0.11': {} + '@rollup/rollup-darwin-x64@4.57.1': + optional: true - '@tsconfig/node12@1.0.11': {} + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true - '@tsconfig/node14@1.0.3': {} + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true - '@tsconfig/node16@1.0.4': {} + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true - '@types/diff-match-patch@1.0.36': {} + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true - '@types/estree@1.0.6': {} + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true - '@types/node-fetch@2.6.11': - dependencies: - '@types/node': 20.16.10 - form-data: 4.0.0 + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true - '@types/node@12.20.55': {} + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true - '@types/node@18.19.54': - dependencies: - undici-types: 5.26.5 + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true - '@types/node@20.16.10': - dependencies: - undici-types: 6.19.8 + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true - '@types/object-hash@3.0.6': {} + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true - '@types/retry@0.12.0': {} + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true - '@types/uuid@10.0.0': {} + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true - '@vitest/expect@2.1.2': - dependencies: - '@vitest/spy': 2.1.2 - '@vitest/utils': 2.1.2 - chai: 5.1.1 - tinyrainbow: 1.2.0 + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true - '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@20.16.10))': - dependencies: - '@vitest/spy': 2.1.2 - estree-walker: 3.0.3 - magic-string: 0.30.11 - optionalDependencies: - vite: 5.4.8(@types/node@20.16.10) + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true - '@vitest/pretty-format@2.1.2': - dependencies: - tinyrainbow: 1.2.0 + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true - '@vitest/runner@2.1.2': - dependencies: - '@vitest/utils': 2.1.2 - pathe: 1.1.2 + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true - '@vitest/snapshot@2.1.2': - dependencies: - '@vitest/pretty-format': 2.1.2 - magic-string: 0.30.11 - pathe: 1.1.2 + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true - '@vitest/spy@2.1.2': - dependencies: - tinyspy: 3.0.2 + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true - '@vitest/utils@2.1.2': - dependencies: - '@vitest/pretty-format': 2.1.2 - loupe: 3.1.1 - tinyrainbow: 1.2.0 + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true - '@vue/compiler-core@3.5.11': - dependencies: - '@babel/parser': 7.25.7 - '@vue/shared': 3.5.11 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.1 + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true - '@vue/compiler-dom@3.5.11': - dependencies: - '@vue/compiler-core': 3.5.11 - '@vue/shared': 3.5.11 + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true - '@vue/compiler-sfc@3.5.11': - dependencies: - '@babel/parser': 7.25.7 - '@vue/compiler-core': 3.5.11 - '@vue/compiler-dom': 3.5.11 - '@vue/compiler-ssr': 3.5.11 - '@vue/shared': 3.5.11 - estree-walker: 2.0.2 - magic-string: 0.30.11 - postcss: 8.4.47 - source-map-js: 1.2.1 + '@standard-schema/spec@1.1.0': {} - '@vue/compiler-ssr@3.5.11': + '@tybys/wasm-util@0.10.1': dependencies: - '@vue/compiler-dom': 3.5.11 - '@vue/shared': 3.5.11 + tslib: 2.8.1 + optional: true - '@vue/reactivity@3.5.11': - dependencies: - '@vue/shared': 3.5.11 + '@types/estree@1.0.8': {} + + '@types/jsesc@2.5.1': {} - '@vue/runtime-core@3.5.11': + '@types/node@12.20.55': {} + + '@types/node@20.19.30': dependencies: - '@vue/reactivity': 3.5.11 - '@vue/shared': 3.5.11 + undici-types: 6.21.0 - '@vue/runtime-dom@3.5.11': + '@vercel/oidc@3.1.0': {} + + '@vitest/expect@2.1.9': dependencies: - '@vue/reactivity': 3.5.11 - '@vue/runtime-core': 3.5.11 - '@vue/shared': 3.5.11 - csstype: 3.1.3 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 - '@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2))': + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.19.30))': dependencies: - '@vue/compiler-ssr': 3.5.11 - '@vue/shared': 3.5.11 - vue: 3.5.11(typescript@5.6.2) + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@20.19.30) - '@vue/shared@3.5.11': {} + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 - '@xstate/graph@2.0.1(xstate@5.18.2)': + '@vitest/runner@2.1.9': dependencies: - xstate: 5.18.2 + '@vitest/utils': 2.1.9 + pathe: 1.1.2 - abort-controller@3.0.0: + '@vitest/snapshot@2.1.9': dependencies: - event-target-shim: 5.0.1 + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 - acorn-walk@8.3.4: + '@vitest/spy@2.1.9': dependencies: - acorn: 8.12.1 + tinyspy: 3.0.2 - acorn@8.12.1: {} + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 - agentkeepalive@4.5.0: + '@xstate/graph@2.0.1(xstate@5.26.0)': dependencies: - humanize-ms: 1.2.1 + xstate: 5.26.0 - ai@3.4.9(openai@4.67.1(zod@3.23.8))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.11(typescript@5.6.2))(zod@3.23.8): + ai@6.0.67(zod@4.3.6): dependencies: - '@ai-sdk/provider': 0.0.24 - '@ai-sdk/provider-utils': 1.0.20(zod@3.23.8) - '@ai-sdk/react': 0.0.62(react@18.3.1)(zod@3.23.8) - '@ai-sdk/solid': 0.0.49(zod@3.23.8) - '@ai-sdk/svelte': 0.0.51(svelte@4.2.19)(zod@3.23.8) - '@ai-sdk/ui-utils': 0.0.46(zod@3.23.8) - '@ai-sdk/vue': 0.0.54(vue@3.5.11(typescript@5.6.2))(zod@3.23.8) + '@ai-sdk/gateway': 3.0.32(zod@4.3.6) + '@ai-sdk/provider': 3.0.7 + '@ai-sdk/provider-utils': 4.0.13(zod@4.3.6) '@opentelemetry/api': 1.9.0 - eventsource-parser: 1.1.2 - json-schema: 0.4.0 - jsondiffpatch: 0.6.0 - nanoid: 3.3.6 - secure-json-parse: 2.7.0 - zod-to-json-schema: 3.23.2(zod@3.23.8) - optionalDependencies: - openai: 4.67.1(zod@3.23.8) - react: 18.3.1 - sswr: 2.1.0(svelte@4.2.19) - svelte: 4.2.19 - zod: 3.23.8 - transitivePeerDependencies: - - solid-js - - vue + zod: 4.3.6 ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - ansi-styles@6.2.1: {} - - any-promise@1.3.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - arg@4.1.3: {} + ansis@4.2.0: {} argparse@1.0.10: dependencies: sprintf-js: 1.0.3 - aria-query@5.3.2: {} + argparse@2.0.1: {} array-union@2.1.0: {} assertion-error@2.0.1: {} - asynckit@0.4.0: {} - - axios@1.7.7: + ast-kit@3.0.0-beta.1: dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - - axobject-query@4.1.0: {} - - balanced-match@1.0.2: {} - - base64-js@1.5.1: {} + '@babel/parser': 8.0.0-rc.3 + estree-walker: 3.0.3 + pathe: 2.0.3 better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 - binary-extensions@2.3.0: {} - - binary-search@1.3.6: {} - - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 + birpc@4.0.0: {} braces@3.0.3: dependencies: fill-range: 7.1.1 - bundle-require@5.0.0(esbuild@0.23.1): - dependencies: - esbuild: 0.23.1 - load-tsconfig: 0.2.5 - cac@6.7.14: {} - camelcase@4.1.0: {} + cac@7.0.0: {} - camelcase@6.3.0: {} - - chai@5.1.1: + chai@5.3.3: dependencies: assertion-error: 2.0.1 - check-error: 2.1.1 + check-error: 2.1.3 deep-eql: 5.0.2 - loupe: 3.1.1 - pathval: 2.0.0 - - chalk@5.3.0: {} + loupe: 3.2.1 + pathval: 2.0.1 - chardet@0.7.0: {} + chardet@2.1.1: {} - check-error@2.1.1: {} - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 + check-error@2.1.3: {} ci-info@3.9.0: {} - client-only@0.0.1: {} - - code-red@1.0.4: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - '@types/estree': 1.0.6 - acorn: 8.12.1 - estree-walker: 3.0.3 - periscopic: 3.1.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - - commander@10.0.1: {} - - commander@4.1.1: {} - - consola@3.2.3: {} - - create-require@1.1.1: {} - - cross-spawn@5.1.0: - dependencies: - lru-cache: 4.1.5 - shebang-command: 1.2.0 - which: 1.3.1 - - cross-spawn@7.0.3: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - css-tree@2.3.1: - dependencies: - mdn-data: 2.0.30 - source-map-js: 1.2.1 - - csstype@3.1.3: {} - dataloader@1.4.0: {} - debug@4.3.7: + debug@4.4.3: dependencies: ms: 2.1.3 - decamelize@1.2.0: {} - deep-eql@5.0.2: {} - delayed-stream@1.0.0: {} + defu@6.1.4: {} detect-indent@6.1.0: {} - diff-match-patch@1.0.5: {} - - diff@4.0.2: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 - dotenv@16.4.5: {} + dotenv@16.6.1: {} dotenv@8.6.0: {} - eastasianwidth@0.2.0: {} - - emoji-regex@8.0.0: {} + dts-resolver@2.1.3: {} - emoji-regex@9.2.2: {} + empathic@2.0.0: {} enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - entities@4.5.0: {} + es-module-lexer@1.7.0: {} esbuild@0.21.5: optionalDependencies: @@ -3201,70 +1998,19 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 - esbuild@0.23.1: - optionalDependencies: - '@esbuild/aix-ppc64': 0.23.1 - '@esbuild/android-arm': 0.23.1 - '@esbuild/android-arm64': 0.23.1 - '@esbuild/android-x64': 0.23.1 - '@esbuild/darwin-arm64': 0.23.1 - '@esbuild/darwin-x64': 0.23.1 - '@esbuild/freebsd-arm64': 0.23.1 - '@esbuild/freebsd-x64': 0.23.1 - '@esbuild/linux-arm': 0.23.1 - '@esbuild/linux-arm64': 0.23.1 - '@esbuild/linux-ia32': 0.23.1 - '@esbuild/linux-loong64': 0.23.1 - '@esbuild/linux-mips64el': 0.23.1 - '@esbuild/linux-ppc64': 0.23.1 - '@esbuild/linux-riscv64': 0.23.1 - '@esbuild/linux-s390x': 0.23.1 - '@esbuild/linux-x64': 0.23.1 - '@esbuild/netbsd-x64': 0.23.1 - '@esbuild/openbsd-arm64': 0.23.1 - '@esbuild/openbsd-x64': 0.23.1 - '@esbuild/sunos-x64': 0.23.1 - '@esbuild/win32-arm64': 0.23.1 - '@esbuild/win32-ia32': 0.23.1 - '@esbuild/win32-x64': 0.23.1 - esprima@4.0.1: {} - estree-walker@2.0.2: {} - estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.6 - - event-target-shim@5.0.1: {} + '@types/estree': 1.0.8 - eventemitter3@4.0.7: {} + eventsource-parser@3.0.6: {} - eventsource-parser@1.1.2: {} - - execa@5.1.1: - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - expr-eval@2.0.2: {} + expect-type@1.3.0: {} extendable-error@0.1.7: {} - external-editor@3.1.0: - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - - fast-glob@3.3.2: + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 @@ -3272,13 +2018,13 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fastq@1.17.1: + fastq@1.20.1: dependencies: - reusify: 1.0.4 + reusify: 1.1.0 - fdir@6.4.0(picomatch@4.0.2): + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 fill-range@7.1.1: dependencies: @@ -3289,28 +2035,6 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 - flat@5.0.2: {} - - follow-redirects@1.15.9: {} - - foreground-child@3.3.0: - dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.1.0 - - form-data-encoder@1.7.2: {} - - form-data@4.0.0: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - - formdata-node@4.4.1: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 4.0.0-beta.3 - fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -3326,74 +2050,45 @@ snapshots: fsevents@2.3.3: optional: true - get-func-name@2.0.2: {} - - get-stream@6.0.1: {} + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 glob-parent@5.1.2: dependencies: is-glob: 4.0.3 - glob@10.4.5: - dependencies: - foreground-child: 3.3.0 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - globby@11.1.0: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.3.2 + fast-glob: 3.3.3 ignore: 5.3.2 merge2: 1.4.1 slash: 3.0.0 graceful-fs@4.2.11: {} - human-id@1.0.2: {} - - human-signals@2.1.0: {} + hookable@6.1.0: {} - humanize-ms@1.2.1: - dependencies: - ms: 2.1.3 + human-id@4.1.3: {} - iconv-lite@0.4.24: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 ignore@5.3.2: {} - infobox-parser@3.6.4: - dependencies: - camelcase: 4.1.0 - - is-any-array@2.0.1: {} - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 + import-without-cache@0.2.5: {} is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-number@7.0.0: {} - is-reference@3.0.2: - dependencies: - '@types/estree': 1.0.6 - - is-stream@2.0.1: {} - is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -3402,93 +2097,34 @@ snapshots: isexe@2.0.0: {} - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - joycon@3.1.1: {} - - js-tiktoken@1.0.15: - dependencies: - base64-js: 1.5.1 - - js-tokens@4.0.0: {} - - js-yaml@3.14.1: + js-yaml@3.14.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 - json-schema-to-ts@3.1.1: + js-yaml@4.1.1: dependencies: - '@babel/runtime': 7.25.7 - ts-algebra: 2.0.0 + argparse: 2.0.1 - json-schema@0.4.0: {} + jsesc@3.1.0: {} - jsondiffpatch@0.6.0: - dependencies: - '@types/diff-match-patch': 1.0.36 - chalk: 5.3.0 - diff-match-patch: 1.0.5 + json-schema@0.4.0: {} jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 - langsmith@0.1.61(openai@4.67.1(zod@3.23.8)): - dependencies: - '@types/uuid': 10.0.0 - commander: 10.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - semver: 7.6.3 - uuid: 10.0.0 - optionalDependencies: - openai: 4.67.1(zod@3.23.8) - - lilconfig@3.1.2: {} - - lines-and-columns@1.2.4: {} - - load-tsconfig@0.2.5: {} - - locate-character@3.0.0: {} - locate-path@5.0.0: dependencies: p-locate: 4.1.0 - lodash.sortby@4.7.0: {} - lodash.startcase@4.4.0: {} - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - - loupe@3.1.1: - dependencies: - get-func-name: 2.0.2 - - lru-cache@10.4.3: {} + loupe@3.2.1: {} - lru-cache@4.1.5: + magic-string@0.30.21: dependencies: - pseudomap: 1.0.2 - yallist: 2.1.2 - - magic-string@0.30.11: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - - make-error@1.3.6: {} - - mdn-data@2.0.30: {} - - merge-stream@2.0.0: {} + '@jridgewell/sourcemap-codec': 1.5.5 merge2@1.4.1: {} @@ -3497,94 +2133,17 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mimic-fn@2.1.0: {} - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.1 - - minipass@7.1.2: {} - - ml-array-mean@1.1.6: - dependencies: - ml-array-sum: 1.1.6 - - ml-array-sum@1.1.6: - dependencies: - is-any-array: 2.0.1 - - ml-distance-euclidean@2.0.0: {} - - ml-distance@4.0.1: - dependencies: - ml-array-mean: 1.1.6 - ml-distance-euclidean: 2.0.0 - ml-tree-similarity: 1.0.0 - - ml-tree-similarity@1.0.0: - dependencies: - binary-search: 1.3.6 - num-sort: 2.1.0 - mri@1.2.0: {} ms@2.1.3: {} - mustache@4.2.0: {} - - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - - nanoid@3.3.6: {} - - nanoid@3.3.7: {} - - node-domexception@1.0.0: {} + nanoid@3.3.11: {} node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 - normalize-path@3.0.0: {} - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - num-sort@2.1.0: {} - - object-assign@4.1.1: {} - - object-hash@3.0.0: {} - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - openai@4.67.1(zod@3.23.8): - dependencies: - '@types/node': 18.19.54 - '@types/node-fetch': 2.6.11 - abort-controller: 3.0.0 - agentkeepalive: 4.5.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0 - optionalDependencies: - zod: 3.23.8 - transitivePeerDependencies: - - encoding - - os-tmpdir@1.0.2: {} + obug@2.1.1: {} outdent@0.5.0: {} @@ -3592,8 +2151,6 @@ snapshots: dependencies: p-map: 2.1.0 - p-finally@1.0.0: {} - p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -3604,122 +2161,132 @@ snapshots: p-map@2.1.0: {} - p-queue@6.6.2: - dependencies: - eventemitter3: 4.0.7 - p-timeout: 3.2.0 - - p-retry@4.6.2: - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - - p-timeout@3.2.0: - dependencies: - p-finally: 1.0.0 - p-try@2.2.0: {} - package-json-from-dist@1.0.1: {} - - package-manager-detector@0.2.1: {} + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 path-exists@4.0.0: {} path-key@3.1.1: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - path-type@4.0.0: {} pathe@1.1.2: {} - pathval@2.0.0: {} + pathe@2.0.3: {} - periscopic@3.1.0: - dependencies: - '@types/estree': 1.0.6 - estree-walker: 3.0.3 - is-reference: 3.0.2 + pathval@2.0.1: {} - picocolors@1.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} - picomatch@4.0.2: {} - - pify@4.0.1: {} + picomatch@4.0.3: {} - pirates@4.0.6: {} + picomatch@4.0.4: {} - postcss-load-config@6.0.1(postcss@8.4.47): - dependencies: - lilconfig: 3.1.2 - optionalDependencies: - postcss: 8.4.47 + pify@4.0.1: {} - postcss@8.4.47: + postcss@8.5.6: dependencies: - nanoid: 3.3.7 - picocolors: 1.1.0 + nanoid: 3.3.11 + picocolors: 1.1.1 source-map-js: 1.2.1 prettier@2.8.8: {} - proxy-from-env@1.1.0: {} - - pseudomap@1.0.2: {} + quansync@0.2.11: {} - punycode@2.3.1: {} + quansync@1.0.0: {} queue-microtask@1.2.3: {} - react@18.3.1: - dependencies: - loose-envify: 1.4.0 - read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 - js-yaml: 3.14.1 + js-yaml: 3.14.2 pify: 4.0.1 strip-bom: 3.0.0 - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 + resolve-from@5.0.0: {} - regenerator-runtime@0.14.1: {} + resolve-pkg-maps@1.0.0: {} - resolve-from@5.0.0: {} + reusify@1.1.0: {} - retry@0.13.1: {} + rolldown-plugin-dts@0.23.2(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(typescript@5.9.3): + dependencies: + '@babel/generator': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 + ast-kit: 3.0.0-beta.1 + birpc: 4.0.0 + dts-resolver: 2.1.3 + get-tsconfig: 4.13.7 + obug: 2.1.1 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - oxc-resolver - reusify@1.0.4: {} + rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - rollup@4.24.0: + rollup@4.57.1: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.24.0 - '@rollup/rollup-android-arm64': 4.24.0 - '@rollup/rollup-darwin-arm64': 4.24.0 - '@rollup/rollup-darwin-x64': 4.24.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.24.0 - '@rollup/rollup-linux-arm-musleabihf': 4.24.0 - '@rollup/rollup-linux-arm64-gnu': 4.24.0 - '@rollup/rollup-linux-arm64-musl': 4.24.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.24.0 - '@rollup/rollup-linux-riscv64-gnu': 4.24.0 - '@rollup/rollup-linux-s390x-gnu': 4.24.0 - '@rollup/rollup-linux-x64-gnu': 4.24.0 - '@rollup/rollup-linux-x64-musl': 4.24.0 - '@rollup/rollup-win32-arm64-msvc': 4.24.0 - '@rollup/rollup-win32-ia32-msvc': 4.24.0 - '@rollup/rollup-win32-x64-msvc': 4.24.0 + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 run-parallel@1.2.0: @@ -3728,231 +2295,125 @@ snapshots: safer-buffer@2.1.2: {} - secure-json-parse@2.7.0: {} + semver@7.7.3: {} - semver@7.6.3: {} - - shebang-command@1.2.0: - dependencies: - shebang-regex: 1.0.0 + semver@7.7.4: {} shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 - shebang-regex@1.0.0: {} - shebang-regex@3.0.0: {} siginfo@2.0.0: {} - signal-exit@3.0.7: {} - signal-exit@4.1.0: {} slash@3.0.0: {} source-map-js@1.2.1: {} - source-map@0.8.0-beta.0: + spawndamnit@3.0.1: dependencies: - whatwg-url: 7.1.0 - - spawndamnit@2.0.0: - dependencies: - cross-spawn: 5.1.0 - signal-exit: 3.0.7 + cross-spawn: 7.0.6 + signal-exit: 4.1.0 sprintf-js@1.0.3: {} - sswr@2.1.0(svelte@4.2.19): - dependencies: - svelte: 4.2.19 - swrev: 4.0.0 - stackback@0.0.2: {} - std-env@3.7.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + std-env@3.10.0: {} strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.1.0 - strip-bom@3.0.0: {} - strip-final-newline@2.0.0: {} - - sucrase@3.35.0: - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - commander: 4.1.1 - glob: 10.4.5 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.6 - ts-interface-checker: 0.1.13 - - svelte@4.2.19: - dependencies: - '@ampproject/remapping': 2.3.0 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - '@types/estree': 1.0.6 - acorn: 8.12.1 - aria-query: 5.3.2 - axobject-query: 4.1.0 - code-red: 1.0.4 - css-tree: 2.3.1 - estree-walker: 3.0.3 - is-reference: 3.0.2 - locate-character: 3.0.0 - magic-string: 0.30.11 - periscopic: 3.1.0 - - swr@2.2.5(react@18.3.1): - dependencies: - client-only: 0.0.1 - react: 18.3.1 - use-sync-external-store: 1.2.2(react@18.3.1) - - swrev@4.0.0: {} - - swrv@1.0.4(vue@3.5.11(typescript@5.6.2)): - dependencies: - vue: 3.5.11(typescript@5.6.2) - term-size@2.2.1: {} - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - tinybench@2.9.0: {} - tinyexec@0.3.0: {} + tinyexec@0.3.2: {} + + tinyexec@1.0.4: {} - tinyglobby@0.2.9: + tinyglobby@0.2.15: dependencies: - fdir: 6.4.0(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 - tinypool@1.0.1: {} + tinypool@1.1.1: {} tinyrainbow@1.2.0: {} tinyspy@3.0.2: {} - tmp@0.0.33: - dependencies: - os-tmpdir: 1.0.2 - - to-fast-properties@2.0.0: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 tr46@0.0.3: {} - tr46@1.0.1: - dependencies: - punycode: 2.3.1 - tree-kill@1.2.2: {} - ts-algebra@2.0.0: {} - - ts-interface-checker@0.1.13: {} - - ts-node@10.9.2(@types/node@20.16.10)(typescript@5.6.2): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 20.16.10 - acorn: 8.12.1 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.6.2 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - - tsup@8.3.0(postcss@8.4.47)(typescript@5.6.2): - dependencies: - bundle-require: 5.0.0(esbuild@0.23.1) - cac: 6.7.14 - chokidar: 3.6.0 - consola: 3.2.3 - debug: 4.3.7 - esbuild: 0.23.1 - execa: 5.1.1 - joycon: 3.1.1 - picocolors: 1.1.0 - postcss-load-config: 6.0.1(postcss@8.4.47) - resolve-from: 5.0.0 - rollup: 4.24.0 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tinyglobby: 0.2.9 + tsdown@0.21.7(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(typescript@5.9.3): + dependencies: + ansis: 4.2.0 + cac: 7.0.0 + defu: 6.1.4 + empathic: 2.0.0 + hookable: 6.1.0 + import-without-cache: 0.2.5 + obug: 2.1.1 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + rolldown-plugin-dts: 0.23.2(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(typescript@5.9.3) + semver: 7.7.4 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 tree-kill: 1.2.2 + unconfig-core: 7.5.0 + unrun: 0.2.34(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) optionalDependencies: - postcss: 8.4.47 - typescript: 5.6.2 + typescript: 5.9.3 transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - - typescript@5.6.2: {} + - '@emnapi/core' + - '@emnapi/runtime' + - '@ts-macro/tsc' + - '@typescript/native-preview' + - oxc-resolver + - synckit + - vue-tsc - undici-types@5.26.5: {} - - undici-types@6.19.8: {} + tslib@2.8.1: + optional: true - universalify@0.1.2: {} + typescript@5.9.3: {} - use-sync-external-store@1.2.2(react@18.3.1): + unconfig-core@7.5.0: dependencies: - react: 18.3.1 + '@quansync/fs': 1.0.0 + quansync: 1.0.0 - uuid@10.0.0: {} + undici-types@6.21.0: {} - uuid@9.0.1: {} + universalify@0.1.2: {} - v8-compile-cache-lib@3.0.1: {} + unrun@0.2.34(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + dependencies: + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - vite-node@2.1.2(@types/node@20.16.10): + vite-node@2.1.9(@types/node@20.19.30): dependencies: cac: 6.7.14 - debug: 4.3.7 + debug: 4.4.3 + es-module-lexer: 1.7.0 pathe: 1.1.2 - vite: 5.4.8(@types/node@20.16.10) + vite: 5.4.21(@types/node@20.19.30) transitivePeerDependencies: - '@types/node' - less @@ -3964,38 +2425,39 @@ snapshots: - supports-color - terser - vite@5.4.8(@types/node@20.16.10): + vite@5.4.21(@types/node@20.19.30): dependencies: esbuild: 0.21.5 - postcss: 8.4.47 - rollup: 4.24.0 + postcss: 8.5.6 + rollup: 4.57.1 optionalDependencies: - '@types/node': 20.16.10 + '@types/node': 20.19.30 fsevents: 2.3.3 - vitest@2.1.2(@types/node@20.16.10): - dependencies: - '@vitest/expect': 2.1.2 - '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@20.16.10)) - '@vitest/pretty-format': 2.1.2 - '@vitest/runner': 2.1.2 - '@vitest/snapshot': 2.1.2 - '@vitest/spy': 2.1.2 - '@vitest/utils': 2.1.2 - chai: 5.1.1 - debug: 4.3.7 - magic-string: 0.30.11 + vitest@2.1.9(@types/node@20.19.30): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.30)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 pathe: 1.1.2 - std-env: 3.7.0 + std-env: 3.10.0 tinybench: 2.9.0 - tinyexec: 0.3.0 - tinypool: 1.0.1 + tinyexec: 0.3.2 + tinypool: 1.1.1 tinyrainbow: 1.2.0 - vite: 5.4.8(@types/node@20.16.10) - vite-node: 2.1.2(@types/node@20.16.10) + vite: 5.4.21(@types/node@20.19.30) + vite-node: 2.1.9(@types/node@20.19.30) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 20.16.10 + '@types/node': 20.19.30 transitivePeerDependencies: - less - lightningcss @@ -4007,37 +2469,13 @@ snapshots: - supports-color - terser - vue@3.5.11(typescript@5.6.2): - dependencies: - '@vue/compiler-dom': 3.5.11 - '@vue/compiler-sfc': 3.5.11 - '@vue/runtime-dom': 3.5.11 - '@vue/server-renderer': 3.5.11(vue@3.5.11(typescript@5.6.2)) - '@vue/shared': 3.5.11 - optionalDependencies: - typescript: 5.6.2 - - web-streams-polyfill@4.0.0-beta.3: {} - webidl-conversions@3.0.1: {} - webidl-conversions@4.0.2: {} - whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - whatwg-url@7.1.0: - dependencies: - lodash.sortby: 4.7.0 - tr46: 1.0.1 - webidl-conversions: 4.0.2 - - which@1.3.1: - dependencies: - isexe: 2.0.0 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -4047,37 +2485,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wikipedia@2.1.2: - dependencies: - axios: 1.7.7 - infobox-parser: 3.6.4 - transitivePeerDependencies: - - debug - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - - xstate@5.18.2: {} - - yallist@2.1.2: {} - - yn@3.1.1: {} - - zod-to-json-schema@3.23.2(zod@3.23.8): - dependencies: - zod: 3.23.8 - - zod-to-json-schema@3.23.3(zod@3.23.8): - dependencies: - zod: 3.23.8 + xstate@5.26.0: {} - zod@3.23.8: {} + zod@4.3.6: {} diff --git a/src/adapter.ts b/src/adapter.ts new file mode 100644 index 0000000..9cc3e54 --- /dev/null +++ b/src/adapter.ts @@ -0,0 +1,8 @@ +import type { AgentAdapter } from './types.js'; + +/** + * Create a custom adapter for AI primitives (classify/decide). + */ +export function createAdapter(impl: AgentAdapter): AgentAdapter { + return impl; +} diff --git a/src/agent.test.ts b/src/agent.test.ts index 443f423..0d1d94f 100644 --- a/src/agent.test.ts +++ b/src/agent.test.ts @@ -1,326 +1,1267 @@ -import { test, expect, vi } from 'vitest'; -import { createAgent } from './'; -import { createActor, createMachine } from 'xstate'; -import { LanguageModelV1CallOptions } from 'ai'; +import { describe, expect, test, vi } from 'vitest'; import { z } from 'zod'; -import { dummyResponseValues, MockLanguageModelV1 } from './mockModel'; +import { + createAgentMachine, + createInitialState, + step, + run, + stream, + sendEvent, + decide, + classify, + createAdapter, +} from './index.js'; +import type { AgentAdapter } from './types.js'; -test('an agent has the expected interface', () => { - const agent = createAgent({ - name: 'test', - events: {}, - model: new MockLanguageModelV1(), +// ─── Test helpers ─── + +function mockAdapter( + responses: Array<{ choice: string; data?: Record; reasoning?: string }> +): AgentAdapter { + let index = 0; + return { + decide: async () => { + const response = responses[index++]; + if (!response) throw new Error('No more mock responses'); + return { + choice: response.choice, + data: response.data ?? {}, + reasoning: response.reasoning, + }; + }, + }; +} + +// ─── Simple machine for basic tests ─── + +function createSimpleMachine() { + return createAgentMachine({ + id: 'simple', + context: () => ({ count: 0 }), + initial: 'idle', + states: { + idle: { + on: { + start: () => ({ target: 'running' }), + }, + }, + running: { + run: async ({ context }) => { + return { value: (context.count as number) + 1 }; + }, + onDone: ({ result, context }) => ({ + target: 'done', + context: { count: (result as any).value }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ result: context.count }), + }, + }, + }); +} + +// ─── Machine with events for HITL ─── + +function createHitlMachine() { + return createAgentMachine({ + id: 'hitl', + inputSchema: z.object({ task: z.string() }), + context: (input) => ({ + task: input.task, + messages: [] as Array<{ role: string; content: string }>, + result: null as string | null, + }), + events: { + 'user.message': z.object({ message: z.string() }), + 'user.approve': z.object({}), + 'user.cancel': z.object({}), + }, + initial: 'gathering', + states: { + gathering: { + on: { + 'user.message': ({ event, context }) => ({ + context: { + messages: [ + ...(context.messages as any[]), + { role: 'user', content: (event as any).message }, + ], + }, + }), + 'user.approve': ({ context }) => ({ + target: 'processing', + }), + 'user.cancel': () => ({ target: 'cancelled' }), + }, + }, + processing: { + run: async ({ context }) => { + const msgs = context.messages as Array<{ content: string }>; + return { output: `Processed: ${msgs.map((m) => m.content).join(', ')}` }; + }, + onDone: ({ result }) => ({ + target: 'reviewing', + context: { result: (result as any).output }, + }), + }, + reviewing: { + on: { + 'user.approve': () => ({ target: 'done' }), + 'user.message': ({ event, context }) => ({ + target: 'processing', + context: { + messages: [ + ...(context.messages as any[]), + { role: 'user', content: (event as any).message }, + ], + }, + }), + 'user.cancel': () => ({ target: 'cancelled' }), + }, + }, + done: { + type: 'final', + output: ({ context }) => ({ result: context.result }), + }, + cancelled: { + type: 'final', + output: () => ({ cancelled: true }), + }, + }, + }); +} + +// ─── Machine with decide state ─── + +function createDecideMachine(adapter: AgentAdapter) { + return createAgentMachine({ + id: 'decider', + context: () => ({ + issue: 'App crashes on login', + category: null as string | null, + }), + adapter, + initial: 'classifying', + states: { + classifying: decide({ + model: 'test-model', + prompt: ({ context }) => `Classify: ${context.issue}`, + options: { + billing: { description: 'Billing issues' }, + technical: { description: 'Technical issues' }, + general: { description: 'General inquiries' }, + }, + onDone: ({ result }) => ({ + target: 'handling', + context: { category: result.choice }, + }), + }), + handling: { + run: async ({ context }) => ({ + resolution: `Handled ${context.category} issue`, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { resolution: (result as any).resolution }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + category: context.category, + resolution: context.resolution, + }), + }, + }, + }); +} + +// ─── Machine with classify state ─── + +function createClassifyMachine(adapter: AgentAdapter) { + return createAgentMachine({ + id: 'classifier', + context: () => ({ issue: 'I want my money back', category: null as string | null }), + adapter, + initial: 'classifyIntent', + states: { + classifyIntent: classify({ + model: 'test-model', + prompt: ({ context }) => `Classify: "${context.issue}"`, + into: { + billing: { description: 'Billing, payments, refunds' }, + technical: { description: 'Technical issues, bugs' }, + general: { description: 'General inquiries' }, + }, + onDone: ({ result }) => ({ + target: 'done', + context: { category: result.category }, + }), + }), + done: { + type: 'final', + output: ({ context }) => ({ category: context.category }), + }, + }, + }); +} + +// ─── Nested/compound state machine ─── + +function createNestedMachine() { + return createAgentMachine({ + id: 'nested', + context: () => ({ + resolution: null as string | null, + category: 'billing' as string, + }), + initial: 'handling', + states: { + handling: { + initial: ({ context }) => { + if (context.category === 'billing') { + return { target: 'checkEligibility' }; + } + return { target: 'diagnose' }; + }, + states: { + checkEligibility: { + run: async () => ({ eligible: true }), + onDone: ({ result }) => { + if ((result as any).eligible) return { target: 'processRefund' }; + return { target: 'deny' }; + }, + }, + processRefund: { + run: async () => ({}), + onDone: ({ context }) => ({ + target: 'childDone', + context: { resolution: 'Refund processed' }, + }), + }, + deny: { + run: async () => ({ message: 'Not eligible' }), + onDone: ({ result }) => ({ + target: 'childDone', + context: { resolution: (result as any).message }, + }), + }, + diagnose: { + run: async () => ({ diagnosis: 'It is a bug' }), + onDone: ({ result }) => ({ + target: 'childDone', + context: { resolution: (result as any).diagnosis }, + }), + }, + childDone: { type: 'final' }, + }, + onDone: () => ({ + target: 'respond', + }), + on: { + 'user.cancel': () => ({ target: 'cancelled' }), + }, + }, + respond: { + run: async ({ context }) => ({ message: context.resolution }), + onDone: () => ({ target: 'done' }), + }, + done: { + type: 'final', + output: ({ context }) => ({ resolution: context.resolution }), + }, + cancelled: { + type: 'final', + output: () => ({ cancelled: true }), + }, + }, }); +} - expect(agent.decide).toBeDefined(); +// ═══════════════════════════════════════ +// Tests +// ═══════════════════════════════════════ - expect(agent.addMessage).toBeDefined(); - expect(agent.addObservation).toBeDefined(); - expect(agent.addFeedback).toBeDefined(); - expect(agent.addPlan).toBeDefined(); +describe('createAgentMachine', () => { + test('creates a machine config', () => { + const machine = createSimpleMachine(); + expect(machine.id).toBe('simple'); + expect(machine.states).toBeDefined(); + expect(machine.states.idle).toBeDefined(); + expect(machine.states.running).toBeDefined(); + expect(machine.states.done).toBeDefined(); + }); +}); + +describe('createInitialState', () => { + test('creates initial state with context', async () => { + const machine = createSimpleMachine(); + const state = await createInitialState(machine, undefined); + expect(state.value).toBe('idle'); + expect(state.context).toEqual({ count: 0 }); + expect(state.status).toBe('running'); + expect(state.params).toEqual({}); + }); + + test('validates input against schema', async () => { + const machine = createHitlMachine(); + const state = await createInitialState(machine, { task: 'test task' }); + expect(state.context.task).toBe('test task'); + expect(state.value).toBe('gathering'); + }); - expect(agent.getMessages).toBeDefined(); - expect(agent.getObservations).toBeDefined(); - expect(agent.getFeedback).toBeDefined(); - expect(agent.getPlans).toBeDefined(); + test('rejects invalid input', async () => { + const machine = createHitlMachine(); + await expect(createInitialState(machine, { task: 123 })).rejects.toThrow(); + }); + + test('resolves string initial', async () => { + const machine = createSimpleMachine(); + const state = await createInitialState(machine, undefined); + expect(state.value).toBe('idle'); + }); + + test('resolves function initial', async () => { + const machine = createAgentMachine({ + id: 'fn-initial', + context: (input) => ({ mode: input }), + initial: ({ context }) => ({ + target: context.mode === 'fast' ? 'fast' : 'slow', + }), + states: { + fast: { type: 'final' }, + slow: { type: 'final' }, + }, + }); + const state = await createInitialState(machine, 'fast'); + expect(state.value).toBe('fast'); + }); - expect(agent.interact).toBeDefined(); + test('resolves compound state initial', async () => { + const machine = createNestedMachine(); + const state = await createInitialState(machine, undefined); + // Should enter handling → checkEligibility (since category is 'billing') + expect(state.value).toBe('handling.checkEligibility'); + }); }); -test('agent.addMessage() adds to message history', () => { - const model = new MockLanguageModelV1(); +describe('step', () => { + test('executes run and transitions via onDone', async () => { + const machine = createSimpleMachine(); + let state = await createInitialState(machine, undefined); + // idle → send start event to get to 'running' + state = sendEvent(machine, state, { type: 'start' }); + expect(state.value).toBe('running'); + + state = await step(machine, state); + expect(state.value).toBe('done'); + expect(state.context.count).toBe(1); + }); - const agent = createAgent({ - name: 'test', - events: {}, - model, + test('returns waiting for event-only states', async () => { + const machine = createHitlMachine(); + let state = await createInitialState(machine, { task: 'test' }); + state = await step(machine, state); + expect(state.status).toBe('waiting'); + expect(state.value).toBe('gathering'); }); - agent.addMessage({ - role: 'user', - content: [{ type: 'text', text: 'msg 1' }], + test('returns done for final states', async () => { + const machine = createSimpleMachine(); + let state = await createInitialState(machine, undefined); + state = sendEvent(machine, state, { type: 'start' }); + state = await step(machine, state); // run → done + state = await step(machine, state); // final + expect(state.status).toBe('done'); + expect(state.output).toEqual({ result: 1 }); }); - const messageHistory = agent.addMessage({ - role: 'assistant', - content: [{ type: 'text', text: 'response 1' }], + test('handles context updates in transitions', async () => { + const machine = createSimpleMachine(); + let state = await createInitialState(machine, undefined); + state = sendEvent(machine, state, { type: 'start' }); + state = await step(machine, state); + expect(state.context.count).toBe(1); }); - expect(messageHistory.sessionId).toEqual(agent.sessionId); + test('handles decide state with adapter', async () => { + const adapter = mockAdapter([ + { choice: 'technical', data: {} }, + ]); + const machine = createDecideMachine(adapter); + let state = await createInitialState(machine, undefined); + expect(state.value).toBe('classifying'); + + state = await step(machine, state); + expect(state.value).toBe('handling'); + expect(state.context.category).toBe('technical'); + }); - expect(agent.getMessages()).toContainEqual( - expect.objectContaining({ - content: [expect.objectContaining({ text: 'msg 1' })], - }) - ); + test('handles classify state', async () => { + const adapter = mockAdapter([ + { choice: 'billing', data: {} }, + ]); + const machine = createClassifyMachine(adapter); + let state = await createInitialState(machine, undefined); + + state = await step(machine, state); + expect(state.value).toBe('done'); + expect(state.context.category).toBe('billing'); + }); + + test('errors without adapter on decide state', async () => { + const machine = createAgentMachine({ + id: 'no-adapter', + context: () => ({}), + initial: 'deciding', + states: { + deciding: decide({ + model: 'test', + prompt: 'test', + options: { a: { description: 'A' } }, + onDone: () => ({ target: 'done' }), + }), + done: { type: 'final' }, + }, + }); + const state = await createInitialState(machine, undefined); + const result = await step(machine, state); + expect(result.status).toBe('error'); + expect(result.error).toContain('No adapter'); + }); + + test('bubbles error from run', async () => { + const machine = createAgentMachine({ + id: 'error-machine', + context: () => ({}), + initial: 'failing', + states: { + failing: { + run: async () => { + throw new Error('boom'); + }, + onDone: () => ({ target: 'done' }), + }, + done: { type: 'final' }, + }, + }); + const state = await createInitialState(machine, undefined); + const result = await step(machine, state); + expect(result.status).toBe('error'); + expect((result.error as Error).message).toBe('boom'); + }); - expect(agent.getMessages()).toContainEqual( - expect.objectContaining({ - content: [expect.objectContaining({ text: 'response 1' })], - sessionId: expect.any(String), - timestamp: expect.any(Number), - }) - ); + test('handles nested state entry and execution', async () => { + const machine = createNestedMachine(); + let state = await createInitialState(machine, undefined); + expect(state.value).toBe('handling.checkEligibility'); + + // Step through checkEligibility → processRefund + state = await step(machine, state); + expect(state.value).toBe('handling.processRefund'); + + // Step through processRefund → childDone + state = await step(machine, state); + expect(state.value).toBe('handling.childDone'); + expect(state.context.resolution).toBe('Refund processed'); + + // Step: childDone is final → parent onDone → respond + state = await step(machine, state); + expect(state.value).toBe('respond'); + }); }); -test('agent.addFeedback() adds to feedback', () => { - const agent = createAgent({ - name: 'test', - events: {}, - model: {} as any, +describe('run', () => { + test('runs until completion', async () => { + const machine = createSimpleMachine(); + let state = await createInitialState(machine, undefined); + state = sendEvent(machine, state, { type: 'start' }); + + const result = await run(machine, state); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ result: 1 }); + expect(result.context.count).toBe(1); + } }); - const feedback = agent.addFeedback({ - attributes: { - score: -1, - }, - goal: 'Win the game', - observationId: 'obs-1', - }); - - expect(feedback.sessionId).toEqual(agent.sessionId); - - expect(agent.getFeedback()).toContainEqual( - expect.objectContaining({ - attributes: { - score: -1, - }, - goal: 'Win the game', - observationId: 'obs-1', - sessionId: expect.any(String), - timestamp: expect.any(Number), - }) - ); - expect(agent.getFeedback()).toContainEqual( - expect.objectContaining({ - attributes: { - score: -1, - }, - goal: 'Win the game', - observationId: 'obs-1', - sessionId: expect.any(String), - timestamp: expect.any(Number), - }) - ); + test('stops at waiting state', async () => { + const machine = createHitlMachine(); + const state = await createInitialState(machine, { task: 'test' }); + + const result = await run(machine, state); + expect(result.status).toBe('waiting'); + if (result.status === 'waiting') { + expect(result.value).toBe('gathering'); + expect(result.events).toBeDefined(); + } + }); + + test('stops on error', async () => { + const machine = createAgentMachine({ + id: 'err', + context: () => ({}), + initial: 'fail', + states: { + fail: { + run: async () => { + throw new Error('nope'); + }, + onDone: () => ({ target: 'ok' }), + }, + ok: { type: 'final' }, + }, + }); + const state = await createInitialState(machine, undefined); + const result = await run(machine, state); + expect(result.status).toBe('error'); + }); + + test('runs through multiple transitions', async () => { + const adapter = mockAdapter([{ choice: 'technical' }]); + const machine = createDecideMachine(adapter); + const state = await createInitialState(machine, undefined); + + const result = await run(machine, state); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + category: 'technical', + resolution: 'Handled technical issue', + }); + } + }); + + test('runs nested states to completion', async () => { + const machine = createNestedMachine(); + const state = await createInitialState(machine, undefined); + + const result = await run(machine, state); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ resolution: 'Refund processed' }); + } + }); + + test('waiting result includes available events', async () => { + const machine = createHitlMachine(); + const state = await createInitialState(machine, { task: 'test' }); + + const result = await run(machine, state); + expect(result.status).toBe('waiting'); + if (result.status === 'waiting') { + expect(result.events['user.message']).toBeDefined(); + expect(result.events['user.approve']).toBeDefined(); + expect(result.events['user.cancel']).toBeDefined(); + } + }); }); -test('agent.addObservation() adds to observations', () => { - const agent = createAgent({ - name: 'test', - events: {}, - model: {} as any, +describe('sendEvent', () => { + test('transitions on matching event', async () => { + const machine = createSimpleMachine(); + const state = await createInitialState(machine, undefined); + const next = sendEvent(machine, state, { type: 'start' }); + expect(next.value).toBe('running'); + expect(next.status).toBe('running'); + }); + + test('handles self-transition (no target)', async () => { + const machine = createHitlMachine(); + let state = await createInitialState(machine, { task: 'test' }); + state = await step(machine, state); // → waiting + + const next = sendEvent(machine, state, { + type: 'user.message', + message: 'hello', + }); + expect(next.value).toBe('gathering'); // same state + expect((next.context.messages as any[]).length).toBe(1); + expect((next.context.messages as any[])[0].content).toBe('hello'); + }); + + test('accumulates context on repeated self-transitions', async () => { + const machine = createHitlMachine(); + let state = await createInitialState(machine, { task: 'test' }); + state = await step(machine, state); // → waiting + + state = sendEvent(machine, state, { type: 'user.message', message: 'one' }); + state = sendEvent(machine, state, { type: 'user.message', message: 'two' }); + state = sendEvent(machine, state, { type: 'user.message', message: 'three' }); + + expect((state.context.messages as any[]).length).toBe(3); + }); + + test('transitions to new state with event', async () => { + const machine = createHitlMachine(); + let state = await createInitialState(machine, { task: 'test' }); + state = await step(machine, state); // → waiting at gathering + + state = sendEvent(machine, state, { type: 'user.approve' }); + expect(state.value).toBe('processing'); + expect(state.status).toBe('running'); }); - const observation = agent.addObservation({ - prevState: { value: 'playing', context: {} }, - event: { type: 'play', position: 3 }, - state: { value: 'lost', context: {} }, + test('throws on unknown event', async () => { + const machine = createSimpleMachine(); + const state = await createInitialState(machine, undefined); + expect(() => + sendEvent(machine, state, { type: 'nonexistent' }) + ).toThrow("No handler for event 'nonexistent'"); }); - expect(observation.sessionId).toEqual(agent.sessionId); + test('parent event preempts child in nested state', async () => { + const machine = createNestedMachine(); + let state = await createInitialState(machine, undefined); + expect(state.value).toBe('handling.checkEligibility'); - expect(agent.getObservations()).toContainEqual( - expect.objectContaining({ - prevState: { value: 'playing', context: {} }, - event: { type: 'play', position: 3 }, - state: { value: 'lost', context: {} }, - sessionId: expect.any(String), - timestamp: expect.any(Number), - }) - ); + // Parent's on handler should preempt + const next = sendEvent(machine, state, { type: 'user.cancel' }); + expect(next.value).toBe('cancelled'); + }); }); -test('agent.addObservation() adds to observations with machine hash', () => { - const agent = createAgent({ - name: 'test', - events: {}, - model: {} as any, +describe('stream', () => { + test('yields snapshots for each transition', async () => { + const adapter = mockAdapter([{ choice: 'technical' }]); + const machine = createDecideMachine(adapter); + const state = await createInitialState(machine, undefined); + + const snapshots = []; + for await (const snapshot of stream(machine, state)) { + snapshots.push(snapshot); + } + + expect(snapshots.length).toBeGreaterThanOrEqual(3); // initial + classifying→handling + handling→done + done + expect(snapshots[0]!.value).toBe('classifying'); + const last = snapshots[snapshots.length - 1]!; + expect(last.status).toBe('done'); }); +}); - const machine = createMachine({ - initial: 'playing', - states: { - playing: { - on: { - play: 'lost', - }, +describe('decide', () => { + test('creates state config with decide type', () => { + const config = decide({ + model: 'test', + prompt: 'test prompt', + options: { + a: { description: 'Option A' }, + b: { description: 'Option B' }, }, - lost: {}, - }, + onDone: ({ result }) => ({ target: result.choice }), + }); + expect(config.__type).toBe('decide'); + expect(config.__decideConfig).toBeDefined(); + expect(config.__decideConfig!.model).toBe('test'); + }); + + test('calls adapter with resolved prompt function', async () => { + const decideSpy = vi.fn().mockResolvedValue({ + choice: 'a', + data: {}, + }); + const adapter: AgentAdapter = { decide: decideSpy }; + + const machine = createAgentMachine({ + id: 'decide-test', + context: () => ({ topic: 'cats' }), + adapter, + initial: 'choosing', + states: { + choosing: decide({ + model: 'my-model', + prompt: ({ context }) => `About ${context.topic}`, + options: { + a: { description: 'A' }, + b: { description: 'B' }, + }, + onDone: ({ result }) => ({ + target: 'done', + context: { choice: result.choice }, + }), + }), + done: { type: 'final' }, + }, + }); + + const state = await createInitialState(machine, undefined); + await step(machine, state); + + expect(decideSpy).toHaveBeenCalledWith({ + model: 'my-model', + prompt: 'About cats', + options: { + a: { description: 'A' }, + b: { description: 'B' }, + }, + reasoning: undefined, + }); }); - const observation = agent.addObservation({ - prevState: { value: 'playing', context: {} }, - event: { type: 'play', position: 3 }, - state: { value: 'lost', context: {} }, - machine, + test('supports per-state adapter override', async () => { + const machineAdapter = mockAdapter([{ choice: 'machine' }]); + const stateAdapter = mockAdapter([{ choice: 'state' }]); + + const machine = createAgentMachine({ + id: 'override-test', + context: () => ({ choice: null as string | null }), + adapter: machineAdapter, + initial: 'choosing', + states: { + choosing: decide({ + model: 'test', + adapter: stateAdapter, // overrides machine adapter + prompt: 'pick', + options: { state: { description: 'S' }, machine: { description: 'M' } }, + onDone: ({ result }) => ({ + target: 'done', + context: { choice: result.choice }, + }), + }), + done: { type: 'final' }, + }, + }); + + const state = await createInitialState(machine, undefined); + const result = await run(machine, state); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.context.choice).toBe('state'); // used state adapter, not machine + } }); - expect(observation.sessionId).toEqual(agent.sessionId); + test('supports reasoning', async () => { + const adapter: AgentAdapter = { + decide: async () => ({ + choice: 'a', + data: {}, + reasoning: 'Because reasons', + }), + }; - expect(agent.getObservations()).toContainEqual( - expect.objectContaining({ - prevState: { value: 'playing', context: {} }, - event: { type: 'play', position: 3 }, - state: { value: 'lost', context: {} }, - machineHash: expect.any(String), - sessionId: expect.any(String), - timestamp: expect.any(Number), - }) - ); + const machine = createAgentMachine({ + id: 'reasoning-test', + context: () => ({ reasoning: null as string | null }), + adapter, + initial: 'choosing', + states: { + choosing: decide({ + model: 'test', + prompt: 'pick', + reasoning: true, + options: { a: { description: 'A' } }, + onDone: ({ result }) => ({ + target: 'done', + context: { reasoning: result.reasoning ?? null }, + }), + }), + done: { type: 'final' }, + }, + }); + + const state = await createInitialState(machine, undefined); + const result = await run(machine, state); + if (result.status === 'done') { + expect(result.context.reasoning).toBe('Because reasons'); + } + }); + + test('decide with option schemas passes data', async () => { + const adapter: AgentAdapter = { + decide: async () => ({ + choice: 'withData', + data: { items: ['a', 'b'] }, + }), + }; + + const machine = createAgentMachine({ + id: 'data-test', + context: () => ({ items: null as string[] | null }), + adapter, + initial: 'choosing', + states: { + choosing: decide({ + model: 'test', + prompt: 'pick', + options: { + withData: { + description: 'Has data', + schema: z.object({ items: z.array(z.string()) }), + }, + withoutData: { description: 'No data' }, + }, + onDone: ({ result }) => ({ + target: 'done', + context: { + items: result.choice === 'withData' ? (result.data as any).items : null, + }, + }), + }), + done: { type: 'final' }, + }, + }); + + const state = await createInitialState(machine, undefined); + const result = await run(machine, state); + if (result.status === 'done') { + expect(result.context.items).toEqual(['a', 'b']); + } + }); }); -test('agent.interact() observes machine actors (no 2nd arg)', () => { - const machine = createMachine({ - initial: 'a', - states: { - a: { - on: { NEXT: 'b' }, +describe('classify', () => { + test('creates state config with classify type', () => { + const config = classify({ + model: 'test', + prompt: 'classify this', + into: { + a: { description: 'Category A' }, + b: { description: 'Category B' }, }, - b: {}, - }, + onDone: ({ result }) => ({ target: result.category }), + }); + expect(config.__type).toBe('classify'); + expect(config.__classifyConfig).toBeDefined(); + expect(config.__decideConfig).toBeDefined(); // classify wraps decide }); - const agent = createAgent({ - name: 'test', - events: {}, - model: {} as any, + test('result has category field', async () => { + const adapter = mockAdapter([{ choice: 'billing' }]); + const machine = createClassifyMachine(adapter); + const state = await createInitialState(machine, undefined); + + const result = await run(machine, state); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ category: 'billing' }); + } }); +}); - const actor = createActor(machine); +describe('nested states', () => { + test('enters compound state initial child', async () => { + const machine = createNestedMachine(); + const state = await createInitialState(machine, undefined); + expect(state.value).toBe('handling.checkEligibility'); + }); - agent.interact(actor); + test('conditional compound initial based on context', async () => { + const machine = createAgentMachine({ + id: 'cond-nested', + context: () => ({ category: 'technical' as string }), + initial: 'handling', + states: { + handling: { + initial: ({ context }) => { + if (context.category === 'billing') return { target: 'billing' }; + return { target: 'technical' }; + }, + states: { + billing: { + run: async () => ({ result: 'billing handled' }), + onDone: () => ({ target: 'childDone' }), + }, + technical: { + run: async () => ({ result: 'tech handled' }), + onDone: ({ result }) => ({ + target: 'childDone', + context: { resolution: (result as any).result }, + }), + }, + childDone: { type: 'final' }, + }, + onDone: () => ({ target: 'done' }), + }, + done: { + type: 'final', + output: ({ context }) => ({ resolution: context.resolution }), + }, + }, + }); - actor.start(); + const state = await createInitialState(machine, undefined); + expect(state.value).toBe('handling.technical'); - expect(agent.getObservations()).toContainEqual( - expect.objectContaining({ - prevState: undefined, - state: expect.objectContaining({ value: 'a' }), - }) - ); - expect(agent.getObservations()).toContainEqual( - expect.objectContaining({ - prevState: undefined, - state: expect.objectContaining({ value: 'a' }), - }) - ); + const result = await run(machine, state); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ resolution: 'tech handled' }); + } + }); - actor.send({ type: 'NEXT' }); + test('parent onDone fires when child reaches final', async () => { + const machine = createNestedMachine(); + const state = await createInitialState(machine, undefined); - expect(agent.getObservations()).toContainEqual( - expect.objectContaining({ - prevState: expect.objectContaining({ value: 'a' }), - event: { type: 'NEXT' }, - state: expect.objectContaining({ value: 'b' }), - }) - ); + const result = await run(machine, state); + expect(result.status).toBe('done'); + if (result.status === 'done') { + // The chain: checkEligibility → processRefund → childDone → (parent onDone) → respond → done + expect(result.output).toEqual({ resolution: 'Refund processed' }); + } + }); + + test('parent event handler preempts children', async () => { + const machine = createNestedMachine(); + const state = await createInitialState(machine, undefined); + expect(state.value).toBe('handling.checkEligibility'); + + const next = sendEvent(machine, state, { type: 'user.cancel' }); + expect(next.value).toBe('cancelled'); + expect(next.status).toBe('running'); + + const result = await run(machine, next); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ cancelled: true }); + } + }); }); -test('You can listen for feedback events', () => { - const fn = vi.fn(); - const agent = createAgent({ - name: 'test', - events: {}, - model: {} as any, +describe('full workflow: HITL', () => { + test('gather → process → review → done', async () => { + const machine = createHitlMachine(); + + // Start + let state = await createInitialState(machine, { task: 'build feature' }); + let result = await run(machine, state); + expect(result.status).toBe('waiting'); + expect(result.status === 'waiting' && result.value).toBe('gathering'); + + // Send messages + state = sendEvent(machine, result.state, { type: 'user.message', message: 'req A' }); + state = sendEvent(machine, state, { type: 'user.message', message: 'req B' }); + + // Approve to move to processing + state = sendEvent(machine, state, { type: 'user.approve' }); + result = await run(machine, state); + expect(result.status).toBe('waiting'); + expect(result.status === 'waiting' && result.value).toBe('reviewing'); + expect(result.status === 'waiting' && result.context.result).toBe('Processed: req A, req B'); + + // Approve the review + state = sendEvent(machine, result.state, { type: 'user.approve' }); + result = await run(machine, state); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ result: 'Processed: req A, req B' }); + } }); - agent.on('feedback', fn); + test('gather → process → review → reject → process → review → done', async () => { + const machine = createHitlMachine(); - agent.addFeedback({ - attributes: { - score: -1, - }, - goal: 'Win the game', - observationId: 'obs-1', + let state = await createInitialState(machine, { task: 'write code' }); + let result = await run(machine, state); + + // Send a message + state = sendEvent(machine, result.state, { type: 'user.message', message: 'initial' }); + state = sendEvent(machine, state, { type: 'user.approve' }); + result = await run(machine, state); + expect(result.status === 'waiting' && result.value).toBe('reviewing'); + + // Reject with feedback (sends us back to processing) + state = sendEvent(machine, result.state, { type: 'user.message', message: 'fix this' }); + result = await run(machine, state); + expect(result.status === 'waiting' && result.value).toBe('reviewing'); + expect(result.status === 'waiting' && result.context.result).toBe('Processed: initial, fix this'); + + // Approve + state = sendEvent(machine, result.state, { type: 'user.approve' }); + result = await run(machine, state); + expect(result.status).toBe('done'); }); - expect(fn).toHaveBeenCalled(); + test('cancel at any point', async () => { + const machine = createHitlMachine(); + + let state = await createInitialState(machine, { task: 'test' }); + let result = await run(machine, state); + + state = sendEvent(machine, result.state, { type: 'user.cancel' }); + result = await run(machine, state); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ cancelled: true }); + } + }); }); -test('You can listen for plan events', async () => { - const fn = vi.fn(); - const model = new MockLanguageModelV1({ - doGenerate: async (params: LanguageModelV1CallOptions) => { - const keys = - params.mode.type === 'regular' - ? params.mode.tools?.map((t) => t.name) - : []; +describe('serialization', () => { + test('state round-trips through JSON', async () => { + const machine = createHitlMachine(); + let state = await createInitialState(machine, { task: 'test' }); + let result = await run(machine, state); - return { - ...dummyResponseValues, - finishReason: 'tool-calls', - toolCalls: [ - { - toolCallType: 'function', - toolCallId: 'call-1', - toolName: keys![0], - args: `{ "type": "${keys?.[0]}" }`, - }, - ], - } as any; - }, + // Serialize → deserialize + const json = JSON.stringify(result.state); + const restored = JSON.parse(json); + + // Send event on restored state + const next = sendEvent(machine, restored, { + type: 'user.message', + message: 'from restored', + }); + expect((next.context.messages as any[])[0].content).toBe('from restored'); }); - const agent = createAgent({ - name: 'test', - model, - events: { - WIN: z.object({}), - }, + test('nested state round-trips through JSON', async () => { + const machine = createNestedMachine(); + const state = await createInitialState(machine, undefined); + + const json = JSON.stringify(state); + const restored = JSON.parse(json); + + expect(restored.value).toBe('handling.checkEligibility'); + + // Can continue execution from restored state + const result = await run(machine, restored); + expect(result.status).toBe('done'); }); +}); - agent.on('plan', fn); +describe('createAdapter', () => { + test('creates a custom adapter', () => { + const adapter = createAdapter({ + decide: async () => ({ choice: 'a', data: {} }), + }); + expect(adapter.decide).toBeDefined(); + }); +}); - await agent.decide({ - goal: 'Win the game', - state: { - value: 'playing', - context: {}, - }, - machine: createMachine({ - initial: 'playing', +describe('edge cases', () => { + test('state with run but no onDone and no on is a dead end', async () => { + const machine = createAgentMachine({ + id: 'dead-end', + context: () => ({}), + initial: 'stuck', states: { - playing: { - on: { - WIN: { - target: 'won', + stuck: { + run: async () => ({ done: true }), + }, + }, + }); + const state = await createInitialState(machine, undefined); + const result = await step(machine, state); + // run completes but no onDone and no on → state doesn't change + expect(result.value).toBe('stuck'); + }); + + test('already done state returns as-is', async () => { + const machine = createSimpleMachine(); + const doneState = { + value: 'done', + params: {}, + context: { count: 1 }, + status: 'done' as const, + output: { result: 1 }, + }; + const result = await step(machine, doneState); + expect(result).toEqual(doneState); + }); + + test('already errored state returns as-is', async () => { + const machine = createSimpleMachine(); + const errorState = { + value: 'running', + params: {}, + context: { count: 0 }, + status: 'error' as const, + error: 'something went wrong', + }; + const result = await step(machine, errorState); + expect(result).toEqual(errorState); + }); +}); + +describe('P1: nested final state without parent onDone', () => { + test('does not mark machine as done when parent lacks onDone', async () => { + // a.b.c where c is final, b has NO onDone, a has onDone + const machine = createAgentMachine({ + id: 'p1-bug', + context: () => ({ resolved: false }), + initial: 'a', + states: { + a: { + initial: 'b', + states: { + b: { + initial: 'c', + // NO onDone — should halt here, not mark machine done + states: { + c: { type: 'final' }, + }, }, }, + onDone: () => ({ + target: 'result', + context: { resolved: true }, + }), + }, + result: { + type: 'final', + output: ({ context }) => ({ resolved: context.resolved }), }, - won: {}, }, - }), + }); + + const state = await createInitialState(machine, undefined); + expect(state.value).toBe('a.b.c'); + + // Step: c is final, parent b has no onDone → should wait, NOT done + const next = await step(machine, state); + expect(next.status).toBe('waiting'); + expect(next.value).toBe('a.b.c'); // stays put }); - expect(fn).toHaveBeenCalledWith( - expect.objectContaining({ - plan: expect.objectContaining({ - nextEvent: { - type: 'WIN', + test('correctly bubbles when parent has onDone', async () => { + // Same structure but b HAS onDone → bDone(final) → a.onDone → result + const machine = createAgentMachine({ + id: 'p1-fixed', + context: () => ({}), + initial: 'a', + states: { + a: { + initial: 'b', + states: { + b: { + initial: 'c', + states: { + c: { type: 'final' }, + }, + onDone: () => ({ target: 'bDone' }), + }, + bDone: { type: 'final' }, + }, + onDone: () => ({ target: 'result' }), }, - }), - }) - ); + result: { + type: 'final', + output: () => ({ ok: true }), + }, + }, + }); + + const state = await createInitialState(machine, undefined); + const result = await run(machine, state); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ ok: true }); + } + }); + + test('ancestor on handlers still work when halted at final child', async () => { + const machine = createAgentMachine({ + id: 'p1-escape', + context: () => ({}), + initial: 'a', + states: { + a: { + initial: 'b', + states: { + b: { + initial: 'c', + states: { c: { type: 'final' } }, + // no onDone + }, + }, + on: { + escape: () => ({ target: 'escaped' }), + }, + }, + escaped: { + type: 'final', + output: () => ({ escaped: true }), + }, + }, + }); + + const state = await createInitialState(machine, undefined); + let result = await run(machine, state); + expect(result.status).toBe('waiting'); // halted at a.b.c + + // Ancestor on handler should still be reachable + const next = sendEvent(machine, result.state, { type: 'escape' }); + expect(next.value).toBe('escaped'); + result = await run(machine, next); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ escaped: true }); + } + }); }); -test('agent.types provides context and event types', () => { - const agent = createAgent({ - model: {} as any, - events: { - setScore: z.object({ - score: z.number(), - }), - }, - context: { - score: z.number(), - }, +describe('P2: event payload validation', () => { + test('rejects event with invalid payload', async () => { + const machine = createHitlMachine(); + let state = await createInitialState(machine, { task: 'test' }); + state = await step(machine, state); // → waiting + + // user.message schema requires { message: string } + // Sending wrong type should throw + expect(() => + sendEvent(machine, state, { type: 'user.message', message: 123 as any }) + ).toThrow(); + }); + + test('accepts event with valid payload', async () => { + const machine = createHitlMachine(); + let state = await createInitialState(machine, { task: 'test' }); + state = await step(machine, state); + + // Should not throw + const next = sendEvent(machine, state, { + type: 'user.message', + message: 'valid string', + }); + expect((next.context.messages as any[]).length).toBe(1); }); - agent.types satisfies { context: any; events: any }; + test('skips validation when no schema declared', async () => { + const machine = createSimpleMachine(); + const state = await createInitialState(machine, undefined); + + // 'start' event has no schema — should not throw + const next = sendEvent(machine, state, { type: 'start' }); + expect(next.value).toBe('running'); + }); + + test('state-level schema overrides root-level', async () => { + const machine = createAgentMachine({ + id: 'schema-override', + context: () => ({ val: '' }), + events: { + act: z.object({ type: z.literal('act'), val: z.string() }), + }, + initial: 'a', + states: { + a: { + events: { + // Override: requires val to be a number + act: z.object({ type: z.literal('act'), val: z.number() }), + }, + on: { + act: ({ event }) => ({ + target: 'b', + context: { val: String((event as any).val) }, + }), + }, + }, + b: { type: 'final' }, + }, + }); + + const state = await createInitialState(machine, undefined); - agent.types.context satisfies { score: number }; + // String val should fail (state schema requires number) + expect(() => + sendEvent(machine, state, { type: 'act', val: 'nope' }) + ).toThrow(); - // @ts-expect-error - agent.types.context satisfies { score: string }; + // Number val should succeed + const next = sendEvent(machine, state, { type: 'act', val: 42 }); + expect(next.value).toBe('b'); + }); }); diff --git a/src/agent.ts b/src/agent.ts deleted file mode 100644 index 74f3a45..0000000 --- a/src/agent.ts +++ /dev/null @@ -1,687 +0,0 @@ -import { - Actor, - AnyActorRef, - AnyEventObject, - AnyStateMachine, - EventObject, - fromTransition, - Subscription, -} from 'xstate'; -import { ZodContextMapping, ZodEventMapping } from './schemas'; -import { - AgentLogic, - AgentMessage, - AgentPlanner, - EventsFromZodEventMapping, - GenerateTextOptions, - AgentLongTermMemory, - ObservedState, - AgentObservationInput, - AgentMemoryContext, - AgentObservation, - ContextFromZodContextMapping, - AgentFeedback, - AgentMessageInput, - AgentFeedbackInput, - AgentPlan, - AnyAgent, - Compute, - AgentDecisionInput, - AgentDecideOptions, -} from './types'; -import { simplePlanner } from './planners/simplePlanner'; -import { agentDecide } from './decision'; -import { getMachineHash, randomId } from './utils'; -import { - experimental_wrapLanguageModel, - LanguageModel, - LanguageModelV1, -} from 'ai'; -import { createAgentMiddleware } from './middleware'; - -export const agentLogic: AgentLogic = fromTransition( - (state, event, { emit }) => { - switch (event.type) { - case 'agent.feedback': { - state.feedback.push(event.feedback); - emit({ - type: 'feedback', - // @ts-ignore TODO: fix types in XState - feedback: event.feedback, - }); - break; - } - case 'agent.observe': { - state.observations.push(event.observation); - emit({ - type: 'observation', - // @ts-ignore TODO: fix types in XState - observation: event.observation, - }); - break; - } - case 'agent.message': { - state.messages.push(event.message); - emit({ - type: 'message', - // @ts-ignore TODO: fix types in XState - message: event.message, - }); - break; - } - case 'agent.plan': { - state.plans.push(event.plan); - emit({ - type: 'plan', - // @ts-ignore TODO: fix types in XState - plan: event.plan, - }); - break; - } - default: - break; - } - return state; - }, - () => - ({ - feedback: [], - messages: [], - observations: [], - plans: [], - } as AgentMemoryContext) -); - -export function createAgent< - const TContextSchema extends ZodContextMapping, - const TEventSchemas extends ZodEventMapping, - TEvents extends EventObject = EventsFromZodEventMapping, - TContext = ContextFromZodContextMapping ->({ - id, - name, - description, - model, - events, - context, - planner = simplePlanner as AgentPlanner>, - stringify = JSON.stringify, - getMemory, - logic = agentLogic as AgentLogic, - ...generateTextOptions -}: { - /** - * The unique identifier for the agent. - * - * This should be the same across all sessions of a specific agent, as it can be - * used to retrieve memory for this agent. - * - * @example - * ```ts - * const agent = createAgent({ - * id: 'recipe-assistant', - * // ... - * }); - * ``` - */ - id?: string; - /** - * The name of the agent - */ - name?: string; - /** - * A description of the role of the agent - */ - description?: string; - /** - * Events that the agent can cause (send) in an environment - * that the agent knows about. - */ - events: TEventSchemas; - context?: TContextSchema; - planner?: AgentPlanner>; - stringify?: typeof JSON.stringify; - /** - * A function that retrieves the agent's long term memory - */ - getMemory?: ( - agent: Agent - ) => AgentLongTermMemory; - /** - * Agent logic - */ - logic?: AgentLogic; -} & GenerateTextOptions): Agent { - return new Agent({ - id, - context, - events, - name, - description, - planner, - model, - logic, - }) as any; - // const agent = createActor(logic) as unknown as Agent; - // agent.events = events; - // agent.model = model; - // agent.name = name; - // agent.description = description; - // agent.defaultOptions = { ...generateTextOptions, model }; - // agent.memory = getMemory ? getMemory(agent) : undefined; - - // agent.onMessage = (callback) => { - // agent.on('message', (ev) => callback(ev.message)); - // }; - - // agent.decide = (opts) => { - // return agentDecide(agent, opts); - // }; - - // agent.addMessage = (messageInput) => { - // const message = { - // ...messageInput, - // id: messageInput.id ?? randomId(), - // timestamp: messageInput.timestamp ?? Date.now(), - // sessionId: agent.sessionId, - // } satisfies AgentMessage; - // agent.send({ - // type: 'agent.message', - // message, - // }); - - // return message; - // }; - // agent.getMessages = () => agent.getSnapshot().context.messages; - - // agent.addFeedback = (feedbackInput) => { - // const feedback = { - // ...feedbackInput, - // attributes: { ...feedbackInput.attributes }, - // reward: feedbackInput.reward ?? 0, - // timestamp: feedbackInput.timestamp ?? Date.now(), - // sessionId: agent.sessionId, - // } satisfies AgentFeedback; - // agent.send({ - // type: 'agent.feedback', - // feedback, - // }); - // return feedback; - // }; - // agent.getFeedback = () => agent.getSnapshot().context.feedback; - - // agent.addObservation = (observationInput) => { - // const { prevState, event, state } = observationInput; - // const observedState = { context: state.context, value: state.value }; - // const observedPrevState = prevState - // ? { - // context: prevState.context, - // value: prevState.value, - // } - // : undefined; - // const observation = { - // prevState: observedPrevState, - // event, - // state: observedState, - // id: observationInput.id ?? randomId(), - // sessionId: agent.sessionId, - // timestamp: observationInput.timestamp ?? Date.now(), - // machineHash: observationInput.machine - // ? getMachineHash(observationInput.machine) - // : undefined, - // } satisfies AgentObservation; - - // agent.send({ - // type: 'agent.observe', - // observation, - // }); - - // return observation; - // }; - // agent.getObservations = () => agent.getSnapshot().context.observations; - - // agent.addPlan = (plan) => { - // agent.send({ - // type: 'agent.plan', - // plan, - // }); - // }; - // agent.getPlans = () => agent.getSnapshot().context.plans; - - // agent.interact = ((actorRef, getInput) => { - // let prevState: ObservedState | undefined = undefined; - // let subscribed = true; - - // async function handleObservation(observationInput: AgentObservationInput) { - // const observation = agent.addObservation(observationInput); - - // const input = getInput?.(observation); - - // if (input) { - // await agentDecide(agent, { - // machine: actorRef.src as AnyStateMachine, - // state: observation.state, - // execute: async (event) => { - // actorRef.send(event); - // }, - // ...input, - // }); - // } - - // prevState = observationInput.state; - // } - - // // Inspect system, but only observe specified actor - // const sub = actorRef.system.inspect({ - // next: async (inspEvent) => { - // if ( - // !subscribed || - // inspEvent.actorRef !== actorRef || - // inspEvent.type !== '@xstate.snapshot' - // ) { - // return; - // } - - // const observationInput = { - // event: inspEvent.event, - // prevState, - // state: inspEvent.snapshot as any, - // machine: (actorRef as any).src, - // } satisfies AgentObservationInput; - - // await handleObservation(observationInput); - // }, - // }); - - // // If actor already started, interact with current state - // if ((actorRef as any)._processingStatus === 1) { - // handleObservation({ - // prevState: undefined, - // event: { type: '' }, // TODO: unknown events? - // state: actorRef.getSnapshot(), - // machine: (actorRef as any).src, - // }); - // } - - // return { - // unsubscribe: () => { - // sub.unsubscribe(); - // subscribed = false; - // }, - // }; - // }) as typeof agent.interact; - - // agent.observe = (actorRef) => { - // let prevState: ObservedState = actorRef.getSnapshot(); - - // const sub = actorRef.system.inspect({ - // next: async (inspEvent) => { - // if ( - // inspEvent.actorRef !== actorRef || - // inspEvent.type !== '@xstate.snapshot' - // ) { - // return; - // } - - // const observationInput = { - // event: inspEvent.event, - // prevState, - // state: inspEvent.snapshot as any, - // machine: (actorRef as any).src, - // } satisfies AgentObservationInput; - - // prevState = observationInput.state; - - // agent.addObservation(observationInput); - // }, - // }); - - // return sub; - // }; - - // agent.types = {} as any; - - // agent.wrap = (modelToWrap) => - // experimental_wrapLanguageModel({ - // model: modelToWrap, - // middleware: createAgentMiddleware(agent), - // }); - - // agent.model = experimental_wrapLanguageModel({ - // model, - // middleware: createAgentMiddleware(agent), - // }); - - // agent.start(); - - // return agent; -} - -export class Agent< - const TContextSchema extends ZodContextMapping, - const TEventSchemas extends ZodEventMapping, - TEvents extends EventObject = EventsFromZodEventMapping, - TContext = ContextFromZodContextMapping -> extends Actor> { - /** - * The name of the agent. All agents with the same name are related and - * able to share experiences (observations, feedback) with each other. - */ - public name?: string; - /** - * The unique identifier for the agent. - */ - public id: string; - public description?: string; - public events: TEventSchemas; - public context?: TContextSchema; - public planner?: AgentPlanner>; - public types: { - events: TEvents; - context: Compute; - }; - public model: LanguageModel; - public memory: AgentLongTermMemory | undefined; - public defaultOptions: any; // todo - - constructor({ - logic = agentLogic as AgentLogic, - id, - name, - description, - model, - events, - context, - planner = simplePlanner, - }: { - logic: AgentLogic; - id?: string; - name?: string; - description?: string; - model: GenerateTextOptions['model']; - events: TEventSchemas; - context?: TContextSchema; - planner?: AgentPlanner>; - }) { - super(logic); - this.model = model; - this.id = id ?? ''; - this.name = name; - this.description = description; - this.events = events; - this.context = context; - this.planner = planner; - this.types = {} as any; - - this.start(); - } - - /** - * Called whenever the agent (LLM assistant) receives or sends a message. - */ - public onMessage(fn: (message: AgentMessage) => void) { - return this.on('message', (ev) => fn(ev.message)); - } - - /** - * Retrieves messages from the agent's short-term (local) memory. - */ - public addMessage(messageInput: AgentMessageInput) { - const message = { - ...messageInput, - id: messageInput.id ?? randomId(), - timestamp: messageInput.timestamp ?? Date.now(), - sessionId: this.sessionId, - } satisfies AgentMessage; - this.send({ - type: 'agent.message', - message, - }); - - return message; - } - - public getMessages() { - return this.getSnapshot().context.messages; - } - - public addFeedback(feedbackInput: AgentFeedbackInput) { - const feedback = { - ...feedbackInput, - attributes: { ...feedbackInput.attributes }, - reward: feedbackInput.reward ?? 0, - timestamp: feedbackInput.timestamp ?? Date.now(), - sessionId: this.sessionId, - } satisfies AgentFeedback; - this.send({ - type: 'agent.feedback', - feedback, - }); - return feedback; - } - - /** - * Retrieves feedback from the agent's short-term (local) memory. - */ - public getFeedback() { - return this.getSnapshot().context.feedback; - } - - public addObservation( - observationInput: AgentObservationInput - ): AgentObservation { - const { prevState, event, state } = observationInput; - const observedState = { context: state.context, value: state.value }; - const observedPrevState = prevState - ? { - context: prevState.context, - value: prevState.value, - } - : undefined; - const observation = { - prevState: observedPrevState, - event, - state: observedState, - id: observationInput.id ?? randomId(), - sessionId: this.sessionId, - timestamp: observationInput.timestamp ?? Date.now(), - machineHash: observationInput.machine - ? getMachineHash(observationInput.machine) - : undefined, - } satisfies AgentObservation; - - this.send({ - type: 'agent.observe', - observation, - }); - - return observation; - } - - /** - * Retrieves observations from the agent's short-term (local) memory. - */ - public getObservations() { - return this.getSnapshot().context.observations; - } - - public addPlan(plan: AgentPlan) { - this.send({ - type: 'agent.plan', - plan, - }); - } - /** - * Retrieves strategies from the agent's short-term (local) memory. - */ - public getPlans() { - return this.getSnapshot().context.plans; - } - - /** - * Interacts with this state machine actor by inspecting state transitions and storing them as observations. - * - * Observations contain the `prevState`, `event`, and current `state` of this - * actor, as well as other properties that are useful when recalled. - * These observations are stored in the `agent`'s short-term (local) memory - * and can be retrieved via `agent.getObservations()`. - * - * @example - * ```ts - * // Only observes the actor's state transitions - * agent.interact(actor); - * - * actor.start(); - * ``` - */ - public interact(actorRef: TActor): Subscription; - /** - * Interacts with this state machine actor by: - * 1. Inspecting state transitions and storing them as observations - * 2. Deciding what to do next (which event to send the actor) based on - * the agent input returned from `getInput(observation)`, if `getInput(…)` is provided as the 2nd argument. - * - * Observations contain the `prevState`, `event`, and current `state` of this - * actor, as well as other properties that are useful when recalled. - * These observations are stored in the `agent`'s short-term (local) memory - * and can be retrieved via `agent.getObservations()`. - * - * @example - * ```ts - * // Observes the actor's state transitions and - * // makes a decision if on the "summarize" state - * agent.interact(actor, observed => { - * if (observed.state.matches('summarize')) { - * return { - * context: observed.state.context, - * goal: 'Summarize the message' - * } - * } - * }); - * - * actor.start(); - * ``` - */ - public interact( - actorRef: TActor, - getInput: ( - observation: AgentObservation - ) => AgentDecisionInput | undefined - ): Subscription; - public interact( - actorRef: TActor, - getInput?: ( - observation: AgentObservation - ) => AgentDecisionInput | undefined - ): Subscription { - let prevState: ObservedState | undefined = undefined; - let subscribed = true; - - const agent = this; - - async function handleObservation(observationInput: AgentObservationInput) { - const observation = agent.addObservation(observationInput); - - const input = getInput?.(observation); - - if (input) { - await agentDecide(agent, { - machine: actorRef.src as AnyStateMachine, - state: observation.state, - execute: async (event) => { - actorRef.send(event); - }, - ...input, - }); - } - - prevState = observationInput.state; - } - - // Inspect system, but only observe specified actor - const sub = actorRef.system.inspect({ - next: async (inspEvent) => { - if ( - !subscribed || - inspEvent.actorRef !== actorRef || - inspEvent.type !== '@xstate.snapshot' - ) { - return; - } - - const observationInput = { - event: inspEvent.event, - prevState, - state: inspEvent.snapshot as any, - machine: (actorRef as any).src, - } satisfies AgentObservationInput; - - await handleObservation(observationInput); - }, - }); - - // If actor already started, interact with current state - if ((actorRef as any)._processingStatus === 1) { - handleObservation({ - prevState: undefined, - event: { type: '' }, // TODO: unknown events? - state: actorRef.getSnapshot(), - machine: (actorRef as any).src, - }); - } - - return { - unsubscribe: () => { - sub.unsubscribe(); - subscribed = false; - }, - }; - } - - public observe(actorRef: TActor) { - let prevState: ObservedState = actorRef.getSnapshot(); - - const sub = actorRef.system.inspect({ - next: async (inspEvent) => { - if ( - inspEvent.actorRef !== actorRef || - inspEvent.type !== '@xstate.snapshot' - ) { - return; - } - - const observationInput = { - event: inspEvent.event, - prevState, - state: inspEvent.snapshot as any, - machine: (actorRef as any).src, - } satisfies AgentObservationInput; - - prevState = observationInput.state; - - this.addObservation(observationInput); - }, - }); - - return sub; - } - - public wrap(modelToWrap: LanguageModelV1) { - return experimental_wrapLanguageModel({ - model: modelToWrap, - middleware: createAgentMiddleware(this), - }); - } - - /** - * Resolves with an `AgentPlan` based on the information provided in the `options`, including: - * - * - The `goal` for the agent to achieve - * - The observed current `state` - * - The `machine` (e.g. a state machine) that specifies what can happen next - * - Additional `context` - */ - public decide(opts: AgentDecideOptions) { - return agentDecide(this, opts); - } -} diff --git a/src/ai-sdk/index.ts b/src/ai-sdk/index.ts new file mode 100644 index 0000000..1215ee2 --- /dev/null +++ b/src/ai-sdk/index.ts @@ -0,0 +1,93 @@ +import { generateObject } from 'ai'; +import { z } from 'zod'; +import type { AgentAdapter, StandardSchemaV1 } from '../types.js'; + +/** + * Create an adapter that uses the Vercel AI SDK for decide/classify. + * Model strings like 'anthropic/claude-sonnet-4.5' are resolved via the + * AI SDK's model registry. + */ +export function createAiSdkAdapter(): AgentAdapter { + return { + async decide({ model, prompt, options, reasoning }) { + // Build the discriminated union schema for options + const optionKeys = Object.keys(options); + + // Build per-option schemas + const optionSchemas: Record = {}; + for (const [key, opt] of Object.entries(options)) { + if (opt.schema) { + // Use the provided schema as the data shape + optionSchemas[key] = z.object({ + choice: z.literal(key), + data: toZodSchema(opt.schema), + ...(reasoning ? { reasoning: z.string().describe('Chain-of-thought reasoning for this decision') } : {}), + }); + } else { + optionSchemas[key] = z.object({ + choice: z.literal(key), + data: z.object({}), + ...(reasoning ? { reasoning: z.string().describe('Chain-of-thought reasoning for this decision') } : {}), + }); + } + } + + // Build the union schema + const schemas = optionKeys.map((k) => optionSchemas[k]!); + const schema = + schemas.length === 1 + ? schemas[0]! + : z.union(schemas as [z.ZodType, z.ZodType, ...z.ZodType[]]); + + // Build the system prompt with option descriptions + const optionDescriptions = Object.entries(options) + .map(([key, opt]) => `- ${key}: ${opt.description}`) + .join('\n'); + + const systemPrompt = `You must choose exactly one of the following options:\n${optionDescriptions}\n\nRespond with your choice and any required data.`; + + const result = await generateObject({ + model: resolveModel(model), + system: systemPrompt, + prompt, + schema, + }); + + const obj = result.object as { + choice: string; + data: Record; + reasoning?: string; + }; + + return { + choice: obj.choice, + data: obj.data ?? {}, + reasoning: obj.reasoning, + }; + }, + }; +} + +/** + * Convert a StandardSchemaV1 to a zod schema. + * If it's already a zod schema, return as-is. + * Otherwise, fall back to z.record for basic compatibility. + */ +function toZodSchema(schema: StandardSchemaV1): z.ZodType { + // Check if it's already a zod schema (has _zod property in v4) + if ('_zod' in schema || '_def' in schema) { + return schema as unknown as z.ZodType; + } + // Fallback: accept any object + return z.record(z.string(), z.unknown()); +} + +/** + * Resolve a model string to an AI SDK model. + * Supports the `provider/model` format via the AI SDK registry. + */ +function resolveModel(model: string): Parameters[0]['model'] { + // The AI SDK accepts model strings when using a provider registry. + // For now, return as-is — users configure their provider registry externally. + return model as any; +} diff --git a/src/classify.ts b/src/classify.ts new file mode 100644 index 0000000..78590ba --- /dev/null +++ b/src/classify.ts @@ -0,0 +1,32 @@ +import type { ClassifyConfig, StateConfig } from './types.js'; + +/** + * Create a classification state. Sugar over `decide` for simple routing — + * categories with descriptions, no per-option schemas. + */ +export function classify(config: ClassifyConfig): StateConfig { + // Convert classify categories into decide options + const decideOptions: Record = {}; + for (const [key, val] of Object.entries(config.into)) { + decideOptions[key] = { description: val.description }; + } + + return { + __type: 'classify', + __classifyConfig: config, + __decideConfig: { + model: config.model, + adapter: config.adapter, + prompt: config.prompt, + options: decideOptions, + onDone: ({ result, context }) => { + // Transform decide result → classify result + return config.onDone({ + result: { category: result.choice }, + context, + }); + }, + }, + on: config.on, + }; +} diff --git a/src/decide.ts b/src/decide.ts new file mode 100644 index 0000000..cdc84aa --- /dev/null +++ b/src/decide.ts @@ -0,0 +1,13 @@ +import type { DecideConfig, StateConfig } from './types.js'; + +/** + * Create a decision state where an LLM picks from constrained options. + * Each option has a description and optional schema for structured data. + */ +export function decide(config: DecideConfig): StateConfig { + return { + __type: 'decide', + __decideConfig: config, + on: config.on, + }; +} diff --git a/src/decision.test.ts b/src/decision.test.ts deleted file mode 100644 index 5a768b3..0000000 --- a/src/decision.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { test, expect } from 'vitest'; -import { createAgent, fromDecision } from '.'; -import { createActor, createMachine, waitFor } from 'xstate'; -import { z } from 'zod'; -import { LanguageModelV1CallOptions } from 'ai'; -import { dummyResponseValues, MockLanguageModelV1 } from './mockModel'; - -const doGenerate = async (params: LanguageModelV1CallOptions) => { - const keys = - params.mode.type === 'regular' ? params.mode.tools?.map((t) => t.name) : []; - - return { - ...dummyResponseValues, - finishReason: 'tool-calls', - toolCalls: [ - { - toolCallType: 'function', - toolCallId: 'call-1', - toolName: keys![0], - args: `{ "type": "${keys?.[0]}" }`, - }, - ], - } as any; -}; - -test('fromDecision() makes a decision', async () => { - const model = new MockLanguageModelV1({ - doGenerate, - }); - const agent = createAgent({ - name: 'test', - model, - events: { - doFirst: z.object({}), - doSecond: z.object({}), - }, - }); - - const machine = createMachine({ - initial: 'first', - states: { - first: { - invoke: { - src: fromDecision(agent), - }, - on: { - doFirst: 'second', - }, - }, - second: { - invoke: { - src: fromDecision(agent), - }, - on: { - doSecond: 'third', - }, - }, - third: {}, - }, - }); - - const actor = createActor(machine); - - actor.start(); - - await waitFor(actor, (s) => s.matches('third')); - - expect(actor.getSnapshot().value).toBe('third'); -}); - -test('interacts with an actor', async () => { - const model = new MockLanguageModelV1({ - doGenerate, - }); - const agent = createAgent({ - name: 'test', - model, - events: { - doFirst: z.object({}), - doSecond: z.object({}), - }, - }); - - const machine = createMachine({ - initial: 'first', - states: { - first: { - on: { - doFirst: 'second', - }, - }, - second: { - on: { - doSecond: 'third', - }, - }, - third: {}, - }, - }); - - const actor = createActor(machine); - - agent.interact(actor, () => ({ - goal: 'Some goal', - })); - - actor.start(); - - await waitFor(actor, (s) => s.matches('third')); - - expect(actor.getSnapshot().value).toBe('third'); -}); - -test('interacts with an actor (late interaction)', async () => { - const model = new MockLanguageModelV1({ - doGenerate, - }); - const agent = createAgent({ - name: 'test', - model, - events: { - doFirst: z.object({}), - doSecond: z.object({}), - }, - }); - - const machine = createMachine({ - initial: 'first', - states: { - first: { - on: { - doFirst: 'second', - }, - }, - second: { - on: { - doSecond: 'third', - }, - }, - third: {}, - }, - }); - - const actor = createActor(machine); - - actor.start(); - - agent.interact(actor, () => ({ - goal: 'Some goal', - })); - - await waitFor(actor, (s) => s.matches('third')); - - expect(actor.getSnapshot().value).toBe('third'); -}); diff --git a/src/decision.ts b/src/decision.ts deleted file mode 100644 index 7f93c94..0000000 --- a/src/decision.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { AnyActor, AnyMachineSnapshot, fromPromise } from 'xstate'; -import { - AnyAgent, - AgentDecideOptions, - AgentDecisionLogic, - AgentDecisionInput, - AgentPlanner, - AgentPlan, - EventsFromZodEventMapping, -} from './types'; -import { simplePlanner } from './planners/simplePlanner'; - -export async function agentDecide( - agent: T, - options: AgentDecideOptions -): Promise> | undefined> { - const resolvedOptions = { - ...agent.defaultOptions, - ...options, - }; - const { - planner = simplePlanner as AgentPlanner, - goal, - events = agent.events, - state, - machine, - model = agent.model, - ...otherPlanInput - } = resolvedOptions; - - const plan = await planner(agent, { - model, - goal, - events, - state, - machine, - ...otherPlanInput, - }); - - if (plan?.nextEvent) { - agent.addPlan(plan); - await resolvedOptions.execute?.(plan.nextEvent); - } - - return plan; -} - -export function fromDecision( - agent: AnyAgent, - defaultInput?: AgentDecisionInput -): AgentDecisionLogic { - return fromPromise(async ({ input, self }) => { - const parentRef = self._parent; - if (!parentRef) { - return; - } - - const snapshot = parentRef.getSnapshot() as AnyMachineSnapshot; - const inputObject = typeof input === 'string' ? { goal: input } : input; - const resolvedInput = { - ...defaultInput, - ...inputObject, - }; - const contextToInclude = - resolvedInput.context === true - ? // include entire context - parentRef.getSnapshot().context - : resolvedInput.context; - const state = { - value: snapshot.value, - context: contextToInclude, - }; - - const plan = await agentDecide(agent, { - machine: (parentRef as AnyActor).logic, - state, - execute: async (event) => { - parentRef.send(event); - }, - ...resolvedInput, - }); - - return plan; - }) as AgentDecisionLogic; -} diff --git a/src/event.ts b/src/event.ts new file mode 100644 index 0000000..151167d --- /dev/null +++ b/src/event.ts @@ -0,0 +1,107 @@ +import type { AgentEvent, AgentMachine, AgentState, StandardSchemaV1 } from './types.js'; +import { applyTransition, resolveStateConfig } from './utils.js'; + +/** + * Send a typed event to the current state. + * Validates the event payload against declared schemas, then searches from + * the current state up through ancestors for a matching handler. + * Parent handlers preempt children. + * + * Returns a new AgentState (synchronous — no async work). + */ +export function sendEvent( + machine: AgentMachine, + state: AgentState, + event: AgentEvent +): AgentState { + // Validate event payload against declared schemas + validateEventSync(machine, state.value, event); + + const parts = state.value.split('.'); + + // Walk from outermost to innermost for preemption semantics: + // parent `on` preempts children. + for (let i = 1; i <= parts.length; i++) { + const path = parts.slice(0, i).join('.'); + const config = resolveStateConfig(machine, path); + + if (config.on && config.on[event.type]) { + const handler = config.on[event.type]!; + const transition = handler({ context: state.context, event }); + + if (transition.target) { + return applyTransition(machine, state, transition, path); + } + + // Self-transition: update context, keep same state/status + return { + ...state, + context: transition.context + ? { ...state.context, ...transition.context } + : state.context, + }; + } + } + + throw new Error( + `No handler for event '${event.type}' in state '${state.value}'` + ); +} + +/** + * Validate event payload against the schema declared in state-level or + * root-level `events`. State events override root events. + * Uses synchronous validation — throws on invalid payload. + */ +function validateEventSync( + machine: AgentMachine, + value: string, + event: AgentEvent +): void { + const schema = findEventSchema(machine, value, event.type); + if (!schema) return; // no schema declared — skip validation + + const result = schema['~standard'].validate(event); + + // Handle sync result (most schema libs return sync for simple schemas) + if (result && typeof result === 'object' && 'issues' in result && result.issues) { + const messages = (result.issues as Array<{ message: string }>) + .map((i) => i.message) + .join(', '); + throw new Error( + `Invalid event '${event.type}': ${messages}` + ); + } + + // If validate returns a Promise, we can't block on it synchronously. + // For async schemas, users should validate before calling sendEvent. + if (result instanceof Promise) { + // Can't await in sync function — skip async validation. + // This is a known limitation; createInitialState handles async validation. + return; + } +} + +/** + * Find the event schema for a given event type. + * Walks from the current state up to root, with state-level schemas + * overriding root-level schemas. + */ +function findEventSchema( + machine: AgentMachine, + value: string, + eventType: string +): StandardSchemaV1 | undefined { + // Check state-level events (innermost wins for schemas) + const parts = value.split('.'); + for (let i = parts.length; i >= 1; i--) { + const path = parts.slice(0, i).join('.'); + const config = resolveStateConfig(machine, path); + if (config.events?.[eventType]) { + return config.events[eventType]; + } + } + + // Fall back to root-level events + return machine.events?.[eventType]; +} diff --git a/src/graph/index.ts b/src/graph/index.ts new file mode 100644 index 0000000..5e5554e --- /dev/null +++ b/src/graph/index.ts @@ -0,0 +1,33 @@ +import type { AgentMachine } from '../types.js'; + +export interface GraphNode { + id: string; + type: 'state' | 'decide' | 'classify' | 'final'; +} + +export interface GraphEdge { + source: string; + target: string; + label?: string; +} + +export interface Graph { + nodes: GraphNode[]; + edges: GraphEdge[]; +} + +/** + * Convert an agent machine to a graph representation. + * TODO: implement AST analysis for edge extraction + */ +export function toGraph(_machine: AgentMachine): Graph { + throw new Error('toGraph is not yet implemented'); +} + +/** + * Convert an agent machine to a Mermaid stateDiagram-v2 string. + * TODO: implement + */ +export function toMermaid(_machine: AgentMachine): string { + throw new Error('toMermaid is not yet implemented'); +} diff --git a/src/index.ts b/src/index.ts index 8e2826a..4e5e097 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,37 @@ -export { createAgent } from './agent'; -export { fromText, fromTextStream } from './text'; -export { fromDecision } from './decision'; -export * from './types'; +// Core +export { createAgentMachine } from './machine.js'; +export { createInitialState } from './state.js'; +export { step } from './step.js'; +export { run } from './run.js'; +export { stream } from './stream.js'; +export { sendEvent } from './event.js'; + +// AI primitives +export { decide } from './decide.js'; +export { classify } from './classify.js'; + +// Adapter +export { createAdapter } from './adapter.js'; + +// Types +export type { + AgentAdapter, + AgentEvent, + AgentMachine, + AgentRunResult, + AgentSnapshot, + AgentState, + ClassifyConfig, + ClassifyResult, + DecideConfig, + DecideResult, + MachineConfig, + OnDoneArgs, + OutputArgs, + RunArgs, + StandardSchemaV1, + StateConfig, + Trace, + TransitionArgs, + TransitionResult, +} from './types.js'; diff --git a/src/machine.ts b/src/machine.ts new file mode 100644 index 0000000..2f07bbf --- /dev/null +++ b/src/machine.ts @@ -0,0 +1,9 @@ +import type { AgentMachine, MachineConfig } from './types.js'; + +/** + * Create an agent machine definition. + * The machine is a pure configuration object — no runtime state. + */ +export function createAgentMachine(config: MachineConfig): AgentMachine { + return config; +} diff --git a/src/memory.ts b/src/memory.ts deleted file mode 100644 index a6994ab..0000000 --- a/src/memory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { AgentMemory, AgentMemoryContext } from './types'; - -export function createAgentMemory(): AgentMemory { - const storage = { - sessions: {} as Record, - }; - - return { - append: async (sessionId, key, item) => { - storage.sessions[sessionId] = - storage.sessions[sessionId] || - ({ - observations: [], - messages: [], - plans: [], - feedback: [], - } satisfies AgentMemoryContext); - - storage.sessions[sessionId]![key].push(item as any); - }, - getAll: async (sessionId, key) => { - return storage.sessions[sessionId]?.[key]; - }, - }; -} diff --git a/src/middleware.ts b/src/middleware.ts deleted file mode 100644 index ff1713c..0000000 --- a/src/middleware.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { - Experimental_LanguageModelV1Middleware as LanguageModelV1Middleware, - LanguageModelV1StreamPart, -} from 'ai'; -import { - AnyAgent, - LanguageModelV1TextPart, - LanguageModelV1ToolCallPart, -} from './types'; -import { randomId } from './utils'; - -export function createAgentMiddleware(agent: AnyAgent) { - const middleware: LanguageModelV1Middleware = { - transformParams: async ({ params }) => { - return params; - }, - wrapGenerate: async ({ doGenerate, params }) => { - const id = randomId(); - - params.prompt.forEach((p) => { - agent.addMessage({ - id, - ...p, - timestamp: Date.now(), - correlationId: params.providerMetadata - ?.correlationId as unknown as string, - parentCorrelationId: params.providerMetadata - ?.parentCorrelationId as unknown as string, - }); - }); - - const result = await doGenerate(); - - return result; - }, - - wrapStream: async ({ doStream, params }) => { - const id = randomId(); - - params.prompt.forEach((message) => { - message.content; - agent.addMessage({ - id, - ...message, - timestamp: Date.now(), - correlationId: params.providerMetadata - ?.correlationId as unknown as string, - parentCorrelationId: params.providerMetadata - ?.parentCorrelationId as unknown as string, - }); - }); - - const { stream, ...rest } = await doStream(); - - let generatedText = ''; - - const transformStream = new TransformStream< - LanguageModelV1StreamPart, - LanguageModelV1StreamPart - >({ - transform(chunk, controller) { - if (chunk.type === 'text-delta') { - generatedText += chunk.textDelta; - } - - controller.enqueue(chunk); - }, - - flush() { - const content: ( - | LanguageModelV1TextPart - | LanguageModelV1ToolCallPart - )[] = []; - - if (generatedText) { - content.push({ - type: 'text', - text: generatedText, - }); - } - - agent.addMessage({ - id: randomId(), - timestamp: Date.now(), - role: 'assistant', - content, - responseId: id, - correlationId: params.providerMetadata - ?.correlationId as unknown as string, - parentCorrelationId: params.providerMetadata - ?.parentCorrelationId as unknown as string, - }); - }, - }); - - return { - stream: stream.pipeThrough(transformStream), - ...rest, - }; - }, - }; - return middleware; -} diff --git a/src/mockModel.ts b/src/mockModel.ts deleted file mode 100644 index 653bd38..0000000 --- a/src/mockModel.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { LanguageModelV1 } from 'ai'; - -export class MockLanguageModelV1 implements LanguageModelV1 { - readonly specificationVersion = 'v1'; - - readonly provider: LanguageModelV1['provider']; - readonly modelId: LanguageModelV1['modelId']; - - doGenerate: LanguageModelV1['doGenerate']; - doStream: LanguageModelV1['doStream']; - - readonly defaultObjectGenerationMode: LanguageModelV1['defaultObjectGenerationMode']; - readonly supportsStructuredOutputs: LanguageModelV1['supportsStructuredOutputs']; - constructor({ - provider = 'mock-provider', - modelId = 'mock-model-id', - doGenerate = notImplemented, - doStream = notImplemented, - defaultObjectGenerationMode = undefined, - supportsStructuredOutputs = undefined, - }: { - provider?: LanguageModelV1['provider']; - modelId?: LanguageModelV1['modelId']; - doGenerate?: LanguageModelV1['doGenerate']; - doStream?: LanguageModelV1['doStream']; - defaultObjectGenerationMode?: LanguageModelV1['defaultObjectGenerationMode']; - supportsStructuredOutputs?: LanguageModelV1['supportsStructuredOutputs']; - } = {}) { - this.provider = provider; - this.modelId = modelId; - this.doGenerate = doGenerate; - this.doStream = doStream; - - this.defaultObjectGenerationMode = defaultObjectGenerationMode; - this.supportsStructuredOutputs = supportsStructuredOutputs; - } -} - -function notImplemented(): never { - throw new Error('Not implemented'); -} - -export const dummyResponseValues = { - rawCall: { rawPrompt: 'prompt', rawSettings: {} }, - finishReason: 'stop' as const, - usage: { promptTokens: 10, completionTokens: 20 }, -}; diff --git a/src/planners/shortestPathPlanner.ts b/src/planners/shortestPathPlanner.ts deleted file mode 100644 index 3abc85c..0000000 --- a/src/planners/shortestPathPlanner.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { AgentPlan, AgentPlanInput, AnyAgent } from '../types'; -import { getShortestPaths } from '@xstate/graph'; - -export async function simplePlanner( - agent: T, - input: AgentPlanInput -): Promise | undefined> { - // 1. Determine goal state criteria - // e.g. a state where the agent has won a game - void 0; - - // 2. Determine possible events that can occur - void 0; - - // 3. Get shortest paths from current state to - // a state matching the criteria, using - // possible events - void 0; - - // 4. Return shortest path as a plan - return null as any; -} diff --git a/src/planners/simplePlanner.ts b/src/planners/simplePlanner.ts deleted file mode 100644 index bb2de97..0000000 --- a/src/planners/simplePlanner.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { CoreMessage, type CoreTool, generateText, tool } from 'ai'; -import { - AgentPlan, - AgentPlanInput, - ObservedState, - PromptTemplate, - TransitionData, - AnyAgent, -} from '../types'; -import { getAllTransitions, randomId } from '../utils'; -import { AnyStateMachine } from 'xstate'; -import { defaultTextTemplate } from '../templates/defaultText'; -import { getMessages } from '../text'; - -function getTransitions( - state: ObservedState, - machine: AnyStateMachine -): TransitionData[] { - if (!machine) { - return []; - } - - const resolvedState = machine.resolveState(state); - return getAllTransitions(resolvedState); -} - -const simplePlannerPromptTemplate: PromptTemplate = (data) => { - return ` -${defaultTextTemplate(data)} - -Make at most one tool call to achieve the above goal. If the goal cannot be achieved with any tool calls, do not make any tool call. - `.trim(); -}; - -export async function simplePlanner( - agent: T, - input: AgentPlanInput -): Promise | undefined> { - // Get all of the possible next transitions - const transitions: TransitionData[] = input.machine - ? getTransitions(input.state, input.machine) - : Object.entries(input.events).map(([eventType, { description }]) => ({ - eventType, - description, - })); - - // Only keep the transitions that match the event types that are in the event mapping - // TODO: allow for custom filters - const filter = (eventType: string) => - Object.keys(input.events).includes(eventType); - - // Mapping of each event type (e.g. "mouse.click") - // to a valid function name (e.g. "mouse_click") - const functionNameMapping: Record = {}; - - const toolTransitions = transitions - .filter((t) => { - return filter(t.eventType); - }) - .map((t) => { - const name = t.eventType.replace(/\./g, '_'); - functionNameMapping[name] = t.eventType; - - return { - type: 'function', - eventType: t.eventType, - description: t.description, - name, - } as const; - }); - - // Convert the transition data to a tool map that the - // Vercel AI SDK can use - const toolMap: Record> = {}; - for (const toolTransitionData of toolTransitions) { - const toolZodType = input.events?.[toolTransitionData.eventType]; - - if (!toolZodType) { - continue; - } - - toolMap[toolTransitionData.name] = tool({ - description: toolZodType?.description ?? toolTransitionData.description, - parameters: toolZodType, - execute: async (params: Record) => { - const event = { - type: toolTransitionData.eventType, - ...params, - }; - - return event; - }, - }); - } - - if (!Object.keys(toolMap).length) { - // No valid transitions for the specified tools - return undefined; - } - - // Create a prompt with the given context and goal. - // The template is used to ensure that a single tool call at most is made. - const prompt = simplePlannerPromptTemplate({ - context: input.state.context, - goal: input.goal, - }); - - const messages = await getMessages(agent, prompt, input); - - const model = input.model ? agent.wrap(input.model) : agent.model; - - const { - state, - machine, - previousPlan, - events, - goal, - model: _, - ...rest - } = input; - - const result = await generateText({ - // ...input, - ...rest, - model, - messages, - tools: toolMap as any, - toolChoice: input.toolChoice ?? 'required', - }); - - result.responseMessages.forEach((m) => { - const message: CoreMessage = m; - - agent.addMessage({ - ...message, - id: randomId(), - timestamp: Date.now(), - }); - }); - - const singleResult = result.toolResults[0]; - - if (!singleResult) { - // TODO: retries? - console.warn('No tool call results returned'); - return undefined; - } - - return { - goal: input.goal, - state: input.state, - execute: async (state) => { - if (JSON.stringify(state) === JSON.stringify(input.state)) { - return singleResult.result; - } - return undefined; - }, - nextEvent: singleResult.result, - sessionId: agent.sessionId, - timestamp: Date.now(), - }; -} diff --git a/src/run.ts b/src/run.ts new file mode 100644 index 0000000..16590ce --- /dev/null +++ b/src/run.ts @@ -0,0 +1,51 @@ +import type { AgentMachine, AgentRunResult, AgentState } from './types.js'; +import { step } from './step.js'; +import { getAvailableEvents, resolveStateConfig } from './utils.js'; + +/** + * Run the machine until completion, waiting, or error. + * Loops `step()` while status is 'running'. + */ +export async function run( + machine: AgentMachine, + state: AgentState +): Promise { + let current = state; + + while (current.status === 'running') { + current = await step(machine, current); + } + + switch (current.status) { + case 'done': + return { + status: 'done', + state: current, + output: current.output, + context: current.context, + }; + + case 'waiting': + return { + status: 'waiting', + state: current, + value: current.value, + events: getAvailableEvents(machine, current.value), + context: current.context, + }; + + case 'error': + return { + status: 'error', + state: current, + error: current.error, + }; + + default: + return { + status: 'error', + state: current, + error: `Unexpected status: ${current.status}`, + }; + } +} diff --git a/src/schemas.ts b/src/schemas.ts deleted file mode 100644 index aefa24a..0000000 --- a/src/schemas.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ZodType, type SomeZodObject } from 'zod'; - -export type ZodEventMapping = { - // map event types to Zod types - [eventType: string]: SomeZodObject; -}; - -export type ZodContextMapping = { - // map context keys to Zod types - [contextKey: string]: ZodType; -}; diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..b9d595b --- /dev/null +++ b/src/state.ts @@ -0,0 +1,50 @@ +import type { AgentMachine, AgentState } from './types.js'; +import { + enterCompoundStates, + resolveInitial, + validateSchema, +} from './utils.js'; + +/** + * Create the initial serializable state for a machine + input. + * Validates input, initializes context, resolves the initial transition. + */ +export async function createInitialState( + machine: AgentMachine, + input: unknown +): Promise { + // Validate input if schema provided + let validatedInput = input; + if (machine.inputSchema) { + validatedInput = await validateSchema(machine.inputSchema, input); + } + + // Initialize context + const context = machine.context(validatedInput); + + // Resolve initial transition + const init = resolveInitial(machine.initial, { + context, + parentParams: {}, + }); + + if (!init.target) { + throw new Error('Initial transition must specify a target state'); + } + + let state: AgentState = { + value: init.target, + params: {}, + context: init.context ? { ...context, ...init.context } : context, + status: 'running', + }; + + if (init.params) { + state.params = { [init.target]: init.params }; + } + + // Enter compound states if needed + state = enterCompoundStates(machine, state); + + return state; +} diff --git a/src/step.ts b/src/step.ts new file mode 100644 index 0000000..a873ce6 --- /dev/null +++ b/src/step.ts @@ -0,0 +1,167 @@ +import type { AgentMachine, AgentState } from './types.js'; +import { + applyTransition, + getParentConfig, + getParentParams, + resolveStateConfig, +} from './utils.js'; + +/** + * Execute one state transition. + * + * - Final state → status 'done' (or bubble to parent onDone) + * - Decide/classify → call adapter, apply onDone + * - Run state → execute run, apply onDone + * - Waiting state (on, no run) → status 'waiting' + */ +export async function step( + machine: AgentMachine, + state: AgentState +): Promise { + if (state.status === 'done' || state.status === 'error') { + return state; + } + + const config = resolveStateConfig(machine, state.value); + + // ─── Final state ─── + if (config.type === 'final') { + return handleFinalState(machine, state); + } + + // ─── Decide / Classify state ─── + if (config.__decideConfig) { + return handleDecideState(machine, state); + } + + // ─── Run state ─── + if (config.run) { + return handleRunState(machine, state); + } + + // ─── Waiting state ─── + if (config.on) { + return { ...state, status: 'waiting' }; + } + + // ─── Compound state with no run (just initial + children) ─── + // This shouldn't normally happen since enterCompoundStates resolves on entry. + // But handle defensively. + if (config.states && config.initial) { + return { ...state, status: 'running' }; + } + + return { + ...state, + status: 'error', + error: `State '${state.value}' has no run, events, or children`, + }; +} + +async function handleFinalState( + machine: AgentMachine, + state: AgentState +): Promise { + const config = resolveStateConfig(machine, state.value); + + // Compute output + const output = config.output + ? config.output({ context: state.context }) + : undefined; + + const parts = state.value.split('.'); + + // Root-level final state → done + if (parts.length <= 1) { + return { ...state, status: 'done', output }; + } + + // Nested final state — check parent for onDone + const parentConfig = getParentConfig(machine, state.value); + if (parentConfig?.onDone) { + const parentPath = parts.slice(0, -1).join('.'); + const transition = parentConfig.onDone({ + result: output, + context: state.context, + }); + return applyTransition(machine, state, transition, parentPath); + } + + // Parent has no onDone — match xstate semantics: compound state is "done" + // but no transition fires. Machine halts here; ancestor on handlers can + // still match events via sendEvent. + return { ...state, status: 'waiting' }; +} + +async function handleDecideState( + machine: AgentMachine, + state: AgentState +): Promise { + const config = resolveStateConfig(machine, state.value); + const decideConfig = config.__decideConfig!; + + // Get adapter + const adapter = decideConfig.adapter ?? machine.adapter; + if (!adapter) { + return { + ...state, + status: 'error', + error: `No adapter configured for decide state '${state.value}'`, + }; + } + + // Resolve prompt + const parentParams = getParentParams(state); + const prompt = + typeof decideConfig.prompt === 'function' + ? decideConfig.prompt({ context: state.context, parentParams }) + : decideConfig.prompt; + + try { + const result = await adapter.decide({ + model: decideConfig.model, + prompt, + options: decideConfig.options, + reasoning: decideConfig.reasoning, + }); + + // Apply onDone + const transition = decideConfig.onDone({ + result, + context: state.context, + }); + return applyTransition(machine, state, transition, state.value); + } catch (error) { + return { ...state, status: 'error', error }; + } +} + +async function handleRunState( + machine: AgentMachine, + state: AgentState +): Promise { + const config = resolveStateConfig(machine, state.value); + + try { + const result = await config.run!({ + context: state.context, + parentParams: getParentParams(state), + }); + + if (config.onDone) { + const transition = config.onDone({ + result, + context: state.context, + }); + return applyTransition(machine, state, transition, state.value); + } + + // run with no onDone — stay in state, mark waiting if has events + if (config.on) { + return { ...state, status: 'waiting' }; + } + return state; + } catch (error) { + return { ...state, status: 'error', error }; + } +} diff --git a/src/strategies/chain-of-note.ts b/src/strategies/chain-of-note.ts deleted file mode 100644 index 877799c..0000000 --- a/src/strategies/chain-of-note.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { GenerateTextResult, LanguageModel } from 'ai'; -import wiki, { wikiSearchResult, wikiSummary } from 'wikipedia'; -import { assign, fromPromise, setup } from 'xstate'; -import { AnyAgent } from '../types'; - -const searchWiki = fromPromise( - async ({ - input, - }: { - input: { - query: string; - limit?: number; - }; - }) => { - const passages = await wiki.search(input.query, { - limit: input.limit ?? 5, - }); - return passages; - } -); - -const extractSummaries = fromPromise( - async ({ - input, - }: { - input: { - searchResult: wikiSearchResult; - }; - }) => { - const summaries = await Promise.all( - input.searchResult.results.map(async (result) => { - const summary = await wiki.summary(result.title); - return { - title: result.title, - summary, - }; - }) - ); - return summaries; - } -); - -export const chainOfNote = setup({ - types: { - input: {} as { - model: LanguageModel; - agent: AnyAgent; - prompt: string; - }, - context: {} as { - searchResults: wikiSearchResult | null; - summaries: - | { - title: any; - summary: wikiSummary; - }[] - | null; - model: LanguageModel; - agent: AnyAgent; - prompt: string; - }, - output: {} as GenerateTextResult, - }, - actors: { - searchWiki, - extractSummaries, - }, -}).createMachine({ - initial: 'searching', - context: ({ input }) => ({ - ...input, - searchResults: null, - summaries: null, - }), - states: { - searching: { - invoke: { - src: 'searchWiki', - input: ({ context }) => ({ - query: context.prompt, - }), - onDone: { - actions: assign({ - searchResults: ({ event }) => event.output, - }), - target: 'extracting', - }, - }, - }, - extracting: { - invoke: { - src: 'extractSummaries', - input: ({ context }) => ({ - searchResult: context.searchResults!, - }), - onDone: { - actions: assign({ - summaries: ({ event }) => event.output, - }), - target: 'generating', - }, - }, - }, - generating: {}, - }, -}); diff --git a/src/stream.ts b/src/stream.ts new file mode 100644 index 0000000..b7261aa --- /dev/null +++ b/src/stream.ts @@ -0,0 +1,29 @@ +import type { AgentMachine, AgentSnapshot, AgentState } from './types.js'; +import { step } from './step.js'; + +/** + * Yields a snapshot after each transition until completion, waiting, or error. + */ +export async function* stream( + machine: AgentMachine, + state: AgentState +): AsyncGenerator { + let current = state; + + // Yield initial snapshot + yield toSnapshot(current); + + while (current.status === 'running') { + current = await step(machine, current); + yield toSnapshot(current); + } +} + +function toSnapshot(state: AgentState): AgentSnapshot { + return { + value: state.value, + context: state.context, + status: state.status, + params: state.params, + }; +} diff --git a/src/templates/defaultText.ts b/src/templates/defaultText.ts deleted file mode 100644 index 2cb841b..0000000 --- a/src/templates/defaultText.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PromptTemplate } from '../types'; -import { wrapInXml } from '../utils'; - -export const defaultTextTemplate: PromptTemplate = (data) => { - const preamble = [ - data.context - ? wrapInXml('context', JSON.stringify(data.context)) - : undefined, - ] - .filter(Boolean) - .join('\n'); - - return ` -${preamble} - -${data.goal} - `.trim(); -}; diff --git a/src/text.ts b/src/text.ts deleted file mode 100644 index 5f66b6e..0000000 --- a/src/text.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { - generateText, - streamText, - type CoreMessage, - type CoreTool, - type GenerateTextResult, -} from 'ai'; -import { - AgentGenerateTextOptions, - AgentStreamTextOptions, - AnyAgent, -} from './types'; -import { defaultTextTemplate } from './templates/defaultText'; -import { - ObservableActorLogic, - Observer, - PromiseActorLogic, - fromObservable, - fromPromise, - toObserver, -} from 'xstate'; - -/** - * Gets an array of messages from the given prompt, based on the agent and options. - * - * @param agent - * @param prompt - * @param options - * @returns - */ -export async function getMessages( - agent: AnyAgent, - prompt: string, - options: Omit -): Promise { - let messages: CoreMessage[] = []; - if (typeof options.messages === 'function') { - messages = await options.messages(agent); - } else if (options.messages) { - messages = options.messages; - } - - messages = messages.concat({ - role: 'user', - content: prompt, - }); - - return messages; -} - -export function fromTextStream( - agent: T, - options?: AgentStreamTextOptions -): ObservableActorLogic< - { textDelta: string }, - Omit & { - context?: AgentStreamTextOptions['context']; - } -> { - const template = options?.template ?? defaultTextTemplate; - return fromObservable(({ input }) => { - const observers = new Set>(); - - // TODO: check if messages was provided instead - - (async () => { - const model = input.model ? agent.wrap(input.model) : agent.model; - const goal = - typeof input.prompt === 'string' - ? input.prompt - : await input.prompt(agent); - const promptWithContext = template({ - goal, - context: input.context, - }); - const messages = await getMessages(agent, promptWithContext, input); - const result = await streamText({ - ...options, - ...input, - prompt: undefined, // overwritten by messages - model, - messages, - }); - - for await (const part of result.fullStream) { - if (part.type === 'text-delta') { - observers.forEach((observer) => { - observer.next?.(part); - }); - } - } - })(); - - return { - subscribe: (...args: any[]) => { - const observer = toObserver(...args); - observers.add(observer); - - return { - unsubscribe: () => { - observers.delete(observer); - }, - }; - }, - }; - }); -} - -export function fromText( - agent: T, - options?: AgentGenerateTextOptions -): PromiseActorLogic< - GenerateTextResult>>, - Omit & { - context?: AgentGenerateTextOptions['context']; - } -> { - const resolvedOptions = { - ...agent.defaultOptions, - ...options, - }; - - const template = resolvedOptions.template ?? defaultTextTemplate; - - return fromPromise(async ({ input }) => { - const goal = - typeof input.prompt === 'string' - ? input.prompt - : await input.prompt(agent); - - const promptWithContext = template({ - goal, - context: input.context, - }); - - const messages = await getMessages(agent, promptWithContext, input); - - const model = input.model ? agent.wrap(input.model) : agent.model; - - return await generateText({ - ...input, - ...options, - prompt: undefined, - messages, - model, - }); - }); -} diff --git a/src/types.ts b/src/types.ts index 5e504ca..93c5a4a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,447 +1,230 @@ -import { - ActorLogic, - ActorRefFrom, - AnyActorRef, - AnyEventObject, - AnyStateMachine, - EventFrom, - EventObject, - PromiseActorLogic, - SnapshotFrom, - StateValue, - Subscription, - TransitionSnapshot, - Values, -} from 'xstate'; -import { - CoreMessage, - generateText, - GenerateTextResult, - LanguageModel, - streamText, -} from 'ai'; -import { ZodContextMapping, ZodEventMapping } from './schemas'; -import { TypeOf } from 'zod'; -import { Agent } from './agent'; - -export type GenerateTextOptions = Parameters[0]; - -export type StreamTextOptions = Parameters[0]; - -export type AgentPlanInput = Omit< - GenerateTextOptions, - 'prompt' | 'tools' -> & { - /** - * The currently observed state. - */ - state: ObservedState; - /** - * The goal for the agent to accomplish. - * The agent will create a plan based on this goal. - */ - goal: string; - /** - * The events that the agent can trigger. This is a mapping of - * event types to Zod event schemas. - */ - events: ZodEventMapping; - /** - * The state machine that represents the environment the agent - * is interacting with. - */ - machine?: AnyStateMachine; - /** - * The previous plan. - */ - previousPlan?: AgentPlan; -}; - -export type AgentPlan = { - goal: string; - state: ObservedState; - content?: string; - /** - * Executes the plan based on the given `state` and resolves with - * a potential next `event` to trigger to achieve the `goal`. - */ - execute: (state: ObservedState) => Promise; - nextEvent: TEvent | undefined; - sessionId: string; - timestamp: number; -}; - -export interface TransitionData { - eventType: string; - description?: string; - guard?: { type: string }; - target?: any; +// ─── Standard Schema compatibility ─── +// Minimal Standard Schema V1 interface so any compliant library (zod, valibot, arktype) works. + +export interface StandardSchemaV1 { + readonly '~standard': { + readonly version: 1; + readonly vendor: string; + readonly validate: (value: unknown) => any; + readonly types?: { readonly input?: unknown; readonly output?: Output }; + }; } -export type PromptTemplate = (data: { - goal: string; - /** - * The observed state - */ - state?: ObservedState; - /** - * The context to provide. - * This overrides the observed state.context, if provided. - */ - context?: any; - /** - * The state machine model of the observed environment - */ - machine?: unknown; - /** - * The potential next transitions that can be taken - * in the state machine - */ - transitions?: TransitionData[]; - /** - * Past observations - */ - observations?: AgentObservation[]; // TODO - feedback?: AgentFeedback[]; - messages?: AgentMessage[]; - plans?: AgentPlan[]; -}) => string; - -export type AgentPlanner = ( - agent: T, - input: AgentPlanInput -) => Promise | undefined>; - -export type AgentDecideOptions = { - goal: string; - model?: LanguageModel; - state: ObservedState; - machine?: AnyStateMachine; - execute?: (event: AnyEventObject) => Promise; - planner?: AgentPlanner; - events?: ZodEventMapping; -} & Omit[0], 'model' | 'tools' | 'prompt'>; - -export interface AgentFeedback { - goal?: string; - observationId?: string; - /** - * The message correlation that the feedback is relevant for - */ - correlationId?: string; - attributes: Record; - reward: number; - timestamp: number; - sessionId: string; +export type StandardSchemaResult = + | { value: T; issues?: undefined } + | { value?: undefined; issues: ReadonlyArray<{ message: string }> }; + +export type InferOutput = T extends StandardSchemaV1 ? O : never; + +// ─── Adapter ─── + +export interface AgentAdapter { + decide: (options: { + model: string; + prompt: string; + options: Record; + reasoning?: boolean; + }) => Promise<{ + choice: string; + data: Record; + reasoning?: string; + }>; } -export interface AgentFeedbackInput { - goal?: string; - observationId?: string; - correlationId?: string; - attributes?: Record; - timestamp?: number; - reward?: number; +// ─── Events ─── + +export interface AgentEvent { + type: string; + [key: string]: unknown; } -export type AgentMessage = CoreMessage & { - timestamp: number; - id: string; - /** - * The response ID of the message, which references - * which message this message is responding to, if any. - */ - responseId?: string; - result?: GenerateTextResult; - sessionId: string; -}; - -type JSONObject = { - [key: string]: JSONValue; -}; -type JSONArray = JSONValue[]; -type JSONValue = null | string | number | boolean | JSONObject | JSONArray; - -type LanguageModelV1ProviderMetadata = Record< - string, - Record ->; - -interface LanguageModelV1ImagePart { - type: 'image'; - /** -Image data as a Uint8Array (e.g. from a Blob or Buffer) or a URL. - */ - image: Uint8Array | URL; - /** -Optional mime type of the image. - */ - mimeType?: string; - /** - * Additional provider-specific metadata. They are passed through - * to the provider from the AI SDK and enable provider-specific - * functionality that can be fully encapsulated in the provider. - */ - providerMetadata?: LanguageModelV1ProviderMetadata; +// ─── Transition ─── + +export interface TransitionResult { + target?: string; + context?: Record; + params?: Record; } -export interface LanguageModelV1TextPart { - type: 'text'; - /** -The text content. - */ - text: string; - /** - * Additional provider-specific metadata. They are passed through - * to the provider from the AI SDK and enable provider-specific - * functionality that can be fully encapsulated in the provider. - */ - providerMetadata?: LanguageModelV1ProviderMetadata; +export interface TransitionArgs< + TContext = Record, + TEvent extends AgentEvent = AgentEvent, +> { + context: TContext; + event: TEvent; } -export interface LanguageModelV1ToolCallPart { - type: 'tool-call'; - /** -ID of the tool call. This ID is used to match the tool call with the tool result. - */ - toolCallId: string; - /** -Name of the tool that is being called. - */ - toolName: string; - /** -Arguments of the tool call. This is a JSON-serializable object that matches the tool's input schema. - */ - args: unknown; - /** - * Additional provider-specific metadata. They are passed through - * to the provider from the AI SDK and enable provider-specific - * functionality that can be fully encapsulated in the provider. - */ - providerMetadata?: LanguageModelV1ProviderMetadata; +export interface OnDoneArgs< + TContext = Record, + TResult = unknown, +> { + result: TResult; + context: TContext; } -interface LanguageModelV1ToolResultPart { - type: 'tool-result'; - /** -ID of the tool call that this result is associated with. - */ - toolCallId: string; - /** -Name of the tool that generated this result. - */ - toolName: string; - /** -Result of the tool call. This is a JSON-serializable object. - */ - result: unknown; - /** -Optional flag if the result is an error or an error message. - */ - isError?: boolean; - /** - * Additional provider-specific metadata. They are passed through - * to the provider from the AI SDK and enable provider-specific - * functionality that can be fully encapsulated in the provider. - */ - providerMetadata?: LanguageModelV1ProviderMetadata; + +export interface RunArgs> { + context: TContext; + parentParams: Record; + signal?: AbortSignal; } -type LanguageModelV1Message = ( - | { - role: 'system'; - content: string; - } - | { - role: 'user'; - content: Array; - } - | { - role: 'assistant'; - content: Array; - } - | { - role: 'tool'; - content: Array; - } -) & { - /** - * Additional provider-specific metadata. They are passed through - * to the provider from the AI SDK and enable provider-specific - * functionality that can be fully encapsulated in the provider. - */ - providerMetadata?: LanguageModelV1ProviderMetadata; -}; - -export type AgentMessageInput = CoreMessage & { - timestamp?: number; - id?: string; - /** - * The response ID of the message, which references - * which message this message is responding to, if any. - */ - responseId?: string; - correlationId?: string; - parentCorrelationId?: string; - result?: GenerateTextResult; -}; - -export interface AgentObservation { - id: string; - prevState: SnapshotFrom | undefined; - event: EventFrom; - state: SnapshotFrom; - machineHash: string | undefined; - sessionId: string; - timestamp: number; + +export interface OutputArgs> { + context: TContext; } -export interface AgentObservationInput { - id?: string; - prevState: ObservedState | undefined; - event: AnyEventObject; - state: ObservedState; - machine?: AnyStateMachine; - timestamp?: number; +// ─── Decide / Classify ─── + +export interface DecideResult { + choice: string; + data: Record; + reasoning?: string; } -export type AgentDecisionInput = { - goal: string; - model?: LanguageModel; - context?: any; -} & Omit[0], 'model' | 'tools' | 'prompt'>; +export interface ClassifyResult { + category: string; +} -export type AgentDecisionLogic = PromiseActorLogic< - AgentPlan | undefined, - AgentDecisionInput | string ->; +export interface DecideConfig { + model: string; + adapter?: AgentAdapter; + prompt: + | string + | ((args: { + context: Record; + parentParams: Record; + }) => string); + options: Record< + string, + { description: string; schema?: StandardSchemaV1 } + >; + reasoning?: boolean; + onDone: (args: OnDoneArgs, DecideResult>) => TransitionResult; + on?: Record< + string, + (args: TransitionArgs) => TransitionResult + >; +} -export type AgentEmitted = - | { - type: 'feedback'; - feedback: AgentFeedback; - } +export interface ClassifyConfig { + model: string; + adapter?: AgentAdapter; + prompt: + | string + | ((args: { + context: Record; + parentParams: Record; + }) => string); + into: Record; + examples?: Array<{ input: string; category: string }>; + onDone: ( + args: OnDoneArgs, ClassifyResult> + ) => TransitionResult; + on?: Record< + string, + (args: TransitionArgs) => TransitionResult + >; +} + +// ─── State config ─── + +export interface StateConfig { + type?: 'final'; + outputSchema?: StandardSchemaV1; + paramsSchema?: StandardSchemaV1; + run?: (args: RunArgs) => Promise; + onDone?: (args: OnDoneArgs) => TransitionResult; + on?: Record< + string, + (args: TransitionArgs) => TransitionResult + >; + events?: Record; + output?: (args: OutputArgs) => unknown; + // Compound state + initial?: + | string + | ((args: { + context: Record; + parentParams: Record; + }) => TransitionResult); + states?: Record; + + // Internal — set by decide/classify helpers + /** @internal */ + __type?: 'decide' | 'classify'; + /** @internal */ + __decideConfig?: DecideConfig; + /** @internal */ + __classifyConfig?: ClassifyConfig; +} + +// ─── Machine config ─── + +export interface MachineConfig { + id: string; + inputSchema?: StandardSchemaV1; + context: (input: any) => Record; + contextSchema?: StandardSchemaV1; + events?: Record; + adapter?: AgentAdapter; + initial: + | string + | ((args: { context: Record }) => TransitionResult); + states: Record; +} + +// ─── Agent Machine (returned by createAgentMachine) ─── + +export interface AgentMachine extends MachineConfig {} + +// ─── Agent State (serializable) ─── + +export interface AgentState { + value: string; + params: Record>; + context: Record; + status: 'running' | 'waiting' | 'done' | 'error'; + output?: unknown; + error?: unknown; +} + +// ─── Run result (discriminated union) ─── + +export type AgentRunResult = | { - type: 'observation'; - observation: AgentObservation; // TODO + status: 'done'; + state: AgentState; + output: unknown; + context: Record; } | { - type: 'message'; - message: AgentMessage; + status: 'waiting'; + state: AgentState; + value: string; + events: Record; + context: Record; } | { - type: 'plan'; - plan: AgentPlan; + status: 'error'; + state: AgentState; + error: unknown; }; -export type AgentLogic = ActorLogic< - TransitionSnapshot, - | { - type: 'agent.feedback'; - feedback: AgentFeedback; - } - | { - type: 'agent.observe'; - observation: AgentObservation; // TODO - } - | { - type: 'agent.message'; - message: AgentMessage; - } - | { - type: 'agent.plan'; - plan: AgentPlan; - }, - any, // TODO: input - any, - AgentEmitted ->; - -export type EventsFromZodEventMapping = - Values<{ - [K in keyof TEventSchemas & string]: { - type: K; - } & TypeOf; - }>; +// ─── Snapshot (for streaming) ─── -export type ContextFromZodContextMapping< - TContextSchema extends ZodContextMapping -> = { - [K in keyof TContextSchema & string]: TypeOf; -}; - -export type AnyAgent = Agent; - -export type FromAgent = T | ((agent: AnyAgent) => T | Promise); - -export type CommonTextOptions = { - prompt: FromAgent; - model?: LanguageModel; - context?: Record; - messages?: FromAgent; - template?: PromptTemplate; -}; - -export type AgentGenerateTextOptions = Omit< - GenerateTextOptions, - 'model' | 'prompt' | 'messages' -> & - CommonTextOptions; - -export type AgentStreamTextOptions = Omit< - StreamTextOptions, - 'model' | 'prompt' | 'messages' -> & - CommonTextOptions; - -export interface ObservedState { - /** - * The current state value of the state machine, e.g. - * `"loading"` or `"processing"` or `"ready"` - */ - value: StateValue; - /** - * Additional contextual data related to the current state - */ +export interface AgentSnapshot { + value: string; context: Record; + status: AgentState['status']; + params: Record>; } -export type ObservedStateFrom = Pick< - SnapshotFrom, - 'value' | 'context' ->; - -export type AgentMemoryContext = { - observations: AgentObservation[]; // TODO - messages: AgentMessage[]; - plans: AgentPlan[]; - feedback: AgentFeedback[]; -}; - -export type AgentMemory = AppendOnlyStorage; - -export interface AppendOnlyStorage> { - append( - sessionId: string, - key: K, - item: T[K][0] - ): Promise; - getAll( - sessionId: string, - key: K - ): Promise; -} +// ─── Trace ─── -export interface AgentLongTermMemory { - get( - key: K - ): Promise; - append( - key: K, - item: AgentMemoryContext[K][0] - ): Promise; - set( - key: K, - items: AgentMemoryContext[K] - ): Promise; +export interface Trace { + state: string; + event: { + type: string; + timestamp: number; + [key: string]: unknown; + }; } - -export type Compute = { [K in keyof A]: A[K] } & unknown; diff --git a/src/utils.ts b/src/utils.ts index a1ae4ac..23524b7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,72 +1,249 @@ -import { AnyMachineSnapshot, AnyStateMachine, AnyStateNode } from 'xstate'; -import hash from 'object-hash'; -import { TransitionData } from './types'; - -export function getAllTransitions(state: AnyMachineSnapshot): TransitionData[] { - const nodes = state._nodes; - const transitions = (nodes as AnyStateNode[]) - .map((node) => [...(node as AnyStateNode).transitions.values()]) - .map((nodeTransitions) => { - return nodeTransitions.map((nodeEventTransitions) => { - return nodeEventTransitions.map((transition) => { - return { - ...transition, - guard: - typeof transition.guard === 'string' - ? { type: transition.guard } - : (transition.guard as any), // TODO: fix - }; - }); - }); - }) - .flat(2); - - return transitions; +import type { + AgentMachine, + AgentState, + StandardSchemaResult, + StandardSchemaV1, + StateConfig, + TransitionResult, +} from './types.js'; + +/** + * Validate a value against a Standard Schema V1 schema. + */ +export async function validateSchema( + schema: StandardSchemaV1, + value: unknown +): Promise { + const result = await schema['~standard'].validate(value) as StandardSchemaResult; + if (result.issues) { + const messages = result.issues.map((i: { message: string }) => i.message).join(', '); + throw new Error(`Validation failed: ${messages}`); + } + return result.value as T; +} + +/** + * Resolve a StateConfig from a dot-separated state path. + */ +export function resolveStateConfig( + machine: AgentMachine, + value: string +): StateConfig { + const parts = value.split('.'); + let current: Record = machine.states; + let config: StateConfig | undefined; + + for (const part of parts) { + config = current[part]; + if (!config) { + throw new Error(`State '${part}' not found in path '${value}'`); + } + if (config.states) { + current = config.states; + } + } + + return config!; +} + +/** + * Get the parent state config for a nested state, or null for root states. + */ +export function getParentConfig( + machine: AgentMachine, + value: string +): StateConfig | null { + const parts = value.split('.'); + if (parts.length <= 1) return null; + const parentPath = parts.slice(0, -1).join('.'); + return resolveStateConfig(machine, parentPath); +} + +/** + * Get the parent's params for the current state. + */ +export function getParentParams( + state: AgentState +): Record { + const parts = state.value.split('.'); + if (parts.length <= 1) return {}; + const parentPath = parts.slice(0, -1).join('.'); + return state.params[parentPath] ?? {}; +} + +/** + * Resolve an initial transition value. + * Accepts string shorthand, object shorthand, or function. + */ +export function resolveInitial( + initial: + | string + | ((args: { + context: Record; + parentParams: Record; + }) => TransitionResult), + args: { + context: Record; + parentParams: Record; + } +): TransitionResult { + if (typeof initial === 'string') { + return { target: initial }; + } + return initial(args); +} + +/** + * Resolve a target state path. Targets are siblings of the handler's state. + * `handlerStatePath` is the dot-path of the state where the handler is defined. + */ +export function resolveTarget( + handlerStatePath: string, + target: string +): string { + const parts = handlerStatePath.split('.'); + if (parts.length <= 1) { + // Handler on a root-level state → target is root-level + return target; + } + // Handler on a nested state → target is a sibling (under same parent) + const parentParts = parts.slice(0, -1); + return [...parentParts, target].join('.'); } -export function getAllMachineTransitions( - stateNode: AnyStateNode -): TransitionData[] { - const transitions: TransitionData[] = [...stateNode.transitions.values()] - .map((nodeTransitions) => { - return nodeTransitions.map((transition) => { - return { - ...transition, - guard: - typeof transition.guard === 'string' - ? { type: transition.guard } - : (transition.guard as any), // TODO: fix - }; - }); - }) - .flat(2); - - for (const s of Object.values(stateNode.states)) { - const stateTransitions = getAllMachineTransitions(s); - transitions.push(...stateTransitions); +/** + * Apply a transition result to produce a new state. + * Handles context merging, target resolution, and compound state entry. + */ +export function applyTransition( + machine: AgentMachine, + state: AgentState, + transition: TransitionResult, + handlerStatePath: string +): AgentState { + let newState = { ...state }; + + // Merge context + if (transition.context) { + newState.context = { ...state.context, ...transition.context }; + } + + if (transition.target) { + // Resolve target relative to handler's scope + newState.value = resolveTarget(handlerStatePath, transition.target); + newState.status = 'running'; + + // Store params if provided + if (transition.params) { + newState.params = { + ...state.params, + [newState.value]: transition.params, + }; + } + + // Enter compound states recursively + newState = enterCompoundStates(machine, newState); } - return transitions; + return newState; } -export function wrapInXml(tagName: string, content: string): string { - return `<${tagName}>${content}`; +/** + * If the current state is a compound state, resolve its initial and descend. + * Repeats for nested compounds. + */ +export function enterCompoundStates( + machine: AgentMachine, + state: AgentState +): AgentState { + let current = state; + + for (;;) { + const config = resolveStateConfig(machine, current.value); + if (!config.states || !config.initial) break; + + const parentParams = current.params[current.value] ?? {}; + const init = resolveInitial(config.initial, { + context: current.context, + parentParams, + }); + + if (!init.target) break; + + const childValue = `${current.value}.${init.target}`; + current = { ...current, value: childValue }; + + if (init.context) { + current.context = { ...current.context, ...init.context }; + } + if (init.params) { + current.params = { + ...current.params, + [current.value]: init.params, + }; + } + } + + return current; } -export function randomId() { - const timestamp = Date.now().toString(36); - const random = Math.random().toString(36).substring(2, 9); - return timestamp + random; +/** + * Collect available events for a given state path. + * Walks from the current state up to root, merging event schemas. + * State-level events override root-level events. + */ +export function getAvailableEvents( + machine: AgentMachine, + value: string +): Record { + const events: Record = {}; + + // Root-level events + if (machine.events) { + Object.assign(events, machine.events); + } + + // Walk up from current state, collecting event schemas + const parts = value.split('.'); + for (let i = 0; i < parts.length; i++) { + const path = parts.slice(0, i + 1).join('.'); + const config = resolveStateConfig(machine, path); + if (config.events) { + Object.assign(events, config.events); + } + } + + // Filter to only events that have handlers on the current state or ancestors + const handledEvents = getHandledEventTypes(machine, value); + const result: Record = {}; + for (const eventType of handledEvents) { + if (events[eventType]) { + result[eventType] = events[eventType]; + } + } + + return result; } -const machineHashes: WeakMap = new WeakMap(); /** - * Returns a string hash representing only the transitions in the state machine. + * Get all event types that have handlers on the current state or any ancestor. */ -export function getMachineHash(machine: AnyStateMachine): string { - if (machineHashes.has(machine)) return machineHashes.get(machine)!; - const transitions = getAllMachineTransitions(machine.root); - const machineHash = hash(transitions); - machineHashes.set(machine, machineHash); - return machineHash; +function getHandledEventTypes( + machine: AgentMachine, + value: string +): Set { + const handled = new Set(); + const parts = value.split('.'); + + for (let i = parts.length; i >= 1; i--) { + const path = parts.slice(0, i).join('.'); + const config = resolveStateConfig(machine, path); + if (config.on) { + for (const eventType of Object.keys(config.on)) { + handled.add(eventType); + } + } + } + + return handled; } diff --git a/src/xstate/index.ts b/src/xstate/index.ts new file mode 100644 index 0000000..0bd2c66 --- /dev/null +++ b/src/xstate/index.ts @@ -0,0 +1,10 @@ +import type { AgentMachine } from '../types.js'; + +/** + * Convert an agent machine to an XState machine definition + * for visualization in the Stately Editor. + * TODO: implement + */ +export function toXStateMachine(_machine: AgentMachine): unknown { + throw new Error('toXStateMachine is not yet implemented'); +} diff --git a/tsconfig.json b/tsconfig.json index e568eb1..68211cf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,109 +1,18 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "commonjs" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - "sourceMap": true /* Create source map files for emitted JavaScript files. */, - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - "noEmit": true /* Disable emitting files from a compilation. */, - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - "noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */, - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "noEmit": true, + "declaration": true, + "sourceMap": true, + "isolatedModules": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "examples"] } diff --git a/tsdown.config.ts b/tsdown.config.ts new file mode 100644 index 0000000..333bb5f --- /dev/null +++ b/tsdown.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: { + index: 'src/index.ts', + 'ai-sdk': 'src/ai-sdk/index.ts', + graph: 'src/graph/index.ts', + xstate: 'src/xstate/index.ts', + }, + format: ['esm', 'cjs'], + dts: true, + clean: true, +}); From 0123b2ec7430773df678669a903c462c9ea0322b Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 3 Apr 2026 11:15:27 -0400 Subject: [PATCH 02/34] Rework --- pnpm-lock.yaml | 12 - src/agent.test.ts | 1502 ++++++++++++++++++++++++--------------------- src/classify.ts | 15 +- src/decide.ts | 14 +- src/event.ts | 107 ---- src/index.ts | 20 +- src/machine.ts | 418 ++++++++++++- src/run.ts | 51 -- src/state.ts | 50 -- src/step.ts | 167 ----- src/stream.ts | 29 - src/types.ts | 338 +++++----- src/utils.ts | 205 ++++--- 13 files changed, 1542 insertions(+), 1386 deletions(-) delete mode 100644 src/event.ts delete mode 100644 src/run.ts delete mode 100644 src/state.ts delete mode 100644 src/step.ts delete mode 100644 src/stream.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86020b6..678ed1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@xstate/graph': - specifier: ^2.0.1 - version: 2.0.1(xstate@5.26.0) ai: specifier: ^6.0.67 version: 6.0.67(zod@4.3.6) @@ -646,11 +643,6 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==, tarball: https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz} - '@xstate/graph@2.0.1': - resolution: {integrity: sha512-WWfL97yvyVISbmetqrspd6mUn13UKoHZ+/FBSU17n+YPdMrYnKaP8UDe/HjNoZAVYsR3wuQLoitTW9cxud0DIA==, tarball: https://registry.npmjs.org/@xstate/graph/-/graph-2.0.1.tgz} - peerDependencies: - xstate: ^5.18.2 - ai@6.0.67: resolution: {integrity: sha512-xBnTcByHCj3OcG6V8G1s6zvSEqK0Bdiu+IEXYcpGrve1iGFFRgcrKeZtr/WAW/7gupnSvBbDF24BEv1OOfqi1g==, tarball: https://registry.npmjs.org/ai/-/ai-6.0.67.tgz} engines: {node: '>=18'} @@ -1873,10 +1865,6 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 - '@xstate/graph@2.0.1(xstate@5.26.0)': - dependencies: - xstate: 5.26.0 - ai@6.0.67(zod@4.3.6): dependencies: '@ai-sdk/gateway': 3.0.32(zod@4.3.6) diff --git a/src/agent.test.ts b/src/agent.test.ts index 0d1d94f..ee63967 100644 --- a/src/agent.test.ts +++ b/src/agent.test.ts @@ -2,11 +2,6 @@ import { describe, expect, test, vi } from 'vitest'; import { z } from 'zod'; import { createAgentMachine, - createInitialState, - step, - run, - stream, - sendEvent, decide, classify, createAdapter, @@ -16,7 +11,11 @@ import type { AgentAdapter } from './types.js'; // ─── Test helpers ─── function mockAdapter( - responses: Array<{ choice: string; data?: Record; reasoning?: string }> + responses: Array<{ + choice: string; + data?: Record; + reasoning?: string; + }> ): AgentAdapter { let index = 0; return { @@ -32,7 +31,7 @@ function mockAdapter( }; } -// ─── Simple machine for basic tests ─── +// ─── Simple machine (no schemas — inferred from context) ─── function createSimpleMachine() { return createAgentMachine({ @@ -46,12 +45,13 @@ function createSimpleMachine() { }, }, running: { - run: async ({ context }) => { - return { value: (context.count as number) + 1 }; + invoke: async ({ context }) => { + // context.count is typed as number ✓ + return { value: context.count + 1 }; }, onDone: ({ result, context }) => ({ target: 'done', - context: { count: (result as any).value }, + context: { count: (result as { value: number }).value }, }), }, done: { @@ -62,22 +62,24 @@ function createSimpleMachine() { }); } -// ─── Machine with events for HITL ─── +// ─── HITL machine (with schemas) ─── function createHitlMachine() { return createAgentMachine({ id: 'hitl', - inputSchema: z.object({ task: z.string() }), - context: (input) => ({ + schemas: { + input: z.object({ task: z.string() }), + events: { + 'user.message': z.object({ message: z.string() }), + 'user.approve': z.object({}), + 'user.cancel': z.object({}), + }, + }, + context: (input: { task: string }) => ({ task: input.task, messages: [] as Array<{ role: string; content: string }>, result: null as string | null, }), - events: { - 'user.message': z.object({ message: z.string() }), - 'user.approve': z.object({}), - 'user.cancel': z.object({}), - }, initial: 'gathering', states: { gathering: { @@ -85,25 +87,25 @@ function createHitlMachine() { 'user.message': ({ event, context }) => ({ context: { messages: [ - ...(context.messages as any[]), - { role: 'user', content: (event as any).message }, + ...context.messages, + { role: 'user', content: (event as { message: string }).message }, ], }, }), - 'user.approve': ({ context }) => ({ - target: 'processing', - }), + 'user.approve': () => ({ target: 'processing' }), 'user.cancel': () => ({ target: 'cancelled' }), }, }, processing: { - run: async ({ context }) => { - const msgs = context.messages as Array<{ content: string }>; - return { output: `Processed: ${msgs.map((m) => m.content).join(', ')}` }; + invoke: async ({ context }) => { + // context.messages is typed ✓ + return { + output: `Processed: ${context.messages.map((m) => m.content).join(', ')}`, + }; }, onDone: ({ result }) => ({ target: 'reviewing', - context: { result: (result as any).output }, + context: { result: (result as { output: string }).output }, }), }, reviewing: { @@ -113,8 +115,8 @@ function createHitlMachine() { target: 'processing', context: { messages: [ - ...(context.messages as any[]), - { role: 'user', content: (event as any).message }, + ...context.messages, + { role: 'user', content: (event as { message: string }).message }, ], }, }), @@ -133,7 +135,7 @@ function createHitlMachine() { }); } -// ─── Machine with decide state ─── +// ─── Decide machine ─── function createDecideMachine(adapter: AgentAdapter) { return createAgentMachine({ @@ -141,6 +143,7 @@ function createDecideMachine(adapter: AgentAdapter) { context: () => ({ issue: 'App crashes on login', category: null as string | null, + resolution: null as string | null, }), adapter, initial: 'classifying', @@ -156,15 +159,17 @@ function createDecideMachine(adapter: AgentAdapter) { onDone: ({ result }) => ({ target: 'handling', context: { category: result.choice }, + params: { category: result.choice }, }), }), handling: { - run: async ({ context }) => ({ - resolution: `Handled ${context.category} issue`, + paramsSchema: z.object({ category: z.string() }), + invoke: async ({ context, params }) => ({ + resolution: `Handled ${params.category} issue`, }), onDone: ({ result }) => ({ target: 'done', - context: { resolution: (result as any).resolution }, + context: { resolution: (result as { resolution: string }).resolution }, }), }, done: { @@ -178,12 +183,15 @@ function createDecideMachine(adapter: AgentAdapter) { }); } -// ─── Machine with classify state ─── +// ─── Classify machine ─── function createClassifyMachine(adapter: AgentAdapter) { return createAgentMachine({ id: 'classifier', - context: () => ({ issue: 'I want my money back', category: null as string | null }), + context: () => ({ + issue: 'I want my money back', + category: null as string | null, + }), adapter, initial: 'classifyIntent', states: { @@ -208,7 +216,7 @@ function createClassifyMachine(adapter: AgentAdapter) { }); } -// ─── Nested/compound state machine ─── +// ─── Nested machine ─── function createNestedMachine() { return createAgentMachine({ @@ -221,51 +229,53 @@ function createNestedMachine() { states: { handling: { initial: ({ context }) => { - if (context.category === 'billing') { + if (context.category === 'billing') return { target: 'checkEligibility' }; - } return { target: 'diagnose' }; }, states: { checkEligibility: { - run: async () => ({ eligible: true }), + invoke: async () => ({ eligible: true }), onDone: ({ result }) => { - if ((result as any).eligible) return { target: 'processRefund' }; + if ((result as { eligible: boolean }).eligible) + return { target: 'processRefund' }; return { target: 'deny' }; }, }, processRefund: { - run: async () => ({}), - onDone: ({ context }) => ({ + invoke: async () => ({}), + onDone: () => ({ target: 'childDone', context: { resolution: 'Refund processed' }, }), }, deny: { - run: async () => ({ message: 'Not eligible' }), + invoke: async () => ({ message: 'Not eligible' }), onDone: ({ result }) => ({ target: 'childDone', - context: { resolution: (result as any).message }, + context: { + resolution: (result as { message: string }).message, + }, }), }, diagnose: { - run: async () => ({ diagnosis: 'It is a bug' }), + invoke: async () => ({ diagnosis: 'It is a bug' }), onDone: ({ result }) => ({ target: 'childDone', - context: { resolution: (result as any).diagnosis }, + context: { + resolution: (result as { diagnosis: string }).diagnosis, + }, }), }, childDone: { type: 'final' }, }, - onDone: () => ({ - target: 'respond', - }), + onDone: () => ({ target: 'respond' }), on: { 'user.cancel': () => ({ target: 'cancelled' }), }, }, respond: { - run: async ({ context }) => ({ message: context.resolution }), + invoke: async ({ context }) => ({ message: context.resolution }), onDone: () => ({ target: 'done' }), }, done: { @@ -285,133 +295,110 @@ function createNestedMachine() { // ═══════════════════════════════════════ describe('createAgentMachine', () => { - test('creates a machine config', () => { + test('returns machine with typed methods', () => { const machine = createSimpleMachine(); expect(machine.id).toBe('simple'); - expect(machine.states).toBeDefined(); - expect(machine.states.idle).toBeDefined(); - expect(machine.states.running).toBeDefined(); - expect(machine.states.done).toBeDefined(); + expect(typeof machine.getInitialState).toBe('function'); + expect(typeof machine.transition).toBe('function'); + expect(typeof machine.invoke).toBe('function'); + expect(typeof machine.execute).toBe('function'); + expect(typeof machine.stream).toBe('function'); + expect(typeof machine.resolveState).toBe('function'); }); }); -describe('createInitialState', () => { - test('creates initial state with context', async () => { +describe('getInitialState', () => { + test('creates initial state (sync)', () => { const machine = createSimpleMachine(); - const state = await createInitialState(machine, undefined); + const state = machine.getInitialState(); expect(state.value).toBe('idle'); expect(state.context).toEqual({ count: 0 }); - expect(state.status).toBe('running'); - expect(state.params).toEqual({}); + expect(state.status).toBe('active'); }); - test('validates input against schema', async () => { + test('validates input via schemas.input (sync)', () => { const machine = createHitlMachine(); - const state = await createInitialState(machine, { task: 'test task' }); + const state = machine.getInitialState({ task: 'test task' }); expect(state.context.task).toBe('test task'); - expect(state.value).toBe('gathering'); }); - test('rejects invalid input', async () => { + test('rejects invalid input', () => { const machine = createHitlMachine(); - await expect(createInitialState(machine, { task: 123 })).rejects.toThrow(); + // @ts-expect-error — deliberately invalid input for runtime test + expect(() => machine.getInitialState({ task: 123 })).toThrow(); }); - test('resolves string initial', async () => { + test('resolves string initial', () => { const machine = createSimpleMachine(); - const state = await createInitialState(machine, undefined); - expect(state.value).toBe('idle'); + expect(machine.getInitialState().value).toBe('idle'); }); - test('resolves function initial', async () => { + test('resolves function initial', () => { const machine = createAgentMachine({ id: 'fn-initial', - context: (input) => ({ mode: input }), + context: (input: string) => ({ mode: input }), initial: ({ context }) => ({ - target: context.mode === 'fast' ? 'fast' : 'slow', + target: (context.mode === 'fast' ? 'fast' : 'slow') as 'fast' | 'slow', }), states: { fast: { type: 'final' }, slow: { type: 'final' }, }, }); - const state = await createInitialState(machine, 'fast'); - expect(state.value).toBe('fast'); + expect(machine.getInitialState('fast').value).toBe('fast'); }); - test('resolves compound state initial', async () => { + test('resolves compound state initial', () => { const machine = createNestedMachine(); - const state = await createInitialState(machine, undefined); - // Should enter handling → checkEligibility (since category is 'billing') - expect(state.value).toBe('handling.checkEligibility'); + expect(machine.getInitialState().value).toEqual({ handling: 'checkEligibility' }); }); }); -describe('step', () => { - test('executes run and transitions via onDone', async () => { +describe('invoke', () => { + test('executes invoke and transitions via onDone', async () => { const machine = createSimpleMachine(); - let state = await createInitialState(machine, undefined); - // idle → send start event to get to 'running' - state = sendEvent(machine, state, { type: 'start' }); - expect(state.value).toBe('running'); - - state = await step(machine, state); + let state = machine.getInitialState(); + state = machine.transition(state, { type: 'start' }); + state = await machine.invoke(state); expect(state.value).toBe('done'); expect(state.context.count).toBe(1); }); - test('returns waiting for event-only states', async () => { + test('returns pending for event-only states', async () => { const machine = createHitlMachine(); - let state = await createInitialState(machine, { task: 'test' }); - state = await step(machine, state); - expect(state.status).toBe('waiting'); + const state = await machine.invoke(machine.getInitialState({ task: 'x' })); + expect(state.status).toBe('pending'); expect(state.value).toBe('gathering'); }); test('returns done for final states', async () => { const machine = createSimpleMachine(); - let state = await createInitialState(machine, undefined); - state = sendEvent(machine, state, { type: 'start' }); - state = await step(machine, state); // run → done - state = await step(machine, state); // final - expect(state.status).toBe('done'); - expect(state.output).toEqual({ result: 1 }); + let s = machine.transition(machine.getInitialState(), { type: 'start' }); + s = await machine.invoke(s); + s = await machine.invoke(s); + expect(s.status).toBe('done'); + expect(s.output).toEqual({ result: 1 }); }); - test('handles context updates in transitions', async () => { - const machine = createSimpleMachine(); - let state = await createInitialState(machine, undefined); - state = sendEvent(machine, state, { type: 'start' }); - state = await step(machine, state); - expect(state.context.count).toBe(1); + test('handles decide with adapter', async () => { + const machine = createDecideMachine( + mockAdapter([{ choice: 'technical' }]) + ); + const s = await machine.invoke(machine.getInitialState()); + expect(s.value).toBe('handling'); + expect(s.context.category).toBe('technical'); }); - test('handles decide state with adapter', async () => { - const adapter = mockAdapter([ - { choice: 'technical', data: {} }, - ]); - const machine = createDecideMachine(adapter); - let state = await createInitialState(machine, undefined); - expect(state.value).toBe('classifying'); - - state = await step(machine, state); - expect(state.value).toBe('handling'); - expect(state.context.category).toBe('technical'); + test('handles classify', async () => { + const machine = createClassifyMachine( + mockAdapter([{ choice: 'billing' }]) + ); + const s = await machine.invoke(machine.getInitialState()); + expect(s.value).toBe('done'); + expect(s.context.category).toBe('billing'); }); - test('handles classify state', async () => { - const adapter = mockAdapter([ - { choice: 'billing', data: {} }, - ]); - const machine = createClassifyMachine(adapter); - let state = await createInitialState(machine, undefined); - - state = await step(machine, state); - expect(state.value).toBe('done'); - expect(state.context.category).toBe('billing'); - }); - - test('errors without adapter on decide state', async () => { + test('errors without adapter', async () => { const machine = createAgentMachine({ id: 'no-adapter', context: () => ({}), @@ -426,76 +413,105 @@ describe('step', () => { done: { type: 'final' }, }, }); - const state = await createInitialState(machine, undefined); - const result = await step(machine, state); - expect(result.status).toBe('error'); - expect(result.error).toContain('No adapter'); + const s = await machine.invoke(machine.getInitialState()); + expect(s.status).toBe('error'); }); - test('bubbles error from run', async () => { + test('catches invoke errors', async () => { const machine = createAgentMachine({ - id: 'error-machine', + id: 'err', context: () => ({}), - initial: 'failing', + initial: 'fail', states: { - failing: { - run: async () => { + fail: { + invoke: async () => { throw new Error('boom'); }, - onDone: () => ({ target: 'done' }), + onDone: () => ({ target: 'ok' }), }, - done: { type: 'final' }, + ok: { type: 'final' }, }, }); - const state = await createInitialState(machine, undefined); - const result = await step(machine, state); - expect(result.status).toBe('error'); - expect((result.error as Error).message).toBe('boom'); + const s = await machine.invoke(machine.getInitialState()); + expect(s.status).toBe('error'); + expect((s.error as Error).message).toBe('boom'); }); - test('handles nested state entry and execution', async () => { + test('nested state entry and execution', async () => { const machine = createNestedMachine(); - let state = await createInitialState(machine, undefined); - expect(state.value).toBe('handling.checkEligibility'); + let s = machine.getInitialState(); + expect(s.value).toEqual({ handling: 'checkEligibility' }); - // Step through checkEligibility → processRefund - state = await step(machine, state); - expect(state.value).toBe('handling.processRefund'); + s = await machine.invoke(s); + expect(s.value).toEqual({ handling: 'processRefund' }); - // Step through processRefund → childDone - state = await step(machine, state); - expect(state.value).toBe('handling.childDone'); - expect(state.context.resolution).toBe('Refund processed'); + s = await machine.invoke(s); + expect(s.value).toEqual({ handling: 'childDone' }); - // Step: childDone is final → parent onDone → respond - state = await step(machine, state); - expect(state.value).toBe('respond'); + s = await machine.invoke(s); + expect(s.value).toBe('respond'); }); }); -describe('run', () => { - test('runs until completion', async () => { +describe('transition', () => { + test('transitions on matching event', () => { const machine = createSimpleMachine(); - let state = await createInitialState(machine, undefined); - state = sendEvent(machine, state, { type: 'start' }); - - const result = await run(machine, state); - expect(result.status).toBe('done'); - if (result.status === 'done') { - expect(result.output).toEqual({ result: 1 }); - expect(result.context.count).toBe(1); - } + const s = machine.transition(machine.getInitialState(), { type: 'start' }); + expect(s.value).toBe('running'); + expect(s.status).toBe('active'); + }); + + test('self-transition (no target)', async () => { + const machine = createHitlMachine(); + let s = await machine.invoke(machine.getInitialState({ task: 'x' })); + s = machine.transition(s, { type: 'user.message', message: 'hello' }); + expect(s.value).toBe('gathering'); + expect(s.context.messages[0]!.content).toBe('hello'); }); - test('stops at waiting state', async () => { + test('accumulates context', async () => { const machine = createHitlMachine(); - const state = await createInitialState(machine, { task: 'test' }); + let s = await machine.invoke(machine.getInitialState({ task: 'x' })); + s = machine.transition(s, { type: 'user.message', message: 'one' }); + s = machine.transition(s, { type: 'user.message', message: 'two' }); + expect(s.context.messages.length).toBe(2); + }); + + test('throws on unknown event', () => { + const machine = createSimpleMachine(); + expect(() => + machine.transition(machine.getInitialState(), { type: 'nope' }) + ).toThrow("No handler for event 'nope'"); + }); + + test('parent preempts child', () => { + const machine = createNestedMachine(); + const s = machine.transition(machine.getInitialState(), { + type: 'user.cancel', + }); + expect(s.value).toBe('cancelled'); + }); +}); - const result = await run(machine, state); - expect(result.status).toBe('waiting'); - if (result.status === 'waiting') { - expect(result.value).toBe('gathering'); - expect(result.events).toBeDefined(); +describe('execute', () => { + test('runs until done', async () => { + const machine = createSimpleMachine(); + let s = machine.transition(machine.getInitialState(), { type: 'start' }); + const r = await machine.execute(s); + expect(r.status).toBe('done'); + if (r.status === 'done') { + expect(r.output).toEqual({ result: 1 }); + expect(r.context.count).toBe(1); + } + }); + + test('stops at pending', async () => { + const machine = createHitlMachine(); + const r = await machine.execute(machine.getInitialState({ task: 'x' })); + expect(r.status).toBe('pending'); + if (r.status === 'pending') { + expect(r.value).toBe('gathering'); + expect(r.events['user.message']).toBeDefined(); } }); @@ -506,7 +522,7 @@ describe('run', () => { initial: 'fail', states: { fail: { - run: async () => { + invoke: async () => { throw new Error('nope'); }, onDone: () => ({ target: 'ok' }), @@ -514,20 +530,18 @@ describe('run', () => { ok: { type: 'final' }, }, }); - const state = await createInitialState(machine, undefined); - const result = await run(machine, state); - expect(result.status).toBe('error'); + const r = await machine.execute(machine.getInitialState()); + expect(r.status).toBe('error'); }); test('runs through multiple transitions', async () => { - const adapter = mockAdapter([{ choice: 'technical' }]); - const machine = createDecideMachine(adapter); - const state = await createInitialState(machine, undefined); - - const result = await run(machine, state); - expect(result.status).toBe('done'); - if (result.status === 'done') { - expect(result.output).toEqual({ + const machine = createDecideMachine( + mockAdapter([{ choice: 'technical' }]) + ); + const r = await machine.execute(machine.getInitialState()); + expect(r.status).toBe('done'); + if (r.status === 'done') { + expect(r.output).toEqual({ category: 'technical', resolution: 'Handled technical issue', }); @@ -536,147 +550,63 @@ describe('run', () => { test('runs nested states to completion', async () => { const machine = createNestedMachine(); - const state = await createInitialState(machine, undefined); - - const result = await run(machine, state); - expect(result.status).toBe('done'); - if (result.status === 'done') { - expect(result.output).toEqual({ resolution: 'Refund processed' }); - } + const r = await machine.execute(machine.getInitialState()); + expect(r.status === 'done' && r.output).toEqual({ + resolution: 'Refund processed', + }); }); +}); - test('waiting result includes available events', async () => { - const machine = createHitlMachine(); - const state = await createInitialState(machine, { task: 'test' }); - - const result = await run(machine, state); - expect(result.status).toBe('waiting'); - if (result.status === 'waiting') { - expect(result.events['user.message']).toBeDefined(); - expect(result.events['user.approve']).toBeDefined(); - expect(result.events['user.cancel']).toBeDefined(); +describe('stream', () => { + test('yields snapshots', async () => { + const machine = createDecideMachine( + mockAdapter([{ choice: 'technical' }]) + ); + const snaps = []; + for await (const snap of machine.stream(machine.getInitialState())) { + snaps.push(snap); } + expect(snaps.length).toBeGreaterThanOrEqual(3); + expect(snaps[0]!.value).toBe('classifying'); + expect(snaps[snaps.length - 1]!.status).toBe('done'); }); }); -describe('sendEvent', () => { - test('transitions on matching event', async () => { - const machine = createSimpleMachine(); - const state = await createInitialState(machine, undefined); - const next = sendEvent(machine, state, { type: 'start' }); - expect(next.value).toBe('running'); - expect(next.status).toBe('running'); - }); - - test('handles self-transition (no target)', async () => { +describe('resolveState', () => { + test('restores from JSON', async () => { const machine = createHitlMachine(); - let state = await createInitialState(machine, { task: 'test' }); - state = await step(machine, state); // → waiting - - const next = sendEvent(machine, state, { + const r = await machine.execute(machine.getInitialState({ task: 'x' })); + const restored = machine.resolveState(JSON.parse(JSON.stringify(r.state))); + const next = machine.transition(restored, { type: 'user.message', - message: 'hello', + message: 'restored', }); - expect(next.value).toBe('gathering'); // same state - expect((next.context.messages as any[]).length).toBe(1); - expect((next.context.messages as any[])[0].content).toBe('hello'); - }); - - test('accumulates context on repeated self-transitions', async () => { - const machine = createHitlMachine(); - let state = await createInitialState(machine, { task: 'test' }); - state = await step(machine, state); // → waiting - - state = sendEvent(machine, state, { type: 'user.message', message: 'one' }); - state = sendEvent(machine, state, { type: 'user.message', message: 'two' }); - state = sendEvent(machine, state, { type: 'user.message', message: 'three' }); - - expect((state.context.messages as any[]).length).toBe(3); + expect(next.context.messages[0]!.content).toBe('restored'); }); - test('transitions to new state with event', async () => { - const machine = createHitlMachine(); - let state = await createInitialState(machine, { task: 'test' }); - state = await step(machine, state); // → waiting at gathering - - state = sendEvent(machine, state, { type: 'user.approve' }); - expect(state.value).toBe('processing'); - expect(state.status).toBe('running'); - }); - - test('throws on unknown event', async () => { - const machine = createSimpleMachine(); - const state = await createInitialState(machine, undefined); - expect(() => - sendEvent(machine, state, { type: 'nonexistent' }) - ).toThrow("No handler for event 'nonexistent'"); - }); - - test('parent event preempts child in nested state', async () => { + test('nested round-trip', async () => { const machine = createNestedMachine(); - let state = await createInitialState(machine, undefined); - expect(state.value).toBe('handling.checkEligibility'); - - // Parent's on handler should preempt - const next = sendEvent(machine, state, { type: 'user.cancel' }); - expect(next.value).toBe('cancelled'); - }); -}); - -describe('stream', () => { - test('yields snapshots for each transition', async () => { - const adapter = mockAdapter([{ choice: 'technical' }]); - const machine = createDecideMachine(adapter); - const state = await createInitialState(machine, undefined); - - const snapshots = []; - for await (const snapshot of stream(machine, state)) { - snapshots.push(snapshot); - } - - expect(snapshots.length).toBeGreaterThanOrEqual(3); // initial + classifying→handling + handling→done + done - expect(snapshots[0]!.value).toBe('classifying'); - const last = snapshots[snapshots.length - 1]!; - expect(last.status).toBe('done'); + const s = machine.getInitialState(); + const restored = machine.resolveState(JSON.parse(JSON.stringify(s))); + expect(restored.value).toEqual({ handling: 'checkEligibility' }); + const r = await machine.execute(restored); + expect(r.status).toBe('done'); }); }); describe('decide', () => { - test('creates state config with decide type', () => { - const config = decide({ - model: 'test', - prompt: 'test prompt', - options: { - a: { description: 'Option A' }, - b: { description: 'Option B' }, - }, - onDone: ({ result }) => ({ target: result.choice }), - }); - expect(config.__type).toBe('decide'); - expect(config.__decideConfig).toBeDefined(); - expect(config.__decideConfig!.model).toBe('test'); - }); - - test('calls adapter with resolved prompt function', async () => { - const decideSpy = vi.fn().mockResolvedValue({ - choice: 'a', - data: {}, - }); - const adapter: AgentAdapter = { decide: decideSpy }; - + test('calls adapter with resolved prompt', async () => { + const spy = vi.fn().mockResolvedValue({ choice: 'a', data: {} }); const machine = createAgentMachine({ - id: 'decide-test', + id: 'dtest', context: () => ({ topic: 'cats' }), - adapter, + adapter: { decide: spy }, initial: 'choosing', states: { choosing: decide({ model: 'my-model', prompt: ({ context }) => `About ${context.topic}`, - options: { - a: { description: 'A' }, - b: { description: 'B' }, - }, + options: { a: { description: 'A' }, b: { description: 'B' } }, onDone: ({ result }) => ({ target: 'done', context: { choice: result.choice }, @@ -685,36 +615,24 @@ describe('decide', () => { done: { type: 'final' }, }, }); - - const state = await createInitialState(machine, undefined); - await step(machine, state); - - expect(decideSpy).toHaveBeenCalledWith({ - model: 'my-model', - prompt: 'About cats', - options: { - a: { description: 'A' }, - b: { description: 'B' }, - }, - reasoning: undefined, - }); + await machine.invoke(machine.getInitialState()); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ model: 'my-model', prompt: 'About cats' }) + ); }); - test('supports per-state adapter override', async () => { - const machineAdapter = mockAdapter([{ choice: 'machine' }]); - const stateAdapter = mockAdapter([{ choice: 'state' }]); - + test('per-state adapter override', async () => { const machine = createAgentMachine({ - id: 'override-test', + id: 'override', context: () => ({ choice: null as string | null }), - adapter: machineAdapter, + adapter: mockAdapter([{ choice: 'machine' }]), initial: 'choosing', states: { choosing: decide({ model: 'test', - adapter: stateAdapter, // overrides machine adapter + adapter: mockAdapter([{ choice: 'state' }]), prompt: 'pick', - options: { state: { description: 'S' }, machine: { description: 'M' } }, + options: { s: { description: 'S' }, m: { description: 'M' } }, onDone: ({ result }) => ({ target: 'done', context: { choice: result.choice }, @@ -723,151 +641,157 @@ describe('decide', () => { done: { type: 'final' }, }, }); - - const state = await createInitialState(machine, undefined); - const result = await run(machine, state); - expect(result.status).toBe('done'); - if (result.status === 'done') { - expect(result.context.choice).toBe('state'); // used state adapter, not machine - } + const r = await machine.execute(machine.getInitialState()); + expect(r.status === 'done' && r.context.choice).toBe('state'); }); - test('supports reasoning', async () => { - const adapter: AgentAdapter = { - decide: async () => ({ - choice: 'a', - data: {}, - reasoning: 'Because reasons', - }), - }; - + test('option schemas typed data', async () => { const machine = createAgentMachine({ - id: 'reasoning-test', - context: () => ({ reasoning: null as string | null }), - adapter, + id: 'data', + context: () => ({ items: null as string[] | null }), + adapter: { + decide: async () => ({ + choice: 'withData', + data: { items: ['a', 'b'] }, + }), + }, initial: 'choosing', states: { choosing: decide({ model: 'test', prompt: 'pick', - reasoning: true, - options: { a: { description: 'A' } }, + options: { + withData: { + description: 'Has data', + schema: z.object({ items: z.array(z.string()) }), + }, + withoutData: { description: 'No data' }, + }, onDone: ({ result }) => ({ target: 'done', - context: { reasoning: result.reasoning ?? null }, + context: { + items: + result.choice === 'withData' + ? (result.data as { items: string[] }).items + : null, + }, }), }), done: { type: 'final' }, }, }); + const r = await machine.execute(machine.getInitialState()); + expect(r.status === 'done' && r.context.items).toEqual(['a', 'b']); + }); +}); + +describe('type: choice', () => { + test('inline choice state with typed context', async () => { + const adapter = mockAdapter([{ choice: 'technical' }]); + const machine = createAgentMachine({ + id: 'choice-test', + context: () => ({ issue: 'App crashes', result: null as string | null }), + adapter, + initial: 'routing', + states: { + routing: { + type: 'choice', + model: 'test-model', + prompt: ({ context }) => `Route: ${context.issue}`, // context typed ✓ + options: { + billing: { description: 'Billing' }, + technical: { description: 'Technical' }, + }, + onDone: ({ result, context }) => ({ + target: 'done', + context: { result: `${result.choice}: ${context.issue}` }, + }), + }, + done: { type: 'final', output: ({ context }) => ({ result: context.result }) }, + }, + }); - const state = await createInitialState(machine, undefined); - const result = await run(machine, state); - if (result.status === 'done') { - expect(result.context.reasoning).toBe('Because reasons'); + const r = await machine.execute(machine.getInitialState()); + expect(r.status).toBe('done'); + if (r.status === 'done') { + expect(r.output).toEqual({ result: 'technical: App crashes' }); } }); - test('decide with option schemas passes data', async () => { + test('choice with event preemption', async () => { + let called = false; const adapter: AgentAdapter = { - decide: async () => ({ - choice: 'withData', - data: { items: ['a', 'b'] }, - }), + decide: async () => { + called = true; + // Slow adapter — in real use, event would preempt + return { choice: 'a', data: {} }; + }, }; - const machine = createAgentMachine({ - id: 'data-test', - context: () => ({ items: null as string[] | null }), + id: 'choice-preempt', + context: () => ({}), adapter, initial: 'choosing', states: { - choosing: decide({ + choosing: { + type: 'choice', model: 'test', prompt: 'pick', - options: { - withData: { - description: 'Has data', - schema: z.object({ items: z.array(z.string()) }), - }, - withoutData: { description: 'No data' }, + options: { a: { description: 'A' } }, + onDone: () => ({ target: 'done' }), + on: { + cancel: () => ({ target: 'cancelled' }), }, - onDone: ({ result }) => ({ - target: 'done', - context: { - items: result.choice === 'withData' ? (result.data as any).items : null, - }, - }), - }), + }, done: { type: 'final' }, + cancelled: { type: 'final' }, }, }); - const state = await createInitialState(machine, undefined); - const result = await run(machine, state); - if (result.status === 'done') { - expect(result.context.items).toEqual(['a', 'b']); - } + // Can send event to choice state (preemption) + const state = machine.getInitialState(); + const next = machine.transition(state, { type: 'cancel' }); + expect(next.value).toBe('cancelled'); }); }); describe('classify', () => { - test('creates state config with classify type', () => { - const config = classify({ - model: 'test', - prompt: 'classify this', - into: { - a: { description: 'Category A' }, - b: { description: 'Category B' }, - }, - onDone: ({ result }) => ({ target: result.category }), - }); - expect(config.__type).toBe('classify'); - expect(config.__classifyConfig).toBeDefined(); - expect(config.__decideConfig).toBeDefined(); // classify wraps decide - }); - - test('result has category field', async () => { - const adapter = mockAdapter([{ choice: 'billing' }]); - const machine = createClassifyMachine(adapter); - const state = await createInitialState(machine, undefined); - - const result = await run(machine, state); - expect(result.status).toBe('done'); - if (result.status === 'done') { - expect(result.output).toEqual({ category: 'billing' }); - } + test('result has typed category', async () => { + const machine = createClassifyMachine( + mockAdapter([{ choice: 'billing' }]) + ); + const r = await machine.execute(machine.getInitialState()); + expect(r.status === 'done' && r.output).toEqual({ category: 'billing' }); }); }); describe('nested states', () => { - test('enters compound state initial child', async () => { - const machine = createNestedMachine(); - const state = await createInitialState(machine, undefined); - expect(state.value).toBe('handling.checkEligibility'); - }); - - test('conditional compound initial based on context', async () => { + test('conditional compound initial', async () => { const machine = createAgentMachine({ - id: 'cond-nested', - context: () => ({ category: 'technical' as string }), + id: 'cond', + context: () => ({ + category: 'technical' as string, + resolution: null as string | null, + }), initial: 'handling', states: { handling: { - initial: ({ context }) => { - if (context.category === 'billing') return { target: 'billing' }; - return { target: 'technical' }; - }, + initial: ({ context }) => + context.category === 'billing' + ? { target: 'billing' } + : { target: 'technical' }, states: { billing: { - run: async () => ({ result: 'billing handled' }), + invoke: async () => ({}), onDone: () => ({ target: 'childDone' }), }, technical: { - run: async () => ({ result: 'tech handled' }), + invoke: async () => ({ result: 'tech handled' }), onDone: ({ result }) => ({ target: 'childDone', - context: { resolution: (result as any).result }, + context: { + resolution: (result as { result: string }).result, + }, }), }, childDone: { type: 'final' }, @@ -880,388 +804,558 @@ describe('nested states', () => { }, }, }); - - const state = await createInitialState(machine, undefined); - expect(state.value).toBe('handling.technical'); - - const result = await run(machine, state); - expect(result.status).toBe('done'); - if (result.status === 'done') { - expect(result.output).toEqual({ resolution: 'tech handled' }); - } + expect(machine.getInitialState().value).toEqual({ handling: 'technical' }); + const r = await machine.execute(machine.getInitialState()); + expect(r.status === 'done' && r.output).toEqual({ + resolution: 'tech handled', + }); }); - test('parent onDone fires when child reaches final', async () => { + test('parent preempts → cancel', async () => { const machine = createNestedMachine(); - const state = await createInitialState(machine, undefined); - - const result = await run(machine, state); - expect(result.status).toBe('done'); - if (result.status === 'done') { - // The chain: checkEligibility → processRefund → childDone → (parent onDone) → respond → done - expect(result.output).toEqual({ resolution: 'Refund processed' }); - } + let s = machine.transition(machine.getInitialState(), { + type: 'user.cancel', + }); + const r = await machine.execute(s); + expect(r.status === 'done' && r.output).toEqual({ cancelled: true }); }); +}); - test('parent event handler preempts children', async () => { - const machine = createNestedMachine(); - const state = await createInitialState(machine, undefined); - expect(state.value).toBe('handling.checkEligibility'); - - const next = sendEvent(machine, state, { type: 'user.cancel' }); - expect(next.value).toBe('cancelled'); - expect(next.status).toBe('running'); +describe('P1: nested final without parent onDone', () => { + test('halts at pending', async () => { + const machine = createAgentMachine({ + id: 'p1', + context: () => ({}), + initial: 'a', + states: { + a: { + initial: 'b', + states: { + b: { + initial: 'c', + states: { c: { type: 'final' } }, + }, + }, + onDone: () => ({ target: 'result' }), + }, + result: { type: 'final' }, + }, + }); + const s = await machine.invoke(machine.getInitialState()); + expect(s.status).toBe('pending'); + expect(s.value).toEqual({ a: { b: 'c' } }); + }); - const result = await run(machine, next); - expect(result.status).toBe('done'); - if (result.status === 'done') { - expect(result.output).toEqual({ cancelled: true }); - } + test('ancestor on still reachable', async () => { + const machine = createAgentMachine({ + id: 'p1-esc', + context: () => ({}), + initial: 'a', + states: { + a: { + initial: 'b', + states: { + b: { + initial: 'c', + states: { c: { type: 'final' } }, + }, + }, + on: { escape: () => ({ target: 'out' }) }, + }, + out: { type: 'final', output: () => ({ escaped: true }) }, + }, + }); + let r = await machine.execute(machine.getInitialState()); + expect(r.status).toBe('pending'); + const s = machine.transition(r.state, { type: 'escape' }); + r = await machine.execute(s); + expect(r.status === 'done' && r.output).toEqual({ escaped: true }); }); }); -describe('full workflow: HITL', () => { - test('gather → process → review → done', async () => { +describe('P2: event validation', () => { + test('rejects invalid payload', async () => { const machine = createHitlMachine(); - - // Start - let state = await createInitialState(machine, { task: 'build feature' }); - let result = await run(machine, state); - expect(result.status).toBe('waiting'); - expect(result.status === 'waiting' && result.value).toBe('gathering'); - - // Send messages - state = sendEvent(machine, result.state, { type: 'user.message', message: 'req A' }); - state = sendEvent(machine, state, { type: 'user.message', message: 'req B' }); - - // Approve to move to processing - state = sendEvent(machine, state, { type: 'user.approve' }); - result = await run(machine, state); - expect(result.status).toBe('waiting'); - expect(result.status === 'waiting' && result.value).toBe('reviewing'); - expect(result.status === 'waiting' && result.context.result).toBe('Processed: req A, req B'); - - // Approve the review - state = sendEvent(machine, result.state, { type: 'user.approve' }); - result = await run(machine, state); - expect(result.status).toBe('done'); - if (result.status === 'done') { - expect(result.output).toEqual({ result: 'Processed: req A, req B' }); - } + const s = await machine.invoke(machine.getInitialState({ task: 'x' })); + expect(() => + // @ts-expect-error — deliberately invalid for runtime test + machine.transition(s, { type: 'user.message', message: 123 }) + ).toThrow(); }); - test('gather → process → review → reject → process → review → done', async () => { + test('accepts valid payload', async () => { const machine = createHitlMachine(); - - let state = await createInitialState(machine, { task: 'write code' }); - let result = await run(machine, state); - - // Send a message - state = sendEvent(machine, result.state, { type: 'user.message', message: 'initial' }); - state = sendEvent(machine, state, { type: 'user.approve' }); - result = await run(machine, state); - expect(result.status === 'waiting' && result.value).toBe('reviewing'); - - // Reject with feedback (sends us back to processing) - state = sendEvent(machine, result.state, { type: 'user.message', message: 'fix this' }); - result = await run(machine, state); - expect(result.status === 'waiting' && result.value).toBe('reviewing'); - expect(result.status === 'waiting' && result.context.result).toBe('Processed: initial, fix this'); - - // Approve - state = sendEvent(machine, result.state, { type: 'user.approve' }); - result = await run(machine, state); - expect(result.status).toBe('done'); + const s = await machine.invoke(machine.getInitialState({ task: 'x' })); + const next = machine.transition(s, { + type: 'user.message', + message: 'ok', + }); + expect(next.context.messages.length).toBe(1); }); - test('cancel at any point', async () => { - const machine = createHitlMachine(); - - let state = await createInitialState(machine, { task: 'test' }); - let result = await run(machine, state); - - state = sendEvent(machine, result.state, { type: 'user.cancel' }); - result = await run(machine, state); - expect(result.status).toBe('done'); - if (result.status === 'done') { - expect(result.output).toEqual({ cancelled: true }); - } + test('skips when no schema', () => { + const machine = createSimpleMachine(); + const s = machine.transition(machine.getInitialState(), { type: 'start' }); + expect(s.value).toBe('running'); }); }); -describe('serialization', () => { - test('state round-trips through JSON', async () => { +describe('full HITL workflow', () => { + test('gather → process → review → done', async () => { const machine = createHitlMachine(); - let state = await createInitialState(machine, { task: 'test' }); - let result = await run(machine, state); + let s = machine.getInitialState({ task: 'build' }); + let r = await machine.execute(s); + expect(r.status).toBe('pending'); - // Serialize → deserialize - const json = JSON.stringify(result.state); - const restored = JSON.parse(json); - - // Send event on restored state - const next = sendEvent(machine, restored, { + s = machine.transition(r.state, { type: 'user.message', - message: 'from restored', + message: 'req A', + }); + s = machine.transition(s, { type: 'user.message', message: 'req B' }); + s = machine.transition(s, { type: 'user.approve' }); + r = await machine.execute(s); + expect(r.status === 'pending' && r.context.result).toBe( + 'Processed: req A, req B' + ); + + s = machine.transition(r.state, { type: 'user.approve' }); + r = await machine.execute(s); + expect(r.status === 'done' && r.output).toEqual({ + result: 'Processed: req A, req B', }); - expect((next.context.messages as any[])[0].content).toBe('from restored'); }); - test('nested state round-trips through JSON', async () => { - const machine = createNestedMachine(); - const state = await createInitialState(machine, undefined); - - const json = JSON.stringify(state); - const restored = JSON.parse(json); - - expect(restored.value).toBe('handling.checkEligibility'); - - // Can continue execution from restored state - const result = await run(machine, restored); - expect(result.status).toBe('done'); + test('cancel', async () => { + const machine = createHitlMachine(); + let r = await machine.execute(machine.getInitialState({ task: 'x' })); + const s = machine.transition(r.state, { type: 'user.cancel' }); + r = await machine.execute(s); + expect(r.status === 'done' && r.output).toEqual({ cancelled: true }); }); }); -describe('createAdapter', () => { - test('creates a custom adapter', () => { - const adapter = createAdapter({ - decide: async () => ({ choice: 'a', data: {} }), +describe('type inference', () => { + // ─── state.value ─── + + test('state.value is typed union of state names', () => { + const machine = createAgentMachine({ + id: 't', + context: () => ({ x: 1 }), + initial: 'a', + states: { + a: { on: { go: () => ({ target: 'b' }) } }, + b: { type: 'final' }, + }, }); - expect(adapter.decide).toBeDefined(); + const s = machine.getInitialState(); + + s.value satisfies 'a' | 'b'; + // @ts-expect-error — 'c' is not a valid state name + s.value satisfies 'c'; }); -}); -describe('edge cases', () => { - test('state with run but no onDone and no on is a dead end', async () => { + test('nested state values are xstate-style objects', () => { const machine = createAgentMachine({ - id: 'dead-end', + id: 't', context: () => ({}), - initial: 'stuck', + initial: 'parent', states: { - stuck: { - run: async () => ({ done: true }), + parent: { + initial: 'child', + states: { child: { type: 'final' } }, + onDone: () => ({ target: 'done' }), }, + done: { type: 'final' }, }, }); - const state = await createInitialState(machine, undefined); - const result = await step(machine, state); - // run completes but no onDone and no on → state doesn't change - expect(result.value).toBe('stuck'); - }); + const s = machine.getInitialState(); - test('already done state returns as-is', async () => { - const machine = createSimpleMachine(); - const doneState = { - value: 'done', - params: {}, - context: { count: 1 }, - status: 'done' as const, - output: { result: 1 }, - }; - const result = await step(machine, doneState); - expect(result).toEqual(doneState); + // Runtime check — nested values are objects + expect(s.value).toEqual({ parent: 'child' }); }); - test('already errored state returns as-is', async () => { - const machine = createSimpleMachine(); - const errorState = { - value: 'running', - params: {}, - context: { count: 0 }, - status: 'error' as const, - error: 'something went wrong', - }; - const result = await step(machine, errorState); - expect(result).toEqual(errorState); + // ─── state.context ─── + + test('context typed from context() return', () => { + const machine = createAgentMachine({ + id: 't', + context: () => ({ name: 'test', count: 0, flag: true }), + initial: 'idle', + states: { idle: { type: 'final' } }, + }); + const s = machine.getInitialState(); + + s.context.name satisfies string; + s.context.count satisfies number; + s.context.flag satisfies boolean; + // @ts-expect-error — name is string not number + s.context.name satisfies number; + // @ts-expect-error — 'nope' does not exist + s.context.nope; }); -}); -describe('P1: nested final state without parent onDone', () => { - test('does not mark machine as done when parent lacks onDone', async () => { - // a.b.c where c is final, b has NO onDone, a has onDone + test('context typed in on handlers', () => { const machine = createAgentMachine({ - id: 'p1-bug', - context: () => ({ resolved: false }), - initial: 'a', + id: 't', + context: () => ({ items: ['a', 'b'] }), + initial: 'idle', states: { - a: { - initial: 'b', - states: { - b: { - initial: 'c', - // NO onDone — should halt here, not mark machine done - states: { - c: { type: 'final' }, - }, + idle: { + on: { + add: ({ context }) => { + context.items satisfies string[]; + // @ts-expect-error — 'nope' does not exist + context.nope; + return { context: { items: [...context.items, 'c'] } }; }, }, - onDone: () => ({ - target: 'result', - context: { resolved: true }, + }, + }, + }); + const next = machine.transition(machine.getInitialState(), { type: 'add' }); + expect(next.context.items).toEqual(['a', 'b', 'c']); + }); + + test('context typed in invoke', () => { + const machine = createAgentMachine({ + id: 't', + context: () => ({ n: 42 }), + initial: 'work', + states: { + work: { + invoke: async ({ context }) => { + context.n satisfies number; + // @ts-expect-error — 'nope' does not exist + context.nope; + return { doubled: context.n * 2 }; + }, + onDone: ({ result }) => ({ + target: 'done', + context: { n: (result as { doubled: number }).doubled }, }), }, - result: { + done: { type: 'final' }, + }, + }); + return machine.execute(machine.getInitialState()).then((r) => { + expect(r.status === 'done' && r.context.n).toBe(84); + }); + }); + + test('context typed in output', () => { + const machine = createAgentMachine({ + id: 't', + context: () => ({ score: 100 }), + initial: 'done', + states: { + done: { type: 'final', - output: ({ context }) => ({ resolved: context.resolved }), + output: ({ context }) => { + context.score satisfies number; + // @ts-expect-error — 'nope' does not exist + context.nope; + return { score: context.score }; + }, }, }, }); + expect(machine.getInitialState).toBeDefined(); + }); - const state = await createInitialState(machine, undefined); - expect(state.value).toBe('a.b.c'); - - // Step: c is final, parent b has no onDone → should wait, NOT done - const next = await step(machine, state); - expect(next.status).toBe('waiting'); - expect(next.value).toBe('a.b.c'); // stays put + test('context typed in initial function', () => { + const machine = createAgentMachine({ + id: 't', + context: () => ({ mode: 'fast' as 'fast' | 'slow' }), + initial: ({ context }) => { + context.mode satisfies 'fast' | 'slow'; + // @ts-expect-error — 'nope' does not exist + context.nope; + return { target: (context.mode === 'fast' ? 'a' : 'b') as 'a' | 'b' }; + }, + states: { + a: { type: 'final' }, + b: { type: 'final' }, + }, + }); + expect(machine.getInitialState().value).toBe('a'); }); - test('correctly bubbles when parent has onDone', async () => { - // Same structure but b HAS onDone → bDone(final) → a.onDone → result + // ─── schemas.context (overload 1) ─── + + test('schemas.context drives TContext + input typed from schemas.input', () => { const machine = createAgentMachine({ - id: 'p1-fixed', - context: () => ({}), - initial: 'a', + id: 't', + schemas: { + context: z.object({ count: z.number(), label: z.string() }), + input: z.object({ initial: z.number() }), + }, + context: (input) => { + input.initial satisfies number; + // @ts-expect-error — 'nope' does not exist on input + input.nope; + return { count: input.initial, label: 'hello' }; + }, + initial: 'idle', states: { - a: { - initial: 'b', - states: { - b: { - initial: 'c', - states: { - c: { type: 'final' }, - }, - onDone: () => ({ target: 'bDone' }), - }, - bDone: { type: 'final' }, + idle: { + invoke: async ({ context }) => { + context.count satisfies number; + context.label satisfies string; + // @ts-expect-error — 'nope' does not exist + context.nope; + return {}; }, - onDone: () => ({ target: 'result' }), - }, - result: { - type: 'final', - output: () => ({ ok: true }), }, }, }); + const s = machine.getInitialState({ initial: 5 }); - const state = await createInitialState(machine, undefined); - const result = await run(machine, state); - expect(result.status).toBe('done'); - if (result.status === 'done') { - expect(result.output).toEqual({ ok: true }); - } + s.context.count satisfies number; + s.context.label satisfies string; + // @ts-expect-error — 'nope' does not exist + s.context.nope; + expect(s.context.count).toBe(5); }); - test('ancestor on handlers still work when halted at final child', async () => { + // ─── schemas.events ─── + + test('transition events typed from schemas.events', () => { const machine = createAgentMachine({ - id: 'p1-escape', - context: () => ({}), - initial: 'a', + id: 't', + schemas: { + events: { + greet: z.object({ name: z.string() }), + ping: z.object({}), + }, + }, + context: () => ({ msg: '' }), + initial: 'idle', states: { - a: { - initial: 'b', - states: { - b: { - initial: 'c', - states: { c: { type: 'final' } }, - // no onDone - }, - }, + idle: { on: { - escape: () => ({ target: 'escaped' }), + greet: ({ event }) => ({ + context: { msg: `hi ${(event as { name: string }).name}` }, + }), + ping: () => ({}), }, }, - escaped: { - type: 'final', - output: () => ({ escaped: true }), - }, }, }); + const s = machine.getInitialState(); - const state = await createInitialState(machine, undefined); - let result = await run(machine, state); - expect(result.status).toBe('waiting'); // halted at a.b.c - - // Ancestor on handler should still be reachable - const next = sendEvent(machine, result.state, { type: 'escape' }); - expect(next.value).toBe('escaped'); - result = await run(machine, next); - expect(result.status).toBe('done'); - if (result.status === 'done') { - expect(result.output).toEqual({ escaped: true }); - } - }); -}); + // Valid events compile + machine.transition(s, { type: 'greet', name: 'world' }); + machine.transition(s, { type: 'ping' }); -describe('P2: event payload validation', () => { - test('rejects event with invalid payload', async () => { - const machine = createHitlMachine(); - let state = await createInitialState(machine, { task: 'test' }); - state = await step(machine, state); // → waiting + // @ts-expect-error — 'bogus' is not a valid event type + expect(() => machine.transition(s, { type: 'bogus' })).toThrow(); + + // @ts-expect-error — missing required 'name' field + expect(() => machine.transition(s, { type: 'greet' })).toThrow(); - // user.message schema requires { message: string } - // Sending wrong type should throw expect(() => - sendEvent(machine, state, { type: 'user.message', message: 123 as any }) + machine.transition(s, { + type: 'greet', + // @ts-expect-error — name must be string + name: 123, + }) ).toThrow(); - }); - test('accepts event with valid payload', async () => { - const machine = createHitlMachine(); - let state = await createInitialState(machine, { task: 'test' }); - state = await step(machine, state); + const next = machine.transition(s, { type: 'greet', name: 'world' }); + expect(next.context.msg).toBe('hi world'); + }); - // Should not throw - const next = sendEvent(machine, state, { - type: 'user.message', - message: 'valid string', + test('no schemas.events → untyped events (any type string)', () => { + const machine = createAgentMachine({ + id: 't', + context: () => ({}), + initial: 'idle', + states: { + idle: { on: { anything: () => ({}) } }, + }, }); - expect((next.context.messages as any[]).length).toBe(1); + // Any event type string accepted when no schemas.events + machine.transition(machine.getInitialState(), { type: 'anything' }); + // Unknown events still throw at runtime (no handler) + expect(() => + machine.transition(machine.getInitialState(), { type: 'nope' }) + ).toThrow(); }); - test('skips validation when no schema declared', async () => { - const machine = createSimpleMachine(); - const state = await createInitialState(machine, undefined); - - // 'start' event has no schema — should not throw - const next = sendEvent(machine, state, { type: 'start' }); - expect(next.value).toBe('running'); - }); + // ─── paramsSchema per state ─── - test('state-level schema overrides root-level', async () => { + test('params typed per state from paramsSchema', async () => { const machine = createAgentMachine({ - id: 'schema-override', - context: () => ({ val: '' }), - events: { - act: z.object({ type: z.literal('act'), val: z.string() }), - }, + id: 't', + context: () => ({ result: '' }), initial: 'a', states: { a: { - events: { - // Override: requires val to be a number - act: z.object({ type: z.literal('act'), val: z.number() }), + paramsSchema: z.object({ count: z.number() }), + invoke: async ({ params }) => { + params.count satisfies number; + // @ts-expect-error — count is number not string + params.count satisfies string; + // @ts-expect-error — 'name' not on a's params + params.name; + return { doubled: params.count * 2 }; }, - on: { - act: ({ event }) => ({ - target: 'b', - context: { val: String((event as any).val) }, - }), + onDone: ({ result }) => ({ + target: 'b', + params: { name: 'hello' }, + context: { result: String((result as { doubled: number }).doubled) }, + }), + }, + b: { + paramsSchema: z.object({ name: z.string() }), + invoke: async ({ params }) => { + params.name satisfies string; + // @ts-expect-error — name is string not number + params.name satisfies number; + // @ts-expect-error — 'count' not on b's params + params.count; + return { greeting: `hi ${params.name}` }; }, + onDone: ({ result }) => ({ + target: 'done', + context: { result: (result as { greeting: string }).greeting }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ result: context.result }), }, - b: { type: 'final' }, }, }); - const state = await createInitialState(machine, undefined); + let state = machine.resolveState({ + ...machine.getInitialState(), + params: { a: { count: 21 } }, + }); + const r = await machine.execute(state); + expect(r.status === 'done' && r.output).toEqual({ result: 'hi hello' }); + }); + + test('no paramsSchema → params is Record', () => { + createAgentMachine({ + id: 't', + context: () => ({}), + initial: 'idle', + states: { + idle: { + invoke: async ({ params }) => { + params satisfies Record; + return {}; + }, + }, + }, + }); + }); + + // ─── type: 'choice' context typing ─── + + test('type: choice gets typed context in prompt and onDone', () => { + const adapter = mockAdapter([{ choice: 'a' }]); + const machine = createAgentMachine({ + id: 't', + context: () => ({ topic: 'cats', result: '' }), + adapter, + initial: 'choosing', + states: { + choosing: { + type: 'choice', + model: 'test', + prompt: ({ context }) => { + context.topic satisfies string; + // @ts-expect-error — 'nope' does not exist + context.nope; + return `About ${context.topic}`; + }, + options: { a: { description: 'A' } }, + onDone: ({ result, context }) => { + result.choice satisfies string; + context.topic satisfies string; + return { target: 'done', context: { result: result.choice } }; + }, + }, + done: { type: 'final' }, + }, + }); + expect(machine.id).toBe('t'); + }); + + // ─── getInitialState input typing ─── + + test('getInitialState requires input when schemas.input provided', () => { + const machine = createAgentMachine({ + id: 't', + schemas: { + context: z.object({ task: z.string() }), + input: z.object({ task: z.string() }), + }, + context: (input) => ({ task: input.task }), + initial: 'idle', + states: { idle: { type: 'final' } }, + }); + + // Valid + machine.getInitialState({ task: 'hello' }); - // String val should fail (state schema requires number) expect(() => - sendEvent(machine, state, { type: 'act', val: 'nope' }) + machine.getInitialState({ + // @ts-expect-error — task must be string + task: 123, + }) ).toThrow(); - // Number val should succeed - const next = sendEvent(machine, state, { type: 'act', val: 42 }); - expect(next.value).toBe('b'); + // @ts-expect-error — missing required input (runtime: validates) + expect(() => machine.getInitialState()).toThrow(); + }); + + test('getInitialState optional when no input schema', () => { + const machine = createAgentMachine({ + id: 't', + context: () => ({ x: 1 }), + initial: 'idle', + states: { idle: { type: 'final' } }, + }); + + // Both valid + machine.getInitialState(); + machine.getInitialState(undefined); + }); +}); + +describe('edge cases', () => { + test('invoke with no onDone is dead end', async () => { + const machine = createAgentMachine({ + id: 'dead', + context: () => ({}), + initial: 'stuck', + states: { stuck: { invoke: async () => ({}) } }, + }); + const s = await machine.invoke(machine.getInitialState()); + expect(s.value).toBe('stuck'); + }); + + test('done state returns as-is', async () => { + const machine = createSimpleMachine(); + const done = { + value: 'done', + params: {}, + context: { count: 1 }, + status: 'done', + output: { result: 1 }, + } as const; + expect(await machine.invoke(done)).toEqual(done); + }); +}); + +describe('createAdapter', () => { + test('creates custom adapter', () => { + const a = createAdapter({ + decide: async () => ({ choice: 'a', data: {} }), + }); + expect(a.decide).toBeDefined(); }); }); diff --git a/src/classify.ts b/src/classify.ts index 78590ba..c4bfff1 100644 --- a/src/classify.ts +++ b/src/classify.ts @@ -1,11 +1,17 @@ -import type { ClassifyConfig, StateConfig } from './types.js'; +import type { ClassifyConfig } from './types.js'; /** * Create a classification state. Sugar over `decide` for simple routing — * categories with descriptions, no per-option schemas. + * + * `result.category` is typed as a union of the `into` keys. + * + * Note: context in prompt callback is untyped. For typed context, use + * inline `type: 'choice'` instead. */ -export function classify(config: ClassifyConfig): StateConfig { - // Convert classify categories into decide options +export function classify< + const TCategories extends Record, +>(config: ClassifyConfig): any { const decideOptions: Record = {}; for (const [key, val] of Object.entries(config.into)) { decideOptions[key] = { description: val.description }; @@ -19,8 +25,7 @@ export function classify(config: ClassifyConfig): StateConfig { adapter: config.adapter, prompt: config.prompt, options: decideOptions, - onDone: ({ result, context }) => { - // Transform decide result → classify result + onDone: ({ result, context }: any) => { return config.onDone({ result: { category: result.choice }, context, diff --git a/src/decide.ts b/src/decide.ts index cdc84aa..4fa8fc6 100644 --- a/src/decide.ts +++ b/src/decide.ts @@ -1,10 +1,20 @@ -import type { DecideConfig, StateConfig } from './types.js'; +import type { DecideConfig, StandardSchemaV1 } from './types.js'; /** * Create a decision state where an LLM picks from constrained options. * Each option has a description and optional schema for structured data. + * + * The result type is a discriminated union — `result.choice` narrows `result.data`. + * + * Note: context in prompt callback is untyped. For typed context, use + * inline `type: 'choice'` instead. */ -export function decide(config: DecideConfig): StateConfig { +export function decide< + const TOptions extends Record< + string, + { description: string; schema?: StandardSchemaV1 } + >, +>(config: DecideConfig): any { return { __type: 'decide', __decideConfig: config, diff --git a/src/event.ts b/src/event.ts deleted file mode 100644 index 151167d..0000000 --- a/src/event.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { AgentEvent, AgentMachine, AgentState, StandardSchemaV1 } from './types.js'; -import { applyTransition, resolveStateConfig } from './utils.js'; - -/** - * Send a typed event to the current state. - * Validates the event payload against declared schemas, then searches from - * the current state up through ancestors for a matching handler. - * Parent handlers preempt children. - * - * Returns a new AgentState (synchronous — no async work). - */ -export function sendEvent( - machine: AgentMachine, - state: AgentState, - event: AgentEvent -): AgentState { - // Validate event payload against declared schemas - validateEventSync(machine, state.value, event); - - const parts = state.value.split('.'); - - // Walk from outermost to innermost for preemption semantics: - // parent `on` preempts children. - for (let i = 1; i <= parts.length; i++) { - const path = parts.slice(0, i).join('.'); - const config = resolveStateConfig(machine, path); - - if (config.on && config.on[event.type]) { - const handler = config.on[event.type]!; - const transition = handler({ context: state.context, event }); - - if (transition.target) { - return applyTransition(machine, state, transition, path); - } - - // Self-transition: update context, keep same state/status - return { - ...state, - context: transition.context - ? { ...state.context, ...transition.context } - : state.context, - }; - } - } - - throw new Error( - `No handler for event '${event.type}' in state '${state.value}'` - ); -} - -/** - * Validate event payload against the schema declared in state-level or - * root-level `events`. State events override root events. - * Uses synchronous validation — throws on invalid payload. - */ -function validateEventSync( - machine: AgentMachine, - value: string, - event: AgentEvent -): void { - const schema = findEventSchema(machine, value, event.type); - if (!schema) return; // no schema declared — skip validation - - const result = schema['~standard'].validate(event); - - // Handle sync result (most schema libs return sync for simple schemas) - if (result && typeof result === 'object' && 'issues' in result && result.issues) { - const messages = (result.issues as Array<{ message: string }>) - .map((i) => i.message) - .join(', '); - throw new Error( - `Invalid event '${event.type}': ${messages}` - ); - } - - // If validate returns a Promise, we can't block on it synchronously. - // For async schemas, users should validate before calling sendEvent. - if (result instanceof Promise) { - // Can't await in sync function — skip async validation. - // This is a known limitation; createInitialState handles async validation. - return; - } -} - -/** - * Find the event schema for a given event type. - * Walks from the current state up to root, with state-level schemas - * overriding root-level schemas. - */ -function findEventSchema( - machine: AgentMachine, - value: string, - eventType: string -): StandardSchemaV1 | undefined { - // Check state-level events (innermost wins for schemas) - const parts = value.split('.'); - for (let i = parts.length; i >= 1; i--) { - const path = parts.slice(0, i).join('.'); - const config = resolveStateConfig(machine, path); - if (config.events?.[eventType]) { - return config.events[eventType]; - } - } - - // Fall back to root-level events - return machine.events?.[eventType]; -} diff --git a/src/index.ts b/src/index.ts index 4e5e097..a3d1751 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,5 @@ // Core export { createAgentMachine } from './machine.js'; -export { createInitialState } from './state.js'; -export { step } from './step.js'; -export { run } from './run.js'; -export { stream } from './stream.js'; -export { sendEvent } from './event.js'; // AI primitives export { decide } from './decide.js'; @@ -16,22 +11,21 @@ export { createAdapter } from './adapter.js'; // Types export type { AgentAdapter, - AgentEvent, AgentMachine, - AgentRunResult, AgentSnapshot, AgentState, ClassifyConfig, - ClassifyResult, DecideConfig, - DecideResult, + DecideResultFor, + EventUnion, + ExecuteResult, + InferOutput, MachineConfig, - OnDoneArgs, - OutputArgs, - RunArgs, StandardSchemaV1, StateConfig, + StateValue, + StateValueOf, Trace, - TransitionArgs, + TransitionEvent, TransitionResult, } from './types.js'; diff --git a/src/machine.ts b/src/machine.ts index 2f07bbf..1027e97 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -1,9 +1,411 @@ -import type { AgentMachine, MachineConfig } from './types.js'; - -/** - * Create an agent machine definition. - * The machine is a pure configuration object — no runtime state. - */ -export function createAgentMachine(config: MachineConfig): AgentMachine { - return config; +import type { + AgentMachine, + AgentSnapshot, + AgentState, + ExecuteResult, + MachineConfig, + StandardSchemaV1, + StateConfig, + StateValue, + TransitionEvent, + TransitionResult, +} from './types.js'; +import { + applyTransition, + enterCompoundStates, + findEventSchema, + getAvailableEvents, + getParentConfig, + getParams, + pathToValue, + resolveInitial, + resolveStateConfig, + validateSchemaSync, + valueToPath, +} from './utils.js'; + +import type { InternalState } from './utils.js'; + +function toInternal(state: AgentState): InternalState { + return { ...state, value: valueToPath(state.value) }; +} + +function toExternal(state: InternalState): AgentState { + return { ...state, value: pathToValue(state.value) }; +} + +// Per-state node config with typed params via TParamsMap[K] +type StateNodeDef, TParams> = { + type?: 'final' | 'choice'; + paramsSchema?: StandardSchemaV1; + invoke?: (args: { + context: TContext; + params: NoInfer; + signal?: AbortSignal; + }) => Promise; + onDone?: (args: { result: any; context: TContext }) => TransitionResult; + on?: Record TransitionResult>; + events?: Record; + output?: (args: { context: TContext }) => unknown; + initial?: string | ((args: { context: TContext; params: Record }) => TransitionResult); + states?: Record>; + // choice-specific + model?: string; + adapter?: import('./types.js').AgentAdapter; + prompt?: string | ((args: { context: TContext; params: NoInfer }) => string); + options?: Record; + reasoning?: boolean; + // internal (from decide/classify wrappers) + __type?: 'decide' | 'classify'; + __decideConfig?: any; + __classifyConfig?: any; +}; + +// Mapped states type: each key K gets its own params from TParamsMap[K] +type StatesWithParams< + TContext extends Record, + TParamsMap extends Record, +> = { + [K in keyof TParamsMap]: StateNodeDef; +}; + +// ─── Overload 1: schemas.context drives TContext ─── +export function createAgentMachine< + TInput, + TContext extends Record, + const TEvents extends Record, + const TParamsMap extends Record, +>(config: { + id: string; + schemas: { + context: StandardSchemaV1; + input?: StandardSchemaV1; + events?: TEvents; + }; + context: (input: NoInfer) => NoInfer; + adapter?: import('./types.js').AgentAdapter; + initial: + | (keyof TParamsMap & string) + | ((args: { context: NoInfer }) => { + target: keyof TParamsMap & string; + params?: Record; + }); + states: StatesWithParams, TParamsMap>; +}): AgentMachine>; + +// ─── Overload 2: context() return drives TContext ─── +export function createAgentMachine< + TInput, + TContext extends Record, + const TEvents extends Record, + const TParamsMap extends Record, +>(config: { + id: string; + schemas?: { + input?: StandardSchemaV1; + context?: never; + events?: TEvents; + }; + context: (input: TInput) => TContext; + adapter?: import('./types.js').AgentAdapter; + initial: + | (keyof TParamsMap & string) + | ((args: { context: TContext }) => { + target: keyof TParamsMap & string; + params?: Record; + }); + states: StatesWithParams; +}): AgentMachine>; + +// ─── Implementation ─── + +export function createAgentMachine( + machineConfig: MachineConfig +): AgentMachine { + const cfg = machineConfig; + + // ─── getInitialState (sync) ─── + + function getInitialState(...args: [input?: unknown]): AgentState { + const input = args[0]; + + let validatedInput = input; + const inputSchema = cfg.schemas?.input; + if (inputSchema) { + validatedInput = validateSchemaSync(inputSchema, input); + } + + const context = cfg.context(validatedInput); + const init = resolveInitial(cfg.initial, { context, params: {} }); + + if (!init.target) { + throw new Error('Initial transition must specify a target state'); + } + + let internal: InternalState = { + value: init.target, + params: {}, + context: init.context ? { ...context, ...init.context } : context, + status: 'active', + }; + if (init.params) { + internal.params = { [init.target]: init.params }; + } + internal = enterCompoundStates(cfg, internal as any) as any; + return toExternal(internal); + } + + // ─── resolveState ─── + + function resolveState(raw: { + value: StateValue; + context: Record; + params?: Record>; + status?: AgentState['status']; + output?: unknown; + error?: unknown; + }): AgentState { + return { + value: raw.value, + context: raw.context, + status: raw.status ?? 'active', + params: raw.params ?? {}, + output: raw.output, + error: raw.error, + }; + } + + // ─── transition (sync) ─── + + function transition(state: AgentState, event: { type: string; [k: string]: unknown }): AgentState { + const internal = toInternal(state); + validateEventPayload(internal, event); + + const parts = internal.value.split('.'); + for (let i = 1; i <= parts.length; i++) { + const path = parts.slice(0, i).join('.'); + const stateConfig = resolveStateConfig(cfg, path); + + if (stateConfig.on?.[event.type]) { + const handler = stateConfig.on[event.type]!; + const result = handler({ context: internal.context, event }); + + if (result.target) { + return toExternal( + applyTransition(cfg, internal as any, result, path) as any + ); + } + + return toExternal({ + ...internal, + context: result.context + ? { ...internal.context, ...result.context } + : internal.context, + }); + } + } + + throw new Error( + `No handler for event '${event.type}' in state '${internal.value}'` + ); + } + + function validateEventPayload( + internal: InternalState, + event: { type: string; [k: string]: unknown } + ): void { + const schema = findEventSchema(cfg, internal.value, event.type); + if (!schema) return; + const result = schema['~standard'].validate(event); + if (result instanceof Promise) return; + if (result && typeof result === 'object' && 'issues' in result && result.issues) { + const messages = (result.issues as Array<{ message: string }>) + .map((i) => i.message) + .join(', '); + throw new Error(`Invalid event '${event.type}': ${messages}`); + } + } + + // ─── invoke (async, one step) ─── + + async function invoke(state: AgentState): Promise { + const internal = toInternal(state); + if (internal.status === 'done' || internal.status === 'error') { + return state; + } + const result = await invokeInternal(internal); + return toExternal(result); + } + + async function invokeInternal(state: InternalState): Promise { + const stateConfig = resolveStateConfig(cfg, state.value) as any; + + if (stateConfig.type === 'final') { + return handleFinal(state, stateConfig); + } + // type: 'choice' — inline decide config + if (stateConfig.type === 'choice') { + return handleChoice(state, stateConfig); + } + // decide()/classify() wrapper — __decideConfig set internally + if (stateConfig.__decideConfig) { + return handleDecide(state, stateConfig); + } + if (stateConfig.invoke) { + return handleInvoke(state, stateConfig); + } + if (stateConfig.on) { + return { ...state, status: 'pending' }; + } + if (stateConfig.states && stateConfig.initial) { + return { ...state, status: 'active' }; + } + return { + ...state, + status: 'error', + error: `State '${state.value}' has no invoke, events, or children`, + }; + } + + function handleFinal(state: InternalState, config: any): InternalState { + const output = config.output + ? config.output({ context: state.context }) + : undefined; + + const parts = state.value.split('.'); + if (parts.length <= 1) { + return { ...state, status: 'done', output }; + } + + const parentConfig = getParentConfig(cfg, state.value); + if (parentConfig?.onDone) { + const parentPath = parts.slice(0, -1).join('.'); + const trans = parentConfig.onDone({ result: output, context: state.context }); + return applyTransition(cfg, state as any, trans, parentPath) as any; + } + + return { ...state, status: 'pending' }; + } + + async function handleChoice(state: InternalState, sc: any): Promise { + const adapter = sc.adapter ?? cfg.adapter; + if (!adapter) { + return { ...state, status: 'error', error: `No adapter for choice state '${state.value}'` }; + } + + const params = getParams(state.value, state.params); + const prompt = typeof sc.prompt === 'function' + ? sc.prompt({ context: state.context, params }) + : sc.prompt; + + try { + const result = await adapter.decide({ + model: sc.model, + prompt, + options: sc.options, + reasoning: sc.reasoning, + }); + const trans = sc.onDone({ result, context: state.context }); + return applyTransition(cfg, state as any, trans, state.value) as any; + } catch (error) { + return { ...state, status: 'error', error }; + } + } + + async function handleDecide(state: InternalState, stateConfig: StateConfig): Promise { + const dc = (stateConfig as any).__decideConfig!; + const adapter = dc.adapter ?? cfg.adapter; + if (!adapter) { + return { ...state, status: 'error', error: `No adapter for '${state.value}'` }; + } + + const params = getParams(state.value, state.params); + const prompt = typeof dc.prompt === 'function' + ? dc.prompt({ context: state.context, params }) + : dc.prompt; + + try { + const result = await adapter.decide({ + model: dc.model, + prompt, + options: dc.options, + reasoning: dc.reasoning, + }); + const trans = dc.onDone({ result, context: state.context }); + return applyTransition(cfg, state as any, trans, state.value) as any; + } catch (error) { + return { ...state, status: 'error', error }; + } + } + + async function handleInvoke(state: InternalState, stateConfig: any): Promise { + try { + const result = await stateConfig.invoke!({ + context: state.context, + params: getParams(state.value, state.params), + }); + if (stateConfig.onDone) { + const trans = stateConfig.onDone({ result, context: state.context }); + return applyTransition(cfg, state as any, trans, state.value) as any; + } + if (stateConfig.on) { + return { ...state, status: 'pending' }; + } + return state; + } catch (error) { + return { ...state, status: 'error', error }; + } + } + + // ─── execute ─── + + async function execute(state: AgentState): Promise { + let internal = toInternal(state); + while (internal.status === 'active') { + internal = await invokeInternal(internal); + } + const ext = toExternal(internal); + + switch (internal.status) { + case 'done': + return { status: 'done', state: ext, output: internal.output, context: internal.context }; + case 'pending': + return { + status: 'pending', + state: ext, + value: ext.value, + events: getAvailableEvents(cfg, internal.value), + context: internal.context, + }; + case 'error': + return { status: 'error', state: ext, error: internal.error }; + default: + return { status: 'error', state: ext, error: `Unexpected: ${internal.status}` }; + } + } + + // ─── stream ─── + + async function* stream(state: AgentState): AsyncGenerator { + let internal = toInternal(state); + yield toSnap(internal); + while (internal.status === 'active') { + internal = await invokeInternal(internal); + yield toSnap(internal); + } + } + + function toSnap(s: InternalState): AgentSnapshot { + return { value: pathToValue(s.value), context: s.context, status: s.status, params: s.params }; + } + + return { + id: cfg.id, + getInitialState, + resolveState, + transition, + invoke, + execute, + stream, + } as any; } diff --git a/src/run.ts b/src/run.ts deleted file mode 100644 index 16590ce..0000000 --- a/src/run.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { AgentMachine, AgentRunResult, AgentState } from './types.js'; -import { step } from './step.js'; -import { getAvailableEvents, resolveStateConfig } from './utils.js'; - -/** - * Run the machine until completion, waiting, or error. - * Loops `step()` while status is 'running'. - */ -export async function run( - machine: AgentMachine, - state: AgentState -): Promise { - let current = state; - - while (current.status === 'running') { - current = await step(machine, current); - } - - switch (current.status) { - case 'done': - return { - status: 'done', - state: current, - output: current.output, - context: current.context, - }; - - case 'waiting': - return { - status: 'waiting', - state: current, - value: current.value, - events: getAvailableEvents(machine, current.value), - context: current.context, - }; - - case 'error': - return { - status: 'error', - state: current, - error: current.error, - }; - - default: - return { - status: 'error', - state: current, - error: `Unexpected status: ${current.status}`, - }; - } -} diff --git a/src/state.ts b/src/state.ts deleted file mode 100644 index b9d595b..0000000 --- a/src/state.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { AgentMachine, AgentState } from './types.js'; -import { - enterCompoundStates, - resolveInitial, - validateSchema, -} from './utils.js'; - -/** - * Create the initial serializable state for a machine + input. - * Validates input, initializes context, resolves the initial transition. - */ -export async function createInitialState( - machine: AgentMachine, - input: unknown -): Promise { - // Validate input if schema provided - let validatedInput = input; - if (machine.inputSchema) { - validatedInput = await validateSchema(machine.inputSchema, input); - } - - // Initialize context - const context = machine.context(validatedInput); - - // Resolve initial transition - const init = resolveInitial(machine.initial, { - context, - parentParams: {}, - }); - - if (!init.target) { - throw new Error('Initial transition must specify a target state'); - } - - let state: AgentState = { - value: init.target, - params: {}, - context: init.context ? { ...context, ...init.context } : context, - status: 'running', - }; - - if (init.params) { - state.params = { [init.target]: init.params }; - } - - // Enter compound states if needed - state = enterCompoundStates(machine, state); - - return state; -} diff --git a/src/step.ts b/src/step.ts deleted file mode 100644 index a873ce6..0000000 --- a/src/step.ts +++ /dev/null @@ -1,167 +0,0 @@ -import type { AgentMachine, AgentState } from './types.js'; -import { - applyTransition, - getParentConfig, - getParentParams, - resolveStateConfig, -} from './utils.js'; - -/** - * Execute one state transition. - * - * - Final state → status 'done' (or bubble to parent onDone) - * - Decide/classify → call adapter, apply onDone - * - Run state → execute run, apply onDone - * - Waiting state (on, no run) → status 'waiting' - */ -export async function step( - machine: AgentMachine, - state: AgentState -): Promise { - if (state.status === 'done' || state.status === 'error') { - return state; - } - - const config = resolveStateConfig(machine, state.value); - - // ─── Final state ─── - if (config.type === 'final') { - return handleFinalState(machine, state); - } - - // ─── Decide / Classify state ─── - if (config.__decideConfig) { - return handleDecideState(machine, state); - } - - // ─── Run state ─── - if (config.run) { - return handleRunState(machine, state); - } - - // ─── Waiting state ─── - if (config.on) { - return { ...state, status: 'waiting' }; - } - - // ─── Compound state with no run (just initial + children) ─── - // This shouldn't normally happen since enterCompoundStates resolves on entry. - // But handle defensively. - if (config.states && config.initial) { - return { ...state, status: 'running' }; - } - - return { - ...state, - status: 'error', - error: `State '${state.value}' has no run, events, or children`, - }; -} - -async function handleFinalState( - machine: AgentMachine, - state: AgentState -): Promise { - const config = resolveStateConfig(machine, state.value); - - // Compute output - const output = config.output - ? config.output({ context: state.context }) - : undefined; - - const parts = state.value.split('.'); - - // Root-level final state → done - if (parts.length <= 1) { - return { ...state, status: 'done', output }; - } - - // Nested final state — check parent for onDone - const parentConfig = getParentConfig(machine, state.value); - if (parentConfig?.onDone) { - const parentPath = parts.slice(0, -1).join('.'); - const transition = parentConfig.onDone({ - result: output, - context: state.context, - }); - return applyTransition(machine, state, transition, parentPath); - } - - // Parent has no onDone — match xstate semantics: compound state is "done" - // but no transition fires. Machine halts here; ancestor on handlers can - // still match events via sendEvent. - return { ...state, status: 'waiting' }; -} - -async function handleDecideState( - machine: AgentMachine, - state: AgentState -): Promise { - const config = resolveStateConfig(machine, state.value); - const decideConfig = config.__decideConfig!; - - // Get adapter - const adapter = decideConfig.adapter ?? machine.adapter; - if (!adapter) { - return { - ...state, - status: 'error', - error: `No adapter configured for decide state '${state.value}'`, - }; - } - - // Resolve prompt - const parentParams = getParentParams(state); - const prompt = - typeof decideConfig.prompt === 'function' - ? decideConfig.prompt({ context: state.context, parentParams }) - : decideConfig.prompt; - - try { - const result = await adapter.decide({ - model: decideConfig.model, - prompt, - options: decideConfig.options, - reasoning: decideConfig.reasoning, - }); - - // Apply onDone - const transition = decideConfig.onDone({ - result, - context: state.context, - }); - return applyTransition(machine, state, transition, state.value); - } catch (error) { - return { ...state, status: 'error', error }; - } -} - -async function handleRunState( - machine: AgentMachine, - state: AgentState -): Promise { - const config = resolveStateConfig(machine, state.value); - - try { - const result = await config.run!({ - context: state.context, - parentParams: getParentParams(state), - }); - - if (config.onDone) { - const transition = config.onDone({ - result, - context: state.context, - }); - return applyTransition(machine, state, transition, state.value); - } - - // run with no onDone — stay in state, mark waiting if has events - if (config.on) { - return { ...state, status: 'waiting' }; - } - return state; - } catch (error) { - return { ...state, status: 'error', error }; - } -} diff --git a/src/stream.ts b/src/stream.ts deleted file mode 100644 index b7261aa..0000000 --- a/src/stream.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { AgentMachine, AgentSnapshot, AgentState } from './types.js'; -import { step } from './step.js'; - -/** - * Yields a snapshot after each transition until completion, waiting, or error. - */ -export async function* stream( - machine: AgentMachine, - state: AgentState -): AsyncGenerator { - let current = state; - - // Yield initial snapshot - yield toSnapshot(current); - - while (current.status === 'running') { - current = await step(machine, current); - yield toSnapshot(current); - } -} - -function toSnapshot(state: AgentState): AgentSnapshot { - return { - value: state.value, - context: state.context, - status: state.status, - params: state.params, - }; -} diff --git a/src/types.ts b/src/types.ts index 93c5a4a..8df97c6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,4 @@ -// ─── Standard Schema compatibility ─── -// Minimal Standard Schema V1 interface so any compliant library (zod, valibot, arktype) works. +// ─── Standard Schema V1 ─── export interface StandardSchemaV1 { readonly '~standard': { @@ -16,6 +15,37 @@ export type StandardSchemaResult = export type InferOutput = T extends StandardSchemaV1 ? O : never; +// ─── State Value (xstate-style) ─── + +/** `'idle'` or `{ handling: 'check' }` or `{ a: { b: 'deep' } }` */ +export type StateValue = string | { [key: string]: StateValue }; + +/** Derive the state value union from a states config (depth-limited to 4) */ +export type StateValueOf = _SV1; +type _SV1 = T extends Record + ? { [K in keyof T & string]: T[K] extends { states: infer S extends Record } ? { [P in K]: _SV2 } : K }[keyof T & string] + : never; +type _SV2 = T extends Record + ? { [K in keyof T & string]: T[K] extends { states: infer S extends Record } ? { [P in K]: _SV3 } : K }[keyof T & string] + : never; +type _SV3 = T extends Record + ? { [K in keyof T & string]: K }[keyof T & string] + : never; + +// ─── Event Helpers ─── + +type EventPayload = T extends Record ? unknown : T; + +export type EventUnion> = { + [K in keyof T & string]: { type: K } & EventPayload>; +}[keyof T & string]; + +export type TransitionEvent< + TEvents extends Record, +> = [keyof TEvents & string] extends [never] + ? { type: string; [key: string]: unknown } + : EventUnion; + // ─── Adapter ─── export interface AgentAdapter { @@ -31,13 +61,6 @@ export interface AgentAdapter { }>; } -// ─── Events ─── - -export interface AgentEvent { - type: string; - [key: string]: unknown; -} - // ─── Transition ─── export interface TransitionResult { @@ -46,185 +69,178 @@ export interface TransitionResult { params?: Record; } -export interface TransitionArgs< - TContext = Record, - TEvent extends AgentEvent = AgentEvent, -> { - context: TContext; - event: TEvent; -} +// ─── State Config ─── -export interface OnDoneArgs< - TContext = Record, - TResult = unknown, +export interface StateConfig< + TContext extends Record = Record, > { - result: TResult; - context: TContext; + type?: 'final' | 'choice'; + paramsSchema?: StandardSchemaV1; + invoke?: (args: { + context: TContext; + params: Record; + signal?: AbortSignal; + }) => Promise; + onDone?: (args: { result: any; context: TContext }) => TransitionResult; + on?: Record TransitionResult>; + events?: Record; + output?: (args: { context: TContext }) => unknown; + initial?: + | string + | ((args: { context: TContext; params: Record }) => TransitionResult); + states?: Record>; + // choice-specific + model?: string; + adapter?: AgentAdapter; + prompt?: string | ((args: { context: TContext; params: Record }) => string); + options?: Record; + reasoning?: boolean; + /** @internal */ __type?: 'decide' | 'classify'; + /** @internal */ __decideConfig?: any; + /** @internal */ __classifyConfig?: any; } -export interface RunArgs> { - context: TContext; - parentParams: Record; - signal?: AbortSignal; -} +// ─── Agent State (POJO) ─── -export interface OutputArgs> { +export interface AgentState< + TContext extends Record = Record, + TValue extends StateValue = StateValue, +> { + value: TValue; context: TContext; + status: 'active' | 'pending' | 'done' | 'error'; + params: Record>; + output?: unknown; + error?: unknown; } -// ─── Decide / Classify ─── - -export interface DecideResult { - choice: string; - data: Record; - reasoning?: string; -} - -export interface ClassifyResult { - category: string; -} - -export interface DecideConfig { - model: string; - adapter?: AgentAdapter; - prompt: - | string - | ((args: { - context: Record; - parentParams: Record; - }) => string); - options: Record< - string, - { description: string; schema?: StandardSchemaV1 } - >; - reasoning?: boolean; - onDone: (args: OnDoneArgs, DecideResult>) => TransitionResult; - on?: Record< - string, - (args: TransitionArgs) => TransitionResult - >; -} +// ─── Execute Result ─── -export interface ClassifyConfig { - model: string; - adapter?: AgentAdapter; - prompt: - | string - | ((args: { - context: Record; - parentParams: Record; - }) => string); - into: Record; - examples?: Array<{ input: string; category: string }>; - onDone: ( - args: OnDoneArgs, ClassifyResult> - ) => TransitionResult; - on?: Record< - string, - (args: TransitionArgs) => TransitionResult - >; -} +export type ExecuteResult< + TContext extends Record = Record, + TValue extends StateValue = StateValue, + TEvents extends Record = {}, +> = + | { status: 'done'; state: AgentState; output: unknown; context: TContext } + | { status: 'pending'; state: AgentState; value: TValue; events: Record; context: TContext } + | { status: 'error'; state: AgentState; error: unknown }; -// ─── State config ─── +// ─── Snapshot ─── -export interface StateConfig { - type?: 'final'; - outputSchema?: StandardSchemaV1; - paramsSchema?: StandardSchemaV1; - run?: (args: RunArgs) => Promise; - onDone?: (args: OnDoneArgs) => TransitionResult; - on?: Record< - string, - (args: TransitionArgs) => TransitionResult - >; - events?: Record; - output?: (args: OutputArgs) => unknown; - // Compound state - initial?: - | string - | ((args: { - context: Record; - parentParams: Record; - }) => TransitionResult); - states?: Record; - - // Internal — set by decide/classify helpers - /** @internal */ - __type?: 'decide' | 'classify'; - /** @internal */ - __decideConfig?: DecideConfig; - /** @internal */ - __classifyConfig?: ClassifyConfig; +export interface AgentSnapshot< + TContext extends Record = Record, + TValue extends StateValue = StateValue, +> { + value: TValue; + context: TContext; + status: AgentState['status']; + params: Record>; } -// ─── Machine config ─── +// ─── Agent Machine ─── -export interface MachineConfig { +export interface AgentMachine< + TInput = unknown, + TContext extends Record = Record, + TEvents extends Record = {}, + TStates extends Record = Record>, +> { + readonly id: string; + + getInitialState( + ...args: unknown extends TInput ? [input?: TInput] : [input: TInput] + ): AgentState>; + + resolveState(raw: { + value: StateValue; + context: TContext; + params?: Record>; + status?: AgentState['status']; + output?: unknown; + error?: unknown; + }): AgentState>; + + transition( + state: AgentState>, + event: TransitionEvent + ): AgentState>; + + invoke( + state: AgentState> + ): Promise>>; + + execute( + state: AgentState> + ): Promise, TEvents>>; + + stream( + state: AgentState> + ): AsyncGenerator>>; +} + +// ─── Machine Config (internal) ─── + +export interface MachineConfig< + TInput = unknown, + TContext extends Record = Record, + TEvents extends Record = {}, + TStates extends Record> = Record>, +> { id: string; - inputSchema?: StandardSchemaV1; - context: (input: any) => Record; - contextSchema?: StandardSchemaV1; - events?: Record; + schemas?: { + input?: StandardSchemaV1; + context?: StandardSchemaV1; + events?: TEvents; + }; + context: (input: TInput) => TContext; adapter?: AgentAdapter; initial: - | string - | ((args: { context: Record }) => TransitionResult); - states: Record; + | (keyof TStates & string) + | ((args: { context: TContext }) => { target: keyof TStates & string; params?: Record }); + states: TStates; } -// ─── Agent Machine (returned by createAgentMachine) ─── +// ─── Decide (wrapper fn — typed result, untyped context) ─── -export interface AgentMachine extends MachineConfig {} - -// ─── Agent State (serializable) ─── +export type DecideResultFor< + TOptions extends Record, +> = { + [K in keyof TOptions & string]: { + choice: K; + data: TOptions[K] extends { schema: StandardSchemaV1 } ? O : Record; + reasoning?: string; + }; +}[keyof TOptions & string]; -export interface AgentState { - value: string; - params: Record>; - context: Record; - status: 'running' | 'waiting' | 'done' | 'error'; - output?: unknown; - error?: unknown; +export interface DecideConfig< + TOptions extends Record = Record, +> { + model: string; + adapter?: AgentAdapter; + prompt: string | ((args: { context: Record; params: Record }) => string); + options: TOptions; + reasoning?: boolean; + onDone: (args: { result: DecideResultFor; context: Record }) => TransitionResult; + on?: Record }) => TransitionResult>; } -// ─── Run result (discriminated union) ─── - -export type AgentRunResult = - | { - status: 'done'; - state: AgentState; - output: unknown; - context: Record; - } - | { - status: 'waiting'; - state: AgentState; - value: string; - events: Record; - context: Record; - } - | { - status: 'error'; - state: AgentState; - error: unknown; - }; - -// ─── Snapshot (for streaming) ─── - -export interface AgentSnapshot { - value: string; - context: Record; - status: AgentState['status']; - params: Record>; +// ─── Classify (wrapper fn — typed category, untyped context) ─── + +export interface ClassifyConfig< + TCategories extends Record = Record, +> { + model: string; + adapter?: AgentAdapter; + prompt: string | ((args: { context: Record; params: Record }) => string); + into: TCategories; + examples?: Array<{ input: string; category: keyof TCategories & string }>; + onDone: (args: { result: { category: keyof TCategories & string }; context: Record }) => TransitionResult; + on?: Record }) => TransitionResult>; } // ─── Trace ─── export interface Trace { state: string; - event: { - type: string; - timestamp: number; - [key: string]: unknown; - }; + event: { type: string; timestamp: number; [key: string]: unknown }; } diff --git a/src/utils.ts b/src/utils.ts index 23524b7..e8a38d0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,90 +1,137 @@ import type { - AgentMachine, - AgentState, + MachineConfig, StandardSchemaResult, StandardSchemaV1, StateConfig, + StateValue, TransitionResult, } from './types.js'; +/** Internal state representation with dot-path string value */ +export interface InternalState { + value: string; + context: Record; + status: 'active' | 'pending' | 'done' | 'error'; + params: Record>; + output?: unknown; + error?: unknown; +} + +// ─── StateValue ↔ dot-path conversion ─── + +/** Convert xstate-style value `{ handling: 'check' }` to dot-path `'handling.check'` */ +export function valueToPath(value: StateValue): string { + if (typeof value === 'string') return value; + const key = Object.keys(value)[0]!; + const child = (value as Record)[key]!; + return typeof child === 'string' + ? `${key}.${child}` + : `${key}.${valueToPath(child)}`; +} + +/** Convert dot-path `'handling.check'` to xstate-style value `{ handling: 'check' }` */ +export function pathToValue(path: string): StateValue { + const parts = path.split('.'); + if (parts.length === 1) return parts[0]!; + let result: StateValue = parts[parts.length - 1]!; + for (let i = parts.length - 2; i >= 0; i--) { + result = { [parts[i]!]: result }; + } + return result; +} + /** - * Validate a value against a Standard Schema V1 schema. + * Validate a value against a Standard Schema synchronously. + * Throws if validation returns a Promise (async schemas not supported here). */ -export async function validateSchema( +export function validateSchemaSync( schema: StandardSchemaV1, value: unknown -): Promise { - const result = await schema['~standard'].validate(value) as StandardSchemaResult; - if (result.issues) { - const messages = result.issues.map((i: { message: string }) => i.message).join(', '); +): T { + const result = schema['~standard'].validate(value); + if (result instanceof Promise) { + throw new Error( + 'Async schema validation is not supported in sync context. Validate input before calling getInitialState.' + ); + } + const syncResult = result as StandardSchemaResult; + if (syncResult.issues) { + const messages = syncResult.issues + .map((i: { message: string }) => i.message) + .join(', '); throw new Error(`Validation failed: ${messages}`); } - return result.value as T; + return syncResult.value as T; } /** * Resolve a StateConfig from a dot-separated state path. */ export function resolveStateConfig( - machine: AgentMachine, + config: MachineConfig, value: string -): StateConfig { +): any { const parts = value.split('.'); - let current: Record = machine.states; - let config: StateConfig | undefined; + let current: Record = config.states; + let stateConfig: any; for (const part of parts) { - config = current[part]; - if (!config) { + stateConfig = current[part]; + if (!stateConfig) { throw new Error(`State '${part}' not found in path '${value}'`); } - if (config.states) { - current = config.states; + if (stateConfig.states) { + current = stateConfig.states; } } - return config!; + return stateConfig!; } /** - * Get the parent state config for a nested state, or null for root states. + * Get the parent state config, or null for root states. */ export function getParentConfig( - machine: AgentMachine, + config: MachineConfig, value: string -): StateConfig | null { +): any { const parts = value.split('.'); if (parts.length <= 1) return null; const parentPath = parts.slice(0, -1).join('.'); - return resolveStateConfig(machine, parentPath); + return resolveStateConfig(config, parentPath); } /** - * Get the parent's params for the current state. + * Get the params for the current state. + * Params are stored at `state.params[statePath]` when transitioning. + * For nested states, also checks the parent path. */ -export function getParentParams( - state: AgentState +export function getParams( + valuePath: string, + params: Record> ): Record { - const parts = state.value.split('.'); + // Check own params first (set when transitioning TO this state) + if (params[valuePath]) return params[valuePath]!; + // Fall back to parent params (for compound state children) + const parts = valuePath.split('.'); if (parts.length <= 1) return {}; const parentPath = parts.slice(0, -1).join('.'); - return state.params[parentPath] ?? {}; + return params[parentPath] ?? {}; } /** - * Resolve an initial transition value. - * Accepts string shorthand, object shorthand, or function. + * Resolve an initial transition (string shorthand or function). */ export function resolveInitial( initial: | string | ((args: { context: Record; - parentParams: Record; + params: Record; }) => TransitionResult), args: { context: Record; - parentParams: Record; + params: Record; } ): TransitionResult { if (typeof initial === 'string') { @@ -94,46 +141,38 @@ export function resolveInitial( } /** - * Resolve a target state path. Targets are siblings of the handler's state. - * `handlerStatePath` is the dot-path of the state where the handler is defined. + * Resolve a target relative to the handler's state path. + * Targets are siblings of the state where the handler is defined. */ export function resolveTarget( handlerStatePath: string, target: string ): string { const parts = handlerStatePath.split('.'); - if (parts.length <= 1) { - // Handler on a root-level state → target is root-level - return target; - } - // Handler on a nested state → target is a sibling (under same parent) + if (parts.length <= 1) return target; const parentParts = parts.slice(0, -1); return [...parentParts, target].join('.'); } /** * Apply a transition result to produce a new state. - * Handles context merging, target resolution, and compound state entry. */ export function applyTransition( - machine: AgentMachine, - state: AgentState, + config: MachineConfig, + state: InternalState, transition: TransitionResult, handlerStatePath: string -): AgentState { +): InternalState { let newState = { ...state }; - // Merge context if (transition.context) { newState.context = { ...state.context, ...transition.context }; } if (transition.target) { - // Resolve target relative to handler's scope newState.value = resolveTarget(handlerStatePath, transition.target); - newState.status = 'running'; + newState.status = 'active'; - // Store params if provided if (transition.params) { newState.params = { ...state.params, @@ -141,8 +180,7 @@ export function applyTransition( }; } - // Enter compound states recursively - newState = enterCompoundStates(machine, newState); + newState = enterCompoundStates(config, newState); } return newState; @@ -150,22 +188,21 @@ export function applyTransition( /** * If the current state is a compound state, resolve its initial and descend. - * Repeats for nested compounds. */ export function enterCompoundStates( - machine: AgentMachine, - state: AgentState -): AgentState { + config: MachineConfig, + state: InternalState +): InternalState { let current = state; for (;;) { - const config = resolveStateConfig(machine, current.value); - if (!config.states || !config.initial) break; + const stateConfig = resolveStateConfig(config, current.value); + if (!stateConfig.states || !stateConfig.initial) break; - const parentParams = current.params[current.value] ?? {}; - const init = resolveInitial(config.initial, { + const params = current.params[current.value] ?? {}; + const init = resolveInitial(stateConfig.initial, { context: current.context, - parentParams, + params, }); if (!init.target) break; @@ -188,35 +225,32 @@ export function enterCompoundStates( } /** - * Collect available events for a given state path. - * Walks from the current state up to root, merging event schemas. + * Collect available events for a state path. * State-level events override root-level events. + * Only includes events that have handlers. */ export function getAvailableEvents( - machine: AgentMachine, + config: MachineConfig, value: string ): Record { const events: Record = {}; - // Root-level events - if (machine.events) { - Object.assign(events, machine.events); + if (config.schemas?.events) { + Object.assign(events, config.schemas.events); } - // Walk up from current state, collecting event schemas const parts = value.split('.'); for (let i = 0; i < parts.length; i++) { const path = parts.slice(0, i + 1).join('.'); - const config = resolveStateConfig(machine, path); - if (config.events) { - Object.assign(events, config.events); + const stateConfig = resolveStateConfig(config, path); + if (stateConfig.events) { + Object.assign(events, stateConfig.events); } } - // Filter to only events that have handlers on the current state or ancestors - const handledEvents = getHandledEventTypes(machine, value); + const handledTypes = getHandledEventTypes(config, value); const result: Record = {}; - for (const eventType of handledEvents) { + for (const eventType of handledTypes) { if (events[eventType]) { result[eventType] = events[eventType]; } @@ -225,11 +259,8 @@ export function getAvailableEvents( return result; } -/** - * Get all event types that have handlers on the current state or any ancestor. - */ function getHandledEventTypes( - machine: AgentMachine, + config: MachineConfig, value: string ): Set { const handled = new Set(); @@ -237,9 +268,9 @@ function getHandledEventTypes( for (let i = parts.length; i >= 1; i--) { const path = parts.slice(0, i).join('.'); - const config = resolveStateConfig(machine, path); - if (config.on) { - for (const eventType of Object.keys(config.on)) { + const stateConfig = resolveStateConfig(config, path); + if (stateConfig.on) { + for (const eventType of Object.keys(stateConfig.on)) { handled.add(eventType); } } @@ -247,3 +278,23 @@ function getHandledEventTypes( return handled; } + +/** + * Find the event schema for a given event type. + * State-level schemas override root-level. + */ +export function findEventSchema( + config: MachineConfig, + value: string, + eventType: string +): StandardSchemaV1 | undefined { + const parts = value.split('.'); + for (let i = parts.length; i >= 1; i--) { + const path = parts.slice(0, i).join('.'); + const stateConfig = resolveStateConfig(config, path); + if (stateConfig.events?.[eventType]) { + return stateConfig.events[eventType]; + } + } + return config.schemas?.events?.[eventType]; +} From 2020c013e549200f3e000114056a7c48c2bda927 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 4 Apr 2026 05:26:41 -0400 Subject: [PATCH 03/34] Types WIP --- src/agent.test.ts | 153 +++++++++++++++++++++++++++++++++++++++++----- src/index.ts | 1 + src/machine.ts | 109 +++++++++++++++++++++++++++------ src/types.ts | 4 +- 4 files changed, 230 insertions(+), 37 deletions(-) diff --git a/src/agent.test.ts b/src/agent.test.ts index ee63967..2722d05 100644 --- a/src/agent.test.ts +++ b/src/agent.test.ts @@ -45,17 +45,19 @@ function createSimpleMachine() { }, }, running: { + resultSchema: z.object({ value: z.number() }), invoke: async ({ context }) => { // context.count is typed as number ✓ return { value: context.count + 1 }; }, - onDone: ({ result, context }) => ({ + onDone: ({ result }) => ({ target: 'done', - context: { count: (result as { value: number }).value }, + context: { count: result.value }, }), }, done: { type: 'final', + // is the machine output inferred? should we have top-level outputSchema? output: ({ context }) => ({ result: context.count }), }, }, @@ -75,7 +77,7 @@ function createHitlMachine() { 'user.cancel': z.object({}), }, }, - context: (input: { task: string }) => ({ + context: (input) => ({ task: input.task, messages: [] as Array<{ role: string; content: string }>, result: null as string | null, @@ -84,19 +86,22 @@ function createHitlMachine() { states: { gathering: { on: { + // events are now typed from schemas.events 'user.message': ({ event, context }) => ({ context: { messages: [ ...context.messages, - { role: 'user', content: (event as { message: string }).message }, + { role: 'user', content: event.message }, ], }, }), - 'user.approve': () => ({ target: 'processing' }), - 'user.cancel': () => ({ target: 'cancelled' }), + // static shorthand — string target + 'user.approve': 'processing', + 'user.cancel': 'cancelled', }, }, processing: { + resultSchema: z.object({ output: z.string() }), invoke: async ({ context }) => { // context.messages is typed ✓ return { @@ -105,22 +110,23 @@ function createHitlMachine() { }, onDone: ({ result }) => ({ target: 'reviewing', - context: { result: (result as { output: string }).output }, + context: { result: result.output }, }), }, reviewing: { on: { - 'user.approve': () => ({ target: 'done' }), + // static shorthand — object target + 'user.approve': { target: 'done' }, 'user.message': ({ event, context }) => ({ target: 'processing', context: { messages: [ ...context.messages, - { role: 'user', content: (event as { message: string }).message }, + { role: 'user', content: event.message }, ], }, }), - 'user.cancel': () => ({ target: 'cancelled' }), + 'user.cancel': 'cancelled', }, }, done: { @@ -150,6 +156,7 @@ function createDecideMachine(adapter: AgentAdapter) { states: { classifying: decide({ model: 'test-model', + // context is Record here, not typed from context!! prompt: ({ context }) => `Classify: ${context.issue}`, options: { billing: { description: 'Billing issues' }, @@ -164,12 +171,13 @@ function createDecideMachine(adapter: AgentAdapter) { }), handling: { paramsSchema: z.object({ category: z.string() }), + resultSchema: z.object({ resolution: z.string() }), invoke: async ({ context, params }) => ({ resolution: `Handled ${params.category} issue`, }), onDone: ({ result }) => ({ target: 'done', - context: { resolution: (result as { resolution: string }).resolution }, + context: { resolution: result.resolution }, }), }, done: { @@ -1023,6 +1031,7 @@ describe('type inference', () => { initial: 'work', states: { work: { + resultSchema: z.object({ doubled: z.number() }), invoke: async ({ context }) => { context.n satisfies number; // @ts-expect-error — 'nope' does not exist @@ -1031,7 +1040,7 @@ describe('type inference', () => { }, onDone: ({ result }) => ({ target: 'done', - context: { n: (result as { doubled: number }).doubled }, + context: { n: result.doubled }, }), }, done: { type: 'final' }, @@ -1134,7 +1143,7 @@ describe('type inference', () => { idle: { on: { greet: ({ event }) => ({ - context: { msg: `hi ${(event as { name: string }).name}` }, + context: { msg: `hi ${event.name}` }, }), ping: () => ({}), }, @@ -1192,6 +1201,7 @@ describe('type inference', () => { states: { a: { paramsSchema: z.object({ count: z.number() }), + resultSchema: z.object({ doubled: z.number() }), invoke: async ({ params }) => { params.count satisfies number; // @ts-expect-error — count is number not string @@ -1203,11 +1213,12 @@ describe('type inference', () => { onDone: ({ result }) => ({ target: 'b', params: { name: 'hello' }, - context: { result: String((result as { doubled: number }).doubled) }, + context: { result: String(result.doubled) }, }), }, b: { paramsSchema: z.object({ name: z.string() }), + resultSchema: z.object({ greeting: z.string() }), invoke: async ({ params }) => { params.name satisfies string; // @ts-expect-error — name is string not number @@ -1218,7 +1229,7 @@ describe('type inference', () => { }, onDone: ({ result }) => ({ target: 'done', - context: { result: (result as { greeting: string }).greeting }, + context: { result: result.greeting }, }), }, done: { @@ -1324,6 +1335,118 @@ describe('type inference', () => { machine.getInitialState(); machine.getInitialState(undefined); }); + + // ─── resultSchema ─── + + test('resultSchema types invoke return and onDone result', () => { + createAgentMachine({ + id: 't', + context: () => ({ total: 0 }), + initial: 'work', + states: { + work: { + resultSchema: z.object({ value: z.number() }), + invoke: async () => { + // return type must match resultSchema + return { value: 42 }; + }, + onDone: ({ result }) => { + // result is typed from resultSchema + result.value satisfies number; + // @ts-expect-error — 'nope' does not exist on result + result.nope; + return { target: 'done', context: { total: result.value } }; + }, + }, + done: { type: 'final' }, + }, + }); + }); + + test('no resultSchema → onDone result is any', () => { + createAgentMachine({ + id: 't', + context: () => ({}), + initial: 'work', + states: { + work: { + invoke: async () => ({ anything: true }), + onDone: ({ result }) => { + // result is any when no resultSchema — no errors + result.whatever; + return { target: 'done' }; + }, + }, + done: { type: 'final' }, + }, + }); + }); + + // ─── events typed in on handlers ─── + + test('on handler event typed from schemas.events', () => { + createAgentMachine({ + id: 't', + schemas: { + events: { + 'msg': z.object({ text: z.string() }), + }, + }, + context: () => ({ last: '' }), + initial: 'idle', + states: { + idle: { + on: { + msg: ({ event }) => { + // event.text is typed from schemas.events + event.text satisfies string; + event.type satisfies 'msg'; + return { context: { last: event.text } }; + }, + }, + }, + }, + }); + }); + + // ─── static transition shorthand ─── + + test('on handler accepts string shorthand', () => { + const machine = createAgentMachine({ + id: 't', + context: () => ({}), + initial: 'a', + states: { + a: { + on: { + go: 'b', + }, + }, + b: { type: 'final' }, + }, + }); + const s = machine.transition(machine.getInitialState(), { type: 'go' }); + expect(s.value).toBe('b'); + }); + + test('on handler accepts static TransitionResult object', () => { + const machine = createAgentMachine({ + id: 't', + context: () => ({ x: 0 }), + initial: 'a', + states: { + a: { + on: { + go: { target: 'b', context: { x: 1 } }, + }, + }, + b: { type: 'final' }, + }, + }); + const s = machine.transition(machine.getInitialState(), { type: 'go' }); + expect(s.value).toBe('b'); + expect(s.context.x).toBe(1); + }); }); describe('edge cases', () => { diff --git a/src/index.ts b/src/index.ts index a3d1751..ead8991 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ export type { ClassifyConfig, DecideConfig, DecideResultFor, + EventPayload, EventUnion, ExecuteResult, InferOutput, diff --git a/src/machine.ts b/src/machine.ts index 1027e97..4fb1b86 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -2,7 +2,9 @@ import type { AgentMachine, AgentSnapshot, AgentState, + EventPayload, ExecuteResult, + InferOutput, MachineConfig, StandardSchemaV1, StateConfig, @@ -34,17 +36,48 @@ function toExternal(state: InternalState): AgentState { return { ...state, value: pathToValue(state.value) }; } -// Per-state node config with typed params via TParamsMap[K] -type StateNodeDef, TParams> = { +// Falls back to `any` when TResult was not inferred (unknown) +type FallbackAny = unknown extends T ? any : T; + +// Handler for a specific known event type +type TypedOnHandler> = + | string + | TransitionResult + | ((args: { + event: { type: E } & EventPayload>; + context: TContext; + }) => TransitionResult); + +// Handler for an unknown event type +type UntypedOnHandler> = + | string + | TransitionResult + | ((args: { event: any; context: TContext }) => TransitionResult); + +// When TEvents has keys, known events get typed handlers; others get untyped. +// When TEvents is empty (no schemas.events), all handlers are untyped. +type OnHandlers> = + [keyof TEvents] extends [never] + ? Record> + : { [E in keyof TEvents & string]?: TypedOnHandler }; + +// Per-state node config with typed params via TParamsMap[K] and typed result via TResultMap[K] +type StateNodeDef< + TContext extends Record, + TParams, + TResult, + TEvents, +> = { type?: 'final' | 'choice'; paramsSchema?: StandardSchemaV1; + resultSchema?: StandardSchemaV1; invoke?: (args: { context: TContext; params: NoInfer; signal?: AbortSignal; - }) => Promise; - onDone?: (args: { result: any; context: TContext }) => TransitionResult; - on?: Record TransitionResult>; + }) => Promise>; + onDone?: (args: { result: FallbackAny>; context: TContext }) => TransitionResult; + on?: Record TransitionResult)>; events?: Record; output?: (args: { context: TContext }) => unknown; initial?: string | ((args: { context: TContext; params: Record }) => TransitionResult); @@ -61,12 +94,14 @@ type StateNodeDef, TParams> = { __classifyConfig?: any; }; -// Mapped states type: each key K gets its own params from TParamsMap[K] -type StatesWithParams< +// Mapped states type: each key K gets its own params and result types +type StatesMap< TContext extends Record, TParamsMap extends Record, + TResultMap extends Record, + TEvents, > = { - [K in keyof TParamsMap]: StateNodeDef; + [K in keyof TParamsMap & keyof TResultMap]: StateNodeDef; }; // ─── Overload 1: schemas.context drives TContext ─── @@ -75,6 +110,7 @@ export function createAgentMachine< TContext extends Record, const TEvents extends Record, const TParamsMap extends Record, + TResultMap extends Record, >(config: { id: string; schemas: { @@ -85,37 +121,63 @@ export function createAgentMachine< context: (input: NoInfer) => NoInfer; adapter?: import('./types.js').AgentAdapter; initial: - | (keyof TParamsMap & string) + | (keyof TParamsMap & keyof TResultMap & string) | ((args: { context: NoInfer }) => { - target: keyof TParamsMap & string; + target: keyof TParamsMap & keyof TResultMap & string; + params?: Record; + }); + states: StatesMap, TParamsMap, TResultMap, TEvents>; +}): AgentMachine>; + +// ─── Overload 2: schemas.input present, context() return drives TContext ─── +export function createAgentMachine< + TInput, + TContext extends Record, + const TEvents extends Record, + const TParamsMap extends Record, + TResultMap extends Record, +>(config: { + id: string; + schemas: { + input: StandardSchemaV1; + context?: never; + events?: TEvents; + }; + context: (input: NoInfer) => TContext; + adapter?: import('./types.js').AgentAdapter; + initial: + | (keyof TParamsMap & keyof TResultMap & string) + | ((args: { context: TContext }) => { + target: keyof TParamsMap & keyof TResultMap & string; params?: Record; }); - states: StatesWithParams, TParamsMap>; -}): AgentMachine>; + states: StatesMap; +}): AgentMachine>; -// ─── Overload 2: context() return drives TContext ─── +// ─── Overload 3: no schemas.input/context — all from context() ─── export function createAgentMachine< TInput, TContext extends Record, const TEvents extends Record, const TParamsMap extends Record, + TResultMap extends Record, >(config: { id: string; schemas?: { - input?: StandardSchemaV1; + input?: never; context?: never; events?: TEvents; }; context: (input: TInput) => TContext; adapter?: import('./types.js').AgentAdapter; initial: - | (keyof TParamsMap & string) + | (keyof TParamsMap & keyof TResultMap & string) | ((args: { context: TContext }) => { - target: keyof TParamsMap & string; + target: keyof TParamsMap & keyof TResultMap & string; params?: Record; }); - states: StatesWithParams; -}): AgentMachine>; + states: StatesMap; +}): AgentMachine>; // ─── Implementation ─── @@ -186,9 +248,16 @@ export function createAgentMachine( const path = parts.slice(0, i).join('.'); const stateConfig = resolveStateConfig(cfg, path); - if (stateConfig.on?.[event.type]) { + if (stateConfig.on?.[event.type] !== undefined) { const handler = stateConfig.on[event.type]!; - const result = handler({ context: internal.context, event }); + let result: TransitionResult; + if (typeof handler === 'string') { + result = { target: handler }; + } else if (typeof handler === 'function') { + result = handler({ context: internal.context, event }); + } else { + result = handler; + } if (result.target) { return toExternal( diff --git a/src/types.ts b/src/types.ts index 8df97c6..1de6054 100644 --- a/src/types.ts +++ b/src/types.ts @@ -34,7 +34,7 @@ type _SV3 = T extends Record // ─── Event Helpers ─── -type EventPayload = T extends Record ? unknown : T; +export type EventPayload = T extends Record ? unknown : T; export type EventUnion> = { [K in keyof T & string]: { type: K } & EventPayload>; @@ -82,7 +82,7 @@ export interface StateConfig< signal?: AbortSignal; }) => Promise; onDone?: (args: { result: any; context: TContext }) => TransitionResult; - on?: Record TransitionResult>; + on?: Record TransitionResult)>; events?: Record; output?: (args: { context: TContext }) => unknown; initial?: From 27a6697b5bd56776ebbf3870b0d2229f3a5ad01e Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 4 Apr 2026 21:04:45 -0400 Subject: [PATCH 04/34] Simplify: flat state --- src/agent.test.ts | 298 ++++++---------------------------- src/index.ts | 2 - src/machine.ts | 403 ++++++++++++++++++---------------------------- src/types.ts | 63 +++----- src/utils.ts | 234 ++++++--------------------- 5 files changed, 278 insertions(+), 722 deletions(-) diff --git a/src/agent.test.ts b/src/agent.test.ts index 2722d05..6bb8d7c 100644 --- a/src/agent.test.ts +++ b/src/agent.test.ts @@ -96,8 +96,8 @@ function createHitlMachine() { }, }), // static shorthand — string target - 'user.approve': 'processing', - 'user.cancel': 'cancelled', + 'user.approve': { target: 'processing' }, + 'user.cancel': { target: 'cancelled' }, }, }, processing: { @@ -126,7 +126,7 @@ function createHitlMachine() { ], }, }), - 'user.cancel': 'cancelled', + 'user.cancel': { target: 'cancelled' }, }, }, done: { @@ -224,79 +224,6 @@ function createClassifyMachine(adapter: AgentAdapter) { }); } -// ─── Nested machine ─── - -function createNestedMachine() { - return createAgentMachine({ - id: 'nested', - context: () => ({ - resolution: null as string | null, - category: 'billing' as string, - }), - initial: 'handling', - states: { - handling: { - initial: ({ context }) => { - if (context.category === 'billing') - return { target: 'checkEligibility' }; - return { target: 'diagnose' }; - }, - states: { - checkEligibility: { - invoke: async () => ({ eligible: true }), - onDone: ({ result }) => { - if ((result as { eligible: boolean }).eligible) - return { target: 'processRefund' }; - return { target: 'deny' }; - }, - }, - processRefund: { - invoke: async () => ({}), - onDone: () => ({ - target: 'childDone', - context: { resolution: 'Refund processed' }, - }), - }, - deny: { - invoke: async () => ({ message: 'Not eligible' }), - onDone: ({ result }) => ({ - target: 'childDone', - context: { - resolution: (result as { message: string }).message, - }, - }), - }, - diagnose: { - invoke: async () => ({ diagnosis: 'It is a bug' }), - onDone: ({ result }) => ({ - target: 'childDone', - context: { - resolution: (result as { diagnosis: string }).diagnosis, - }, - }), - }, - childDone: { type: 'final' }, - }, - onDone: () => ({ target: 'respond' }), - on: { - 'user.cancel': () => ({ target: 'cancelled' }), - }, - }, - respond: { - invoke: async ({ context }) => ({ message: context.resolution }), - onDone: () => ({ target: 'done' }), - }, - done: { - type: 'final', - output: ({ context }) => ({ resolution: context.resolution }), - }, - cancelled: { - type: 'final', - output: () => ({ cancelled: true }), - }, - }, - }); -} // ═══════════════════════════════════════ // Tests @@ -332,7 +259,7 @@ describe('getInitialState', () => { test('rejects invalid input', () => { const machine = createHitlMachine(); - // @ts-expect-error — deliberately invalid input for runtime test + // Runtime validation catches invalid input (schemas.input validates) expect(() => machine.getInitialState({ task: 123 })).toThrow(); }); @@ -356,10 +283,6 @@ describe('getInitialState', () => { expect(machine.getInitialState('fast').value).toBe('fast'); }); - test('resolves compound state initial', () => { - const machine = createNestedMachine(); - expect(machine.getInitialState().value).toEqual({ handling: 'checkEligibility' }); - }); }); describe('invoke', () => { @@ -445,20 +368,6 @@ describe('invoke', () => { expect((s.error as Error).message).toBe('boom'); }); - test('nested state entry and execution', async () => { - const machine = createNestedMachine(); - let s = machine.getInitialState(); - expect(s.value).toEqual({ handling: 'checkEligibility' }); - - s = await machine.invoke(s); - expect(s.value).toEqual({ handling: 'processRefund' }); - - s = await machine.invoke(s); - expect(s.value).toEqual({ handling: 'childDone' }); - - s = await machine.invoke(s); - expect(s.value).toBe('respond'); - }); }); describe('transition', () => { @@ -492,13 +401,6 @@ describe('transition', () => { ).toThrow("No handler for event 'nope'"); }); - test('parent preempts child', () => { - const machine = createNestedMachine(); - const s = machine.transition(machine.getInitialState(), { - type: 'user.cancel', - }); - expect(s.value).toBe('cancelled'); - }); }); describe('execute', () => { @@ -556,13 +458,6 @@ describe('execute', () => { } }); - test('runs nested states to completion', async () => { - const machine = createNestedMachine(); - const r = await machine.execute(machine.getInitialState()); - expect(r.status === 'done' && r.output).toEqual({ - resolution: 'Refund processed', - }); - }); }); describe('stream', () => { @@ -592,14 +487,6 @@ describe('resolveState', () => { expect(next.context.messages[0]!.content).toBe('restored'); }); - test('nested round-trip', async () => { - const machine = createNestedMachine(); - const s = machine.getInitialState(); - const restored = machine.resolveState(JSON.parse(JSON.stringify(s))); - expect(restored.value).toEqual({ handling: 'checkEligibility' }); - const r = await machine.execute(restored); - expect(r.status).toBe('done'); - }); }); describe('decide', () => { @@ -680,7 +567,7 @@ describe('decide', () => { context: { items: result.choice === 'withData' - ? (result.data as { items: string[] }).items + ? result.data.items : null, }, }), @@ -773,114 +660,6 @@ describe('classify', () => { }); }); -describe('nested states', () => { - test('conditional compound initial', async () => { - const machine = createAgentMachine({ - id: 'cond', - context: () => ({ - category: 'technical' as string, - resolution: null as string | null, - }), - initial: 'handling', - states: { - handling: { - initial: ({ context }) => - context.category === 'billing' - ? { target: 'billing' } - : { target: 'technical' }, - states: { - billing: { - invoke: async () => ({}), - onDone: () => ({ target: 'childDone' }), - }, - technical: { - invoke: async () => ({ result: 'tech handled' }), - onDone: ({ result }) => ({ - target: 'childDone', - context: { - resolution: (result as { result: string }).result, - }, - }), - }, - childDone: { type: 'final' }, - }, - onDone: () => ({ target: 'done' }), - }, - done: { - type: 'final', - output: ({ context }) => ({ resolution: context.resolution }), - }, - }, - }); - expect(machine.getInitialState().value).toEqual({ handling: 'technical' }); - const r = await machine.execute(machine.getInitialState()); - expect(r.status === 'done' && r.output).toEqual({ - resolution: 'tech handled', - }); - }); - - test('parent preempts → cancel', async () => { - const machine = createNestedMachine(); - let s = machine.transition(machine.getInitialState(), { - type: 'user.cancel', - }); - const r = await machine.execute(s); - expect(r.status === 'done' && r.output).toEqual({ cancelled: true }); - }); -}); - -describe('P1: nested final without parent onDone', () => { - test('halts at pending', async () => { - const machine = createAgentMachine({ - id: 'p1', - context: () => ({}), - initial: 'a', - states: { - a: { - initial: 'b', - states: { - b: { - initial: 'c', - states: { c: { type: 'final' } }, - }, - }, - onDone: () => ({ target: 'result' }), - }, - result: { type: 'final' }, - }, - }); - const s = await machine.invoke(machine.getInitialState()); - expect(s.status).toBe('pending'); - expect(s.value).toEqual({ a: { b: 'c' } }); - }); - - test('ancestor on still reachable', async () => { - const machine = createAgentMachine({ - id: 'p1-esc', - context: () => ({}), - initial: 'a', - states: { - a: { - initial: 'b', - states: { - b: { - initial: 'c', - states: { c: { type: 'final' } }, - }, - }, - on: { escape: () => ({ target: 'out' }) }, - }, - out: { type: 'final', output: () => ({ escaped: true }) }, - }, - }); - let r = await machine.execute(machine.getInitialState()); - expect(r.status).toBe('pending'); - const s = machine.transition(r.state, { type: 'escape' }); - r = await machine.execute(s); - expect(r.status === 'done' && r.output).toEqual({ escaped: true }); - }); -}); - describe('P2: event validation', () => { test('rejects invalid payload', async () => { const machine = createHitlMachine(); @@ -962,26 +741,6 @@ describe('type inference', () => { s.value satisfies 'c'; }); - test('nested state values are xstate-style objects', () => { - const machine = createAgentMachine({ - id: 't', - context: () => ({}), - initial: 'parent', - states: { - parent: { - initial: 'child', - states: { child: { type: 'final' } }, - onDone: () => ({ target: 'done' }), - }, - done: { type: 'final' }, - }, - }); - const s = machine.getInitialState(); - - // Runtime check — nested values are objects - expect(s.value).toEqual({ parent: 'child' }); - }); - // ─── state.context ─── test('context typed from context() return', () => { @@ -1002,9 +761,48 @@ describe('type inference', () => { s.context.nope; }); + test('transition context is Partial — rejects unknown keys', () => { + createAgentMachine({ + id: 't', + schemas: { events: { go: z.object({}) } }, + context: () => ({ count: 0, name: 'hello' }), + initial: 'idle', + states: { + idle: { + on: { + go: ({ context }) => ({ + target: 'idle', + // valid: known key + context: { count: context.count + 1 }, + }), + }, + }, + }, + }); + + // @ts-expect-error — 'foo' not a valid context key + createAgentMachine({ + id: 't2', + schemas: { events: { go: z.object({}) } }, + context: () => ({ count: 0 }), + initial: 'idle', + states: { + idle: { + on: { + go: () => ({ + target: 'idle', + context: { foo: 'bar' }, + }), + }, + }, + }, + }); + }); + test('context typed in on handlers', () => { const machine = createAgentMachine({ id: 't', + schemas: { events: { add: z.object({}) } }, context: () => ({ items: ['a', 'b'] }), initial: 'idle', states: { @@ -1285,6 +1083,8 @@ describe('type inference', () => { options: { a: { description: 'A' } }, onDone: ({ result, context }) => { result.choice satisfies string; + // @ts-expect-error + result.nope; context.topic satisfies string; return { target: 'done', context: { result: result.choice } }; }, @@ -1372,7 +1172,9 @@ describe('type inference', () => { work: { invoke: async () => ({ anything: true }), onDone: ({ result }) => { - // result is any when no resultSchema — no errors + // Without resultSchema, result is ChoiceResult (default) + result.choice satisfies string; + // @ts-expect-error — 'whatever' not on ChoiceResult result.whatever; return { target: 'done' }; }, @@ -1419,7 +1221,7 @@ describe('type inference', () => { states: { a: { on: { - go: 'b', + go: { target: 'b' }, }, }, b: { type: 'final' }, diff --git a/src/index.ts b/src/index.ts index ead8991..5868e9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,8 +24,6 @@ export type { MachineConfig, StandardSchemaV1, StateConfig, - StateValue, - StateValueOf, Trace, TransitionEvent, TransitionResult, diff --git a/src/machine.ts b/src/machine.ts index 4fb1b86..9f6f7df 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -7,61 +7,33 @@ import type { InferOutput, MachineConfig, StandardSchemaV1, - StateConfig, - StateValue, - TransitionEvent, TransitionResult, } from './types.js'; import { applyTransition, - enterCompoundStates, findEventSchema, getAvailableEvents, - getParentConfig, getParams, - pathToValue, resolveInitial, resolveStateConfig, validateSchemaSync, - valueToPath, } from './utils.js'; +import type { StateConfigAny } from './utils.js'; -import type { InternalState } from './utils.js'; +// ─── Type helpers ─── -function toInternal(state: AgentState): InternalState { - return { ...state, value: valueToPath(state.value) }; -} +type FallbackAny = unknown extends T ? any : T; -function toExternal(state: InternalState): AgentState { - return { ...state, value: pathToValue(state.value) }; -} +/** Choice result shape — always the same for type: 'choice' */ +type ChoiceResult = { choice: string; data: Record; reasoning?: string }; -// Falls back to `any` when TResult was not inferred (unknown) -type FallbackAny = unknown extends T ? any : T; +/** Result type for onDone: typed from resultSchema when present */ +type OnDoneResult = unknown extends TResult ? ChoiceResult : NoInfer; + +type EventFor = E extends keyof TEvents & string + ? { type: E } & EventPayload> + : { type: E & string; [k: string]: unknown }; -// Handler for a specific known event type -type TypedOnHandler> = - | string - | TransitionResult - | ((args: { - event: { type: E } & EventPayload>; - context: TContext; - }) => TransitionResult); - -// Handler for an unknown event type -type UntypedOnHandler> = - | string - | TransitionResult - | ((args: { event: any; context: TContext }) => TransitionResult); - -// When TEvents has keys, known events get typed handlers; others get untyped. -// When TEvents is empty (no schemas.events), all handlers are untyped. -type OnHandlers> = - [keyof TEvents] extends [never] - ? Record> - : { [E in keyof TEvents & string]?: TypedOnHandler }; - -// Per-state node config with typed params via TParamsMap[K] and typed result via TResultMap[K] type StateNodeDef< TContext extends Record, TParams, @@ -76,25 +48,24 @@ type StateNodeDef< params: NoInfer; signal?: AbortSignal; }) => Promise>; - onDone?: (args: { result: FallbackAny>; context: TContext }) => TransitionResult; - on?: Record TransitionResult)>; + onDone?: (args: { result: OnDoneResult; context: TContext }) => TransitionResult; + on?: { [E in keyof TEvents & string]?: TransitionResult | ((args: { + event: EventFor; + context: TContext; + }) => TransitionResult) }; events?: Record; output?: (args: { context: TContext }) => unknown; - initial?: string | ((args: { context: TContext; params: Record }) => TransitionResult); - states?: Record>; // choice-specific model?: string; adapter?: import('./types.js').AgentAdapter; prompt?: string | ((args: { context: TContext; params: NoInfer }) => string); options?: Record; reasoning?: boolean; - // internal (from decide/classify wrappers) + // internal __type?: 'decide' | 'classify'; - __decideConfig?: any; - __classifyConfig?: any; + __decideConfig?: Record; }; -// Mapped states type: each key K gets its own params and result types type StatesMap< TContext extends Record, TParamsMap extends Record, @@ -104,7 +75,7 @@ type StatesMap< [K in keyof TParamsMap & keyof TResultMap]: StateNodeDef; }; -// ─── Overload 1: schemas.context drives TContext ─── +// ─── Overload A: schemas.context present ─── export function createAgentMachine< TInput, TContext extends Record, @@ -129,34 +100,8 @@ export function createAgentMachine< states: StatesMap, TParamsMap, TResultMap, TEvents>; }): AgentMachine>; -// ─── Overload 2: schemas.input present, context() return drives TContext ─── +// ─── Overload B: no schemas.context ─── export function createAgentMachine< - TInput, - TContext extends Record, - const TEvents extends Record, - const TParamsMap extends Record, - TResultMap extends Record, ->(config: { - id: string; - schemas: { - input: StandardSchemaV1; - context?: never; - events?: TEvents; - }; - context: (input: NoInfer) => TContext; - adapter?: import('./types.js').AgentAdapter; - initial: - | (keyof TParamsMap & keyof TResultMap & string) - | ((args: { context: TContext }) => { - target: keyof TParamsMap & keyof TResultMap & string; - params?: Record; - }); - states: StatesMap; -}): AgentMachine>; - -// ─── Overload 3: no schemas.input/context — all from context() ─── -export function createAgentMachine< - TInput, TContext extends Record, const TEvents extends Record, const TParamsMap extends Record, @@ -164,11 +109,11 @@ export function createAgentMachine< >(config: { id: string; schemas?: { - input?: never; + input?: StandardSchemaV1; context?: never; events?: TEvents; }; - context: (input: TInput) => TContext; + context: (...args: any[]) => TContext; adapter?: import('./types.js').AgentAdapter; initial: | (keyof TParamsMap & keyof TResultMap & string) @@ -177,24 +122,21 @@ export function createAgentMachine< params?: Record; }); states: StatesMap; -}): AgentMachine>; +}): AgentMachine>; // ─── Implementation ─── export function createAgentMachine( machineConfig: MachineConfig -): AgentMachine { - const cfg = machineConfig; - - // ─── getInitialState (sync) ─── +): AgentMachine { + const cfg = machineConfig as MachineConfig; function getInitialState(...args: [input?: unknown]): AgentState { const input = args[0]; let validatedInput = input; - const inputSchema = cfg.schemas?.input; - if (inputSchema) { - validatedInput = validateSchemaSync(inputSchema, input); + if (cfg.schemas?.input) { + validatedInput = validateSchemaSync(cfg.schemas.input, input); } const context = cfg.context(validatedInput); @@ -204,23 +146,16 @@ export function createAgentMachine( throw new Error('Initial transition must specify a target state'); } - let internal: InternalState = { + return { value: init.target, - params: {}, context: init.context ? { ...context, ...init.context } : context, status: 'active', + params: init.params ? { [init.target]: init.params } : {}, }; - if (init.params) { - internal.params = { [init.target]: init.params }; - } - internal = enterCompoundStates(cfg, internal as any) as any; - return toExternal(internal); } - // ─── resolveState ─── - function resolveState(raw: { - value: StateValue; + value: string; context: Record; params?: Record>; status?: AgentState['status']; @@ -237,57 +172,51 @@ export function createAgentMachine( }; } - // ─── transition (sync) ─── - - function transition(state: AgentState, event: { type: string; [k: string]: unknown }): AgentState { - const internal = toInternal(state); - validateEventPayload(internal, event); - - const parts = internal.value.split('.'); - for (let i = 1; i <= parts.length; i++) { - const path = parts.slice(0, i).join('.'); - const stateConfig = resolveStateConfig(cfg, path); - - if (stateConfig.on?.[event.type] !== undefined) { - const handler = stateConfig.on[event.type]!; - let result: TransitionResult; - if (typeof handler === 'string') { - result = { target: handler }; - } else if (typeof handler === 'function') { - result = handler({ context: internal.context, event }); - } else { - result = handler; - } - - if (result.target) { - return toExternal( - applyTransition(cfg, internal as any, result, path) as any - ); - } - - return toExternal({ - ...internal, - context: result.context - ? { ...internal.context, ...result.context } - : internal.context, - }); + function transition( + state: AgentState, + event: { type: string; [k: string]: unknown } + ): AgentState { + validateEventPayload(state.value, event); + + const sc = resolveStateConfig(cfg, state.value); + if (sc.on?.[event.type] !== undefined) { + const handler = sc.on[event.type]!; + const result: TransitionResult = + typeof handler === 'function' + ? handler({ context: state.context, event }) + : handler; + + if (result.target) { + return applyTransition(state, result); } + + return { + ...state, + context: result.context + ? { ...state.context, ...result.context } + : state.context, + }; } throw new Error( - `No handler for event '${event.type}' in state '${internal.value}'` + `No handler for event '${event.type}' in state '${state.value}'` ); } function validateEventPayload( - internal: InternalState, - event: { type: string; [k: string]: unknown } + value: string, + event: { type: string } ): void { - const schema = findEventSchema(cfg, internal.value, event.type); + const schema = findEventSchema(cfg, value, event.type); if (!schema) return; const result = schema['~standard'].validate(event); if (result instanceof Promise) return; - if (result && typeof result === 'object' && 'issues' in result && result.issues) { + if ( + result && + typeof result === 'object' && + 'issues' in result && + result.issues + ) { const messages = (result.issues as Array<{ message: string }>) .map((i) => i.message) .join(', '); @@ -295,129 +224,92 @@ export function createAgentMachine( } } - // ─── invoke (async, one step) ─── - async function invoke(state: AgentState): Promise { - const internal = toInternal(state); - if (internal.status === 'done' || internal.status === 'error') { + if (state.status === 'done' || state.status === 'error') { return state; } - const result = await invokeInternal(internal); - return toExternal(result); - } - async function invokeInternal(state: InternalState): Promise { - const stateConfig = resolveStateConfig(cfg, state.value) as any; + const sc = resolveStateConfig(cfg, state.value); - if (stateConfig.type === 'final') { - return handleFinal(state, stateConfig); - } - // type: 'choice' — inline decide config - if (stateConfig.type === 'choice') { - return handleChoice(state, stateConfig); + if (sc.type === 'final') { + const output = sc.output + ? sc.output({ context: state.context }) + : undefined; + return { ...state, status: 'done', output }; } - // decide()/classify() wrapper — __decideConfig set internally - if (stateConfig.__decideConfig) { - return handleDecide(state, stateConfig); + + if (sc.type === 'choice' || sc.__decideConfig) { + return handleChoice(state, sc); } - if (stateConfig.invoke) { - return handleInvoke(state, stateConfig); + + if (sc.invoke) { + return handleInvoke(state, sc); } - if (stateConfig.on) { + + if (sc.on) { return { ...state, status: 'pending' }; } - if (stateConfig.states && stateConfig.initial) { - return { ...state, status: 'active' }; - } + return { ...state, status: 'error', - error: `State '${state.value}' has no invoke, events, or children`, + error: `State '${state.value}' has no invoke, events, or final type`, }; } - function handleFinal(state: InternalState, config: any): InternalState { - const output = config.output - ? config.output({ context: state.context }) - : undefined; - - const parts = state.value.split('.'); - if (parts.length <= 1) { - return { ...state, status: 'done', output }; - } - - const parentConfig = getParentConfig(cfg, state.value); - if (parentConfig?.onDone) { - const parentPath = parts.slice(0, -1).join('.'); - const trans = parentConfig.onDone({ result: output, context: state.context }); - return applyTransition(cfg, state as any, trans, parentPath) as any; - } - - return { ...state, status: 'pending' }; - } - - async function handleChoice(state: InternalState, sc: any): Promise { - const adapter = sc.adapter ?? cfg.adapter; + async function handleChoice( + state: AgentState, + sc: StateConfigAny + ): Promise { + // Merge __decideConfig props onto sc for decide() wrapper compat + const dc = sc.__decideConfig + ? { ...sc, ...(sc.__decideConfig as Record) } + : sc; + const adapter = (dc as StateConfigAny).adapter ?? cfg.adapter; if (!adapter) { - return { ...state, status: 'error', error: `No adapter for choice state '${state.value}'` }; + return { + ...state, + status: 'error', + error: `No adapter for '${state.value}'`, + }; } const params = getParams(state.value, state.params); - const prompt = typeof sc.prompt === 'function' - ? sc.prompt({ context: state.context, params }) - : sc.prompt; + const prompt = + typeof dc.prompt === 'function' + ? dc.prompt({ context: state.context, params }) + : dc.prompt; try { const result = await adapter.decide({ - model: sc.model, - prompt, - options: sc.options, - reasoning: sc.reasoning, + model: (dc as StateConfigAny).model!, + prompt: prompt as string, + options: (dc as StateConfigAny).options!, + reasoning: (dc as StateConfigAny).reasoning, }); - const trans = sc.onDone({ result, context: state.context }); - return applyTransition(cfg, state as any, trans, state.value) as any; + const onDone = (dc as StateConfigAny).onDone; + if (!onDone) return { ...state, status: 'error', error: 'choice state missing onDone' }; + const trans = onDone({ result, context: state.context }); + return applyTransition(state, trans); } catch (error) { return { ...state, status: 'error', error }; } } - async function handleDecide(state: InternalState, stateConfig: StateConfig): Promise { - const dc = (stateConfig as any).__decideConfig!; - const adapter = dc.adapter ?? cfg.adapter; - if (!adapter) { - return { ...state, status: 'error', error: `No adapter for '${state.value}'` }; - } - - const params = getParams(state.value, state.params); - const prompt = typeof dc.prompt === 'function' - ? dc.prompt({ context: state.context, params }) - : dc.prompt; - - try { - const result = await adapter.decide({ - model: dc.model, - prompt, - options: dc.options, - reasoning: dc.reasoning, - }); - const trans = dc.onDone({ result, context: state.context }); - return applyTransition(cfg, state as any, trans, state.value) as any; - } catch (error) { - return { ...state, status: 'error', error }; - } - } - - async function handleInvoke(state: InternalState, stateConfig: any): Promise { + async function handleInvoke( + state: AgentState, + sc: StateConfigAny + ): Promise { try { - const result = await stateConfig.invoke!({ + const result = await sc.invoke!({ context: state.context, params: getParams(state.value, state.params), }); - if (stateConfig.onDone) { - const trans = stateConfig.onDone({ result, context: state.context }); - return applyTransition(cfg, state as any, trans, state.value) as any; + if (sc.onDone) { + const trans = sc.onDone({ result, context: state.context }); + return applyTransition(state, trans); } - if (stateConfig.on) { + if (sc.on) { return { ...state, status: 'pending' }; } return state; @@ -426,46 +318,61 @@ export function createAgentMachine( } } - // ─── execute ─── - async function execute(state: AgentState): Promise { - let internal = toInternal(state); - while (internal.status === 'active') { - internal = await invokeInternal(internal); + let current = state; + while (current.status === 'active') { + current = await invoke(current); } - const ext = toExternal(internal); - switch (internal.status) { + switch (current.status) { case 'done': - return { status: 'done', state: ext, output: internal.output, context: internal.context }; + return { + status: 'done', + state: current, + output: current.output, + context: current.context, + }; case 'pending': return { status: 'pending', - state: ext, - value: ext.value, - events: getAvailableEvents(cfg, internal.value), - context: internal.context, + state: current, + value: current.value, + events: getAvailableEvents(cfg, current.value), + context: current.context, }; case 'error': - return { status: 'error', state: ext, error: internal.error }; + return { + status: 'error', + state: current, + error: current.error, + }; default: - return { status: 'error', state: ext, error: `Unexpected: ${internal.status}` }; + return { + status: 'error', + state: current, + error: `Unexpected: ${current.status}`, + }; } } - // ─── stream ─── - - async function* stream(state: AgentState): AsyncGenerator { - let internal = toInternal(state); - yield toSnap(internal); - while (internal.status === 'active') { - internal = await invokeInternal(internal); - yield toSnap(internal); + async function* stream( + state: AgentState + ): AsyncGenerator { + let current = state; + yield toSnap(current); + while (current.status === 'active') { + current = await invoke(current); + yield toSnap(current); } } - function toSnap(s: InternalState): AgentSnapshot { - return { value: pathToValue(s.value), context: s.context, status: s.status, params: s.params }; + function toSnap(s: AgentState): AgentSnapshot { + return { + value: s.value, + context: s.context, + status: s.status, + params: s.params, + }; } return { @@ -476,5 +383,5 @@ export function createAgentMachine( invoke, execute, stream, - } as any; + } as AgentMachine; } diff --git a/src/types.ts b/src/types.ts index 1de6054..6ba580d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,23 +15,6 @@ export type StandardSchemaResult = export type InferOutput = T extends StandardSchemaV1 ? O : never; -// ─── State Value (xstate-style) ─── - -/** `'idle'` or `{ handling: 'check' }` or `{ a: { b: 'deep' } }` */ -export type StateValue = string | { [key: string]: StateValue }; - -/** Derive the state value union from a states config (depth-limited to 4) */ -export type StateValueOf = _SV1; -type _SV1 = T extends Record - ? { [K in keyof T & string]: T[K] extends { states: infer S extends Record } ? { [P in K]: _SV2 } : K }[keyof T & string] - : never; -type _SV2 = T extends Record - ? { [K in keyof T & string]: T[K] extends { states: infer S extends Record } ? { [P in K]: _SV3 } : K }[keyof T & string] - : never; -type _SV3 = T extends Record - ? { [K in keyof T & string]: K }[keyof T & string] - : never; - // ─── Event Helpers ─── export type EventPayload = T extends Record ? unknown : T; @@ -63,9 +46,11 @@ export interface AgentAdapter { // ─── Transition ─── -export interface TransitionResult { +export interface TransitionResult< + TContext extends Record = Record, +> { target?: string; - context?: Record; + context?: Partial; params?: Record; } @@ -81,14 +66,10 @@ export interface StateConfig< params: Record; signal?: AbortSignal; }) => Promise; - onDone?: (args: { result: any; context: TContext }) => TransitionResult; - on?: Record TransitionResult)>; + onDone?: (args: { result: any; context: TContext }) => TransitionResult; + on?: Record | ((args: { event: any; context: TContext }) => TransitionResult)>; events?: Record; output?: (args: { context: TContext }) => unknown; - initial?: - | string - | ((args: { context: TContext; params: Record }) => TransitionResult); - states?: Record>; // choice-specific model?: string; adapter?: AgentAdapter; @@ -104,7 +85,7 @@ export interface StateConfig< export interface AgentState< TContext extends Record = Record, - TValue extends StateValue = StateValue, + TValue extends string = string, > { value: TValue; context: TContext; @@ -118,7 +99,7 @@ export interface AgentState< export type ExecuteResult< TContext extends Record = Record, - TValue extends StateValue = StateValue, + TValue extends string = string, TEvents extends Record = {}, > = | { status: 'done'; state: AgentState; output: unknown; context: TContext } @@ -129,7 +110,7 @@ export type ExecuteResult< export interface AgentSnapshot< TContext extends Record = Record, - TValue extends StateValue = StateValue, + TValue extends string = string, > { value: TValue; context: TContext; @@ -149,33 +130,33 @@ export interface AgentMachine< getInitialState( ...args: unknown extends TInput ? [input?: TInput] : [input: TInput] - ): AgentState>; + ): AgentState; resolveState(raw: { - value: StateValue; + value: string; context: TContext; params?: Record>; status?: AgentState['status']; output?: unknown; error?: unknown; - }): AgentState>; + }): AgentState; transition( - state: AgentState>, + state: AgentState, event: TransitionEvent - ): AgentState>; + ): AgentState; invoke( - state: AgentState> - ): Promise>>; + state: AgentState + ): Promise>; execute( - state: AgentState> - ): Promise, TEvents>>; + state: AgentState + ): Promise>; stream( - state: AgentState> - ): AsyncGenerator>>; + state: AgentState + ): AsyncGenerator>; } // ─── Machine Config (internal) ─── @@ -200,7 +181,7 @@ export interface MachineConfig< states: TStates; } -// ─── Decide (wrapper fn — typed result, untyped context) ─── +// ─── Decide ─── export type DecideResultFor< TOptions extends Record, @@ -224,7 +205,7 @@ export interface DecideConfig< on?: Record }) => TransitionResult>; } -// ─── Classify (wrapper fn — typed category, untyped context) ─── +// ─── Classify ─── export interface ClassifyConfig< TCategories extends Record = Record, diff --git a/src/utils.ts b/src/utils.ts index e8a38d0..1c8b73c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,48 +1,13 @@ import type { + AgentState, MachineConfig, StandardSchemaResult, StandardSchemaV1, - StateConfig, - StateValue, TransitionResult, } from './types.js'; -/** Internal state representation with dot-path string value */ -export interface InternalState { - value: string; - context: Record; - status: 'active' | 'pending' | 'done' | 'error'; - params: Record>; - output?: unknown; - error?: unknown; -} - -// ─── StateValue ↔ dot-path conversion ─── - -/** Convert xstate-style value `{ handling: 'check' }` to dot-path `'handling.check'` */ -export function valueToPath(value: StateValue): string { - if (typeof value === 'string') return value; - const key = Object.keys(value)[0]!; - const child = (value as Record)[key]!; - return typeof child === 'string' - ? `${key}.${child}` - : `${key}.${valueToPath(child)}`; -} - -/** Convert dot-path `'handling.check'` to xstate-style value `{ handling: 'check' }` */ -export function pathToValue(path: string): StateValue { - const parts = path.split('.'); - if (parts.length === 1) return parts[0]!; - let result: StateValue = parts[parts.length - 1]!; - for (let i = parts.length - 2; i >= 0; i--) { - result = { [parts[i]!]: result }; - } - return result; -} - /** * Validate a value against a Standard Schema synchronously. - * Throws if validation returns a Promise (async schemas not supported here). */ export function validateSchemaSync( schema: StandardSchemaV1, @@ -51,7 +16,7 @@ export function validateSchemaSync( const result = schema['~standard'].validate(value); if (result instanceof Promise) { throw new Error( - 'Async schema validation is not supported in sync context. Validate input before calling getInitialState.' + 'Async schema validation is not supported in sync context.' ); } const syncResult = result as StandardSchemaResult; @@ -65,58 +30,44 @@ export function validateSchemaSync( } /** - * Resolve a StateConfig from a dot-separated state path. + * Get the state config for a given state name. */ export function resolveStateConfig( - config: MachineConfig, + config: MachineConfig, value: string -): any { - const parts = value.split('.'); - let current: Record = config.states; - let stateConfig: any; - - for (const part of parts) { - stateConfig = current[part]; - if (!stateConfig) { - throw new Error(`State '${part}' not found in path '${value}'`); - } - if (stateConfig.states) { - current = stateConfig.states; - } +): StateConfigAny { + const stateConfig = config.states[value]; + if (!stateConfig) { + throw new Error(`State '${value}' not found`); } - - return stateConfig!; + return stateConfig as StateConfigAny; } -/** - * Get the parent state config, or null for root states. - */ -export function getParentConfig( - config: MachineConfig, - value: string -): any { - const parts = value.split('.'); - if (parts.length <= 1) return null; - const parentPath = parts.slice(0, -1).join('.'); - return resolveStateConfig(config, parentPath); -} +/** Loose state config for internal runtime use */ +export type StateConfigAny = { + type?: 'final' | 'choice'; + invoke?: (args: { context: Record; params: Record }) => Promise; + onDone?: (args: { result: unknown; context: Record }) => TransitionResult; + on?: Record; context: Record }) => TransitionResult)>; + output?: (args: { context: Record }) => unknown; + model?: string; + adapter?: { decide: (...args: unknown[]) => Promise }; + prompt?: string | ((args: { context: Record; params: Record }) => string); + options?: Record; + reasoning?: boolean; + events?: Record; + __type?: string; + __decideConfig?: Record; +}; /** * Get the params for the current state. - * Params are stored at `state.params[statePath]` when transitioning. - * For nested states, also checks the parent path. */ export function getParams( - valuePath: string, + value: string, params: Record> ): Record { - // Check own params first (set when transitioning TO this state) - if (params[valuePath]) return params[valuePath]!; - // Fall back to parent params (for compound state children) - const parts = valuePath.split('.'); - if (parts.length <= 1) return {}; - const parentPath = parts.slice(0, -1).join('.'); - return params[parentPath] ?? {}; + return params[value] ?? {}; } /** @@ -140,29 +91,13 @@ export function resolveInitial( return initial(args); } -/** - * Resolve a target relative to the handler's state path. - * Targets are siblings of the state where the handler is defined. - */ -export function resolveTarget( - handlerStatePath: string, - target: string -): string { - const parts = handlerStatePath.split('.'); - if (parts.length <= 1) return target; - const parentParts = parts.slice(0, -1); - return [...parentParts, target].join('.'); -} - /** * Apply a transition result to produce a new state. */ export function applyTransition( - config: MachineConfig, - state: InternalState, - transition: TransitionResult, - handlerStatePath: string -): InternalState { + state: AgentState, + transition: TransitionResult +): AgentState { let newState = { ...state }; if (transition.context) { @@ -170,67 +105,25 @@ export function applyTransition( } if (transition.target) { - newState.value = resolveTarget(handlerStatePath, transition.target); + newState.value = transition.target; newState.status = 'active'; if (transition.params) { newState.params = { ...state.params, - [newState.value]: transition.params, + [transition.target]: transition.params, }; } - - newState = enterCompoundStates(config, newState); } return newState; } /** - * If the current state is a compound state, resolve its initial and descend. - */ -export function enterCompoundStates( - config: MachineConfig, - state: InternalState -): InternalState { - let current = state; - - for (;;) { - const stateConfig = resolveStateConfig(config, current.value); - if (!stateConfig.states || !stateConfig.initial) break; - - const params = current.params[current.value] ?? {}; - const init = resolveInitial(stateConfig.initial, { - context: current.context, - params, - }); - - if (!init.target) break; - - const childValue = `${current.value}.${init.target}`; - current = { ...current, value: childValue }; - - if (init.context) { - current.context = { ...current.context, ...init.context }; - } - if (init.params) { - current.params = { - ...current.params, - [current.value]: init.params, - }; - } - } - - return current; -} - -/** - * Collect available events for a state path. - * State-level events override root-level events. - * Only includes events that have handlers. + * Collect available events for a state. */ export function getAvailableEvents( - config: MachineConfig, + config: MachineConfig, value: string ): Record { const events: Record = {}; @@ -239,62 +132,37 @@ export function getAvailableEvents( Object.assign(events, config.schemas.events); } - const parts = value.split('.'); - for (let i = 0; i < parts.length; i++) { - const path = parts.slice(0, i + 1).join('.'); - const stateConfig = resolveStateConfig(config, path); - if (stateConfig.events) { - Object.assign(events, stateConfig.events); - } - } - - const handledTypes = getHandledEventTypes(config, value); - const result: Record = {}; - for (const eventType of handledTypes) { - if (events[eventType]) { - result[eventType] = events[eventType]; - } + const stateConfig = resolveStateConfig(config, value); + if (stateConfig.events) { + Object.assign(events, stateConfig.events); } - return result; -} - -function getHandledEventTypes( - config: MachineConfig, - value: string -): Set { - const handled = new Set(); - const parts = value.split('.'); - - for (let i = parts.length; i >= 1; i--) { - const path = parts.slice(0, i).join('.'); - const stateConfig = resolveStateConfig(config, path); - if (stateConfig.on) { - for (const eventType of Object.keys(stateConfig.on)) { - handled.add(eventType); + if (stateConfig.on) { + const handled = new Set(Object.keys(stateConfig.on)); + const result: Record = {}; + for (const key of handled) { + if (events[key]) { + result[key] = events[key]; } } + return result; } - return handled; + return {}; } /** * Find the event schema for a given event type. - * State-level schemas override root-level. */ export function findEventSchema( - config: MachineConfig, + config: MachineConfig, value: string, eventType: string ): StandardSchemaV1 | undefined { - const parts = value.split('.'); - for (let i = parts.length; i >= 1; i--) { - const path = parts.slice(0, i).join('.'); - const stateConfig = resolveStateConfig(config, path); - if (stateConfig.events?.[eventType]) { - return stateConfig.events[eventType]; - } + const stateConfig = resolveStateConfig(config, value); + if (stateConfig.events?.[eventType]) { + return stateConfig.events[eventType]; } - return config.schemas?.events?.[eventType]; + const events = config.schemas?.events as Record | undefined; + return events?.[eventType]; } From a4b80719e1ac06d78cae0198c8f8eec75c7e9da4 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 8 Apr 2026 19:35:06 -0400 Subject: [PATCH 05/34] docs: add langgraph core replacement design --- ...04-08-langgraph-core-replacement-design.md | 549 ++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md diff --git a/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md b/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md new file mode 100644 index 0000000..29eeb94 --- /dev/null +++ b/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md @@ -0,0 +1,549 @@ +# LangGraph Core Replacement Design + +## Goal + +Evolve `agent` into a LangGraph-core replacement in terms of runtime behavior and developer outcomes, while improving on LangGraph through a simpler, more explicit state-machine model. + +The target is semantic parity for core orchestration use cases, not API compatibility. Developers should be able to build the same classes of systems in `agent` that they can build with LangGraph core, but using `agent`'s state-machine API and philosophy. + +## Core Philosophies + +The design is constrained by these principles: + +1. Logic is pure. + The semantic center remains: + + ```ts + (currentState, event) => { + return { nextState, effects }; + } + ``` + + State transition logic should stay deterministic, replayable, and inspectable. + +2. Effect execution is first-class. + The runtime must make it easy to both transition state and execute effects, but without collapsing transition logic into effectful code. Effects are driven by the machine, not hidden as the machine. + +3. Durability is core. + `agent` should treat persisted state and event history as first-class runtime concerns, not as optional add-ons. + +4. Runner-agnostic execution. + The runtime must be able to run anywhere: Node, Vercel, Cloudflare, Durable Objects, workers, and other environments. Storage and execution coordination must be abstracted behind portable interfaces. + +5. Improve on LangGraph rather than imitate it. + Do not copy LangGraph's graph-builder surface area or its more complex runtime semantics where a simpler state-machine formulation produces the same outcome. + +## Scope + +In scope: + +- core orchestration behavior currently covered by `@langchain/langgraph` +- runtime behavior tests and runnable examples from LangGraph core, rewritten as `agent`-idiomatic equivalents +- persistence, replay, resume, streaming, pending states, submachine composition, and high-value prebuilt agent patterns + +Out of scope for this design: + +- LangGraph monorepo packages outside core +- API/CLI/server packages +- UI framework SDKs and app templates +- type-level compatibility with LangGraph +- exact API or import-path matching + +## Design Summary + +`agent` should become a durable run engine for state machines. + +A machine definition remains declarative and mostly pure: + +- states +- transitions +- invoke/effect boundaries +- final outputs + +A run becomes the primary runtime object: + +- backed by an append-only event log +- accelerated by persisted snapshots +- observable through a first-class event stream +- resumable from persisted state +- portable across runners via abstract persistence and scheduling interfaces + +This yields a model where the semantics are simple: + +- transitions are deterministic +- invokes are explicit effect boundaries +- external events drive progress +- streaming is run-level, not bolted on +- persistence is a core contract + +## Runtime Model + +The machine model should stay state-machine-first rather than graph-builder-first. + +### Machine Definition + +A machine definition remains responsible for: + +- context initialization +- current state value +- transition handlers +- invoke definitions +- terminal outputs + +The machine should continue to express workflows such as: + +- branching +- tool-using agents +- human review loops +- multi-step planning and execution +- nested machine orchestration + +### Run Model + +Introduce a durable run as the central execution concept. + +Each run has: + +- `runId` +- `machineId` +- input payload +- current snapshot +- append-only event history +- status +- subscribers + +Suggested shape: + +```ts +interface AgentRun { + id: string; + status: "active" | "pending" | "done" | "error"; + getSnapshot(): AgentState; + send(event: { type: string; [key: string]: unknown }): Promise; + on(type: string, handler: (event: unknown) => void): () => void; + [Symbol.asyncIterator](): AsyncIterator; +} +``` + +### Durable Execution Boundaries + +Phase 1 durability should exist at machine boundaries, not inside arbitrary user async code. + +Persist: + +- run start +- external event receipt +- state entry +- invoke start +- invoke success +- invoke failure +- transition application +- run completion +- run failure + +Do not claim sub-invoke durability for plain `Promise.all(...)` or arbitrary nested promises. + +### Pending and Human-in-the-Loop + +Do not introduce an interrupt primitive as a core concept. + +Use explicit pending states and external events: + +```ts +review: { + on: { + approve: { target: "send" }, + reject: { target: "revise" }, + }, +} +``` + +This preserves: + +- deterministic replay +- explicit control flow +- durable resume semantics +- runner portability + +### Submachine Composition + +Do not introduce graph/subgraph composition as a first-class structural primitive in phase 1. + +Instead, allow composition through normal execution: + +```ts +writing: { + invoke: async ({ context }) => { + return executeAgentMachine(writerMachine, { + input: { + topic: context.topic, + research: context.research, + }, + }); + }, +} +``` + +This is sufficient for most LangGraph subgraph outcomes without graph-specific composition APIs. + +## Purity and Effects + +The central architectural requirement is preserving pure transition logic while still making effects first-class. + +Conceptually, every runtime step should be explainable as: + +```ts +const { nextState, effects } = transition(currentState, event); +``` + +Where: + +- `nextState` is deterministic +- `effects` are explicit runtime work to perform next + +In practice, current `agent` APIs already combine these concerns inside state configs. The design should move the runtime toward an explicit internal split even if the external authoring API remains ergonomic. + +That means: + +- transition logic should remain replayable without rerunning effects +- effect lifecycle should be represented in runtime events +- invoke results should be fed back as events, not hidden mutations + +This is the main improvement opportunity over LangGraph's more graph-runtime-centric model. + +## Persistence Model + +The canonical persisted representation is an append-only event log. + +Snapshots are derived state used to accelerate replay and resume. + +### Event Log + +Suggested minimal durable event family: + +```ts +type PersistedRunEvent = + | { type: "run.started"; input: unknown; at: number } + | { type: "event.received"; event: { type: string; [k: string]: unknown }; at: number } + | { type: "state.entered"; value: string; params?: Record; at: number } + | { type: "invoke.started"; state: string; at: number } + | { type: "invoke.succeeded"; state: string; result: unknown; at: number } + | { type: "invoke.failed"; state: string; error: SerializedError; at: number } + | { type: "transition.applied"; from: string; to: string; at: number } + | { type: "run.completed"; output: unknown; at: number } + | { type: "run.failed"; error: SerializedError; at: number }; +``` + +### Snapshots + +Suggested snapshot shape: + +```ts +type PersistedSnapshot = { + runId: string; + version: number; + state: AgentState; + lastEventIndex: number; + createdAt: number; +}; +``` + +### Replay Model + +Restore a run by: + +1. loading the latest snapshot +2. replaying all events after that snapshot +3. reconstructing the current live run state + +If no snapshot exists, replay from `run.started`. + +### Storage Interface + +Persistence must be abstracted behind a portable interface: + +```ts +interface RunStore { + append(runId: string, event: PersistedRunEvent): Promise; + loadEvents(runId: string, afterVersion?: number): Promise; + loadLatestSnapshot(runId: string): Promise; + saveSnapshot(snapshot: PersistedSnapshot): Promise; +} +``` + +This is what makes the runtime portable to: + +- in-memory test stores +- SQL or key-value stores +- Cloudflare Durable Objects +- Vercel-backed durable layers +- custom app infrastructure + +### Important Phase 1 Constraint + +Invoke internals are opaque unless user code or future helpers explicitly expose finer-grained durable progress. + +This means: + +- plain async code remains ergonomic +- invoke-level durability is honest +- future `task(...)` or `parallel(...)` helpers remain additive + +## Streaming Model + +Streaming must be a first-class capability of a run. + +Separate: + +1. durable runtime events +2. ephemeral stream parts + +### Run-Level Events + +Suggested public stream model: + +```ts +type RunEmitterEvent = + | { type: "state"; snapshot: AgentState } + | { type: "machine.event"; event: PersistedRunEvent } + | { type: "part"; part: StreamPart } + | { type: "done"; output: unknown } + | { type: "error"; error: unknown }; +``` + +### Stream Parts + +For common model/tool streaming shapes, align with Vercel AI SDK-style part conventions where practical: + +```ts +type StreamPart = + | { type: "text-start"; id: string } + | { type: "text-delta"; id: string; delta: string } + | { type: "text-end"; id: string } + | { type: "tool-input-start"; toolCallId: string; toolName: string } + | { type: "tool-input-delta"; toolCallId: string; inputTextDelta: string } + | { type: "tool-input-available"; toolCallId: string; toolName: string; input: unknown } + | { type: "tool-output-available"; toolCallId: string; output: unknown } + | { type: "reasoning-part"; text: string } + | { type: "data"; data: unknown } + | { type: "error"; errorText: string }; +``` + +Provide convenience listeners on top: + +```ts +run.on("textPart", ({ delta }) => {}); +run.on("toolCall", ({ toolCallId, toolName, input }) => {}); +run.on("toolResult", ({ toolCallId, output }) => {}); +``` + +### Emission Model + +Invoke code should be able to emit live parts: + +```ts +drafting: { + invoke: async ({ emit }) => { + for await (const chunk of streamText(...)) { + emit({ type: "text-delta", id: "draft", delta: chunk }); + } + return { draft: finalText }; + }, +} +``` + +Durable runtime events are persisted. Stream parts are ephemeral by default in phase 1. + +## Runner-Agnostic Architecture + +The runtime must not assume: + +- long-lived Node processes +- a specific queue system +- a specific database +- process-local memory as truth + +The core should be split into: + +1. pure machine semantics +2. durable run orchestration +3. storage abstraction +4. environment-specific runner adapters + +This makes it possible to showcase: + +- standard Node process usage +- Vercel usage +- Cloudflare Worker usage +- Cloudflare Durable Object usage + +Durable Objects are especially relevant because they demonstrate the design clearly: + +- event log and snapshot persistence can live in DO state +- run coordination can be serialized naturally +- stream subscriptions can be implemented via the object lifecycle + +The important point is that Durable Objects should be an example adapter, not the core assumption. + +## Capability Mapping from LangGraph Core + +### Directly Mappable + +- graph orchestration -> explicit machine states and transitions +- shared state update workflows -> `invoke` + `onDone` context updates +- human-in-the-loop -> pending states + external events +- subgraphs/subflows -> nested machine execution +- streaming -> run-level event emitter + stream parts +- persistence/resume -> event log + snapshots +- prebuilt agent patterns -> curated machine factories + +### Needs Reinterpretation + +- reducers/channels -> avoid first-class graph-channel runtime semantics in phase 1 +- graph builder APIs -> do not mirror +- `START` / `END` constants -> unnecessary as authoring primitives +- explicit interrupt primitive -> defer + +### Deferred + +- graph-level true concurrent branch semantics with reducer joins +- durable sub-invoke task boundaries +- remote/API client compatibility +- type-level compatibility tests + +## LangGraph Test Port Strategy + +Only port: + +- runtime behavior tests +- runnable examples + +Do not port: + +- type-only tests +- API surface compatibility tests + +### Priority Test Groups + +1. Graph/state behavior + - `graph.test.ts` + - `errors.test.ts` + - `constants.test.ts` + +2. Execution/runtime behavior + - selected `pregel.test.ts` + - `pregel.read.test.ts` + - `pregel/stream.test.ts` + - `execution_info.test.ts` + +3. Persistence and replay + - `python_port/checkpoint.test.ts` + - `remote-graph-resumable.test.ts` + +4. Prebuilt agent behavior + - `prebuilt.test.ts` + - `prebuilt.int.test.ts` + +5. Runtime schema behavior + - relevant portions of `zod_state.test.ts` + +Each imported test should become an `agent`-idiomatic equivalent that asserts the same end-result behavior through the state-machine runtime. + +## Example Port Strategy + +Priority LangGraph-equivalent examples to rebuild in `agent`: + +1. quickstart +2. branching +3. wait-user-input / breakpoints +4. persistence +5. subgraph +6. tool-calling +7. create-react-agent / react-agent-from-scratch +8. multi-agent-network +9. plan-and-execute +10. reflection +11. rewoo +12. sql-agent + +Each example should: + +- use `agent`'s machine API +- be runnable locally +- demonstrate the same user outcome +- prefer explicit machine structure over graph-builder mimicry + +## Phased Delivery Plan + +### Phase 0: Lock the Core Contract + +Define: + +- durable run contract +- store interfaces +- restore/replay semantics +- stream event model + +### Phase 1: Durable Runtime + +Build: + +- run object +- event logging +- snapshotting +- restoration +- run subscriptions + +### Phase 2: Expressiveness + +Build: + +- better nested machine execution +- pending-state ergonomics +- inspection/trace support +- graph/diagram export + +### Phase 3: Prebuilt Patterns + +Build: + +- ReAct-style machine factory +- tool-calling helpers +- transcript/message helpers + +### Phase 4: Example Corpus + +Rebuild high-value LangGraph examples in `agent`. + +### Phase 5: Behavioral Regression Coverage + +Port and maintain semantic-equivalence tests grouped by capability family. + +## Risks + +1. Conflating transition logic with invoke execution. + This weakens replay semantics and makes portability worse. + +2. Over-promising invoke-level durability. + Plain async code is not automatically resumable at subtask granularity. + +3. Recreating LangGraph builder abstractions instead of improving on them. + This increases complexity without serving the machine-first philosophy. + +4. Mixing durable and ephemeral streams carelessly. + Runtime events and text/tool stream parts need distinct semantics. + +5. Allowing runner assumptions to leak into core. + This would compromise portability across Vercel, Cloudflare, and other environments. + +## Recommendation + +Proceed with a capability-first expansion of `agent`'s runtime: + +- keep the machine API central +- make durable runs the execution center +- treat event persistence and snapshots as first-class +- make streaming run-level and explicit +- port LangGraph tests/examples as semantic benchmarks + +This produces a cleaner, more durable, and more portable core than LangGraph while still reaching the same practical developer outcomes. From a3a2d7551879f819a6a9537f7ea38da0301e7ff1 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 8 Apr 2026 20:48:53 -0400 Subject: [PATCH 06/34] docs: refine langgraph replacement event model --- ...04-08-langgraph-core-replacement-design.md | 218 ++++++++++++++---- 1 file changed, 169 insertions(+), 49 deletions(-) diff --git a/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md b/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md index 29eeb94..0ff6aed 100644 --- a/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md +++ b/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md @@ -62,7 +62,7 @@ A machine definition remains declarative and mostly pure: A run becomes the primary runtime object: -- backed by an append-only event log +- backed by an append-only replay journal - accelerated by persisted snapshots - observable through a first-class event stream - resumable from persisted state @@ -72,7 +72,7 @@ This yields a model where the semantics are simple: - transitions are deterministic - invokes are explicit effect boundaries -- external events drive progress +- external and internal machine events drive progress - streaming is run-level, not bolted on - persistence is a core contract @@ -100,15 +100,19 @@ The machine should continue to express workflows such as: ### Run Model -Introduce a durable run as the central execution concept. +Introduce a durable session as the central execution concept. + +`sessionId` should be the canonical persisted identifier. + +`run` can still be a useful public term for the live handle returned by the runtime, but the durable identity should align with actor/session terminology. Each run has: -- `runId` +- `sessionId` - `machineId` - input payload - current snapshot -- append-only event history +- append-only replay journal - status - subscribers @@ -116,9 +120,9 @@ Suggested shape: ```ts interface AgentRun { - id: string; + sessionId: string; status: "active" | "pending" | "done" | "error"; - getSnapshot(): AgentState; + getSnapshot(): AgentSnapshot; send(event: { type: string; [key: string]: unknown }): Promise; on(type: string, handler: (event: unknown) => void): () => void; [Symbol.asyncIterator](): AsyncIterator; @@ -129,17 +133,12 @@ interface AgentRun { Phase 1 durability should exist at machine boundaries, not inside arbitrary user async code. -Persist: +Persist the replayable machine events: -- run start -- external event receipt -- state entry -- invoke start -- invoke success -- invoke failure -- transition application -- run completion -- run failure +- external events sent to the actor +- internal machine events emitted by the runtime +- invoke completion events +- invoke failure events Do not claim sub-invoke durability for plain `Promise.all(...)` or arbitrary nested promises. @@ -206,57 +205,106 @@ In practice, current `agent` APIs already combine these concerns inside state co That means: - transition logic should remain replayable without rerunning effects -- effect lifecycle should be represented in runtime events +- effect lifecycle should be represented through emitted machine events - invoke results should be fed back as events, not hidden mutations +This should follow the same philosophy as XState invoke completion: + +- invoke completion becomes an internal done event +- invoke failure becomes an internal error event +- the machine progresses by consuming events, not by direct mutation from effect code + This is the main improvement opportunity over LangGraph's more graph-runtime-centric model. ## Persistence Model -The canonical persisted representation is an append-only event log. +The canonical persisted representation is an append-only replay journal. Snapshots are derived state used to accelerate replay and resume. -### Event Log +### Replay Journal + +The replay journal is the source of truth. It contains the actual events consumed by the actor, including synthetic internal events produced by the runtime. -Suggested minimal durable event family: +Suggested minimal replayable event family: ```ts -type PersistedRunEvent = - | { type: "run.started"; input: unknown; at: number } - | { type: "event.received"; event: { type: string; [k: string]: unknown }; at: number } - | { type: "state.entered"; value: string; params?: Record; at: number } - | { type: "invoke.started"; state: string; at: number } - | { type: "invoke.succeeded"; state: string; result: unknown; at: number } - | { type: "invoke.failed"; state: string; error: SerializedError; at: number } - | { type: "transition.applied"; from: string; to: string; at: number } - | { type: "run.completed"; output: unknown; at: number } - | { type: "run.failed"; error: SerializedError; at: number }; +type JournalEvent = + | { type: "xstate.init"; input?: unknown; at: number } + | { type: "user.message"; [key: string]: unknown; at: number } + | { type: "approve"; at: number } + | { type: "xstate.done.invoke.research"; output: unknown; at: number } + | { + type: "xstate.error.invoke.research"; + error: SerializedError; + at: number; + }; ``` +The exact event naming can be refined, but the important property is that invoke done/error are actor events, not metadata records. + +### Runtime and Audit Events + +Derived runtime records can still exist for observability and subscriptions, but they are not the canonical replay source. + +Examples: + +- state entered +- transition applied +- snapshot persisted +- session completed +- session failed + +These belong in the runtime event stream and diagnostics layer. + ### Snapshots Suggested snapshot shape: ```ts +type AgentSnapshot = { + value: string; + context: Record; + status: "active" | "done" | "error" | "pending"; + createdAt: number; + sessionId: string; + output?: unknown; + error?: SerializedError; +}; + type PersistedSnapshot = { - runId: string; - version: number; - state: AgentState; - lastEventIndex: number; + sessionId: string; + sequence: number; + snapshot: AgentSnapshot; + lastJournalIndex: number; createdAt: number; }; ``` +This aligns the live snapshot shape closely with XState snapshots: + +- `value` +- `context` +- `status` + +with additional metadata such as: + +- `createdAt` +- `sessionId` +- optional `output` +- optional `error` + +The `sequence` field exists so storage can identify which snapshot is the latest persisted derivation and so replay can resume from a known journal offset. It should track journal position rather than inventing a separate semantic version. + ### Replay Model Restore a run by: 1. loading the latest snapshot -2. replaying all events after that snapshot +2. replaying all journal events after that snapshot 3. reconstructing the current live run state -If no snapshot exists, replay from `run.started`. +If no snapshot exists, replay from `xstate.init`. ### Storage Interface @@ -264,9 +312,9 @@ Persistence must be abstracted behind a portable interface: ```ts interface RunStore { - append(runId: string, event: PersistedRunEvent): Promise; - loadEvents(runId: string, afterVersion?: number): Promise; - loadLatestSnapshot(runId: string): Promise; + append(sessionId: string, event: JournalEvent): Promise; + loadEvents(sessionId: string, afterSequence?: number): Promise; + loadLatestSnapshot(sessionId: string): Promise; saveSnapshot(snapshot: PersistedSnapshot): Promise; } ``` @@ -304,13 +352,27 @@ Suggested public stream model: ```ts type RunEmitterEvent = - | { type: "state"; snapshot: AgentState } - | { type: "machine.event"; event: PersistedRunEvent } + | { type: "state"; snapshot: AgentSnapshot } + | { type: "machine.event"; event: JournalEvent } + | { type: "runtime"; event: RuntimeEvent } | { type: "part"; part: StreamPart } | { type: "done"; output: unknown } | { type: "error"; error: unknown }; ``` +Where `machine.event` refers to replayable actor events and `runtime` refers to derived lifecycle records useful for debugging and orchestration. + +Suggested runtime event family: + +```ts +type RuntimeEvent = + | { type: "state.entered"; value: string; at: number } + | { type: "transition.applied"; from: string; to: string; at: number } + | { type: "snapshot.saved"; sessionId: string; sequence: number; at: number } + | { type: "session.completed"; sessionId: string; at: number } + | { type: "session.failed"; sessionId: string; error: SerializedError; at: number }; +``` + ### Stream Parts For common model/tool streaming shapes, align with Vercel AI SDK-style part conventions where practical: @@ -339,13 +401,13 @@ run.on("toolResult", ({ toolCallId, output }) => {}); ### Emission Model -Invoke code should be able to emit live parts: +Invoke code should be able to emit live parts using a separate enqueue/emission argument: ```ts drafting: { - invoke: async ({ emit }) => { + invoke: async ({ context }, enq) => { for await (const chunk of streamText(...)) { - emit({ type: "text-delta", id: "draft", delta: chunk }); + enq.emit({ type: "text-delta", id: "draft", delta: chunk }); } return { draft: finalText }; }, @@ -354,6 +416,39 @@ drafting: { Durable runtime events are persisted. Stream parts are ephemeral by default in phase 1. +Using a second argument is important because it preserves a useful authoring distinction: + +- one-argument functions are easier to lint as pure/no-emission +- two-argument functions explicitly opt into streaming side effects + +### Emitted Schemas + +Machine definitions should support emitted event schemas alongside input and external event schemas. + +Suggested direction: + +```ts +schemas: { + input: ..., + events: { + approve: ..., + reject: ..., + }, + emitted: { + textPart: ..., + toolCall: ..., + toolResult: ..., + }, +} +``` + +This gives: + +- typed live emissions +- runtime validation of emitted parts +- stronger UI integration +- symmetry with event schemas + ## Runner-Agnostic Architecture The runtime must not assume: @@ -379,7 +474,7 @@ This makes it possible to showcase: Durable Objects are especially relevant because they demonstrate the design clearly: -- event log and snapshot persistence can live in DO state +- replay journal and snapshot persistence can live in DO state - run coordination can be serialized naturally - stream subscriptions can be implemented via the object lifecycle @@ -393,8 +488,8 @@ The important point is that Durable Objects should be an example adapter, not th - shared state update workflows -> `invoke` + `onDone` context updates - human-in-the-loop -> pending states + external events - subgraphs/subflows -> nested machine execution -- streaming -> run-level event emitter + stream parts -- persistence/resume -> event log + snapshots +- streaming -> run-level event emitter + stream parts + emitted schemas +- persistence/resume -> event journal + snapshots - prebuilt agent patterns -> curated machine factories ### Needs Reinterpretation @@ -489,7 +584,7 @@ Define: Build: - run object -- event logging +- journal append/load - snapshotting - restoration - run subscriptions @@ -536,6 +631,31 @@ Port and maintain semantic-equivalence tests grouped by capability family. 5. Allowing runner assumptions to leak into core. This would compromise portability across Vercel, Cloudflare, and other environments. +## Advantages Over LangGraph + +This design improves on LangGraph core in several important ways: + +1. Clearer semantic center. + LangGraph is graph-runtime-first. This design is actor/state-machine-first, so the progression model stays grounded in event consumption and snapshot derivation. + +2. Better purity boundary. + Transition logic remains conceptually pure, while effect execution is explicit and first-class rather than interwoven with graph runtime semantics. + +3. Simpler human-in-the-loop model. + Pending states plus external events are easier to reason about than a dedicated interrupt abstraction for most workflows. + +4. More honest durability. + The replay source is the actor event journal, not a mixed bag of runtime metadata. This makes replay and debugging cleaner. + +5. Better portability. + The runtime is explicitly designed to be runner-agnostic and storage-agnostic, making it a stronger fit for Vercel, Cloudflare Workers, Durable Objects, and other environments. + +6. Easier mental model for composition. + Nested machine execution is ordinary execution, not a special graph/subgraph system. + +7. Better streaming ergonomics. + Run-level subscriptions plus emitted schemas provide a clearer UI/runtime boundary than LangGraph's graph-oriented stream modes. + ## Recommendation Proceed with a capability-first expansion of `agent`'s runtime: From d1842e2d414db765b184f0b63eeed7128d7d45bb Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 9 Apr 2026 05:48:11 -0400 Subject: [PATCH 07/34] feat: add durable session store foundation --- src/index.ts | 4 ++ src/persistence.test.ts | 84 +++++++++++++++++++++++++++++++++++++ src/runtime/events.ts | 7 ++++ src/runtime/memory-store.ts | 51 ++++++++++++++++++++++ src/runtime/store.ts | 22 ++++++++++ src/session-types.test.ts | 27 ++++++++++++ src/types.ts | 28 +++++++++---- 7 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 src/persistence.test.ts create mode 100644 src/runtime/events.ts create mode 100644 src/runtime/memory-store.ts create mode 100644 src/runtime/store.ts create mode 100644 src/session-types.test.ts diff --git a/src/index.ts b/src/index.ts index 5868e9e..a422e2f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ export { classify } from './classify.js'; // Adapter export { createAdapter } from './adapter.js'; +export { createMemoryRunStore } from './runtime/memory-store.js'; // Types export type { @@ -21,7 +22,10 @@ export type { EventUnion, ExecuteResult, InferOutput, + JournalEvent, MachineConfig, + PersistedSnapshot, + RunStore, StandardSchemaV1, StateConfig, Trace, diff --git a/src/persistence.test.ts b/src/persistence.test.ts new file mode 100644 index 0000000..d4ab086 --- /dev/null +++ b/src/persistence.test.ts @@ -0,0 +1,84 @@ +import { expect, test } from 'vitest'; +import { createMemoryRunStore } from './index.js'; + +test('appends and loads journal events in sequence order', async () => { + const store = createMemoryRunStore(); + + await store.append('session-1', [ + { + sessionId: 'session-1', + sequence: 2, + type: 'xstate.done.invoke.worker', + at: 20, + }, + { + sessionId: 'session-1', + sequence: 1, + type: 'xstate.init', + at: 10, + }, + ]); + + expect(await store.loadEvents('session-1')).toEqual([ + { + sessionId: 'session-1', + sequence: 1, + type: 'xstate.init', + at: 10, + }, + { + sessionId: 'session-1', + sequence: 2, + type: 'xstate.done.invoke.worker', + at: 20, + }, + ]); +}); + +test('loads the latest saved snapshot', async () => { + const store = createMemoryRunStore(); + + await store.saveSnapshot({ + sessionId: 'session-1', + sequence: 1, + snapshot: { + value: 'idle', + context: { count: 1 }, + status: 'active', + createdAt: 100, + sessionId: 'session-1', + }, + lastJournalIndex: 1, + createdAt: 100, + }); + + await store.saveSnapshot({ + sessionId: 'session-1', + sequence: 3, + snapshot: { + value: 'done', + context: { count: 2 }, + status: 'done', + createdAt: 300, + sessionId: 'session-1', + output: { count: 2 }, + }, + lastJournalIndex: 3, + createdAt: 300, + }); + + expect(await store.loadLatestSnapshot('session-1')).toEqual({ + sessionId: 'session-1', + sequence: 3, + snapshot: { + value: 'done', + context: { count: 2 }, + status: 'done', + createdAt: 300, + sessionId: 'session-1', + output: { count: 2 }, + }, + lastJournalIndex: 3, + createdAt: 300, + }); +}); diff --git a/src/runtime/events.ts b/src/runtime/events.ts new file mode 100644 index 0000000..d0a864c --- /dev/null +++ b/src/runtime/events.ts @@ -0,0 +1,7 @@ +export interface JournalEvent { + sessionId: string; + sequence: number; + type: 'xstate.init' | (string & {}); + at: number; + [key: string]: unknown; +} diff --git a/src/runtime/memory-store.ts b/src/runtime/memory-store.ts new file mode 100644 index 0000000..0888ee2 --- /dev/null +++ b/src/runtime/memory-store.ts @@ -0,0 +1,51 @@ +import type { AgentSnapshot } from '../types.js'; +import type { JournalEvent } from './events.js'; +import type { PersistedSnapshot, RunStore } from './store.js'; + +function compareEvents(a: JournalEvent, b: JournalEvent): number { + return a.sequence - b.sequence || a.at - b.at; +} + +function compareSnapshots( + a: PersistedSnapshot, + b: PersistedSnapshot +): number { + return a.sequence - b.sequence || a.createdAt - b.createdAt; +} + +export function createMemoryRunStore< + TSnapshot extends AgentSnapshot = AgentSnapshot, + TEvent extends JournalEvent = JournalEvent, +>(): RunStore { + const journals = new Map(); + const snapshots = new Map[]>(); + + return { + async append(sessionId, events) { + const current = journals.get(sessionId) ?? []; + current.push(...events); + journals.set(sessionId, current); + }, + + async loadEvents(sessionId) { + const events = journals.get(sessionId) ?? []; + return [...events].sort(compareEvents) as TEvent[]; + }, + + async loadLatestSnapshot(sessionId) { + const saved = snapshots.get(sessionId); + if (!saved?.length) { + return null; + } + + const sorted = [...saved].sort(compareSnapshots); + return sorted[sorted.length - 1] ?? null; + }, + + async saveSnapshot(snapshot) { + const current = snapshots.get(snapshot.sessionId) ?? []; + current.push(snapshot); + snapshots.set(snapshot.sessionId, current); + }, + }; +} diff --git a/src/runtime/store.ts b/src/runtime/store.ts new file mode 100644 index 0000000..98089cb --- /dev/null +++ b/src/runtime/store.ts @@ -0,0 +1,22 @@ +import type { AgentSnapshot } from '../types.js'; +import type { JournalEvent } from './events.js'; + +export interface PersistedSnapshot< + TSnapshot extends AgentSnapshot = AgentSnapshot, +> { + sessionId: string; + sequence: number; + snapshot: TSnapshot; + lastJournalIndex: number; + createdAt: number; +} + +export interface RunStore< + TSnapshot extends AgentSnapshot = AgentSnapshot, + TEvent extends JournalEvent = JournalEvent, +> { + append(sessionId: string, events: TEvent[]): Promise; + loadEvents(sessionId: string): Promise; + loadLatestSnapshot(sessionId: string): Promise | null>; + saveSnapshot(snapshot: PersistedSnapshot): Promise; +} diff --git a/src/session-types.test.ts b/src/session-types.test.ts new file mode 100644 index 0000000..35b4e19 --- /dev/null +++ b/src/session-types.test.ts @@ -0,0 +1,27 @@ +import { expect, test } from 'vitest'; +import type { AgentSnapshot, JournalEvent } from './index.js'; + +test('AgentSnapshot includes durable session fields', () => { + const snapshot: AgentSnapshot<{ count: number }, 'idle'> = { + value: 'idle', + context: { count: 1 }, + status: 'active', + createdAt: 123, + sessionId: 'session-1', + }; + + expect(snapshot.sessionId).toBe('session-1'); + expect(snapshot.createdAt).toBe(123); +}); + +test('JournalEvent supports invoke completion events', () => { + const event: JournalEvent = { + sessionId: 'session-1', + sequence: 2, + type: 'xstate.done.invoke.worker', + at: 456, + }; + + expect(event.type).toBe('xstate.done.invoke.worker'); + expect(event.at).toBe(456); +}); diff --git a/src/types.ts b/src/types.ts index 6ba580d..7d4038f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,6 +29,10 @@ export type TransitionEvent< ? { type: string; [key: string]: unknown } : EventUnion; +// ─── Durable Session Vocabulary ─── + +export type { JournalEvent } from './runtime/events.js'; + // ─── Adapter ─── export interface AgentAdapter { @@ -115,9 +119,15 @@ export interface AgentSnapshot< value: TValue; context: TContext; status: AgentState['status']; - params: Record>; + createdAt?: number; + sessionId?: string; + output?: unknown; + error?: unknown; + params?: Record>; } +export type { PersistedSnapshot, RunStore } from './runtime/store.js'; + // ─── Agent Machine ─── export interface AgentMachine< @@ -194,29 +204,33 @@ export type DecideResultFor< }[keyof TOptions & string]; export interface DecideConfig< + TContext extends Record = Record, + TParams extends Record = Record, TOptions extends Record = Record, > { model: string; adapter?: AgentAdapter; - prompt: string | ((args: { context: Record; params: Record }) => string); + prompt: string | ((args: { context: TContext; params: TParams }) => string); options: TOptions; reasoning?: boolean; - onDone: (args: { result: DecideResultFor; context: Record }) => TransitionResult; - on?: Record }) => TransitionResult>; + onDone: (args: { result: DecideResultFor; context: TContext }) => TransitionResult; + on?: Record TransitionResult>; } // ─── Classify ─── export interface ClassifyConfig< + TContext extends Record = Record, + TParams extends Record = Record, TCategories extends Record = Record, > { model: string; adapter?: AgentAdapter; - prompt: string | ((args: { context: Record; params: Record }) => string); + prompt: string | ((args: { context: TContext; params: TParams }) => string); into: TCategories; examples?: Array<{ input: string; category: keyof TCategories & string }>; - onDone: (args: { result: { category: keyof TCategories & string }; context: Record }) => TransitionResult; - on?: Record }) => TransitionResult>; + onDone: (args: { result: { category: keyof TCategories & string }; context: TContext }) => TransitionResult; + on?: Record TransitionResult>; } // ─── Trace ─── From 3c85ab97c0fcd2312ae4d64abfff7e70177591bc Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 9 Apr 2026 05:51:41 -0400 Subject: [PATCH 08/34] fix: align durable snapshot contract --- src/persistence.test.ts | 27 +++++++++++++-------------- src/runtime/memory-store.ts | 4 ++-- src/runtime/store.ts | 2 +- src/types.ts | 5 ++--- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/persistence.test.ts b/src/persistence.test.ts index d4ab086..d4ba8ec 100644 --- a/src/persistence.test.ts +++ b/src/persistence.test.ts @@ -4,20 +4,19 @@ import { createMemoryRunStore } from './index.js'; test('appends and loads journal events in sequence order', async () => { const store = createMemoryRunStore(); - await store.append('session-1', [ - { - sessionId: 'session-1', - sequence: 2, - type: 'xstate.done.invoke.worker', - at: 20, - }, - { - sessionId: 'session-1', - sequence: 1, - type: 'xstate.init', - at: 10, - }, - ]); + await store.append('session-1', { + sessionId: 'session-1', + sequence: 2, + type: 'xstate.done.invoke.worker', + at: 20, + }); + + await store.append('session-1', { + sessionId: 'session-1', + sequence: 1, + type: 'xstate.init', + at: 10, + }); expect(await store.loadEvents('session-1')).toEqual([ { diff --git a/src/runtime/memory-store.ts b/src/runtime/memory-store.ts index 0888ee2..ee77053 100644 --- a/src/runtime/memory-store.ts +++ b/src/runtime/memory-store.ts @@ -21,9 +21,9 @@ export function createMemoryRunStore< const snapshots = new Map[]>(); return { - async append(sessionId, events) { + async append(sessionId, event) { const current = journals.get(sessionId) ?? []; - current.push(...events); + current.push(event); journals.set(sessionId, current); }, diff --git a/src/runtime/store.ts b/src/runtime/store.ts index 98089cb..297bd05 100644 --- a/src/runtime/store.ts +++ b/src/runtime/store.ts @@ -15,7 +15,7 @@ export interface RunStore< TSnapshot extends AgentSnapshot = AgentSnapshot, TEvent extends JournalEvent = JournalEvent, > { - append(sessionId: string, events: TEvent[]): Promise; + append(sessionId: string, event: TEvent): Promise; loadEvents(sessionId: string): Promise; loadLatestSnapshot(sessionId: string): Promise | null>; saveSnapshot(snapshot: PersistedSnapshot): Promise; diff --git a/src/types.ts b/src/types.ts index 7d4038f..f274f16 100644 --- a/src/types.ts +++ b/src/types.ts @@ -119,11 +119,10 @@ export interface AgentSnapshot< value: TValue; context: TContext; status: AgentState['status']; - createdAt?: number; - sessionId?: string; + createdAt: number; + sessionId: string; output?: unknown; error?: unknown; - params?: Record>; } export type { PersistedSnapshot, RunStore } from './runtime/store.js'; From 50b382bb47fe23be9fe9366806c85db840317f67 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 9 Apr 2026 05:56:18 -0400 Subject: [PATCH 09/34] fix: emit durable stream snapshots --- src/machine.ts | 5 ++++- src/stream-snapshot.test.ts | 43 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/stream-snapshot.test.ts diff --git a/src/machine.ts b/src/machine.ts index 9f6f7df..09e2482 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -371,7 +371,10 @@ export function createAgentMachine( value: s.value, context: s.context, status: s.status, - params: s.params, + sessionId: cfg.id, + createdAt: Date.now(), + output: s.output, + error: s.error, }; } diff --git a/src/stream-snapshot.test.ts b/src/stream-snapshot.test.ts new file mode 100644 index 0000000..233fb5f --- /dev/null +++ b/src/stream-snapshot.test.ts @@ -0,0 +1,43 @@ +import { expect, test } from 'vitest'; +import { createAgentMachine } from './index.js'; + +test('stream emits durable snapshots with session metadata', async () => { + const machine = createAgentMachine({ + id: 'snapshot-machine', + context: () => ({}), + initial: 'done', + states: { + done: { + type: 'final', + output: () => ({ ok: true }), + }, + }, + }); + + const snaps = []; + for await (const snap of machine.stream(machine.getInitialState())) { + snaps.push(snap); + } + + expect(snaps.length).toBeGreaterThanOrEqual(2); + expect(snaps[0]).toEqual( + expect.objectContaining({ + sessionId: 'snapshot-machine', + createdAt: expect.any(Number), + value: 'done', + context: {}, + status: 'active', + }) + ); + expect(snaps[0]).not.toHaveProperty('params'); + expect(snaps[snaps.length - 1]).toEqual( + expect.objectContaining({ + sessionId: 'snapshot-machine', + createdAt: expect.any(Number), + value: 'done', + context: {}, + status: 'done', + output: { ok: true }, + }) + ); +}); From 18057e6d3e2ec60fc7f2fac916f6ccc17a510f2f Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 9 Apr 2026 06:00:25 -0400 Subject: [PATCH 10/34] fix: key run store by event session --- src/persistence.test.ts | 4 ++-- src/runtime/memory-store.ts | 6 +++--- src/runtime/store.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/persistence.test.ts b/src/persistence.test.ts index d4ba8ec..3bc7ecf 100644 --- a/src/persistence.test.ts +++ b/src/persistence.test.ts @@ -4,14 +4,14 @@ import { createMemoryRunStore } from './index.js'; test('appends and loads journal events in sequence order', async () => { const store = createMemoryRunStore(); - await store.append('session-1', { + await store.append({ sessionId: 'session-1', sequence: 2, type: 'xstate.done.invoke.worker', at: 20, }); - await store.append('session-1', { + await store.append({ sessionId: 'session-1', sequence: 1, type: 'xstate.init', diff --git a/src/runtime/memory-store.ts b/src/runtime/memory-store.ts index ee77053..70db402 100644 --- a/src/runtime/memory-store.ts +++ b/src/runtime/memory-store.ts @@ -21,10 +21,10 @@ export function createMemoryRunStore< const snapshots = new Map[]>(); return { - async append(sessionId, event) { - const current = journals.get(sessionId) ?? []; + async append(event) { + const current = journals.get(event.sessionId) ?? []; current.push(event); - journals.set(sessionId, current); + journals.set(event.sessionId, current); }, async loadEvents(sessionId) { diff --git a/src/runtime/store.ts b/src/runtime/store.ts index 297bd05..67acede 100644 --- a/src/runtime/store.ts +++ b/src/runtime/store.ts @@ -15,7 +15,7 @@ export interface RunStore< TSnapshot extends AgentSnapshot = AgentSnapshot, TEvent extends JournalEvent = JournalEvent, > { - append(sessionId: string, event: TEvent): Promise; + append(event: TEvent): Promise; loadEvents(sessionId: string): Promise; loadLatestSnapshot(sessionId: string): Promise | null>; saveSnapshot(snapshot: PersistedSnapshot): Promise; From 07c06bcfcca941174a38c6dbd71a94f2682aa8cd Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 9 Apr 2026 06:11:21 -0400 Subject: [PATCH 11/34] fix: stabilize stream snapshot runtime --- src/machine.ts | 69 +++++++++++++++++++++++++++++++++---- src/persistence.test.ts | 25 +++++++------- src/runtime/events.ts | 4 +-- src/runtime/memory-store.ts | 24 +++++++------ src/runtime/store.ts | 4 +-- src/session-types.test.ts | 2 -- src/stream-snapshot.test.ts | 23 ++++++++++--- 7 files changed, 112 insertions(+), 39 deletions(-) diff --git a/src/machine.ts b/src/machine.ts index 09e2482..74f3cd0 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -101,6 +101,31 @@ export function createAgentMachine< }): AgentMachine>; // ─── Overload B: no schemas.context ─── +export function createAgentMachine< + TInput, + TContext extends Record, + const TEvents extends Record, + const TParamsMap extends Record, + TResultMap extends Record, +>(config: { + id: string; + schemas: { + input: StandardSchemaV1; + context?: never; + events?: TEvents; + }; + context: (input: NoInfer) => TContext; + adapter?: import('./types.js').AgentAdapter; + initial: + | (keyof TParamsMap & keyof TResultMap & string) + | ((args: { context: TContext }) => { + target: keyof TParamsMap & keyof TResultMap & string; + params?: Record; + }); + states: StatesMap; +}): AgentMachine>; + +// ─── Overload C: no schemas.input or schemas.context ─── export function createAgentMachine< TContext extends Record, const TEvents extends Record, @@ -109,7 +134,7 @@ export function createAgentMachine< >(config: { id: string; schemas?: { - input?: StandardSchemaV1; + input?: never; context?: never; events?: TEvents; }; @@ -130,6 +155,33 @@ export function createAgentMachine( machineConfig: MachineConfig ): AgentMachine { const cfg = machineConfig as MachineConfig; + const snapshotRuntimeByState = new WeakMap(); + + function createSnapshotRuntime() { + const sessionId = + typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' + ? crypto.randomUUID() + : `session-${Math.random().toString(36).slice(2)}`; + + return { + sessionId, + createdAt: Date.now(), + }; + } + + function getSnapshotRuntime(state: AgentState) { + let runtime = snapshotRuntimeByState.get(state); + if (!runtime) { + runtime = createSnapshotRuntime(); + snapshotRuntimeByState.set(state, runtime); + } + return runtime; + } + + function bindSnapshotRuntime(state: AgentState, runtime: { sessionId: string; createdAt: number }) { + snapshotRuntimeByState.set(state, runtime); + return state; + } function getInitialState(...args: [input?: unknown]): AgentState { const input = args[0]; @@ -359,20 +411,25 @@ export function createAgentMachine( state: AgentState ): AsyncGenerator { let current = state; - yield toSnap(current); + const runtime = getSnapshotRuntime(current); + yield toSnap(current, runtime); while (current.status === 'active') { current = await invoke(current); - yield toSnap(current); + bindSnapshotRuntime(current, runtime); + yield toSnap(current, runtime); } } - function toSnap(s: AgentState): AgentSnapshot { + function toSnap( + s: AgentState, + runtime = getSnapshotRuntime(s) + ): AgentSnapshot { return { value: s.value, context: s.context, status: s.status, - sessionId: cfg.id, - createdAt: Date.now(), + sessionId: runtime.sessionId, + createdAt: runtime.createdAt, output: s.output, error: s.error, }; diff --git a/src/persistence.test.ts b/src/persistence.test.ts index 3bc7ecf..b9584a9 100644 --- a/src/persistence.test.ts +++ b/src/persistence.test.ts @@ -4,32 +4,31 @@ import { createMemoryRunStore } from './index.js'; test('appends and loads journal events in sequence order', async () => { const store = createMemoryRunStore(); - await store.append({ - sessionId: 'session-1', - sequence: 2, + await store.append('session-1', { type: 'xstate.done.invoke.worker', at: 20, }); - await store.append({ - sessionId: 'session-1', - sequence: 1, + await store.append('session-1', { type: 'xstate.init', at: 10, }); expect(await store.loadEvents('session-1')).toEqual([ { - sessionId: 'session-1', - sequence: 1, - type: 'xstate.init', + at: 20, + type: 'xstate.done.invoke.worker', + }, + { at: 10, + type: 'xstate.init', }, + ]); + + expect(await store.loadEvents('session-1', 1)).toEqual([ { - sessionId: 'session-1', - sequence: 2, - type: 'xstate.done.invoke.worker', - at: 20, + at: 10, + type: 'xstate.init', }, ]); }); diff --git a/src/runtime/events.ts b/src/runtime/events.ts index d0a864c..ee30061 100644 --- a/src/runtime/events.ts +++ b/src/runtime/events.ts @@ -1,7 +1,7 @@ export interface JournalEvent { - sessionId: string; - sequence: number; type: 'xstate.init' | (string & {}); at: number; + sessionId?: string; + sequence?: number; [key: string]: unknown; } diff --git a/src/runtime/memory-store.ts b/src/runtime/memory-store.ts index 70db402..0f1f60b 100644 --- a/src/runtime/memory-store.ts +++ b/src/runtime/memory-store.ts @@ -2,9 +2,10 @@ import type { AgentSnapshot } from '../types.js'; import type { JournalEvent } from './events.js'; import type { PersistedSnapshot, RunStore } from './store.js'; -function compareEvents(a: JournalEvent, b: JournalEvent): number { - return a.sequence - b.sequence || a.at - b.at; -} +type StoredJournalEvent = { + sequence: number; + event: TEvent; +}; function compareSnapshots( a: PersistedSnapshot, @@ -17,19 +18,22 @@ export function createMemoryRunStore< TSnapshot extends AgentSnapshot = AgentSnapshot, TEvent extends JournalEvent = JournalEvent, >(): RunStore { - const journals = new Map(); + const journals = new Map>>(); const snapshots = new Map[]>(); return { - async append(event) { - const current = journals.get(event.sessionId) ?? []; - current.push(event); - journals.set(event.sessionId, current); + async append(sessionId, event) { + const current = journals.get(sessionId) ?? []; + const sequence = current.length === 0 ? 1 : current[current.length - 1]!.sequence + 1; + current.push({ sequence, event }); + journals.set(sessionId, current); }, - async loadEvents(sessionId) { + async loadEvents(sessionId, afterSequence = 0) { const events = journals.get(sessionId) ?? []; - return [...events].sort(compareEvents) as TEvent[]; + return events + .filter((entry) => entry.sequence > afterSequence) + .map((entry) => entry.event); }, async loadLatestSnapshot(sessionId) { diff --git a/src/runtime/store.ts b/src/runtime/store.ts index 67acede..ddd841e 100644 --- a/src/runtime/store.ts +++ b/src/runtime/store.ts @@ -15,8 +15,8 @@ export interface RunStore< TSnapshot extends AgentSnapshot = AgentSnapshot, TEvent extends JournalEvent = JournalEvent, > { - append(event: TEvent): Promise; - loadEvents(sessionId: string): Promise; + append(sessionId: string, event: TEvent): Promise; + loadEvents(sessionId: string, afterSequence?: number): Promise; loadLatestSnapshot(sessionId: string): Promise | null>; saveSnapshot(snapshot: PersistedSnapshot): Promise; } diff --git a/src/session-types.test.ts b/src/session-types.test.ts index 35b4e19..821721d 100644 --- a/src/session-types.test.ts +++ b/src/session-types.test.ts @@ -16,8 +16,6 @@ test('AgentSnapshot includes durable session fields', () => { test('JournalEvent supports invoke completion events', () => { const event: JournalEvent = { - sessionId: 'session-1', - sequence: 2, type: 'xstate.done.invoke.worker', at: 456, }; diff --git a/src/stream-snapshot.test.ts b/src/stream-snapshot.test.ts index 233fb5f..34fbd66 100644 --- a/src/stream-snapshot.test.ts +++ b/src/stream-snapshot.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'vitest'; import { createAgentMachine } from './index.js'; -test('stream emits durable snapshots with session metadata', async () => { +async function collectSnapshots() { const machine = createAgentMachine({ id: 'snapshot-machine', context: () => ({}), @@ -19,10 +19,18 @@ test('stream emits durable snapshots with session metadata', async () => { snaps.push(snap); } + return snaps; +} + +test('stream emits durable snapshots with stable session metadata', async () => { + const snaps = await collectSnapshots(); + expect(snaps.length).toBeGreaterThanOrEqual(2); + expect(new Set(snaps.map((snap) => snap.sessionId)).size).toBe(1); + expect(new Set(snaps.map((snap) => snap.createdAt)).size).toBe(1); expect(snaps[0]).toEqual( expect.objectContaining({ - sessionId: 'snapshot-machine', + sessionId: expect.any(String), createdAt: expect.any(Number), value: 'done', context: {}, @@ -32,8 +40,8 @@ test('stream emits durable snapshots with session metadata', async () => { expect(snaps[0]).not.toHaveProperty('params'); expect(snaps[snaps.length - 1]).toEqual( expect.objectContaining({ - sessionId: 'snapshot-machine', - createdAt: expect.any(Number), + sessionId: snaps[0]!.sessionId, + createdAt: snaps[0]!.createdAt, value: 'done', context: {}, status: 'done', @@ -41,3 +49,10 @@ test('stream emits durable snapshots with session metadata', async () => { }) ); }); + +test('separate machine executions get distinct session ids', async () => { + const firstRun = await collectSnapshots(); + const secondRun = await collectSnapshots(); + + expect(firstRun[0]!.sessionId).not.toBe(secondRun[0]!.sessionId); +}); From d659e947c8429325322e02e14a16c94f6d46d8af Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 9 Apr 2026 06:29:09 -0400 Subject: [PATCH 12/34] fix: harden stream and replay metadata --- src/index.ts | 1 + src/machine.ts | 28 +++++++--------------------- src/persistence.test.ts | 16 ++++++++++++++-- src/runtime/memory-store.ts | 19 ++++++++----------- src/runtime/store.ts | 10 +++++++++- src/stream-snapshot.test.ts | 30 ++++++++++++++++-------------- src/types.ts | 3 +-- 7 files changed, 56 insertions(+), 51 deletions(-) diff --git a/src/index.ts b/src/index.ts index a422e2f..6ad8a53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ export type { ExecuteResult, InferOutput, JournalEvent, + JournalEventRecord, MachineConfig, PersistedSnapshot, RunStore, diff --git a/src/machine.ts b/src/machine.ts index 74f3cd0..b80479a 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -155,34 +155,21 @@ export function createAgentMachine( machineConfig: MachineConfig ): AgentMachine { const cfg = machineConfig as MachineConfig; - const snapshotRuntimeByState = new WeakMap(); + let snapshotRunIndex = 0; function createSnapshotRuntime() { const sessionId = - typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' - ? crypto.randomUUID() + typeof globalThis.crypto !== 'undefined' && + typeof globalThis.crypto.randomUUID === 'function' + ? globalThis.crypto.randomUUID() : `session-${Math.random().toString(36).slice(2)}`; return { sessionId, - createdAt: Date.now(), + createdAt: Date.now() + snapshotRunIndex++, }; } - function getSnapshotRuntime(state: AgentState) { - let runtime = snapshotRuntimeByState.get(state); - if (!runtime) { - runtime = createSnapshotRuntime(); - snapshotRuntimeByState.set(state, runtime); - } - return runtime; - } - - function bindSnapshotRuntime(state: AgentState, runtime: { sessionId: string; createdAt: number }) { - snapshotRuntimeByState.set(state, runtime); - return state; - } - function getInitialState(...args: [input?: unknown]): AgentState { const input = args[0]; @@ -411,18 +398,17 @@ export function createAgentMachine( state: AgentState ): AsyncGenerator { let current = state; - const runtime = getSnapshotRuntime(current); + const runtime = createSnapshotRuntime(); yield toSnap(current, runtime); while (current.status === 'active') { current = await invoke(current); - bindSnapshotRuntime(current, runtime); yield toSnap(current, runtime); } } function toSnap( s: AgentState, - runtime = getSnapshotRuntime(s) + runtime: { sessionId: string; createdAt: number } ): AgentSnapshot { return { value: s.value, diff --git a/src/persistence.test.ts b/src/persistence.test.ts index b9584a9..887dc9a 100644 --- a/src/persistence.test.ts +++ b/src/persistence.test.ts @@ -16,17 +16,20 @@ test('appends and loads journal events in sequence order', async () => { expect(await store.loadEvents('session-1')).toEqual([ { - at: 20, + sequence: 1, type: 'xstate.done.invoke.worker', + at: 20, }, { - at: 10, + sequence: 2, type: 'xstate.init', + at: 10, }, ]); expect(await store.loadEvents('session-1', 1)).toEqual([ { + sequence: 2, at: 10, type: 'xstate.init', }, @@ -46,6 +49,9 @@ test('loads the latest saved snapshot', async () => { createdAt: 100, sessionId: 'session-1', }, + params: { + idle: { count: 1 }, + }, lastJournalIndex: 1, createdAt: 100, }); @@ -61,6 +67,9 @@ test('loads the latest saved snapshot', async () => { sessionId: 'session-1', output: { count: 2 }, }, + params: { + done: { count: 2 }, + }, lastJournalIndex: 3, createdAt: 300, }); @@ -76,6 +85,9 @@ test('loads the latest saved snapshot', async () => { sessionId: 'session-1', output: { count: 2 }, }, + params: { + done: { count: 2 }, + }, lastJournalIndex: 3, createdAt: 300, }); diff --git a/src/runtime/memory-store.ts b/src/runtime/memory-store.ts index 0f1f60b..bd65f35 100644 --- a/src/runtime/memory-store.ts +++ b/src/runtime/memory-store.ts @@ -1,11 +1,10 @@ import type { AgentSnapshot } from '../types.js'; import type { JournalEvent } from './events.js'; -import type { PersistedSnapshot, RunStore } from './store.js'; - -type StoredJournalEvent = { - sequence: number; - event: TEvent; -}; +import type { + JournalEventRecord, + PersistedSnapshot, + RunStore, +} from './store.js'; function compareSnapshots( a: PersistedSnapshot, @@ -18,22 +17,20 @@ export function createMemoryRunStore< TSnapshot extends AgentSnapshot = AgentSnapshot, TEvent extends JournalEvent = JournalEvent, >(): RunStore { - const journals = new Map>>(); + const journals = new Map>>(); const snapshots = new Map[]>(); return { async append(sessionId, event) { const current = journals.get(sessionId) ?? []; const sequence = current.length === 0 ? 1 : current[current.length - 1]!.sequence + 1; - current.push({ sequence, event }); + current.push({ ...event, sequence }); journals.set(sessionId, current); }, async loadEvents(sessionId, afterSequence = 0) { const events = journals.get(sessionId) ?? []; - return events - .filter((entry) => entry.sequence > afterSequence) - .map((entry) => entry.event); + return events.filter((entry) => entry.sequence > afterSequence); }, async loadLatestSnapshot(sessionId) { diff --git a/src/runtime/store.ts b/src/runtime/store.ts index ddd841e..4aa8adf 100644 --- a/src/runtime/store.ts +++ b/src/runtime/store.ts @@ -1,12 +1,17 @@ import type { AgentSnapshot } from '../types.js'; import type { JournalEvent } from './events.js'; +export type JournalEventRecord< + TEvent extends JournalEvent = JournalEvent, +> = TEvent & { sequence: number }; + export interface PersistedSnapshot< TSnapshot extends AgentSnapshot = AgentSnapshot, > { sessionId: string; sequence: number; snapshot: TSnapshot; + params: Record>; lastJournalIndex: number; createdAt: number; } @@ -16,7 +21,10 @@ export interface RunStore< TEvent extends JournalEvent = JournalEvent, > { append(sessionId: string, event: TEvent): Promise; - loadEvents(sessionId: string, afterSequence?: number): Promise; + loadEvents( + sessionId: string, + afterSequence?: number + ): Promise[]>; loadLatestSnapshot(sessionId: string): Promise | null>; saveSnapshot(snapshot: PersistedSnapshot): Promise; } diff --git a/src/stream-snapshot.test.ts b/src/stream-snapshot.test.ts index 34fbd66..3a8d7a6 100644 --- a/src/stream-snapshot.test.ts +++ b/src/stream-snapshot.test.ts @@ -1,21 +1,21 @@ import { expect, test } from 'vitest'; import { createAgentMachine } from './index.js'; -async function collectSnapshots() { - const machine = createAgentMachine({ - id: 'snapshot-machine', - context: () => ({}), - initial: 'done', - states: { - done: { - type: 'final', - output: () => ({ ok: true }), - }, +const machine = createAgentMachine({ + id: 'snapshot-machine', + context: () => ({}), + initial: 'done', + states: { + done: { + type: 'final', + output: () => ({ ok: true }), }, - }); + }, +}); +async function collectSnapshots(state = machine.getInitialState()) { const snaps = []; - for await (const snap of machine.stream(machine.getInitialState())) { + for await (const snap of machine.stream(state)) { snaps.push(snap); } @@ -51,8 +51,10 @@ test('stream emits durable snapshots with stable session metadata', async () => }); test('separate machine executions get distinct session ids', async () => { - const firstRun = await collectSnapshots(); - const secondRun = await collectSnapshots(); + const state = machine.getInitialState(); + const firstRun = await collectSnapshots(state); + const secondRun = await collectSnapshots(state); expect(firstRun[0]!.sessionId).not.toBe(secondRun[0]!.sessionId); + expect(firstRun[0]!.createdAt).not.toBe(secondRun[0]!.createdAt); }); diff --git a/src/types.ts b/src/types.ts index f274f16..b39f942 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,6 +32,7 @@ export type TransitionEvent< // ─── Durable Session Vocabulary ─── export type { JournalEvent } from './runtime/events.js'; +export type { JournalEventRecord, PersistedSnapshot, RunStore } from './runtime/store.js'; // ─── Adapter ─── @@ -125,8 +126,6 @@ export interface AgentSnapshot< error?: unknown; } -export type { PersistedSnapshot, RunStore } from './runtime/store.js'; - // ─── Agent Machine ─── export interface AgentMachine< From 6c4bccb0758187212accadeffc193f848e37df10 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 9 Apr 2026 07:00:45 -0400 Subject: [PATCH 13/34] fix: unify durable snapshot restore shape --- src/machine.ts | 19 +++++++++++++++---- src/persistence.test.ts | 24 ++++++++++++------------ src/runtime/store.ts | 3 +-- src/session-types.test.ts | 1 + src/stream-snapshot.test.ts | 25 +++++++++++++++++++++---- src/types.ts | 5 +++++ 6 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/machine.ts b/src/machine.ts index b80479a..52bdc2e 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -155,9 +155,15 @@ export function createAgentMachine( machineConfig: MachineConfig ): AgentMachine { const cfg = machineConfig as MachineConfig; - let snapshotRunIndex = 0; - function createSnapshotRuntime() { + function createSnapshotRuntime(state: AgentState) { + if (state.sessionId && state.createdAt !== undefined) { + return { + sessionId: state.sessionId, + createdAt: state.createdAt, + }; + } + const sessionId = typeof globalThis.crypto !== 'undefined' && typeof globalThis.crypto.randomUUID === 'function' @@ -166,7 +172,7 @@ export function createAgentMachine( return { sessionId, - createdAt: Date.now() + snapshotRunIndex++, + createdAt: Date.now(), }; } @@ -197,6 +203,8 @@ export function createAgentMachine( value: string; context: Record; params?: Record>; + sessionId?: string; + createdAt?: number; status?: AgentState['status']; output?: unknown; error?: unknown; @@ -206,6 +214,8 @@ export function createAgentMachine( context: raw.context, status: raw.status ?? 'active', params: raw.params ?? {}, + sessionId: raw.sessionId, + createdAt: raw.createdAt, output: raw.output, error: raw.error, }; @@ -398,7 +408,7 @@ export function createAgentMachine( state: AgentState ): AsyncGenerator { let current = state; - const runtime = createSnapshotRuntime(); + const runtime = createSnapshotRuntime(current); yield toSnap(current, runtime); while (current.status === 'active') { current = await invoke(current); @@ -416,6 +426,7 @@ export function createAgentMachine( status: s.status, sessionId: runtime.sessionId, createdAt: runtime.createdAt, + params: s.params, output: s.output, error: s.error, }; diff --git a/src/persistence.test.ts b/src/persistence.test.ts index 887dc9a..bd89816 100644 --- a/src/persistence.test.ts +++ b/src/persistence.test.ts @@ -42,53 +42,53 @@ test('loads the latest saved snapshot', async () => { await store.saveSnapshot({ sessionId: 'session-1', sequence: 1, + afterSequence: 1, snapshot: { value: 'idle', context: { count: 1 }, status: 'active', createdAt: 100, sessionId: 'session-1', + params: { + idle: { count: 1 }, + }, }, - params: { - idle: { count: 1 }, - }, - lastJournalIndex: 1, createdAt: 100, }); await store.saveSnapshot({ sessionId: 'session-1', sequence: 3, + afterSequence: 3, snapshot: { value: 'done', context: { count: 2 }, status: 'done', createdAt: 300, sessionId: 'session-1', + params: { + done: { count: 2 }, + }, output: { count: 2 }, }, - params: { - done: { count: 2 }, - }, - lastJournalIndex: 3, createdAt: 300, }); expect(await store.loadLatestSnapshot('session-1')).toEqual({ sessionId: 'session-1', sequence: 3, + afterSequence: 3, snapshot: { value: 'done', context: { count: 2 }, status: 'done', createdAt: 300, sessionId: 'session-1', + params: { + done: { count: 2 }, + }, output: { count: 2 }, }, - params: { - done: { count: 2 }, - }, - lastJournalIndex: 3, createdAt: 300, }); }); diff --git a/src/runtime/store.ts b/src/runtime/store.ts index 4aa8adf..07c7a7f 100644 --- a/src/runtime/store.ts +++ b/src/runtime/store.ts @@ -11,8 +11,7 @@ export interface PersistedSnapshot< sessionId: string; sequence: number; snapshot: TSnapshot; - params: Record>; - lastJournalIndex: number; + afterSequence: number; createdAt: number; } diff --git a/src/session-types.test.ts b/src/session-types.test.ts index 821721d..c1cf2ac 100644 --- a/src/session-types.test.ts +++ b/src/session-types.test.ts @@ -8,6 +8,7 @@ test('AgentSnapshot includes durable session fields', () => { status: 'active', createdAt: 123, sessionId: 'session-1', + params: {}, }; expect(snapshot.sessionId).toBe('session-1'); diff --git a/src/stream-snapshot.test.ts b/src/stream-snapshot.test.ts index 3a8d7a6..91157f8 100644 --- a/src/stream-snapshot.test.ts +++ b/src/stream-snapshot.test.ts @@ -4,7 +4,10 @@ import { createAgentMachine } from './index.js'; const machine = createAgentMachine({ id: 'snapshot-machine', context: () => ({}), - initial: 'done', + initial: () => ({ + target: 'done', + params: { step: 1 }, + }), states: { done: { type: 'final', @@ -28,6 +31,7 @@ test('stream emits durable snapshots with stable session metadata', async () => expect(snaps.length).toBeGreaterThanOrEqual(2); expect(new Set(snaps.map((snap) => snap.sessionId)).size).toBe(1); expect(new Set(snaps.map((snap) => snap.createdAt)).size).toBe(1); + expect(snaps[0]!.params).toEqual({ done: { step: 1 } }); expect(snaps[0]).toEqual( expect.objectContaining({ sessionId: expect.any(String), @@ -35,9 +39,9 @@ test('stream emits durable snapshots with stable session metadata', async () => value: 'done', context: {}, status: 'active', + params: { done: { step: 1 } }, }) ); - expect(snaps[0]).not.toHaveProperty('params'); expect(snaps[snaps.length - 1]).toEqual( expect.objectContaining({ sessionId: snaps[0]!.sessionId, @@ -45,16 +49,29 @@ test('stream emits durable snapshots with stable session metadata', async () => value: 'done', context: {}, status: 'done', + params: { done: { step: 1 } }, output: { ok: true }, }) ); }); -test('separate machine executions get distinct session ids', async () => { +test('snapshot roundtrips through resolveState without losing identity', async () => { + const emitted = await collectSnapshots(); + const restored = machine.resolveState(emitted[0]!); + const rerun = await collectSnapshots(restored); + + expect(restored.sessionId).toBe(emitted[0]!.sessionId); + expect(restored.createdAt).toBe(emitted[0]!.createdAt); + expect(restored.params).toEqual(emitted[0]!.params); + expect(rerun[0]!.sessionId).toBe(emitted[0]!.sessionId); + expect(rerun[0]!.createdAt).toBe(emitted[0]!.createdAt); + expect(rerun[0]!.params).toEqual(emitted[0]!.params); +}); + +test('fresh machine executions on the same raw state get distinct session ids', async () => { const state = machine.getInitialState(); const firstRun = await collectSnapshots(state); const secondRun = await collectSnapshots(state); expect(firstRun[0]!.sessionId).not.toBe(secondRun[0]!.sessionId); - expect(firstRun[0]!.createdAt).not.toBe(secondRun[0]!.createdAt); }); diff --git a/src/types.ts b/src/types.ts index b39f942..2528c13 100644 --- a/src/types.ts +++ b/src/types.ts @@ -96,6 +96,8 @@ export interface AgentState< context: TContext; status: 'active' | 'pending' | 'done' | 'error'; params: Record>; + sessionId?: string; + createdAt?: number; output?: unknown; error?: unknown; } @@ -122,6 +124,7 @@ export interface AgentSnapshot< status: AgentState['status']; createdAt: number; sessionId: string; + params: Record>; output?: unknown; error?: unknown; } @@ -144,6 +147,8 @@ export interface AgentMachine< value: string; context: TContext; params?: Record>; + sessionId?: string; + createdAt?: number; status?: AgentState['status']; output?: unknown; error?: unknown; From 4f305d192f2bab2c3f523ebd801eba45bf644d77 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 9 Apr 2026 07:06:12 -0400 Subject: [PATCH 14/34] fix: simplify replay cursor contract --- src/persistence.test.ts | 58 +++++++++++++++++++++++++++++++++---- src/runtime/memory-store.ts | 7 +++-- src/runtime/store.ts | 3 +- src/types.ts | 24 ++++++++------- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/src/persistence.test.ts b/src/persistence.test.ts index bd89816..406ef40 100644 --- a/src/persistence.test.ts +++ b/src/persistence.test.ts @@ -4,16 +4,19 @@ import { createMemoryRunStore } from './index.js'; test('appends and loads journal events in sequence order', async () => { const store = createMemoryRunStore(); - await store.append('session-1', { + const first = await store.append('session-1', { type: 'xstate.done.invoke.worker', at: 20, }); - await store.append('session-1', { + const second = await store.append('session-1', { type: 'xstate.init', at: 10, }); + expect(first.sequence).toBe(1); + expect(second.sequence).toBe(2); + expect(await store.loadEvents('session-1')).toEqual([ { sequence: 1, @@ -36,12 +39,11 @@ test('appends and loads journal events in sequence order', async () => { ]); }); -test('loads the latest saved snapshot', async () => { +test('loads the most replay-advanced saved snapshot', async () => { const store = createMemoryRunStore(); await store.saveSnapshot({ sessionId: 'session-1', - sequence: 1, afterSequence: 1, snapshot: { value: 'idle', @@ -58,7 +60,6 @@ test('loads the latest saved snapshot', async () => { await store.saveSnapshot({ sessionId: 'session-1', - sequence: 3, afterSequence: 3, snapshot: { value: 'done', @@ -76,7 +77,6 @@ test('loads the latest saved snapshot', async () => { expect(await store.loadLatestSnapshot('session-1')).toEqual({ sessionId: 'session-1', - sequence: 3, afterSequence: 3, snapshot: { value: 'done', @@ -92,3 +92,49 @@ test('loads the latest saved snapshot', async () => { createdAt: 300, }); }); + +test('loads the most replay-advanced snapshot even if saved earlier', async () => { + const store = createMemoryRunStore(); + + await store.saveSnapshot({ + sessionId: 'session-1', + afterSequence: 5, + snapshot: { + value: 'done', + context: { count: 5 }, + status: 'done', + createdAt: 500, + sessionId: 'session-1', + params: { done: { count: 5 } }, + }, + createdAt: 500, + }); + + await store.saveSnapshot({ + sessionId: 'session-1', + afterSequence: 2, + snapshot: { + value: 'review', + context: { count: 2 }, + status: 'active', + createdAt: 200, + sessionId: 'session-1', + params: { review: { count: 2 } }, + }, + createdAt: 200, + }); + + expect(await store.loadLatestSnapshot('session-1')).toEqual({ + sessionId: 'session-1', + afterSequence: 5, + snapshot: { + value: 'done', + context: { count: 5 }, + status: 'done', + createdAt: 500, + sessionId: 'session-1', + params: { done: { count: 5 } }, + }, + createdAt: 500, + }); +}); diff --git a/src/runtime/memory-store.ts b/src/runtime/memory-store.ts index bd65f35..f2a6961 100644 --- a/src/runtime/memory-store.ts +++ b/src/runtime/memory-store.ts @@ -10,7 +10,7 @@ function compareSnapshots( a: PersistedSnapshot, b: PersistedSnapshot ): number { - return a.sequence - b.sequence || a.createdAt - b.createdAt; + return a.afterSequence - b.afterSequence || a.createdAt - b.createdAt; } export function createMemoryRunStore< @@ -26,11 +26,14 @@ export function createMemoryRunStore< const sequence = current.length === 0 ? 1 : current[current.length - 1]!.sequence + 1; current.push({ ...event, sequence }); journals.set(sessionId, current); + return { sequence }; }, async loadEvents(sessionId, afterSequence = 0) { const events = journals.get(sessionId) ?? []; - return events.filter((entry) => entry.sequence > afterSequence); + return [...events] + .filter((entry) => entry.sequence > afterSequence) + .sort((a, b) => a.sequence - b.sequence); }, async loadLatestSnapshot(sessionId) { diff --git a/src/runtime/store.ts b/src/runtime/store.ts index 07c7a7f..4446165 100644 --- a/src/runtime/store.ts +++ b/src/runtime/store.ts @@ -9,7 +9,6 @@ export interface PersistedSnapshot< TSnapshot extends AgentSnapshot = AgentSnapshot, > { sessionId: string; - sequence: number; snapshot: TSnapshot; afterSequence: number; createdAt: number; @@ -19,7 +18,7 @@ export interface RunStore< TSnapshot extends AgentSnapshot = AgentSnapshot, TEvent extends JournalEvent = JournalEvent, > { - append(sessionId: string, event: TEvent): Promise; + append(sessionId: string, event: TEvent): Promise<{ sequence: number }>; loadEvents( sessionId: string, afterSequence?: number diff --git a/src/types.ts b/src/types.ts index 2528c13..4082e00 100644 --- a/src/types.ts +++ b/src/types.ts @@ -143,16 +143,20 @@ export interface AgentMachine< ...args: unknown extends TInput ? [input?: TInput] : [input: TInput] ): AgentState; - resolveState(raw: { - value: string; - context: TContext; - params?: Record>; - sessionId?: string; - createdAt?: number; - status?: AgentState['status']; - output?: unknown; - error?: unknown; - }): AgentState; + resolveState( + raw: + | AgentSnapshot + | { + value: string; + context: TContext; + params?: Record>; + sessionId?: string; + createdAt?: number; + status?: AgentState['status']; + output?: unknown; + error?: unknown; + } + ): AgentState; transition( state: AgentState, From 4b9ab8df556f0e6a30b292b67f9ee03048c4cd83 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 9 Apr 2026 07:21:06 -0400 Subject: [PATCH 15/34] feat: add durable session runtime --- src/index.ts | 7 + src/invoke-events.test.ts | 86 ++++++++++ src/machine.ts | 276 +++++++++++++++++++++++++------- src/restore.test.ts | 74 +++++++++ src/runtime/emitter.ts | 45 ++++++ src/runtime/session.ts | 305 ++++++++++++++++++++++++++++++++++++ src/session-runtime.test.ts | 60 +++++++ src/streaming.test.ts | 103 ++++++++++++ src/types.ts | 40 ++++- src/utils.ts | 52 +++++- 10 files changed, 989 insertions(+), 59 deletions(-) create mode 100644 src/invoke-events.test.ts create mode 100644 src/restore.test.ts create mode 100644 src/runtime/emitter.ts create mode 100644 src/runtime/session.ts create mode 100644 src/session-runtime.test.ts create mode 100644 src/streaming.test.ts diff --git a/src/index.ts b/src/index.ts index 6ad8a53..f59582c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,25 +8,32 @@ export { classify } from './classify.js'; // Adapter export { createAdapter } from './adapter.js'; export { createMemoryRunStore } from './runtime/memory-store.js'; +export { restoreSession, startSession } from './runtime/session.js'; // Types export type { AgentAdapter, AgentMachine, + AgentRun, AgentSnapshot, AgentState, ClassifyConfig, DecideConfig, DecideResultFor, + EmittedPart, + EmittedUnion, EventPayload, EventUnion, ExecuteResult, InferOutput, + InvokeEnqueue, JournalEvent, JournalEventRecord, MachineConfig, PersistedSnapshot, + RestoreSessionOptions, RunStore, + SessionOptions, StandardSchemaV1, StateConfig, Trace, diff --git a/src/invoke-events.test.ts b/src/invoke-events.test.ts new file mode 100644 index 0000000..e70061a --- /dev/null +++ b/src/invoke-events.test.ts @@ -0,0 +1,86 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from './index.js'; + +test('invoke success is journaled as an internal machine event', async () => { + const machine = createAgentMachine({ + id: 'invoke-success', + context: () => ({ result: null as string | null }), + initial: 'processing', + states: { + processing: { + resultSchema: z.object({ value: z.string() }), + invoke: async () => ({ value: 'ok' }), + onDone: ({ result }) => ({ + target: 'done', + context: { result: result.value }, + }), + }, + done: { + type: 'final', + output: ({ context }) => context, + }, + }, + }); + + const store = createMemoryRunStore(); + const run = await startSession(machine, { store }); + const journal = await store.loadEvents(run.sessionId); + + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + context: { result: 'ok' }, + output: { result: 'ok' }, + }) + ); + expect(journal).toEqual([ + expect.objectContaining({ sequence: 1, type: 'xstate.init' }), + expect.objectContaining({ + sequence: 2, + type: 'xstate.done.invoke.processing', + output: { value: 'ok' }, + }), + ]); +}); + +test('invoke failure is journaled as an internal machine event', async () => { + const machine = createAgentMachine({ + id: 'invoke-failure', + context: () => ({ count: 0 }), + initial: 'processing', + states: { + processing: { + invoke: async () => { + throw new Error('boom'); + }, + }, + }, + }); + + const store = createMemoryRunStore(); + const run = await startSession(machine, { store }); + const journal = await store.loadEvents(run.sessionId); + + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'processing', + status: 'error', + context: { count: 0 }, + error: expect.objectContaining({ message: 'boom' }), + }) + ); + expect(journal).toEqual([ + expect.objectContaining({ sequence: 1, type: 'xstate.init' }), + expect.objectContaining({ + sequence: 2, + type: 'xstate.error.invoke.processing', + error: expect.objectContaining({ message: 'boom' }), + }), + ]); +}); diff --git a/src/machine.ts b/src/machine.ts index 52bdc2e..1c521f0 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -2,6 +2,7 @@ import type { AgentMachine, AgentSnapshot, AgentState, + EmittedPart, EventPayload, ExecuteResult, InferOutput, @@ -9,13 +10,19 @@ import type { StandardSchemaV1, TransitionResult, } from './types.js'; +import type { JournalEvent } from './runtime/events.js'; import { applyTransition, + findEmittedSchema, findEventSchema, + formatSchemaIssues, getAvailableEvents, getParams, + isDoneInvokeEventType, + isErrorInvokeEventType, resolveInitial, resolveStateConfig, + serializeError, validateSchemaSync, } from './utils.js'; import type { StateConfigAny } from './utils.js'; @@ -47,7 +54,7 @@ type StateNodeDef< context: TContext; params: NoInfer; signal?: AbortSignal; - }) => Promise>; + }, enq: { emit(part: EmittedPart): void }) => Promise>; onDone?: (args: { result: OnDoneResult; context: TContext }) => TransitionResult; on?: { [E in keyof TEvents & string]?: TransitionResult | ((args: { event: EventFor; @@ -88,6 +95,7 @@ export function createAgentMachine< context: StandardSchemaV1; input?: StandardSchemaV1; events?: TEvents; + emitted?: Record; }; context: (input: NoInfer) => NoInfer; adapter?: import('./types.js').AgentAdapter; @@ -113,6 +121,7 @@ export function createAgentMachine< input: StandardSchemaV1; context?: never; events?: TEvents; + emitted?: Record; }; context: (input: NoInfer) => TContext; adapter?: import('./types.js').AgentAdapter; @@ -137,6 +146,7 @@ export function createAgentMachine< input?: never; context?: never; events?: TEvents; + emitted?: Record; }; context: (...args: any[]) => TContext; adapter?: import('./types.js').AgentAdapter; @@ -156,6 +166,8 @@ export function createAgentMachine( ): AgentMachine { const cfg = machineConfig as MachineConfig; + type SnapshotRuntime = { sessionId: string; createdAt: number }; + function createSnapshotRuntime(state: AgentState) { if (state.sessionId && state.createdAt !== undefined) { return { @@ -176,6 +188,17 @@ export function createAgentMachine( }; } + function withRuntimeMetadata( + state: AgentState, + runtime: SnapshotRuntime + ): AgentState { + return { + ...state, + sessionId: runtime.sessionId, + createdAt: runtime.createdAt, + }; + } + function getInitialState(...args: [input?: unknown]): AgentState { const input = args[0]; @@ -225,9 +248,87 @@ export function createAgentMachine( state: AgentState, event: { type: string; [k: string]: unknown } ): AgentState { + const sc = resolveStateConfig(cfg, state.value); + const effectiveConfig = sc.__decideConfig + ? { ...sc, ...(sc.__decideConfig as Record) } + : sc; + if (isDoneInvokeEventType(state.value, event.type)) { + const result = 'output' in event ? event.output : undefined; + const validatedResult = effectiveConfig.resultSchema + ? validateSchemaSync(effectiveConfig.resultSchema, result) + : result; + + if (effectiveConfig.onDone) { + const trans = effectiveConfig.onDone({ + result: validatedResult, + context: state.context, + }); + + if (trans.target) { + return applyTransition(state, trans); + } + + return { + ...state, + status: 'pending', + context: trans.context + ? { ...state.context, ...trans.context } + : state.context, + }; + } + + const internalHandler = sc.on?.[event.type]; + if (internalHandler !== undefined) { + const result: TransitionResult = + typeof internalHandler === 'function' + ? internalHandler({ context: state.context, event }) + : internalHandler; + + if (result.target) { + return applyTransition(state, result); + } + + return { + ...state, + status: 'pending', + context: result.context + ? { ...state.context, ...result.context } + : state.context, + }; + } + + return { ...state, status: 'pending' }; + } + + if (isErrorInvokeEventType(state.value, event.type)) { + const internalHandler = sc.on?.[event.type]; + if (internalHandler !== undefined) { + const result: TransitionResult = + typeof internalHandler === 'function' + ? internalHandler({ context: state.context, event }) + : internalHandler; + + if (result.target) { + return applyTransition(state, result); + } + + return { + ...state, + context: result.context + ? { ...state.context, ...result.context } + : state.context, + }; + } + + return { + ...state, + status: 'error', + error: 'error' in event ? event.error : undefined, + }; + } + validateEventPayload(state.value, event); - const sc = resolveStateConfig(cfg, state.value); if (sc.on?.[event.type] !== undefined) { const handler = sc.on[event.type]!; const result: TransitionResult = @@ -266,60 +367,52 @@ export function createAgentMachine( 'issues' in result && result.issues ) { - const messages = (result.issues as Array<{ message: string }>) - .map((i) => i.message) - .join(', '); + const messages = formatSchemaIssues( + result.issues as Array<{ message: string }> + ); throw new Error(`Invalid event '${event.type}': ${messages}`); } } - async function invoke(state: AgentState): Promise { - if (state.status === 'done' || state.status === 'error') { - return state; - } - - const sc = resolveStateConfig(cfg, state.value); - - if (sc.type === 'final') { - const output = sc.output - ? sc.output({ context: state.context }) - : undefined; - return { ...state, status: 'done', output }; + function validateEmittedPart(part: EmittedPart): void { + const schema = findEmittedSchema(cfg, part.type); + if (!schema) { + return; } - if (sc.type === 'choice' || sc.__decideConfig) { - return handleChoice(state, sc); + const result = schema['~standard'].validate(part); + if (result instanceof Promise) { + throw new Error( + 'Async schema validation is not supported in sync context.' + ); } - if (sc.invoke) { - return handleInvoke(state, sc); - } - - if (sc.on) { - return { ...state, status: 'pending' }; + if (result.issues) { + const messages = formatSchemaIssues(result.issues); + throw new Error(`Invalid emitted part '${part.type}': ${messages}`); } + } + function createEnqueue(onEmit?: (part: EmittedPart) => void) { return { - ...state, - status: 'error', - error: `State '${state.value}' has no invoke, events, or final type`, + emit(part: EmittedPart) { + validateEmittedPart(part); + onEmit?.(part); + }, }; } - async function handleChoice( - state: AgentState, - sc: StateConfigAny - ): Promise { - // Merge __decideConfig props onto sc for decide() wrapper compat + async function createChoiceEvent(state: AgentState): Promise { + const sc = resolveStateConfig(cfg, state.value); const dc = sc.__decideConfig ? { ...sc, ...(sc.__decideConfig as Record) } : sc; const adapter = (dc as StateConfigAny).adapter ?? cfg.adapter; if (!adapter) { return { - ...state, - status: 'error', - error: `No adapter for '${state.value}'`, + type: `xstate.error.invoke.${state.value}`, + error: { message: `No adapter for '${state.value}'` }, + at: Date.now(), }; } @@ -336,37 +429,99 @@ export function createAgentMachine( options: (dc as StateConfigAny).options!, reasoning: (dc as StateConfigAny).reasoning, }); - const onDone = (dc as StateConfigAny).onDone; - if (!onDone) return { ...state, status: 'error', error: 'choice state missing onDone' }; - const trans = onDone({ result, context: state.context }); - return applyTransition(state, trans); + + return { + type: `xstate.done.invoke.${state.value}`, + output: result, + at: Date.now(), + }; } catch (error) { - return { ...state, status: 'error', error }; + return { + type: `xstate.error.invoke.${state.value}`, + error: serializeError(error), + at: Date.now(), + }; } } - async function handleInvoke( + async function createInvokeEvent( state: AgentState, - sc: StateConfigAny - ): Promise { + sc: StateConfigAny, + onEmit?: (part: EmittedPart) => void + ): Promise { try { - const result = await sc.invoke!({ - context: state.context, - params: getParams(state.value, state.params), - }); - if (sc.onDone) { - const trans = sc.onDone({ result, context: state.context }); - return applyTransition(state, trans); - } - if (sc.on) { - return { ...state, status: 'pending' }; - } - return state; + const result = await sc.invoke!( + { + context: state.context, + params: getParams(state.value, state.params), + }, + createEnqueue(onEmit) + ); + + return { + type: `xstate.done.invoke.${state.value}`, + output: result, + at: Date.now(), + }; } catch (error) { - return { ...state, status: 'error', error }; + return { + type: `xstate.error.invoke.${state.value}`, + error: serializeError(error), + at: Date.now(), + }; } } + async function getEffectEvent( + state: AgentState, + onEmit?: (part: EmittedPart) => void + ): Promise { + if (state.status === 'done' || state.status === 'error') { + return null; + } + + const sc = resolveStateConfig(cfg, state.value); + if (sc.type === 'choice' || sc.__decideConfig) { + return createChoiceEvent(state); + } + + if (sc.invoke) { + return createInvokeEvent(state, sc, onEmit); + } + + return null; + } + + async function invoke(state: AgentState): Promise { + if (state.status === 'done' || state.status === 'error') { + return state; + } + + const sc = resolveStateConfig(cfg, state.value); + + if (sc.type === 'final') { + const output = sc.output + ? sc.output({ context: state.context }) + : undefined; + return { ...state, status: 'done', output }; + } + + const effectEvent = await getEffectEvent(state); + if (effectEvent) { + return transition(state, effectEvent); + } + + if (sc.on) { + return { ...state, status: 'pending' }; + } + + return { + ...state, + status: 'error', + error: `State '${state.value}' has no invoke, events, or final type`, + }; + } + async function execute(state: AgentState): Promise { let current = state; while (current.status === 'active') { @@ -409,9 +564,11 @@ export function createAgentMachine( ): AsyncGenerator { let current = state; const runtime = createSnapshotRuntime(current); + current = withRuntimeMetadata(current, runtime); yield toSnap(current, runtime); while (current.status === 'active') { current = await invoke(current); + current = withRuntimeMetadata(current, runtime); yield toSnap(current, runtime); } } @@ -440,5 +597,10 @@ export function createAgentMachine( invoke, execute, stream, + __runtime: { + toSnapshot: toSnap, + withRuntimeMetadata, + getEffectEvent, + }, } as AgentMachine; } diff --git a/src/restore.test.ts b/src/restore.test.ts new file mode 100644 index 0000000..7d9de1f --- /dev/null +++ b/src/restore.test.ts @@ -0,0 +1,74 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + restoreSession, + startSession, +} from './index.js'; + +test('restoreSession reconstructs from the latest snapshot plus replay tail', async () => { + const machine = createAgentMachine({ + id: 'restore-session', + context: () => ({ approved: false, result: null as string | null }), + initial: 'review', + states: { + review: { + on: { + approve: { + target: 'processing', + context: { approved: true }, + }, + }, + }, + processing: { + resultSchema: z.object({ value: z.string() }), + invoke: async ({ context }) => ({ + value: context.approved ? 'approved' : 'rejected', + }), + onDone: ({ result }) => ({ + target: 'done', + context: { result: result.value }, + }), + }, + done: { + type: 'final', + output: ({ context }) => context, + }, + }, + }); + + const baseStore = createMemoryRunStore(); + let snapshotWrites = 0; + const store = { + append: baseStore.append, + loadEvents: baseStore.loadEvents, + loadLatestSnapshot: baseStore.loadLatestSnapshot, + async saveSnapshot(snapshot: Awaited< + ReturnType + > extends infer TSaved + ? Exclude + : never) { + snapshotWrites += 1; + if (snapshotWrites === 1) { + await baseStore.saveSnapshot(snapshot); + } + }, + }; + + const liveRun = await startSession(machine, { store }); + await liveRun.send({ type: 'approve' }); + + expect(await store.loadLatestSnapshot(liveRun.sessionId)).toEqual( + expect.objectContaining({ + afterSequence: 1, + }) + ); + + const restoredRun = await restoreSession(machine, { + sessionId: liveRun.sessionId, + store, + }); + + expect(restoredRun.getSnapshot()).toEqual(liveRun.getSnapshot()); +}); diff --git a/src/runtime/emitter.ts b/src/runtime/emitter.ts new file mode 100644 index 0000000..1b83786 --- /dev/null +++ b/src/runtime/emitter.ts @@ -0,0 +1,45 @@ +type Handler = (event: unknown) => void; + +export interface RunEmitter { + emit(type: string, event: unknown): void; + on(type: string, handler: Handler): () => void; +} + +export function createRunEmitter(): RunEmitter { + const listeners = new Map>(); + const history = new Map(); + + return { + emit(type, event) { + const events = history.get(type) ?? []; + events.push(event); + history.set(type, events); + + for (const handler of listeners.get(type) ?? []) { + handler(event); + } + }, + + on(type, handler) { + const current = listeners.get(type) ?? new Set(); + current.add(handler); + listeners.set(type, current); + + for (const event of history.get(type) ?? []) { + handler(event); + } + + return () => { + const active = listeners.get(type); + if (!active) { + return; + } + + active.delete(handler); + if (active.size === 0) { + listeners.delete(type); + } + }; + }, + }; +} diff --git a/src/runtime/session.ts b/src/runtime/session.ts new file mode 100644 index 0000000..c47a0de --- /dev/null +++ b/src/runtime/session.ts @@ -0,0 +1,305 @@ +import type { JournalEvent } from './events.js'; +import { createRunEmitter } from './emitter.js'; +import type { + AgentMachine, + AgentRun, + AgentSnapshot, + AgentState, + EmittedPart, + RestoreSessionOptions, + SessionOptions, +} from '../types.js'; + +type SnapshotRuntime = { + sessionId: string; + createdAt: number; +}; + +type RuntimeMachine = AgentMachine & { + __runtime: { + toSnapshot(state: AgentState, runtime: SnapshotRuntime): AgentSnapshot; + withRuntimeMetadata(state: AgentState, runtime: SnapshotRuntime): AgentState; + getEffectEvent( + state: AgentState, + onEmit?: (part: EmittedPart) => void + ): Promise; + }; +}; + +type RunState = { + current: AgentState; + snapshot: AgentSnapshot; + lastSequence: number; + runtime: SnapshotRuntime; +}; + +function createSessionId(): string { + if ( + typeof globalThis.crypto !== 'undefined' && + typeof globalThis.crypto.randomUUID === 'function' + ) { + return globalThis.crypto.randomUUID(); + } + + return `session-${Math.random().toString(36).slice(2)}`; +} + +function asRuntimeMachine(machine: AgentMachine): RuntimeMachine { + const runtimeMachine = machine as RuntimeMachine; + if (!runtimeMachine.__runtime) { + throw new Error('Machine runtime internals are unavailable'); + } + + return runtimeMachine; +} + +function toJournalEvent( + event: { type: string; [key: string]: unknown } +): JournalEvent { + return { + ...event, + at: typeof event.at === 'number' ? event.at : Date.now(), + }; +} + +function createRun( + machine: AgentMachine, + store: SessionOptions['store'], + runtimeMachine: RuntimeMachine, + runState: RunState, + emitter = createRunEmitter() +): AgentRun { + async function persistSnapshot() { + runState.snapshot = runtimeMachine.__runtime.toSnapshot( + runState.current, + runState.runtime + ); + + await store.saveSnapshot({ + sessionId: runState.runtime.sessionId, + afterSequence: runState.lastSequence, + snapshot: runState.snapshot, + createdAt: Date.now(), + }); + + emitter.emit('runtime', { + type: 'snapshot.persisted', + sessionId: runState.runtime.sessionId, + afterSequence: runState.lastSequence, + }); + emitter.emit('state', runState.snapshot); + } + + async function appendMachineEvent(event: JournalEvent) { + const record = await store.append(runState.runtime.sessionId, event); + runState.lastSequence = record.sequence; + emitter.emit('machine.event', { + ...event, + sequence: record.sequence, + }); + } + + async function settle() { + while (runState.current.status === 'active') { + const effectEvent = await runtimeMachine.__runtime.getEffectEvent( + runState.current, + (part) => { + emitter.emit(part.type, part); + } + ); + + if (effectEvent) { + await appendMachineEvent(effectEvent); + runState.current = runtimeMachine.__runtime.withRuntimeMetadata( + machine.transition(runState.current, effectEvent), + runState.runtime + ); + await persistSnapshot(); + continue; + } + + runState.current = runtimeMachine.__runtime.withRuntimeMetadata( + await machine.invoke(runState.current), + runState.runtime + ); + await persistSnapshot(); + } + } + + return { + get sessionId() { + return runState.runtime.sessionId; + }, + + get status() { + return runState.snapshot.status; + }, + + getSnapshot() { + return runState.snapshot; + }, + + async send(event) { + const journalEvent = toJournalEvent(event); + const next = machine.transition(runState.current, journalEvent); + + await appendMachineEvent(journalEvent); + runState.current = runtimeMachine.__runtime.withRuntimeMetadata( + next, + runState.runtime + ); + await persistSnapshot(); + await settle(); + }, + + on(type, handler) { + return emitter.on(type, handler); + }, + + /** @internal */ + async __persistCurrent() { + await persistSnapshot(); + }, + + /** @internal */ + async __settle() { + await settle(); + }, + + /** @internal */ + __emit(type: string, event: unknown) { + emitter.emit(type, event); + }, + } as AgentRun; +} + +export async function startSession( + machine: AgentMachine, + options: SessionOptions +): Promise { + const runtimeMachine = asRuntimeMachine(machine); + const initialState = machine.getInitialState(options.input); + const runtime = { + sessionId: options.sessionId ?? createSessionId(), + createdAt: Date.now(), + }; + const runState: RunState = { + current: runtimeMachine.__runtime.withRuntimeMetadata(initialState, runtime), + snapshot: runtimeMachine.__runtime.toSnapshot(initialState, runtime), + lastSequence: 0, + runtime, + }; + + const run = createRun( + machine, + options.store, + runtimeMachine, + runState + ) as AgentRun & { + __persistCurrent(): Promise; + __settle(): Promise; + __emit(type: string, event: unknown): void; + }; + + const initEvent = { + type: 'xstate.init', + input: options.input, + at: runtime.createdAt, + } satisfies JournalEvent; + const record = await options.store.append(runtime.sessionId, initEvent); + runState.lastSequence = record.sequence; + run.__emit('machine.event', { ...initEvent, sequence: record.sequence }); + run.__emit('runtime', { + type: 'session.started', + sessionId: runtime.sessionId, + }); + + await run.__persistCurrent(); + await run.__settle(); + + return run; +} + +export async function restoreSession( + machine: AgentMachine, + options: RestoreSessionOptions +): Promise { + const runtimeMachine = asRuntimeMachine(machine); + const persisted = await options.store.loadLatestSnapshot(options.sessionId); + const allEvents = await options.store.loadEvents(options.sessionId); + const initEvent = allEvents.find( + (event) => event.type === 'xstate.init' + ); + + if (!persisted && !initEvent) { + throw new Error(`No persisted session '${options.sessionId}' found`); + } + + const runtime = { + sessionId: options.sessionId, + createdAt: persisted?.snapshot.createdAt ?? initEvent?.at ?? Date.now(), + }; + const initialState = persisted + ? machine.resolveState(persisted.snapshot) + : machine.getInitialState(initEvent?.input); + const runState: RunState = { + current: runtimeMachine.__runtime.withRuntimeMetadata(initialState, runtime), + snapshot: + persisted?.snapshot + ?? runtimeMachine.__runtime.toSnapshot(initialState, runtime), + lastSequence: persisted?.afterSequence ?? (initEvent?.sequence ?? 0), + runtime, + }; + const run = createRun( + machine, + options.store, + runtimeMachine, + runState + ) as AgentRun & { + __persistCurrent(): Promise; + __settle(): Promise; + __emit(type: string, event: unknown): void; + }; + + if (initEvent && !persisted) { + run.__emit('machine.event', initEvent); + } + + const replayTail = await options.store.loadEvents( + options.sessionId, + runState.lastSequence + ); + + if (!persisted) { + run.__emit('state', runState.snapshot); + } + + for (const event of replayTail) { + runState.current = runtimeMachine.__runtime.withRuntimeMetadata( + machine.transition(runState.current, event), + runState.runtime + ); + runState.lastSequence = event.sequence; + runState.snapshot = runtimeMachine.__runtime.toSnapshot( + runState.current, + runState.runtime + ); + run.__emit('machine.event', event); + run.__emit('state', runState.snapshot); + } + + if (persisted) { + run.__emit('state', runState.snapshot); + } + + run.__emit('runtime', { + type: 'session.restored', + sessionId: runState.runtime.sessionId, + afterSequence: runState.lastSequence, + }); + + await run.__persistCurrent(); + await run.__settle(); + + return run; +} diff --git a/src/session-runtime.test.ts b/src/session-runtime.test.ts new file mode 100644 index 0000000..f6efd81 --- /dev/null +++ b/src/session-runtime.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from 'vitest'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from './index.js'; + +test('startSession creates a session and persists xstate.init', async () => { + const machine = createAgentMachine({ + id: 'session-runtime', + context: () => ({ count: 0 }), + initial: 'idle', + states: { + idle: { + on: { + increment: { + target: 'done', + context: { count: 1 }, + }, + }, + }, + done: { + type: 'final', + output: ({ context }) => context, + }, + }, + }); + + const store = createMemoryRunStore(); + const run = await startSession(machine, { store }); + const snapshot = run.getSnapshot(); + const journal = await store.loadEvents(run.sessionId); + const persisted = await store.loadLatestSnapshot(run.sessionId); + + expect(run.sessionId).toBe(snapshot.sessionId); + expect(run.status).toBe('pending'); + expect(snapshot).toEqual( + expect.objectContaining({ + sessionId: run.sessionId, + value: 'idle', + status: 'pending', + context: { count: 0 }, + params: {}, + }) + ); + expect(journal).toEqual([ + expect.objectContaining({ + sequence: 1, + type: 'xstate.init', + at: expect.any(Number), + }), + ]); + expect(persisted).toEqual( + expect.objectContaining({ + sessionId: run.sessionId, + afterSequence: 1, + snapshot, + }) + ); +}); diff --git a/src/streaming.test.ts b/src/streaming.test.ts new file mode 100644 index 0000000..8ec5530 --- /dev/null +++ b/src/streaming.test.ts @@ -0,0 +1,103 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from './index.js'; + +test('emitted parts flow through the run-level API', async () => { + const machine = createAgentMachine({ + id: 'streaming-parts', + schemas: { + emitted: { + textPart: z.object({ delta: z.string() }), + }, + }, + context: () => ({ finalText: '' }), + initial: 'writing', + states: { + writing: { + resultSchema: z.object({ text: z.string() }), + invoke: async (_args, enq) => { + enq.emit({ type: 'textPart', delta: 'hel' }); + enq.emit({ type: 'textPart', delta: 'lo' }); + + return { text: 'hello' }; + }, + onDone: ({ result }) => ({ + target: 'done', + context: { finalText: result.text }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ text: context.finalText }), + }, + }, + }); + + const store = createMemoryRunStore(); + const run = await startSession(machine, { store }); + const parts: Array<{ type: string; delta: string }> = []; + const states: string[] = []; + const events: string[] = []; + + const offPart = run.on('textPart', (part) => { + parts.push(part as { type: string; delta: string }); + }); + const offState = run.on('state', (snapshot) => { + states.push((snapshot as { value: string }).value); + }); + const offEvent = run.on('machine.event', (event) => { + events.push((event as { type: string }).type); + }); + + expect(parts).toEqual([ + { type: 'textPart', delta: 'hel' }, + { type: 'textPart', delta: 'lo' }, + ]); + expect(states).toContain('writing'); + expect(states[states.length - 1]).toBe('done'); + expect(events).toContain('xstate.done.invoke.writing'); + expect(run.getSnapshot().output).toEqual({ text: 'hello' }); + + offPart(); + offState(); + offEvent(); +}); + +test('invalid emitted parts are rejected', async () => { + const machine = createAgentMachine({ + id: 'streaming-invalid-parts', + schemas: { + emitted: { + textPart: z.object({ delta: z.string().min(1) }), + }, + }, + context: () => ({ count: 0 }), + initial: 'writing', + states: { + writing: { + invoke: async (_args, enq) => { + enq.emit({ type: 'textPart', delta: '' }); + return { ok: true }; + }, + }, + }, + }); + + const run = await startSession(machine, { + store: createMemoryRunStore(), + }); + + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'writing', + status: 'error', + error: expect.objectContaining({ + message: expect.stringContaining("Invalid emitted part 'textPart'"), + }), + }) + ); +}); diff --git a/src/types.ts b/src/types.ts index 4082e00..4f6219d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,12 +23,20 @@ export type EventUnion> = { [K in keyof T & string]: { type: K } & EventPayload>; }[keyof T & string]; +export type EmittedUnion> = EventUnion; + export type TransitionEvent< TEvents extends Record, > = [keyof TEvents & string] extends [never] ? { type: string; [key: string]: unknown } : EventUnion; +export type EmittedPart = { type: string; [key: string]: unknown }; + +export interface InvokeEnqueue { + emit(part: EmittedPart): void; +} + // ─── Durable Session Vocabulary ─── export type { JournalEvent } from './runtime/events.js'; @@ -66,11 +74,12 @@ export interface StateConfig< > { type?: 'final' | 'choice'; paramsSchema?: StandardSchemaV1; + resultSchema?: StandardSchemaV1; invoke?: (args: { context: TContext; params: Record; signal?: AbortSignal; - }) => Promise; + }, enq: InvokeEnqueue) => Promise; onDone?: (args: { result: any; context: TContext }) => TransitionResult; on?: Record | ((args: { event: any; context: TContext }) => TransitionResult)>; events?: Record; @@ -176,6 +185,34 @@ export interface AgentMachine< ): AsyncGenerator>; } +export interface AgentRun< + TContext extends Record = Record, + TValue extends string = string, + TEvents extends Record = {}, +> { + readonly sessionId: string; + readonly status: AgentSnapshot['status']; + getSnapshot(): AgentSnapshot; + send(event: TransitionEvent): Promise; + on(type: string, handler: (event: unknown) => void): () => void; +} + +export interface SessionOptions< + TInput = unknown, + TSnapshot extends AgentSnapshot = AgentSnapshot, +> { + input?: TInput; + sessionId?: string; + store: import('./runtime/store.js').RunStore; +} + +export interface RestoreSessionOptions< + TSnapshot extends AgentSnapshot = AgentSnapshot, +> { + sessionId: string; + store: import('./runtime/store.js').RunStore; +} + // ─── Machine Config (internal) ─── export interface MachineConfig< @@ -189,6 +226,7 @@ export interface MachineConfig< input?: StandardSchemaV1; context?: StandardSchemaV1; events?: TEvents; + emitted?: Record; }; context: (input: TInput) => TContext; adapter?: AgentAdapter; diff --git a/src/utils.ts b/src/utils.ts index 1c8b73c..f6cd7c0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -46,10 +46,17 @@ export function resolveStateConfig( /** Loose state config for internal runtime use */ export type StateConfigAny = { type?: 'final' | 'choice'; - invoke?: (args: { context: Record; params: Record }) => Promise; + invoke?: ( + args: { + context: Record; + params: Record; + }, + enq: { emit(part: { type: string; [key: string]: unknown }): void } + ) => Promise; onDone?: (args: { result: unknown; context: Record }) => TransitionResult; on?: Record; context: Record }) => TransitionResult)>; output?: (args: { context: Record }) => unknown; + resultSchema?: StandardSchemaV1; model?: string; adapter?: { decide: (...args: unknown[]) => Promise }; prompt?: string | ((args: { context: Record; params: Record }) => string); @@ -166,3 +173,46 @@ export function findEventSchema( const events = config.schemas?.events as Record | undefined; return events?.[eventType]; } + +export function findEmittedSchema( + config: MachineConfig, + eventType: string +): StandardSchemaV1 | undefined { + const emitted = config.schemas?.emitted as + | Record + | undefined; + + return emitted?.[eventType]; +} + +export function formatSchemaIssues( + issues: ReadonlyArray<{ message: string }> +): string { + return issues.map((issue) => issue.message).join(', '); +} + +export function isDoneInvokeEventType( + stateValue: string, + eventType: string +): boolean { + return eventType === `xstate.done.invoke.${stateValue}`; +} + +export function isErrorInvokeEventType( + stateValue: string, + eventType: string +): boolean { + return eventType === `xstate.error.invoke.${stateValue}`; +} + +export function serializeError(error: unknown): unknown { + if (error instanceof Error) { + return { + name: error.name, + message: error.message, + stack: error.stack, + }; + } + + return error; +} From ce8dce45203918f92da6989031257822a0a01e40 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 10 Apr 2026 13:36:18 -0400 Subject: [PATCH 16/34] WIP --- .gitignore | 2 + .vscode/launch.json | 4 +- examples/_run.ts | 211 +++++ examples/adapter.ts | 86 ++ examples/branching.ts | 132 ++++ examples/chatbot.ts | 162 ++++ examples/classify.ts | 65 ++ examples/customer-service-sim.ts | 134 ++++ examples/decide.ts | 83 ++ examples/email.ts | 250 ++++++ examples/hitl.ts | 121 +++ examples/index.ts | 21 + examples/joke.ts | 111 +++ examples/jugs.ts | 127 +++ examples/map-reduce.ts | 120 +++ examples/newspaper.ts | 180 +++++ examples/plan-and-execute.ts | 170 ++++ examples/raffle.ts | 116 +++ examples/react-agent.ts | 111 +++ examples/reflection.ts | 155 ++++ examples/river-crossing.ts | 172 ++++ examples/simple.ts | 66 ++ examples/subflow.ts | 139 ++++ examples/tool-calling.ts | 119 +++ examples/tutor.ts | 99 +++ examples_old/executor.ts | 2 +- examples_old/multi.ts | 2 +- package.json | 1 + pnpm-lock.yaml | 283 +++++++ readme.md | 16 + src/agent.test.ts | 41 +- src/ai-sdk/index.ts | 68 +- src/classify.ts | 71 +- src/decide.ts | 72 +- src/examples.test.ts | 747 ++++++++++++++++++ src/index.ts | 6 + src/invoke-events.test.ts | 51 ++ src/langgraph-equivalents/branching.test.ts | 76 ++ src/langgraph-equivalents/graph.test.ts | 118 +++ src/langgraph-equivalents/hitl.test.ts | 76 ++ src/langgraph-equivalents/map-reduce.test.ts | 79 ++ src/langgraph-equivalents/persistence.test.ts | 88 +++ .../plan-and-execute.test.ts | 35 + .../prebuilt-react.test.ts | 89 +++ src/langgraph-equivalents/reflection.test.ts | 30 + src/langgraph-equivalents/streaming.test.ts | 71 ++ src/langgraph-equivalents/subflow.test.ts | 101 +++ .../tool-calling.test.ts | 97 +++ src/machine.ts | 223 ++++-- src/prebuilt/react.ts | 225 ++++++ src/restore.test.ts | 5 +- src/runtime/emitter.ts | 9 - src/runtime/session.ts | 176 +++-- src/session-runtime.test.ts | 132 +++- src/streaming.test.ts | 155 +++- src/target-types.assert.ts | 204 +++++ src/types.ts | 48 +- src/utils.ts | 15 +- 58 files changed, 6165 insertions(+), 203 deletions(-) create mode 100644 examples/_run.ts create mode 100644 examples/adapter.ts create mode 100644 examples/branching.ts create mode 100644 examples/chatbot.ts create mode 100644 examples/classify.ts create mode 100644 examples/customer-service-sim.ts create mode 100644 examples/decide.ts create mode 100644 examples/email.ts create mode 100644 examples/hitl.ts create mode 100644 examples/index.ts create mode 100644 examples/joke.ts create mode 100644 examples/jugs.ts create mode 100644 examples/map-reduce.ts create mode 100644 examples/newspaper.ts create mode 100644 examples/plan-and-execute.ts create mode 100644 examples/raffle.ts create mode 100644 examples/react-agent.ts create mode 100644 examples/reflection.ts create mode 100644 examples/river-crossing.ts create mode 100644 examples/simple.ts create mode 100644 examples/subflow.ts create mode 100644 examples/tool-calling.ts create mode 100644 examples/tutor.ts create mode 100644 src/examples.test.ts create mode 100644 src/langgraph-equivalents/branching.test.ts create mode 100644 src/langgraph-equivalents/graph.test.ts create mode 100644 src/langgraph-equivalents/hitl.test.ts create mode 100644 src/langgraph-equivalents/map-reduce.test.ts create mode 100644 src/langgraph-equivalents/persistence.test.ts create mode 100644 src/langgraph-equivalents/plan-and-execute.test.ts create mode 100644 src/langgraph-equivalents/prebuilt-react.test.ts create mode 100644 src/langgraph-equivalents/reflection.test.ts create mode 100644 src/langgraph-equivalents/streaming.test.ts create mode 100644 src/langgraph-equivalents/subflow.test.ts create mode 100644 src/langgraph-equivalents/tool-calling.test.ts create mode 100644 src/prebuilt/react.ts create mode 100644 src/target-types.assert.ts diff --git a/.gitignore b/.gitignore index 323729b..a52881a 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,5 @@ dist/ .pnp.* .vscode/settings.json + +docs/superpowers diff --git a/.vscode/launch.json b/.vscode/launch.json index d6e2a9a..87a02fb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,8 +19,8 @@ "name": "Debug Current File", "program": "${file}", "cwd": "${workspaceFolder}", - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ts-node", - "runtimeArgs": ["--transpile-only"], + "runtimeExecutable": "node", + "runtimeArgs": ["--import", "tsx"], "outFiles": ["${workspaceFolder}/dist/**/*.js"], "sourceMaps": true, "smartStep": true, diff --git a/examples/_run.ts b/examples/_run.ts new file mode 100644 index 0000000..36f0b65 --- /dev/null +++ b/examples/_run.ts @@ -0,0 +1,211 @@ +import 'dotenv/config'; + +import { generateText, Output } from 'ai'; +import { openai } from '@ai-sdk/openai'; +import { createInterface } from 'node:readline/promises'; +import { stdin as input, stdout as output } from 'node:process'; +import { pathToFileURL } from 'node:url'; +import { z } from 'zod'; +import type { AgentAdapter, ExecuteResult, StandardSchemaV1 } from '../src/index.js'; + +export function isMain(moduleUrl: string): boolean { + const entry = process.argv[1]; + return !!entry && moduleUrl === pathToFileURL(entry).href; +} + +let bufferedLinesPromise: Promise | null = null; +let bufferedLineIndex = 0; + +async function getBufferedLines(): Promise { + if (!bufferedLinesPromise) { + bufferedLinesPromise = (async () => { + const chunks: string[] = []; + + for await (const chunk of input) { + chunks.push(String(chunk)); + } + + return chunks.join('').split(/\r?\n/); + })(); + } + + return bufferedLinesPromise; +} + +export async function prompt(label: string): Promise { + if (!input.isTTY) { + output.write(`${label}: `); + const lines = await getBufferedLines(); + const value = lines[bufferedLineIndex] ?? ''; + bufferedLineIndex += 1; + return value.trim(); + } + + const rl = createInterface({ input, output }); + try { + const value = await rl.question(`${label}: `); + return value.trim(); + } finally { + rl.close(); + } +} + +export function closePrompt(): void { + bufferedLinesPromise = null; + bufferedLineIndex = 0; +} + +export function createExampleModel( + model = 'openai/gpt-5.4-nano' +): Parameters[0]['model'] { + if (!process.env.OPENAI_API_KEY) { + throw new Error('OPENAI_API_KEY is required to run the examples.'); + } + + return openai(resolveOpenAiModel(model)); +} + +export function formatResult(result: ExecuteResult) { + if (result.status === 'done') { + return { + status: result.status, + value: result.state.value, + context: result.context, + output: result.output, + }; + } + + if (result.status === 'pending') { + return { + status: result.status, + value: result.value, + context: result.context, + events: Object.keys(result.events), + }; + } + + return { + status: result.status, + value: result.state.value, + error: result.error, + }; +} + +export function createOpenAiDecisionAdapter(): AgentAdapter { + return { + async decide({ model, prompt, options, reasoning }) { + const optionKeys = Object.keys(options); + + const allSchemaLess = Object.values(options).every((option) => !option.schema); + + if (allSchemaLess && !reasoning) { + const choiceResult = await generateText({ + model: createExampleModel(model), + system: [ + 'Choose exactly one option.', + ...Object.entries(options).map(([key, option]) => `${key}: ${option.description}`), + ].join('\n'), + prompt, + output: Output.choice({ + options: optionKeys, + }), + }); + + return { + choice: choiceResult.output, + data: {} as Record, + }; + } + + const decisionSchemas = optionKeys.map((key) => { + const option = options[key]!; + + return z.object({ + decision: z.literal(key), + data: option.schema ? toZodSchema(option.schema) : z.object({}), + ...(reasoning + ? { reasoning: z.string() } + : {}), + }); + }); + + const decisionSchema = + decisionSchemas.length === 1 + ? decisionSchemas[0]! + : z.union( + decisionSchemas as unknown as [ + z.ZodTypeAny, + z.ZodTypeAny, + ...z.ZodTypeAny[], + ] + ); + + const result = await generateText({ + model: createExampleModel(model), + system: [ + 'Choose exactly one option and return structured output.', + ...Object.entries(options).map(([key, option]) => `${key}: ${option.description}`), + ].join('\n'), + prompt, + output: Output.object({ + schema: decisionSchema, + }), + }); + const output = result.output as { + decision: string; + data: Record; + reasoning?: string; + }; + + return { + choice: output.decision, + data: output.data, + reasoning: output.reasoning, + }; + }, + }; +} + +export async function generateExampleObject(options: { + schema: StandardSchemaV1; + prompt: string; + system?: string; + model?: string; +}): Promise { + const result = await generateText({ + model: createExampleModel(options.model), + output: Output.object({ + schema: toZodSchema(options.schema), + }), + system: options.system, + prompt: options.prompt, + }); + + return result.output as T; +} + +export async function generateExampleText(options: { + prompt: string; + system?: string; + model?: string; +}): Promise { + const result = await generateText({ + model: createExampleModel(options.model), + system: options.system, + prompt: options.prompt, + }); + + return result.text.trim(); +} + +function resolveOpenAiModel(model: string): string { + return model.startsWith('openai/') ? model.slice('openai/'.length) : model; +} + +function toZodSchema(schema: StandardSchemaV1): z.ZodTypeAny { + if ('_zod' in schema || '_def' in schema) { + return schema as unknown as z.ZodTypeAny; + } + + return z.record(z.string(), z.unknown()); +} diff --git a/examples/adapter.ts b/examples/adapter.ts new file mode 100644 index 0000000..c3c2ee3 --- /dev/null +++ b/examples/adapter.ts @@ -0,0 +1,86 @@ +import { z } from 'zod'; +import { + createAdapter, + createAgentMachine, + decide, + type AgentAdapter, +} from '../src/index.js'; +import { + closePrompt, + createOpenAiDecisionAdapter, + formatResult, + isMain, + prompt, +} from './_run.js'; + +export function createAdapterExample( + adapter: AgentAdapter = createAdapter({ + decide: createOpenAiDecisionAdapter().decide, + }) +) { + return createAgentMachine({ + id: 'adapter-example', + schemas: { + input: z.object({ message: z.string() }), + }, + context: (input) => ({ + message: input.message, + route: null as string | null, + confidence: null as number | null, + }), + adapter, + initial: 'route', + states: { + route: decide({ + model: 'openai/gpt-5.4-nano', + prompt: ({ context }) => [ + 'Route this support request.', + 'Return billing only when the request is clearly about invoices, refunds, or charges.', + 'Otherwise return general.', + '', + context.message, + ].join('\n'), + options: { + billing: { + description: 'Send the request to billing support.', + schema: z.object({ confidence: z.number().min(0).max(1) }), + }, + general: { + description: 'Handle the request in general support.', + schema: z.object({ confidence: z.number().min(0).max(1) }), + }, + }, + reasoning: false, + onDone: ({ result }) => ({ + target: 'done', + context: { + route: result.choice, + confidence: result.data.confidence, + }, + }), + }), + done: { + type: 'final', + output: ({ context }) => ({ + route: context.route, + confidence: context.confidence, + }), + }, + }, + }); +} + +async function main() { + try { + const message = await prompt('Message to route'); + const machine = createAdapterExample(); + + console.log(formatResult(await machine.execute(machine.getInitialState({ message })))); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/branching.ts b/examples/branching.ts new file mode 100644 index 0000000..ddaef9f --- /dev/null +++ b/examples/branching.ts @@ -0,0 +1,132 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + generateExampleText, + isMain, + prompt, +} from './_run.js'; + +const branchResultSchema = z.object({ + docs: z.string(), + issues: z.string(), + code: z.string(), +}); + +const summarySchema = z.object({ + summary: z.string(), +}); + +export function createBranchingExample( + options: { + analyzeDocs?: (topic: string) => Promise; + analyzeIssues?: (topic: string) => Promise; + analyzeCode?: (topic: string) => Promise; + summarize?: (parts: { + docs: string; + issues: string; + code: string; + }) => Promise>; + } = {} +) { + return createAgentMachine({ + id: 'branching-example', + schemas: { + input: z.object({ topic: z.string() }), + }, + context: (input) => ({ + topic: input.topic, + docs: null as string | null, + issues: null as string | null, + code: null as string | null, + summary: null as string | null, + }), + initial: 'analyzing', + states: { + analyzing: { + resultSchema: branchResultSchema, + invoke: async ({ context }) => { + const [docs, issues, code] = await Promise.all([ + (options.analyzeDocs + ?? ((topic) => + generateExampleText({ + system: 'You are a repository docs analyst. Be concise and concrete.', + prompt: `Summarize what the documentation angle should cover for this topic in 2 short sentences:\n\n${topic}`, + })))(context.topic), + (options.analyzeIssues + ?? ((topic) => + generateExampleText({ + system: 'You analyze likely issue patterns and risks. Be concise and concrete.', + prompt: `Summarize the likely issue and operational concerns for this topic in 2 short sentences:\n\n${topic}`, + })))(context.topic), + (options.analyzeCode + ?? ((topic) => + generateExampleText({ + system: 'You analyze code-level implementation concerns. Be concise and concrete.', + prompt: `Summarize the likely code architecture and implementation concerns for this topic in 2 short sentences:\n\n${topic}`, + })))(context.topic), + ]); + + return { docs, issues, code }; + }, + onDone: ({ result }) => ({ + target: 'summarizing', + context: result, + }), + }, + summarizing: { + resultSchema: summarySchema, + invoke: async ({ context }) => + (options.summarize + ?? (({ docs, issues, code }) => + generateExampleObject({ + schema: summarySchema, + system: 'You synthesize technical analysis into a concise summary.', + prompt: [ + 'Combine these three perspectives into a concise high-level summary.', + '', + `Docs:\n${docs}`, + '', + `Issues:\n${issues}`, + '', + `Code:\n${code}`, + ].join('\n'), + })))({ + docs: context.docs ?? '', + issues: context.issues ?? '', + code: context.code ?? '', + }), + onDone: ({ result }) => ({ + target: 'done', + context: { summary: result.summary }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + docs: context.docs, + issues: context.issues, + code: context.code, + summary: context.summary, + }), + }, + }, + }); +} + +async function main() { + try { + const topic = await prompt('Topic'); + const machine = createBranchingExample(); + const result = await machine.execute(machine.getInitialState({ topic })); + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/chatbot.ts b/examples/chatbot.ts new file mode 100644 index 0000000..b3a9616 --- /dev/null +++ b/examples/chatbot.ts @@ -0,0 +1,162 @@ +import { z } from 'zod'; +import { createAgentMachine, decide, type AgentAdapter } from '../src/index.js'; +import { + closePrompt, + createOpenAiDecisionAdapter, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const replySchema = z.object({ + response: z.string(), +}); + +export function createChatbotExample( + options: { + adapter?: AgentAdapter; + reply?: (transcript: string[]) => Promise>; + } = {} +) { + const adapter = + options.adapter ?? + (process.env.OPENAI_API_KEY ? createOpenAiDecisionAdapter() : undefined); + const reply = + options.reply ?? + ((transcript: string[]) => + generateExampleObject({ + schema: replySchema, + system: 'You are a concise, helpful assistant in a terminal chat.', + prompt: [ + 'Write the assistant reply for the conversation below.', + 'Keep it short and directly responsive.', + '', + transcript.join('\n'), + ].join('\n'), + })); + + return createAgentMachine({ + id: 'chatbot-example', + schemas: { + events: { + 'user.message': z.object({ message: z.string() }), + 'user.exit': z.object({}), + }, + }, + context: () => ({ + transcript: [] as string[], + lastUserMessage: null as string | null, + lastAssistantMessage: null as string | null, + ended: false, + }), + adapter, + initial: 'listening', + states: { + listening: { + on: { + 'user.message': ({ event, context }) => ({ + target: 'deciding', + context: { + lastUserMessage: event.message, + transcript: [...context.transcript, `User: ${event.message}`], + }, + }), + 'user.exit': { + target: 'done', + context: { ended: true }, + }, + }, + }, + deciding: decide({ + model: 'openai/gpt-5.4-nano', + prompt: ({ context }) => + [ + 'Decide whether the assistant should answer or end the conversation.', + 'End only when the user is clearly saying goodbye or asking to stop.', + '', + (context as { transcript: string[] }).transcript.join('\n'), + ].join('\n'), + options: { + respond: { description: 'Reply to the user and continue chatting.' }, + end: { description: 'End the conversation now.' }, + }, + onDone: ({ result }) => ({ + target: result.choice === 'end' ? 'done' : 'replying', + context: result.choice === 'end' ? { ended: true } : {}, + }), + }), + replying: { + resultSchema: replySchema, + invoke: async ({ context }) => reply(context.transcript), + onDone: ({ result, context }) => ({ + target: 'listening', + context: { + lastAssistantMessage: result.response, + transcript: [...context.transcript, `Assistant: ${result.response}`], + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + transcript: context.transcript, + ended: context.ended, + lastAssistantMessage: context.lastAssistantMessage, + }), + }, + }, + }); +} + +async function main() { + try { + const machine = createChatbotExample(); + let state = machine.getInitialState(); + let lastPrintedAssistantMessage: string | null = null; + + while (true) { + const result = await machine.execute(state); + + if (result.status === 'done') { + if ( + result.output && + typeof result.output === 'object' && + 'lastAssistantMessage' in result.output && + result.output.lastAssistantMessage && + result.output.lastAssistantMessage !== lastPrintedAssistantMessage + ) { + console.log(`Assistant: ${result.output.lastAssistantMessage}`); + } + console.log(formatResult(result)); + break; + } + + if (result.status !== 'pending') { + throw new Error('Chatbot example entered an unexpected error state.'); + } + + if ( + result.context.lastAssistantMessage && + result.context.lastAssistantMessage !== lastPrintedAssistantMessage + ) { + console.log(`Assistant: ${result.context.lastAssistantMessage}`); + lastPrintedAssistantMessage = result.context.lastAssistantMessage; + } + + const message = await prompt('User (blank to exit)'); + state = machine.transition( + result.state, + message + ? { type: 'user.message', message } + : { type: 'user.exit' } + ); + } + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/classify.ts b/examples/classify.ts new file mode 100644 index 0000000..d2befb2 --- /dev/null +++ b/examples/classify.ts @@ -0,0 +1,65 @@ +import { z } from 'zod'; +import { + createAgentMachine, + classify, + type AgentAdapter, +} from '../src/index.js'; +import { + closePrompt, + createOpenAiDecisionAdapter, + formatResult, + isMain, + prompt, +} from './_run.js'; + +export function createClassifyExample( + adapter: AgentAdapter = createOpenAiDecisionAdapter() +) { + return createAgentMachine({ + id: 'classify-example', + schemas: { + input: z.object({ request: z.string() }), + }, + context: (input) => ({ + request: input.request, + category: null as string | null, + }), + adapter, + initial: 'routing', + states: { + routing: classify({ + model: 'openai/gpt-5.4-nano', + prompt: ({ context }) => `Classify this support request:\n\n${context.request}`, + into: { + billing: { description: 'Payments, invoices, refunds, and charges.' }, + technical: { description: 'Bugs, outages, and product issues.' }, + general: { description: 'Everything else.' }, + }, + onDone: ({ result }) => ({ + target: 'done', + context: { category: result.category }, + }), + }), + done: { + // use params; category should alwyas be defined when entering + type: 'final', + output: ({ context }) => ({ category: context.category }), + }, + }, + }); +} + +async function main() { + try { + const request = await prompt('Support request'); + const machine = createClassifyExample(); + + console.log(formatResult(await machine.execute(machine.getInitialState({ request })))); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/customer-service-sim.ts b/examples/customer-service-sim.ts new file mode 100644 index 0000000..bd21acf --- /dev/null +++ b/examples/customer-service-sim.ts @@ -0,0 +1,134 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const serviceReplySchema = z.object({ + response: z.string(), +}); + +const customerReplySchema = z.object({ + response: z.string(), + done: z.boolean(), + outcome: z.string().nullable(), +}); + +type TranscriptContext = { + issue: string; + transcript: string[]; + turnCount: number; + maxTurns: number; + outcome: string | null; +}; + +export function createCustomerServiceSimExample( + options: { + serviceReply?: (context: TranscriptContext) => Promise>; + customerReply?: (context: TranscriptContext) => Promise>; + maxTurns?: number; + } = {} +) { + const serviceReply = + options.serviceReply ?? + ((context: TranscriptContext) => + generateExampleObject({ + schema: serviceReplySchema, + system: 'You are a customer support agent negotiating calmly and pragmatically.', + prompt: [ + `Issue: ${context.issue}`, + `Turn count: ${context.turnCount}`, + `Current outcome: ${context.outcome ?? 'none'}`, + '', + 'Transcript so far:', + context.transcript.join('\n'), + '', + 'Write the next support agent response in one short paragraph.', + ].join('\n'), + })); + const customerReply = + options.customerReply ?? + ((context: TranscriptContext) => + generateExampleObject({ + schema: customerReplySchema, + system: 'You are the customer in the support exchange. Stay realistic and concise.', + prompt: [ + `Original issue: ${context.issue}`, + `Turn count: ${context.turnCount}`, + '', + 'Transcript so far:', + context.transcript.join('\n'), + '', + 'Write the next customer reply. Set done=true only if the issue is resolved or the customer accepts the proposed outcome. Use outcome to summarize the result when done.', + ].join('\n'), + })); + + return createAgentMachine({ + id: 'customer-service-sim-example', + schemas: { + input: z.object({ issue: z.string() }), + }, + context: (input) => ({ + issue: input.issue, + transcript: [`Customer: ${input.issue}`], + turnCount: 0, + maxTurns: options.maxTurns ?? 4, + outcome: null as string | null, + }), + initial: 'service', + states: { + service: { + resultSchema: serviceReplySchema, + invoke: async ({ context }) => serviceReply(context), + onDone: ({ result, context }) => ({ + target: context.turnCount + 1 >= context.maxTurns ? 'done' : 'customer', + context: { + transcript: [...context.transcript, `Agent: ${result.response}`], + outcome: + context.turnCount + 1 >= context.maxTurns + ? 'max-turns-reached' + : context.outcome, + }, + }), + }, + customer: { + resultSchema: customerReplySchema, + invoke: async ({ context }) => customerReply(context), + onDone: ({ result, context }) => ({ + target: result.done ? 'done' : 'service', + context: { + transcript: [...context.transcript, `Customer: ${result.response}`], + turnCount: context.turnCount + 1, + outcome: result.outcome, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + transcript: context.transcript, + turnCount: context.turnCount, + outcome: context.outcome, + }), + }, + }, + }); +} + +async function main() { + try { + const issue = await prompt('Customer issue'); + const machine = createCustomerServiceSimExample(); + console.log(formatResult(await machine.execute(machine.getInitialState({ issue })))); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/decide.ts b/examples/decide.ts new file mode 100644 index 0000000..3288ae2 --- /dev/null +++ b/examples/decide.ts @@ -0,0 +1,83 @@ +import { z } from 'zod'; +import { + createAgentMachine, + decide, + type AgentAdapter, +} from '../src/index.js'; +import { + closePrompt, + createOpenAiDecisionAdapter, + formatResult, + isMain, + prompt, +} from './_run.js'; + +export function createDecideExample(adapter: AgentAdapter = createOpenAiDecisionAdapter()) { + return createAgentMachine({ + id: 'decide-example', + schemas: { + input: z.object({ request: z.string() }), + }, + context: (input) => ({ + request: input.request, + action: null as string | null, + payload: null as Record | null, + }), + adapter, + initial: 'triage', + states: { + triage: decide({ + model: 'openai/gpt-5.4-nano', + prompt: ({ context }) => [ + 'Choose the best next step for this support request.', + 'Prefer asking a single clarification question when key facts are missing.', + '', + `Request: ${context.request}`, + ].join('\n'), + options: { + reply: { + description: 'Reply directly to the customer.', + schema: z.object({ message: z.string() }), + }, + askForClarification: { + description: 'Ask one follow-up question before proceeding.', + schema: z.object({ question: z.string() }), + }, + escalate: { + description: 'Escalate to a human specialist.', + schema: z.object({ team: z.string() }), + }, + }, + onDone: ({ result }) => ({ + target: 'done', + context: { + action: result.choice, + payload: result.data, + }, + }), + }), + done: { + type: 'final', + output: ({ context }) => ({ + action: context.action, + payload: context.payload, + }), + }, + }, + }); +} + +async function main() { + try { + const request = await prompt('Support request'); + const machine = createDecideExample(); + + console.log(formatResult(await machine.execute(machine.getInitialState({ request })))); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/email.ts b/examples/email.ts new file mode 100644 index 0000000..1f33eb3 --- /dev/null +++ b/examples/email.ts @@ -0,0 +1,250 @@ +import { z } from 'zod'; +import { createAgentMachine, decide, type AgentAdapter } from '../src/index.js'; +import { + closePrompt, + createOpenAiDecisionAdapter, + formatResult, + generateExampleObject, + generateExampleText, + isMain, + prompt, +} from './_run.js'; + +const draftSchema = z.object({ + replyEmail: z.string(), +}); + +type EmailTools = { + lookupContactName: (email: string) => Promise; + lookupAvailability: () => Promise; + createSignature: (name: string) => Promise; +}; + +export function createEmailExample( + options: { + adapter?: AgentAdapter; + tools?: Partial; + compose?: ( + input: { + email: string; + instructions: string; + clarifications: string[]; + contactName: string; + availability: string[]; + signature: string; + } + ) => Promise>; + } = {} +) { + const adapter = + options.adapter ?? + (process.env.OPENAI_API_KEY ? createOpenAiDecisionAdapter() : undefined); + const tools: EmailTools = { + lookupContactName: + options.tools?.lookupContactName ?? + (async (email) => { + const result = await generateExampleObject({ + schema: z.object({ name: z.string() }), + system: 'Infer a plausible recipient/contact name from an email thread when possible.', + prompt: `Infer the recipient or contact name from this email. If unclear, return a reasonable professional placeholder.\n\n${email}`, + }); + + return result.name; + }), + lookupAvailability: + options.tools?.lookupAvailability ?? + (async () => { + const result = await generateExampleObject({ + schema: z.object({ + availability: z.array(z.string()).min(2).max(3), + }), + system: 'Produce plausible professional meeting slots.', + prompt: + 'Return 2 or 3 plausible meeting times for next week, written in a concise natural style.', + }); + + return result.availability; + }), + createSignature: + options.tools?.createSignature ?? + (async (name) => + generateExampleText({ + system: 'Write a concise professional email signature.', + prompt: `Write a short professional sign-off for the sender named ${name}.`, + })), + }; + const compose = + options.compose ?? + (({ + email, + instructions, + clarifications, + contactName, + availability, + signature, + }) => + generateExampleObject({ + schema: draftSchema, + system: 'You write concise professional email replies.', + prompt: [ + `Incoming email:\n${email}`, + '', + `Instructions:\n${instructions}`, + '', + `Contact name: ${contactName}`, + `Availability: ${availability.join(' | ')}`, + `Signature:\n${signature}`, + clarifications.length + ? `Clarifications:\n${clarifications.map((item) => `- ${item}`).join('\n')}` + : 'Clarifications: none', + '', + 'Draft the reply email.', + ].join('\n'), + })); + + return createAgentMachine({ + id: 'email-example', + schemas: { + input: z.object({ + email: z.string(), + instructions: z.string(), + }), + events: { + 'user.answer': z.object({ answer: z.string() }), + }, + }, + context: (input) => ({ + email: input.email, + instructions: input.instructions, + clarifications: [] as string[], + questions: [] as string[], + replyEmail: null as string | null, + }), + adapter, + initial: 'checking', + states: { + checking: decide({ + model: 'openai/gpt-5.4-nano', + prompt: ({ context }) => { // why is this Record instead of a specific type? + const emailContext = context as { + email: string; + instructions: string; + clarifications: string[]; + }; + + return [ + 'Decide whether there is enough information to draft the reply email.', + 'Choose askForClarification only if key scheduling or identity details are missing.', + '', + `Email: ${emailContext.email}`, + `Instructions: ${emailContext.instructions}`, + `Clarifications: ${emailContext.clarifications.join(' | ') || 'none'}`, + ].join('\n'); + }, + options: { + askForClarification: { + description: 'Ask one or more clarifying questions before drafting.', + schema: z.object({ + questions: z.array(z.string()).min(1), + }), + }, + draft: { + description: 'Draft the email reply now.', + }, + }, + onDone: ({ result, context }) => { + const emailContext = context as { clarifications: string[] }; + + return ({ + target: + result.choice === 'askForClarification' && + emailContext.clarifications.length === 0 + ? 'clarifying' + : 'drafting', + context: + result.choice === 'askForClarification' && + emailContext.clarifications.length === 0 + ? { questions: result.data.questions } + : { questions: [] }, + }); + }, + }), + clarifying: { + on: { + 'user.answer': ({ event, context }) => ({ + target: 'checking', + context: { + clarifications: [...context.clarifications, event.answer], + questions: [], + }, + }), + }, + }, + drafting: { + resultSchema: draftSchema, + invoke: async ({ context }) => { + const contactName = await tools.lookupContactName(context.email); + const availability = await tools.lookupAvailability(); + const signature = await tools.createSignature(contactName); + + return compose({ + email: context.email, + instructions: context.instructions, + clarifications: context.clarifications, + contactName, + availability, + signature, + }); + }, + onDone: ({ result }) => ({ + target: 'done', + context: { replyEmail: result.replyEmail }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + replyEmail: context.replyEmail, + clarifications: context.clarifications, + }), + }, + }, + }); +} + +async function main() { + try { + const email = await prompt('Incoming email'); + const instructions = await prompt('Instructions'); + const machine = createEmailExample(); + let state = machine.getInitialState({ email, instructions }); + + while (true) { + const result = await machine.execute(state); + + if (result.status === 'done') { + console.log(formatResult(result)); + break; + } + + if (result.status !== 'pending') { + throw new Error('Email example entered an unexpected error state.'); + } + + if (result.value === 'clarifying') { + console.log(result.context.questions.join('\n')); + const answer = await prompt('Clarification'); + state = machine.transition(result.state, { type: 'user.answer', answer }); + continue; + } + + state = result.state; + } + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/hitl.ts b/examples/hitl.ts new file mode 100644 index 0000000..31cd220 --- /dev/null +++ b/examples/hitl.ts @@ -0,0 +1,121 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const draftSchema = z.object({ + draft: z.string(), +}); + +export function createHitlExample( + draftReply: (args: { + task: string; + notes: string[]; + }) => Promise> = async ({ task, notes }) => { + return generateExampleObject({ + schema: draftSchema, + prompt: [ + `Task: ${task}`, + '', + 'Use the notes below to draft a concise response:', + ...notes.map((note, index) => `${index + 1}. ${note}`), + ].join('\n'), + }); + } +) { + return createAgentMachine({ + id: 'hitl-example', + schemas: { + input: z.object({ task: z.string() }), + events: { + 'user.message': z.object({ message: z.string() }), + 'user.approve': z.object({}), + 'user.cancel': z.object({}), + }, + }, + context: (input) => ({ + task: input.task, + notes: [] as string[], + draft: null as string | null, + }), + initial: 'gathering', + states: { + gathering: { + on: { + 'user.message': ({ context, event }) => ({ + context: { + notes: context.notes.concat(event.message), + }, + }), + 'user.approve': { target: 'drafting' }, + 'user.cancel': { target: 'cancelled' }, + }, + }, + drafting: { + resultSchema: draftSchema, + invoke: async ({ context }) => + draftReply({ + task: context.task, + notes: context.notes, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { draft: result.draft }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ draft: context.draft }), + }, + cancelled: { + type: 'final', + output: () => ({ cancelled: true }), + }, + }, + }); +} + +async function main() { + try { + const task = await prompt('Task'); + const machine = createHitlExample(); + let state = await machine.invoke(machine.getInitialState({ task })); + + while (state.status === 'pending') { + const message = await prompt('Add note, or type /approve or /cancel'); + + if (message === '/approve') { + state = machine.transition(state, { type: 'user.approve' }); + break; + } + + if (message === '/cancel') { + state = machine.transition(state, { type: 'user.cancel' }); + break; + } + + state = machine.transition(state, { + type: 'user.message', + message, + }); + console.log({ + status: state.status, + value: state.value, + context: state.context, + }); + } + + console.log(formatResult(await machine.execute(state))); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/index.ts b/examples/index.ts new file mode 100644 index 0000000..3917d73 --- /dev/null +++ b/examples/index.ts @@ -0,0 +1,21 @@ +export { createSimpleExample } from './simple.js'; +export { createHitlExample } from './hitl.js'; +export { createDecideExample } from './decide.js'; +export { createClassifyExample } from './classify.js'; +export { createAdapterExample } from './adapter.js'; +export { createChatbotExample } from './chatbot.js'; +export { createCustomerServiceSimExample } from './customer-service-sim.js'; +export { createEmailExample } from './email.js'; +export { createJokeExample } from './joke.js'; +export { createJugsExample } from './jugs.js'; +export { createMapReduceExample } from './map-reduce.js'; +export { createNewspaperExample } from './newspaper.js'; +export { createPlanAndExecuteExample } from './plan-and-execute.js'; +export { createRaffleExample } from './raffle.js'; +export { createReactAgentExample } from './react-agent.js'; +export { createReflectionExample } from './reflection.js'; +export { createRiverCrossingExample } from './river-crossing.js'; +export { createBranchingExample } from './branching.js'; +export { createSubflowExample } from './subflow.js'; +export { createToolCallingExample } from './tool-calling.js'; +export { createTutorExample } from './tutor.js'; diff --git a/examples/joke.ts b/examples/joke.ts new file mode 100644 index 0000000..413f864 --- /dev/null +++ b/examples/joke.ts @@ -0,0 +1,111 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const jokeSchema = z.object({ + joke: z.string(), +}); + +const ratingSchema = z.object({ + rating: z.number().min(1).max(10), + explanation: z.string(), +}); + +export function createJokeExample( + options: { + tellJoke?: (topic: string) => Promise>; + rateJoke?: ( + topic: string, + joke: string + ) => Promise>; + } = {} +) { + const tellJoke = + options.tellJoke ?? + ((topic: string) => + generateExampleObject({ + schema: jokeSchema, + system: 'You write short, clean jokes.', + prompt: `Write one short joke about ${topic}.`, + })); + const rateJoke = + options.rateJoke ?? + ((topic: string, joke: string) => + generateExampleObject({ + schema: ratingSchema, + system: 'You are a joke critic. Be fair and concise.', + prompt: [ + `Topic: ${topic}`, + `Joke: ${joke}`, + '', + 'Rate the joke from 1 to 10 and explain briefly.', + ].join('\n'), + })); + + return createAgentMachine({ + id: 'joke-example', + schemas: { + input: z.object({ topic: z.string() }), + }, + context: (input) => ({ + topic: input.topic, + joke: null as string | null, + rating: null as number | null, + explanation: null as string | null, + accepted: false, + }), + initial: 'telling', + states: { + telling: { + resultSchema: jokeSchema, + invoke: async ({ context }) => tellJoke(context.topic), + onDone: ({ result }) => ({ + target: 'rating', + context: { joke: result.joke }, + }), + }, + rating: { + resultSchema: ratingSchema, + invoke: async ({ context }) => rateJoke(context.topic, context.joke ?? ''), + onDone: ({ result }) => ({ + target: 'done', + context: { + rating: result.rating, + explanation: result.explanation, + accepted: result.rating >= 7, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + topic: context.topic, + joke: context.joke, + rating: context.rating, + explanation: context.explanation, + accepted: context.accepted, + }), + }, + }, + }); +} + +async function main() { + try { + const topic = await prompt('Joke topic'); + const machine = createJokeExample(); + console.log(formatResult(await machine.execute(machine.getInitialState({ topic })))); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/jugs.ts b/examples/jugs.ts new file mode 100644 index 0000000..53fcf8f --- /dev/null +++ b/examples/jugs.ts @@ -0,0 +1,127 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { formatResult, isMain } from './_run.js'; + +const moveSchema = z.object({ + move: z + .enum(['fill5', 'pour5to3', 'empty3', 'done']) + .describe('The next move in the water jug puzzle'), + reasoning: z.string(), +}); + +const applySchema = z.object({ + jug3: z.number().int(), + jug5: z.number().int(), + step: z.string(), +}); + +function chooseWaterJugMove(jug3: number, jug5: number): z.infer { + const key = `${jug3},${jug5}`; + const plan: Record> = { + '0,0': { move: 'fill5', reasoning: 'Start by filling the larger jug.' }, + '0,5': { move: 'pour5to3', reasoning: 'Transfer water into the 3-gallon jug.' }, + '3,2': { move: 'empty3', reasoning: 'Empty the smaller jug to make room.' }, + '0,2': { move: 'pour5to3', reasoning: 'Move the remaining water into the 3-gallon jug.' }, + '2,0': { move: 'fill5', reasoning: 'Refill the 5-gallon jug.' }, + '2,5': { move: 'pour5to3', reasoning: 'Top off the 3-gallon jug to leave 4 gallons.' }, + '3,4': { move: 'done', reasoning: 'The 5-gallon jug now holds exactly 4 gallons.' }, + }; + + return plan[key] ?? { move: 'done', reasoning: 'No further move required.' }; +} + +function applyWaterJugMove( + jug3: number, + jug5: number, + move: z.infer['move'] +): z.infer { + switch (move) { + case 'fill5': + return { jug3, jug5: 5, step: 'Filled the 5-gallon jug.' }; + case 'pour5to3': { + const transfer = Math.min(3 - jug3, jug5); + return { + jug3: jug3 + transfer, + jug5: jug5 - transfer, + step: 'Poured from the 5-gallon jug into the 3-gallon jug.', + }; + } + case 'empty3': + return { jug3: 0, jug5, step: 'Emptied the 3-gallon jug.' }; + default: + return { jug3, jug5, step: 'Solved the puzzle.' }; + } +} + +export function createJugsExample() { + return createAgentMachine({ + id: 'jugs-example', + context: () => ({ + jug3: 0, + jug5: 0, + steps: [] as string[], + reasoning: [] as string[], + }), + initial: 'choosing', + states: { + choosing: { + resultSchema: moveSchema, + invoke: async ({ context }) => chooseWaterJugMove(context.jug3, context.jug5), + onDone: ({ result, context }) => { + const nextReasoning = [...context.reasoning, result.reasoning]; + + if (result.move === 'done') { + return { + target: 'done' as const, + context: { reasoning: nextReasoning }, + }; + } + + return { + target: 'applying' as const, + params: { move: result.move }, + context: { reasoning: nextReasoning }, + }; + }, + }, + applying: { + paramsSchema: z.object({ + move: moveSchema.shape.move.exclude(['done']), + }), + resultSchema: applySchema, + invoke: async ({ context, params }) => + applyWaterJugMove( + context.jug3, + context.jug5, + params.move as 'fill5' | 'pour5to3' | 'empty3' + ), + onDone: ({ result, context }) => ({ + target: 'choosing', + context: { + jug3: result.jug3, + jug5: result.jug5, + steps: [...context.steps, result.step], + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + jug3: context.jug3, + jug5: context.jug5, + steps: context.steps, + reasoning: context.reasoning, + }), + }, + }, + }); +} + +async function main() { + const machine = createJugsExample(); + console.log(formatResult(await machine.execute(machine.getInitialState()))); +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/map-reduce.ts b/examples/map-reduce.ts new file mode 100644 index 0000000..ab881fd --- /dev/null +++ b/examples/map-reduce.ts @@ -0,0 +1,120 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + generateExampleText, + isMain, + prompt, +} from './_run.js'; + +const subjectsSchema = z.object({ + subjects: z.array(z.string()), +}); + +const jokesSchema = z.object({ + jokes: z.array(z.string()), +}); + +const bestJokeSchema = z.object({ + bestJoke: z.string(), +}); + +export function createMapReduceExample( + options: { + planSubjects?: (topic: string) => Promise>; + writeJoke?: (subject: string) => Promise; + chooseBest?: (jokes: string[]) => Promise>; + } = {} +) { + return createAgentMachine({ + id: 'map-reduce-example', + schemas: { + input: z.object({ topic: z.string() }), + }, + context: (input) => ({ + topic: input.topic, + subjects: [] as string[], + jokes: [] as string[], + bestJoke: null as string | null, + }), + initial: 'planning', + states: { + planning: { + resultSchema: subjectsSchema, + invoke: async ({ context }) => + (options.planSubjects + ?? ((topic) => + generateExampleObject({ + schema: subjectsSchema, + system: 'You break a topic into a few concrete subtopics.', + prompt: `List 2 to 4 specific subtopics worth covering for: ${topic}`, + })))(context.topic), + onDone: ({ result }) => ({ + target: 'mapping', + context: { subjects: result.subjects }, + }), + }, + mapping: { + resultSchema: jokesSchema, + invoke: async ({ context }) => { + const jokes = await Promise.all( + context.subjects.map((subject) => + (options.writeJoke + ?? ((value) => + generateExampleText({ + system: 'You write one-line jokes.', + prompt: `Write one short joke about ${value}.`, + })))(subject) + ) + ); + + return { jokes }; + }, + onDone: ({ result }) => ({ + target: 'reducing', + context: { jokes: result.jokes }, + }), + }, + reducing: { + resultSchema: bestJokeSchema, + invoke: async ({ context }) => + (options.chooseBest + ?? ((jokes) => + generateExampleObject({ + schema: bestJokeSchema, + system: 'You pick the strongest joke from a list.', + prompt: ['Choose the best joke from this list:', ...jokes].join('\n'), + })))(context.jokes), + onDone: ({ result }) => ({ + target: 'done', + context: { bestJoke: result.bestJoke }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + subjects: context.subjects, + jokes: context.jokes, + bestJoke: context.bestJoke, + }), + }, + }, + }); +} + +async function main() { + try { + const topic = await prompt('Topic'); + const machine = createMapReduceExample(); + const result = await machine.execute(machine.getInitialState({ topic })); + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/newspaper.ts b/examples/newspaper.ts new file mode 100644 index 0000000..b312d1b --- /dev/null +++ b/examples/newspaper.ts @@ -0,0 +1,180 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const searchSchema = z.object({ + searchResults: z.array(z.string()), +}); + +const articleSchema = z.object({ + article: z.string(), +}); + +const critiqueSchema = z.object({ + critique: z.string().nullable(), +}); + +export function createNewspaperExample( + options: { + search?: (topic: string) => Promise>; + curate?: (topic: string, searchResults: string[]) => Promise>; + write?: (topic: string, searchResults: string[]) => Promise>; + critique?: (article: string, revisionCount: number) => Promise>; + revise?: (article: string, critique: string) => Promise>; + maxRevisions?: number; + } = {} +) { + const search = + options.search ?? + ((topic: string) => + generateExampleObject({ + schema: searchSchema, + system: 'You brainstorm plausible research leads for an article topic.', + prompt: `List 3 to 5 concise research leads or search angles for an article about ${topic}.`, + })); + const curate = + options.curate ?? + ((topic: string, searchResults: string[]) => + generateExampleObject({ + schema: searchSchema, + system: 'You curate research inputs for a focused article.', + prompt: [ + `Topic: ${topic}`, + 'Choose the best 2 or 3 research leads from the list below.', + ...searchResults.map((result) => `- ${result}`), + ].join('\n'), + })); + const write = + options.write ?? + ((topic: string, searchResults: string[]) => + generateExampleObject({ + schema: articleSchema, + system: 'You write short newspaper-style drafts in Markdown.', + prompt: [ + `Topic: ${topic}`, + 'Write a short article draft using these research leads:', + ...searchResults.map((result) => `- ${result}`), + ].join('\n'), + })); + const critique = + options.critique ?? + ((article: string, revisionCount: number) => + generateExampleObject({ + schema: critiqueSchema, + system: 'You critique article drafts. Return null when no further revision is needed.', + prompt: [ + `Revision count: ${revisionCount}`, + 'Review this article draft and either return one concise critique or null if it is ready.', + '', + article, + ].join('\n'), + })); + const revise = + options.revise ?? + ((article: string, notes: string) => + generateExampleObject({ + schema: articleSchema, + system: 'You revise article drafts while preserving the main facts.', + prompt: [ + 'Revise the article to address this critique:', + notes, + '', + article, + ].join('\n'), + })); + + return createAgentMachine({ + id: 'newspaper-example', + schemas: { + input: z.object({ topic: z.string() }), + }, + context: (input) => ({ + topic: input.topic, + searchResults: [] as string[], + article: null as string | null, + critique: null as string | null, + revisionCount: 0, + maxRevisions: options.maxRevisions ?? 2, + }), + initial: 'searching', + states: { + searching: { + resultSchema: searchSchema, + invoke: async ({ context }) => search(context.topic), + onDone: ({ result }) => ({ + target: 'curating', + context: { searchResults: result.searchResults }, + }), + }, + curating: { + resultSchema: searchSchema, + invoke: async ({ context }) => curate(context.topic, context.searchResults), + onDone: ({ result }) => ({ + target: 'writing', + context: { searchResults: result.searchResults }, + }), + }, + writing: { + resultSchema: articleSchema, + invoke: async ({ context }) => write(context.topic, context.searchResults), + onDone: ({ result }) => ({ + target: 'critiquing', + context: { article: result.article }, + }), + }, + critiquing: { + resultSchema: critiqueSchema, + invoke: async ({ context }) => + critique(context.article ?? '', context.revisionCount), + onDone: ({ result, context }) => ({ + target: + !result.critique || context.revisionCount >= context.maxRevisions + ? 'done' + : 'revising', + context: { critique: result.critique }, + }), + }, + revising: { + resultSchema: articleSchema, + invoke: async ({ context }) => + revise(context.article ?? '', context.critique ?? ''), + onDone: ({ result, context }) => ({ + target: 'critiquing', + context: { + article: result.article, + revisionCount: context.revisionCount + 1, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + topic: context.topic, + article: context.article, + revisionCount: context.revisionCount, + searchResults: context.searchResults, + }), + }, + }, + }); +} + +async function main() { + try { + const topic = await prompt('Newspaper topic'); + const machine = createNewspaperExample(); + console.log(formatResult(await machine.execute(machine.getInitialState({ topic })))); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/plan-and-execute.ts b/examples/plan-and-execute.ts new file mode 100644 index 0000000..658d673 --- /dev/null +++ b/examples/plan-and-execute.ts @@ -0,0 +1,170 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const planSchema = z.object({ + plan: z.array(z.string()).min(1).max(5), +}); + +const stepResultSchema = z.object({ + result: z.string(), +}); + +const finalAnswerSchema = z.object({ + answer: z.string(), +}); + +export function createPlanAndExecuteExample( + options: { + plan?: (goal: string) => Promise>; + executeStep?: (args: { + goal: string; + step: string; + priorResults: string[]; + }) => Promise>; + synthesize?: (args: { + goal: string; + plan: string[]; + stepResults: string[]; + }) => Promise>; + } = {} +) { + const planner = + options.plan ?? + ((goal: string) => + generateExampleObject({ + schema: planSchema, + system: 'You are a planner. Break goals into a short actionable sequence.', + prompt: `Create a short plan with 2 to 5 steps for this goal:\n\n${goal}`, + })); + const executeStep = + options.executeStep ?? + ((args: { goal: string; step: string; priorResults: string[] }) => + generateExampleObject({ + schema: stepResultSchema, + system: 'You execute one plan step at a time and report the result concisely.', + prompt: [ + `Goal: ${args.goal}`, + `Current step: ${args.step}`, + args.priorResults.length + ? `Prior results:\n${args.priorResults.map((result, index) => `${index + 1}. ${result}`).join('\n')}` + : 'Prior results: none', + '', + 'Execute the current step conceptually and return a concise result.', + ].join('\n'), + })); + const synthesize = + options.synthesize ?? + ((args: { goal: string; plan: string[]; stepResults: string[] }) => + generateExampleObject({ + schema: finalAnswerSchema, + system: 'You synthesize completed plan results into a final answer.', + prompt: [ + `Goal: ${args.goal}`, + '', + 'Plan:', + ...args.plan.map((step, index) => `${index + 1}. ${step}`), + '', + 'Step results:', + ...args.stepResults.map((result, index) => `${index + 1}. ${result}`), + '', + 'Write the final answer.', + ].join('\n'), + })); + + return createAgentMachine({ + id: 'plan-and-execute-example', + schemas: { + input: z.object({ goal: z.string() }), + }, + context: (input) => ({ + goal: input.goal, + plan: [] as string[], + stepResults: [] as string[], + answer: null as string | null, + }), + initial: 'planning', + states: { + planning: { + resultSchema: planSchema, + invoke: async ({ context }) => planner(context.goal), + onDone: ({ result }) => ({ + target: 'executing', + context: { plan: result.plan }, + params: { index: 0 } + }), + }, + executing: { + paramsSchema: z.object({ + index: z.number().int().min(0), + }), + resultSchema: stepResultSchema, + invoke: async ({ context, params }) => + executeStep({ + goal: context.goal, + step: context.plan[params.index] ?? '', + priorResults: context.stepResults, + }), + onDone: ({ result, context }) => { + const nextStepResults = [...context.stepResults, result.result]; + const nextIndex = nextStepResults.length; + + if (nextIndex < context.plan.length) { + return { + target: 'executing' as const, + context: { stepResults: nextStepResults }, + params: { index: nextIndex }, + }; + } + + return { + target: 'synthesizing' as const, + context: { stepResults: nextStepResults }, + }; + }, + }, + synthesizing: { + resultSchema: finalAnswerSchema, + invoke: async ({ context }) => + synthesize({ + goal: context.goal, + plan: context.plan, + stepResults: context.stepResults, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { answer: result.answer }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + goal: context.goal, + plan: context.plan, + stepResults: context.stepResults, + answer: context.answer, + }), + }, + }, + }); +} + +async function main() { + try { + const goal = await prompt('Goal'); + const machine = createPlanAndExecuteExample(); + console.log(formatResult(await machine.execute(machine.getInitialState({ goal })))); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/raffle.ts b/examples/raffle.ts new file mode 100644 index 0000000..4714e98 --- /dev/null +++ b/examples/raffle.ts @@ -0,0 +1,116 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const winnerSchema = z.object({ + winningEntry: z.string(), + firstRunnerUp: z.string(), + secondRunnerUp: z.string(), + explanation: z.string(), +}); + +export function createRaffleExample( + pickWinner: (entries: string[]) => Promise> = async ( + entries + ) => + generateExampleObject({ + schema: winnerSchema, + system: 'You are conducting a transparent demo raffle draw.', + prompt: [ + 'Choose one winner and two runners-up from the entries below.', + 'Do not invent names. Explain your selection briefly.', + ...entries.map((entry, index) => `${index + 1}. ${entry}`), + ].join('\n'), + }) +) { + return createAgentMachine({ + id: 'raffle-example', + schemas: { + events: { + 'user.entry': z.object({ entry: z.string() }), + 'user.draw': z.object({}), + }, + }, + context: () => ({ + entries: [] as string[], + winner: null as string | null, + firstRunnerUp: null as string | null, + secondRunnerUp: null as string | null, + explanation: null as string | null, + }), + initial: 'collecting', + states: { + collecting: { + on: { + 'user.entry': ({ event, context }) => ({ + context: { entries: [...context.entries, event.entry] }, + }), + 'user.draw': ({ context }) => ({ + target: context.entries.length >= 3 ? 'drawing' : 'collecting', + }), + }, + }, + drawing: { + resultSchema: winnerSchema, + invoke: async ({ context }) => pickWinner(context.entries), + onDone: ({ result }) => ({ + target: 'done', + context: { + winner: result.winningEntry, + firstRunnerUp: result.firstRunnerUp, + secondRunnerUp: result.secondRunnerUp, + explanation: result.explanation, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + entries: context.entries, + winner: context.winner, + firstRunnerUp: context.firstRunnerUp, + secondRunnerUp: context.secondRunnerUp, + explanation: context.explanation, + }), + }, + }, + }); +} + +async function main() { + try { + const machine = createRaffleExample(); + let state = machine.getInitialState(); + + while (true) { + const result = await machine.execute(state); + + if (result.status === 'done') { + console.log(formatResult(result)); + break; + } + + if (result.status !== 'pending') { + throw new Error('Raffle example entered an unexpected error state.'); + } + + const entry = await prompt('Entry (blank to draw)'); + state = machine.transition( + result.state, + entry ? { type: 'user.entry', entry } : { type: 'user.draw' } + ); + } + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/react-agent.ts b/examples/react-agent.ts new file mode 100644 index 0000000..ecdc3d1 --- /dev/null +++ b/examples/react-agent.ts @@ -0,0 +1,111 @@ +import { z } from 'zod'; +import { + createMemoryRunStore, + createReactAgent, + startSession, +} from '../src/index.js'; +import { + closePrompt, + generateExampleObject, + generateExampleText, + isMain, + prompt, +} from './_run.js'; + +const reactModelResultSchema = z.discriminatedUnion('kind', [ + z.object({ + kind: z.literal('tool'), + toolName: z.literal('search'), + input: z.object({ + query: z.string(), + }), + message: z.string().optional(), + }), + z.object({ + kind: z.literal('final'), + message: z.string(), + }), +]); + +export function createReactAgentExample(options: { + search?: (query: string) => Promise; + model?: (args: { + messages: Array<{ + role: 'system' | 'user' | 'assistant' | 'tool'; + content: string; + name?: string; + }>; + }) => Promise>; +} = {}) { + return createReactAgent({ + prompt: 'You are a helpful assistant.', + tools: [ + { + name: 'search', + description: 'Searches the knowledge base.', + execute: async (input) => + (options.search + ?? ((query) => + generateExampleText({ + system: 'You are a concise search backend returning a short factual result snippet.', + prompt: `Return a short search result snippet for the query: ${query}`, + })))(String(input.query)), + }, + ], + model: + options.model + ?? (({ messages }) => + generateExampleObject({ + schema: reactModelResultSchema, + system: [ + 'You are a ReAct-style assistant.', + 'If you still need outside information, call the search tool.', + 'If the latest tool result is enough, answer directly with kind="final".', + ].join('\n'), + prompt: messages + .map((message) => `${message.role.toUpperCase()}: ${message.content}`) + .join('\n'), + })), + }); +} + +async function main() { + try { + const message = await prompt('User'); + const agent = createReactAgentExample(); + const run = await startSession(agent, { + store: createMemoryRunStore(), + input: { + messages: [{ role: 'user', content: message }], + }, + }); + + run.on('toolCall', (event) => { + const call = event as { toolName: string; input: { query: string } }; + console.log(`Calling ${call.toolName}(${call.input.query})`); + }); + run.on('toolResult', (event) => { + const result = event as { + toolName: string; + output: unknown; + }; + console.log(`${result.toolName} -> ${String(result.output)}`); + }); + + await new Promise((resolve, reject) => { + run.on('done', (event) => { + console.log((event as { output: unknown }).output); + resolve(); + }); + run.on('error', (event) => { + reject((event as { error: unknown }).error); + }); + }); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/reflection.ts b/examples/reflection.ts new file mode 100644 index 0000000..86abe8e --- /dev/null +++ b/examples/reflection.ts @@ -0,0 +1,155 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const draftSchema = z.object({ + draft: z.string(), +}); + +const feedbackSchema = z.object({ + feedback: z.string().nullable(), +}); + +export function createReflectionExample( + options: { + draft?: (task: string) => Promise>; + reflect?: (args: { + task: string; + draft: string; + revisionCount: number; + }) => Promise>; + revise?: (args: { + task: string; + draft: string; + feedback: string; + }) => Promise>; + maxRevisions?: number; + } = {} +) { + const draft = + options.draft ?? + ((task: string) => + generateExampleObject({ + schema: draftSchema, + system: 'You write concise first drafts.', + prompt: `Write a short draft for this task:\n\n${task}`, + })); + const reflect = + options.reflect ?? + ((args: { task: string; draft: string; revisionCount: number }) => + generateExampleObject({ + schema: feedbackSchema, + system: 'You critique drafts and return null when no more revision is needed.', + prompt: [ + `Task: ${args.task}`, + `Revision count: ${args.revisionCount}`, + '', + 'Draft:', + args.draft, + '', + 'Return one concise revision note, or null if the draft is already good enough.', + ].join('\n'), + })); + const revise = + options.revise ?? + ((args: { task: string; draft: string; feedback: string }) => + generateExampleObject({ + schema: draftSchema, + system: 'You revise drafts to address the provided feedback.', + prompt: [ + `Task: ${args.task}`, + `Feedback: ${args.feedback}`, + '', + 'Current draft:', + args.draft, + '', + 'Revise the draft.', + ].join('\n'), + })); + + return createAgentMachine({ + id: 'reflection-example', + schemas: { + input: z.object({ task: z.string() }), + }, + context: (input) => ({ + task: input.task, + draft: null as string | null, + feedback: null as string | null, + revisionCount: 0, + maxRevisions: options.maxRevisions ?? 2, + }), + initial: 'drafting', + states: { + drafting: { + resultSchema: draftSchema, + invoke: async ({ context }) => draft(context.task), + onDone: ({ result }) => ({ + target: 'reflecting', + context: { draft: result.draft }, + }), + }, + reflecting: { + resultSchema: feedbackSchema, + invoke: async ({ context }) => + reflect({ + task: context.task, + draft: context.draft ?? '', + revisionCount: context.revisionCount, + }), + onDone: ({ result, context }) => ({ + target: + !result.feedback || context.revisionCount >= context.maxRevisions + ? 'done' + : 'revising', + context: { feedback: result.feedback }, + }), + }, + revising: { + resultSchema: draftSchema, + invoke: async ({ context }) => + revise({ + task: context.task, + draft: context.draft ?? '', + feedback: context.feedback ?? '', + }), + onDone: ({ result, context }) => ({ + target: 'reflecting', + context: { + draft: result.draft, + revisionCount: context.revisionCount + 1, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + task: context.task, + draft: context.draft, + feedback: context.feedback, + revisionCount: context.revisionCount, + }), + }, + }, + }); +} + +async function main() { + try { + const task = await prompt('Task'); + const machine = createReflectionExample(); + console.log(formatResult(await machine.execute(machine.getInitialState({ task })))); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/river-crossing.ts b/examples/river-crossing.ts new file mode 100644 index 0000000..dd738bb --- /dev/null +++ b/examples/river-crossing.ts @@ -0,0 +1,172 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { formatResult, isMain } from './_run.js'; + +const bankItem = z.enum(['wolf', 'goat', 'cabbage']); + +const crossingMoveSchema = z.object({ + move: z.enum(['takeGoat', 'takeWolf', 'takeCabbage', 'returnEmpty', 'done']), + reasoning: z.string(), +}); + +const crossingStateSchema = z.object({ + leftBank: z.array(bankItem), + rightBank: z.array(bankItem), + farmerPosition: z.enum(['left', 'right']), + step: z.string(), +}); + +function chooseCrossingMove( + leftBank: string[], + rightBank: string[], + farmerPosition: 'left' | 'right' +): z.infer { + const key = `${farmerPosition}|${leftBank.sort().join(',')}|${rightBank.sort().join(',')}`; + const plan: Record> = { + 'left|cabbage,goat,wolf|': { + move: 'takeGoat', + reasoning: 'Move the goat first so it is not left with the cabbage.', + }, + 'right|cabbage,wolf|goat': { + move: 'returnEmpty', + reasoning: 'Return alone to ferry another item.', + }, + 'left|cabbage,wolf|goat': { + move: 'takeWolf', + reasoning: 'Take the wolf across while the goat waits safely alone.', + }, + 'right|cabbage|goat,wolf': { + move: 'takeGoat', + reasoning: 'Bring the goat back so the wolf is not left with it.', + }, + 'left|cabbage,goat|wolf': { + move: 'takeCabbage', + reasoning: 'Take the cabbage across now that the goat is with you.', + }, + 'right|goat|cabbage,wolf': { + move: 'returnEmpty', + reasoning: 'Return alone to fetch the goat.', + }, + 'left|goat|cabbage,wolf': { + move: 'takeGoat', + reasoning: 'Bring the goat across to complete the crossing.', + }, + 'right||cabbage,goat,wolf': { + move: 'done', + reasoning: 'Everyone is safely across.', + }, + }; + + return plan[key] ?? { move: 'done', reasoning: 'No further move required.' }; +} + +function moveItem( + leftBank: Array<'wolf' | 'goat' | 'cabbage'>, + rightBank: Array<'wolf' | 'goat' | 'cabbage'>, + farmerPosition: 'left' | 'right', + move: z.infer['move'] +): z.infer { + const fromLeft = farmerPosition === 'left'; + + if (move === 'returnEmpty') { + return { + leftBank, + rightBank, + farmerPosition: fromLeft ? 'right' : 'left', + step: 'The farmer crossed the river alone.', + }; + } + + const item = move.replace(/^take/, '').toLowerCase() as 'wolf' | 'goat' | 'cabbage'; + return { + leftBank: fromLeft + ? leftBank.filter((value) => value !== item) + : [...leftBank, item].sort() as Array<'wolf' | 'goat' | 'cabbage'>, + rightBank: fromLeft + ? [...rightBank, item].sort() as Array<'wolf' | 'goat' | 'cabbage'> + : rightBank.filter((value) => value !== item), + farmerPosition: fromLeft ? 'right' : 'left', + step: `The farmer took the ${item} across the river.`, + }; +} + +export function createRiverCrossingExample() { + return createAgentMachine({ + id: 'river-crossing-example', + context: () => ({ + leftBank: ['wolf', 'goat', 'cabbage'] as Array<'wolf' | 'goat' | 'cabbage'>, + rightBank: [] as Array<'wolf' | 'goat' | 'cabbage'>, + farmerPosition: 'left' as 'left' | 'right', + steps: [] as string[], + reasoning: [] as string[], + }), + initial: 'choosing', + states: { + choosing: { + resultSchema: crossingMoveSchema, + invoke: async ({ context }) => + chooseCrossingMove( + [...context.leftBank], + [...context.rightBank], + context.farmerPosition + ), + onDone: ({ result, context }) => { + const nextReasoning = [...context.reasoning, result.reasoning]; + + if (result.move === 'done') { + return { + target: 'done' as const, + context: { reasoning: nextReasoning }, + }; + } + + return { + target: 'moving' as const, + params: { move: result.move }, + context: { reasoning: nextReasoning }, + }; + }, + }, + moving: { + paramsSchema: z.object({ + move: crossingMoveSchema.shape.move.exclude(['done']), + }), + resultSchema: crossingStateSchema, + invoke: async ({ context, params }) => + moveItem( + [...context.leftBank], + [...context.rightBank], + context.farmerPosition, + params.move as 'takeGoat' | 'takeWolf' | 'takeCabbage' | 'returnEmpty' + ), + onDone: ({ result, context }) => ({ + target: 'choosing', + context: { + leftBank: result.leftBank, + rightBank: result.rightBank, + farmerPosition: result.farmerPosition, + steps: [...context.steps, result.step], + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + leftBank: context.leftBank, + rightBank: context.rightBank, + steps: context.steps, + reasoning: context.reasoning, + }), + }, + }, + }); +} + +async function main() { + const machine = createRiverCrossingExample(); + console.log(formatResult(await machine.execute(machine.getInitialState()))); +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/simple.ts b/examples/simple.ts new file mode 100644 index 0000000..5ab67bb --- /dev/null +++ b/examples/simple.ts @@ -0,0 +1,66 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const summarySchema = z.object({ + summary: z.string(), +}); + +export function createSimpleExample( + summarize: (text: string) => Promise> = async ( + text + ) => { + return generateExampleObject({ + schema: summarySchema, + prompt: `Summarize this text in one sentence:\n\n${text}`, + }); + } +) { + return createAgentMachine({ + id: 'simple-example', + schemas: { + input: z.object({ text: z.string() }), + }, + context: (input) => ({ + text: input.text, + summary: null as string | null, + }), + initial: 'summarizing', + states: { + summarizing: { + resultSchema: summarySchema, + invoke: async ({ context }) => summarize(context.text), + onDone: ({ result }) => ({ + target: 'done', + context: { summary: result.summary }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ summary: context.summary }), + }, + }, + }); +} + +async function main() { + try { + const text = await prompt('Text to summarize'); + const machine = createSimpleExample(); + const result = await machine.execute(machine.getInitialState({ text })); + + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/subflow.ts b/examples/subflow.ts new file mode 100644 index 0000000..6afd439 --- /dev/null +++ b/examples/subflow.ts @@ -0,0 +1,139 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const researchSchema = z.object({ + bullets: z.array(z.string()), +}); + +const draftSchema = z.object({ + draft: z.string(), +}); + +export function createSubflowExample( + options: { + research?: (topic: string) => Promise>; + write?: (input: { + topic: string; + bullets: string[]; + }) => Promise>; + } = {} +) { + const childMachine = createAgentMachine({ + id: 'subflow-child', + schemas: { + input: z.object({ topic: z.string() }), + }, + context: (input) => ({ + topic: input.topic, + bullets: [] as string[], + }), + initial: 'researching', + states: { + researching: { + resultSchema: researchSchema, + invoke: async ({ context }) => + (options.research + ?? ((topic) => + generateExampleObject({ + schema: researchSchema, + system: 'You research a topic and return concise bullet points.', + prompt: `Return 2 to 4 concise research bullets about ${topic}.`, + })))(context.topic), + onDone: ({ result }) => ({ + target: 'done', + context: { bullets: result.bullets }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ bullets: context.bullets }), + }, + }, + }); + + return createAgentMachine({ + id: 'subflow-example', + schemas: { + input: z.object({ topic: z.string() }), + }, + context: (input) => ({ + topic: input.topic, + bullets: [] as string[], + draft: null as string | null, + }), + initial: 'researching', + states: { + researching: { + resultSchema: researchSchema, + invoke: async ({ context }) => { + const result = await childMachine.execute( + childMachine.getInitialState({ topic: context.topic }) + ); + + if (result.status !== 'done') { + throw new Error('Child machine did not finish'); + } + + return { + bullets: (result.output as { bullets: string[] }).bullets, + }; + }, + onDone: ({ result }) => ({ + target: 'writing', + context: { bullets: result.bullets }, + }), + }, + writing: { + resultSchema: draftSchema, + invoke: async ({ context }) => + (options.write + ?? (({ topic, bullets }) => + generateExampleObject({ + schema: draftSchema, + system: 'You turn research bullets into a short coherent draft.', + prompt: [ + `Topic: ${topic}`, + 'Use these bullets to write a short draft:', + ...bullets.map((bullet) => `- ${bullet}`), + ].join('\n'), + })))({ + topic: context.topic, + bullets: context.bullets, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { draft: result.draft }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + bullets: context.bullets, + draft: context.draft, + }), + }, + }, + }); +} + +async function main() { + try { + const topic = await prompt('Topic'); + const machine = createSubflowExample(); + const result = await machine.execute(machine.getInitialState({ topic })); + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/tool-calling.ts b/examples/tool-calling.ts new file mode 100644 index 0000000..cae6c45 --- /dev/null +++ b/examples/tool-calling.ts @@ -0,0 +1,119 @@ +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from '../src/index.js'; +import { + closePrompt, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const forecastSchema = z.object({ + forecast: z.string(), +}); + +export function createToolCallingExample( + getWeather: (city: string) => Promise> = async ( + city + ) => + generateExampleObject({ + schema: forecastSchema, + system: 'You generate plausible demo weather forecasts.', + prompt: `Return a short weather forecast for ${city}.`, + }) +) { + return createAgentMachine({ + id: 'tool-calling-example', + schemas: { + input: z.object({ city: z.string() }), + emitted: { + toolCall: z.object({ + toolName: z.string(), + input: z.object({ city: z.string() }), + }), + toolResult: z.object({ + toolName: z.string(), + output: forecastSchema, + }), + }, + }, + context: (input) => ({ + city: input.city, + forecast: null as string | null, + }), + initial: 'checkingWeather', + states: { + checkingWeather: { + resultSchema: forecastSchema, + invoke: async ({ context }, enq) => { + enq.emit({ + type: 'toolCall', + toolName: 'getWeather', + input: { city: context.city }, + }); + + const output = await getWeather(context.city); + + enq.emit({ + type: 'toolResult', + toolName: 'getWeather', + output, + }); + + return output; + }, + onDone: ({ result }) => ({ + target: 'done', + context: { forecast: result.forecast }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ forecast: context.forecast }), + }, + }, + }); +} + +async function main() { + try { + const city = await prompt('City'); + const machine = createToolCallingExample(); + const run = await startSession(machine, { + store: createMemoryRunStore(), + input: { city }, + }); + + run.on('toolCall', (event) => { + const tool = event as { toolName: string; input: { city: string } }; + console.log(`Calling ${tool.toolName}(${tool.input.city})`); + }); + + run.on('toolResult', (event) => { + const result = event as { + toolName: string; + output: { forecast: string }; + }; + console.log(`${result.toolName} -> ${result.output.forecast}`); + }); + + await new Promise((resolve, reject) => { + run.on('done', (event) => { + console.log((event as { output: unknown }).output); + resolve(); + }); + run.on('error', (event) => { + reject((event as { error: unknown }).error); + }); + }); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/tutor.ts b/examples/tutor.ts new file mode 100644 index 0000000..7b12af8 --- /dev/null +++ b/examples/tutor.ts @@ -0,0 +1,99 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const feedbackSchema = z.object({ + instruction: z.string(), +}); + +const responseSchema = z.object({ + response: z.string(), +}); + +export function createTutorExample( + options: { + teach?: (message: string) => Promise>; + respond?: (message: string) => Promise>; + } = {} +) { + const teach = + options.teach ?? + ((message: string) => + generateExampleObject({ + schema: feedbackSchema, + system: 'You are a Spanish tutor giving concise corrective feedback in English.', + prompt: `Give one short piece of coaching feedback for this learner message: ${message}`, + })); + const respond = + options.respond ?? + ((message: string) => + generateExampleObject({ + schema: responseSchema, + system: 'You are a friendly Spanish tutor. Reply in simple Spanish.', + prompt: `Respond to this learner message in simple Spanish and keep the conversation going: ${message}`, + })); + + return createAgentMachine({ + id: 'tutor-example', + schemas: { + input: z.object({ message: z.string() }), + }, + context: (input) => ({ + conversation: [`User: ${input.message}`], + feedback: null as string | null, + response: null as string | null, + }), + initial: 'teaching', + states: { + teaching: { + resultSchema: feedbackSchema, + invoke: async ({ context }) => + teach(context.conversation.at(-1)?.replace(/^User:\s*/, '') ?? ''), + onDone: ({ result }) => ({ + target: 'responding', + context: { feedback: result.instruction }, + }), + }, + responding: { + resultSchema: responseSchema, + invoke: async ({ context }) => + respond(context.conversation.at(-1)?.replace(/^User:\s*/, '') ?? ''), + onDone: ({ result, context }) => ({ + target: 'done', + context: { + response: result.response, + conversation: [...context.conversation, `Tutor: ${result.response}`], + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + conversation: context.conversation, + feedback: context.feedback, + response: context.response, + }), + }, + }, + }); +} + +async function main() { + try { + const message = await prompt('Say something in Spanish'); + const machine = createTutorExample(); + console.log(formatResult(await machine.execute(machine.getInitialState({ message })))); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples_old/executor.ts b/examples_old/executor.ts index b56e36c..bf56fbe 100644 --- a/examples_old/executor.ts +++ b/examples_old/executor.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; import { fromTerminal } from './helpers/helpers'; const agent = createAgent({ - model: openai('gpt-4o-mini'), + model: openai('gpt-5.4-nano'), events: { getTime: z.object({}).describe('Get the current time'), other: z.object({}).describe('Do something else'), diff --git a/examples_old/multi.ts b/examples_old/multi.ts index b82971b..1b5b74e 100644 --- a/examples_old/multi.ts +++ b/examples_old/multi.ts @@ -6,7 +6,7 @@ import { openai } from '@ai-sdk/openai'; const agent = createAgent({ name: 'multi', - model: openai('gpt-4o-mini'), + model: openai('gpt-5.4-nano'), events: { 'agent.respond': z.object({ response: z.string().describe('The response from the agent'), diff --git a/package.json b/package.json index a4b1c0e..88cb9a5 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@types/node": "^20.16.10", "dotenv": "^16.4.5", "tsdown": "^0.21.7", + "tsx": "^4.21.0", "typescript": "^5.6.2", "vitest": "^2.1.2", "zod": "^4.3.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 678ed1c..397027b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: tsdown: specifier: ^0.21.7 version: 0.21.7(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(typescript@5.9.3) + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: ^5.6.2 version: 5.9.3 @@ -168,138 +171,294 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==, tarball: https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==, tarball: https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==, tarball: https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==, tarball: https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==, tarball: https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==, tarball: https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==, tarball: https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==, tarball: https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==, tarball: https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==, tarball: https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==, tarball: https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==, tarball: https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==, tarball: https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==, tarball: https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==, tarball: https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==, tarball: https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==, tarball: https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==, tarball: https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==, tarball: https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==, tarball: https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==, tarball: https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==, tarball: https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==, tarball: https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==, tarball: https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==, tarball: https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==, tarball: https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==, tarball: https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz} engines: {node: '>=18'} @@ -777,6 +936,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==, tarball: https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz} + engines: {node: '>=18'} + hasBin: true + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, tarball: https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz} engines: {node: '>=4'} @@ -1213,6 +1377,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, tarball: https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==, tarball: https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz} + engines: {node: '>=18.0.0'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, tarball: https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz} engines: {node: '>=14.17'} @@ -1549,72 +1718,150 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm64@0.27.7': + optional: true + '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-arm@0.27.7': + optional: true + '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/android-x64@0.27.7': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.27.7': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/darwin-x64@0.27.7': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.27.7': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm64@0.27.7': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true + '@esbuild/linux-arm@0.27.7': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-ia32@0.27.7': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-loong64@0.27.7': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-mips64el@0.27.7': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.27.7': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-s390x@0.27.7': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.27.7': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-arm64@0.27.7': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-ia32@0.27.7': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + '@inquirer/external-editor@1.0.3(@types/node@20.19.30)': dependencies: chardet: 2.1.1 @@ -1986,6 +2233,35 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + esprima@4.0.1: {} estree-walker@3.0.3: @@ -2377,6 +2653,13 @@ snapshots: tslib@2.8.1: optional: true + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + typescript@5.9.3: {} unconfig-core@7.5.0: diff --git a/readme.md b/readme.md index dbca6f1..59970fe 100644 --- a/readme.md +++ b/readme.md @@ -7,4 +7,20 @@ Stately Agent is a flexible framework for building AI agents using state machine - Enabling custom **planning** abilities for agents to achieve specific goals based on state machine logic, observations, and feedback - First-class integration with the [Vercel AI SDK](https://sdk.vercel.ai/) to easily support multiple model providers, such as OpenAI, Anthropic, Google, Mistral, Groq, Perplexity, and more +## Examples + +The examples in [`examples/`](/Users/davidkpiano/Code/agent/examples) are intentionally small, run in the CLI, and use real OpenAI calls when `OPENAI_API_KEY` is set. + +Run them with `node --import tsx examples/.ts`. + +Each example demonstrates one concept: + +- [`examples/simple.ts`](/Users/davidkpiano/Code/agent/examples/simple.ts): the smallest `createAgentMachine(...)` flow with an `invoke` state that makes a real LLM call +- [`examples/hitl.ts`](/Users/davidkpiano/Code/agent/examples/hitl.ts): a human-in-the-loop machine that pauses in a pending state, accepts typed events, and drafts with an LLM after approval +- [`examples/decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts): choosing the next branch with structured branch-specific payloads +- [`examples/classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts): routing into a fixed category set when you only need a label +- [`examples/adapter.ts`](/Users/davidkpiano/Code/agent/examples/adapter.ts): plugging in a custom adapter that still uses a real OpenAI model under the hood + +Use `classify(...)` when the result is just "what kind of thing is this?" Use `decide(...)` when the result is "what should happen next?" and the chosen branch may need structured data. + **Read the documentation: [stately.ai/docs/agents](https://stately.ai/docs/agents)** diff --git a/src/agent.test.ts b/src/agent.test.ts index 6bb8d7c..7c2eaaa 100644 --- a/src/agent.test.ts +++ b/src/agent.test.ts @@ -41,7 +41,7 @@ function createSimpleMachine() { states: { idle: { on: { - start: () => ({ target: 'running' }), + start: ({ target: 'running' }), }, }, running: { @@ -156,7 +156,6 @@ function createDecideMachine(adapter: AgentAdapter) { states: { classifying: decide({ model: 'test-model', - // context is Record here, not typed from context!! prompt: ({ context }) => `Classify: ${context.issue}`, options: { billing: { description: 'Billing issues' }, @@ -166,14 +165,12 @@ function createDecideMachine(adapter: AgentAdapter) { onDone: ({ result }) => ({ target: 'handling', context: { category: result.choice }, - params: { category: result.choice }, }), }), handling: { - paramsSchema: z.object({ category: z.string() }), resultSchema: z.object({ resolution: z.string() }), - invoke: async ({ context, params }) => ({ - resolution: `Handled ${params.category} issue`, + invoke: async ({ context }) => ({ + resolution: `Handled ${context.category} issue`, }), onDone: ({ result }) => ({ target: 'done', @@ -260,7 +257,8 @@ describe('getInitialState', () => { test('rejects invalid input', () => { const machine = createHitlMachine(); // Runtime validation catches invalid input (schemas.input validates) - expect(() => machine.getInitialState({ task: 123 })).toThrow(); + const invalidInput = { task: 123 } as unknown as { task: string }; + expect(() => machine.getInitialState(invalidInput)).toThrow(); }); test('resolves string initial', () => { @@ -780,13 +778,13 @@ describe('type inference', () => { }, }); - // @ts-expect-error — 'foo' not a valid context key createAgentMachine({ id: 't2', schemas: { events: { go: z.object({}) } }, context: () => ({ count: 0 }), initial: 'idle', states: { + // @ts-expect-error — 'foo' not a valid context key idle: { on: { go: () => ({ @@ -924,6 +922,33 @@ describe('type inference', () => { expect(s.context.count).toBe(5); }); + test('schemas.input alone drives context input typing', () => { + const machine = createAgentMachine({ + id: 't-input-only', + schemas: { + input: z.object({ message: z.string() }), + }, + context: (input) => { + input.message satisfies string; + // @ts-expect-error — 'nope' does not exist on input + input.nope; + return { message: input.message, count: 0 }; + }, + initial: 'idle', + states: { + idle: { + type: 'final', + }, + }, + }); + + machine.getInitialState({ message: 'hello' }); + if (false) { + // @ts-expect-error — message must be string + machine.getInitialState({ message: 123 }); + } + }); + // ─── schemas.events ─── test('transition events typed from schemas.events', () => { diff --git a/src/ai-sdk/index.ts b/src/ai-sdk/index.ts index 1215ee2..8ba186d 100644 --- a/src/ai-sdk/index.ts +++ b/src/ai-sdk/index.ts @@ -1,4 +1,4 @@ -import { generateObject } from 'ai'; +import { generateText, Output } from 'ai'; import { z } from 'zod'; import type { AgentAdapter, StandardSchemaV1 } from '../types.js'; @@ -10,57 +10,69 @@ import type { AgentAdapter, StandardSchemaV1 } from '../types.js'; export function createAiSdkAdapter(): AgentAdapter { return { async decide({ model, prompt, options, reasoning }) { - // Build the discriminated union schema for options const optionKeys = Object.keys(options); + const allSchemaLess = Object.values(options).every((option) => !option.schema); - // Build per-option schemas - const optionSchemas: Record = {}; + if (allSchemaLess && !reasoning) { + const optionDescriptions = Object.entries(options) + .map(([key, opt]) => `- ${key}: ${opt.description}`) + .join('\n'); + + const result = await generateText({ + model: resolveModel(model), + system: `You must choose exactly one of the following options:\n${optionDescriptions}`, + prompt, + output: Output.choice({ + options: optionKeys, + }), + }); + + return { + choice: result.output, + data: {}, + }; + } + + const optionSchemas: z.ZodTypeAny[] = []; for (const [key, opt] of Object.entries(options)) { - if (opt.schema) { - // Use the provided schema as the data shape - optionSchemas[key] = z.object({ - choice: z.literal(key), - data: toZodSchema(opt.schema), - ...(reasoning ? { reasoning: z.string().describe('Chain-of-thought reasoning for this decision') } : {}), - }); - } else { - optionSchemas[key] = z.object({ - choice: z.literal(key), - data: z.object({}), - ...(reasoning ? { reasoning: z.string().describe('Chain-of-thought reasoning for this decision') } : {}), - }); - } + optionSchemas.push( + z.object({ + decision: z.literal(key), + data: opt.schema ? toZodSchema(opt.schema) : z.object({}), + ...(reasoning ? { reasoning: z.string() } : {}), + }) + ); } - // Build the union schema - const schemas = optionKeys.map((k) => optionSchemas[k]!); + const schemas = optionSchemas; const schema = schemas.length === 1 ? schemas[0]! : z.union(schemas as [z.ZodType, z.ZodType, ...z.ZodType[]]); - // Build the system prompt with option descriptions const optionDescriptions = Object.entries(options) .map(([key, opt]) => `- ${key}: ${opt.description}`) .join('\n'); - const systemPrompt = `You must choose exactly one of the following options:\n${optionDescriptions}\n\nRespond with your choice and any required data.`; + const systemPrompt = `You must choose exactly one of the following options:\n${optionDescriptions}\n\nRespond with structured output containing the chosen decision and any required data.`; - const result = await generateObject({ + const result = await generateText({ model: resolveModel(model), system: systemPrompt, prompt, - schema, + output: Output.object({ + schema, + }), }); - const obj = result.object as { - choice: string; + const obj = result.output as { + decision: string; data: Record; reasoning?: string; }; return { - choice: obj.choice, + choice: obj.decision, data: obj.data ?? {}, reasoning: obj.reasoning, }; @@ -86,7 +98,7 @@ function toZodSchema(schema: StandardSchemaV1): z.ZodType { * Resolve a model string to an AI SDK model. * Supports the `provider/model` format via the AI SDK registry. */ -function resolveModel(model: string): Parameters[0]['model'] { +function resolveModel(model: string): Parameters[0]['model'] { // The AI SDK accepts model strings when using a provider registry. // For now, return as-is — users configure their provider registry externally. return model as any; diff --git a/src/classify.ts b/src/classify.ts index c4bfff1..d0ee89c 100644 --- a/src/classify.ts +++ b/src/classify.ts @@ -1,4 +1,34 @@ -import type { ClassifyConfig } from './types.js'; +import type { + AgentAdapter, + InvokeEnqueue, + StateConfig, + TransitionResult, +} from './types.js'; + +type TransitionTargetOf = T extends { target?: infer TTarget } + ? Extract + : never; + +type HandlerTargetOf = T extends (...args: any[]) => infer TResult + ? TransitionTargetOf + : TransitionTargetOf; + +type OnTargets = TOn extends Record + ? HandlerTargetOf + : never; + +type ClassifyStateConfig< + TContext extends Record, + TTarget extends string, + TParamsByTarget extends Record, +> = Pick< + StateConfig, + 'on' +> & { + __type: 'classify'; + __classifyConfig: Record; + __decideConfig: Record; +}; /** * Create a classification state. Sugar over `decide` for simple routing — @@ -6,12 +36,37 @@ import type { ClassifyConfig } from './types.js'; * * `result.category` is typed as a union of the `into` keys. * - * Note: context in prompt callback is untyped. For typed context, use - * inline `type: 'choice'` instead. */ export function classify< + TContext extends Record, const TCategories extends Record, ->(config: ClassifyConfig): any { + TParams extends Record = Record, + TTarget extends string = string, + TParamsByTarget extends Record = {}, +>( + config: { + model: string; + adapter?: AgentAdapter; + prompt: string | ((args: { context: TContext; params: TParams }) => string); + into: TCategories; + examples?: Array<{ input: string; category: keyof TCategories & string }>; + onDone: (args: { + result: { category: keyof TCategories & string }; + context: TContext; + }) => TransitionResult; + on?: Record< + string, + ( + args: { event: any; context: TContext }, + enq: InvokeEnqueue + ) => TransitionResult + >; + } +): ClassifyStateConfig< + TContext, + TTarget, + TParamsByTarget +> { const decideOptions: Record = {}; for (const [key, val] of Object.entries(config.into)) { decideOptions[key] = { description: val.description }; @@ -19,7 +74,7 @@ export function classify< return { __type: 'classify', - __classifyConfig: config, + __classifyConfig: config as unknown as Record, __decideConfig: { model: config.model, adapter: config.adapter, @@ -32,6 +87,10 @@ export function classify< }); }, }, - on: config.on, + on: config.on as StateConfig< + TContext, + TTarget, + TParamsByTarget + >['on'], }; } diff --git a/src/decide.ts b/src/decide.ts index 4fa8fc6..b4dbfb4 100644 --- a/src/decide.ts +++ b/src/decide.ts @@ -1,4 +1,35 @@ -import type { DecideConfig, StandardSchemaV1 } from './types.js'; +import type { + AgentAdapter, + DecideResultFor, + InvokeEnqueue, + StandardSchemaV1, + StateConfig, + TransitionResult, +} from './types.js'; + +type TransitionTargetOf = T extends { target?: infer TTarget } + ? Extract + : never; + +type HandlerTargetOf = T extends (...args: any[]) => infer TResult + ? TransitionTargetOf + : TransitionTargetOf; + +type OnTargets = TOn extends Record + ? HandlerTargetOf + : never; + +type DecideStateConfig< + TContext extends Record, + TTarget extends string, + TParamsByTarget extends Record, +> = Pick< + StateConfig, + 'on' +> & { + __type: 'decide'; + __decideConfig: Record; +}; /** * Create a decision state where an LLM picks from constrained options. @@ -6,18 +37,47 @@ import type { DecideConfig, StandardSchemaV1 } from './types.js'; * * The result type is a discriminated union — `result.choice` narrows `result.data`. * - * Note: context in prompt callback is untyped. For typed context, use - * inline `type: 'choice'` instead. */ export function decide< + TContext extends Record, const TOptions extends Record< string, { description: string; schema?: StandardSchemaV1 } >, ->(config: DecideConfig): any { + TParams extends Record = Record, + TTarget extends string = string, + TParamsByTarget extends Record = {}, +>( + config: { + model: string; + adapter?: AgentAdapter; + prompt: string | ((args: { context: TContext; params: TParams }) => string); + options: TOptions; + reasoning?: boolean; + onDone: (args: { + result: DecideResultFor; + context: TContext; + }) => TransitionResult; + on?: Record< + string, + ( + args: { event: any; context: TContext }, + enq: InvokeEnqueue + ) => TransitionResult + >; + } +): DecideStateConfig< + TContext, + TTarget, + TParamsByTarget +> { return { __type: 'decide', - __decideConfig: config, - on: config.on, + __decideConfig: config as unknown as Record, + on: config.on as StateConfig< + TContext, + TTarget, + TParamsByTarget + >['on'], }; } diff --git a/src/examples.test.ts b/src/examples.test.ts new file mode 100644 index 0000000..55829a1 --- /dev/null +++ b/src/examples.test.ts @@ -0,0 +1,747 @@ +import { describe, expect, test } from 'vitest'; +import { z } from 'zod'; +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { + createChatbotExample, + createAdapterExample, + createBranchingExample, + createClassifyExample, + createCustomerServiceSimExample, + createDecideExample, + createEmailExample, + createHitlExample, + createJokeExample, + createJugsExample, + createMapReduceExample, + createNewspaperExample, + createPlanAndExecuteExample, + createRaffleExample, + createReactAgentExample, + createReflectionExample, + createRiverCrossingExample, + createSimpleExample, + createSubflowExample, + createToolCallingExample, + createTutorExample, +} from '../examples/index.js'; + +describe('curated examples', () => { + test('ships the canonical examples directory', () => { + const examplesDir = resolve(process.cwd(), 'examples'); + expect(existsSync(resolve(examplesDir, 'simple.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'hitl.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'decide.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'classify.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'adapter.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'branching.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'chatbot.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'customer-service-sim.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'email.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'joke.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'jugs.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'map-reduce.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'newspaper.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'plan-and-execute.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'raffle.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'react-agent.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'reflection.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'river-crossing.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'subflow.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'tool-calling.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'tutor.ts'))).toBe(true); + }); + + test('simple example runs to a final output', async () => { + const machine = createSimpleExample(async () => ({ + summary: 'A short summary.', + })); + const result = await machine.execute( + machine.getInitialState({ text: 'Longer source text.' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ summary: 'A short summary.' }); + } + }); + + test('hitl example exposes typed pending events', async () => { + const machine = createHitlExample(); + const result = await machine.execute( + machine.getInitialState({ task: 'Draft an answer' }) + ); + + expect(result.status).toBe('pending'); + if (result.status === 'pending') { + expect(result.value).toBe('gathering'); + expect(result.events['user.message']).toBeDefined(); + expect(result.events['user.approve']).toBeDefined(); + } + }); + + test('decide example chooses a branch and carries typed data', async () => { + const machine = createDecideExample({ + decide: async () => ({ + choice: 'askForClarification', + data: { question: 'Which order is affected?' }, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ + request: 'The customer says their invoice is wrong.', + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + action: 'askForClarification', + payload: { question: 'Which order is affected?' }, + }); + } + }); + + test('classify example reduces to a category only', async () => { + const machine = createClassifyExample({ + decide: async () => ({ + choice: 'billing', + data: {}, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ + request: 'I need help with a refund for my duplicate charge.', + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ category: 'billing' }); + } + }); + + test('adapter example uses the provided schema-aware adapter', async () => { + const machine = createAdapterExample({ + decide: async () => ({ + choice: 'billing', + data: { confidence: 0.9 }, + }), + }); + const result = await machine.execute(machine.getInitialState({ message: 'refund my last invoice' })); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + route: 'billing', + confidence: 0.9, + }); + } + }); + + test('branching example fans out plain async work and summarizes it', async () => { + const machine = createBranchingExample({ + analyzeDocs: async () => 'docs', + analyzeIssues: async () => 'issues', + analyzeCode: async () => 'code', + summarize: async ({ docs, issues, code }) => ({ + summary: `${docs}/${issues}/${code}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ topic: 'agents' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + docs: 'docs', + issues: 'issues', + code: 'code', + summary: 'docs/issues/code', + }); + } + }); + + test('decide example uses structured payloads while classify does not', async () => { + const decideMachine = createDecideExample({ + decide: async () => ({ + choice: 'reply', + data: { message: 'Hello there' }, + }), + }); + const classifyMachine = createClassifyExample({ + decide: async () => ({ + choice: 'general', + data: {}, + }), + }); + + const decideResult = await decideMachine.execute( + decideMachine.getInitialState({ + request: 'Please answer this support question.', + }) + ); + const classifyResult = await classifyMachine.execute( + classifyMachine.getInitialState({ + request: 'This is a general support question.', + }) + ); + + expect(decideResult.status).toBe('done'); + expect(classifyResult.status).toBe('done'); + + if (decideResult.status === 'done' && classifyResult.status === 'done') { + expect(decideResult.output).toEqual({ + action: 'reply', + payload: { message: 'Hello there' }, + }); + expect(classifyResult.output).toEqual({ category: 'general' }); + } + }); + + test('hitl example event schemas validate payloads', async () => { + const machine = createHitlExample(); + const pending = await machine.execute( + machine.getInitialState({ task: 'Draft an answer' }) + ); + + expect(pending.status).toBe('pending'); + if (pending.status === 'pending') { + const validation = pending.events['user.message']!['~standard'].validate({ + type: 'user.message', + message: 'Here is the missing detail', + }); + + expect(validation.issues).toBeUndefined(); + } + }); + + test('decide example uses schemas on branch payloads', async () => { + const machine = createDecideExample({ + decide: async () => ({ + choice: 'reply', + data: { message: 'Resolved' }, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ + request: 'Please respond to this support request.', + }) + ); + + expect(result.status).toBe('done'); + expect( + z + .object({ + action: z.string(), + payload: z.object({ message: z.string() }), + }) + .safeParse(result.status === 'done' ? result.output : null).success + ).toBe(true); + }); + + test('chatbot example accepts a user message and replies', async () => { + const machine = createChatbotExample({ + adapter: { + decide: async () => ({ choice: 'respond', data: {} }), + }, + reply: async () => ({ response: 'Assistant reply' }), + }); + + const pending = await machine.execute(machine.getInitialState()); + expect(pending.status).toBe('pending'); + + if (pending.status === 'pending') { + const next = machine.transition(pending.state, { + type: 'user.message', + message: 'Hello there', + }); + const result = await machine.execute(next); + + expect(result.status).toBe('pending'); + if (result.status === 'pending') { + expect(result.context.transcript).toEqual([ + 'User: Hello there', + 'Assistant: Assistant reply', + ]); + } + } + }); + + test('customer service sim example reaches a terminal outcome', async () => { + const machine = createCustomerServiceSimExample({ + serviceReply: async () => ({ response: 'We can help.' }), + customerReply: async () => ({ + response: 'Thanks, that works.', + done: true, + outcome: 'resolved', + }), + }); + + const result = await machine.execute( + machine.getInitialState({ issue: 'I want a refund.' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + transcript: [ + 'Customer: I want a refund.', + 'Agent: We can help.', + 'Customer: Thanks, that works.', + ], + turnCount: 1, + outcome: 'resolved', + }); + } + }); + + test('email example can pause for clarification and then draft using tools', async () => { + let checkCount = 0; + const machine = createEmailExample({ + adapter: { + decide: async () => { + checkCount += 1; + return checkCount === 1 + ? { + choice: 'askForClarification', + data: { questions: ['Which day should I offer?'] }, + } + : { choice: 'draft', data: {} }; + }, + }, + tools: { + lookupContactName: async () => 'Pat Lee', + lookupAvailability: async () => ['Friday at 1 PM'], + createSignature: async (name) => `Best,\n${name}`, + }, + compose: async ({ + email, + instructions, + clarifications, + contactName, + availability, + signature, + }) => ({ + replyEmail: [ + `Hi ${contactName},`, + '', + `Thanks for your note: "${email}"`, + instructions, + clarifications.join(' '), + `I am available ${availability.join(' or ')}.`, + '', + signature, + ] + .filter(Boolean) + .join('\n'), + }), + }); + + const first = await machine.execute( + machine.getInitialState({ + email: 'Can you meet next week?', + instructions: 'Reply with one specific slot.', + }) + ); + + expect(first.status).toBe('pending'); + if (first.status === 'pending') { + expect(first.context.questions).toEqual(['Which day should I offer?']); + + const next = machine.transition(first.state, { + type: 'user.answer', + answer: 'Offer Friday afternoon.', + }); + const done = await machine.execute(next); + + expect(done.status).toBe('done'); + if (done.status === 'done') { + expect( + z + .object({ + replyEmail: z.string(), + clarifications: z.array(z.string()), + }) + .safeParse(done.output).success + ).toBe(true); + } + } + }); + + test('joke example produces a rating and acceptance flag', async () => { + const machine = createJokeExample({ + tellJoke: async () => ({ joke: 'A short joke about ducks.' }), + rateJoke: async () => ({ rating: 9, explanation: 'It works.' }), + }); + + const result = await machine.execute( + machine.getInitialState({ topic: 'ducks' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + topic: 'ducks', + joke: 'A short joke about ducks.', + rating: 9, + explanation: 'It works.', + accepted: true, + }); + } + }); + + test('jugs example solves the 3 and 5 gallon puzzle', async () => { + const machine = createJugsExample(); + const result = await machine.execute(machine.getInitialState()); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + jug3: 3, + jug5: 4, + steps: [ + 'Filled the 5-gallon jug.', + 'Poured from the 5-gallon jug into the 3-gallon jug.', + 'Emptied the 3-gallon jug.', + 'Poured from the 5-gallon jug into the 3-gallon jug.', + 'Filled the 5-gallon jug.', + 'Poured from the 5-gallon jug into the 3-gallon jug.', + ], + reasoning: [ + 'Start by filling the larger jug.', + 'Transfer water into the 3-gallon jug.', + 'Empty the smaller jug to make room.', + 'Move the remaining water into the 3-gallon jug.', + 'Refill the 5-gallon jug.', + 'Top off the 3-gallon jug to leave 4 gallons.', + 'The 5-gallon jug now holds exactly 4 gallons.', + ], + }); + } + }); + + test('map-reduce example decomposes work items and reduces the result', async () => { + const machine = createMapReduceExample({ + planSubjects: async () => ({ + subjects: ['one', 'two'], + }), + writeJoke: async (subject) => `joke:${subject}`, + chooseBest: async (jokes) => ({ + bestJoke: jokes.at(-1) ?? '', + }), + }); + + const result = await machine.execute( + machine.getInitialState({ topic: 'agents' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + subjects: ['one', 'two'], + jokes: ['joke:one', 'joke:two'], + bestJoke: 'joke:two', + }); + } + }); + + test('subflow example composes a child machine inside a parent workflow', async () => { + const machine = createSubflowExample({ + research: async (topic) => ({ + bullets: [`fact about ${topic}`, `detail about ${topic}`], + }), + write: async ({ topic, bullets }) => ({ + draft: `${topic}: ${bullets.join(' / ')}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ topic: 'agents' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + bullets: ['fact about agents', 'detail about agents'], + draft: 'agents: fact about agents / detail about agents', + }); + } + }); + + test('tool-calling example emits live tool activity and completes with output', async () => { + const machine = createToolCallingExample(async (city) => ({ + forecast: `Rainy in ${city}`, + })); + + const { createMemoryRunStore, startSession } = await import('./index.js'); + const run = await startSession(machine, { + store: createMemoryRunStore(), + input: { city: 'New York' }, + }); + const events: string[] = []; + + run.on('toolCall', (event) => { + events.push(`call:${(event as { toolName: string }).toolName}`); + }); + run.on('toolResult', (event) => { + events.push(`result:${(event as { toolName: string }).toolName}`); + }); + + await new Promise((resolve, reject) => { + run.on('done', () => resolve()); + run.on('error', (event) => reject((event as { error: unknown }).error)); + }); + + expect(events).toEqual(['call:getWeather', 'result:getWeather']); + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + output: { forecast: 'Rainy in New York' }, + }) + ); + }); + + test('react agent example loops through a tool and returns a final answer', async () => { + const { createMemoryRunStore, startSession } = await import('./index.js'); + const agent = createReactAgentExample({ + search: async (query) => `result for ${query}`, + model: async ({ messages }) => { + const last = messages.at(-1); + + if (!last || last.role === 'user') { + return { + kind: 'tool' as const, + toolName: 'search', + input: { query: 'weather in sf' }, + message: 'Searching for weather in sf', + }; + } + + if (last.role === 'tool') { + return { + kind: 'final' as const, + message: `I found: ${last.content}`, + }; + } + + return { + kind: 'final' as const, + message: 'I could not complete the request.', + }; + }, + }); + const run = await startSession(agent, { + store: createMemoryRunStore(), + input: { + messages: [{ role: 'user', content: 'weather in sf' }], + }, + }); + const events: string[] = []; + + run.on('toolCall', (event) => { + events.push(`call:${(event as { toolName: string }).toolName}`); + }); + run.on('toolResult', (event) => { + events.push(`result:${(event as { toolName: string }).toolName}`); + }); + + await new Promise((resolve, reject) => { + run.on('done', () => resolve()); + run.on('error', (event) => reject((event as { error: unknown }).error)); + }); + + expect(events).toEqual(['call:search', 'result:search']); + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + output: expect.objectContaining({ + finalMessage: 'I found: result for weather in sf', + }), + }) + ); + }); + + test('newspaper example loops through critique and revision', async () => { + const machine = createNewspaperExample({ + search: async () => ({ searchResults: ['a', 'b', 'c'] }), + curate: async () => ({ searchResults: ['a', 'b'] }), + write: async () => ({ article: 'Draft article' }), + critique: async (_article, revisionCount) => ({ + critique: revisionCount === 0 ? 'Tighten the ending.' : null, + }), + revise: async (article, critique) => ({ + article: `${article} Revised: ${critique}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ topic: 'Robotics' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + topic: 'Robotics', + article: 'Draft article Revised: Tighten the ending.', + revisionCount: 1, + searchResults: ['a', 'b'], + }); + } + }); + + test('plan-and-execute example creates a plan, executes steps, and synthesizes', async () => { + const machine = createPlanAndExecuteExample({ + plan: async () => ({ + plan: ['one', 'two'], + }), + executeStep: async ({ step }) => ({ + result: `result:${step}`, + }), + synthesize: async ({ stepResults }) => ({ + answer: stepResults.join(' + '), + }), + }); + + const result = await machine.execute( + machine.getInitialState({ goal: 'ship a feature' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + goal: 'ship a feature', + plan: ['one', 'two'], + stepResults: ['result:one', 'result:two'], + answer: 'result:one + result:two', + }); + } + }); + + test('raffle example collects entries and reports a winner', async () => { + const machine = createRaffleExample(async (entries) => ({ + winningEntry: entries[1] ?? '', + firstRunnerUp: entries[0] ?? '', + secondRunnerUp: entries[2] ?? '', + explanation: 'Selected the second entry for the demo.', + })); + + const pending = await machine.execute(machine.getInitialState()); + expect(pending.status).toBe('pending'); + + if (pending.status === 'pending') { + let state = machine.transition(pending.state, { + type: 'user.entry', + entry: 'TypeScript', + }); + state = machine.transition(state, { + type: 'user.entry', + entry: 'Rust', + }); + state = machine.transition(state, { + type: 'user.entry', + entry: 'Go', + }); + state = machine.transition(state, { type: 'user.draw' }); + + const result = await machine.execute(state); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + entries: ['TypeScript', 'Rust', 'Go'], + winner: 'Rust', + firstRunnerUp: 'TypeScript', + secondRunnerUp: 'Go', + explanation: 'Selected the second entry for the demo.', + }); + } + } + }); + + test('reflection example loops through critique and revision until ready', async () => { + const machine = createReflectionExample({ + draft: async () => ({ + draft: 'Initial draft', + }), + reflect: async ({ revisionCount }) => ({ + feedback: revisionCount === 0 ? 'Clarify the main point.' : null, + }), + revise: async ({ draft, feedback }) => ({ + draft: `${draft} Revised: ${feedback}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ task: 'Explain event sourcing simply.' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + task: 'Explain event sourcing simply.', + draft: 'Initial draft Revised: Clarify the main point.', + feedback: null, + revisionCount: 1, + }); + } + }); + + test('river crossing example moves every item safely to the right bank', async () => { + const machine = createRiverCrossingExample(); + const result = await machine.execute(machine.getInitialState()); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + leftBank: [], + rightBank: ['cabbage', 'goat', 'wolf'], + steps: [ + 'The farmer took the goat across the river.', + 'The farmer crossed the river alone.', + 'The farmer took the wolf across the river.', + 'The farmer took the goat across the river.', + 'The farmer took the cabbage across the river.', + 'The farmer crossed the river alone.', + 'The farmer took the goat across the river.', + ], + reasoning: [ + 'Move the goat first so it is not left with the cabbage.', + 'Return alone to ferry another item.', + 'Take the wolf across while the goat waits safely alone.', + 'Bring the goat back so the wolf is not left with it.', + 'Take the cabbage across now that the goat is with you.', + 'Return alone to fetch the goat.', + 'Bring the goat across to complete the crossing.', + 'Everyone is safely across.', + ], + }); + } + }); + + test('tutor example gives feedback and a response', async () => { + const machine = createTutorExample({ + teach: async () => ({ instruction: 'Use a more complete sentence.' }), + respond: async () => ({ response: 'Claro, puedo ayudarte.' }), + }); + + const result = await machine.execute( + machine.getInitialState({ message: 'Yo necesito ayuda' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + conversation: [ + 'User: Yo necesito ayuda', + 'Tutor: Claro, puedo ayudarte.', + ], + feedback: 'Use a more complete sentence.', + response: 'Claro, puedo ayudarte.', + }); + } + }); +}); diff --git a/src/index.ts b/src/index.ts index f59582c..6692285 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,12 @@ export { createAgentMachine } from './machine.js'; // AI primitives export { decide } from './decide.js'; export { classify } from './classify.js'; +export { createReactAgent } from './prebuilt/react.js'; +export type { + ReactAgentMessage, + ReactAgentModelResult, + ReactTool, +} from './prebuilt/react.js'; // Adapter export { createAdapter } from './adapter.js'; diff --git a/src/invoke-events.test.ts b/src/invoke-events.test.ts index e70061a..a886ada 100644 --- a/src/invoke-events.test.ts +++ b/src/invoke-events.test.ts @@ -6,6 +6,18 @@ import { startSession, } from './index.js'; +function once( + run: { on(type: string, handler: (event: unknown) => void): () => void }, + type: string +) { + return new Promise((resolve) => { + const off = run.on(type, (event) => { + off(); + resolve(event as T); + }); + }); +} + test('invoke success is journaled as an internal machine event', async () => { const machine = createAgentMachine({ id: 'invoke-success', @@ -29,6 +41,7 @@ test('invoke success is journaled as an internal machine event', async () => { const store = createMemoryRunStore(); const run = await startSession(machine, { store }); + await once(run, 'done'); const journal = await store.loadEvents(run.sessionId); expect(run.getSnapshot()).toEqual( @@ -65,6 +78,7 @@ test('invoke failure is journaled as an internal machine event', async () => { const store = createMemoryRunStore(); const run = await startSession(machine, { store }); + await once(run, 'error'); const journal = await store.loadEvents(run.sessionId); expect(run.getSnapshot()).toEqual( @@ -84,3 +98,40 @@ test('invoke failure is journaled as an internal machine event', async () => { }), ]); }); + +test('invalid invoke results fail without journaling a done event', async () => { + const machine = createAgentMachine({ + id: 'invoke-invalid-result', + context: () => ({ count: 0 }), + initial: 'processing', + states: { + processing: { + resultSchema: z.object({ value: z.string() }), + invoke: async () => ({ value: 42 } as unknown as { value: string }), + }, + }, + }); + + const store = createMemoryRunStore(); + const run = await startSession(machine, { store }); + await once(run, 'error'); + const journal = await store.loadEvents(run.sessionId); + + expect(journal.map((event) => event.type)).toEqual([ + 'xstate.init', + 'xstate.error.invoke.processing', + ]); + expect(journal).not.toContainEqual( + expect.objectContaining({ + type: 'xstate.done.invoke.processing', + }) + ); + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + status: 'error', + error: expect.objectContaining({ + message: expect.stringContaining('Validation failed'), + }), + }) + ); +}); diff --git a/src/langgraph-equivalents/branching.test.ts b/src/langgraph-equivalents/branching.test.ts new file mode 100644 index 0000000..a06f91f --- /dev/null +++ b/src/langgraph-equivalents/branching.test.ts @@ -0,0 +1,76 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { createAgentMachine } from '../index.js'; + +test('supports branching-style orchestration with plain async fan-out inside invoke', async () => { + const machine = createAgentMachine({ + id: 'langgraph-equivalent-branching', + schemas: { + input: z.object({ topic: z.string() }), + }, + context: (input) => ({ + topic: input.topic, + docs: null as string | null, + issues: null as string | null, + code: null as string | null, + summary: null as string | null, + }), + initial: 'analyzing', + states: { + analyzing: { + resultSchema: z.object({ + docs: z.string(), + issues: z.string(), + code: z.string(), + }), + invoke: async ({ context }) => { + const [docs, issues, code] = await Promise.all([ + Promise.resolve(`docs about ${context.topic}`), + Promise.resolve(`issues about ${context.topic}`), + Promise.resolve(`code about ${context.topic}`), + ]); + + return { docs, issues, code }; + }, + onDone: ({ result }) => ({ + target: 'summarizing', + context: result, + }), + }, + summarizing: { + // paramsschema could help here, the summary has lots of string | null + resultSchema: z.object({ summary: z.string() }), + invoke: async ({ context }) => ({ + summary: [context.docs, context.issues, context.code].join(' | '), + }), + onDone: ({ result }) => ({ + target: 'done', + context: { summary: result.summary }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + docs: context.docs, + issues: context.issues, + code: context.code, + summary: context.summary, + }), + }, + }, + }); + + const result = await machine.execute( + machine.getInitialState({ topic: 'agents' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + docs: 'docs about agents', + issues: 'issues about agents', + code: 'code about agents', + summary: 'docs about agents | issues about agents | code about agents', + }); + } +}); diff --git a/src/langgraph-equivalents/graph.test.ts b/src/langgraph-equivalents/graph.test.ts new file mode 100644 index 0000000..33b1b45 --- /dev/null +++ b/src/langgraph-equivalents/graph.test.ts @@ -0,0 +1,118 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { createAgentMachine } from '../index.js'; + +test('supports multi-step workflow accumulation like a sequential state graph', async () => { + const machine = createAgentMachine({ + id: 'langgraph-equivalent-sequence', + context: () => ({ messages: [] as string[] }), + initial: 'node1', + states: { + node1: { + resultSchema: z.object({ messages: z.array(z.string()) }), + invoke: async () => ({ messages: ['from node1'] }), + onDone: ({ result, context }) => ({ + target: 'node2', + context: { messages: [...context.messages, ...result.messages] }, + }), + }, + node2: { + resultSchema: z.object({ messages: z.array(z.string()) }), + invoke: async () => ({ messages: ['from node2'] }), + onDone: ({ result, context }) => ({ + target: 'node3', + context: { messages: [...context.messages, ...result.messages] }, + }), + }, + node3: { + resultSchema: z.object({ messages: z.array(z.string()) }), + invoke: async () => ({ messages: ['from node3'] }), + onDone: ({ result, context }) => ({ + target: 'done', + context: { messages: [...context.messages, ...result.messages] }, + }), + }, + done: { + type: 'final', + output: ({ context }) => context, + }, + }, + }); + + const result = await machine.execute(machine.getInitialState()); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + messages: ['from node1', 'from node2', 'from node3'], + }); + } +}); + +test('supports conditional routing with explicit machine transitions', async () => { + const machine = createAgentMachine({ + id: 'langgraph-equivalent-routing', + schemas: { + input: z.object({ request: z.string() }), + }, + context: (input) => ({ + request: input.request, + route: null as string | null, + handledBy: null as string | null, + }), + initial: 'routeRequest', + states: { + routeRequest: { + resultSchema: z.object({ + route: z.enum(['billing', 'general']), + }), + invoke: async ({ context }) => { + const route = context.request.toLowerCase().includes('refund') + ? 'billing' + : 'general'; + + return { route } as const; + }, + onDone: ({ result }) => ({ + target: result.route, + context: { route: result.route }, + }), + }, + billing: { + resultSchema: z.object({ handledBy: z.literal('billing') }), + invoke: async () => ({ handledBy: 'billing' as const }), // why do we need to cast to const here? + onDone: ({ result }) => ({ + target: 'done', + context: { handledBy: result.handledBy }, + }), + }, + general: { + resultSchema: z.object({ handledBy: z.literal('general') }), + invoke: async () => ({ handledBy: 'general' as const }), + onDone: ({ result }) => ({ + target: 'done', + context: { handledBy: result.handledBy }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + route: context.route, + handledBy: context.handledBy, + }), + }, + }, + }); + + const result = await machine.execute( + machine.getInitialState({ request: 'I need a refund for my invoice.' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + route: 'billing', + handledBy: 'billing', + }); + } +}); diff --git a/src/langgraph-equivalents/hitl.test.ts b/src/langgraph-equivalents/hitl.test.ts new file mode 100644 index 0000000..3cf8f6e --- /dev/null +++ b/src/langgraph-equivalents/hitl.test.ts @@ -0,0 +1,76 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { createAgentMachine } from '../index.js'; + +test('supports human-in-the-loop review with explicit pending states and external events', async () => { + const machine = createAgentMachine({ + id: 'langgraph-equivalent-hitl', + schemas: { + input: z.object({ task: z.string() }), + events: { + approve: z.object({}), + revise: z.object({ note: z.string() }), + }, + }, + context: (input) => ({ + task: input.task, + notes: [] as string[], + draft: null as string | null, + }), + initial: 'drafting', + states: { + drafting: { + resultSchema: z.object({ draft: z.string() }), + invoke: async ({ context }) => ({ + draft: `Draft for ${context.task}${context.notes.length ? ` (${context.notes.join(', ')})` : ''}`, + }), + onDone: ({ result }) => ({ + target: 'review', + context: { draft: result.draft }, + }), + }, + review: { + on: { + approve: { target: 'done' }, + revise: ({ event, context }) => ({ + target: 'drafting', + context: { notes: [...context.notes, event.note] }, + }), + }, + }, + done: { + type: 'final', + output: ({ context }) => ({ draft: context.draft }), + }, + }, + }); + + const first = await machine.execute( + machine.getInitialState({ task: 'reply to customer' }) + ); + + expect(first.status).toBe('pending'); + if (first.status !== 'pending') return; + + expect(first.value).toBe('review'); + expect(first.context.draft).toContain('reply to customer'); + + const revised = machine.transition(first.state, { + type: 'revise', + note: 'make it shorter', + }); + const second = await machine.execute(revised); + + expect(second.status).toBe('pending'); + if (second.status !== 'pending') return; + + const approved = machine.transition(second.state, { type: 'approve' }); + const done = await machine.execute(approved); + + expect(done.status).toBe('done'); + if (done.status === 'done') { + expect(done.output).toEqual({ + draft: 'Draft for reply to customer (make it shorter)', + }); + } +}); diff --git a/src/langgraph-equivalents/map-reduce.test.ts b/src/langgraph-equivalents/map-reduce.test.ts new file mode 100644 index 0000000..34e0762 --- /dev/null +++ b/src/langgraph-equivalents/map-reduce.test.ts @@ -0,0 +1,79 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { createAgentMachine } from '../index.js'; + +test('supports map-reduce style orchestration with dynamic work items inside invoke', async () => { + const machine = createAgentMachine({ + id: 'langgraph-equivalent-map-reduce', + schemas: { + input: z.object({ topic: z.string() }), + }, + context: (input) => ({ + topic: input.topic, + subjects: [] as string[], + jokes: [] as string[], + bestJoke: null as string | null, + }), + initial: 'planning', + states: { + planning: { + resultSchema: z.object({ subjects: z.array(z.string()) }), + invoke: async ({ context }) => ({ + subjects: [`${context.topic} basics`, `${context.topic} advanced`], + }), + onDone: ({ result }) => ({ + target: 'mapping', + context: { subjects: result.subjects }, + }), + }, + mapping: { + resultSchema: z.object({ jokes: z.array(z.string()) }), + invoke: async ({ context }) => { + const jokes = await Promise.all( + context.subjects.map(async (subject) => `joke about ${subject}`) + ); + + return { jokes }; + }, + onDone: ({ result }) => ({ + target: 'reducing', + context: { jokes: result.jokes }, + }), + }, + reducing: { + resultSchema: z.object({ bestJoke: z.string() }), + invoke: async ({ context }) => ({ + bestJoke: context.jokes[0] ?? '', + }), + onDone: ({ result }) => ({ + target: 'done', + context: { bestJoke: result.bestJoke }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + subjects: context.subjects, + jokes: context.jokes, + bestJoke: context.bestJoke, + }), + }, + }, + }); + + const result = await machine.execute( + machine.getInitialState({ topic: 'state machines' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + subjects: ['state machines basics', 'state machines advanced'], + jokes: [ + 'joke about state machines basics', + 'joke about state machines advanced', + ], + bestJoke: 'joke about state machines basics', + }); + } +}); diff --git a/src/langgraph-equivalents/persistence.test.ts b/src/langgraph-equivalents/persistence.test.ts new file mode 100644 index 0000000..be6f53f --- /dev/null +++ b/src/langgraph-equivalents/persistence.test.ts @@ -0,0 +1,88 @@ +import { expect, test, vi } from 'vitest'; +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + restoreSession, + startSession, +} from '../index.js'; + +test('persists and restores a long-running approval workflow', async () => { + const machine = createAgentMachine({ + id: 'langgraph-equivalent-persistence', + context: () => ({ + approved: false, + summary: null as string | null, + }), + initial: 'review', + states: { + review: { + on: { + approve: { + target: 'summarize', + context: { approved: true }, + }, + }, + }, + summarize: { + resultSchema: z.object({ summary: z.string() }), + invoke: async ({ context }) => ({ + summary: context.approved ? 'approved summary' : 'rejected summary', + }), + onDone: ({ result }) => ({ + target: 'done', + context: { summary: result.summary }, + }), + }, + done: { + type: 'final', + output: ({ context }) => context, + }, + }, + }); + + const baseStore = createMemoryRunStore(); + let snapshotWrites = 0; + const store = { + append: baseStore.append, + loadEvents: baseStore.loadEvents, + loadLatestSnapshot: baseStore.loadLatestSnapshot, + async saveSnapshot(snapshot: Awaited< + ReturnType + > extends infer TSaved + ? Exclude + : never) { + snapshotWrites += 1; + if (snapshotWrites === 1) { + await baseStore.saveSnapshot(snapshot); + } + }, + }; + + const liveRun = await startSession(machine, { store }); + await liveRun.send({ type: 'approve' }); + + const restoredRun = await restoreSession(machine, { + sessionId: liveRun.sessionId, + store, + }); + + await vi.waitFor(() => { + expect(restoredRun.getSnapshot()).toEqual(liveRun.getSnapshot()); + }); + + expect(restoredRun.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + context: { + approved: true, + summary: 'approved summary', + }, + output: { + approved: true, + summary: 'approved summary', + }, + }) + ); +}); diff --git a/src/langgraph-equivalents/plan-and-execute.test.ts b/src/langgraph-equivalents/plan-and-execute.test.ts new file mode 100644 index 0000000..781b646 --- /dev/null +++ b/src/langgraph-equivalents/plan-and-execute.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from 'vitest'; +import { createPlanAndExecuteExample } from '../../examples/plan-and-execute.js'; + +test('plan-and-execute workflow decomposes a goal and synthesizes a final answer', async () => { + const machine = createPlanAndExecuteExample({ + plan: async () => ({ + plan: ['inspect docs', 'inspect code', 'summarize findings'], + }), + executeStep: async ({ step }) => ({ + result: `done:${step}`, + }), + synthesize: async ({ stepResults }) => ({ + answer: stepResults.join(' | '), + }), + }); + + const result = await machine.execute( + machine.getInitialState({ goal: 'understand the repo' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + goal: 'understand the repo', + plan: ['inspect docs', 'inspect code', 'summarize findings'], + stepResults: [ + 'done:inspect docs', + 'done:inspect code', + 'done:summarize findings', + ], + answer: + 'done:inspect docs | done:inspect code | done:summarize findings', + }); + } +}); diff --git a/src/langgraph-equivalents/prebuilt-react.test.ts b/src/langgraph-equivalents/prebuilt-react.test.ts new file mode 100644 index 0000000..ec4cfb0 --- /dev/null +++ b/src/langgraph-equivalents/prebuilt-react.test.ts @@ -0,0 +1,89 @@ +import { expect, test } from 'vitest'; +import { + createMemoryRunStore, + createReactAgent, + startSession, +} from '../index.js'; + +function once( + run: { on(type: string, handler: (event: unknown) => void): () => void }, + type: string +) { + return new Promise((resolve) => { + let off = () => {}; + off = run.on(type, (event) => { + off(); + resolve(event as T); + }); + }); +} + +test('prebuilt react agent loops through a tool call and returns a final answer', async () => { + const agent = createReactAgent({ + prompt: 'You are helpful.', + tools: [ + { + name: 'search', + description: 'Searches for a query', + execute: async (input) => `result for ${String(input.query)}`, + }, + ], + model: async ({ messages }) => { + const last = messages.at(-1); + + if (!last || last.role === 'user') { + return { + kind: 'tool' as const, + toolName: 'search', + input: { query: 'weather in sf' }, + message: 'I should search first.', + }; + } + + if (last.role === 'tool') { + return { + kind: 'final' as const, + message: `Answer based on: ${last.content}`, + }; + } + + throw new Error('Unexpected transcript state'); + }, + }); + + const run = await startSession(agent, { + store: createMemoryRunStore(), + input: { + messages: [{ role: 'user', content: 'What is the weather?' }], + }, + }); + const toolEvents: string[] = []; + + run.on('toolCall', (event) => { + toolEvents.push(`call:${(event as { toolName: string }).toolName}`); + }); + run.on('toolResult', (event) => { + toolEvents.push(`result:${(event as { toolName: string }).toolName}`); + }); + + await once(run, 'done'); + + expect(toolEvents).toEqual(['call:search', 'result:search']); + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { + finalMessage: 'Answer based on: result for weather in sf', + messages: [ + { role: 'system', content: 'You are helpful.' }, + { role: 'user', content: 'What is the weather?' }, + { role: 'assistant', content: 'I should search first.' }, + { role: 'tool', name: 'search', content: 'result for weather in sf' }, + { role: 'assistant', content: 'Answer based on: result for weather in sf' }, + ], + steps: 2, + }, + }) + ); +}); diff --git a/src/langgraph-equivalents/reflection.test.ts b/src/langgraph-equivalents/reflection.test.ts new file mode 100644 index 0000000..f248a2f --- /dev/null +++ b/src/langgraph-equivalents/reflection.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from 'vitest'; +import { createReflectionExample } from '../../examples/reflection.js'; + +test('reflection workflow revises a draft until critique is cleared', async () => { + const machine = createReflectionExample({ + draft: async () => ({ + draft: 'Initial draft', + }), + reflect: async ({ revisionCount }) => ({ + feedback: revisionCount === 0 ? 'Add more detail.' : null, + }), + revise: async ({ draft, feedback }) => ({ + draft: `${draft} Revised: ${feedback}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ task: 'Write a short explanation.' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + task: 'Write a short explanation.', + draft: 'Initial draft Revised: Add more detail.', + feedback: null, + revisionCount: 1, + }); + } +}); diff --git a/src/langgraph-equivalents/streaming.test.ts b/src/langgraph-equivalents/streaming.test.ts new file mode 100644 index 0000000..779b3ad --- /dev/null +++ b/src/langgraph-equivalents/streaming.test.ts @@ -0,0 +1,71 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from '../index.js'; + +function once( + run: { on(type: string, handler: (event: unknown) => void): () => void }, + type: string +) { + return new Promise((resolve) => { + let off = () => {}; + off = run.on(type, (event) => { + off(); + resolve(event as T); + }); + }); +} + +test('streams live invoke output while preserving durable state history', async () => { + const machine = createAgentMachine({ + id: 'langgraph-equivalent-streaming', + schemas: { + emitted: { + textPart: z.object({ delta: z.string() }), + }, + }, + context: () => ({ text: '' }), + initial: 'write', + states: { + write: { + resultSchema: z.object({ text: z.string() }), + invoke: async (_args, enq) => { + enq.emit({ type: 'textPart', delta: 'hello' }); + enq.emit({ type: 'textPart', delta: ' world' }); + return { text: 'hello world' }; + }, + onDone: ({ result }) => ({ + target: 'done', + context: { text: result.text }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ text: context.text }), + }, + }, + }); + + const run = await startSession(machine, { + store: createMemoryRunStore(), + }); + const liveParts: string[] = []; + + run.on('textPart', (part) => { + liveParts.push((part as { delta: string }).delta); + }); + + await once(run, 'done'); + + expect(liveParts).toEqual(['hello', ' world']); + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { text: 'hello world' }, + }) + ); +}); diff --git a/src/langgraph-equivalents/subflow.test.ts b/src/langgraph-equivalents/subflow.test.ts new file mode 100644 index 0000000..4da8014 --- /dev/null +++ b/src/langgraph-equivalents/subflow.test.ts @@ -0,0 +1,101 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { createAgentMachine } from '../index.js'; + +test('supports subflow composition by executing a child machine inside a parent invoke', async () => { + const childMachine = createAgentMachine({ + id: 'child-research', + schemas: { + input: z.object({ topic: z.string() }), + }, + context: (input) => ({ + topic: input.topic, + bullets: [] as string[], + }), + initial: 'researching', + states: { + researching: { + resultSchema: z.object({ bullets: z.array(z.string()) }), + invoke: async ({ context }) => ({ + bullets: [`fact about ${context.topic}`, `another fact about ${context.topic}`], + }), + onDone: ({ result }) => ({ + target: 'done', + context: { bullets: result.bullets }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ bullets: context.bullets }), + }, + }, + }); + + const parentMachine = createAgentMachine({ + id: 'parent-writer', + schemas: { + input: z.object({ topic: z.string() }), + }, + context: (input) => ({ + topic: input.topic, + bullets: [] as string[], + draft: null as string | null, + }), + initial: 'researching', + states: { + researching: { + resultSchema: z.object({ bullets: z.array(z.string()) }), + invoke: async ({ context }) => { + const result = await childMachine.execute( + childMachine.getInitialState({ topic: context.topic }) + ); + + if (result.status !== 'done') { + throw new Error('Child machine did not finish'); + } + + return { + bullets: (result.output as { bullets: string[] }).bullets, + }; + }, + onDone: ({ result }) => ({ + target: 'writing', + context: { bullets: result.bullets }, + }), + }, + writing: { + resultSchema: z.object({ draft: z.string() }), + invoke: async ({ context }) => ({ + draft: `${context.topic}: ${context.bullets.join('; ')}`, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { draft: result.draft }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + bullets: context.bullets, + draft: context.draft, + }), + }, + }, + }); + + const result = await parentMachine.execute( + parentMachine.getInitialState({ topic: 'state machines' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + bullets: [ + 'fact about state machines', + 'another fact about state machines', + ], + draft: + 'state machines: fact about state machines; another fact about state machines', + }); + } +}); diff --git a/src/langgraph-equivalents/tool-calling.test.ts b/src/langgraph-equivalents/tool-calling.test.ts new file mode 100644 index 0000000..0d290b7 --- /dev/null +++ b/src/langgraph-equivalents/tool-calling.test.ts @@ -0,0 +1,97 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from '../index.js'; + +function once( + run: { on(type: string, handler: (event: unknown) => void): () => void }, + type: string +) { + return new Promise((resolve) => { + let off = () => {}; + off = run.on(type, (event) => { + off(); + resolve(event as T); + }); + }); +} + +test('supports tool-call style invokes with live tool events and final output', async () => { + const machine = createAgentMachine({ + id: 'langgraph-equivalent-tool-calling', + schemas: { + emitted: { + toolCall: z.object({ + toolName: z.string(), + input: z.object({ city: z.string() }), + }), + toolResult: z.object({ + toolName: z.string(), + output: z.object({ forecast: z.string() }), + }), + }, + input: z.object({ city: z.string() }), + }, + context: (input) => ({ + city: input.city, + forecast: null as string | null, + }), + initial: 'checkingWeather', + states: { + checkingWeather: { + resultSchema: z.object({ forecast: z.string() }), + invoke: async ({ context }, enq) => { + enq.emit({ + type: 'toolCall', + toolName: 'getWeather', + input: { city: context.city }, + }); + + const output = { forecast: `Sunny in ${context.city}` }; + enq.emit({ + type: 'toolResult', + toolName: 'getWeather', + output, + }); + + return output; + }, + onDone: ({ result }) => ({ + target: 'done', + context: { forecast: result.forecast }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ forecast: context.forecast }), + }, + }, + }); + + const run = await startSession(machine, { + store: createMemoryRunStore(), + input: { city: 'Boston' }, + }); + const events: string[] = []; + + run.on('toolCall', (event) => { + events.push(`call:${(event as { toolName: string }).toolName}`); + }); + run.on('toolResult', (event) => { + events.push(`result:${(event as { toolName: string }).toolName}`); + }); + + await once(run, 'done'); + + expect(events).toEqual(['call:getWeather', 'result:getWeather']); + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { forecast: 'Sunny in Boston' }, + }) + ); +}); diff --git a/src/machine.ts b/src/machine.ts index 1c521f0..322699a 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -46,7 +46,10 @@ type StateNodeDef< TParams, TResult, TEvents, -> = { + TTarget extends string, + TParamsMap extends Record, +> = + | { type?: 'final' | 'choice'; paramsSchema?: StandardSchemaV1; resultSchema?: StandardSchemaV1; @@ -55,11 +58,11 @@ type StateNodeDef< params: NoInfer; signal?: AbortSignal; }, enq: { emit(part: EmittedPart): void }) => Promise>; - onDone?: (args: { result: OnDoneResult; context: TContext }) => TransitionResult; - on?: { [E in keyof TEvents & string]?: TransitionResult | ((args: { + onDone?: (args: { result: OnDoneResult; context: TContext }) => TransitionResult; + on?: { [E in keyof TEvents & string]?: TransitionResult | ((args: { event: EventFor; context: TContext; - }) => TransitionResult) }; + }, enq: { emit(part: EmittedPart): void }) => TransitionResult) }; events?: Record; output?: (args: { context: TContext }) => unknown; // choice-specific @@ -71,7 +74,13 @@ type StateNodeDef< // internal __type?: 'decide' | 'classify'; __decideConfig?: Record; -}; +} + | { + on?: StateConfigAny['on']; + __type: 'decide' | 'classify'; + __decideConfig: Record; + __classifyConfig?: Record; + }; type StatesMap< TContext extends Record, @@ -79,7 +88,14 @@ type StatesMap< TResultMap extends Record, TEvents, > = { - [K in keyof TParamsMap & keyof TResultMap]: StateNodeDef; + [K in keyof TParamsMap & keyof TResultMap]: StateNodeDef< + TContext, + TParamsMap[K], + TResultMap[K], + TEvents, + keyof TParamsMap & keyof TResultMap & string, + TParamsMap + >; }; // ─── Overload A: schemas.context present ─── @@ -248,10 +264,59 @@ export function createAgentMachine( state: AgentState, event: { type: string; [k: string]: unknown } ): AgentState { + return transitionWithEffects(state, event).next; + } + + function transitionWithEffects( + state: AgentState, + event: { type: string; [k: string]: unknown }, + onEmit?: (part: EmittedPart) => void + ): { next: AgentState; emitted: EmittedPart[] } { + const emitted: EmittedPart[] = []; + const enqueue = createEnqueue((part) => { + emitted.push(part); + onEmit?.(part); + }); const sc = resolveStateConfig(cfg, state.value); - const effectiveConfig = sc.__decideConfig - ? { ...sc, ...(sc.__decideConfig as Record) } - : sc; + const effectiveConfig = getEffectiveStateConfig(state.value); + + function applyResult( + result: TransitionResult, + status = state.status + ): AgentState { + if (result.target) { + return applyTransition(state, result); + } + + return { + ...state, + status, + context: result.context + ? { ...state.context, ...result.context } + : state.context, + }; + } + + function resolveHandlerResult( + handler: + | TransitionResult + | ((args: { + event: { type: string; [k: string]: unknown }; + context: Record; + }, enq: { emit(part: EmittedPart): void }) => TransitionResult), + status = state.status + ): { next: AgentState; emitted: EmittedPart[] } { + const result: TransitionResult = + typeof handler === 'function' + ? handler({ context: state.context, event }, enqueue) + : handler; + + return { + next: applyResult(result, status), + emitted, + }; + } + if (isDoneInvokeEventType(state.value, event.type)) { const result = 'output' in event ? event.output : undefined; const validatedResult = effectiveConfig.resultSchema @@ -264,66 +329,33 @@ export function createAgentMachine( context: state.context, }); - if (trans.target) { - return applyTransition(state, trans); - } - return { - ...state, - status: 'pending', - context: trans.context - ? { ...state.context, ...trans.context } - : state.context, + next: applyResult(trans, 'pending'), + emitted, }; } const internalHandler = sc.on?.[event.type]; if (internalHandler !== undefined) { - const result: TransitionResult = - typeof internalHandler === 'function' - ? internalHandler({ context: state.context, event }) - : internalHandler; - - if (result.target) { - return applyTransition(state, result); - } - - return { - ...state, - status: 'pending', - context: result.context - ? { ...state.context, ...result.context } - : state.context, - }; + return resolveHandlerResult(internalHandler, 'pending'); } - return { ...state, status: 'pending' }; + return { next: { ...state, status: 'pending' }, emitted }; } if (isErrorInvokeEventType(state.value, event.type)) { const internalHandler = sc.on?.[event.type]; if (internalHandler !== undefined) { - const result: TransitionResult = - typeof internalHandler === 'function' - ? internalHandler({ context: state.context, event }) - : internalHandler; - - if (result.target) { - return applyTransition(state, result); - } - - return { - ...state, - context: result.context - ? { ...state.context, ...result.context } - : state.context, - }; + return resolveHandlerResult(internalHandler); } return { - ...state, - status: 'error', - error: 'error' in event ? event.error : undefined, + next: { + ...state, + status: 'error', + error: 'error' in event ? event.error : undefined, + }, + emitted, }; } @@ -331,21 +363,7 @@ export function createAgentMachine( if (sc.on?.[event.type] !== undefined) { const handler = sc.on[event.type]!; - const result: TransitionResult = - typeof handler === 'function' - ? handler({ context: state.context, event }) - : handler; - - if (result.target) { - return applyTransition(state, result); - } - - return { - ...state, - context: result.context - ? { ...state.context, ...result.context } - : state.context, - }; + return resolveHandlerResult(handler); } throw new Error( @@ -353,6 +371,25 @@ export function createAgentMachine( ); } + function getEffectiveStateConfig(value: string): StateConfigAny { + const sc = resolveStateConfig(cfg, value); + return sc.__decideConfig + ? { ...sc, ...(sc.__decideConfig as Record) } + : sc; + } + + function validateReplayableResult( + value: string, + result: unknown + ): unknown { + const effectiveConfig = getEffectiveStateConfig(value); + if (!effectiveConfig.resultSchema) { + return result; + } + + return validateSchemaSync(effectiveConfig.resultSchema, result); + } + function validateEventPayload( value: string, event: { type: string } @@ -374,6 +411,17 @@ export function createAgentMachine( } } + function toInvokeErrorEvent( + state: AgentState, + error: unknown + ): JournalEvent { + return { + type: `xstate.error.invoke.${state.value}`, + error: serializeError(error), + at: Date.now(), + }; + } + function validateEmittedPart(part: EmittedPart): void { const schema = findEmittedSchema(cfg, part.type); if (!schema) { @@ -403,10 +451,7 @@ export function createAgentMachine( } async function createChoiceEvent(state: AgentState): Promise { - const sc = resolveStateConfig(cfg, state.value); - const dc = sc.__decideConfig - ? { ...sc, ...(sc.__decideConfig as Record) } - : sc; + const dc = getEffectiveStateConfig(state.value); const adapter = (dc as StateConfigAny).adapter ?? cfg.adapter; if (!adapter) { return { @@ -429,10 +474,11 @@ export function createAgentMachine( options: (dc as StateConfigAny).options!, reasoning: (dc as StateConfigAny).reasoning, }); + const validatedResult = validateReplayableResult(state.value, result); return { type: `xstate.done.invoke.${state.value}`, - output: result, + output: validatedResult, at: Date.now(), }; } catch (error) { @@ -457,10 +503,11 @@ export function createAgentMachine( }, createEnqueue(onEmit) ); + const validatedResult = validateReplayableResult(state.value, result); return { type: `xstate.done.invoke.${state.value}`, - output: result, + output: validatedResult, at: Date.now(), }; } catch (error) { @@ -492,6 +539,30 @@ export function createAgentMachine( return null; } + function resolveEffectTransition( + state: AgentState, + effectEvent: JournalEvent, + onEmit?: (part: EmittedPart) => void + ): { event: JournalEvent; next: AgentState } { + try { + return { + event: effectEvent, + next: transitionWithEffects(state, effectEvent, onEmit).next, + }; + } catch (error) { + if (isDoneInvokeEventType(state.value, effectEvent.type)) { + const errorEvent = toInvokeErrorEvent(state, error); + + return { + event: errorEvent, + next: transitionWithEffects(state, errorEvent, onEmit).next, + }; + } + + throw error; + } + } + async function invoke(state: AgentState): Promise { if (state.status === 'done' || state.status === 'error') { return state; @@ -508,7 +579,7 @@ export function createAgentMachine( const effectEvent = await getEffectEvent(state); if (effectEvent) { - return transition(state, effectEvent); + return resolveEffectTransition(state, effectEvent).next; } if (sc.on) { @@ -601,6 +672,8 @@ export function createAgentMachine( toSnapshot: toSnap, withRuntimeMetadata, getEffectEvent, + resolveEffectTransition, + transitionWithEffects, }, } as AgentMachine; } diff --git a/src/prebuilt/react.ts b/src/prebuilt/react.ts new file mode 100644 index 0000000..e4292d6 --- /dev/null +++ b/src/prebuilt/react.ts @@ -0,0 +1,225 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../machine.js'; +import type { AgentMachine, StandardSchemaV1 } from '../types.js'; + +const messageSchema = z.object({ + role: z.enum(['system', 'user', 'assistant', 'tool']), + content: z.string(), + name: z.string().optional(), +}); + +const toolCallSchema = z.object({ + kind: z.literal('tool'), + toolName: z.string(), + input: z.record(z.string(), z.unknown()), + message: z.string().optional(), +}); + +const finalAnswerSchema = z.object({ + kind: z.literal('final'), + message: z.string(), +}); + +const modelResultSchema = z.discriminatedUnion('kind', [ + toolCallSchema, + finalAnswerSchema, +]); + +export type ReactAgentMessage = z.infer; + +export type ReactTool = { + name: string; + description: string; + schema?: StandardSchemaV1; + execute: (input: Record) => Promise; +}; + +export type ReactAgentModelResult = z.infer; + +export function createReactAgent(options: { + prompt?: string; + maxSteps?: number; + tools?: ReactTool[]; + model: (args: { + messages: ReactAgentMessage[]; + tools: Array<{ + name: string; + description: string; + schema?: StandardSchemaV1; + }>; + }) => Promise; +}): AgentMachine< + { messages?: ReactAgentMessage[] }, + { + messages: ReactAgentMessage[]; + stepCount: number; + pendingToolCall: + | { toolName: string; input: Record } + | null; + } +> { + const tools = options.tools ?? []; + const maxSteps = options.maxSteps ?? 8; + const toolDefinitions = tools.map(({ name, description, schema }) => ({ + name, + description, + schema, + })); + const toolsByName = new Map(tools.map((tool) => [tool.name, tool])); + + function serializeToolOutput(output: unknown): string { + return typeof output === 'string' ? output : JSON.stringify(output); + } + + return createAgentMachine({ + id: 'prebuilt-react-agent', + schemas: { + input: z.object({ + messages: z.array(messageSchema).optional(), + }), + emitted: { + textPart: z.object({ delta: z.string() }), + toolCall: z.object({ + toolName: z.string(), + input: z.record(z.string(), z.unknown()), + }), + toolResult: z.object({ + toolName: z.string(), + output: z.unknown(), + }), + }, + }, + context: (input) => ({ + messages: [ + ...(options.prompt + ? ([{ role: 'system', content: options.prompt }] satisfies ReactAgentMessage[]) + : []), + ...(input.messages ?? []), + ], + stepCount: 0, + pendingToolCall: + null as { toolName: string; input: Record } | null, + }), + initial: 'agent', + states: { + agent: { + resultSchema: modelResultSchema, + invoke: async ({ context }, enq) => { + if (context.stepCount >= maxSteps) { + return { + kind: 'final' as const, + message: 'Stopped because the maximum step count was reached.', + }; + } + + const result = await options.model({ + messages: context.messages, + tools: toolDefinitions, + }); + + if (result.kind === 'final') { + enq.emit({ type: 'textPart', delta: result.message }); + } + + return result; + }, + onDone: ({ result, context }) => { + if (result.kind === 'final') { + return { + target: 'done' as const, + context: { + stepCount: context.stepCount + 1, + messages: [ + ...context.messages, + { role: 'assistant', content: result.message }, + ], + }, + }; + } + + return { + target: 'tool' as const, + context: { + stepCount: context.stepCount + 1, + pendingToolCall: { + toolName: result.toolName, + input: result.input, + }, + messages: [ + ...context.messages, + { + role: 'assistant', + content: + result.message + ?? `Calling tool ${result.toolName} with ${JSON.stringify(result.input)}`, + }, + ], + }, + params: { + toolName: result.toolName, + input: result.input, + }, + }; + }, + }, + tool: { + paramsSchema: z.object({ + toolName: z.string(), + input: z.record(z.string(), z.unknown()), + }), + resultSchema: z.object({ + toolName: z.string(), + output: z.unknown(), + }), + invoke: async ({ params }, enq) => { + const tool = toolsByName.get(params.toolName); + + if (!tool) { + throw new Error(`Tool '${params.toolName}' not found`); + } + + enq.emit({ + type: 'toolCall', + toolName: params.toolName, + input: params.input, + }); + + const output = await tool.execute(params.input); + + enq.emit({ + type: 'toolResult', + toolName: params.toolName, + output, + }); + + return { + toolName: params.toolName, + output, + }; + }, + onDone: ({ result, context }) => ({ + target: 'agent' as const, + context: { + pendingToolCall: null, + messages: [ + ...context.messages, + { + role: 'tool', + name: result.toolName, + content: serializeToolOutput(result.output), + }, + ], + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + messages: context.messages, + finalMessage: context.messages.at(-1)?.content ?? null, + steps: context.stepCount, + }), + }, + }, + }); +} diff --git a/src/restore.test.ts b/src/restore.test.ts index 7d9de1f..2dda9cf 100644 --- a/src/restore.test.ts +++ b/src/restore.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'vitest'; +import { expect, test, vi } from 'vitest'; import { z } from 'zod'; import { createAgentMachine, @@ -69,6 +69,9 @@ test('restoreSession reconstructs from the latest snapshot plus replay tail', as sessionId: liveRun.sessionId, store, }); + await vi.waitFor(() => { + expect(restoredRun.getSnapshot()).toEqual(liveRun.getSnapshot()); + }); expect(restoredRun.getSnapshot()).toEqual(liveRun.getSnapshot()); }); diff --git a/src/runtime/emitter.ts b/src/runtime/emitter.ts index 1b83786..4e9f2d5 100644 --- a/src/runtime/emitter.ts +++ b/src/runtime/emitter.ts @@ -7,14 +7,9 @@ export interface RunEmitter { export function createRunEmitter(): RunEmitter { const listeners = new Map>(); - const history = new Map(); return { emit(type, event) { - const events = history.get(type) ?? []; - events.push(event); - history.set(type, events); - for (const handler of listeners.get(type) ?? []) { handler(event); } @@ -25,10 +20,6 @@ export function createRunEmitter(): RunEmitter { current.add(handler); listeners.set(type, current); - for (const event of history.get(type) ?? []) { - handler(event); - } - return () => { const active = listeners.get(type); if (!active) { diff --git a/src/runtime/session.ts b/src/runtime/session.ts index c47a0de..e916f1d 100644 --- a/src/runtime/session.ts +++ b/src/runtime/session.ts @@ -9,6 +9,7 @@ import type { RestoreSessionOptions, SessionOptions, } from '../types.js'; +import { isReservedInternalEventType } from '../utils.js'; type SnapshotRuntime = { sessionId: string; @@ -23,6 +24,16 @@ type RuntimeMachine = AgentMachine & { state: AgentState, onEmit?: (part: EmittedPart) => void ): Promise; + resolveEffectTransition( + state: AgentState, + effectEvent: JournalEvent, + onEmit?: (part: EmittedPart) => void + ): { event: JournalEvent; next: AgentState }; + transitionWithEffects( + state: AgentState, + event: { type: string; [key: string]: unknown }, + onEmit?: (part: EmittedPart) => void + ): { next: AgentState; emitted: EmittedPart[] }; }; }; @@ -35,8 +46,8 @@ type RunState = { function createSessionId(): string { if ( - typeof globalThis.crypto !== 'undefined' && - typeof globalThis.crypto.randomUUID === 'function' + typeof globalThis.crypto !== 'undefined' + && typeof globalThis.crypto.randomUUID === 'function' ) { return globalThis.crypto.randomUUID(); } @@ -69,6 +80,62 @@ function createRun( runState: RunState, emitter = createRunEmitter() ): AgentRun { + let releaseStart!: () => void; + let operation = new Promise((resolve) => { + releaseStart = resolve; + }); + let startScheduled = false; + let terminalEmitted = false; + + function emitPart(part: EmittedPart) { + emitter.emit('part', part); + emitter.emit(part.type, part); + } + + function enqueue(op: () => Promise): Promise { + const result = operation.then(op); + operation = result.then( + () => undefined, + () => undefined + ); + + return result; + } + + function emitTerminalIfNeeded() { + if (terminalEmitted) { + return; + } + + if (runState.snapshot.status === 'done') { + terminalEmitted = true; + emitter.emit('runtime', { + type: 'session.completed', + sessionId: runState.runtime.sessionId, + at: Date.now(), + }); + emitter.emit('done', { + output: runState.snapshot.output, + snapshot: runState.snapshot, + }); + return; + } + + if (runState.snapshot.status === 'error') { + terminalEmitted = true; + emitter.emit('runtime', { + type: 'session.failed', + sessionId: runState.runtime.sessionId, + error: runState.snapshot.error, + at: Date.now(), + }); + emitter.emit('error', { + error: runState.snapshot.error, + snapshot: runState.snapshot, + }); + } + } + async function persistSnapshot() { runState.snapshot = runtimeMachine.__runtime.toSnapshot( runState.current, @@ -86,8 +153,10 @@ function createRun( type: 'snapshot.persisted', sessionId: runState.runtime.sessionId, afterSequence: runState.lastSequence, + at: Date.now(), }); emitter.emit('state', runState.snapshot); + emitTerminalIfNeeded(); } async function appendMachineEvent(event: JournalEvent) { @@ -103,15 +172,19 @@ function createRun( while (runState.current.status === 'active') { const effectEvent = await runtimeMachine.__runtime.getEffectEvent( runState.current, - (part) => { - emitter.emit(part.type, part); - } + emitPart ); if (effectEvent) { - await appendMachineEvent(effectEvent); + const resolved = runtimeMachine.__runtime.resolveEffectTransition( + runState.current, + effectEvent, + emitPart + ); + + await appendMachineEvent(resolved.event); runState.current = runtimeMachine.__runtime.withRuntimeMetadata( - machine.transition(runState.current, effectEvent), + resolved.next, runState.runtime ); await persistSnapshot(); @@ -126,6 +199,20 @@ function createRun( } } + function scheduleStart() { + if (startScheduled) { + return; + } + + startScheduled = true; + void enqueue(async () => { + await settle(); + }); + queueMicrotask(() => { + releaseStart(); + }); + } + return { get sessionId() { return runState.runtime.sessionId; @@ -140,16 +227,28 @@ function createRun( }, async send(event) { - const journalEvent = toJournalEvent(event); - const next = machine.transition(runState.current, journalEvent); + if (isReservedInternalEventType(event.type)) { + throw new Error( + `Cannot send reserved internal event '${event.type}' to a session` + ); + } - await appendMachineEvent(journalEvent); - runState.current = runtimeMachine.__runtime.withRuntimeMetadata( - next, - runState.runtime - ); - await persistSnapshot(); - await settle(); + return enqueue(async () => { + const journalEvent = toJournalEvent(event); + const next = runtimeMachine.__runtime.transitionWithEffects( + runState.current, + journalEvent, + emitPart + ).next; + + await appendMachineEvent(journalEvent); + runState.current = runtimeMachine.__runtime.withRuntimeMetadata( + next, + runState.runtime + ); + await persistSnapshot(); + await settle(); + }); }, on(type, handler) { @@ -163,12 +262,14 @@ function createRun( /** @internal */ async __settle() { - await settle(); + await enqueue(async () => { + await settle(); + }); }, /** @internal */ - __emit(type: string, event: unknown) { - emitter.emit(type, event); + __scheduleStart() { + scheduleStart(); }, } as AgentRun; } @@ -198,7 +299,7 @@ export async function startSession( ) as AgentRun & { __persistCurrent(): Promise; __settle(): Promise; - __emit(type: string, event: unknown): void; + __scheduleStart(): void; }; const initEvent = { @@ -208,14 +309,9 @@ export async function startSession( } satisfies JournalEvent; const record = await options.store.append(runtime.sessionId, initEvent); runState.lastSequence = record.sequence; - run.__emit('machine.event', { ...initEvent, sequence: record.sequence }); - run.__emit('runtime', { - type: 'session.started', - sessionId: runtime.sessionId, - }); await run.__persistCurrent(); - await run.__settle(); + run.__scheduleStart(); return run; } @@ -258,21 +354,14 @@ export async function restoreSession( ) as AgentRun & { __persistCurrent(): Promise; __settle(): Promise; - __emit(type: string, event: unknown): void; + __scheduleStart(): void; }; - if (initEvent && !persisted) { - run.__emit('machine.event', initEvent); - } - const replayTail = await options.store.loadEvents( options.sessionId, runState.lastSequence ); - - if (!persisted) { - run.__emit('state', runState.snapshot); - } + let replayed = false; for (const event of replayTail) { runState.current = runtimeMachine.__runtime.withRuntimeMetadata( @@ -284,22 +373,13 @@ export async function restoreSession( runState.current, runState.runtime ); - run.__emit('machine.event', event); - run.__emit('state', runState.snapshot); + replayed = true; } - if (persisted) { - run.__emit('state', runState.snapshot); + if (!persisted || replayed) { + await run.__persistCurrent(); } - - run.__emit('runtime', { - type: 'session.restored', - sessionId: runState.runtime.sessionId, - afterSequence: runState.lastSequence, - }); - - await run.__persistCurrent(); - await run.__settle(); + run.__scheduleStart(); return run; } diff --git a/src/session-runtime.test.ts b/src/session-runtime.test.ts index f6efd81..10df0ff 100644 --- a/src/session-runtime.test.ts +++ b/src/session-runtime.test.ts @@ -1,11 +1,21 @@ -import { expect, test } from 'vitest'; +import { expect, test, vi } from 'vitest'; +import { z } from 'zod'; import { createAgentMachine, createMemoryRunStore, startSession, } from './index.js'; -test('startSession creates a session and persists xstate.init', async () => { +function deferred() { + let resolve!: (value: T | PromiseLike) => void; + const promise = new Promise((nextResolve) => { + resolve = nextResolve; + }); + + return { promise, resolve }; +} + +test('startSession creates a session, persists xstate.init, and returns before start effects run', async () => { const machine = createAgentMachine({ id: 'session-runtime', context: () => ({ count: 0 }), @@ -33,16 +43,23 @@ test('startSession creates a session and persists xstate.init', async () => { const persisted = await store.loadLatestSnapshot(run.sessionId); expect(run.sessionId).toBe(snapshot.sessionId); - expect(run.status).toBe('pending'); expect(snapshot).toEqual( expect.objectContaining({ sessionId: run.sessionId, value: 'idle', - status: 'pending', + status: 'active', context: { count: 0 }, params: {}, }) ); + await vi.waitFor(() => { + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'idle', + status: 'pending', + }) + ); + }); expect(journal).toEqual([ expect.objectContaining({ sequence: 1, @@ -58,3 +75,110 @@ test('startSession creates a session and persists xstate.init', async () => { }) ); }); + +test('serializes concurrent sends so each event applies from the latest snapshot', async () => { + const gates = [deferred(), deferred()]; + let invocations = 0; + const machine = createAgentMachine({ + id: 'serialized-send', + schemas: { + events: { + increment: z.object({ amount: z.number() }), + }, + }, + context: () => ({ count: 0 }), + initial: 'ready', + states: { + ready: { + on: { + increment: ({ event, context }) => ({ + target: 'working', + context: { count: context.count + event.amount }, + }), + }, + }, + working: { + resultSchema: z.object({ count: z.number() }), + invoke: async ({ context }) => { + const gate = gates[invocations++]!; + await gate.promise; + return { count: context.count }; + }, + onDone: ({ result }) => ({ + target: 'ready', + context: { count: result.count }, + }), + }, + }, + }); + + const run = await startSession(machine, { store: createMemoryRunStore() }); + await vi.waitFor(() => { + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'ready', + status: 'pending', + }) + ); + }); + + const first = run.send({ type: 'increment', amount: 1 }); + const second = run.send({ type: 'increment', amount: 10 }); + + await vi.waitFor(() => { + expect(invocations).toBe(1); + }); + + gates[0]!.resolve(); + await first; + await vi.waitFor(() => { + expect(invocations).toBe(2); + }); + + gates[1]!.resolve(); + await second; + + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'ready', + status: 'pending', + context: { count: 11 }, + }) + ); +}); + +test('rejects reserved internal events from run.send', async () => { + const machine = createAgentMachine({ + id: 'reserved-events', + context: () => ({ count: 0 }), + initial: 'ready', + states: { + ready: { + on: { + go: { target: 'done' }, + }, + }, + done: { type: 'final' }, + }, + }); + + const run = await startSession(machine, { store: createMemoryRunStore() }); + await vi.waitFor(() => { + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'ready', + status: 'pending', + }) + ); + }); + + await expect(run.send({ type: 'xstate.init' })).rejects.toThrow( + /reserved internal event/i + ); + await expect( + run.send({ type: 'xstate.done.invoke.worker' }) + ).rejects.toThrow(/reserved internal event/i); + await expect( + run.send({ type: 'xstate.error.invoke.worker' }) + ).rejects.toThrow(/reserved internal event/i); +}); diff --git a/src/streaming.test.ts b/src/streaming.test.ts index 8ec5530..ec782f6 100644 --- a/src/streaming.test.ts +++ b/src/streaming.test.ts @@ -6,7 +6,20 @@ import { startSession, } from './index.js'; -test('emitted parts flow through the run-level API', async () => { +function once( + run: { on(type: string, handler: (event: unknown) => void): () => void }, + type: string +) { + return new Promise((resolve) => { + let off = () => {}; + off = run.on(type, (event) => { + off(); + resolve(event as T); + }); + }); +} + +test('returns a live run before initial invoke output and emits ephemeral parts', async () => { const machine = createAgentMachine({ id: 'streaming-parts', schemas: { @@ -40,12 +53,17 @@ test('emitted parts flow through the run-level API', async () => { const store = createMemoryRunStore(); const run = await startSession(machine, { store }); const parts: Array<{ type: string; delta: string }> = []; + const allParts: Array<{ type: string; delta: string }> = []; const states: string[] = []; const events: string[] = []; + const done = once<{ output: { text: string } }>(run, 'done'); const offPart = run.on('textPart', (part) => { parts.push(part as { type: string; delta: string }); }); + const offAnyPart = run.on('part', (part) => { + allParts.push(part as { type: string; delta: string }); + }); const offState = run.on('state', (snapshot) => { states.push((snapshot as { value: string }).value); }); @@ -53,20 +71,91 @@ test('emitted parts flow through the run-level API', async () => { events.push((event as { type: string }).type); }); + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'writing', + status: 'active', + }) + ); + + await done; + expect(parts).toEqual([ { type: 'textPart', delta: 'hel' }, { type: 'textPart', delta: 'lo' }, ]); - expect(states).toContain('writing'); - expect(states[states.length - 1]).toBe('done'); + expect(allParts).toEqual([ + { type: 'textPart', delta: 'hel' }, + { type: 'textPart', delta: 'lo' }, + ]); + expect(states.length).toBeGreaterThan(0); + expect(states.every((state) => state === 'done')).toBe(true); expect(events).toContain('xstate.done.invoke.writing'); expect(run.getSnapshot().output).toEqual({ text: 'hello' }); offPart(); + offAnyPart(); offState(); offEvent(); }); +test('does not replay prior events to late subscribers', async () => { + const machine = createAgentMachine({ + id: 'late-streaming-subscriber', + schemas: { + emitted: { + textPart: z.object({ delta: z.string() }), + }, + }, + context: () => ({ finalText: '' }), + initial: 'writing', + states: { + writing: { + resultSchema: z.object({ text: z.string() }), + invoke: async (_args, enq) => { + enq.emit({ type: 'textPart', delta: 'hel' }); + enq.emit({ type: 'textPart', delta: 'lo' }); + return { text: 'hello' }; + }, + onDone: ({ result }) => ({ + target: 'done', + context: { finalText: result.text }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ text: context.finalText }), + }, + }, + }); + + const run = await startSession(machine, { + store: createMemoryRunStore(), + }); + await once(run, 'done'); + + const lateParts: Array<{ type: string; delta: string }> = []; + const replayedStates: string[] = []; + const replayedEvents: string[] = []; + + run.on('textPart', (part) => { + lateParts.push(part as { type: string; delta: string }); + }); + run.on('state', (snapshot) => { + replayedStates.push((snapshot as { value: string }).value); + }); + run.on('machine.event', (event) => { + replayedEvents.push((event as { type: string }).type); + }); + run.on('done', () => { + replayedEvents.push('done'); + }); + + expect(lateParts).toEqual([]); + expect(replayedStates).toEqual([]); + expect(replayedEvents).toEqual([]); +}); + test('invalid emitted parts are rejected', async () => { const machine = createAgentMachine({ id: 'streaming-invalid-parts', @@ -90,6 +179,7 @@ test('invalid emitted parts are rejected', async () => { const run = await startSession(machine, { store: createMemoryRunStore(), }); + await once(run, 'error'); expect(run.getSnapshot()).toEqual( expect.objectContaining({ @@ -101,3 +191,62 @@ test('invalid emitted parts are rejected', async () => { }) ); }); + +test('transition handlers can emit live effects without journaling them', async () => { + const machine = createAgentMachine({ + id: 'transition-handler-emits', + schemas: { + emitted: { + textPart: z.object({ delta: z.string() }), + }, + events: { + send: z.object({}), + }, + }, + context: () => ({ sent: false }), + initial: 'ready', + states: { + ready: { + on: { + send: ({ context }, enq) => { + enq.emit({ type: 'textPart', delta: 'sending' }); + + return { + target: 'done', + context: { sent: !context.sent }, + }; + }, + }, + }, + done: { + type: 'final', + output: ({ context }) => context, + }, + }, + }); + + const store = createMemoryRunStore(); + const run = await startSession(machine, { store }); + const parts: string[] = []; + + run.on('textPart', (part) => { + parts.push((part as { delta: string }).delta); + }); + + await run.send({ type: 'send' }); + + expect(parts).toEqual(['sending']); + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + context: { sent: true }, + }) + ); + + const journal = await store.loadEvents(run.sessionId); + expect(journal.map((event) => event.type)).toEqual([ + 'xstate.init', + 'send', + ]); +}); diff --git a/src/target-types.assert.ts b/src/target-types.assert.ts new file mode 100644 index 0000000..ee3e9a0 --- /dev/null +++ b/src/target-types.assert.ts @@ -0,0 +1,204 @@ +import { z } from 'zod'; +import { createAgentMachine } from './machine.js'; + +const machine = createAgentMachine({ + id: 'typed-targets', + context: () => ({ count: 0 }), + initial: 'idle', + states: { + idle: { + on: { + advance: () => ({ + target: 'done', + }), + }, + }, + done: { + type: 'final', + }, + }, + schemas: { + events: { + advance: z.object({ + type: z.literal('advance'), + }), + }, + }, +}); + +machine.transition(machine.getInitialState(), { type: 'advance' }); + +createAgentMachine({ + id: 'typed-target-params', + context: () => ({ count: 0 }), + initial: 'idle', + states: { + idle: { + on: { + advance: () => ({ + target: 'working', + params: { + index: 0, + }, + }), + }, + }, + working: { + paramsSchema: z.object({ + index: z.number(), + }), + }, + }, + schemas: { + events: { + advance: z.object({ + type: z.literal('advance'), + }), + }, + }, +}); + +createAgentMachine({ + id: 'missing-required-target-params', + context: () => ({ count: 0 }), + initial: 'idle', + states: { + // @ts-expect-error params should be required when the target has paramsSchema + idle: { + on: { + advance: () => ({ + target: 'working', + }), + }, + }, + working: { + paramsSchema: z.object({ + index: z.number(), + }), + }, + }, + schemas: { + events: { + advance: z.object({ + type: z.literal('advance'), + }), + }, + }, +}); + +createAgentMachine({ + id: 'invalid-target', + context: () => ({ count: 0 }), + initial: 'idle', + states: { + // @ts-expect-error invalid targets should be rejected at author time + idle: { + on: { + advance: () => ({ + target: 'missing', + }), + }, + }, + done: { + type: 'final', + }, + }, + schemas: { + events: { + advance: z.object({ + type: z.literal('advance'), + }), + }, + }, +}); + +createAgentMachine({ + id: 'unexpected-target-params', + context: () => ({ count: 0 }), + initial: 'idle', + states: { + idle: { + on: { + // @ts-expect-error params should be rejected when the target has no paramsSchema + advance: () => ({ + target: 'done', + params: { + anything: true, + }, + }), + }, + }, + done: { + type: 'final', + }, + }, + schemas: { + events: { + advance: z.object({ + type: z.literal('advance'), + }), + }, + }, +}); + +createAgentMachine({ + id: 'invalid-target-params', + context: () => ({ count: 0 }), + initial: 'idle', + states: { + idle: { + on: { + // @ts-expect-error target params should match the target state's params schema + advance: () => ({ + target: 'working', + params: { + wrong: true, + }, + }), + }, + }, + working: { + paramsSchema: z.object({ + index: z.number(), + }), + }, + }, + schemas: { + events: { + advance: z.object({ + type: z.literal('advance'), + }), + }, + }, +}); + +createAgentMachine({ + id: 'invalid-target-param-types', + context: () => ({ count: 0 }), + initial: 'idle', + states: { + idle: { + on: { + // @ts-expect-error target params should match the target param field types + advance: () => ({ + target: 'working', + params: { + index: 'hello', + }, + }), + }, + }, + working: { + paramsSchema: z.object({ + index: z.number(), + }), + }, + }, + schemas: { + events: { + advance: z.object({ + type: z.literal('advance'), + }), + }, + }, +}); diff --git a/src/types.ts b/src/types.ts index 4f6219d..961dc33 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,6 +37,10 @@ export interface InvokeEnqueue { emit(part: EmittedPart): void; } +type IsExactlyUnknown = unknown extends T + ? ([T] extends [unknown] ? true : false) + : false; + // ─── Durable Session Vocabulary ─── export type { JournalEvent } from './runtime/events.js'; @@ -59,10 +63,32 @@ export interface AgentAdapter { // ─── Transition ─── -export interface TransitionResult< +export type TransitionResult< + TContext extends Record = Record, + TTarget extends string = string, + TParamsByTarget extends Record = {}, +> = + | { + target?: undefined; + context?: Partial; + params?: never; + } + | { + [K in TTarget]: { + target: K; + context?: Partial; + } & (K extends keyof TParamsByTarget + ? IsExactlyUnknown extends true + ? { params?: never } + : { params: TParamsByTarget[K] } + : { params?: never }) + }[TTarget]; + +export interface InitialTransitionResult< TContext extends Record = Record, + TTarget extends string = string, > { - target?: string; + target: TTarget; context?: Partial; params?: Record; } @@ -71,6 +97,8 @@ export interface TransitionResult< export interface StateConfig< TContext extends Record = Record, + TTarget extends string = string, + TParamsByTarget extends Record = {}, > { type?: 'final' | 'choice'; paramsSchema?: StandardSchemaV1; @@ -80,8 +108,8 @@ export interface StateConfig< params: Record; signal?: AbortSignal; }, enq: InvokeEnqueue) => Promise; - onDone?: (args: { result: any; context: TContext }) => TransitionResult; - on?: Record | ((args: { event: any; context: TContext }) => TransitionResult)>; + onDone?: (args: { result: any; context: TContext }) => TransitionResult; + on?: Record | ((args: { event: any; context: TContext }, enq: InvokeEnqueue) => TransitionResult)>; events?: Record; output?: (args: { context: TContext }) => unknown; // choice-specific @@ -252,14 +280,16 @@ export interface DecideConfig< TContext extends Record = Record, TParams extends Record = Record, TOptions extends Record = Record, + TTarget extends string = string, + TParamsByTarget extends Record = {}, > { model: string; adapter?: AgentAdapter; prompt: string | ((args: { context: TContext; params: TParams }) => string); options: TOptions; reasoning?: boolean; - onDone: (args: { result: DecideResultFor; context: TContext }) => TransitionResult; - on?: Record TransitionResult>; + onDone: (args: { result: DecideResultFor; context: TContext }) => TransitionResult; + on?: Record TransitionResult>; } // ─── Classify ─── @@ -268,14 +298,16 @@ export interface ClassifyConfig< TContext extends Record = Record, TParams extends Record = Record, TCategories extends Record = Record, + TTarget extends string = string, + TParamsByTarget extends Record = {}, > { model: string; adapter?: AgentAdapter; prompt: string | ((args: { context: TContext; params: TParams }) => string); into: TCategories; examples?: Array<{ input: string; category: keyof TCategories & string }>; - onDone: (args: { result: { category: keyof TCategories & string }; context: TContext }) => TransitionResult; - on?: Record TransitionResult>; + onDone: (args: { result: { category: keyof TCategories & string }; context: TContext }) => TransitionResult; + on?: Record TransitionResult>; } // ─── Trace ─── diff --git a/src/utils.ts b/src/utils.ts index f6cd7c0..8ac89c8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import type { AgentState, + InitialTransitionResult, MachineConfig, StandardSchemaResult, StandardSchemaV1, @@ -54,7 +55,7 @@ export type StateConfigAny = { enq: { emit(part: { type: string; [key: string]: unknown }): void } ) => Promise; onDone?: (args: { result: unknown; context: Record }) => TransitionResult; - on?: Record; context: Record }) => TransitionResult)>; + on?: Record; context: Record }, enq: { emit(part: { type: string; [key: string]: unknown }): void }) => TransitionResult)>; output?: (args: { context: Record }) => unknown; resultSchema?: StandardSchemaV1; model?: string; @@ -86,12 +87,12 @@ export function resolveInitial( | ((args: { context: Record; params: Record; - }) => TransitionResult), + }) => InitialTransitionResult), args: { context: Record; params: Record; } -): TransitionResult { +): InitialTransitionResult { if (typeof initial === 'string') { return { target: initial }; } @@ -205,6 +206,14 @@ export function isErrorInvokeEventType( return eventType === `xstate.error.invoke.${stateValue}`; } +export function isReservedInternalEventType(eventType: string): boolean { + return ( + eventType === 'xstate.init' + || eventType.startsWith('xstate.done.invoke.') + || eventType.startsWith('xstate.error.invoke.') + ); +} + export function serializeError(error: unknown): unknown { if (error instanceof Error) { return { From 186aefb76e8e39d8d14d53c7677881dc267b77d7 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 18 Apr 2026 14:33:16 -0400 Subject: [PATCH 17/34] feat: expand durable agent runtime and langgraph coverage --- ...04-08-langgraph-core-replacement-design.md | 21 +- examples/adapter.ts | 69 ++-- examples/branching.ts | 6 + examples/chatbot.ts | 42 ++- examples/classify.ts | 30 +- examples/customer-service-sim.ts | 5 + examples/decide.ts | 59 ++-- examples/email.ts | 94 ++--- examples/hitl.ts | 8 +- examples/index.ts | 3 + examples/joke.ts | 7 + examples/jugs.ts | 16 +- examples/map-reduce.ts | 5 + examples/multi-agent-network.ts | 334 ++++++++++++++++++ examples/newspaper.ts | 6 + examples/plan-and-execute.ts | 16 +- examples/raffle.ts | 7 + examples/react-agent.ts | 17 +- examples/reflection.ts | 6 + examples/rewoo.ts | 235 ++++++++++++ examples/river-crossing.ts | 16 +- examples/simple.ts | 1 + examples/subflow.ts | 7 +- examples/supervisor.ts | 252 +++++++++++++ examples/tool-calling.ts | 18 +- examples/tutor.ts | 5 + src/agent.test.ts | 275 +++++++++----- src/classify.ts | 139 +++----- src/decide.ts | 144 ++++---- src/examples.test.ts | 185 +++++++++- src/graph/index.ts | 2 +- src/index.ts | 9 +- src/invoke-events.test.ts | 13 +- .../multi-agent-network.test.ts | 60 ++++ .../prebuilt-react.test.ts | 13 +- src/langgraph-equivalents/rewoo.test.ts | 56 +++ src/langgraph-equivalents/streaming.test.ts | 11 +- src/langgraph-equivalents/supervisor.test.ts | 62 ++++ .../tool-calling.test.ts | 13 +- src/machine.ts | 175 +++++---- src/persistence.test.ts | 12 +- src/prebuilt/react.ts | 42 +-- src/runtime/session.ts | 94 ++++- src/session-runtime.test.ts | 2 +- src/session-types.test.ts | 2 +- src/stream-snapshot.test.ts | 12 +- src/streaming.test.ts | 39 +- src/target-types.assert.ts | 109 +++++- src/types.ts | 164 +++++---- src/utils.ts | 26 +- 50 files changed, 2229 insertions(+), 715 deletions(-) create mode 100644 examples/multi-agent-network.ts create mode 100644 examples/rewoo.ts create mode 100644 examples/supervisor.ts create mode 100644 src/langgraph-equivalents/multi-agent-network.test.ts create mode 100644 src/langgraph-equivalents/rewoo.test.ts create mode 100644 src/langgraph-equivalents/supervisor.test.ts diff --git a/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md b/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md index 0ff6aed..f30ecb3 100644 --- a/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md +++ b/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md @@ -125,10 +125,13 @@ interface AgentRun { getSnapshot(): AgentSnapshot; send(event: { type: string; [key: string]: unknown }): Promise; on(type: string, handler: (event: unknown) => void): () => void; - [Symbol.asyncIterator](): AsyncIterator; } ``` +An async-iterator surface is still useful, but it is additive. The emitter-style `on(...)` API is the required phase-1 contract. + +`on(...)` is a live listener only. It should not be treated as a history or replay API. Historical actor events belong to the journal/store layer. + ### Durable Execution Boundaries Phase 1 durability should exist at machine boundaries, not inside arbitrary user async code. @@ -268,15 +271,15 @@ type AgentSnapshot = { status: "active" | "done" | "error" | "pending"; createdAt: number; sessionId: string; + params: Record>; output?: unknown; error?: SerializedError; }; type PersistedSnapshot = { sessionId: string; - sequence: number; snapshot: AgentSnapshot; - lastJournalIndex: number; + afterSequence: number; createdAt: number; }; ``` @@ -294,7 +297,7 @@ with additional metadata such as: - optional `output` - optional `error` -The `sequence` field exists so storage can identify which snapshot is the latest persisted derivation and so replay can resume from a known journal offset. It should track journal position rather than inventing a separate semantic version. +The `afterSequence` field identifies the last replayable journal event already reflected in the snapshot, so replay can resume from a known journal offset without inventing a separate semantic version. ### Replay Model @@ -362,17 +365,21 @@ type RunEmitterEvent = Where `machine.event` refers to replayable actor events and `runtime` refers to derived lifecycle records useful for debugging and orchestration. +These event shapes describe what a live run may emit. They do not imply that late subscribers receive replayed history through `on(...)`. + Suggested runtime event family: ```ts type RuntimeEvent = - | { type: "state.entered"; value: string; at: number } - | { type: "transition.applied"; from: string; to: string; at: number } - | { type: "snapshot.saved"; sessionId: string; sequence: number; at: number } + | { type: "session.started"; sessionId: string; at: number } + | { type: "session.restored"; sessionId: string; afterSequence: number; at: number } + | { type: "snapshot.persisted"; sessionId: string; afterSequence: number; at: number } | { type: "session.completed"; sessionId: string; at: number } | { type: "session.failed"; sessionId: string; error: SerializedError; at: number }; ``` +Derived events such as `state.entered` and `transition.applied` are still useful for richer inspection, but they are not required for this phase. + ### Stream Parts For common model/tool streaming shapes, align with Vercel AI SDK-style part conventions where practical: diff --git a/examples/adapter.ts b/examples/adapter.ts index c3c2ee3..a88e6c1 100644 --- a/examples/adapter.ts +++ b/examples/adapter.ts @@ -3,6 +3,7 @@ import { createAdapter, createAgentMachine, decide, + decideResultSchema, type AgentAdapter, } from '../src/index.js'; import { @@ -18,47 +19,59 @@ export function createAdapterExample( decide: createOpenAiDecisionAdapter().decide, }) ) { + const routeOptions = { + billing: { + description: 'Send the request to billing support.', + schema: z.object({ confidence: z.number().min(0).max(1) }), + }, + general: { + description: 'Handle the request in general support.', + schema: z.object({ confidence: z.number().min(0).max(1) }), + }, + } as const; + return createAgentMachine({ id: 'adapter-example', schemas: { input: z.object({ message: z.string() }), + output: z.object({ + route: z.string().nullable(), + confidence: z.number().nullable(), + }), }, context: (input) => ({ message: input.message, route: null as string | null, confidence: null as number | null, }), - adapter, initial: 'route', states: { - route: decide({ - model: 'openai/gpt-5.4-nano', - prompt: ({ context }) => [ - 'Route this support request.', - 'Return billing only when the request is clearly about invoices, refunds, or charges.', - 'Otherwise return general.', - '', - context.message, - ].join('\n'), - options: { - billing: { - description: 'Send the request to billing support.', - schema: z.object({ confidence: z.number().min(0).max(1) }), - }, - general: { - description: 'Handle the request in general support.', - schema: z.object({ confidence: z.number().min(0).max(1) }), - }, + route: { + resultSchema: decideResultSchema(routeOptions), + invoke: async ({ context }) => + decide({ + adapter, + model: 'openai/gpt-5.4-nano', + prompt: [ + 'Route this support request.', + 'Return billing only when the request is clearly about invoices, refunds, or charges.', + 'Otherwise return general.', + '', + context.message, + ].join('\n'), + options: routeOptions, + reasoning: false, + }), + onDone: ({ result }) => { + return { + target: 'done', + context: { + route: result.choice, + confidence: result.data.confidence, + }, + }; }, - reasoning: false, - onDone: ({ result }) => ({ - target: 'done', - context: { - route: result.choice, - confidence: result.data.confidence, - }, - }), - }), + }, done: { type: 'final', output: ({ context }) => ({ diff --git a/examples/branching.ts b/examples/branching.ts index ddaef9f..d50d78d 100644 --- a/examples/branching.ts +++ b/examples/branching.ts @@ -35,6 +35,12 @@ export function createBranchingExample( id: 'branching-example', schemas: { input: z.object({ topic: z.string() }), + output: z.object({ + docs: z.string().nullable(), + issues: z.string().nullable(), + code: z.string().nullable(), + summary: z.string().nullable(), + }), }, context: (input) => ({ topic: input.topic, diff --git a/examples/chatbot.ts b/examples/chatbot.ts index b3a9616..ab390c1 100644 --- a/examples/chatbot.ts +++ b/examples/chatbot.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { createAgentMachine, decide, type AgentAdapter } from '../src/index.js'; +import { createAgentMachine, decide, decideResultSchema, type AgentAdapter } from '../src/index.js'; import { closePrompt, createOpenAiDecisionAdapter, @@ -19,6 +19,11 @@ export function createChatbotExample( reply?: (transcript: string[]) => Promise>; } = {} ) { + const decisionOptions = { + respond: { description: 'Reply to the user and continue chatting.' }, + end: { description: 'End the conversation now.' }, + } as const; + const adapter = options.adapter ?? (process.env.OPENAI_API_KEY ? createOpenAiDecisionAdapter() : undefined); @@ -39,6 +44,11 @@ export function createChatbotExample( return createAgentMachine({ id: 'chatbot-example', schemas: { + output: z.object({ + transcript: z.array(z.string()), + ended: z.boolean(), + lastAssistantMessage: z.string().nullable(), + }), events: { 'user.message': z.object({ message: z.string() }), 'user.exit': z.object({}), @@ -50,7 +60,6 @@ export function createChatbotExample( lastAssistantMessage: null as string | null, ended: false, }), - adapter, initial: 'listening', states: { listening: { @@ -68,24 +77,25 @@ export function createChatbotExample( }, }, }, - deciding: decide({ - model: 'openai/gpt-5.4-nano', - prompt: ({ context }) => - [ - 'Decide whether the assistant should answer or end the conversation.', - 'End only when the user is clearly saying goodbye or asking to stop.', - '', - (context as { transcript: string[] }).transcript.join('\n'), - ].join('\n'), - options: { - respond: { description: 'Reply to the user and continue chatting.' }, - end: { description: 'End the conversation now.' }, - }, + deciding: { + resultSchema: decideResultSchema(decisionOptions), + invoke: async ({ context }) => + decide({ + adapter, + model: 'openai/gpt-5.4-nano', + prompt: [ + 'Decide whether the assistant should answer or end the conversation.', + 'End only when the user is clearly saying goodbye or asking to stop.', + '', + context.transcript.join('\n'), + ].join('\n'), + options: decisionOptions, + }), onDone: ({ result }) => ({ target: result.choice === 'end' ? 'done' : 'replying', context: result.choice === 'end' ? { ended: true } : {}, }), - }), + }, replying: { resultSchema: replySchema, invoke: async ({ context }) => reply(context.transcript), diff --git a/examples/classify.ts b/examples/classify.ts index d2befb2..eb4eaab 100644 --- a/examples/classify.ts +++ b/examples/classify.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { createAgentMachine, classify, + classifyResultSchema, type AgentAdapter, } from '../src/index.js'; import { @@ -15,33 +16,40 @@ import { export function createClassifyExample( adapter: AgentAdapter = createOpenAiDecisionAdapter() ) { + const categories = { + billing: { description: 'Payments, invoices, refunds, and charges.' }, + technical: { description: 'Bugs, outages, and product issues.' }, + general: { description: 'Everything else.' }, + } as const; + return createAgentMachine({ id: 'classify-example', schemas: { input: z.object({ request: z.string() }), + output: z.object({ category: z.string().nullable() }), }, context: (input) => ({ request: input.request, category: null as string | null, }), - adapter, initial: 'routing', states: { - routing: classify({ - model: 'openai/gpt-5.4-nano', - prompt: ({ context }) => `Classify this support request:\n\n${context.request}`, - into: { - billing: { description: 'Payments, invoices, refunds, and charges.' }, - technical: { description: 'Bugs, outages, and product issues.' }, - general: { description: 'Everything else.' }, - }, + routing: { + resultSchema: classifyResultSchema(categories), + invoke: async ({ context }) => + classify({ + adapter, + model: 'openai/gpt-5.4-nano', + prompt: `Classify this support request:\n\n${context.request}`, + into: categories, + }), onDone: ({ result }) => ({ target: 'done', context: { category: result.category }, }), - }), + }, done: { - // use params; category should alwyas be defined when entering + // use input; category should always be defined when entering type: 'final', output: ({ context }) => ({ category: context.category }), }, diff --git a/examples/customer-service-sim.ts b/examples/customer-service-sim.ts index bd21acf..3bade57 100644 --- a/examples/customer-service-sim.ts +++ b/examples/customer-service-sim.ts @@ -71,6 +71,11 @@ export function createCustomerServiceSimExample( id: 'customer-service-sim-example', schemas: { input: z.object({ issue: z.string() }), + output: z.object({ + transcript: z.array(z.string()), + turnCount: z.number(), + outcome: z.string().nullable(), + }), }, context: (input) => ({ issue: input.issue, diff --git a/examples/decide.ts b/examples/decide.ts index 3288ae2..723a3ef 100644 --- a/examples/decide.ts +++ b/examples/decide.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { createAgentMachine, decide, + decideResultSchema, type AgentAdapter, } from '../src/index.js'; import { @@ -13,41 +14,51 @@ import { } from './_run.js'; export function createDecideExample(adapter: AgentAdapter = createOpenAiDecisionAdapter()) { + const triageOptions = { + reply: { + description: 'Reply directly to the customer.', + schema: z.object({ message: z.string() }), + }, + askForClarification: { + description: 'Ask one follow-up question before proceeding.', + schema: z.object({ question: z.string() }), + }, + escalate: { + description: 'Escalate to a human specialist.', + schema: z.object({ team: z.string() }), + }, + } as const; + return createAgentMachine({ id: 'decide-example', schemas: { input: z.object({ request: z.string() }), + output: z.object({ + action: z.string().nullable(), + payload: z.record(z.string(), z.unknown()).nullable(), + }), }, context: (input) => ({ request: input.request, action: null as string | null, payload: null as Record | null, }), - adapter, initial: 'triage', states: { - triage: decide({ - model: 'openai/gpt-5.4-nano', - prompt: ({ context }) => [ - 'Choose the best next step for this support request.', - 'Prefer asking a single clarification question when key facts are missing.', - '', - `Request: ${context.request}`, - ].join('\n'), - options: { - reply: { - description: 'Reply directly to the customer.', - schema: z.object({ message: z.string() }), - }, - askForClarification: { - description: 'Ask one follow-up question before proceeding.', - schema: z.object({ question: z.string() }), - }, - escalate: { - description: 'Escalate to a human specialist.', - schema: z.object({ team: z.string() }), - }, - }, + triage: { + resultSchema: decideResultSchema(triageOptions), + invoke: async ({ context }) => + decide({ + adapter, + model: 'openai/gpt-5.4-nano', + prompt: [ + 'Choose the best next step for this support request.', + 'Prefer asking a single clarification question when key facts are missing.', + '', + `Request: ${context.request}`, + ].join('\n'), + options: triageOptions, + }), onDone: ({ result }) => ({ target: 'done', context: { @@ -55,7 +66,7 @@ export function createDecideExample(adapter: AgentAdapter = createOpenAiDecision payload: result.data, }, }), - }), + }, done: { type: 'final', output: ({ context }) => ({ diff --git a/examples/email.ts b/examples/email.ts index 1f33eb3..053959a 100644 --- a/examples/email.ts +++ b/examples/email.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { createAgentMachine, decide, type AgentAdapter } from '../src/index.js'; +import { createAgentMachine, decide, decideResultSchema, type AgentAdapter } from '../src/index.js'; import { closePrompt, createOpenAiDecisionAdapter, @@ -36,6 +36,18 @@ export function createEmailExample( ) => Promise>; } = {} ) { + const checkingOptions = { + askForClarification: { + description: 'Ask one or more clarifying questions before drafting.', + schema: z.object({ + questions: z.array(z.string()).min(1), + }), + }, + draft: { + description: 'Draft the email reply now.', + }, + } as const; + const adapter = options.adapter ?? (process.env.OPENAI_API_KEY ? createOpenAiDecisionAdapter() : undefined); @@ -109,6 +121,10 @@ export function createEmailExample( email: z.string(), instructions: z.string(), }), + output: z.object({ + replyEmail: z.string().nullable(), + clarifications: z.array(z.string()), + }), events: { 'user.answer': z.object({ answer: z.string() }), }, @@ -120,55 +136,41 @@ export function createEmailExample( questions: [] as string[], replyEmail: null as string | null, }), - adapter, initial: 'checking', states: { - checking: decide({ - model: 'openai/gpt-5.4-nano', - prompt: ({ context }) => { // why is this Record instead of a specific type? - const emailContext = context as { - email: string; - instructions: string; - clarifications: string[]; - }; - - return [ - 'Decide whether there is enough information to draft the reply email.', - 'Choose askForClarification only if key scheduling or identity details are missing.', - '', - `Email: ${emailContext.email}`, - `Instructions: ${emailContext.instructions}`, - `Clarifications: ${emailContext.clarifications.join(' | ') || 'none'}`, - ].join('\n'); - }, - options: { - askForClarification: { - description: 'Ask one or more clarifying questions before drafting.', - schema: z.object({ - questions: z.array(z.string()).min(1), - }), - }, - draft: { - description: 'Draft the email reply now.', - }, - }, + checking: { + resultSchema: decideResultSchema(checkingOptions), + invoke: async ({ context }) => + decide({ + adapter, + model: 'openai/gpt-5.4-nano', + prompt: [ + 'Decide whether there is enough information to draft the reply email.', + 'Choose askForClarification only if key scheduling or identity details are missing.', + '', + `Email: ${context.email}`, + `Instructions: ${context.instructions}`, + `Clarifications: ${context.clarifications.join(' | ') || 'none'}`, + ].join('\n'), + options: checkingOptions, + }), onDone: ({ result, context }) => { - const emailContext = context as { clarifications: string[] }; - - return ({ - target: - result.choice === 'askForClarification' && - emailContext.clarifications.length === 0 - ? 'clarifying' - : 'drafting', - context: - result.choice === 'askForClarification' && - emailContext.clarifications.length === 0 - ? { questions: result.data.questions } - : { questions: [] }, - }); + if ( + result.choice === 'askForClarification' + && context.clarifications.length === 0 + ) { + return { + target: 'clarifying', + context: { questions: result.data.questions }, + }; + } + + return { + target: 'drafting', + context: { questions: [] }, + }; }, - }), + }, clarifying: { on: { 'user.answer': ({ event, context }) => ({ diff --git a/examples/hitl.ts b/examples/hitl.ts index 31cd220..1074b87 100644 --- a/examples/hitl.ts +++ b/examples/hitl.ts @@ -32,6 +32,10 @@ export function createHitlExample( id: 'hitl-example', schemas: { input: z.object({ task: z.string() }), + output: z.object({ + draft: z.string().nullable().optional(), + cancelled: z.literal(true).optional(), + }), events: { 'user.message': z.object({ message: z.string() }), 'user.approve': z.object({}), @@ -70,11 +74,11 @@ export function createHitlExample( }, done: { type: 'final', - output: ({ context }) => ({ draft: context.draft }), + output: ({ context }) => ({ draft: context.draft ?? null }), }, cancelled: { type: 'final', - output: () => ({ cancelled: true }), + output: () => ({ cancelled: true as const }), }, }, }); diff --git a/examples/index.ts b/examples/index.ts index 3917d73..ce34ad0 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -9,13 +9,16 @@ export { createEmailExample } from './email.js'; export { createJokeExample } from './joke.js'; export { createJugsExample } from './jugs.js'; export { createMapReduceExample } from './map-reduce.js'; +export { createMultiAgentNetworkExample } from './multi-agent-network.js'; export { createNewspaperExample } from './newspaper.js'; export { createPlanAndExecuteExample } from './plan-and-execute.js'; export { createRaffleExample } from './raffle.js'; export { createReactAgentExample } from './react-agent.js'; +export { createRewooExample } from './rewoo.js'; export { createReflectionExample } from './reflection.js'; export { createRiverCrossingExample } from './river-crossing.js'; export { createBranchingExample } from './branching.js'; export { createSubflowExample } from './subflow.js'; +export { createSupervisorExample } from './supervisor.js'; export { createToolCallingExample } from './tool-calling.js'; export { createTutorExample } from './tutor.js'; diff --git a/examples/joke.ts b/examples/joke.ts index 413f864..fc4bdaf 100644 --- a/examples/joke.ts +++ b/examples/joke.ts @@ -52,6 +52,13 @@ export function createJokeExample( id: 'joke-example', schemas: { input: z.object({ topic: z.string() }), + output: z.object({ + topic: z.string(), + joke: z.string().nullable(), + rating: z.number().nullable(), + explanation: z.string().nullable(), + accepted: z.boolean(), + }), }, context: (input) => ({ topic: input.topic, diff --git a/examples/jugs.ts b/examples/jugs.ts index 53fcf8f..ae68c7e 100644 --- a/examples/jugs.ts +++ b/examples/jugs.ts @@ -56,6 +56,14 @@ function applyWaterJugMove( export function createJugsExample() { return createAgentMachine({ id: 'jugs-example', + schemas: { + output: z.object({ + jug3: z.number(), + jug5: z.number(), + steps: z.array(z.string()), + reasoning: z.array(z.string()), + }), + }, context: () => ({ jug3: 0, jug5: 0, @@ -79,21 +87,21 @@ export function createJugsExample() { return { target: 'applying' as const, - params: { move: result.move }, + input: { move: result.move }, context: { reasoning: nextReasoning }, }; }, }, applying: { - paramsSchema: z.object({ + inputSchema: z.object({ move: moveSchema.shape.move.exclude(['done']), }), resultSchema: applySchema, - invoke: async ({ context, params }) => + invoke: async ({ context, input }) => applyWaterJugMove( context.jug3, context.jug5, - params.move as 'fill5' | 'pour5to3' | 'empty3' + input.move as 'fill5' | 'pour5to3' | 'empty3' ), onDone: ({ result, context }) => ({ target: 'choosing', diff --git a/examples/map-reduce.ts b/examples/map-reduce.ts index ab881fd..2f6c255 100644 --- a/examples/map-reduce.ts +++ b/examples/map-reduce.ts @@ -32,6 +32,11 @@ export function createMapReduceExample( id: 'map-reduce-example', schemas: { input: z.object({ topic: z.string() }), + output: z.object({ + subjects: z.array(z.string()), + jokes: z.array(z.string()), + bestJoke: z.string().nullable(), + }), }, context: (input) => ({ topic: input.topic, diff --git a/examples/multi-agent-network.ts b/examples/multi-agent-network.ts new file mode 100644 index 0000000..65c0501 --- /dev/null +++ b/examples/multi-agent-network.ts @@ -0,0 +1,334 @@ +import { z } from 'zod'; +import { + createAgentMachine, + decide, + decideResultSchema, + type AgentAdapter, +} from '../src/index.js'; +import { + closePrompt, + createOpenAiDecisionAdapter, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const researchParamsSchema = z.object({ + focus: z.string(), +}); + +const writeParamsSchema = z.object({ + angle: z.string(), +}); + +const researchNotesSchema = z.object({ + notes: z.array(z.string()).min(2).max(5), +}); + +const researchHandoffSchema = z.object({ + notes: z.array(z.string()).min(2).max(5), + handoff: z.string(), +}); + +const draftSchema = z.object({ + draft: z.string(), +}); + +const draftHandoffSchema = z.object({ + draft: z.string(), + handoff: z.string(), +}); + +export function createMultiAgentNetworkExample( + options: { + adapter?: AgentAdapter; + research?: (args: { + topic: string; + focus: string; + }) => Promise>; + write?: (args: { + topic: string; + notes: string[]; + angle: string; + }) => Promise>; + } = {} +) { + const coordinatorOptions = { + research: { + description: 'Send the task to the research specialist.', + schema: researchParamsSchema, + }, + write: { + description: 'Send the task to the writing specialist.', + schema: writeParamsSchema, + }, + finalize: { + description: 'Stop the network and return the current result.', + }, + } as const; + + const adapter = options.adapter ?? createOpenAiDecisionAdapter(); + + const research = + options.research ?? + ((args: { topic: string; focus: string }) => + generateExampleObject({ + schema: researchNotesSchema, + system: 'You are a research specialist. Return concise notes only.', + prompt: [ + `Topic: ${args.topic}`, + `Focus: ${args.focus}`, + '', + 'Return 2 to 5 concise research notes that help another specialist continue the task.', + ].join('\n'), + })); + + const write = + options.write ?? + ((args: { topic: string; notes: string[]; angle: string }) => + generateExampleObject({ + schema: draftSchema, + system: 'You are a writing specialist. Turn notes into a concise draft.', + prompt: [ + `Topic: ${args.topic}`, + `Angle: ${args.angle}`, + '', + 'Notes:', + ...args.notes.map((note) => `- ${note}`), + '', + 'Write a short specialist draft.', + ].join('\n'), + })); + + const researchAgent = createAgentMachine({ + id: 'network-research-agent', + schemas: { + input: z.object({ + topic: z.string(), + focus: z.string(), + }), + output: z.object({ + notes: z.array(z.string()), + }), + }, + context: (input) => ({ + topic: input.topic, + focus: input.focus, + notes: [] as string[], + }), + initial: 'researching', + states: { + researching: { + resultSchema: researchNotesSchema, + invoke: async ({ context }) => + research({ + topic: context.topic, + focus: context.focus, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { notes: result.notes }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + notes: context.notes, + }), + }, + }, + }); + + const writerAgent = createAgentMachine({ + id: 'network-writer-agent', + schemas: { + input: z.object({ + topic: z.string(), + notes: z.array(z.string()), + angle: z.string(), + }), + output: z.object({ + draft: z.string(), + }), + }, + context: (input) => ({ + topic: input.topic, + notes: input.notes, + angle: input.angle, + draft: null as string | null, + }), + initial: 'writing', + states: { + writing: { + resultSchema: draftSchema, + invoke: async ({ context }) => + write({ + topic: context.topic, + notes: context.notes, + angle: context.angle, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { draft: result.draft }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + draft: context.draft ?? '', + }), + }, + }, + }); + + return createAgentMachine({ + id: 'multi-agent-network-example', + schemas: { + input: z.object({ topic: z.string() }), + output: z.object({ + topic: z.string(), + notes: z.array(z.string()), + draft: z.string().nullable(), + handoffs: z.array(z.string()), + }), + }, + context: (input) => ({ + topic: input.topic, + notes: [] as string[], + draft: null as string | null, + handoffs: [] as string[], + }), + initial: 'coordinating', + states: { + coordinating: { + resultSchema: decideResultSchema(coordinatorOptions), + invoke: async ({ context }) => + decide({ + adapter, + model: 'openai/gpt-5.4-nano', + prompt: [ + 'You are a coordinator deciding which specialist should act next.', + 'Route to research when the task needs more facts.', + 'Route to writing when there are enough notes to draft.', + 'Finalize only when a usable draft already exists.', + '', + `Topic: ${context.topic}`, + context.notes.length + ? `Notes:\n${context.notes.map((note) => `- ${note}`).join('\n')}` + : 'Notes: none yet', + context.draft ? `Current draft:\n${context.draft}` : 'Current draft: none yet', + context.handoffs.length + ? `Prior handoffs:\n${context.handoffs.map((handoff, index) => `${index + 1}. ${handoff}`).join('\n')}` + : 'Prior handoffs: none', + ].join('\n'), + options: coordinatorOptions, + }), + onDone: ({ result }) => { + if (result.choice === 'research') { + return { + target: 'researching', + input: { + focus: result.data.focus ?? 'gather the most useful supporting facts', + }, + }; + } + + if (result.choice === 'write') { + return { + target: 'writing', + input: { + angle: result.data.angle ?? 'produce the clearest concise draft', + }, + }; + } + + return { + target: 'done', + }; + }, + }, + researching: { + inputSchema: researchParamsSchema, + resultSchema: researchHandoffSchema, + invoke: async ({ context, input }) => { + const result = await researchAgent.execute( + researchAgent.getInitialState({ + topic: context.topic, + focus: input.focus, + }) + ); + + if (result.status !== 'done') { + throw new Error('Research agent did not finish'); + } + + return { + notes: result.output.notes, + handoff: `researcher:${input.focus}`, + }; + }, + onDone: ({ result, context }) => ({ + target: 'coordinating', + context: { + notes: result.notes, + handoffs: [...context.handoffs, result.handoff], + }, + }), + }, + writing: { + inputSchema: writeParamsSchema, + resultSchema: draftHandoffSchema, + invoke: async ({ context, input }) => { + const result = await writerAgent.execute( + writerAgent.getInitialState({ + topic: context.topic, + notes: context.notes, + angle: input.angle, + }) + ); + + if (result.status !== 'done') { + throw new Error('Writer agent did not finish'); + } + + return { + draft: result.output.draft, + handoff: `writer:${input.angle}`, + }; + }, + onDone: ({ result, context }) => ({ + target: 'coordinating', + context: { + draft: result.draft, + handoffs: [...context.handoffs, result.handoff], + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + topic: context.topic, + notes: context.notes, + draft: context.draft, + handoffs: context.handoffs, + }), + }, + }, + }); +} + +async function main() { + try { + const topic = await prompt('Topic'); + const machine = createMultiAgentNetworkExample(); + const result = await machine.execute(machine.getInitialState({ topic })); + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/newspaper.ts b/examples/newspaper.ts index b312d1b..ec74eb3 100644 --- a/examples/newspaper.ts +++ b/examples/newspaper.ts @@ -93,6 +93,12 @@ export function createNewspaperExample( id: 'newspaper-example', schemas: { input: z.object({ topic: z.string() }), + output: z.object({ + topic: z.string(), + article: z.string().nullable(), + revisionCount: z.number(), + searchResults: z.array(z.string()), + }), }, context: (input) => ({ topic: input.topic, diff --git a/examples/plan-and-execute.ts b/examples/plan-and-execute.ts index 658d673..86f7d08 100644 --- a/examples/plan-and-execute.ts +++ b/examples/plan-and-execute.ts @@ -82,6 +82,12 @@ export function createPlanAndExecuteExample( id: 'plan-and-execute-example', schemas: { input: z.object({ goal: z.string() }), + output: z.object({ + goal: z.string(), + plan: z.array(z.string()), + stepResults: z.array(z.string()), + answer: z.string().nullable(), + }), }, context: (input) => ({ goal: input.goal, @@ -97,18 +103,18 @@ export function createPlanAndExecuteExample( onDone: ({ result }) => ({ target: 'executing', context: { plan: result.plan }, - params: { index: 0 } + input: { index: 0 } }), }, executing: { - paramsSchema: z.object({ + inputSchema: z.object({ index: z.number().int().min(0), }), resultSchema: stepResultSchema, - invoke: async ({ context, params }) => + invoke: async ({ context, input }) => executeStep({ goal: context.goal, - step: context.plan[params.index] ?? '', + step: context.plan[input.index] ?? '', priorResults: context.stepResults, }), onDone: ({ result, context }) => { @@ -119,7 +125,7 @@ export function createPlanAndExecuteExample( return { target: 'executing' as const, context: { stepResults: nextStepResults }, - params: { index: nextIndex }, + input: { index: nextIndex }, }; } diff --git a/examples/raffle.ts b/examples/raffle.ts index 4714e98..b649860 100644 --- a/examples/raffle.ts +++ b/examples/raffle.ts @@ -32,6 +32,13 @@ export function createRaffleExample( return createAgentMachine({ id: 'raffle-example', schemas: { + output: z.object({ + entries: z.array(z.string()), + winner: z.string().nullable(), + firstRunnerUp: z.string().nullable(), + secondRunnerUp: z.string().nullable(), + explanation: z.string().nullable(), + }), events: { 'user.entry': z.object({ entry: z.string() }), 'user.draw': z.object({}), diff --git a/examples/react-agent.ts b/examples/react-agent.ts index ecdc3d1..b38b391 100644 --- a/examples/react-agent.ts +++ b/examples/react-agent.ts @@ -81,24 +81,19 @@ async function main() { }); run.on('toolCall', (event) => { - const call = event as { toolName: string; input: { query: string } }; - console.log(`Calling ${call.toolName}(${call.input.query})`); + console.log(`Calling ${event.toolName}(${event.input.query})`); }); run.on('toolResult', (event) => { - const result = event as { - toolName: string; - output: unknown; - }; - console.log(`${result.toolName} -> ${String(result.output)}`); + console.log(`${event.toolName} -> ${String(event.output)}`); }); await new Promise((resolve, reject) => { - run.on('done', (event) => { - console.log((event as { output: unknown }).output); + run.onDone((event) => { + console.log(event.output); resolve(); }); - run.on('error', (event) => { - reject((event as { error: unknown }).error); + run.onError((event) => { + reject(event.error); }); }); } finally { diff --git a/examples/reflection.ts b/examples/reflection.ts index 86abe8e..529faa9 100644 --- a/examples/reflection.ts +++ b/examples/reflection.ts @@ -77,6 +77,12 @@ export function createReflectionExample( id: 'reflection-example', schemas: { input: z.object({ task: z.string() }), + output: z.object({ + task: z.string(), + draft: z.string().nullable(), + feedback: z.string().nullable(), + revisionCount: z.number(), + }), }, context: (input) => ({ task: input.task, diff --git a/examples/rewoo.ts b/examples/rewoo.ts new file mode 100644 index 0000000..fba6b82 --- /dev/null +++ b/examples/rewoo.ts @@ -0,0 +1,235 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const rewooPlanSchema = z.object({ + steps: z + .array( + z.object({ + id: z.string().regex(/^E\d+$/), + instruction: z.string(), + input: z.string(), + }) + ) + .min(1) + .max(5), +}); + +const rewooStepResultSchema = z.object({ + result: z.string(), +}); + +const rewooAnswerSchema = z.object({ + answer: z.string(), +}); + +type RewooPlan = z.infer; +type RewooStep = RewooPlan['steps'][number]; + +function resolveStepInput( + template: string, + resultsById: Record +): string { + return template.replace(/#(E\d+)/g, (_match, id: string) => resultsById[id] ?? ''); +} + +export function createRewooExample( + options: { + plan?: (objective: string) => Promise; + executeStep?: (args: { + objective: string; + step: RewooStep; + resolvedInput: string; + resultsById: Record; + }) => Promise>; + solve?: (args: { + objective: string; + steps: RewooPlan['steps']; + resultsById: Record; + }) => Promise>; + } = {} +) { + const plan = + options.plan ?? + ((objective: string) => + generateExampleObject({ + schema: rewooPlanSchema, + system: [ + 'You are a ReWOO-style planner.', + 'Produce a short sequence of executable steps.', + 'Each step must have an id like E1, E2, E3.', + 'Later step inputs may reference earlier outputs using #E1, #E2, etc.', + ].join('\n'), + prompt: `Create a compact executable plan for this objective:\n\n${objective}`, + })); + + const executeStep = + options.executeStep ?? + ((args: { + objective: string; + step: RewooStep; + resolvedInput: string; + resultsById: Record; + }) => + generateExampleObject({ + schema: rewooStepResultSchema, + system: 'You execute one specialist step at a time and return a concise result.', + prompt: [ + `Objective: ${args.objective}`, + `Step id: ${args.step.id}`, + `Instruction: ${args.step.instruction}`, + `Resolved input: ${args.resolvedInput}`, + Object.keys(args.resultsById).length + ? `Prior results:\n${Object.entries(args.resultsById) + .map(([id, value]) => `${id}: ${value}`) + .join('\n')}` + : 'Prior results: none', + ].join('\n'), + })); + + const solve = + options.solve ?? + ((args: { + objective: string; + steps: RewooPlan['steps']; + resultsById: Record; + }) => + generateExampleObject({ + schema: rewooAnswerSchema, + system: 'You synthesize completed step results into a direct final answer.', + prompt: [ + `Objective: ${args.objective}`, + '', + 'Completed steps:', + ...args.steps.map((step) => `${step.id}. ${step.instruction}`), + '', + 'Results:', + ...Object.entries(args.resultsById).map(([id, value]) => `${id}: ${value}`), + '', + 'Write the final answer.', + ].join('\n'), + })); + + return createAgentMachine({ + id: 'rewoo-example', + schemas: { + input: z.object({ objective: z.string() }), + output: z.object({ + objective: z.string(), + steps: rewooPlanSchema.shape.steps, + resultsById: z.record(z.string(), z.string()), + answer: z.string().nullable(), + }), + }, + context: (input) => ({ + objective: input.objective, + steps: [] as RewooPlan['steps'], + resultsById: {} as Record, + answer: null as string | null, + }), + initial: 'planning', + states: { + planning: { + resultSchema: rewooPlanSchema, + invoke: async ({ context }) => plan(context.objective), + onDone: ({ result }) => ({ + target: 'executing', + context: { steps: result.steps }, + input: { index: 0 }, + }), + }, + executing: { + inputSchema: z.object({ + index: z.number().int().min(0), + }), + resultSchema: z.object({ + stepId: z.string(), + result: z.string(), + }), + invoke: async ({ context, input }) => { + const step = context.steps[input.index]; + + if (!step) { + throw new Error(`Missing step at index ${input.index}`); + } + + const resolvedInput = resolveStepInput(step.input, context.resultsById); + const outcome = await executeStep({ + objective: context.objective, + step, + resolvedInput, + resultsById: context.resultsById, + }); + + return { + stepId: step.id, + result: outcome.result, + }; + }, + onDone: ({ result, context }) => { + const nextResultsById = { + ...context.resultsById, + [result.stepId]: result.result, + }; + const nextIndex = Object.keys(nextResultsById).length; + + if (nextIndex < context.steps.length) { + return { + target: 'executing', + context: { resultsById: nextResultsById }, + input: { index: nextIndex }, + }; + } + + return { + target: 'solving', + context: { resultsById: nextResultsById }, + }; + }, + }, + solving: { + resultSchema: rewooAnswerSchema, + invoke: async ({ context }) => + solve({ + objective: context.objective, + steps: context.steps, + resultsById: context.resultsById, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { answer: result.answer }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + objective: context.objective, + steps: context.steps, + resultsById: context.resultsById, + answer: context.answer, + }), + }, + }, + }); +} + +async function main() { + try { + const objective = await prompt('Objective'); + const machine = createRewooExample(); + const result = await machine.execute(machine.getInitialState({ objective })); + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/river-crossing.ts b/examples/river-crossing.ts index dd738bb..c628267 100644 --- a/examples/river-crossing.ts +++ b/examples/river-crossing.ts @@ -93,6 +93,14 @@ function moveItem( export function createRiverCrossingExample() { return createAgentMachine({ id: 'river-crossing-example', + schemas: { + output: z.object({ + leftBank: z.array(bankItem), + rightBank: z.array(bankItem), + steps: z.array(z.string()), + reasoning: z.array(z.string()), + }), + }, context: () => ({ leftBank: ['wolf', 'goat', 'cabbage'] as Array<'wolf' | 'goat' | 'cabbage'>, rightBank: [] as Array<'wolf' | 'goat' | 'cabbage'>, @@ -122,22 +130,22 @@ export function createRiverCrossingExample() { return { target: 'moving' as const, - params: { move: result.move }, + input: { move: result.move }, context: { reasoning: nextReasoning }, }; }, }, moving: { - paramsSchema: z.object({ + inputSchema: z.object({ move: crossingMoveSchema.shape.move.exclude(['done']), }), resultSchema: crossingStateSchema, - invoke: async ({ context, params }) => + invoke: async ({ context, input }) => moveItem( [...context.leftBank], [...context.rightBank], context.farmerPosition, - params.move as 'takeGoat' | 'takeWolf' | 'takeCabbage' | 'returnEmpty' + input.move as 'takeGoat' | 'takeWolf' | 'takeCabbage' | 'returnEmpty' ), onDone: ({ result, context }) => ({ target: 'choosing', diff --git a/examples/simple.ts b/examples/simple.ts index 5ab67bb..812fb22 100644 --- a/examples/simple.ts +++ b/examples/simple.ts @@ -26,6 +26,7 @@ export function createSimpleExample( id: 'simple-example', schemas: { input: z.object({ text: z.string() }), + output: z.object({ summary: z.string().nullable() }), }, context: (input) => ({ text: input.text, diff --git a/examples/subflow.ts b/examples/subflow.ts index 6afd439..5946a3a 100644 --- a/examples/subflow.ts +++ b/examples/subflow.ts @@ -29,6 +29,7 @@ export function createSubflowExample( id: 'subflow-child', schemas: { input: z.object({ topic: z.string() }), + output: z.object({ bullets: z.array(z.string()) }), }, context: (input) => ({ topic: input.topic, @@ -62,6 +63,10 @@ export function createSubflowExample( id: 'subflow-example', schemas: { input: z.object({ topic: z.string() }), + output: z.object({ + bullets: z.array(z.string()), + draft: z.string().nullable(), + }), }, context: (input) => ({ topic: input.topic, @@ -82,7 +87,7 @@ export function createSubflowExample( } return { - bullets: (result.output as { bullets: string[] }).bullets, + bullets: result.output.bullets, }; }, onDone: ({ result }) => ({ diff --git a/examples/supervisor.ts b/examples/supervisor.ts new file mode 100644 index 0000000..cdbf1e2 --- /dev/null +++ b/examples/supervisor.ts @@ -0,0 +1,252 @@ +import { z } from 'zod'; +import { + createAgentMachine, + decide, + decideResultSchema, + type AgentAdapter, +} from '../src/index.js'; +import { + closePrompt, + createOpenAiDecisionAdapter, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const handlingParamsSchema = z.object({ + attempt: z.number().int().min(1), + instruction: z.string().nullable().optional(), +}); + +const workerResultSchema = z.discriminatedUnion('status', [ + z.object({ + status: z.literal('resolved'), + response: z.string(), + }), + z.object({ + status: z.literal('blocked'), + issue: z.string(), + }), +]); + +const supervisorOptions = { + retry: { + description: 'Retry the worker with a concrete instruction for the next attempt.', + schema: z.object({ + instruction: z.string(), + }), + }, + escalate: { + description: 'Escalate the task to a human or specialist owner.', + schema: z.object({ + reason: z.string(), + }), + }, +} as const; + +export function createSupervisorExample( + options: { + adapter?: AgentAdapter; + handle?: (args: { + request: string; + attempt: number; + instruction: string | null; + priorIssues: string[]; + }) => Promise>; + maxAttempts?: number; + } = {} +) { + const adapter = options.adapter ?? createOpenAiDecisionAdapter(); + const maxAttempts = options.maxAttempts ?? 2; + const handle = + options.handle ?? + ((args: { + request: string; + attempt: number; + instruction: string | null; + priorIssues: string[]; + }) => + generateExampleObject({ + schema: workerResultSchema, + system: [ + 'You are an operations worker handling a support request.', + 'Resolve the request when you have enough information.', + 'Return status="blocked" with a concise issue when the request cannot be completed yet.', + ].join('\n'), + prompt: [ + `Request: ${args.request}`, + `Attempt: ${args.attempt}`, + args.instruction + ? `Supervisor instruction: ${args.instruction}` + : 'Supervisor instruction: none', + args.priorIssues.length + ? `Prior issues:\n${args.priorIssues.map((issue, index) => `${index + 1}. ${issue}`).join('\n')}` + : 'Prior issues: none', + ].join('\n'), + })); + + return createAgentMachine({ + id: 'supervisor-example', + schemas: { + input: z.object({ + request: z.string(), + }), + output: z.object({ + request: z.string(), + status: z.enum(['resolved', 'escalated']), + resolution: z.string().nullable(), + escalationReason: z.string().nullable(), + attemptCount: z.number().int().min(0), + history: z.array(z.string()), + }), + }, + context: (input) => ({ + request: input.request, + attemptCount: 0, + latestIssue: null as string | null, + resolution: null as string | null, + escalationReason: null as string | null, + history: [] as string[], + priorIssues: [] as string[], + }), + initial: ({ context }) => ({ + target: 'handling', + input: { + attempt: 1, + instruction: null, + }, + context, + }), + states: { + handling: { + inputSchema: handlingParamsSchema, + resultSchema: workerResultSchema, + invoke: async ({ context, input }) => + handle({ + request: context.request, + attempt: input.attempt, + instruction: input.instruction ?? null, + priorIssues: context.priorIssues, + }), + onDone: ({ result, context, }) => { + const nextAttemptCount = context.attemptCount + 1; + + if (result.status === 'resolved') { + return { + target: 'done', + context: { + attemptCount: nextAttemptCount, + resolution: result.response, + history: [ + ...context.history, + `worker:${nextAttemptCount}:resolved:${result.response}`, + ], + }, + }; + } + + return { + target: 'supervising', + context: { + attemptCount: nextAttemptCount, + latestIssue: result.issue, + priorIssues: [...context.priorIssues, result.issue], + history: [ + ...context.history, + `worker:${nextAttemptCount}:blocked:${result.issue}`, + ], + }, + }; + }, + }, + supervising: { + resultSchema: decideResultSchema(supervisorOptions), + invoke: async ({ context }) => + decide({ + adapter, + model: 'openai/gpt-5.4-nano', + prompt: [ + 'You supervise a worker that may need retries or escalation.', + `Max attempts: ${maxAttempts}`, + `Completed attempts: ${context.attemptCount}`, + '', + `Request: ${context.request}`, + `Latest issue: ${context.latestIssue ?? 'none'}`, + context.history.length + ? `History:\n${context.history.map((entry, index) => `${index + 1}. ${entry}`).join('\n')}` + : 'History: none', + '', + context.attemptCount >= maxAttempts + ? 'You should normally escalate because the worker has reached the attempt limit.' + : 'Retry only if a concrete next instruction could unblock the worker.', + ].join('\n'), + options: supervisorOptions, + }), + onDone: ({ result, context }) => { + if (result.choice === 'retry') { + const instruction = + result.data.instruction + ?? 'Retry once with a more concrete plan and any available context.'; + + return { + target: 'handling', + context: { + history: [ + ...context.history, + `supervisor:retry:${instruction}`, + ], + }, + input: { + attempt: context.attemptCount + 1, + instruction, + }, + }; + } + + const reason = + result.data.reason + ?? `Escalated after ${context.attemptCount} unsuccessful attempts.`; + + return { + target: 'done', + context: { + escalationReason: reason, + history: [ + ...context.history, + `supervisor:escalate:${reason}`, + ], + }, + }; + }, + }, + done: { + type: 'final', + output: ({ context }) => ({ + request: context.request, + status: context.resolution ? ('resolved' as const) : ('escalated' as const), + resolution: context.resolution, + escalationReason: context.escalationReason, + attemptCount: context.attemptCount, + history: context.history, + }), + }, + }, + }); +} + +async function main() { + try { + const request = await prompt('Request'); + const machine = createSupervisorExample(); + console.log( + formatResult(await machine.execute(machine.getInitialState({ request }))) + ); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/tool-calling.ts b/examples/tool-calling.ts index cae6c45..8472aae 100644 --- a/examples/tool-calling.ts +++ b/examples/tool-calling.ts @@ -29,6 +29,7 @@ export function createToolCallingExample( id: 'tool-calling-example', schemas: { input: z.object({ city: z.string() }), + output: z.object({ forecast: z.string().nullable() }), emitted: { toolCall: z.object({ toolName: z.string(), @@ -88,25 +89,20 @@ async function main() { }); run.on('toolCall', (event) => { - const tool = event as { toolName: string; input: { city: string } }; - console.log(`Calling ${tool.toolName}(${tool.input.city})`); + console.log(`Calling ${event.toolName}(${event.input.city})`); }); run.on('toolResult', (event) => { - const result = event as { - toolName: string; - output: { forecast: string }; - }; - console.log(`${result.toolName} -> ${result.output.forecast}`); + console.log(`${event.toolName} -> ${event.output.forecast}`); }); await new Promise((resolve, reject) => { - run.on('done', (event) => { - console.log((event as { output: unknown }).output); + run.onDone((event) => { + console.log(event.output); resolve(); }); - run.on('error', (event) => { - reject((event as { error: unknown }).error); + run.onError((event) => { + reject(event.error); }); }); } finally { diff --git a/examples/tutor.ts b/examples/tutor.ts index 7b12af8..16193e3 100644 --- a/examples/tutor.ts +++ b/examples/tutor.ts @@ -43,6 +43,11 @@ export function createTutorExample( id: 'tutor-example', schemas: { input: z.object({ message: z.string() }), + output: z.object({ + conversation: z.array(z.string()), + feedback: z.string().nullable(), + response: z.string().nullable(), + }), }, context: (input) => ({ conversation: [`User: ${input.message}`], diff --git a/src/agent.test.ts b/src/agent.test.ts index 7c2eaaa..7813732 100644 --- a/src/agent.test.ts +++ b/src/agent.test.ts @@ -1,10 +1,12 @@ import { describe, expect, test, vi } from 'vitest'; import { z } from 'zod'; import { - createAgentMachine, - decide, classify, + classifyResultSchema, + createAgentMachine, createAdapter, + decide, + decideResultSchema, } from './index.js'; import type { AgentAdapter } from './types.js'; @@ -31,6 +33,12 @@ function mockAdapter( }; } +const choiceResultSchema = z.object({ + choice: z.string(), + data: z.record(z.string(), z.unknown()), + reasoning: z.string().optional(), +}); + // ─── Simple machine (no schemas — inferred from context) ─── function createSimpleMachine() { @@ -144,6 +152,12 @@ function createHitlMachine() { // ─── Decide machine ─── function createDecideMachine(adapter: AgentAdapter) { + const options = { + billing: { description: 'Billing issues' }, + technical: { description: 'Technical issues' }, + general: { description: 'General inquiries' }, + } as const; + return createAgentMachine({ id: 'decider', context: () => ({ @@ -151,22 +165,22 @@ function createDecideMachine(adapter: AgentAdapter) { category: null as string | null, resolution: null as string | null, }), - adapter, initial: 'classifying', states: { - classifying: decide({ - model: 'test-model', - prompt: ({ context }) => `Classify: ${context.issue}`, - options: { - billing: { description: 'Billing issues' }, - technical: { description: 'Technical issues' }, - general: { description: 'General inquiries' }, - }, + classifying: { + resultSchema: decideResultSchema(options), + invoke: async ({ context }) => + decide({ + adapter, + model: 'test-model', + prompt: `Classify: ${context.issue}`, + options, + }), onDone: ({ result }) => ({ target: 'handling', context: { category: result.choice }, }), - }), + }, handling: { resultSchema: z.object({ resolution: z.string() }), invoke: async ({ context }) => ({ @@ -191,28 +205,34 @@ function createDecideMachine(adapter: AgentAdapter) { // ─── Classify machine ─── function createClassifyMachine(adapter: AgentAdapter) { + const categories = { + billing: { description: 'Billing, payments, refunds' }, + technical: { description: 'Technical issues, bugs' }, + general: { description: 'General inquiries' }, + } as const; + return createAgentMachine({ id: 'classifier', context: () => ({ issue: 'I want my money back', category: null as string | null, }), - adapter, initial: 'classifyIntent', states: { - classifyIntent: classify({ - model: 'test-model', - prompt: ({ context }) => `Classify: "${context.issue}"`, - into: { - billing: { description: 'Billing, payments, refunds' }, - technical: { description: 'Technical issues, bugs' }, - general: { description: 'General inquiries' }, - }, + classifyIntent: { + resultSchema: classifyResultSchema(categories), + invoke: async ({ context }) => + classify({ + adapter, + model: 'test-model', + prompt: `Classify: "${context.issue}"`, + into: categories, + }), onDone: ({ result }) => ({ target: 'done', context: { category: result.category }, }), - }), + }, done: { type: 'final', output: ({ context }) => ({ category: context.category }), @@ -333,12 +353,15 @@ describe('invoke', () => { context: () => ({}), initial: 'deciding', states: { - deciding: decide({ - model: 'test', - prompt: 'test', - options: { a: { description: 'A' } }, + deciding: { + invoke: async () => + decide({ + model: 'test', + prompt: 'test', + options: { a: { description: 'A' } }, + }), onDone: () => ({ target: 'done' }), - }), + }, done: { type: 'final' }, }, }); @@ -492,19 +515,26 @@ describe('decide', () => { const spy = vi.fn().mockResolvedValue({ choice: 'a', data: {} }); const machine = createAgentMachine({ id: 'dtest', - context: () => ({ topic: 'cats' }), - adapter: { decide: spy }, + context: () => ({ topic: 'cats', choice: null as string | null }), initial: 'choosing', states: { - choosing: decide({ - model: 'my-model', - prompt: ({ context }) => `About ${context.topic}`, - options: { a: { description: 'A' }, b: { description: 'B' } }, + choosing: { + resultSchema: decideResultSchema({ + a: { description: 'A' }, + b: { description: 'B' }, + }), + invoke: async ({ context }) => + decide({ + adapter: { decide: spy }, + model: 'my-model', + prompt: `About ${context.topic}`, + options: { a: { description: 'A' }, b: { description: 'B' } }, + }), onDone: ({ result }) => ({ target: 'done', context: { choice: result.choice }, }), - }), + }, done: { type: 'final' }, }, }); @@ -518,19 +548,28 @@ describe('decide', () => { const machine = createAgentMachine({ id: 'override', context: () => ({ choice: null as string | null }), - adapter: mockAdapter([{ choice: 'machine' }]), initial: 'choosing', states: { - choosing: decide({ - model: 'test', - adapter: mockAdapter([{ choice: 'state' }]), - prompt: 'pick', - options: { s: { description: 'S' }, m: { description: 'M' } }, + choosing: { + resultSchema: decideResultSchema({ + state: { description: 'State' }, + machine: { description: 'Machine' }, + }), + invoke: async () => + decide({ + adapter: mockAdapter([{ choice: 'state' }]), + model: 'test', + prompt: 'pick', + options: { + state: { description: 'State' }, + machine: { description: 'Machine' }, + }, + }), onDone: ({ result }) => ({ target: 'done', context: { choice: result.choice }, }), - }), + }, done: { type: 'final' }, }, }); @@ -542,34 +581,46 @@ describe('decide', () => { const machine = createAgentMachine({ id: 'data', context: () => ({ items: null as string[] | null }), - adapter: { - decide: async () => ({ - choice: 'withData', - data: { items: ['a', 'b'] }, - }), - }, initial: 'choosing', states: { - choosing: decide({ - model: 'test', - prompt: 'pick', - options: { + choosing: { + resultSchema: decideResultSchema({ withData: { description: 'Has data', schema: z.object({ items: z.array(z.string()) }), }, withoutData: { description: 'No data' }, - }, - onDone: ({ result }) => ({ - target: 'done', - context: { - items: - result.choice === 'withData' - ? result.data.items - : null, - }, }), - }), + invoke: async () => + decide({ + adapter: { + decide: async () => ({ + choice: 'withData', + data: { items: ['a', 'b'] }, + }), + }, + model: 'test', + prompt: 'pick', + options: { + withData: { + description: 'Has data', + schema: z.object({ items: z.array(z.string()) }), + }, + withoutData: { description: 'No data' }, + }, + }), + onDone: ({ result }) => { + return { + target: 'done', + context: { + items: + result.choice === 'withData' + ? (result.data.items ?? null) + : null, + }, + }; + }, + }, done: { type: 'final' }, }, }); @@ -589,6 +640,7 @@ describe('type: choice', () => { states: { routing: { type: 'choice', + resultSchema: choiceResultSchema, model: 'test-model', prompt: ({ context }) => `Route: ${context.issue}`, // context typed ✓ options: { @@ -628,6 +680,7 @@ describe('type: choice', () => { states: { choosing: { type: 'choice', + resultSchema: choiceResultSchema, model: 'test', prompt: 'pick', options: { a: { description: 'A' } }, @@ -784,9 +837,9 @@ describe('type inference', () => { context: () => ({ count: 0 }), initial: 'idle', states: { - // @ts-expect-error — 'foo' not a valid context key idle: { on: { + // @ts-expect-error — 'foo' not a valid context key go: () => ({ target: 'idle', context: { foo: 'bar' }, @@ -850,6 +903,11 @@ describe('type inference', () => { test('context typed in output', () => { const machine = createAgentMachine({ id: 't', + schemas: { + output: z.object({ + score: z.number(), + }), + }, context: () => ({ score: 100 }), initial: 'done', states: { @@ -1014,41 +1072,41 @@ describe('type inference', () => { ).toThrow(); }); - // ─── paramsSchema per state ─── + // ─── inputSchema per state ─── - test('params typed per state from paramsSchema', async () => { + test('input typed per state from inputSchema', async () => { const machine = createAgentMachine({ id: 't', context: () => ({ result: '' }), initial: 'a', states: { a: { - paramsSchema: z.object({ count: z.number() }), + inputSchema: z.object({ count: z.number() }), resultSchema: z.object({ doubled: z.number() }), - invoke: async ({ params }) => { - params.count satisfies number; + invoke: async ({ input }) => { + input.count satisfies number; // @ts-expect-error — count is number not string - params.count satisfies string; - // @ts-expect-error — 'name' not on a's params - params.name; - return { doubled: params.count * 2 }; + input.count satisfies string; + // @ts-expect-error — 'name' not on a's input + input.name; + return { doubled: input.count * 2 }; }, onDone: ({ result }) => ({ target: 'b', - params: { name: 'hello' }, + input: { name: 'hello' }, context: { result: String(result.doubled) }, }), }, b: { - paramsSchema: z.object({ name: z.string() }), + inputSchema: z.object({ name: z.string() }), resultSchema: z.object({ greeting: z.string() }), - invoke: async ({ params }) => { - params.name satisfies string; + invoke: async ({ input }) => { + input.name satisfies string; // @ts-expect-error — name is string not number - params.name satisfies number; - // @ts-expect-error — 'count' not on b's params - params.count; - return { greeting: `hi ${params.name}` }; + input.name satisfies number; + // @ts-expect-error — 'count' not on b's input + input.count; + return { greeting: `hi ${input.name}` }; }, onDone: ({ result }) => ({ target: 'done', @@ -1064,21 +1122,21 @@ describe('type inference', () => { let state = machine.resolveState({ ...machine.getInitialState(), - params: { a: { count: 21 } }, + input: { a: { count: 21 } }, }); const r = await machine.execute(state); expect(r.status === 'done' && r.output).toEqual({ result: 'hi hello' }); }); - test('no paramsSchema → params is Record', () => { + test('no inputSchema → input is Record', () => { createAgentMachine({ id: 't', context: () => ({}), initial: 'idle', states: { idle: { - invoke: async ({ params }) => { - params satisfies Record; + invoke: async ({ input }) => { + input satisfies Record; return {}; }, }, @@ -1098,6 +1156,7 @@ describe('type inference', () => { states: { choosing: { type: 'choice', + resultSchema: choiceResultSchema, model: 'test', prompt: ({ context }) => { context.topic satisfies string; @@ -1188,7 +1247,7 @@ describe('type inference', () => { }); }); - test('no resultSchema → onDone result is any', () => { + test('no resultSchema → onDone result is inferred from invoke', () => { createAgentMachine({ id: 't', context: () => ({}), @@ -1197,10 +1256,9 @@ describe('type inference', () => { work: { invoke: async () => ({ anything: true }), onDone: ({ result }) => { - // Without resultSchema, result is ChoiceResult (default) - result.choice satisfies string; - // @ts-expect-error — 'whatever' not on ChoiceResult - result.whatever; + result.anything satisfies boolean; + // @ts-expect-error — 'choice' does not exist on invoke result + result.choice; return { target: 'done' }; }, }, @@ -1209,6 +1267,45 @@ describe('type inference', () => { }); }); + test('final output is inferred through execute and snapshots', async () => { + const machine = createAgentMachine({ + id: 'typed-output', + schemas: { + output: z.object({ + count: z.number(), + label: z.string(), + }), + }, + context: () => ({ count: 2 }), + initial: 'done', + states: { + done: { + type: 'final', + output: ({ context }) => ({ + count: context.count, + label: `count:${context.count}`, + }), + }, + }, + }); + + const runResult = await machine.execute(machine.getInitialState()); + if (runResult.status === 'done') { + runResult.output.count satisfies number; + runResult.output.label satisfies string; + // @ts-expect-error output property should be typed + runResult.output.missing; + } + + const snapshot = machine.resolveState(machine.getInitialState()); + snapshot.output satisfies + | { + count: number; + label: string; + } + | undefined; + }); + // ─── events typed in on handlers ─── test('on handler event typed from schemas.events', () => { @@ -1292,7 +1389,7 @@ describe('edge cases', () => { const machine = createSimpleMachine(); const done = { value: 'done', - params: {}, + input: {}, context: { count: 1 }, status: 'done', output: { result: 1 }, diff --git a/src/classify.ts b/src/classify.ts index d0ee89c..12f2c0e 100644 --- a/src/classify.ts +++ b/src/classify.ts @@ -1,96 +1,67 @@ +import { z } from 'zod'; +import { decide } from './decide.js'; import type { - AgentAdapter, - InvokeEnqueue, - StateConfig, - TransitionResult, + ClassifyOptions, + ClassifyResultFor, + StandardSchemaV1, } from './types.js'; -type TransitionTargetOf = T extends { target?: infer TTarget } - ? Extract - : never; +export async function classify< + const TCategories extends Record, +>( + options: ClassifyOptions +): Promise> { + const result = await decide({ + adapter: options.adapter, + model: options.model, + prompt: buildClassificationPrompt(options.prompt, options.examples), + options: options.into, + reasoning: options.reasoning, + }); -type HandlerTargetOf = T extends (...args: any[]) => infer TResult - ? TransitionTargetOf - : TransitionTargetOf; + return { + category: result.choice as keyof TCategories & string, + }; +} -type OnTargets = TOn extends Record - ? HandlerTargetOf - : never; +function buildClassificationPrompt( + prompt: string, + examples: Array<{ input: string; category: string }> | undefined +): string { + if (!examples?.length) { + return prompt; + } -type ClassifyStateConfig< - TContext extends Record, - TTarget extends string, - TParamsByTarget extends Record, -> = Pick< - StateConfig, - 'on' -> & { - __type: 'classify'; - __classifyConfig: Record; - __decideConfig: Record; -}; + return [ + prompt, + '', + 'Examples:', + ...examples.map((example) => `${example.category}: ${example.input}`), + ].join('\n'); +} -/** - * Create a classification state. Sugar over `decide` for simple routing — - * categories with descriptions, no per-option schemas. - * - * `result.category` is typed as a union of the `into` keys. - * - */ -export function classify< - TContext extends Record, +export function classifyResultSchema< const TCategories extends Record, - TParams extends Record = Record, - TTarget extends string = string, - TParamsByTarget extends Record = {}, >( - config: { - model: string; - adapter?: AgentAdapter; - prompt: string | ((args: { context: TContext; params: TParams }) => string); - into: TCategories; - examples?: Array<{ input: string; category: keyof TCategories & string }>; - onDone: (args: { - result: { category: keyof TCategories & string }; - context: TContext; - }) => TransitionResult; - on?: Record< - string, - ( - args: { event: any; context: TContext }, - enq: InvokeEnqueue - ) => TransitionResult - >; - } -): ClassifyStateConfig< - TContext, - TTarget, - TParamsByTarget -> { - const decideOptions: Record = {}; - for (const [key, val] of Object.entries(config.into)) { - decideOptions[key] = { description: val.description }; + into: TCategories +): StandardSchemaV1> { + const categories = Object.keys(into); + if (categories.length === 0) { + throw new Error('classifyResultSchema requires at least one category'); } - return { - __type: 'classify', - __classifyConfig: config as unknown as Record, - __decideConfig: { - model: config.model, - adapter: config.adapter, - prompt: config.prompt, - options: decideOptions, - onDone: ({ result, context }: any) => { - return config.onDone({ - result: { category: result.choice }, - context, - }); - }, - }, - on: config.on as StateConfig< - TContext, - TTarget, - TParamsByTarget - >['on'], - }; + const categorySchema = + categories.length === 1 + ? z.literal(categories[0]!) + : z.union( + categories.map((category) => z.literal(category)) as [ + z.ZodLiteral, + z.ZodLiteral, + ...z.ZodLiteral[], + ] + ); + + return z.object({ + category: categorySchema, + }) as unknown as StandardSchemaV1>; } diff --git a/src/decide.ts b/src/decide.ts index b4dbfb4..62cec7d 100644 --- a/src/decide.ts +++ b/src/decide.ts @@ -1,83 +1,87 @@ +import { z } from 'zod'; +import { validateSchemaSync } from './utils.js'; import type { AgentAdapter, + DecideOptions, DecideResultFor, - InvokeEnqueue, StandardSchemaV1, - StateConfig, - TransitionResult, } from './types.js'; -type TransitionTargetOf = T extends { target?: infer TTarget } - ? Extract - : never; +export async function decide< + const TOptions extends Record, +>( + options: DecideOptions +): Promise> { + const adapter = requireAdapter(options.adapter, 'decide()'); + const result = await adapter.decide({ + model: options.model, + prompt: options.prompt, + options: options.options, + reasoning: options.reasoning, + }); + + const chosen = options.options[result.choice]; + if (!chosen) { + throw new Error( + `Adapter returned unknown decision '${result.choice}' for model '${options.model}'` + ); + } + + const data = chosen.schema + ? validateSchemaSync(chosen.schema, result.data) + : {}; -type HandlerTargetOf = T extends (...args: any[]) => infer TResult - ? TransitionTargetOf - : TransitionTargetOf; + return { + choice: result.choice, + data, + reasoning: result.reasoning, + } as DecideResultFor; +} -type OnTargets = TOn extends Record - ? HandlerTargetOf - : never; +export function requireAdapter( + adapter: AgentAdapter | undefined, + label: string +): AgentAdapter { + if (!adapter) { + throw new Error(`No adapter configured for ${label}`); + } -type DecideStateConfig< - TContext extends Record, - TTarget extends string, - TParamsByTarget extends Record, -> = Pick< - StateConfig, - 'on' -> & { - __type: 'decide'; - __decideConfig: Record; -}; + return adapter; +} -/** - * Create a decision state where an LLM picks from constrained options. - * Each option has a description and optional schema for structured data. - * - * The result type is a discriminated union — `result.choice` narrows `result.data`. - * - */ -export function decide< - TContext extends Record, - const TOptions extends Record< - string, - { description: string; schema?: StandardSchemaV1 } - >, - TParams extends Record = Record, - TTarget extends string = string, - TParamsByTarget extends Record = {}, +export function decideResultSchema< + const TOptions extends Record, >( - config: { - model: string; - adapter?: AgentAdapter; - prompt: string | ((args: { context: TContext; params: TParams }) => string); - options: TOptions; - reasoning?: boolean; - onDone: (args: { - result: DecideResultFor; - context: TContext; - }) => TransitionResult; - on?: Record< - string, - ( - args: { event: any; context: TContext }, - enq: InvokeEnqueue - ) => TransitionResult - >; + options: TOptions, + config: { reasoning?: boolean } = {} +): StandardSchemaV1> { + const schemas = Object.entries(options).map(([choice, option]) => + z.object({ + choice: z.literal(choice), + data: option.schema ? toZodSchema(option.schema) : z.object({}), + ...(config.reasoning ? { reasoning: z.string().optional() } : {}), + }) + ); + + if (schemas.length === 0) { + throw new Error('decideResultSchema requires at least one option'); } -): DecideStateConfig< - TContext, - TTarget, - TParamsByTarget -> { - return { - __type: 'decide', - __decideConfig: config as unknown as Record, - on: config.on as StateConfig< - TContext, - TTarget, - TParamsByTarget - >['on'], - }; + + return (schemas.length === 1 + ? schemas[0]! + : z.union( + schemas as unknown as [ + z.ZodTypeAny, + z.ZodTypeAny, + ...z.ZodTypeAny[], + ] + )) as unknown as StandardSchemaV1>; +} + +function toZodSchema(schema: StandardSchemaV1): z.ZodTypeAny { + if ('_zod' in schema || '_def' in schema) { + return schema as unknown as z.ZodTypeAny; + } + + return z.record(z.string(), z.unknown()); } diff --git a/src/examples.test.ts b/src/examples.test.ts index 55829a1..532dc14 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -15,14 +15,17 @@ import { createJokeExample, createJugsExample, createMapReduceExample, + createMultiAgentNetworkExample, createNewspaperExample, createPlanAndExecuteExample, createRaffleExample, createReactAgentExample, + createRewooExample, createReflectionExample, createRiverCrossingExample, createSimpleExample, createSubflowExample, + createSupervisorExample, createToolCallingExample, createTutorExample, } from '../examples/index.js'; @@ -42,13 +45,16 @@ describe('curated examples', () => { expect(existsSync(resolve(examplesDir, 'joke.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'jugs.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'map-reduce.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'multi-agent-network.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'newspaper.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'plan-and-execute.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'raffle.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'react-agent.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'rewoo.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'reflection.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'river-crossing.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'subflow.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'supervisor.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'tool-calling.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'tutor.ts'))).toBe(true); }); @@ -475,6 +481,64 @@ describe('curated examples', () => { } }); + test('multi-agent network example coordinates specialist handoffs through a supervisor state', async () => { + let step = 0; + + const machine = createMultiAgentNetworkExample({ + adapter: { + decide: async () => { + step += 1; + + if (step === 1) { + return { + choice: 'research', + data: { focus: 'collect technical notes' }, + }; + } + + if (step === 2) { + return { + choice: 'write', + data: { angle: 'produce a short memo' }, + }; + } + + return { + choice: 'finalize', + data: {}, + }; + }, + }, + research: async ({ topic, focus }) => ({ + notes: [`${topic}:${focus}:a`, `${topic}:${focus}:b`], + }), + write: async ({ topic, notes, angle }) => ({ + draft: `${topic} | ${angle} | ${notes.join(' / ')}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ topic: 'durable agents' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + topic: 'durable agents', + notes: [ + 'durable agents:collect technical notes:a', + 'durable agents:collect technical notes:b', + ], + draft: + 'durable agents | produce a short memo | durable agents:collect technical notes:a / durable agents:collect technical notes:b', + handoffs: [ + 'researcher:collect technical notes', + 'writer:produce a short memo', + ], + }); + } + }); + test('tool-calling example emits live tool activity and completes with output', async () => { const machine = createToolCallingExample(async (city) => ({ forecast: `Rainy in ${city}`, @@ -488,15 +552,15 @@ describe('curated examples', () => { const events: string[] = []; run.on('toolCall', (event) => { - events.push(`call:${(event as { toolName: string }).toolName}`); + events.push(`call:${event.toolName}`); }); run.on('toolResult', (event) => { - events.push(`result:${(event as { toolName: string }).toolName}`); + events.push(`result:${event.toolName}`); }); await new Promise((resolve, reject) => { - run.on('done', () => resolve()); - run.on('error', (event) => reject((event as { error: unknown }).error)); + run.onDone(() => resolve()); + run.onError((event) => reject(event.error)); }); expect(events).toEqual(['call:getWeather', 'result:getWeather']); @@ -545,15 +609,15 @@ describe('curated examples', () => { const events: string[] = []; run.on('toolCall', (event) => { - events.push(`call:${(event as { toolName: string }).toolName}`); + events.push(`call:${event.toolName}`); }); run.on('toolResult', (event) => { - events.push(`result:${(event as { toolName: string }).toolName}`); + events.push(`result:${event.toolName}`); }); await new Promise((resolve, reject) => { - run.on('done', () => resolve()); - run.on('error', (event) => reject((event as { error: unknown }).error)); + run.onDone(() => resolve()); + run.onError((event) => reject(event.error)); }); expect(events).toEqual(['call:search', 'result:search']); @@ -566,6 +630,111 @@ describe('curated examples', () => { ); }); + test('rewoo example plans named steps, executes them with references, and solves the objective', async () => { + const machine = createRewooExample({ + plan: async () => ({ + steps: [ + { + id: 'E1', + instruction: 'Collect a fact', + input: 'LangGraphJS', + }, + { + id: 'E2', + instruction: 'Summarize the fact', + input: 'Use #E1 in one concise sentence', + }, + ], + }), + executeStep: async ({ step, resolvedInput }) => ({ + result: `${step.id}:${resolvedInput}`, + }), + solve: async ({ resultsById }) => ({ + answer: `${resultsById.E1} | ${resultsById.E2}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ objective: 'understand the repo' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + objective: 'understand the repo', + steps: [ + { + id: 'E1', + instruction: 'Collect a fact', + input: 'LangGraphJS', + }, + { + id: 'E2', + instruction: 'Summarize the fact', + input: 'Use #E1 in one concise sentence', + }, + ], + resultsById: { + E1: 'E1:LangGraphJS', + E2: 'E2:Use E1:LangGraphJS in one concise sentence', + }, + answer: 'E1:LangGraphJS | E2:Use E1:LangGraphJS in one concise sentence', + }); + } + }); + + test('supervisor example retries a blocked worker and can still resolve the request', async () => { + let decisions = 0; + + const machine = createSupervisorExample({ + adapter: { + decide: async () => { + decisions += 1; + + return { + choice: decisions === 1 ? 'retry' : 'escalate', + data: + decisions === 1 + ? { instruction: 'Retry using the customer email on file.' } + : { reason: 'Escalate to billing.' }, + }; + }, + }, + handle: async ({ attempt, instruction }) => + attempt === 1 + ? { + status: 'blocked' as const, + issue: 'Missing account identifier.', + } + : { + status: 'resolved' as const, + response: `Resolved after retry: ${instruction}`, + }, + }); + + const result = await machine.execute( + machine.getInitialState({ + request: 'Fix the duplicate subscription charge.', + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + request: 'Fix the duplicate subscription charge.', + status: 'resolved', + resolution: 'Resolved after retry: Retry using the customer email on file.', + escalationReason: null, + attemptCount: 2, + history: [ + 'worker:1:blocked:Missing account identifier.', + 'supervisor:retry:Retry using the customer email on file.', + 'worker:2:resolved:Resolved after retry: Retry using the customer email on file.', + ], + }); + } + }); + test('newspaper example loops through critique and revision', async () => { const machine = createNewspaperExample({ search: async () => ({ searchResults: ['a', 'b', 'c'] }), diff --git a/src/graph/index.ts b/src/graph/index.ts index 5e5554e..7d09dfd 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -2,7 +2,7 @@ import type { AgentMachine } from '../types.js'; export interface GraphNode { id: string; - type: 'state' | 'decide' | 'classify' | 'final'; + type: 'state' | 'choice' | 'final'; } export interface GraphEdge { diff --git a/src/index.ts b/src/index.ts index 6692285..e56ee3d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ // Core export { createAgentMachine } from './machine.js'; +export { decide, decideResultSchema, requireAdapter } from './decide.js'; +export { classify, classifyResultSchema } from './classify.js'; // AI primitives -export { decide } from './decide.js'; -export { classify } from './classify.js'; export { createReactAgent } from './prebuilt/react.js'; export type { ReactAgentMessage, @@ -23,8 +23,9 @@ export type { AgentRun, AgentSnapshot, AgentState, - ClassifyConfig, - DecideConfig, + ClassifyOptions, + ClassifyResultFor, + DecideOptions, DecideResultFor, EmittedPart, EmittedUnion, diff --git a/src/invoke-events.test.ts b/src/invoke-events.test.ts index a886ada..0fe92df 100644 --- a/src/invoke-events.test.ts +++ b/src/invoke-events.test.ts @@ -7,13 +7,12 @@ import { } from './index.js'; function once( - run: { on(type: string, handler: (event: unknown) => void): () => void }, - type: string + subscribe: (handler: (event: T) => void) => () => void ) { return new Promise((resolve) => { - const off = run.on(type, (event) => { + const off = subscribe((event) => { off(); - resolve(event as T); + resolve(event); }); }); } @@ -41,7 +40,7 @@ test('invoke success is journaled as an internal machine event', async () => { const store = createMemoryRunStore(); const run = await startSession(machine, { store }); - await once(run, 'done'); + await once(run.onDone.bind(run)); const journal = await store.loadEvents(run.sessionId); expect(run.getSnapshot()).toEqual( @@ -78,7 +77,7 @@ test('invoke failure is journaled as an internal machine event', async () => { const store = createMemoryRunStore(); const run = await startSession(machine, { store }); - await once(run, 'error'); + await once(run.onError.bind(run)); const journal = await store.loadEvents(run.sessionId); expect(run.getSnapshot()).toEqual( @@ -114,7 +113,7 @@ test('invalid invoke results fail without journaling a done event', async () => const store = createMemoryRunStore(); const run = await startSession(machine, { store }); - await once(run, 'error'); + await once(run.onError.bind(run)); const journal = await store.loadEvents(run.sessionId); expect(journal.map((event) => event.type)).toEqual([ diff --git a/src/langgraph-equivalents/multi-agent-network.test.ts b/src/langgraph-equivalents/multi-agent-network.test.ts new file mode 100644 index 0000000..5b1c50c --- /dev/null +++ b/src/langgraph-equivalents/multi-agent-network.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from 'vitest'; +import { createMultiAgentNetworkExample } from '../../examples/multi-agent-network.js'; + +test('multi-agent network coordinates specialist handoffs until a final draft is ready', async () => { + let step = 0; + + const machine = createMultiAgentNetworkExample({ + adapter: { + decide: async () => { + step += 1; + + if (step === 1) { + return { + choice: 'research', + data: { focus: 'collect architecture notes' }, + }; + } + + if (step === 2) { + return { + choice: 'write', + data: { angle: 'turn notes into an executive summary' }, + }; + } + + return { + choice: 'finalize', + data: {}, + }; + }, + }, + research: async ({ topic, focus }) => ({ + notes: [`${topic}:${focus}:1`, `${topic}:${focus}:2`], + }), + write: async ({ topic, notes, angle }) => ({ + draft: `${topic} | ${angle} | ${notes.join(' / ')}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ topic: 'agent runtimes' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + topic: 'agent runtimes', + notes: [ + 'agent runtimes:collect architecture notes:1', + 'agent runtimes:collect architecture notes:2', + ], + draft: + 'agent runtimes | turn notes into an executive summary | agent runtimes:collect architecture notes:1 / agent runtimes:collect architecture notes:2', + handoffs: [ + 'researcher:collect architecture notes', + 'writer:turn notes into an executive summary', + ], + }); + } +}); diff --git a/src/langgraph-equivalents/prebuilt-react.test.ts b/src/langgraph-equivalents/prebuilt-react.test.ts index ec4cfb0..d08435a 100644 --- a/src/langgraph-equivalents/prebuilt-react.test.ts +++ b/src/langgraph-equivalents/prebuilt-react.test.ts @@ -6,14 +6,13 @@ import { } from '../index.js'; function once( - run: { on(type: string, handler: (event: unknown) => void): () => void }, - type: string + subscribe: (handler: (event: T) => void) => () => void ) { return new Promise((resolve) => { let off = () => {}; - off = run.on(type, (event) => { + off = subscribe((event) => { off(); - resolve(event as T); + resolve(event); }); }); } @@ -60,13 +59,13 @@ test('prebuilt react agent loops through a tool call and returns a final answer' const toolEvents: string[] = []; run.on('toolCall', (event) => { - toolEvents.push(`call:${(event as { toolName: string }).toolName}`); + toolEvents.push(`call:${event.toolName}`); }); run.on('toolResult', (event) => { - toolEvents.push(`result:${(event as { toolName: string }).toolName}`); + toolEvents.push(`result:${event.toolName}`); }); - await once(run, 'done'); + await once(run.onDone.bind(run)); expect(toolEvents).toEqual(['call:search', 'result:search']); expect(run.getSnapshot()).toEqual( diff --git a/src/langgraph-equivalents/rewoo.test.ts b/src/langgraph-equivalents/rewoo.test.ts new file mode 100644 index 0000000..3ee9509 --- /dev/null +++ b/src/langgraph-equivalents/rewoo.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from 'vitest'; +import { createRewooExample } from '../../examples/rewoo.js'; + +test('rewoo workflow plans named steps, resolves references, and synthesizes a final answer', async () => { + const machine = createRewooExample({ + plan: async () => ({ + steps: [ + { + id: 'E1', + instruction: 'Find the framework', + input: 'LangGraphJS runtime', + }, + { + id: 'E2', + instruction: 'Summarize the finding', + input: 'Use #E1 to produce a concise takeaway', + }, + ], + }), + executeStep: async ({ step, resolvedInput }) => ({ + result: `${step.id}:${resolvedInput}`, + }), + solve: async ({ resultsById }) => ({ + answer: `${resultsById.E1} | ${resultsById.E2}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ objective: 'understand the runtime' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + objective: 'understand the runtime', + steps: [ + { + id: 'E1', + instruction: 'Find the framework', + input: 'LangGraphJS runtime', + }, + { + id: 'E2', + instruction: 'Summarize the finding', + input: 'Use #E1 to produce a concise takeaway', + }, + ], + resultsById: { + E1: 'E1:LangGraphJS runtime', + E2: 'E2:Use E1:LangGraphJS runtime to produce a concise takeaway', + }, + answer: + 'E1:LangGraphJS runtime | E2:Use E1:LangGraphJS runtime to produce a concise takeaway', + }); + } +}); diff --git a/src/langgraph-equivalents/streaming.test.ts b/src/langgraph-equivalents/streaming.test.ts index 779b3ad..348488a 100644 --- a/src/langgraph-equivalents/streaming.test.ts +++ b/src/langgraph-equivalents/streaming.test.ts @@ -7,14 +7,13 @@ import { } from '../index.js'; function once( - run: { on(type: string, handler: (event: unknown) => void): () => void }, - type: string + subscribe: (handler: (event: T) => void) => () => void ) { return new Promise((resolve) => { let off = () => {}; - off = run.on(type, (event) => { + off = subscribe((event) => { off(); - resolve(event as T); + resolve(event); }); }); } @@ -55,10 +54,10 @@ test('streams live invoke output while preserving durable state history', async const liveParts: string[] = []; run.on('textPart', (part) => { - liveParts.push((part as { delta: string }).delta); + liveParts.push(part.delta); }); - await once(run, 'done'); + await once(run.onDone.bind(run)); expect(liveParts).toEqual(['hello', ' world']); expect(run.getSnapshot()).toEqual( diff --git a/src/langgraph-equivalents/supervisor.test.ts b/src/langgraph-equivalents/supervisor.test.ts new file mode 100644 index 0000000..e2d71f9 --- /dev/null +++ b/src/langgraph-equivalents/supervisor.test.ts @@ -0,0 +1,62 @@ +import { expect, test } from 'vitest'; +import { createSupervisorExample } from '../../examples/supervisor.js'; + +test('supervisor workflow retries a blocked worker and escalates when repeated attempts fail', async () => { + let decisions = 0; + + const machine = createSupervisorExample({ + adapter: { + decide: async () => { + decisions += 1; + + if (decisions === 1) { + return { + choice: 'retry', + data: { + instruction: 'Retry using the customer email already on file.', + }, + }; + } + + return { + choice: 'escalate', + data: { + reason: 'Escalate to billing because the request still lacks a verified account match.', + }, + }; + }, + }, + handle: async ({ attempt, instruction }) => ({ + status: 'blocked', + issue: + attempt === 1 + ? 'Missing account identifier.' + : `Still blocked after retry: ${instruction}`, + }), + maxAttempts: 2, + }); + + const result = await machine.execute( + machine.getInitialState({ + request: 'Refund the duplicate annual subscription charge.', + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + request: 'Refund the duplicate annual subscription charge.', + status: 'escalated', + resolution: null, + escalationReason: + 'Escalate to billing because the request still lacks a verified account match.', + attemptCount: 2, + history: [ + 'worker:1:blocked:Missing account identifier.', + 'supervisor:retry:Retry using the customer email already on file.', + 'worker:2:blocked:Still blocked after retry: Retry using the customer email already on file.', + 'supervisor:escalate:Escalate to billing because the request still lacks a verified account match.', + ], + }); + } +}); diff --git a/src/langgraph-equivalents/tool-calling.test.ts b/src/langgraph-equivalents/tool-calling.test.ts index 0d290b7..2a54431 100644 --- a/src/langgraph-equivalents/tool-calling.test.ts +++ b/src/langgraph-equivalents/tool-calling.test.ts @@ -7,14 +7,13 @@ import { } from '../index.js'; function once( - run: { on(type: string, handler: (event: unknown) => void): () => void }, - type: string + subscribe: (handler: (event: T) => void) => () => void ) { return new Promise((resolve) => { let off = () => {}; - off = run.on(type, (event) => { + off = subscribe((event) => { off(); - resolve(event as T); + resolve(event); }); }); } @@ -78,13 +77,13 @@ test('supports tool-call style invokes with live tool events and final output', const events: string[] = []; run.on('toolCall', (event) => { - events.push(`call:${(event as { toolName: string }).toolName}`); + events.push(`call:${event.toolName}`); }); run.on('toolResult', (event) => { - events.push(`result:${(event as { toolName: string }).toolName}`); + events.push(`result:${event.toolName}`); }); - await once(run, 'done'); + await once(run.onDone.bind(run)); expect(events).toEqual(['call:getWeather', 'result:getWeather']); expect(run.getSnapshot()).toEqual( diff --git a/src/machine.ts b/src/machine.ts index 322699a..fa2942b 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -17,7 +17,7 @@ import { findEventSchema, formatSchemaIssues, getAvailableEvents, - getParams, + getInput, isDoneInvokeEventType, isErrorInvokeEventType, resolveInitial, @@ -28,73 +28,61 @@ import { import type { StateConfigAny } from './utils.js'; // ─── Type helpers ─── - -type FallbackAny = unknown extends T ? any : T; - -/** Choice result shape — always the same for type: 'choice' */ -type ChoiceResult = { choice: string; data: Record; reasoning?: string }; - -/** Result type for onDone: typed from resultSchema when present */ -type OnDoneResult = unknown extends TResult ? ChoiceResult : NoInfer; +/** Result type for onDone: typed from invoke return or resultSchema when present */ +type OnDoneResult = NoInfer; type EventFor = E extends keyof TEvents & string ? { type: E } & EventPayload> : { type: E & string; [k: string]: unknown }; type StateNodeDef< + TState, TContext extends Record, - TParams, + TInput, TResult, TEvents, TTarget extends string, - TParamsMap extends Record, -> = - | { + TInputMap extends Record, + TOutput, +> = { type?: 'final' | 'choice'; - paramsSchema?: StandardSchemaV1; + inputSchema?: StandardSchemaV1; resultSchema?: StandardSchemaV1; invoke?: (args: { context: TContext; - params: NoInfer; + input: NoInfer; signal?: AbortSignal; - }, enq: { emit(part: EmittedPart): void }) => Promise>; - onDone?: (args: { result: OnDoneResult; context: TContext }) => TransitionResult; - on?: { [E in keyof TEvents & string]?: TransitionResult | ((args: { + }, enq: { emit(part: EmittedPart): void }) => Promise; + onDone?: (args: { result: OnDoneResult; context: TContext }) => TransitionResult; + on?: { [E in keyof TEvents & string]?: TransitionResult | ((args: { event: EventFor; context: TContext; - }, enq: { emit(part: EmittedPart): void }) => TransitionResult) }; + }, enq: { emit(part: EmittedPart): void }) => TransitionResult) }; events?: Record; - output?: (args: { context: TContext }) => unknown; - // choice-specific + output?: (args: { context: TContext }) => NoInfer; model?: string; adapter?: import('./types.js').AgentAdapter; - prompt?: string | ((args: { context: TContext; params: NoInfer }) => string); + prompt?: string | ((args: { context: TContext; input: NoInfer }) => string); options?: Record; reasoning?: boolean; - // internal - __type?: 'decide' | 'classify'; - __decideConfig?: Record; -} - | { - on?: StateConfigAny['on']; - __type: 'decide' | 'classify'; - __decideConfig: Record; - __classifyConfig?: Record; - }; +}; type StatesMap< TContext extends Record, - TParamsMap extends Record, + TInputMap extends Record, TResultMap extends Record, + TOutput, TEvents, > = { - [K in keyof TParamsMap & keyof TResultMap]: StateNodeDef< + [K in keyof TInputMap & keyof TResultMap]: StateNodeDef< + unknown, TContext, - TParamsMap[K], + TInputMap[K], TResultMap[K], TEvents, - keyof TParamsMap & keyof TResultMap & string, - TParamsMap + keyof TInputMap & keyof TResultMap & string, + TInputMap, + TOutput >; }; @@ -103,82 +91,91 @@ export function createAgentMachine< TInput, TContext extends Record, const TEvents extends Record, - const TParamsMap extends Record, + const TInputMap extends Record, TResultMap extends Record, + const TEmitted extends Record, + TOutput = unknown, >(config: { id: string; schemas: { context: StandardSchemaV1; input?: StandardSchemaV1; events?: TEvents; - emitted?: Record; + emitted?: TEmitted; + output?: StandardSchemaV1; }; context: (input: NoInfer) => NoInfer; adapter?: import('./types.js').AgentAdapter; initial: - | (keyof TParamsMap & keyof TResultMap & string) + | (keyof TInputMap & keyof TResultMap & string) | ((args: { context: NoInfer }) => { - target: keyof TParamsMap & keyof TResultMap & string; - params?: Record; + target: keyof TInputMap & keyof TResultMap & string; + input?: Record; }); - states: StatesMap, TParamsMap, TResultMap, TEvents>; -}): AgentMachine>; + states: StatesMap, TInputMap, TResultMap, TOutput, TEvents>; +}): AgentMachine, TOutput, TEmitted>; // ─── Overload B: no schemas.context ─── export function createAgentMachine< TInput, TContext extends Record, const TEvents extends Record, - const TParamsMap extends Record, + const TInputMap extends Record, TResultMap extends Record, + const TEmitted extends Record, + TOutput = unknown, >(config: { id: string; schemas: { input: StandardSchemaV1; context?: never; events?: TEvents; - emitted?: Record; + emitted?: TEmitted; + output?: StandardSchemaV1; }; context: (input: NoInfer) => TContext; adapter?: import('./types.js').AgentAdapter; initial: - | (keyof TParamsMap & keyof TResultMap & string) + | (keyof TInputMap & keyof TResultMap & string) | ((args: { context: TContext }) => { - target: keyof TParamsMap & keyof TResultMap & string; - params?: Record; + target: keyof TInputMap & keyof TResultMap & string; + input?: Record; }); - states: StatesMap; -}): AgentMachine>; + states: StatesMap; +}): AgentMachine, TOutput, TEmitted>; // ─── Overload C: no schemas.input or schemas.context ─── export function createAgentMachine< TContext extends Record, const TEvents extends Record, - const TParamsMap extends Record, + const TInputMap extends Record, TResultMap extends Record, + const TEmitted extends Record, + TOutput = unknown, >(config: { id: string; schemas?: { input?: never; context?: never; events?: TEvents; - emitted?: Record; + emitted?: TEmitted; + output?: StandardSchemaV1; }; context: (...args: any[]) => TContext; adapter?: import('./types.js').AgentAdapter; initial: - | (keyof TParamsMap & keyof TResultMap & string) + | (keyof TInputMap & keyof TResultMap & string) | ((args: { context: TContext }) => { - target: keyof TParamsMap & keyof TResultMap & string; - params?: Record; + target: keyof TInputMap & keyof TResultMap & string; + input?: Record; }); - states: StatesMap; -}): AgentMachine>; + states: StatesMap; +}): AgentMachine, TOutput, TEmitted>; // ─── Implementation ─── export function createAgentMachine( - machineConfig: MachineConfig + machineConfig: MachineConfig ): AgentMachine { const cfg = machineConfig as MachineConfig; @@ -224,7 +221,7 @@ export function createAgentMachine( } const context = cfg.context(validatedInput); - const init = resolveInitial(cfg.initial, { context, params: {} }); + const init = resolveInitial(cfg.initial, { context, input: {} }); if (!init.target) { throw new Error('Initial transition must specify a target state'); @@ -234,14 +231,14 @@ export function createAgentMachine( value: init.target, context: init.context ? { ...context, ...init.context } : context, status: 'active', - params: init.params ? { [init.target]: init.params } : {}, + input: init.input ? { [init.target]: init.input } : {}, }; } function resolveState(raw: { value: string; context: Record; - params?: Record>; + input?: Record>; sessionId?: string; createdAt?: number; status?: AgentState['status']; @@ -252,7 +249,7 @@ export function createAgentMachine( value: raw.value, context: raw.context, status: raw.status ?? 'active', - params: raw.params ?? {}, + input: raw.input ?? {}, sessionId: raw.sessionId, createdAt: raw.createdAt, output: raw.output, @@ -278,8 +275,6 @@ export function createAgentMachine( onEmit?.(part); }); const sc = resolveStateConfig(cfg, state.value); - const effectiveConfig = getEffectiveStateConfig(state.value); - function applyResult( result: TransitionResult, status = state.status @@ -319,12 +314,12 @@ export function createAgentMachine( if (isDoneInvokeEventType(state.value, event.type)) { const result = 'output' in event ? event.output : undefined; - const validatedResult = effectiveConfig.resultSchema - ? validateSchemaSync(effectiveConfig.resultSchema, result) + const validatedResult = sc.resultSchema + ? validateSchemaSync(sc.resultSchema, result) : result; - if (effectiveConfig.onDone) { - const trans = effectiveConfig.onDone({ + if (sc.onDone) { + const trans = sc.onDone({ result: validatedResult, context: state.context, }); @@ -371,23 +366,16 @@ export function createAgentMachine( ); } - function getEffectiveStateConfig(value: string): StateConfigAny { - const sc = resolveStateConfig(cfg, value); - return sc.__decideConfig - ? { ...sc, ...(sc.__decideConfig as Record) } - : sc; - } - function validateReplayableResult( value: string, result: unknown ): unknown { - const effectiveConfig = getEffectiveStateConfig(value); - if (!effectiveConfig.resultSchema) { + const sc = resolveStateConfig(cfg, value); + if (!sc.resultSchema) { return result; } - return validateSchemaSync(effectiveConfig.resultSchema, result); + return validateSchemaSync(sc.resultSchema, result); } function validateEventPayload( @@ -451,8 +439,8 @@ export function createAgentMachine( } async function createChoiceEvent(state: AgentState): Promise { - const dc = getEffectiveStateConfig(state.value); - const adapter = (dc as StateConfigAny).adapter ?? cfg.adapter; + const sc = resolveStateConfig(cfg, state.value); + const adapter = sc.adapter ?? cfg.adapter; if (!adapter) { return { type: `xstate.error.invoke.${state.value}`, @@ -461,18 +449,18 @@ export function createAgentMachine( }; } - const params = getParams(state.value, state.params); + const input = getInput(state.value, state.input); const prompt = - typeof dc.prompt === 'function' - ? dc.prompt({ context: state.context, params }) - : dc.prompt; + typeof sc.prompt === 'function' + ? sc.prompt({ context: state.context, input }) + : sc.prompt; try { const result = await adapter.decide({ - model: (dc as StateConfigAny).model!, + model: sc.model!, prompt: prompt as string, - options: (dc as StateConfigAny).options!, - reasoning: (dc as StateConfigAny).reasoning, + options: sc.options!, + reasoning: sc.reasoning, }); const validatedResult = validateReplayableResult(state.value, result); @@ -499,7 +487,7 @@ export function createAgentMachine( const result = await sc.invoke!( { context: state.context, - params: getParams(state.value, state.params), + input: getInput(state.value, state.input), }, createEnqueue(onEmit) ); @@ -528,7 +516,7 @@ export function createAgentMachine( } const sc = resolveStateConfig(cfg, state.value); - if (sc.type === 'choice' || sc.__decideConfig) { + if (sc.type === 'choice') { return createChoiceEvent(state); } @@ -571,9 +559,12 @@ export function createAgentMachine( const sc = resolveStateConfig(cfg, state.value); if (sc.type === 'final') { - const output = sc.output + const rawOutput = sc.output ? sc.output({ context: state.context }) : undefined; + const output = cfg.schemas?.output + ? validateSchemaSync(cfg.schemas.output, rawOutput) + : rawOutput; return { ...state, status: 'done', output }; } @@ -654,7 +645,7 @@ export function createAgentMachine( status: s.status, sessionId: runtime.sessionId, createdAt: runtime.createdAt, - params: s.params, + input: s.input, output: s.output, error: s.error, }; diff --git a/src/persistence.test.ts b/src/persistence.test.ts index 406ef40..ae505e7 100644 --- a/src/persistence.test.ts +++ b/src/persistence.test.ts @@ -51,7 +51,7 @@ test('loads the most replay-advanced saved snapshot', async () => { status: 'active', createdAt: 100, sessionId: 'session-1', - params: { + input: { idle: { count: 1 }, }, }, @@ -67,7 +67,7 @@ test('loads the most replay-advanced saved snapshot', async () => { status: 'done', createdAt: 300, sessionId: 'session-1', - params: { + input: { done: { count: 2 }, }, output: { count: 2 }, @@ -84,7 +84,7 @@ test('loads the most replay-advanced saved snapshot', async () => { status: 'done', createdAt: 300, sessionId: 'session-1', - params: { + input: { done: { count: 2 }, }, output: { count: 2 }, @@ -105,7 +105,7 @@ test('loads the most replay-advanced snapshot even if saved earlier', async () = status: 'done', createdAt: 500, sessionId: 'session-1', - params: { done: { count: 5 } }, + input: { done: { count: 5 } }, }, createdAt: 500, }); @@ -119,7 +119,7 @@ test('loads the most replay-advanced snapshot even if saved earlier', async () = status: 'active', createdAt: 200, sessionId: 'session-1', - params: { review: { count: 2 } }, + input: { review: { count: 2 } }, }, createdAt: 200, }); @@ -133,7 +133,7 @@ test('loads the most replay-advanced snapshot even if saved earlier', async () = status: 'done', createdAt: 500, sessionId: 'session-1', - params: { done: { count: 5 } }, + input: { done: { count: 5 } }, }, createdAt: 500, }); diff --git a/src/prebuilt/react.ts b/src/prebuilt/react.ts index e4292d6..5e64924 100644 --- a/src/prebuilt/react.ts +++ b/src/prebuilt/react.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { createAgentMachine } from '../machine.js'; -import type { AgentMachine, StandardSchemaV1 } from '../types.js'; +import type { StandardSchemaV1 } from '../types.js'; const messageSchema = z.object({ role: z.enum(['system', 'user', 'assistant', 'tool']), @@ -48,16 +48,7 @@ export function createReactAgent(options: { schema?: StandardSchemaV1; }>; }) => Promise; -}): AgentMachine< - { messages?: ReactAgentMessage[] }, - { - messages: ReactAgentMessage[]; - stepCount: number; - pendingToolCall: - | { toolName: string; input: Record } - | null; - } -> { +}) { const tools = options.tools ?? []; const maxSteps = options.maxSteps ?? 8; const toolDefinitions = tools.map(({ name, description, schema }) => ({ @@ -131,7 +122,10 @@ export function createReactAgent(options: { stepCount: context.stepCount + 1, messages: [ ...context.messages, - { role: 'assistant', content: result.message }, + { + role: 'assistant', + content: result.message, + } satisfies ReactAgentMessage, ], }, }; @@ -152,10 +146,10 @@ export function createReactAgent(options: { content: result.message ?? `Calling tool ${result.toolName} with ${JSON.stringify(result.input)}`, - }, + } satisfies ReactAgentMessage, ], }, - params: { + input: { toolName: result.toolName, input: result.input, }, @@ -163,7 +157,7 @@ export function createReactAgent(options: { }, }, tool: { - paramsSchema: z.object({ + inputSchema: z.object({ toolName: z.string(), input: z.record(z.string(), z.unknown()), }), @@ -171,29 +165,29 @@ export function createReactAgent(options: { toolName: z.string(), output: z.unknown(), }), - invoke: async ({ params }, enq) => { - const tool = toolsByName.get(params.toolName); + invoke: async ({ input }, enq) => { + const tool = toolsByName.get(input.toolName); if (!tool) { - throw new Error(`Tool '${params.toolName}' not found`); + throw new Error(`Tool '${input.toolName}' not found`); } enq.emit({ type: 'toolCall', - toolName: params.toolName, - input: params.input, + toolName: input.toolName, + input: input.input, }); - const output = await tool.execute(params.input); + const output = await tool.execute(input.input); enq.emit({ type: 'toolResult', - toolName: params.toolName, + toolName: input.toolName, output, }); return { - toolName: params.toolName, + toolName: input.toolName, output, }; }, @@ -207,7 +201,7 @@ export function createReactAgent(options: { role: 'tool', name: result.toolName, content: serializeToolOutput(result.output), - }, + } satisfies ReactAgentMessage, ], }, }), diff --git a/src/runtime/session.ts b/src/runtime/session.ts index e916f1d..bb208f5 100644 --- a/src/runtime/session.ts +++ b/src/runtime/session.ts @@ -11,6 +11,15 @@ import type { } from '../types.js'; import { isReservedInternalEventType } from '../utils.js'; +const RESERVED_PUBLIC_ON_TYPES = new Set([ + 'part', + 'done', + 'error', + 'state', + 'machine.event', + 'runtime', +]); + type SnapshotRuntime = { sessionId: string; createdAt: number; @@ -74,12 +83,12 @@ function toJournalEvent( } function createRun( - machine: AgentMachine, + machine: AgentMachine, store: SessionOptions['store'], runtimeMachine: RuntimeMachine, runState: RunState, emitter = createRunEmitter() -): AgentRun { +): AgentRun { let releaseStart!: () => void; let operation = new Promise((resolve) => { releaseStart = resolve; @@ -252,7 +261,33 @@ function createRun( }, on(type, handler) { - return emitter.on(type, handler); + if (RESERVED_PUBLIC_ON_TYPES.has(type)) { + throw new Error( + `'${type}' is not an emitted event subscription. Use a dedicated run method instead.` + ); + } + + return emitter.on(type, handler as (event: unknown) => void); + }, + + onEmitted(handler) { + return emitter.on('part', handler as (event: unknown) => void); + }, + + onDone(handler) { + return emitter.on('done', handler as (event: unknown) => void); + }, + + onError(handler) { + return emitter.on('error', handler as (event: unknown) => void); + }, + + onSnapshot(handler) { + return emitter.on('state', handler as (event: unknown) => void); + }, + + onMachineEvent(handler) { + return emitter.on('machine.event', handler as (event: unknown) => void); }, /** @internal */ @@ -271,15 +306,24 @@ function createRun( __scheduleStart() { scheduleStart(); }, - } as AgentRun; + } as AgentRun; } -export async function startSession( - machine: AgentMachine, - options: SessionOptions -): Promise { +export async function startSession< + TInput, + TContext extends Record, + TEvents extends Record, + TStates extends Record, + TOutput, + TEmitted extends Record, +>( + machine: AgentMachine, + options: SessionOptions +): Promise> { const runtimeMachine = asRuntimeMachine(machine); - const initialState = machine.getInitialState(options.input); + const initialState = (machine as AgentMachine).getInitialState( + options.input as TInput + ) as AgentState; const runtime = { sessionId: options.sessionId ?? createSessionId(), createdAt: Date.now(), @@ -296,7 +340,7 @@ export async function startSession( options.store, runtimeMachine, runState - ) as AgentRun & { + ) as AgentRun & { __persistCurrent(): Promise; __settle(): Promise; __scheduleStart(): void; @@ -316,10 +360,17 @@ export async function startSession( return run; } -export async function restoreSession( - machine: AgentMachine, +export async function restoreSession< + TInput, + TContext extends Record, + TEvents extends Record, + TStates extends Record, + TOutput, + TEmitted extends Record, +>( + machine: AgentMachine, options: RestoreSessionOptions -): Promise { +): Promise> { const runtimeMachine = asRuntimeMachine(machine); const persisted = await options.store.loadLatestSnapshot(options.sessionId); const allEvents = await options.store.loadEvents(options.sessionId); @@ -336,8 +387,14 @@ export async function restoreSession( createdAt: persisted?.snapshot.createdAt ?? initEvent?.at ?? Date.now(), }; const initialState = persisted - ? machine.resolveState(persisted.snapshot) - : machine.getInitialState(initEvent?.input); + ? (machine.resolveState( + persisted.snapshot as AgentSnapshot + ) as AgentState) + : ((machine as AgentMachine).getInitialState(initEvent?.input) as AgentState< + TContext, + keyof TStates & string, + TOutput + >); const runState: RunState = { current: runtimeMachine.__runtime.withRuntimeMetadata(initialState, runtime), snapshot: @@ -351,7 +408,7 @@ export async function restoreSession( options.store, runtimeMachine, runState - ) as AgentRun & { + ) as AgentRun & { __persistCurrent(): Promise; __settle(): Promise; __scheduleStart(): void; @@ -365,7 +422,10 @@ export async function restoreSession( for (const event of replayTail) { runState.current = runtimeMachine.__runtime.withRuntimeMetadata( - machine.transition(runState.current, event), + machine.transition( + runState.current as AgentState, + event as unknown as import('../types.js').TransitionEvent + ) as AgentState, runState.runtime ); runState.lastSequence = event.sequence; diff --git a/src/session-runtime.test.ts b/src/session-runtime.test.ts index 10df0ff..7b7d292 100644 --- a/src/session-runtime.test.ts +++ b/src/session-runtime.test.ts @@ -49,7 +49,7 @@ test('startSession creates a session, persists xstate.init, and returns before s value: 'idle', status: 'active', context: { count: 0 }, - params: {}, + input: {}, }) ); await vi.waitFor(() => { diff --git a/src/session-types.test.ts b/src/session-types.test.ts index c1cf2ac..ea4b8a7 100644 --- a/src/session-types.test.ts +++ b/src/session-types.test.ts @@ -8,7 +8,7 @@ test('AgentSnapshot includes durable session fields', () => { status: 'active', createdAt: 123, sessionId: 'session-1', - params: {}, + input: {}, }; expect(snapshot.sessionId).toBe('session-1'); diff --git a/src/stream-snapshot.test.ts b/src/stream-snapshot.test.ts index 91157f8..22248d5 100644 --- a/src/stream-snapshot.test.ts +++ b/src/stream-snapshot.test.ts @@ -6,7 +6,7 @@ const machine = createAgentMachine({ context: () => ({}), initial: () => ({ target: 'done', - params: { step: 1 }, + input: { step: 1 }, }), states: { done: { @@ -31,7 +31,7 @@ test('stream emits durable snapshots with stable session metadata', async () => expect(snaps.length).toBeGreaterThanOrEqual(2); expect(new Set(snaps.map((snap) => snap.sessionId)).size).toBe(1); expect(new Set(snaps.map((snap) => snap.createdAt)).size).toBe(1); - expect(snaps[0]!.params).toEqual({ done: { step: 1 } }); + expect(snaps[0]!.input).toEqual({ done: { step: 1 } }); expect(snaps[0]).toEqual( expect.objectContaining({ sessionId: expect.any(String), @@ -39,7 +39,7 @@ test('stream emits durable snapshots with stable session metadata', async () => value: 'done', context: {}, status: 'active', - params: { done: { step: 1 } }, + input: { done: { step: 1 } }, }) ); expect(snaps[snaps.length - 1]).toEqual( @@ -49,7 +49,7 @@ test('stream emits durable snapshots with stable session metadata', async () => value: 'done', context: {}, status: 'done', - params: { done: { step: 1 } }, + input: { done: { step: 1 } }, output: { ok: true }, }) ); @@ -62,10 +62,10 @@ test('snapshot roundtrips through resolveState without losing identity', async ( expect(restored.sessionId).toBe(emitted[0]!.sessionId); expect(restored.createdAt).toBe(emitted[0]!.createdAt); - expect(restored.params).toEqual(emitted[0]!.params); + expect(restored.input).toEqual(emitted[0]!.input); expect(rerun[0]!.sessionId).toBe(emitted[0]!.sessionId); expect(rerun[0]!.createdAt).toBe(emitted[0]!.createdAt); - expect(rerun[0]!.params).toEqual(emitted[0]!.params); + expect(rerun[0]!.input).toEqual(emitted[0]!.input); }); test('fresh machine executions on the same raw state get distinct session ids', async () => { diff --git a/src/streaming.test.ts b/src/streaming.test.ts index ec782f6..3bdf276 100644 --- a/src/streaming.test.ts +++ b/src/streaming.test.ts @@ -7,14 +7,13 @@ import { } from './index.js'; function once( - run: { on(type: string, handler: (event: unknown) => void): () => void }, - type: string + subscribe: (handler: (event: T) => void) => () => void ) { return new Promise((resolve) => { let off = () => {}; - off = run.on(type, (event) => { + off = subscribe((event) => { off(); - resolve(event as T); + resolve(event); }); }); } @@ -56,19 +55,19 @@ test('returns a live run before initial invoke output and emits ephemeral parts' const allParts: Array<{ type: string; delta: string }> = []; const states: string[] = []; const events: string[] = []; - const done = once<{ output: { text: string } }>(run, 'done'); + const done = once(run.onDone.bind(run)); const offPart = run.on('textPart', (part) => { parts.push(part as { type: string; delta: string }); }); - const offAnyPart = run.on('part', (part) => { - allParts.push(part as { type: string; delta: string }); + const offAnyPart = run.onEmitted((part) => { + allParts.push(part); }); - const offState = run.on('state', (snapshot) => { - states.push((snapshot as { value: string }).value); + const offState = run.onSnapshot((snapshot) => { + states.push(snapshot.value); }); - const offEvent = run.on('machine.event', (event) => { - events.push((event as { type: string }).type); + const offEvent = run.onMachineEvent((event) => { + events.push(event.type); }); expect(run.getSnapshot()).toEqual( @@ -132,22 +131,22 @@ test('does not replay prior events to late subscribers', async () => { const run = await startSession(machine, { store: createMemoryRunStore(), }); - await once(run, 'done'); + await once(run.onDone.bind(run)); const lateParts: Array<{ type: string; delta: string }> = []; const replayedStates: string[] = []; const replayedEvents: string[] = []; run.on('textPart', (part) => { - lateParts.push(part as { type: string; delta: string }); + lateParts.push(part); }); - run.on('state', (snapshot) => { - replayedStates.push((snapshot as { value: string }).value); + run.onSnapshot((snapshot) => { + replayedStates.push(snapshot.value); }); - run.on('machine.event', (event) => { - replayedEvents.push((event as { type: string }).type); + run.onMachineEvent((event) => { + replayedEvents.push(event.type); }); - run.on('done', () => { + run.onDone(() => { replayedEvents.push('done'); }); @@ -179,7 +178,7 @@ test('invalid emitted parts are rejected', async () => { const run = await startSession(machine, { store: createMemoryRunStore(), }); - await once(run, 'error'); + await once(run.onError.bind(run)); expect(run.getSnapshot()).toEqual( expect.objectContaining({ @@ -230,7 +229,7 @@ test('transition handlers can emit live effects without journaling them', async const parts: string[] = []; run.on('textPart', (part) => { - parts.push((part as { delta: string }).delta); + parts.push(part.delta); }); await run.send({ type: 'send' }); diff --git a/src/target-types.assert.ts b/src/target-types.assert.ts index ee3e9a0..80fde55 100644 --- a/src/target-types.assert.ts +++ b/src/target-types.assert.ts @@ -29,7 +29,7 @@ const machine = createAgentMachine({ machine.transition(machine.getInitialState(), { type: 'advance' }); createAgentMachine({ - id: 'typed-target-params', + id: 'typed-target-input', context: () => ({ count: 0 }), initial: 'idle', states: { @@ -37,14 +37,14 @@ createAgentMachine({ on: { advance: () => ({ target: 'working', - params: { + input: { index: 0, }, }), }, }, working: { - paramsSchema: z.object({ + inputSchema: z.object({ index: z.number(), }), }, @@ -58,21 +58,96 @@ createAgentMachine({ }, }); +const typedMachine = createAgentMachine({ + id: 'typed-surface', + schemas: { + input: z.object({ + task: z.string(), + }), + events: { + submit: z.object({ + value: z.number(), + }), + }, + output: z.object({ + task: z.string(), + total: z.number(), + }), + }, + context: (input) => ({ + task: input.task, + total: 0, + }), + initial: 'idle', + states: { + idle: { + on: { + submit: ({ event }) => { + event.value satisfies number; + // @ts-expect-error invalid event payload property + event.missing; + return { + target: 'done', + context: { total: event.value }, + }; + }, + }, + }, + done: { + type: 'final', + output: ({ context }) => ({ + task: context.task, + total: context.total, + }), + }, + }, +}); + +typedMachine.getInitialState({ task: 'ship it' }); +// @ts-expect-error missing required input +typedMachine.getInitialState(); +// @ts-expect-error wrong input type +typedMachine.getInitialState({ task: 42 }); + +const typedState = typedMachine.getInitialState({ task: 'infer state values' }); +typedState.value satisfies 'idle' | 'done'; +// @ts-expect-error invalid state literal +typedState.value satisfies 'missing'; + +typedMachine.transition(typedState, { type: 'submit', value: 1 }); +// @ts-expect-error invalid event type +typedMachine.transition(typedState, { type: 'missing' }); +// @ts-expect-error invalid event payload +typedMachine.transition(typedState, { type: 'submit', value: 'nope' }); + +void (async () => { + const result = await typedMachine.execute( + typedMachine.transition(typedState, { type: 'submit', value: 2 }) + ); + + if (result.status === 'done') { + result.output.total satisfies number; + result.output.task satisfies string; + // @ts-expect-error no missing output property + result.output.missing; + } +})(); + createAgentMachine({ - id: 'missing-required-target-params', + id: 'missing-required-target-input', context: () => ({ count: 0 }), initial: 'idle', states: { - // @ts-expect-error params should be required when the target has paramsSchema idle: { on: { + // @ts-expect-error input should be required when the target has inputSchema advance: () => ({ target: 'working', }), }, }, working: { - paramsSchema: z.object({ + inputSchema: z.object({ index: z.number(), }), }, @@ -91,9 +166,9 @@ createAgentMachine({ context: () => ({ count: 0 }), initial: 'idle', states: { - // @ts-expect-error invalid targets should be rejected at author time idle: { on: { + // @ts-expect-error invalid targets should be rejected at author time advance: () => ({ target: 'missing', }), @@ -113,16 +188,16 @@ createAgentMachine({ }); createAgentMachine({ - id: 'unexpected-target-params', + id: 'unexpected-target-input', context: () => ({ count: 0 }), initial: 'idle', states: { idle: { on: { - // @ts-expect-error params should be rejected when the target has no paramsSchema + // @ts-expect-error input should be rejected when the target has no inputSchema advance: () => ({ target: 'done', - params: { + input: { anything: true, }, }), @@ -142,23 +217,23 @@ createAgentMachine({ }); createAgentMachine({ - id: 'invalid-target-params', + id: 'invalid-target-input', context: () => ({ count: 0 }), initial: 'idle', states: { idle: { on: { - // @ts-expect-error target params should match the target state's params schema + // @ts-expect-error target input should match the target state's input schema advance: () => ({ target: 'working', - params: { + input: { wrong: true, }, }), }, }, working: { - paramsSchema: z.object({ + inputSchema: z.object({ index: z.number(), }), }, @@ -179,17 +254,17 @@ createAgentMachine({ states: { idle: { on: { - // @ts-expect-error target params should match the target param field types + // @ts-expect-error target input should match the target input field types advance: () => ({ target: 'working', - params: { + input: { index: 'hello', }, }), }, }, working: { - paramsSchema: z.object({ + inputSchema: z.object({ index: z.number(), }), }, diff --git a/src/types.ts b/src/types.ts index 961dc33..ec0b612 100644 --- a/src/types.ts +++ b/src/types.ts @@ -66,22 +66,22 @@ export interface AgentAdapter { export type TransitionResult< TContext extends Record = Record, TTarget extends string = string, - TParamsByTarget extends Record = {}, + TInputByTarget extends Record = {}, > = | { target?: undefined; context?: Partial; - params?: never; + input?: never; } | { [K in TTarget]: { target: K; context?: Partial; - } & (K extends keyof TParamsByTarget - ? IsExactlyUnknown extends true - ? { params?: never } - : { params: TParamsByTarget[K] } - : { params?: never }) + } & (K extends keyof TInputByTarget + ? IsExactlyUnknown extends true + ? { input?: never } + : { input: TInputByTarget[K] } + : { input?: never }) }[TTarget]; export interface InitialTransitionResult< @@ -90,7 +90,7 @@ export interface InitialTransitionResult< > { target: TTarget; context?: Partial; - params?: Record; + input?: Record; } // ─── State Config ─── @@ -98,44 +98,53 @@ export interface InitialTransitionResult< export interface StateConfig< TContext extends Record = Record, TTarget extends string = string, - TParamsByTarget extends Record = {}, + TInputByTarget extends Record = {}, > { type?: 'final' | 'choice'; - paramsSchema?: StandardSchemaV1; + inputSchema?: StandardSchemaV1; resultSchema?: StandardSchemaV1; invoke?: (args: { context: TContext; - params: Record; + input: Record; signal?: AbortSignal; }, enq: InvokeEnqueue) => Promise; - onDone?: (args: { result: any; context: TContext }) => TransitionResult; - on?: Record | ((args: { event: any; context: TContext }, enq: InvokeEnqueue) => TransitionResult)>; + onDone?: (args: { result: any; context: TContext }) => TransitionResult; + on?: Record | ((args: { event: any; context: TContext }, enq: InvokeEnqueue) => TransitionResult)>; events?: Record; output?: (args: { context: TContext }) => unknown; // choice-specific model?: string; adapter?: AgentAdapter; - prompt?: string | ((args: { context: TContext; params: Record }) => string); + prompt?: string | ((args: { context: TContext; input: Record }) => string); options?: Record; reasoning?: boolean; - /** @internal */ __type?: 'decide' | 'classify'; - /** @internal */ __decideConfig?: any; - /** @internal */ __classifyConfig?: any; } +type OutputForState = TState extends { + output: (...args: any[]) => infer TOutput; +} + ? TOutput + : never; + +export type OutputForStates> = + [OutputForState] extends [never] + ? unknown + : OutputForState; + // ─── Agent State (POJO) ─── export interface AgentState< TContext extends Record = Record, TValue extends string = string, + TOutput = unknown, > { value: TValue; context: TContext; status: 'active' | 'pending' | 'done' | 'error'; - params: Record>; + input: Record>; sessionId?: string; createdAt?: number; - output?: unknown; + output?: TOutput; error?: unknown; } @@ -145,24 +154,26 @@ export type ExecuteResult< TContext extends Record = Record, TValue extends string = string, TEvents extends Record = {}, + TOutput = unknown, > = - | { status: 'done'; state: AgentState; output: unknown; context: TContext } - | { status: 'pending'; state: AgentState; value: TValue; events: Record; context: TContext } - | { status: 'error'; state: AgentState; error: unknown }; + | { status: 'done'; state: AgentState; output: TOutput; context: TContext } + | { status: 'pending'; state: AgentState; value: TValue; events: Record; context: TContext } + | { status: 'error'; state: AgentState; error: unknown }; // ─── Snapshot ─── export interface AgentSnapshot< TContext extends Record = Record, TValue extends string = string, + TOutput = unknown, > { value: TValue; context: TContext; status: AgentState['status']; createdAt: number; sessionId: string; - params: Record>; - output?: unknown; + input: Record>; + output?: TOutput; error?: unknown; } @@ -173,56 +184,88 @@ export interface AgentMachine< TContext extends Record = Record, TEvents extends Record = {}, TStates extends Record = Record>, + TOutput = OutputForStates, + TEmitted extends Record = {}, > { readonly id: string; getInitialState( ...args: unknown extends TInput ? [input?: TInput] : [input: TInput] - ): AgentState; + ): AgentState; resolveState( raw: - | AgentSnapshot + | AgentSnapshot | { value: string; context: TContext; - params?: Record>; + input?: Record>; sessionId?: string; createdAt?: number; status?: AgentState['status']; - output?: unknown; + output?: TOutput; error?: unknown; } - ): AgentState; + ): AgentState; transition( - state: AgentState, + state: AgentState, event: TransitionEvent - ): AgentState; + ): AgentState; invoke( - state: AgentState - ): Promise>; + state: AgentState + ): Promise>; execute( - state: AgentState - ): Promise>; + state: AgentState + ): Promise>; stream( - state: AgentState - ): AsyncGenerator>; + state: AgentState + ): AsyncGenerator>; } export interface AgentRun< TContext extends Record = Record, TValue extends string = string, TEvents extends Record = {}, + TOutput = unknown, + TEmitted extends Record = {}, > { readonly sessionId: string; - readonly status: AgentSnapshot['status']; - getSnapshot(): AgentSnapshot; + readonly status: AgentSnapshot['status']; + getSnapshot(): AgentSnapshot; send(event: TransitionEvent): Promise; - on(type: string, handler: (event: unknown) => void): () => void; + on( + type: TKey, + handler: (event: { type: TKey } & EventPayload>) => void + ): () => void; + onEmitted( + handler: (event: EmittedUnion) => void + ): () => void; + onDone( + handler: (event: { + output: TOutput; + snapshot: AgentSnapshot; + }) => void + ): () => void; + onError( + handler: (event: { + error: unknown; + snapshot: AgentSnapshot; + }) => void + ): () => void; + onSnapshot( + handler: (snapshot: AgentSnapshot) => void + ): () => void; + onMachineEvent( + handler: ( + event: import('./runtime/store.js').JournalEventRecord< + import('./runtime/events.js').JournalEvent + > + ) => void + ): () => void; } export interface SessionOptions< @@ -247,25 +290,25 @@ export interface MachineConfig< TInput = unknown, TContext extends Record = Record, TEvents extends Record = {}, - TStates extends Record> = Record>, + TStates extends Record = Record>, + TEmitted extends Record = {}, > { id: string; schemas?: { input?: StandardSchemaV1; context?: StandardSchemaV1; events?: TEvents; - emitted?: Record; + emitted?: TEmitted; + output?: StandardSchemaV1; }; context: (input: TInput) => TContext; adapter?: AgentAdapter; initial: | (keyof TStates & string) - | ((args: { context: TContext }) => { target: keyof TStates & string; params?: Record }); + | ((args: { context: TContext }) => { target: keyof TStates & string; input?: Record }); states: TStates; } -// ─── Decide ─── - export type DecideResultFor< TOptions extends Record, > = { @@ -276,38 +319,31 @@ export type DecideResultFor< }; }[keyof TOptions & string]; -export interface DecideConfig< - TContext extends Record = Record, - TParams extends Record = Record, +export interface DecideOptions< TOptions extends Record = Record, - TTarget extends string = string, - TParamsByTarget extends Record = {}, > { - model: string; adapter?: AgentAdapter; - prompt: string | ((args: { context: TContext; params: TParams }) => string); + model: string; + prompt: string; options: TOptions; reasoning?: boolean; - onDone: (args: { result: DecideResultFor; context: TContext }) => TransitionResult; - on?: Record TransitionResult>; } -// ─── Classify ─── +export interface ClassifyResultFor< + TCategories extends Record = Record, +> { + category: keyof TCategories & string; +} -export interface ClassifyConfig< - TContext extends Record = Record, - TParams extends Record = Record, +export interface ClassifyOptions< TCategories extends Record = Record, - TTarget extends string = string, - TParamsByTarget extends Record = {}, > { - model: string; adapter?: AgentAdapter; - prompt: string | ((args: { context: TContext; params: TParams }) => string); + model: string; + prompt: string; into: TCategories; examples?: Array<{ input: string; category: keyof TCategories & string }>; - onDone: (args: { result: { category: keyof TCategories & string }; context: TContext }) => TransitionResult; - on?: Record TransitionResult>; + reasoning?: boolean; } // ─── Trace ─── diff --git a/src/utils.ts b/src/utils.ts index 8ac89c8..2665900 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -50,7 +50,7 @@ export type StateConfigAny = { invoke?: ( args: { context: Record; - params: Record; + input: Record; }, enq: { emit(part: { type: string; [key: string]: unknown }): void } ) => Promise; @@ -60,22 +60,20 @@ export type StateConfigAny = { resultSchema?: StandardSchemaV1; model?: string; adapter?: { decide: (...args: unknown[]) => Promise }; - prompt?: string | ((args: { context: Record; params: Record }) => string); + prompt?: string | ((args: { context: Record; input: Record }) => string); options?: Record; reasoning?: boolean; events?: Record; - __type?: string; - __decideConfig?: Record; }; /** - * Get the params for the current state. + * Get the input for the current state. */ -export function getParams( +export function getInput( value: string, - params: Record> + input: Record> ): Record { - return params[value] ?? {}; + return input[value] ?? {}; } /** @@ -86,11 +84,11 @@ export function resolveInitial( | string | ((args: { context: Record; - params: Record; + input: Record; }) => InitialTransitionResult), args: { context: Record; - params: Record; + input: Record; } ): InitialTransitionResult { if (typeof initial === 'string') { @@ -116,10 +114,10 @@ export function applyTransition( newState.value = transition.target; newState.status = 'active'; - if (transition.params) { - newState.params = { - ...state.params, - [transition.target]: transition.params, + if (transition.input) { + newState.input = { + ...state.input, + [transition.target]: transition.input, }; } } From ccc71e0f02864525bcb46408fc889dcba319a94e Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 18 Apr 2026 15:51:30 -0400 Subject: [PATCH 18/34] feat: add stately graph export and workflow examples --- ...04-08-langgraph-core-replacement-design.md | 2 +- examples/cloudflare-durable-object.ts | 147 ++++++ examples/index.ts | 14 + examples/persistence.ts | 132 +++++ .../react-agent-from-scratch.ts | 14 +- examples/react-agent.ts | 4 +- examples/sql-agent.ts | 279 +++++++++++ package.json | 3 +- pnpm-lock.yaml | 36 +- src/examples.test.ts | 148 ++++++ src/graph/index.test.ts | 175 +++++++ src/graph/index.ts | 456 +++++++++++++++++- src/index.ts | 8 - .../prebuilt-react.test.ts | 6 +- src/langgraph-equivalents/sql-agent.test.ts | 107 ++++ src/machine.ts | 1 + src/types.ts | 2 + 17 files changed, 1491 insertions(+), 43 deletions(-) create mode 100644 examples/cloudflare-durable-object.ts create mode 100644 examples/persistence.ts rename src/prebuilt/react.ts => examples/react-agent-from-scratch.ts (94%) create mode 100644 examples/sql-agent.ts create mode 100644 src/graph/index.test.ts create mode 100644 src/langgraph-equivalents/sql-agent.test.ts diff --git a/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md b/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md index f30ecb3..a304dee 100644 --- a/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md +++ b/docs/superpowers/specs/2026-04-08-langgraph-core-replacement-design.md @@ -271,7 +271,7 @@ type AgentSnapshot = { status: "active" | "done" | "error" | "pending"; createdAt: number; sessionId: string; - params: Record>; + input: Record>; output?: unknown; error?: SerializedError; }; diff --git a/examples/cloudflare-durable-object.ts b/examples/cloudflare-durable-object.ts new file mode 100644 index 0000000..5ff0527 --- /dev/null +++ b/examples/cloudflare-durable-object.ts @@ -0,0 +1,147 @@ +import { + createPersistenceExample, +} from './persistence.js'; +import { + restoreSession, + startSession, + type AgentSnapshot, + type JournalEvent, + type JournalEventRecord, + type PersistedSnapshot, + type RunStore, +} from '../src/index.js'; + +export interface DurableObjectStorageLike { + get(key: string): Promise; + put(key: string, value: T): Promise; +} + +export interface DurableObjectStateLike { + storage: DurableObjectStorageLike; +} + +export function createDurableObjectRunStore( + storage: DurableObjectStorageLike +): RunStore { + return { + async append(sessionId, event) { + const key = journalKey(sessionId); + const current = (await storage.get(key)) ?? []; + const sequence = + current.length === 0 + ? 1 + : current[current.length - 1]!.sequence + 1; + + await storage.put(key, [...current, { ...event, sequence }]); + return { sequence }; + }, + + async loadEvents(sessionId, afterSequence = 0) { + const current = + (await storage.get[]>( + journalKey(sessionId) + )) ?? []; + + return current + .filter((event) => event.sequence > afterSequence) + .sort((a, b) => a.sequence - b.sequence); + }, + + async loadLatestSnapshot(sessionId) { + const snapshots = + (await storage.get[]>( + snapshotsKey(sessionId) + )) ?? []; + + return ( + [...snapshots].sort( + (a, b) => + a.afterSequence - b.afterSequence || a.createdAt - b.createdAt + ).at(-1) ?? null + ); + }, + + async saveSnapshot(snapshot) { + const key = snapshotsKey(snapshot.sessionId); + const current = + (await storage.get[]>(key)) ?? []; + + await storage.put(key, [...current, snapshot]); + }, + }; +} + +export class AgentSessionDurableObject { + private readonly store: RunStore; + + constructor(private readonly state: DurableObjectStateLike) { + this.store = createDurableObjectRunStore(state.storage); + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const machine = createPersistenceExample(async ({ request, approved }) => ({ + summary: `${request} :: approved=${String(approved)}`, + })); + + if (request.method === 'POST' && url.pathname === '/start') { + const body = await request.json() as { request: string }; + const run = await startSession(machine, { + store: this.store, + input: { request: body.request }, + }); + + return Response.json({ + sessionId: run.sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'POST' && url.pathname === '/approve') { + const sessionId = requiredSessionId(url); + const run = await restoreSession(machine, { + store: this.store, + sessionId, + }); + + await run.send({ type: 'approve' }); + + return Response.json({ + sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'GET' && url.pathname === '/status') { + const sessionId = requiredSessionId(url); + const run = await restoreSession(machine, { + store: this.store, + sessionId, + }); + + return Response.json({ + sessionId, + snapshot: run.getSnapshot(), + }); + } + + return new Response('Not found', { status: 404 }); + } +} + +function requiredSessionId(url: URL): string { + const sessionId = url.searchParams.get('sessionId'); + if (!sessionId) { + throw new Error('Missing sessionId'); + } + + return sessionId; +} + +function journalKey(sessionId: string): string { + return `sessions/${sessionId}/journal`; +} + +function snapshotsKey(sessionId: string): string { + return `sessions/${sessionId}/snapshots`; +} diff --git a/examples/index.ts b/examples/index.ts index ce34ad0..8f611f9 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -1,9 +1,16 @@ export { createSimpleExample } from './simple.js'; +export { createSqlAgentExample } from './sql-agent.js'; export { createHitlExample } from './hitl.js'; export { createDecideExample } from './decide.js'; export { createClassifyExample } from './classify.js'; export { createAdapterExample } from './adapter.js'; export { createChatbotExample } from './chatbot.js'; +export { + AgentSessionDurableObject, + createDurableObjectRunStore, + type DurableObjectStateLike, + type DurableObjectStorageLike, +} from './cloudflare-durable-object.js'; export { createCustomerServiceSimExample } from './customer-service-sim.js'; export { createEmailExample } from './email.js'; export { createJokeExample } from './joke.js'; @@ -12,8 +19,15 @@ export { createMapReduceExample } from './map-reduce.js'; export { createMultiAgentNetworkExample } from './multi-agent-network.js'; export { createNewspaperExample } from './newspaper.js'; export { createPlanAndExecuteExample } from './plan-and-execute.js'; +export { createPersistenceExample, runPersistenceExample } from './persistence.js'; export { createRaffleExample } from './raffle.js'; export { createReactAgentExample } from './react-agent.js'; +export { + createReactAgentFromScratch, + type ReactAgentMessage, + type ReactAgentModelResult, + type ReactTool, +} from './react-agent-from-scratch.js'; export { createRewooExample } from './rewoo.js'; export { createReflectionExample } from './reflection.js'; export { createRiverCrossingExample } from './river-crossing.js'; diff --git a/examples/persistence.ts b/examples/persistence.ts new file mode 100644 index 0000000..b74ac48 --- /dev/null +++ b/examples/persistence.ts @@ -0,0 +1,132 @@ +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + restoreSession, + startSession, +} from '../src/index.js'; +import { + closePrompt, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const summarySchema = z.object({ + summary: z.string(), +}); + +export function createPersistenceExample( + summarize: (args: { + request: string; + approved: boolean; + }) => Promise> = async (args) => + generateExampleObject({ + schema: summarySchema, + system: 'You summarize approved requests in one concise sentence.', + prompt: [ + `Request: ${args.request}`, + `Approved: ${String(args.approved)}`, + '', + 'Write a short summary.', + ].join('\n'), + }) +) { + return createAgentMachine({ + id: 'persistence-example', + schemas: { + input: z.object({ + request: z.string(), + }), + output: z.object({ + request: z.string(), + approved: z.boolean(), + summary: z.string().nullable(), + }), + events: { + approve: z.object({}), + }, + }, + context: (input) => ({ + request: input.request, + approved: false, + summary: null as string | null, + }), + initial: 'review', + states: { + review: { + on: { + approve: { + target: 'summarizing', + context: { approved: true }, + }, + }, + }, + summarizing: { + resultSchema: summarySchema, + invoke: async ({ context }) => + summarize({ + request: context.request, + approved: context.approved, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { summary: result.summary }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + request: context.request, + approved: context.approved, + summary: context.summary, + }), + }, + }, + }); +} + +export async function runPersistenceExample( + input: { request: string }, + options: { + summarize?: (args: { + request: string; + approved: boolean; + }) => Promise>; + } = {} +) { + const machine = createPersistenceExample(options.summarize); + const store = createMemoryRunStore(); + + const liveRun = await startSession(machine, { + store, + input, + }); + + await liveRun.send({ type: 'approve' }); + + const restoredRun = await restoreSession(machine, { + sessionId: liveRun.sessionId, + store, + }); + + return { + sessionId: liveRun.sessionId, + liveSnapshot: liveRun.getSnapshot(), + restoredSnapshot: restoredRun.getSnapshot(), + }; +} + +async function main() { + try { + const request = await prompt('Request'); + const result = await runPersistenceExample({ request }); + console.log(result); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/src/prebuilt/react.ts b/examples/react-agent-from-scratch.ts similarity index 94% rename from src/prebuilt/react.ts rename to examples/react-agent-from-scratch.ts index 5e64924..bd82736 100644 --- a/src/prebuilt/react.ts +++ b/examples/react-agent-from-scratch.ts @@ -1,6 +1,5 @@ import { z } from 'zod'; -import { createAgentMachine } from '../machine.js'; -import type { StandardSchemaV1 } from '../types.js'; +import { createAgentMachine, type StandardSchemaV1 } from '../src/index.js'; const messageSchema = z.object({ role: z.enum(['system', 'user', 'assistant', 'tool']), @@ -25,6 +24,12 @@ const modelResultSchema = z.discriminatedUnion('kind', [ finalAnswerSchema, ]); +const reactOutputSchema = z.object({ + messages: z.array(messageSchema), + finalMessage: z.string().nullable(), + steps: z.number().int().min(0), +}); + export type ReactAgentMessage = z.infer; export type ReactTool = { @@ -36,7 +41,7 @@ export type ReactTool = { export type ReactAgentModelResult = z.infer; -export function createReactAgent(options: { +export function createReactAgentFromScratch(options: { prompt?: string; maxSteps?: number; tools?: ReactTool[]; @@ -63,11 +68,12 @@ export function createReactAgent(options: { } return createAgentMachine({ - id: 'prebuilt-react-agent', + id: 'react-agent-from-scratch', schemas: { input: z.object({ messages: z.array(messageSchema).optional(), }), + output: reactOutputSchema, emitted: { textPart: z.object({ delta: z.string() }), toolCall: z.object({ diff --git a/examples/react-agent.ts b/examples/react-agent.ts index b38b391..0d15e45 100644 --- a/examples/react-agent.ts +++ b/examples/react-agent.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; import { createMemoryRunStore, - createReactAgent, startSession, } from '../src/index.js'; +import { createReactAgentFromScratch } from './react-agent-from-scratch.js'; import { closePrompt, generateExampleObject, @@ -37,7 +37,7 @@ export function createReactAgentExample(options: { }>; }) => Promise>; } = {}) { - return createReactAgent({ + return createReactAgentFromScratch({ prompt: 'You are a helpful assistant.', tools: [ { diff --git a/examples/sql-agent.ts b/examples/sql-agent.ts new file mode 100644 index 0000000..14776b9 --- /dev/null +++ b/examples/sql-agent.ts @@ -0,0 +1,279 @@ +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + decide, + decideResultSchema, + startSession, + type AgentAdapter, +} from '../src/index.js'; +import { + closePrompt, + createOpenAiDecisionAdapter, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const sqlValueSchema = z.union([z.string(), z.number(), z.null()]); +const sqlRowsSchema = z.array(z.record(z.string(), sqlValueSchema)); + +const planningOptions = { + query: { + description: 'Write or revise a SQL query that should help answer the question.', + schema: z.object({ + query: z.string(), + }), + }, + answer: { + description: 'Return the final answer once the available query results are sufficient.', + schema: z.object({ + answer: z.string(), + }), + }, +} as const; + +const queryExecutionSchema = z.discriminatedUnion('status', [ + z.object({ + status: z.literal('success'), + query: z.string(), + rows: sqlRowsSchema, + }), + z.object({ + status: z.literal('error'), + query: z.string(), + error: z.string(), + }), +]); + +export function createSqlAgentExample( + options: { + adapter?: AgentAdapter; + executeQuery?: (args: { + question: string; + schema: string; + query: string; + queryHistory: string[]; + }) => Promise< + | { status: 'success'; rows: z.infer } + | { status: 'error'; error: string } + >; + } = {} +) { + const adapter = options.adapter ?? createOpenAiDecisionAdapter(); + const executeQuery = + options.executeQuery ?? + ((args: { + question: string; + schema: string; + query: string; + queryHistory: string[]; + }) => + generateExampleObject({ + schema: z.discriminatedUnion('status', [ + z.object({ + status: z.literal('success'), + rows: sqlRowsSchema, + }), + z.object({ + status: z.literal('error'), + error: z.string(), + }), + ]), + system: [ + 'You simulate a SQL database tool for demos.', + 'Return status="success" with concise rows when the query is plausible.', + 'Return status="error" with a short SQL/tool error when the query is invalid.', + ].join('\n'), + prompt: [ + `Question: ${args.question}`, + `Schema: ${args.schema}`, + `Query: ${args.query}`, + args.queryHistory.length + ? `Prior queries:\n${args.queryHistory.map((query, index) => `${index + 1}. ${query}`).join('\n')}` + : 'Prior queries: none', + ].join('\n'), + })); + + return createAgentMachine({ + id: 'sql-agent-example', + schemas: { + input: z.object({ + question: z.string(), + schema: z.string(), + }), + emitted: { + toolCall: z.object({ + toolName: z.literal('sqlDb'), + input: z.object({ + query: z.string(), + }), + }), + toolResult: z.object({ + toolName: z.literal('sqlDb'), + output: queryExecutionSchema, + }), + }, + output: z.object({ + question: z.string(), + schema: z.string(), + answer: z.string().nullable(), + latestRows: sqlRowsSchema.nullable(), + latestError: z.string().nullable(), + queryHistory: z.array(z.string()), + }), + }, + context: (input) => ({ + question: input.question, + schema: input.schema, + answer: null as string | null, + latestRows: null as z.infer | null, + latestError: null as string | null, + queryHistory: [] as string[], + }), + initial: 'planning', + states: { + planning: { + resultSchema: decideResultSchema(planningOptions), + invoke: async ({ context }) => + decide({ + adapter, + model: 'openai/gpt-5.4-nano', + prompt: [ + 'You are a SQL agent deciding whether to query the database again or answer.', + 'Query when you still need database evidence or when the last query failed.', + 'Answer only when the current rows are enough to respond directly.', + '', + `Question: ${context.question}`, + `Schema: ${context.schema}`, + context.queryHistory.length + ? `Previous queries:\n${context.queryHistory.map((query, index) => `${index + 1}. ${query}`).join('\n')}` + : 'Previous queries: none', + context.latestError + ? `Latest error: ${context.latestError}` + : 'Latest error: none', + context.latestRows + ? `Latest rows:\n${JSON.stringify(context.latestRows, null, 2)}` + : 'Latest rows: none', + ].join('\n'), + options: planningOptions, + }), + onDone: ({ result }) => { + if (result.choice === 'query') { + return { + target: 'querying', + input: { + query: result.data.query, + }, + }; + } + + return { + target: 'done', + context: { + answer: result.data.answer, + }, + }; + }, + }, + querying: { + inputSchema: z.object({ + query: z.string(), + }), + resultSchema: queryExecutionSchema, + invoke: async ({ context, input }, enq) => { + enq.emit({ + type: 'toolCall', + toolName: 'sqlDb', + input, + }); + + const output = await executeQuery({ + question: context.question, + schema: context.schema, + query: input.query, + queryHistory: context.queryHistory, + }); + + const resolvedOutput = + output.status === 'success' + ? { + status: 'success' as const, + query: input.query, + rows: output.rows, + } + : { + status: 'error' as const, + query: input.query, + error: output.error, + }; + + enq.emit({ + type: 'toolResult', + toolName: 'sqlDb', + output: resolvedOutput, + }); + + return resolvedOutput; + }, + onDone: ({ result, context }) => ({ + target: 'planning', + context: { + queryHistory: [ + ...context.queryHistory, + result.query, + ], + latestRows: result.status === 'success' ? result.rows : null, + latestError: result.status === 'error' ? result.error : null, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + question: context.question, + schema: context.schema, + answer: context.answer, + latestRows: context.latestRows, + latestError: context.latestError, + queryHistory: context.queryHistory, + }), + }, + }, + }); +} + +async function main() { + try { + const question = await prompt('Question'); + const schema = await prompt('Schema'); + const machine = createSqlAgentExample(); + const run = await startSession(machine, { + store: createMemoryRunStore(), + input: { question, schema }, + }); + + run.on('toolCall', (event) => { + console.log(`Calling ${event.toolName}(${event.input.query})`); + }); + run.on('toolResult', (event) => { + console.log(`${event.toolName} -> ${JSON.stringify(event.output)}`); + }); + + await new Promise((resolve, reject) => { + run.onDone((event) => { + console.log(event.output); + resolve(); + }); + run.onError((event) => { + reject(event.error); + }); + }); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/package.json b/package.json index 88cb9a5..f4af6b2 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "dotenv": "^16.4.5", "tsdown": "^0.21.7", "tsx": "^4.21.0", - "typescript": "^5.6.2", "vitest": "^2.1.2", "zod": "^4.3.6" }, @@ -95,7 +94,9 @@ "access": "public" }, "dependencies": { + "@statelyai/graph": "^0.11.0", "ai": "^6.0.67", + "typescript": "^5.6.2", "xstate": "^5.26.0" }, "packageManager": "pnpm@10.28.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 397027b..7e25004 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,15 @@ importers: .: dependencies: + '@statelyai/graph': + specifier: ^0.11.0 + version: 0.11.0(zod@4.3.6) ai: specifier: ^6.0.67 version: 6.0.67(zod@4.3.6) + typescript: + specifier: ^5.6.2 + version: 5.9.3 xstate: specifier: ^5.26.0 version: 5.26.0 @@ -36,9 +42,6 @@ importers: tsx: specifier: ^4.21.0 version: 4.21.0 - typescript: - specifier: ^5.6.2 - version: 5.9.3 vitest: specifier: ^2.1.2 version: 2.1.9(@types/node@20.19.30) @@ -754,6 +757,29 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, tarball: https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz} + '@statelyai/graph@0.11.0': + resolution: {integrity: sha512-qm1AmeXTQu6wrA5o4bXIlic4BDWLJCnNisHoqDan7Qa/UWi3yGg10W8xobItql+qvnVcoghJN1ZmR3cUFeHV7A==, tarball: https://registry.npmjs.org/@statelyai/graph/-/graph-0.11.0.tgz} + peerDependencies: + cytoscape: ^3.0.0 + d3-force: ^3.0.0 + dotparser: ^1.0.0 + elkjs: ^0.9.0 || ^0.10.0 || ^0.11.0 + fast-xml-parser: ^5.0.0 + zod: ^4.0.0 + peerDependenciesMeta: + cytoscape: + optional: true + d3-force: + optional: true + dotparser: + optional: true + elkjs: + optional: true + fast-xml-parser: + optional: true + zod: + optional: true + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==, tarball: https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz} @@ -2055,6 +2081,10 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@statelyai/graph@0.11.0(zod@4.3.6)': + optionalDependencies: + zod: 4.3.6 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 diff --git a/src/examples.test.ts b/src/examples.test.ts index 532dc14..8481d1d 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -5,6 +5,7 @@ import { resolve } from 'node:path'; import { createChatbotExample, + createDurableObjectRunStore, createAdapterExample, createBranchingExample, createClassifyExample, @@ -17,6 +18,7 @@ import { createMapReduceExample, createMultiAgentNetworkExample, createNewspaperExample, + runPersistenceExample, createPlanAndExecuteExample, createRaffleExample, createReactAgentExample, @@ -24,6 +26,7 @@ import { createReflectionExample, createRiverCrossingExample, createSimpleExample, + createSqlAgentExample, createSubflowExample, createSupervisorExample, createToolCallingExample, @@ -34,12 +37,14 @@ describe('curated examples', () => { test('ships the canonical examples directory', () => { const examplesDir = resolve(process.cwd(), 'examples'); expect(existsSync(resolve(examplesDir, 'simple.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'sql-agent.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'hitl.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'decide.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'classify.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'adapter.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'branching.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'chatbot.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'cloudflare-durable-object.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'customer-service-sim.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'email.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'joke.ts'))).toBe(true); @@ -47,9 +52,11 @@ describe('curated examples', () => { expect(existsSync(resolve(examplesDir, 'map-reduce.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'multi-agent-network.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'newspaper.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'persistence.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'plan-and-execute.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'raffle.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'react-agent.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'react-agent-from-scratch.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'rewoo.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'reflection.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'river-crossing.ts'))).toBe(true); @@ -73,6 +80,75 @@ describe('curated examples', () => { } }); + test('persistence example restores a durable session to the same final snapshot', async () => { + const result = await runPersistenceExample( + { request: 'Approve the annual budget summary.' }, + { + summarize: async ({ request, approved }) => ({ + summary: `${request} :: approved=${String(approved)}`, + }), + } + ); + + expect(result.liveSnapshot).toEqual(result.restoredSnapshot); + expect(result.liveSnapshot).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { + request: 'Approve the annual budget summary.', + approved: true, + summary: 'Approve the annual budget summary. :: approved=true', + }, + }) + ); + }); + + test('cloudflare durable object example store persists journal and snapshots', async () => { + const storage = new Map(); + const store = createDurableObjectRunStore({ + async get(key) { + return storage.get(key) as never; + }, + async put(key, value) { + storage.set(key, value); + }, + }); + + await store.append('session-1', { + type: 'xstate.init', + at: 1, + }); + await store.append('session-1', { + type: 'approve', + at: 2, + }); + await store.saveSnapshot({ + sessionId: 'session-1', + afterSequence: 2, + snapshot: { + value: 'done', + context: {}, + status: 'done', + createdAt: 2, + sessionId: 'session-1', + input: {}, + }, + createdAt: 2, + }); + + await expect(store.loadEvents('session-1')).resolves.toEqual([ + expect.objectContaining({ sequence: 1, type: 'xstate.init' }), + expect.objectContaining({ sequence: 2, type: 'approve' }), + ]); + await expect(store.loadLatestSnapshot('session-1')).resolves.toEqual( + expect.objectContaining({ + sessionId: 'session-1', + afterSequence: 2, + }) + ); + }); + test('hitl example exposes typed pending events', async () => { const machine = createHitlExample(); const result = await machine.execute( @@ -571,6 +647,78 @@ describe('curated examples', () => { ); }); + test('sql-agent example retries after a bad query and then answers from rows', async () => { + let decisions = 0; + + const machine = createSqlAgentExample({ + adapter: { + decide: async () => { + decisions += 1; + + if (decisions === 1) { + return { + choice: 'query', + data: { + query: 'SELECT total FROM invoices WHERE customer = "Acme"', + }, + }; + } + + if (decisions === 2) { + return { + choice: 'query', + data: { + query: "SELECT customer, total FROM invoices WHERE customer = 'Acme'", + }, + }; + } + + return { + choice: 'answer', + data: { + answer: 'Acme has one invoice total of 42.', + }, + }; + }, + }, + executeQuery: async ({ query }) => { + if (query.includes('"Acme"')) { + return { + status: 'error' as const, + error: 'SQL syntax error near double quotes.', + }; + } + + return { + status: 'success' as const, + rows: [{ customer: 'Acme', total: 42 }], + }; + }, + }); + + const result = await machine.execute( + machine.getInitialState({ + question: 'What is Acme owed?', + schema: 'invoices(customer text, total integer)', + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + question: 'What is Acme owed?', + schema: 'invoices(customer text, total integer)', + answer: 'Acme has one invoice total of 42.', + latestRows: [{ customer: 'Acme', total: 42 }], + latestError: null, + queryHistory: [ + 'SELECT total FROM invoices WHERE customer = "Acme"', + "SELECT customer, total FROM invoices WHERE customer = 'Acme'", + ], + }); + } + }); + test('react agent example loops through a tool and returns a final answer', async () => { const { createMemoryRunStore, startSession } = await import('./index.js'); const agent = createReactAgentExample({ diff --git a/src/graph/index.test.ts b/src/graph/index.test.ts new file mode 100644 index 0000000..117b84c --- /dev/null +++ b/src/graph/index.test.ts @@ -0,0 +1,175 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { createAgentMachine } from '../index.js'; +import { toGraph, toMermaid } from './index.js'; + +test('exports finite states and transition edges as Stately graph JSON', () => { + const machine = createAgentMachine({ + id: 'graph-export', + schemas: { + events: { + submit: z.object({ + type: z.literal('submit'), + count: z.number(), + }), + }, + }, + context: () => ({ + total: 0, + }), + initial: 'idle', + states: { + idle: { + on: { + submit: ({ event }) => { + if (event.count > 0) { + return { + target: 'working', + context: { total: event.count }, + input: { index: event.count }, + }; + } + + return { + target: 'done', + }; + }, + }, + }, + working: { + inputSchema: z.object({ + index: z.number(), + }), + resultSchema: z.object({ + ok: z.boolean(), + }), + invoke: async () => ({ ok: true }), + onDone: () => ({ + target: 'done', + }), + }, + done: { + type: 'final', + output: ({ context }) => context, + }, + }, + }); + + expect(toGraph(machine)).toEqual({ + id: 'graph-export', + type: 'directed', + initialNodeId: 'idle', + data: undefined, + nodes: [ + { type: 'node', id: 'idle', label: 'idle', data: { type: 'state' } }, + { type: 'node', id: 'working', label: 'working', data: { type: 'state' } }, + { type: 'node', id: 'done', label: 'done', data: { type: 'final' } }, + ], + edges: [ + { + type: 'edge', + id: 'idle:submit:0', + sourceId: 'idle', + targetId: 'working', + label: 'submit [event.count > 0]', + data: { + event: 'submit', + guard: { type: 'event.count > 0' }, + actions: { + context: true, + input: true, + }, + }, + }, + { + type: 'edge', + id: 'idle:submit:1', + sourceId: 'idle', + targetId: 'done', + label: 'submit', + data: { + event: 'submit', + }, + }, + { + type: 'edge', + id: 'working:done:2', + sourceId: 'working', + targetId: 'done', + label: 'done', + data: { + event: 'done', + }, + }, + ], + }); +}); + +test('exports a mermaid state diagram from the Stately graph data', () => { + const machine = createAgentMachine({ + id: 'mermaid-export', + context: () => ({}), + initial: 'idle', + states: { + idle: { + on: { + finish: { target: 'done' }, + }, + }, + done: { + type: 'final', + }, + }, + }); + + expect(toMermaid(machine)).toContain('idle --> done: finish'); +}); + +test('infers guards from conditional-expression transition branches', () => { + const machine = createAgentMachine({ + id: 'conditional-export', + schemas: { + events: { + choose: z.object({ + type: z.literal('choose'), + ok: z.boolean(), + }), + }, + }, + context: () => ({}), + initial: 'idle', + states: { + idle: { + on: { + choose: ({ event }) => + event.ok + ? { target: 'accepted' } + : { target: 'rejected' }, + }, + }, + accepted: { + type: 'final', + }, + rejected: { + type: 'final', + }, + }, + }); + + expect(toGraph(machine).edges).toEqual([ + expect.objectContaining({ + sourceId: 'idle', + targetId: 'accepted', + data: expect.objectContaining({ + guard: { type: 'event.ok' }, + }), + }), + expect.objectContaining({ + sourceId: 'idle', + targetId: 'rejected', + data: expect.objectContaining({ + guard: { type: '!(event.ok)' }, + }), + }), + ]); +}); diff --git a/src/graph/index.ts b/src/graph/index.ts index 7d09dfd..78e3dba 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -1,33 +1,447 @@ -import type { AgentMachine } from '../types.js'; +import { + createGraph, + type EdgeConfig, + type Graph as StatelyGraph, + type GraphEdge as StatelyGraphEdge, + type GraphNode as StatelyGraphNode, + type NodeConfig, +} from '@statelyai/graph'; +import ts from 'typescript'; +import type { + AgentMachine, + MachineConfig, + StateConfig, + TransitionResult, +} from '../types.js'; -export interface GraphNode { - id: string; +export interface AgentGraphNodeData { type: 'state' | 'choice' | 'final'; } -export interface GraphEdge { - source: string; - target: string; - label?: string; +export interface AgentGraphEdgeData { + event?: string; + guard?: { + type: string; + }; + actions?: { + context?: boolean; + input?: boolean; + }; } -export interface Graph { - nodes: GraphNode[]; - edges: GraphEdge[]; -} +export interface AgentGraph + extends StatelyGraph {} +export interface AgentGraphNode + extends StatelyGraphNode {} +export interface AgentGraphEdge + extends StatelyGraphEdge {} + +type InternalMachine = AgentMachine & { + __config?: MachineConfig; +}; + +type EdgeCandidate = { + target: string; + guard?: string; + hasContext?: boolean; + hasInput?: boolean; +}; /** - * Convert an agent machine to a graph representation. - * TODO: implement AST analysis for edge extraction + * Convert an agent machine to a Stately graph-compatible plain JSON object. + * + * Finite states come directly from the authored `states` object. Edges are + * inferred from static transition objects and transition handler ASTs. */ -export function toGraph(_machine: AgentMachine): Graph { - throw new Error('toGraph is not yet implemented'); +export function toGraph(machine: AgentMachine): AgentGraph { + const config = (machine as InternalMachine).__config; + if (!config) { + throw new Error('Machine config metadata is unavailable for graph export'); + } + + const nodes: Array> = Object.entries( + config.states + ).map(([id, state]) => ({ + id, + label: id, + data: { + type: getNodeType(state as StateConfig), + }, + })); + + const edges: Array> = []; + for (const [sourceId, state] of Object.entries(config.states)) { + const stateConfig = state as StateConfig; + + if (stateConfig.onDone) { + edges.push( + ...getTransitionEdges({ + sourceId, + event: 'done', + transition: stateConfig.onDone, + ordinalOffset: edges.length, + }) + ); + } + + if (!stateConfig.on) { + continue; + } + + for (const [event, transition] of Object.entries(stateConfig.on)) { + edges.push( + ...getTransitionEdges({ + sourceId, + event, + transition, + ordinalOffset: edges.length, + }) + ); + } + } + + return createGraph({ + id: machine.id, + initialNodeId: + typeof config.initial === 'string' ? config.initial : undefined, + nodes, + edges, + }); } -/** - * Convert an agent machine to a Mermaid stateDiagram-v2 string. - * TODO: implement - */ -export function toMermaid(_machine: AgentMachine): string { - throw new Error('toMermaid is not yet implemented'); +export function toMermaid(machine: AgentMachine): string { + const graph = toGraph(machine); + const lines = ['stateDiagram-v2']; + + if (graph.initialNodeId) { + lines.push(` [*] --> ${graph.initialNodeId}`); + } + + for (const node of graph.nodes) { + if (node.data.type === 'final') { + lines.push(` ${node.id} --> [*]`); + } + } + + for (const edge of graph.edges) { + lines.push( + ` ${edge.sourceId} --> ${edge.targetId}${ + edge.label ? `: ${edge.label}` : '' + }` + ); + } + + return lines.join('\n'); +} + +function getNodeType(state: StateConfig): AgentGraphNodeData['type'] { + if (state.type === 'final') { + return 'final'; + } + + if (state.type === 'choice') { + return 'choice'; + } + + return 'state'; +} + +function getTransitionEdges(args: { + sourceId: string; + event: string; + transition: unknown; + ordinalOffset: number; +}): Array> { + const candidates = + typeof args.transition === 'function' + ? analyzeTransitionFunction(args.transition) + : analyzeTransitionObject(args.transition); + + return candidates.map((candidate, index) => ({ + id: `${args.sourceId}:${args.event}:${args.ordinalOffset + index}`, + sourceId: args.sourceId, + targetId: candidate.target, + label: getEdgeLabel(args.event, candidate.guard), + data: { + event: args.event, + ...(candidate.guard + ? { + guard: { + type: candidate.guard, + }, + } + : {}), + ...((candidate.hasContext || candidate.hasInput) + ? { + actions: { + ...(candidate.hasContext ? { context: true } : {}), + ...(candidate.hasInput ? { input: true } : {}), + }, + } + : {}), + }, + })); +} + +function analyzeTransitionObject(transition: unknown): EdgeCandidate[] { + const target = + transition && typeof transition === 'object' + ? (transition as TransitionResult).target + : undefined; + + if ( + transition + && typeof transition === 'object' + && 'target' in transition + && typeof target === 'string' + ) { + return [ + { + target, + hasContext: 'context' in transition, + hasInput: 'input' in transition, + }, + ]; + } + + return []; +} + +function analyzeTransitionFunction(fn: Function): EdgeCandidate[] { + const source = fn.toString(); + const file = ts.createSourceFile( + 'transition.ts', + `const __transition = ${source};`, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS + ); + const transitionFunction = findTransitionFunction(file); + + if (!transitionFunction) { + return []; + } + + const candidates: EdgeCandidate[] = []; + const ancestors: ts.Node[] = []; + + function visit(node: ts.Node) { + if (node !== transitionFunction && isFunctionLike(node)) { + return; + } + + if ( + ts.isArrowFunction(node) + && !ts.isBlock(node.body) + && ts.isExpression(node.body) + ) { + candidates.push( + ...analyzeTransitionExpression( + node.body, + findGuardForReturnLike(node, ancestors, file), + file + ) + ); + } + + if (ts.isReturnStatement(node) && node.expression) { + candidates.push( + ...analyzeTransitionExpression( + node.expression, + findGuardForReturnLike(node, ancestors, file), + file + ) + ); + } + + ancestors.push(node); + ts.forEachChild(node, visit); + ancestors.pop(); + } + + visit(transitionFunction); + + return candidates; +} + +function findTransitionFunction(file: ts.SourceFile): ts.FunctionLike | undefined { + let transitionFunction: ts.FunctionLike | undefined; + + function visit(node: ts.Node) { + if ( + ts.isVariableDeclaration(node) + && ts.isIdentifier(node.name) + && node.name.text === '__transition' + && node.initializer + && isFunctionLike(node.initializer) + ) { + transitionFunction = node.initializer; + return; + } + + if (!transitionFunction) { + ts.forEachChild(node, visit); + } + } + + visit(file); + return transitionFunction; +} + +function isFunctionLike(node: ts.Node): node is ts.FunctionLike { + return ( + ts.isArrowFunction(node) + || ts.isFunctionExpression(node) + || ts.isFunctionDeclaration(node) + || ts.isMethodDeclaration(node) + ); +} + +function analyzeTransitionExpression( + expression: ts.Expression, + guard: string | undefined, + file: ts.SourceFile +): EdgeCandidate[] { + let current = expression; + + while (ts.isParenthesizedExpression(current)) { + current = current.expression; + } + + if (ts.isConditionalExpression(current)) { + const condition = current.condition.getText(file); + + return [ + ...analyzeTransitionExpression( + current.whenTrue, + combineGuards(guard, condition), + file + ), + ...analyzeTransitionExpression( + current.whenFalse, + combineGuards(guard, `!(${condition})`), + file + ), + ]; + } + + const object = unwrapParenthesizedObject(current); + const target = object ? getStringProperty(object, 'target') : undefined; + if (!target) { + return []; + } + + return [ + { + target, + guard, + hasContext: object ? hasProperty(object, 'context') : false, + hasInput: object ? hasProperty(object, 'input') : false, + }, + ]; +} + +function unwrapParenthesizedObject( + expression: ts.Expression +): ts.ObjectLiteralExpression | undefined { + let current = expression; + + while (ts.isParenthesizedExpression(current)) { + current = current.expression; + } + + return ts.isObjectLiteralExpression(current) ? current : undefined; +} + +function getStringProperty( + object: ts.ObjectLiteralExpression, + name: string +): string | undefined { + const property = object.properties.find((candidate) => { + return ( + ts.isPropertyAssignment(candidate) + && ts.isIdentifier(candidate.name) + && candidate.name.text === name + ); + }); + + if (!property || !ts.isPropertyAssignment(property)) { + return undefined; + } + + const initializer = property.initializer; + return ts.isStringLiteralLike(initializer) ? initializer.text : undefined; +} + +function hasProperty(object: ts.ObjectLiteralExpression, name: string): boolean { + return object.properties.some((candidate) => { + return ( + ts.isPropertyAssignment(candidate) + && ts.isIdentifier(candidate.name) + && candidate.name.text === name + ); + }); +} + +function findGuardForReturnLike( + returnNode: ts.Node, + ancestors: ts.Node[], + file: ts.SourceFile +): string | undefined { + for (let index = ancestors.length - 1; index >= 0; index -= 1) { + const ancestor = ancestors[index]; + if (!ancestor || !ts.isIfStatement(ancestor)) { + continue; + } + + if (containsNode(ancestor.thenStatement, returnNode)) { + return ancestor.expression.getText(file); + } + + if (ancestor.elseStatement && containsNode(ancestor.elseStatement, returnNode)) { + return `!(${ancestor.expression.getText(file)})`; + } + } + + return undefined; +} + +function containsNode(parent: ts.Node, child: ts.Node): boolean { + if (parent === child) { + return true; + } + + let found = false; + function visit(node: ts.Node) { + if (node === child) { + found = true; + return; + } + + if (!found) { + ts.forEachChild(node, visit); + } + } + + visit(parent); + return found; +} + +function getEdgeLabel(event: string, guard: string | undefined): string { + if (!guard) { + return event; + } + + return `${event} [${guard}]`; +} + +function combineGuards( + outer: string | undefined, + inner: string +): string { + if (!outer) { + return inner; + } + + return `(${outer}) && (${inner})`; } diff --git a/src/index.ts b/src/index.ts index e56ee3d..12162ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,14 +3,6 @@ export { createAgentMachine } from './machine.js'; export { decide, decideResultSchema, requireAdapter } from './decide.js'; export { classify, classifyResultSchema } from './classify.js'; -// AI primitives -export { createReactAgent } from './prebuilt/react.js'; -export type { - ReactAgentMessage, - ReactAgentModelResult, - ReactTool, -} from './prebuilt/react.js'; - // Adapter export { createAdapter } from './adapter.js'; export { createMemoryRunStore } from './runtime/memory-store.js'; diff --git a/src/langgraph-equivalents/prebuilt-react.test.ts b/src/langgraph-equivalents/prebuilt-react.test.ts index d08435a..de52d4a 100644 --- a/src/langgraph-equivalents/prebuilt-react.test.ts +++ b/src/langgraph-equivalents/prebuilt-react.test.ts @@ -1,9 +1,9 @@ import { expect, test } from 'vitest'; import { createMemoryRunStore, - createReactAgent, startSession, } from '../index.js'; +import { createReactAgentFromScratch } from '../../examples/react-agent-from-scratch.js'; function once( subscribe: (handler: (event: T) => void) => () => void @@ -17,8 +17,8 @@ function once( }); } -test('prebuilt react agent loops through a tool call and returns a final answer', async () => { - const agent = createReactAgent({ +test('react agent from scratch loops through a tool call and returns a final answer', async () => { + const agent = createReactAgentFromScratch({ prompt: 'You are helpful.', tools: [ { diff --git a/src/langgraph-equivalents/sql-agent.test.ts b/src/langgraph-equivalents/sql-agent.test.ts new file mode 100644 index 0000000..9100120 --- /dev/null +++ b/src/langgraph-equivalents/sql-agent.test.ts @@ -0,0 +1,107 @@ +import { expect, test } from 'vitest'; +import { createMemoryRunStore, startSession } from '../index.js'; +import { createSqlAgentExample } from '../../examples/sql-agent.js'; + +function once( + subscribe: (handler: (event: T) => void) => () => void +) { + return new Promise((resolve) => { + let off = () => {}; + off = subscribe((event) => { + off(); + resolve(event); + }); + }); +} + +test('sql-agent workflow retries after a bad query and answers once rows are available', async () => { + let decisions = 0; + + const machine = createSqlAgentExample({ + adapter: { + decide: async () => { + decisions += 1; + + if (decisions === 1) { + return { + choice: 'query', + data: { + query: 'SELECT total FROM invoices WHERE customer = "Acme"', + }, + }; + } + + if (decisions === 2) { + return { + choice: 'query', + data: { + query: 'SELECT customer, total FROM invoices WHERE customer = \'Acme\'', + }, + }; + } + + return { + choice: 'answer', + data: { + answer: 'Acme has one invoice total of 42.', + }, + }; + }, + }, + executeQuery: async ({ query }) => { + if (query.includes('"Acme"')) { + return { + status: 'error' as const, + error: 'SQL syntax error near double quotes.', + }; + } + + return { + status: 'success' as const, + rows: [{ customer: 'Acme', total: 42 }], + }; + }, + }); + + const run = await startSession(machine, { + store: createMemoryRunStore(), + input: { + question: 'What is Acme owed?', + schema: 'invoices(customer text, total integer)', + }, + }); + const events: string[] = []; + + run.on('toolCall', (event) => { + events.push(`call:${event.input.query}`); + }); + run.on('toolResult', (event) => { + events.push(`result:${event.output.status}`); + }); + + await once(run.onDone.bind(run)); + + expect(events).toEqual([ + 'call:SELECT total FROM invoices WHERE customer = "Acme"', + 'result:error', + "call:SELECT customer, total FROM invoices WHERE customer = 'Acme'", + 'result:success', + ]); + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { + question: 'What is Acme owed?', + schema: 'invoices(customer text, total integer)', + answer: 'Acme has one invoice total of 42.', + latestRows: [{ customer: 'Acme', total: 42 }], + latestError: null, + queryHistory: [ + 'SELECT total FROM invoices WHERE customer = "Acme"', + "SELECT customer, total FROM invoices WHERE customer = 'Acme'", + ], + }, + }) + ); +}); diff --git a/src/machine.ts b/src/machine.ts index fa2942b..5371bc0 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -653,6 +653,7 @@ export function createAgentMachine( return { id: cfg.id, + __config: cfg, getInitialState, resolveState, transition, diff --git a/src/types.ts b/src/types.ts index ec0b612..b6809f9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -188,6 +188,8 @@ export interface AgentMachine< TEmitted extends Record = {}, > { readonly id: string; + /** @internal */ + readonly __config?: unknown; getInitialState( ...args: unknown extends TInput ? [input?: TInput] : [input: TInput] From e513614bd5748c0d1826bfe3f82dfdce5010109d Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 20 Apr 2026 13:16:23 -0400 Subject: [PATCH 19/34] feat: add agent machine conversion CLI --- package.json | 1 + scripts/agent-convert.ts | 212 +++++++++++++++++++++++++++++++++++++++ src/graph/index.test.ts | 5 +- src/graph/index.ts | 91 +++++++++++++---- src/xstate/index.test.ts | 109 ++++++++++++++++++++ src/xstate/index.ts | 184 +++++++++++++++++++++++++++++++-- 6 files changed, 573 insertions(+), 29 deletions(-) create mode 100644 scripts/agent-convert.ts create mode 100644 src/xstate/index.test.ts diff --git a/package.json b/package.json index f4af6b2..990cbd6 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "dist" ], "scripts": { + "agent:convert": "tsx scripts/agent-convert.ts", "build": "tsdown", "lint": "tsc --noEmit", "test": "vitest", diff --git a/scripts/agent-convert.ts b/scripts/agent-convert.ts new file mode 100644 index 0000000..3849d4f --- /dev/null +++ b/scripts/agent-convert.ts @@ -0,0 +1,212 @@ +import { writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { toMermaid } from '../src/graph/index.js'; +import type { AgentMachine } from '../src/index.js'; +import { toXStateMachine } from '../src/xstate/index.js'; + +type Format = 'mermaid' | 'xstate'; + +interface CliOptions { + file?: string; + format: Format; + exportName?: string; + factoryName?: string; + outFile?: string; + help: boolean; +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + + if (options.help || !options.file) { + printHelp(); + process.exit(options.help ? 0 : 1); + } + + const machine = await loadMachine(options); + const output = + options.format === 'mermaid' + ? toMermaid(machine) + : `${JSON.stringify(toXStateMachine(machine), null, 2)}\n`; + + if (options.outFile) { + await writeFile(resolve(options.outFile), output); + return; + } + + process.stdout.write(output.endsWith('\n') ? output : `${output}\n`); +} + +function parseArgs(args: string[]): CliOptions { + const options: CliOptions = { + format: 'mermaid', + help: false, + }; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]!; + + if (arg === '--help' || arg === '-h') { + options.help = true; + continue; + } + + if (arg === '--format' || arg === '-f') { + options.format = parseFormat(requiredValue(args, index, arg)); + index += 1; + continue; + } + + if (arg.startsWith('--format=')) { + options.format = parseFormat(arg.slice('--format='.length)); + continue; + } + + if (arg === '--export' || arg === '-e') { + options.exportName = requiredValue(args, index, arg); + index += 1; + continue; + } + + if (arg.startsWith('--export=')) { + options.exportName = arg.slice('--export='.length); + continue; + } + + if (arg === '--factory') { + options.factoryName = requiredValue(args, index, arg); + index += 1; + continue; + } + + if (arg.startsWith('--factory=')) { + options.factoryName = arg.slice('--factory='.length); + continue; + } + + if (arg === '--out' || arg === '-o') { + options.outFile = requiredValue(args, index, arg); + index += 1; + continue; + } + + if (arg.startsWith('--out=')) { + options.outFile = arg.slice('--out='.length); + continue; + } + + if (arg.startsWith('-')) { + throw new Error(`Unknown option: ${arg}`); + } + + if (options.file) { + throw new Error(`Unexpected positional argument: ${arg}`); + } + + options.file = arg; + } + + return options; +} + +function parseFormat(value: string): Format { + if (value === 'mermaid' || value === 'xstate') { + return value; + } + + throw new Error(`Unsupported format '${value}'. Use 'mermaid' or 'xstate'.`); +} + +function requiredValue(args: string[], index: number, option: string): string { + const value = args[index + 1]; + if (!value || value.startsWith('-')) { + throw new Error(`Missing value for ${option}`); + } + + return value; +} + +async function loadMachine(options: CliOptions): Promise { + const fileUrl = pathToFileURL(resolve(options.file!)).href; + const mod = await import(fileUrl) as Record; + + if (options.factoryName) { + const factory = mod[options.factoryName]; + if (typeof factory !== 'function') { + throw new Error(`Export '${options.factoryName}' is not a function.`); + } + + const machine = await factory(); + return assertAgentMachine(machine, `factory '${options.factoryName}'`); + } + + if (options.exportName) { + return assertAgentMachine( + mod[options.exportName], + `export '${options.exportName}'` + ); + } + + for (const candidate of [mod.default, mod.machine]) { + if (isAgentMachine(candidate)) { + return candidate; + } + } + + const namedMachines = Object.entries(mod).filter(([, value]) => + isAgentMachine(value) + ); + if (namedMachines.length === 1) { + return namedMachines[0]![1] as AgentMachine; + } + + throw new Error( + [ + 'Could not find an agent machine export.', + 'Export a machine as default or named `machine`, or pass `--export `.', + 'For zero-arg factory exports, pass `--factory `.', + ].join(' ') + ); +} + +function assertAgentMachine(value: unknown, label: string): AgentMachine { + if (!isAgentMachine(value)) { + throw new Error(`${label} did not return an agent machine.`); + } + + return value; +} + +function isAgentMachine(value: unknown): value is AgentMachine { + return ( + !!value + && typeof value === 'object' + && typeof (value as AgentMachine).id === 'string' + && typeof (value as AgentMachine).getInitialState === 'function' + && typeof (value as AgentMachine).transition === 'function' + && typeof (value as AgentMachine).execute === 'function' + ); +} + +function printHelp() { + process.stdout.write(`Usage: + pnpm agent:convert [--format mermaid|xstate] + +Options: + -f, --format Output format. Defaults to mermaid. + -e, --export Named export containing an agent machine. + --factory Named zero-arg factory that returns an agent machine. + -o, --out Write output to a file instead of stdout. + -h, --help Show this help. + +Examples: + pnpm agent:convert ./examples/simple.ts --factory createSimpleExample + pnpm agent:convert ./examples/simple.ts --factory createSimpleExample --format xstate +`); +} + +main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exit(1); +}); diff --git a/src/graph/index.test.ts b/src/graph/index.test.ts index 117b84c..b8e7444 100644 --- a/src/graph/index.test.ts +++ b/src/graph/index.test.ts @@ -122,7 +122,10 @@ test('exports a mermaid state diagram from the Stately graph data', () => { }, }); - expect(toMermaid(machine)).toContain('idle --> done: finish'); + expect(toMermaid(machine)).toBe(`stateDiagram-v2 + [*] --> idle + idle --> done : finish + done --> [*]`); }); test('infers guards from conditional-expression transition branches', () => { diff --git a/src/graph/index.ts b/src/graph/index.ts index 78e3dba..8a78aba 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -6,6 +6,13 @@ import { type GraphNode as StatelyGraphNode, type NodeConfig, } from '@statelyai/graph'; +import { + toMermaidState, + type MermaidStateGraph, + type StateEdgeData, + type StateGraphData, + type StateNodeData, +} from '@statelyai/graph/mermaid'; import ts from 'typescript'; import type { AgentMachine, @@ -110,28 +117,7 @@ export function toGraph(machine: AgentMachine): AgentGraph { } export function toMermaid(machine: AgentMachine): string { - const graph = toGraph(machine); - const lines = ['stateDiagram-v2']; - - if (graph.initialNodeId) { - lines.push(` [*] --> ${graph.initialNodeId}`); - } - - for (const node of graph.nodes) { - if (node.data.type === 'final') { - lines.push(` ${node.id} --> [*]`); - } - } - - for (const edge of graph.edges) { - lines.push( - ` ${edge.sourceId} --> ${edge.targetId}${ - edge.label ? `: ${edge.label}` : '' - }` - ); - } - - return lines.join('\n'); + return toMermaidState(toMermaidStateGraph(toGraph(machine))); } function getNodeType(state: StateConfig): AgentGraphNodeData['type'] { @@ -146,6 +132,67 @@ function getNodeType(state: StateConfig): AgentGraphNodeData['type'] { return 'state'; } +function toMermaidStateGraph(graph: AgentGraph): MermaidStateGraph { + const nodes: Array> = graph.nodes.map((node) => ({ + id: node.id, + label: node.label, + data: { + ...(node.data.type === 'choice' ? { stateType: 'choice' as const } : {}), + }, + })); + + const edges: Array> = graph.edges.map((edge) => ({ + id: edge.id, + sourceId: edge.sourceId, + targetId: edge.targetId, + label: edge.label ?? undefined, + data: {}, + })); + + if (graph.initialNodeId) { + const startId = `${graph.id}.__start`; + nodes.push({ + id: startId, + data: { isStart: true }, + }); + edges.unshift({ + id: `${startId}:initial`, + sourceId: startId, + targetId: graph.initialNodeId, + data: {}, + }); + } + + for (const node of graph.nodes) { + if (node.data.type !== 'final') { + continue; + } + + const endId = `${node.id}.__end`; + nodes.push({ + id: endId, + data: { isEnd: true }, + }); + edges.push({ + id: `${node.id}:final`, + sourceId: node.id, + targetId: endId, + data: {}, + }); + } + + return createGraph({ + id: graph.id, + type: graph.type, + initialNodeId: graph.initialNodeId ?? undefined, + data: { + diagramType: 'stateDiagram', + }, + nodes, + edges, + }); +} + function getTransitionEdges(args: { sourceId: string; event: string; diff --git a/src/xstate/index.test.ts b/src/xstate/index.test.ts new file mode 100644 index 0000000..8e7fe97 --- /dev/null +++ b/src/xstate/index.test.ts @@ -0,0 +1,109 @@ +import { expect, test } from 'vitest'; +import { z } from 'zod'; +import { createAgentMachine } from '../index.js'; +import { toXStateMachine } from './index.js'; + +test('exports a serializable XState config for visualization', () => { + const machine = createAgentMachine({ + id: 'xstate-export', + schemas: { + events: { + submit: z.object({ + type: z.literal('submit'), + count: z.number(), + }), + }, + }, + context: () => ({ total: 0 }), + initial: 'idle', + states: { + idle: { + on: { + submit: ({ event }) => { + if (event.count > 0) { + return { + target: 'working', + context: { total: event.count }, + input: { index: event.count }, + }; + } + + return { target: 'done' }; + }, + }, + }, + working: { + inputSchema: z.object({ + index: z.number(), + }), + resultSchema: z.object({ + ok: z.boolean(), + }), + invoke: async () => ({ ok: true }), + onDone: () => ({ + target: 'done', + }), + }, + done: { + type: 'final', + }, + }, + }); + + expect(toXStateMachine(machine)).toEqual({ + id: 'xstate-export', + initial: 'idle', + states: { + idle: { + on: { + submit: [ + { + target: 'working', + guard: { type: 'event.count > 0' }, + actions: ['assignContext', 'assignInput'], + meta: { + agent: { + event: 'submit', + updates: { + context: true, + input: true, + }, + }, + }, + }, + { + target: 'done', + meta: { + agent: { + event: 'submit', + }, + }, + }, + ], + }, + }, + working: { + invoke: { + id: 'invoke.working', + src: 'invoke.working', + onDone: { + target: 'done', + meta: { + agent: { + event: 'done', + }, + }, + }, + }, + meta: { + agent: { + invoke: true, + }, + }, + }, + done: { + type: 'final', + }, + }, + }); +}); diff --git a/src/xstate/index.ts b/src/xstate/index.ts index 0bd2c66..6c112c2 100644 --- a/src/xstate/index.ts +++ b/src/xstate/index.ts @@ -1,10 +1,182 @@ -import type { AgentMachine } from '../types.js'; +import { toGraph, type AgentGraph, type AgentGraphEdge } from '../graph/index.js'; +import type { AgentMachine, MachineConfig, StateConfig } from '../types.js'; + +export interface XStateMachineConfig { + id: string; + initial?: string; + states: Record; +} + +export interface XStateStateConfig { + type?: 'final'; + on?: Record; + invoke?: { + id: string; + src: string; + onDone?: XStateTransitionConfig | XStateTransitionConfig[]; + }; + onDone?: XStateTransitionConfig | XStateTransitionConfig[]; + meta?: { + agent?: { + type?: 'choice'; + invoke?: boolean; + }; + }; +} + +export interface XStateTransitionConfig { + target: string; + guard?: { + type: string; + }; + actions?: string[]; + meta?: { + agent?: { + event?: string; + updates?: { + context?: boolean; + input?: boolean; + }; + }; + }; +} + +type InternalMachine = AgentMachine & { + __config?: MachineConfig; +}; /** - * Convert an agent machine to an XState machine definition - * for visualization in the Stately Editor. - * TODO: implement + * Convert an agent machine to a serializable XState machine config for + * visualization. Runtime behavior is still driven by the agent machine. */ -export function toXStateMachine(_machine: AgentMachine): unknown { - throw new Error('toXStateMachine is not yet implemented'); +export function toXStateMachine(machine: AgentMachine): XStateMachineConfig { + const config = (machine as InternalMachine).__config; + if (!config) { + throw new Error('Machine config metadata is unavailable for XState export'); + } + + const graph = toGraph(machine); + const states: Record = {}; + + for (const [stateId, state] of Object.entries(config.states)) { + const stateConfig = state as StateConfig; + const xstateState: XStateStateConfig = {}; + + if (stateConfig.type === 'final') { + xstateState.type = 'final'; + } + + const meta: NonNullable['agent'] = {}; + if (stateConfig.type === 'choice') { + meta.type = 'choice'; + } + + if (stateConfig.invoke) { + meta.invoke = true; + xstateState.invoke = { + id: `invoke.${stateId}`, + src: `invoke.${stateId}`, + }; + } + + const regularEdges = graph.edges.filter((edge) => + edge.sourceId === stateId + && edge.data.event !== 'done' + ); + + for (const [event, edges] of groupEdgesByEvent(regularEdges)) { + const formatted = formatTransitions(edges); + if (!formatted) { + continue; + } + + xstateState.on ??= {}; + xstateState.on[event] = formatted; + } + + if (stateConfig.onDone) { + const doneEdges = graph.edges.filter((edge) => + edge.sourceId === stateId + && edge.data.event === 'done' + ); + + const formattedDone = formatTransitions(doneEdges); + if (formattedDone) { + if (xstateState.invoke) { + xstateState.invoke.onDone = formattedDone; + } else { + xstateState.onDone = formattedDone; + } + } + } + + if (Object.keys(meta).length > 0) { + xstateState.meta = { agent: meta }; + } + + states[stateId] = xstateState; + } + + return { + id: machine.id, + ...(typeof graph.initialNodeId === 'string' + ? { initial: graph.initialNodeId } + : {}), + states, + }; +} + +function groupEdgesByEvent( + edges: AgentGraph['edges'] +): Map { + const grouped = new Map(); + + for (const edge of edges) { + const event = edge.data.event; + if (!event) { + continue; + } + + grouped.set(event, [...(grouped.get(event) ?? []), edge]); + } + + return grouped; +} + +function formatTransitions( + edges: AgentGraphEdge[] +): XStateTransitionConfig | XStateTransitionConfig[] | undefined { + const transitions = edges.map(formatTransition); + + if (transitions.length === 0) { + return undefined; + } + + return transitions.length === 1 ? transitions[0]! : transitions; +} + +function formatTransition(edge: AgentGraphEdge): XStateTransitionConfig { + const actions = [ + ...(edge.data.actions?.context ? ['assignContext'] : []), + ...(edge.data.actions?.input ? ['assignInput'] : []), + ]; + + return { + target: edge.targetId, + ...(edge.data.guard ? { guard: edge.data.guard } : {}), + ...(actions.length > 0 ? { actions } : {}), + meta: { + agent: { + event: edge.data.event, + ...(edge.data.actions + ? { + updates: { + ...(edge.data.actions.context ? { context: true } : {}), + ...(edge.data.actions.input ? { input: true } : {}), + }, + } + : {}), + }, + }, + }; } From dd82cf9956a4adfdb90e4487bb4affd0f7bafac2 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 23 Apr 2026 11:02:14 -0400 Subject: [PATCH 20/34] feat: improve graph analysis and retry workflows --- examples/error-retry.ts | 134 ++++ examples/index.ts | 1 + readme.md | 5 + src/agent-convert-cli.test.ts | 62 ++ src/examples.test.ts | 25 + src/fixtures/converter-machine.ts | 43 ++ src/graph/index.test.ts | 118 +++- src/graph/index.ts | 622 +++++++++++++----- src/langgraph-equivalents/error-retry.test.ts | 50 ++ src/xstate/index.test.ts | 3 +- src/xstate/index.ts | 4 +- 11 files changed, 894 insertions(+), 173 deletions(-) create mode 100644 examples/error-retry.ts create mode 100644 src/agent-convert-cli.test.ts create mode 100644 src/fixtures/converter-machine.ts create mode 100644 src/langgraph-equivalents/error-retry.test.ts diff --git a/examples/error-retry.ts b/examples/error-retry.ts new file mode 100644 index 0000000..dba62e9 --- /dev/null +++ b/examples/error-retry.ts @@ -0,0 +1,134 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const answerSchema = z.object({ + answer: z.string(), +}); + +export function createErrorRetryExample( + answer: (args: { + question: string; + attempt: number; + }) => Promise> = async ({ question, attempt }) => + generateExampleObject({ + schema: answerSchema, + system: 'Answer the user question in one concise paragraph.', + prompt: [ + `Attempt: ${attempt}`, + '', + `Question: ${question}`, + ].join('\n'), + }), + maxAttempts = 3 +) { + return createAgentMachine({ + id: 'error-retry-example', + schemas: { + input: z.object({ + question: z.string(), + }), + events: { + 'xstate.error.invoke.answering': z.object({ + type: z.literal('xstate.error.invoke.answering'), + error: z.unknown().optional(), + at: z.number().optional(), + }), + }, + output: z.object({ + answer: z.string().nullable(), + attempts: z.number().int().min(1), + errors: z.array(z.string()), + }), + }, + context: (input) => ({ + question: input.question, + answer: null as string | null, + attempt: 1, + errors: [] as string[], + }), + initial: 'answering', + states: { + answering: { + resultSchema: answerSchema, + invoke: async ({ context }) => + answer({ + question: context.question, + attempt: context.attempt, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { + answer: result.answer, + }, + }), + on: { + 'xstate.error.invoke.answering': ({ event, context }) => { + const errors = [...context.errors, formatError(event.error)]; + + if (context.attempt >= maxAttempts) { + return { + target: 'failed', + context: { errors }, + }; + } + + return { + target: 'answering', + context: { + attempt: context.attempt + 1, + errors, + }, + }; + }, + }, + }, + failed: { + type: 'final', + output: ({ context }) => ({ + answer: context.answer, + attempts: context.attempt, + errors: context.errors, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + answer: context.answer, + attempts: context.attempt, + errors: context.errors, + }), + }, + }, + }); +} + +function formatError(error: unknown): string { + if (error && typeof error === 'object' && 'message' in error) { + return String((error as { message: unknown }).message); + } + + return String(error); +} + +async function main() { + try { + const question = await prompt('Question'); + const machine = createErrorRetryExample(); + const result = await machine.execute(machine.getInitialState({ question })); + + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/index.ts b/examples/index.ts index 8f611f9..4205092 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -13,6 +13,7 @@ export { } from './cloudflare-durable-object.js'; export { createCustomerServiceSimExample } from './customer-service-sim.js'; export { createEmailExample } from './email.js'; +export { createErrorRetryExample } from './error-retry.js'; export { createJokeExample } from './joke.js'; export { createJugsExample } from './jugs.js'; export { createMapReduceExample } from './map-reduce.js'; diff --git a/readme.md b/readme.md index 59970fe..1d7d8a6 100644 --- a/readme.md +++ b/readme.md @@ -9,14 +9,19 @@ Stately Agent is a flexible framework for building AI agents using state machine ## Examples + + The examples in [`examples/`](/Users/davidkpiano/Code/agent/examples) are intentionally small, run in the CLI, and use real OpenAI calls when `OPENAI_API_KEY` is set. Run them with `node --import tsx examples/.ts`. +Convert a machine file to diagram output with `pnpm agent:convert --format mermaid` or `pnpm agent:convert --format xstate`. + Each example demonstrates one concept: - [`examples/simple.ts`](/Users/davidkpiano/Code/agent/examples/simple.ts): the smallest `createAgentMachine(...)` flow with an `invoke` state that makes a real LLM call - [`examples/hitl.ts`](/Users/davidkpiano/Code/agent/examples/hitl.ts): a human-in-the-loop machine that pauses in a pending state, accepts typed events, and drafts with an LLM after approval +- [`examples/error-retry.ts`](/Users/davidkpiano/Code/agent/examples/error-retry.ts): retrying transient invoke failures through explicit internal error events - [`examples/decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts): choosing the next branch with structured branch-specific payloads - [`examples/classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts): routing into a fixed category set when you only need a label - [`examples/adapter.ts`](/Users/davidkpiano/Code/agent/examples/adapter.ts): plugging in a custom adapter that still uses a real OpenAI model under the hood diff --git a/src/agent-convert-cli.test.ts b/src/agent-convert-cli.test.ts new file mode 100644 index 0000000..bc573b7 --- /dev/null +++ b/src/agent-convert-cli.test.ts @@ -0,0 +1,62 @@ +import { execFile } from 'node:child_process'; +import { mkdtemp, readFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { promisify } from 'node:util'; +import { expect, test } from 'vitest'; + +const execFileAsync = promisify(execFile); + +test('agent:convert writes Mermaid and XState output from machine files', async () => { + const tmp = await mkdtemp(join(tmpdir(), 'agent-convert-')); + const fixture = resolve('src/fixtures/converter-machine.ts'); + + const mermaidFile = join(tmp, 'default.mmd'); + await runConvert([fixture, '--format', 'mermaid', '--out', mermaidFile]); + await expect(readFile(mermaidFile, 'utf8')).resolves.toBe(`stateDiagram-v2 + [*] --> idle + idle --> done : submit [event.ok] + idle --> rejected : submit [!(event.ok)] + rejected --> [*] + done --> [*]`); + + const namedXStateFile = join(tmp, 'named.json'); + await runConvert([ + fixture, + '--export', + 'namedMachine', + '--format', + 'xstate', + '--out', + namedXStateFile, + ]); + const namedXState = JSON.parse(await readFile(namedXStateFile, 'utf8')) as { + id: string; + initial: string; + states: Record; + }; + expect(namedXState.id).toBe('named-converter-machine'); + expect(namedXState.initial).toBe('idle'); + expect(Object.keys(namedXState.states)).toEqual(['idle', 'rejected', 'done']); + + const factoryXStateFile = join(tmp, 'factory.json'); + await runConvert([ + fixture, + '--factory', + 'createFixtureMachine', + '--format', + 'xstate', + '--out', + factoryXStateFile, + ]); + const factoryXState = JSON.parse(await readFile(factoryXStateFile, 'utf8')) as { + id: string; + }; + expect(factoryXState.id).toBe('factory-converter-machine'); +}); + +async function runConvert(args: string[]) { + await execFileAsync('pnpm', ['agent:convert', ...args], { + cwd: resolve('.'), + }); +} diff --git a/src/examples.test.ts b/src/examples.test.ts index 8481d1d..5650652 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -12,6 +12,7 @@ import { createCustomerServiceSimExample, createDecideExample, createEmailExample, + createErrorRetryExample, createHitlExample, createJokeExample, createJugsExample, @@ -47,6 +48,7 @@ describe('curated examples', () => { expect(existsSync(resolve(examplesDir, 'cloudflare-durable-object.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'customer-service-sim.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'email.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'error-retry.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'joke.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'jugs.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'map-reduce.ts'))).toBe(true); @@ -224,6 +226,29 @@ describe('curated examples', () => { } }); + test('error retry example recovers from transient invoke failures', async () => { + const machine = createErrorRetryExample(async ({ attempt }) => { + if (attempt === 1) { + throw new Error('temporary outage'); + } + + return { answer: 'Recovered answer.' }; + }); + + const result = await machine.execute( + machine.getInitialState({ question: 'Can this retry?' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + answer: 'Recovered answer.', + attempts: 2, + errors: ['temporary outage'], + }); + } + }); + test('branching example fans out plain async work and summarizes it', async () => { const machine = createBranchingExample({ analyzeDocs: async () => 'docs', diff --git a/src/fixtures/converter-machine.ts b/src/fixtures/converter-machine.ts new file mode 100644 index 0000000..c94fea0 --- /dev/null +++ b/src/fixtures/converter-machine.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../index.js'; + +export const namedMachine = createFixtureMachine('named-converter-machine'); + +export default createFixtureMachine('default-converter-machine'); + +export function createFixtureMachine(id = 'factory-converter-machine') { + return createAgentMachine({ + id, + schemas: { + events: { + submit: z.object({ + type: z.literal('submit'), + ok: z.boolean(), + }), + }, + }, + context: () => ({ + approved: false, + }), + initial: 'idle', + states: { + idle: { + on: { + submit: ({ event }) => + event.ok + ? { + target: 'done', + context: { approved: true }, + } + : { target: 'rejected' }, + }, + }, + rejected: { + type: 'final', + }, + done: { + type: 'final', + }, + }, + }); +} diff --git a/src/graph/index.test.ts b/src/graph/index.test.ts index b8e7444..ba3c897 100644 --- a/src/graph/index.test.ts +++ b/src/graph/index.test.ts @@ -3,6 +3,8 @@ import { z } from 'zod'; import { createAgentMachine } from '../index.js'; import { toGraph, toMermaid } from './index.js'; +declare function unknownTransition(): { target: 'done' }; + test('exports finite states and transition edges as Stately graph JSON', () => { const machine = createAgentMachine({ id: 'graph-export', @@ -74,6 +76,7 @@ test('exports finite states and transition edges as Stately graph JSON', () => { label: 'submit [event.count > 0]', data: { event: 'submit', + source: 'event', guard: { type: 'event.count > 0' }, actions: { context: true, @@ -86,25 +89,132 @@ test('exports finite states and transition edges as Stately graph JSON', () => { id: 'idle:submit:1', sourceId: 'idle', targetId: 'done', - label: 'submit', + label: 'submit [!(event.count > 0)]', data: { event: 'submit', + source: 'event', + guard: { type: '!(event.count > 0)' }, }, }, { type: 'edge', - id: 'working:done:2', + id: 'working:done.invoke.working:2', sourceId: 'working', targetId: 'done', - label: 'done', + label: 'done.invoke.working', data: { - event: 'done', + event: 'done.invoke.working', + source: 'invoke.done', }, }, ], }); }); +test('infers switch, early-return, and helper-call transition branches', () => { + const machine = createAgentMachine({ + id: 'ast-rich-export', + schemas: { + events: { + route: z.object({ + type: z.literal('route'), + kind: z.enum(['a', 'b', 'c']), + urgent: z.boolean(), + }), + }, + }, + context: () => ({}), + initial: 'idle', + states: { + idle: { + on: { + route: ({ event }) => { + const toA = () => ({ target: 'a' as const }); + + if (event.urgent) { + return toA(); + } + + switch (event.kind) { + case 'b': + return { target: 'b' as const }; + case 'c': + return { target: 'c' as const }; + default: + return { target: 'fallback' as const }; + } + }, + }, + }, + a: { type: 'final' }, + b: { type: 'final' }, + c: { type: 'final' }, + fallback: { type: 'final' }, + }, + }); + + expect(toGraph(machine).edges).toEqual([ + expect.objectContaining({ + targetId: 'a', + data: expect.objectContaining({ + guard: { type: 'event.urgent' }, + }), + }), + expect.objectContaining({ + targetId: 'b', + data: expect.objectContaining({ + guard: { type: '(!(event.urgent)) && (event.kind === "b")' }, + }), + }), + expect.objectContaining({ + targetId: 'c', + data: expect.objectContaining({ + guard: { type: '(!(event.urgent)) && (event.kind === "c")' }, + }), + }), + expect.objectContaining({ + targetId: 'fallback', + data: expect.objectContaining({ + guard: { + type: '(!(event.urgent)) && (!(event.kind === "b") && !(event.kind === "c"))', + }, + }), + }), + ]); +}); + +test('reports graph warnings for unsupported transition analysis', () => { + const machine = createAgentMachine({ + id: 'ast-warning-export', + schemas: { + events: { + go: z.object({ type: z.literal('go') }), + }, + }, + context: () => ({}), + initial: 'idle', + states: { + idle: { + on: { + go: () => { + return unknownTransition(); + }, + }, + }, + done: { type: 'final' }, + }, + }); + + expect(toGraph(machine).data?.warnings).toEqual([ + { + state: 'idle', + event: 'go', + message: + 'Unsupported helper call: unknownTransition() is not statically resolvable.', + }, + ]); +}); + test('exports a mermaid state diagram from the Stately graph data', () => { const machine = createAgentMachine({ id: 'mermaid-export', diff --git a/src/graph/index.ts b/src/graph/index.ts index 8a78aba..4a7dfe4 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -27,6 +27,7 @@ export interface AgentGraphNodeData { export interface AgentGraphEdgeData { event?: string; + source?: 'event' | 'invoke.done'; guard?: { type: string; }; @@ -36,8 +37,18 @@ export interface AgentGraphEdgeData { }; } +export interface AgentGraphData { + warnings?: AgentGraphWarning[]; +} + +export interface AgentGraphWarning { + state: string; + event: string; + message: string; +} + export interface AgentGraph - extends StatelyGraph {} + extends StatelyGraph {} export interface AgentGraphNode extends StatelyGraphNode {} export interface AgentGraphEdge @@ -54,6 +65,22 @@ type EdgeCandidate = { hasInput?: boolean; }; +type AnalysisResult = { + candidates: EdgeCandidate[]; + warnings: string[]; +}; + +type BlockAnalysis = AnalysisResult & { + exits: boolean; +}; + +type AnalyzableFunction = + | ts.ArrowFunction + | ts.FunctionExpression + | ts.FunctionDeclaration; + +type HelperMap = Map; + /** * Convert an agent machine to a Stately graph-compatible plain JSON object. * @@ -77,18 +104,21 @@ export function toGraph(machine: AgentMachine): AgentGraph { })); const edges: Array> = []; + const warnings: AgentGraphWarning[] = []; for (const [sourceId, state] of Object.entries(config.states)) { const stateConfig = state as StateConfig; if (stateConfig.onDone) { - edges.push( - ...getTransitionEdges({ - sourceId, - event: 'done', - transition: stateConfig.onDone, - ordinalOffset: edges.length, - }) - ); + const event = `done.invoke.${sourceId}`; + const result = getTransitionEdges({ + sourceId, + event, + source: 'invoke.done', + transition: stateConfig.onDone, + ordinalOffset: edges.length, + }); + edges.push(...result.edges); + warnings.push(...formatWarnings(sourceId, event, result.warnings)); } if (!stateConfig.on) { @@ -96,21 +126,23 @@ export function toGraph(machine: AgentMachine): AgentGraph { } for (const [event, transition] of Object.entries(stateConfig.on)) { - edges.push( - ...getTransitionEdges({ - sourceId, - event, - transition, - ordinalOffset: edges.length, - }) - ); + const result = getTransitionEdges({ + sourceId, + event, + source: 'event', + transition, + ordinalOffset: edges.length, + }); + edges.push(...result.edges); + warnings.push(...formatWarnings(sourceId, event, result.warnings)); } } - return createGraph({ + return createGraph({ id: machine.id, initialNodeId: typeof config.initial === 'string' ? config.initial : undefined, + ...(warnings.length > 0 ? { data: { warnings } } : {}), nodes, edges, }); @@ -196,41 +228,49 @@ function toMermaidStateGraph(graph: AgentGraph): MermaidStateGraph { function getTransitionEdges(args: { sourceId: string; event: string; + source: NonNullable; transition: unknown; ordinalOffset: number; -}): Array> { - const candidates = +}): { + edges: Array>; + warnings: string[]; +} { + const result = typeof args.transition === 'function' ? analyzeTransitionFunction(args.transition) : analyzeTransitionObject(args.transition); - return candidates.map((candidate, index) => ({ - id: `${args.sourceId}:${args.event}:${args.ordinalOffset + index}`, - sourceId: args.sourceId, - targetId: candidate.target, - label: getEdgeLabel(args.event, candidate.guard), - data: { - event: args.event, - ...(candidate.guard - ? { - guard: { - type: candidate.guard, - }, - } - : {}), - ...((candidate.hasContext || candidate.hasInput) - ? { - actions: { - ...(candidate.hasContext ? { context: true } : {}), - ...(candidate.hasInput ? { input: true } : {}), - }, - } - : {}), - }, - })); + return { + edges: result.candidates.map((candidate, index) => ({ + id: `${args.sourceId}:${args.event}:${args.ordinalOffset + index}`, + sourceId: args.sourceId, + targetId: candidate.target, + label: getEdgeLabel(args.event, candidate.guard), + data: { + event: args.event, + source: args.source, + ...(candidate.guard + ? { + guard: { + type: candidate.guard, + }, + } + : {}), + ...((candidate.hasContext || candidate.hasInput) + ? { + actions: { + ...(candidate.hasContext ? { context: true } : {}), + ...(candidate.hasInput ? { input: true } : {}), + }, + } + : {}), + }, + })), + warnings: result.warnings, + }; } -function analyzeTransitionObject(transition: unknown): EdgeCandidate[] { +function analyzeTransitionObject(transition: unknown): AnalysisResult { const target = transition && typeof transition === 'object' ? (transition as TransitionResult).target @@ -242,19 +282,20 @@ function analyzeTransitionObject(transition: unknown): EdgeCandidate[] { && 'target' in transition && typeof target === 'string' ) { - return [ - { + return { + candidates: [{ target, hasContext: 'context' in transition, hasInput: 'input' in transition, - }, - ]; + }], + warnings: [], + }; } - return []; + return { candidates: [], warnings: [] }; } -function analyzeTransitionFunction(fn: Function): EdgeCandidate[] { +function analyzeTransitionFunction(fn: Function): AnalysisResult { const source = fn.toString(); const file = ts.createSourceFile( 'transition.ts', @@ -266,53 +307,40 @@ function analyzeTransitionFunction(fn: Function): EdgeCandidate[] { const transitionFunction = findTransitionFunction(file); if (!transitionFunction) { - return []; + return { + candidates: [], + warnings: ['Unable to parse transition function.'], + }; } - const candidates: EdgeCandidate[] = []; - const ancestors: ts.Node[] = []; + const helpers = collectHelpers(transitionFunction); - function visit(node: ts.Node) { - if (node !== transitionFunction && isFunctionLike(node)) { - return; - } - - if ( - ts.isArrowFunction(node) - && !ts.isBlock(node.body) - && ts.isExpression(node.body) - ) { - candidates.push( - ...analyzeTransitionExpression( - node.body, - findGuardForReturnLike(node, ancestors, file), - file - ) - ); - } - - if (ts.isReturnStatement(node) && node.expression) { - candidates.push( - ...analyzeTransitionExpression( - node.expression, - findGuardForReturnLike(node, ancestors, file), - file - ) - ); - } - - ancestors.push(node); - ts.forEachChild(node, visit); - ancestors.pop(); + if (ts.isArrowFunction(transitionFunction) && !ts.isBlock(transitionFunction.body)) { + return analyzeTransitionExpression( + transitionFunction.body, + [], + file, + helpers + ); } - visit(transitionFunction); + if (transitionFunction.body && ts.isBlock(transitionFunction.body)) { + return analyzeStatements( + transitionFunction.body.statements, + [], + file, + helpers + ); + } - return candidates; + return { + candidates: [], + warnings: ['Unsupported transition function body.'], + }; } -function findTransitionFunction(file: ts.SourceFile): ts.FunctionLike | undefined { - let transitionFunction: ts.FunctionLike | undefined; +function findTransitionFunction(file: ts.SourceFile): AnalyzableFunction | undefined { + let transitionFunction: AnalyzableFunction | undefined; function visit(node: ts.Node) { if ( @@ -320,7 +348,7 @@ function findTransitionFunction(file: ts.SourceFile): ts.FunctionLike | undefine && ts.isIdentifier(node.name) && node.name.text === '__transition' && node.initializer - && isFunctionLike(node.initializer) + && isAnalyzableFunction(node.initializer) ) { transitionFunction = node.initializer; return; @@ -335,57 +363,352 @@ function findTransitionFunction(file: ts.SourceFile): ts.FunctionLike | undefine return transitionFunction; } -function isFunctionLike(node: ts.Node): node is ts.FunctionLike { +function isAnalyzableFunction(node: ts.Node): node is AnalyzableFunction { return ( ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) - || ts.isMethodDeclaration(node) ); } -function analyzeTransitionExpression( - expression: ts.Expression, - guard: string | undefined, - file: ts.SourceFile -): EdgeCandidate[] { - let current = expression; +function collectHelpers(fn: AnalyzableFunction): HelperMap { + const helpers: HelperMap = new Map(); + if (!fn.body || !ts.isBlock(fn.body)) { + return helpers; + } - while (ts.isParenthesizedExpression(current)) { - current = current.expression; + for (const statement of fn.body.statements) { + if ( + ts.isFunctionDeclaration(statement) + && statement.name + && statement.body + ) { + helpers.set(statement.name.text, statement); + continue; + } + + if (!ts.isVariableStatement(statement)) { + continue; + } + + for (const declaration of statement.declarationList.declarations) { + if (!ts.isIdentifier(declaration.name) || !declaration.initializer) { + continue; + } + + const initializer = unwrapParenthesized(declaration.initializer); + if ( + isAnalyzableFunction(initializer) + || ts.isObjectLiteralExpression(initializer) + || ts.isConditionalExpression(initializer) + ) { + helpers.set(declaration.name.text, initializer); + } + } + } + + return helpers; +} + +function analyzeStatements( + statements: ts.NodeArray, + guards: string[], + file: ts.SourceFile, + helpers: HelperMap +): BlockAnalysis { + const candidates: EdgeCandidate[] = []; + const warnings: string[] = []; + const fallthroughGuards = [...guards]; + + for (const statement of statements) { + const result = analyzeStatement(statement, fallthroughGuards, file, helpers); + candidates.push(...result.candidates); + warnings.push(...result.warnings); + + if (result.exits) { + return { candidates, warnings, exits: true }; + } + + if ( + ts.isIfStatement(statement) + && isReturnOnlyBranch(statement.thenStatement) + && !statement.elseStatement + ) { + fallthroughGuards.push(`!(${statement.expression.getText(file)})`); + } + } + + return { candidates, warnings, exits: false }; +} + +function analyzeStatement( + statement: ts.Statement, + guards: string[], + file: ts.SourceFile, + helpers: HelperMap +): BlockAnalysis { + if (ts.isReturnStatement(statement)) { + if (!statement.expression) { + return { + candidates: [], + warnings: ['Return statement has no transition object.'], + exits: true, + }; + } + + const result = analyzeTransitionExpression( + statement.expression, + guards, + file, + helpers + ); + + return { + candidates: result.candidates, + warnings: + result.candidates.length === 0 && result.warnings.length === 0 + ? [ + `Unsupported transition return expression: ${statement.expression.getText(file)}`, + ] + : result.warnings, + exits: true, + }; + } + + if (ts.isIfStatement(statement)) { + return analyzeIfStatement(statement, guards, file, helpers); + } + + if (ts.isSwitchStatement(statement)) { + return analyzeSwitchStatement(statement, guards, file, helpers); + } + + if ( + ts.isVariableStatement(statement) + || ts.isFunctionDeclaration(statement) + || ts.isEmptyStatement(statement) + ) { + return { candidates: [], warnings: [], exits: false }; + } + + return { + candidates: [], + warnings: [`Unsupported transition statement: ${statement.getText(file)}`], + exits: false, + }; +} + +function analyzeIfStatement( + statement: ts.IfStatement, + guards: string[], + file: ts.SourceFile, + helpers: HelperMap +): BlockAnalysis { + const condition = statement.expression.getText(file); + const thenResult = analyzeBranch( + statement.thenStatement, + [...guards, condition], + file, + helpers + ); + const elseResult = statement.elseStatement + ? analyzeBranch( + statement.elseStatement, + [...guards, `!(${condition})`], + file, + helpers + ) + : emptyBlockAnalysis(); + + return { + candidates: [...thenResult.candidates, ...elseResult.candidates], + warnings: [...thenResult.warnings, ...elseResult.warnings], + exits: thenResult.exits && !!statement.elseStatement && elseResult.exits, + }; +} + +function analyzeSwitchStatement( + statement: ts.SwitchStatement, + guards: string[], + file: ts.SourceFile, + helpers: HelperMap +): BlockAnalysis { + const candidates: EdgeCandidate[] = []; + const warnings: string[] = []; + const expression = statement.expression.getText(file); + const caseGuards: string[] = []; + let allClausesExit = statement.caseBlock.clauses.length > 0; + + for (const clause of statement.caseBlock.clauses) { + const clauseGuard = ts.isCaseClause(clause) + ? `${expression} === ${clause.expression.getText(file)}` + : caseGuards.length > 0 + ? caseGuards.map((guard) => `!(${guard})`).join(' && ') + : undefined; + + if (clauseGuard) { + caseGuards.push(clauseGuard); + } + + const result = analyzeStatements( + clause.statements, + clauseGuard ? [...guards, clauseGuard] : guards, + file, + helpers + ); + candidates.push(...result.candidates); + warnings.push(...result.warnings); + allClausesExit = allClausesExit && result.exits; + } + + return { + candidates, + warnings, + exits: allClausesExit, + }; +} + +function analyzeBranch( + statement: ts.Statement, + guards: string[], + file: ts.SourceFile, + helpers: HelperMap +): BlockAnalysis { + if (ts.isBlock(statement)) { + return analyzeStatements(statement.statements, guards, file, helpers); } + return analyzeStatement(statement, guards, file, helpers); +} + +function emptyBlockAnalysis(): BlockAnalysis { + return { + candidates: [], + warnings: [], + exits: false, + }; +} + +function isReturnOnlyBranch(statement: ts.Statement): boolean { + if (ts.isReturnStatement(statement)) { + return true; + } + + return ( + ts.isBlock(statement) + && statement.statements.length === 1 + && !!statement.statements[0] + && ts.isReturnStatement(statement.statements[0]) + ); +} + +function analyzeTransitionExpression( + expression: ts.Expression, + guards: string[], + file: ts.SourceFile, + helpers: HelperMap +): AnalysisResult { + const current = unwrapParenthesized(expression); + if (ts.isConditionalExpression(current)) { const condition = current.condition.getText(file); - return [ - ...analyzeTransitionExpression( + return mergeAnalysis([ + analyzeTransitionExpression( current.whenTrue, - combineGuards(guard, condition), - file + [...guards, condition], + file, + helpers ), - ...analyzeTransitionExpression( + analyzeTransitionExpression( current.whenFalse, - combineGuards(guard, `!(${condition})`), - file + [...guards, `!(${condition})`], + file, + helpers ), - ]; + ]); + } + + if ( + ts.isCallExpression(current) + && ts.isIdentifier(current.expression) + && current.arguments.length === 0 + ) { + const helper = helpers.get(current.expression.text); + if (!helper) { + return { + candidates: [], + warnings: [ + `Unsupported helper call: ${current.expression.text}() is not statically resolvable.`, + ], + }; + } + + return analyzeHelper(helper, guards, file, helpers); } const object = unwrapParenthesizedObject(current); const target = object ? getStringProperty(object, 'target') : undefined; if (!target) { - return []; + return { candidates: [], warnings: [] }; } - return [ - { + return { + candidates: [{ target, - guard, + guard: combineGuardList(guards), hasContext: object ? hasProperty(object, 'context') : false, hasInput: object ? hasProperty(object, 'input') : false, - }, - ]; + }], + warnings: [], + }; +} + +function analyzeHelper( + helper: AnalyzableFunction | ts.Expression, + guards: string[], + file: ts.SourceFile, + helpers: HelperMap +): AnalysisResult { + if (isAnalyzableFunction(helper)) { + if (ts.isArrowFunction(helper) && !ts.isBlock(helper.body)) { + return analyzeTransitionExpression(helper.body, guards, file, helpers); + } + + if (helper.body && ts.isBlock(helper.body)) { + const result = analyzeStatements(helper.body.statements, guards, file, helpers); + return { + candidates: result.candidates, + warnings: result.warnings, + }; + } + } + + if (ts.isExpression(helper)) { + return analyzeTransitionExpression(helper, guards, file, helpers); + } + + return { + candidates: [], + warnings: ['Unsupported helper body.'], + }; +} + +function mergeAnalysis(results: AnalysisResult[]): AnalysisResult { + return { + candidates: results.flatMap((result) => result.candidates), + warnings: results.flatMap((result) => result.warnings), + }; +} + +function unwrapParenthesized(expression: T): ts.Expression { + let current: ts.Expression = expression; + + while (ts.isParenthesizedExpression(current)) { + current = current.expression; + } + + return current; } function unwrapParenthesizedObject( @@ -430,50 +753,6 @@ function hasProperty(object: ts.ObjectLiteralExpression, name: string): boolean }); } -function findGuardForReturnLike( - returnNode: ts.Node, - ancestors: ts.Node[], - file: ts.SourceFile -): string | undefined { - for (let index = ancestors.length - 1; index >= 0; index -= 1) { - const ancestor = ancestors[index]; - if (!ancestor || !ts.isIfStatement(ancestor)) { - continue; - } - - if (containsNode(ancestor.thenStatement, returnNode)) { - return ancestor.expression.getText(file); - } - - if (ancestor.elseStatement && containsNode(ancestor.elseStatement, returnNode)) { - return `!(${ancestor.expression.getText(file)})`; - } - } - - return undefined; -} - -function containsNode(parent: ts.Node, child: ts.Node): boolean { - if (parent === child) { - return true; - } - - let found = false; - function visit(node: ts.Node) { - if (node === child) { - found = true; - return; - } - - if (!found) { - ts.forEachChild(node, visit); - } - } - - visit(parent); - return found; -} - function getEdgeLabel(event: string, guard: string | undefined): string { if (!guard) { return event; @@ -482,13 +761,24 @@ function getEdgeLabel(event: string, guard: string | undefined): string { return `${event} [${guard}]`; } -function combineGuards( - outer: string | undefined, - inner: string -): string { - if (!outer) { - return inner; +function combineGuardList(guards: string[]): string | undefined { + if (guards.length === 0) { + return undefined; } - return `(${outer}) && (${inner})`; + return guards + .map((guard) => guards.length === 1 ? guard : `(${guard})`) + .join(' && '); +} + +function formatWarnings( + state: string, + event: string, + warnings: string[] +): AgentGraphWarning[] { + return warnings.map((message) => ({ + state, + event, + message, + })); } diff --git a/src/langgraph-equivalents/error-retry.test.ts b/src/langgraph-equivalents/error-retry.test.ts new file mode 100644 index 0000000..a149c64 --- /dev/null +++ b/src/langgraph-equivalents/error-retry.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from 'vitest'; +import { createErrorRetryExample } from '../../examples/index.js'; + +test('retries failed invoke work through explicit internal error events', async () => { + let attempts = 0; + const machine = createErrorRetryExample(async ({ attempt }) => { + attempts += 1; + + if (attempt < 3) { + throw new Error(`temporary failure ${attempt}`); + } + + return { + answer: `answered on attempt ${attempt}`, + }; + }); + + const result = await machine.execute( + machine.getInitialState({ question: 'What is durable retry?' }) + ); + + expect(attempts).toBe(3); + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + answer: 'answered on attempt 3', + attempts: 3, + errors: ['temporary failure 1', 'temporary failure 2'], + }); + } +}); + +test('fails after the configured retry budget is exhausted', async () => { + const machine = createErrorRetryExample(async ({ attempt }) => { + throw new Error(`still down ${attempt}`); + }, 2); + + const result = await machine.execute( + machine.getInitialState({ question: 'Will this recover?' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + answer: null, + attempts: 2, + errors: ['still down 1', 'still down 2'], + }); + } +}); diff --git a/src/xstate/index.test.ts b/src/xstate/index.test.ts index 8e7fe97..16609a4 100644 --- a/src/xstate/index.test.ts +++ b/src/xstate/index.test.ts @@ -73,6 +73,7 @@ test('exports a serializable XState config for visualization', () => { }, { target: 'done', + guard: { type: '!(event.count > 0)' }, meta: { agent: { event: 'submit', @@ -90,7 +91,7 @@ test('exports a serializable XState config for visualization', () => { target: 'done', meta: { agent: { - event: 'done', + event: 'done.invoke.working', }, }, }, diff --git a/src/xstate/index.ts b/src/xstate/index.ts index 6c112c2..b9a1266 100644 --- a/src/xstate/index.ts +++ b/src/xstate/index.ts @@ -81,7 +81,7 @@ export function toXStateMachine(machine: AgentMachine): XStateMachineConfig { const regularEdges = graph.edges.filter((edge) => edge.sourceId === stateId - && edge.data.event !== 'done' + && edge.data.source !== 'invoke.done' ); for (const [event, edges] of groupEdgesByEvent(regularEdges)) { @@ -97,7 +97,7 @@ export function toXStateMachine(machine: AgentMachine): XStateMachineConfig { if (stateConfig.onDone) { const doneEdges = graph.edges.filter((edge) => edge.sourceId === stateId - && edge.data.event === 'done' + && edge.data.source === 'invoke.done' ); const formattedDone = formatTransitions(doneEdges); From 7b5eea0d4528696cb03ee071ce3d970fd56c0c7c Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 23 Apr 2026 11:10:38 -0400 Subject: [PATCH 21/34] feat: surface conversion warnings and durable retry restore --- readme.md | 2 +- scripts/agent-convert.ts | 17 ++++- src/agent-convert-cli.test.ts | 16 ++++- src/fixtures/converter-machine.ts | 25 +++++++ src/langgraph-equivalents/error-retry.test.ts | 69 ++++++++++++++++++- 5 files changed, 125 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 1d7d8a6..9cefd31 100644 --- a/readme.md +++ b/readme.md @@ -15,7 +15,7 @@ The examples in [`examples/`](/Users/davidkpiano/Code/agent/examples) are intent Run them with `node --import tsx examples/.ts`. -Convert a machine file to diagram output with `pnpm agent:convert --format mermaid` or `pnpm agent:convert --format xstate`. +Convert a machine file to diagram output with `pnpm agent:convert --format mermaid` or `pnpm agent:convert --format xstate`. Static analysis warnings are printed to stderr. Each example demonstrates one concept: diff --git a/scripts/agent-convert.ts b/scripts/agent-convert.ts index 3849d4f..bcfc1a3 100644 --- a/scripts/agent-convert.ts +++ b/scripts/agent-convert.ts @@ -1,7 +1,7 @@ import { writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { toMermaid } from '../src/graph/index.js'; +import { toGraph, toMermaid, type AgentGraphWarning } from '../src/graph/index.js'; import type { AgentMachine } from '../src/index.js'; import { toXStateMachine } from '../src/xstate/index.js'; @@ -25,6 +25,9 @@ async function main() { } const machine = await loadMachine(options); + const graph = toGraph(machine); + writeWarnings(graph.data?.warnings ?? []); + const output = options.format === 'mermaid' ? toMermaid(machine) @@ -38,6 +41,18 @@ async function main() { process.stdout.write(output.endsWith('\n') ? output : `${output}\n`); } +function writeWarnings(warnings: AgentGraphWarning[]) { + for (const warning of warnings) { + process.stderr.write( + [ + '[agent:convert]', + `${warning.state} on ${warning.event}:`, + warning.message, + ].join(' ') + '\n' + ); + } +} + function parseArgs(args: string[]): CliOptions { const options: CliOptions = { format: 'mermaid', diff --git a/src/agent-convert-cli.test.ts b/src/agent-convert-cli.test.ts index bc573b7..6812250 100644 --- a/src/agent-convert-cli.test.ts +++ b/src/agent-convert-cli.test.ts @@ -53,10 +53,24 @@ test('agent:convert writes Mermaid and XState output from machine files', async id: string; }; expect(factoryXState.id).toBe('factory-converter-machine'); + + const warningFile = join(tmp, 'warning.mmd'); + const warningResult = await runConvert([ + fixture, + '--export', + 'warningMachine', + '--format', + 'mermaid', + '--out', + warningFile, + ]); + expect(warningResult.stderr).toContain( + '[agent:convert] idle on go: Unsupported helper call: unknownTransition() is not statically resolvable.' + ); }); async function runConvert(args: string[]) { - await execFileAsync('pnpm', ['agent:convert', ...args], { + return execFileAsync('pnpm', ['agent:convert', ...args], { cwd: resolve('.'), }); } diff --git a/src/fixtures/converter-machine.ts b/src/fixtures/converter-machine.ts index c94fea0..47cfb05 100644 --- a/src/fixtures/converter-machine.ts +++ b/src/fixtures/converter-machine.ts @@ -1,8 +1,33 @@ import { z } from 'zod'; import { createAgentMachine } from '../index.js'; +declare function unknownTransition(): { target: 'done' }; + export const namedMachine = createFixtureMachine('named-converter-machine'); +export const warningMachine = createAgentMachine({ + id: 'warning-converter-machine', + schemas: { + events: { + go: z.object({ + type: z.literal('go'), + }), + }, + }, + context: () => ({}), + initial: 'idle', + states: { + idle: { + on: { + go: () => unknownTransition(), + }, + }, + done: { + type: 'final', + }, + }, +}); + export default createFixtureMachine('default-converter-machine'); export function createFixtureMachine(id = 'factory-converter-machine') { diff --git a/src/langgraph-equivalents/error-retry.test.ts b/src/langgraph-equivalents/error-retry.test.ts index a149c64..8472fd0 100644 --- a/src/langgraph-equivalents/error-retry.test.ts +++ b/src/langgraph-equivalents/error-retry.test.ts @@ -1,5 +1,6 @@ -import { expect, test } from 'vitest'; +import { expect, test, vi } from 'vitest'; import { createErrorRetryExample } from '../../examples/index.js'; +import { createMemoryRunStore, restoreSession } from '../index.js'; test('retries failed invoke work through explicit internal error events', async () => { let attempts = 0; @@ -48,3 +49,69 @@ test('fails after the configured retry budget is exhausted', async () => { }); } }); + +test('restores a durable retry snapshot and continues from the next attempt', async () => { + const sessionId = 'durable-retry-session'; + const machine = createErrorRetryExample(async ({ attempt }) => ({ + answer: `restored attempt ${attempt}`, + })); + const store = createMemoryRunStore(); + const input = { question: 'Can retry survive restore?' }; + const initial = machine.getInitialState(input); + const retryState = machine.transition(initial, { + type: 'xstate.error.invoke.answering', + error: { message: 'network reset' }, + at: 2, + }); + + await store.append(sessionId, { + type: 'xstate.init', + input, + at: 1, + }); + await store.append(sessionId, { + type: 'xstate.error.invoke.answering', + error: { message: 'network reset' }, + at: 2, + }); + await store.saveSnapshot({ + sessionId, + afterSequence: 2, + snapshot: { + value: retryState.value, + context: retryState.context, + status: retryState.status, + input: retryState.input, + createdAt: 1, + sessionId, + }, + createdAt: 2, + }); + + const restored = await restoreSession(machine, { + sessionId, + store, + }); + + await vi.waitFor(() => { + expect(restored.getSnapshot().status).toBe('done'); + }); + + expect(restored.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + context: { + question: 'Can retry survive restore?', + answer: 'restored attempt 2', + attempt: 2, + errors: ['network reset'], + }, + output: { + answer: 'restored attempt 2', + attempts: 2, + errors: ['network reset'], + }, + }) + ); +}); From 118eadf4b8f28c95ac067967906961ad0a1a1ffe Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 23 Apr 2026 11:55:25 -0400 Subject: [PATCH 22/34] feat: clarify xstate visualization and conditional subflows --- examples/conditional-subflow.ts | 206 ++++++++++++++++++ examples/index.ts | 1 + readme.md | 1 + scripts/agent-convert.ts | 4 +- src/agent-convert-cli.test.ts | 8 + src/examples.test.ts | 27 +++ .../conditional-subflow.test.ts | 51 +++++ src/xstate/index.test.ts | 12 +- src/xstate/index.ts | 27 ++- 9 files changed, 330 insertions(+), 7 deletions(-) create mode 100644 examples/conditional-subflow.ts create mode 100644 src/langgraph-equivalents/conditional-subflow.test.ts diff --git a/examples/conditional-subflow.ts b/examples/conditional-subflow.ts new file mode 100644 index 0000000..52793f0 --- /dev/null +++ b/examples/conditional-subflow.ts @@ -0,0 +1,206 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const modeSchema = z.enum(['research', 'draft']); + +const researchSchema = z.object({ + bullets: z.array(z.string()), +}); + +const draftSchema = z.object({ + draft: z.string(), +}); + +export function createConditionalSubflowExample( + options: { + research?: (topic: string) => Promise>; + draft?: (args: { + topic: string; + bullets: string[]; + }) => Promise>; + } = {} +) { + const researchMachine = createAgentMachine({ + id: 'conditional-subflow-research', + schemas: { + input: z.object({ topic: z.string() }), + output: researchSchema, + }, + context: (input) => ({ + topic: input.topic, + bullets: [] as string[], + }), + initial: 'researching', + states: { + researching: { + resultSchema: researchSchema, + invoke: async ({ context }) => + (options.research + ?? ((topic) => + generateExampleObject({ + schema: researchSchema, + system: 'Return concise research bullets.', + prompt: `Return 2 to 4 bullets about ${topic}.`, + })))(context.topic), + onDone: ({ result }) => ({ + target: 'done', + context: { bullets: result.bullets }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ bullets: context.bullets }), + }, + }, + }); + + const draftMachine = createAgentMachine({ + id: 'conditional-subflow-draft', + schemas: { + input: z.object({ + topic: z.string(), + bullets: z.array(z.string()), + }), + output: draftSchema, + }, + context: (input) => ({ + topic: input.topic, + bullets: input.bullets, + draft: null as string | null, + }), + initial: 'drafting', + states: { + drafting: { + resultSchema: draftSchema, + invoke: async ({ context }) => + (options.draft + ?? (({ topic, bullets }) => + generateExampleObject({ + schema: draftSchema, + system: 'Turn bullets into a short draft.', + prompt: [ + `Topic: ${topic}`, + 'Bullets:', + ...bullets.map((bullet) => `- ${bullet}`), + ].join('\n'), + })))({ + topic: context.topic, + bullets: context.bullets, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { draft: result.draft }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ draft: context.draft ?? '' }), + }, + }, + }); + + return createAgentMachine({ + id: 'conditional-subflow-example', + schemas: { + input: z.object({ + topic: z.string(), + mode: modeSchema, + bullets: z.array(z.string()).optional(), + }), + output: z.object({ + mode: modeSchema, + bullets: z.array(z.string()), + draft: z.string().nullable(), + }), + }, + context: (input) => ({ + topic: input.topic, + mode: input.mode, + bullets: input.bullets ?? [], + draft: null as string | null, + }), + initial: ({ context }) => + context.mode === 'research' + ? { target: 'researching' } + : { target: 'drafting', input: { bullets: context.bullets } }, + states: { + researching: { + resultSchema: researchSchema, + invoke: async ({ context }) => { + const result = await researchMachine.execute( + researchMachine.getInitialState({ topic: context.topic }) + ); + + if (result.status !== 'done') { + throw new Error('Research subflow did not finish'); + } + + return result.output; + }, + onDone: ({ result }) => ({ + target: 'done', + context: { bullets: result.bullets }, + }), + }, + drafting: { + inputSchema: z.object({ + bullets: z.array(z.string()), + }), + resultSchema: draftSchema, + invoke: async ({ context, input }) => { + const result = await draftMachine.execute( + draftMachine.getInitialState({ + topic: context.topic, + bullets: input.bullets, + }) + ); + + if (result.status !== 'done') { + throw new Error('Draft subflow did not finish'); + } + + return result.output; + }, + onDone: ({ result }) => ({ + target: 'done', + context: { draft: result.draft }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + mode: context.mode, + bullets: context.bullets, + draft: context.draft, + }), + }, + }, + }); +} + +async function main() { + try { + const topic = await prompt('Topic'); + const modeInput = await prompt('Mode (research/draft)'); + const mode = modeInput === 'draft' ? 'draft' : 'research'; + const machine = createConditionalSubflowExample(); + const result = await machine.execute( + machine.getInitialState({ topic, mode }) + ); + + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/index.ts b/examples/index.ts index 4205092..c652d41 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -5,6 +5,7 @@ export { createDecideExample } from './decide.js'; export { createClassifyExample } from './classify.js'; export { createAdapterExample } from './adapter.js'; export { createChatbotExample } from './chatbot.js'; +export { createConditionalSubflowExample } from './conditional-subflow.js'; export { AgentSessionDurableObject, createDurableObjectRunStore, diff --git a/readme.md b/readme.md index 9cefd31..32ea42e 100644 --- a/readme.md +++ b/readme.md @@ -22,6 +22,7 @@ Each example demonstrates one concept: - [`examples/simple.ts`](/Users/davidkpiano/Code/agent/examples/simple.ts): the smallest `createAgentMachine(...)` flow with an `invoke` state that makes a real LLM call - [`examples/hitl.ts`](/Users/davidkpiano/Code/agent/examples/hitl.ts): a human-in-the-loop machine that pauses in a pending state, accepts typed events, and drafts with an LLM after approval - [`examples/error-retry.ts`](/Users/davidkpiano/Code/agent/examples/error-retry.ts): retrying transient invoke failures through explicit internal error events +- [`examples/conditional-subflow.ts`](/Users/davidkpiano/Code/agent/examples/conditional-subflow.ts): routing directly into one of multiple child subflows from parent input - [`examples/decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts): choosing the next branch with structured branch-specific payloads - [`examples/classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts): routing into a fixed category set when you only need a label - [`examples/adapter.ts`](/Users/davidkpiano/Code/agent/examples/adapter.ts): plugging in a custom adapter that still uses a real OpenAI model under the hood diff --git a/scripts/agent-convert.ts b/scripts/agent-convert.ts index bcfc1a3..ef5773d 100644 --- a/scripts/agent-convert.ts +++ b/scripts/agent-convert.ts @@ -3,7 +3,7 @@ import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { toGraph, toMermaid, type AgentGraphWarning } from '../src/graph/index.js'; import type { AgentMachine } from '../src/index.js'; -import { toXStateMachine } from '../src/xstate/index.js'; +import { toXStateVisualization } from '../src/xstate/index.js'; type Format = 'mermaid' | 'xstate'; @@ -31,7 +31,7 @@ async function main() { const output = options.format === 'mermaid' ? toMermaid(machine) - : `${JSON.stringify(toXStateMachine(machine), null, 2)}\n`; + : `${JSON.stringify(toXStateVisualization(machine), null, 2)}\n`; if (options.outFile) { await writeFile(resolve(options.outFile), output); diff --git a/src/agent-convert-cli.test.ts b/src/agent-convert-cli.test.ts index 6812250..081108b 100644 --- a/src/agent-convert-cli.test.ts +++ b/src/agent-convert-cli.test.ts @@ -37,6 +37,14 @@ test('agent:convert writes Mermaid and XState output from machine files', async }; expect(namedXState.id).toBe('named-converter-machine'); expect(namedXState.initial).toBe('idle'); + expect(namedXState).toMatchObject({ + meta: { + agent: { + format: '@statelyai/agent/xstate-visualization', + runnable: false, + }, + }, + }); expect(Object.keys(namedXState.states)).toEqual(['idle', 'rejected', 'done']); const factoryXStateFile = join(tmp, 'factory.json'); diff --git a/src/examples.test.ts b/src/examples.test.ts index 5650652..da10335 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -9,6 +9,7 @@ import { createAdapterExample, createBranchingExample, createClassifyExample, + createConditionalSubflowExample, createCustomerServiceSimExample, createDecideExample, createEmailExample, @@ -46,6 +47,7 @@ describe('curated examples', () => { expect(existsSync(resolve(examplesDir, 'branching.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'chatbot.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'cloudflare-durable-object.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'conditional-subflow.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'customer-service-sim.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'email.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'error-retry.ts'))).toBe(true); @@ -249,6 +251,31 @@ describe('curated examples', () => { } }); + test('conditional subflow example routes directly into the requested child flow', async () => { + const machine = createConditionalSubflowExample({ + draft: async ({ topic, bullets }) => ({ + draft: `${topic}: ${bullets.join(', ')}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ + topic: 'state machines', + mode: 'draft', + bullets: ['deterministic', 'resumable'], + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + mode: 'draft', + bullets: ['deterministic', 'resumable'], + draft: 'state machines: deterministic, resumable', + }); + } + }); + test('branching example fans out plain async work and summarizes it', async () => { const machine = createBranchingExample({ analyzeDocs: async () => 'docs', diff --git a/src/langgraph-equivalents/conditional-subflow.test.ts b/src/langgraph-equivalents/conditional-subflow.test.ts new file mode 100644 index 0000000..7b0a155 --- /dev/null +++ b/src/langgraph-equivalents/conditional-subflow.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from 'vitest'; +import { createConditionalSubflowExample } from '../../examples/index.js'; + +test('conditionally enters the research subflow from parent input', async () => { + const machine = createConditionalSubflowExample({ + research: async (topic) => ({ + bullets: [`${topic}:fact-1`, `${topic}:fact-2`], + }), + }); + + const result = await machine.execute( + machine.getInitialState({ + topic: 'agent graphs', + mode: 'research', + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + mode: 'research', + bullets: ['agent graphs:fact-1', 'agent graphs:fact-2'], + draft: null, + }); + } +}); + +test('conditionally enters the draft subflow with parent-provided input', async () => { + const machine = createConditionalSubflowExample({ + draft: async ({ topic, bullets }) => ({ + draft: `${topic}: ${bullets.join(' / ')}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ + topic: 'agent graphs', + mode: 'draft', + bullets: ['known fact', 'second fact'], + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + mode: 'draft', + bullets: ['known fact', 'second fact'], + draft: 'agent graphs: known fact / second fact', + }); + } +}); diff --git a/src/xstate/index.test.ts b/src/xstate/index.test.ts index 16609a4..5b00afe 100644 --- a/src/xstate/index.test.ts +++ b/src/xstate/index.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'vitest'; import { z } from 'zod'; import { createAgentMachine } from '../index.js'; -import { toXStateMachine } from './index.js'; +import { toXStateMachine, toXStateVisualization } from './index.js'; test('exports a serializable XState config for visualization', () => { const machine = createAgentMachine({ @@ -50,9 +50,16 @@ test('exports a serializable XState config for visualization', () => { }, }); - expect(toXStateMachine(machine)).toEqual({ + expect(toXStateVisualization(machine)).toEqual({ id: 'xstate-export', initial: 'idle', + meta: { + agent: { + format: '@statelyai/agent/xstate-visualization', + runnable: false, + note: 'Generated for visualization. Runtime semantics remain in the agent machine.', + }, + }, states: { idle: { on: { @@ -107,4 +114,5 @@ test('exports a serializable XState config for visualization', () => { }, }, }); + expect(toXStateMachine(machine)).toEqual(toXStateVisualization(machine)); }); diff --git a/src/xstate/index.ts b/src/xstate/index.ts index b9a1266..14311df 100644 --- a/src/xstate/index.ts +++ b/src/xstate/index.ts @@ -4,6 +4,13 @@ import type { AgentMachine, MachineConfig, StateConfig } from '../types.js'; export interface XStateMachineConfig { id: string; initial?: string; + meta: { + agent: { + format: '@statelyai/agent/xstate-visualization'; + runnable: false; + note: string; + }; + }; states: Record; } @@ -46,10 +53,11 @@ type InternalMachine = AgentMachine & { }; /** - * Convert an agent machine to a serializable XState machine config for - * visualization. Runtime behavior is still driven by the agent machine. + * Convert an agent machine to a serializable XState-like machine config for + * visualization. Guards, actions, and invokes are symbolic metadata, so this + * object is not a runnable replacement for the agent machine. */ -export function toXStateMachine(machine: AgentMachine): XStateMachineConfig { +export function toXStateVisualization(machine: AgentMachine): XStateMachineConfig { const config = (machine as InternalMachine).__config; if (!config) { throw new Error('Machine config metadata is unavailable for XState export'); @@ -122,10 +130,23 @@ export function toXStateMachine(machine: AgentMachine): XStateMachineConfig { ...(typeof graph.initialNodeId === 'string' ? { initial: graph.initialNodeId } : {}), + meta: { + agent: { + format: '@statelyai/agent/xstate-visualization', + runnable: false, + note: 'Generated for visualization. Runtime semantics remain in the agent machine.', + }, + }, states, }; } +/** + * @deprecated Use `toXStateVisualization(...)` to make the visualization-only + * contract explicit. + */ +export const toXStateMachine = toXStateVisualization; + function groupEdgesByEvent( edges: AgentGraph['edges'] ): Map { From 5672cc33057bc41a40457ebd3fc553c3a5c53932 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 24 Apr 2026 07:32:46 -0400 Subject: [PATCH 23/34] feat: add durable workflow parity and node baseline --- .github/actions/ci-setup/action.yml | 2 +- .github/workflows/release.yml | 2 +- .node-version | 1 + .nvmrc | 1 + examples/cloudflare-durable-network.ts | 105 ++++++ examples/index.ts | 5 + examples/persistent-multi-agent-network.ts | 111 ++++++ examples/tool-calling.ts | 44 ++- package.json | 3 + readme.md | 6 +- scripts/agent-convert.ts | 6 +- src/examples.test.ts | 156 +++++++- src/graph/index.test.ts | 61 ++- src/graph/index.ts | 346 +++++++++++++++--- .../persistent-multi-agent-network.test.ts | 63 ++++ .../tool-calling.test.ts | 29 +- 16 files changed, 867 insertions(+), 74 deletions(-) create mode 100644 .node-version create mode 100644 .nvmrc create mode 100644 examples/cloudflare-durable-network.ts create mode 100644 examples/persistent-multi-agent-network.ts create mode 100644 src/langgraph-equivalents/persistent-multi-agent-network.test.ts diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml index ddc9555..51fcd0d 100644 --- a/.github/actions/ci-setup/action.yml +++ b/.github/actions/ci-setup/action.yml @@ -6,7 +6,7 @@ runs: - uses: pnpm/action-setup@v2 - uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.18.0 - name: install pnpm run: npm i pnpm@latest -g diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57c57f8..b6f87ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: - name: setup node.js uses: actions/setup-node@v3 with: - node-version: 20 + node-version: 22.18.0 - name: install pnpm run: npm i pnpm@latest -g - name: setup pnpm config diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..91d5f6f --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22.18.0 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..91d5f6f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.18.0 diff --git a/examples/cloudflare-durable-network.ts b/examples/cloudflare-durable-network.ts new file mode 100644 index 0000000..bca3d33 --- /dev/null +++ b/examples/cloudflare-durable-network.ts @@ -0,0 +1,105 @@ +import { + restoreSession, + startSession, + type AgentSnapshot, +} from '../src/index.js'; +import { + createDurableObjectRunStore, + type DurableObjectStateLike, +} from './cloudflare-durable-object.js'; +import { createMultiAgentNetworkExample } from './multi-agent-network.js'; + +export class AgentNetworkDurableObject { + private readonly store; + + constructor(private readonly state: DurableObjectStateLike) { + this.store = createDurableObjectRunStore(state.storage); + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const machine = createMultiAgentNetworkExample({ + adapter: { + decide: async ({ prompt }) => { + if (!prompt.includes('Notes: none yet')) { + if (!prompt.includes('Current draft: none yet')) { + return { choice: 'finalize', data: {} }; + } + + return { + choice: 'write', + data: { angle: 'turn the current notes into a concise summary' }, + }; + } + + return { + choice: 'research', + data: { focus: 'collect the strongest supporting facts' }, + }; + }, + }, + research: async ({ topic, focus }) => ({ + notes: [`${topic}:${focus}:1`, `${topic}:${focus}:2`], + }), + write: async ({ topic, notes, angle }) => ({ + draft: `${topic} | ${angle} | ${notes.join(' / ')}`, + }), + }); + + if (request.method === 'POST' && url.pathname === '/start') { + const body = await request.json() as { topic: string }; + const run = await startSession(machine, { + store: this.store, + input: { topic: body.topic }, + }); + + return Response.json({ + sessionId: run.sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'POST' && url.pathname === '/resume') { + const sessionId = requiredSessionId(url); + const run = await restoreSession(machine, { + sessionId, + store: this.store, + }); + const snapshot = await waitForTerminalSnapshot(run.getSnapshot, 1000); + + return Response.json({ + sessionId, + snapshot, + }); + } + + return new Response('Not found', { status: 404 }); + } +} + +async function waitForTerminalSnapshot( + getSnapshot: () => AgentSnapshot, + timeoutMs: number +) { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + const snapshot = getSnapshot(); + if (snapshot.status === 'done' || snapshot.status === 'error') { + return snapshot; + } + + await new Promise((resolve) => setTimeout(resolve, 10)); + } + + return getSnapshot(); +} + +function requiredSessionId(url: URL): string { + const sessionId = url.searchParams.get('sessionId'); + if (!sessionId) { + throw new Error('Missing sessionId'); + } + + return sessionId; +} diff --git a/examples/index.ts b/examples/index.ts index c652d41..d43e598 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -12,6 +12,7 @@ export { type DurableObjectStateLike, type DurableObjectStorageLike, } from './cloudflare-durable-object.js'; +export { AgentNetworkDurableObject } from './cloudflare-durable-network.js'; export { createCustomerServiceSimExample } from './customer-service-sim.js'; export { createEmailExample } from './email.js'; export { createErrorRetryExample } from './error-retry.js'; @@ -22,6 +23,10 @@ export { createMultiAgentNetworkExample } from './multi-agent-network.js'; export { createNewspaperExample } from './newspaper.js'; export { createPlanAndExecuteExample } from './plan-and-execute.js'; export { createPersistenceExample, runPersistenceExample } from './persistence.js'; +export { + createPersistentMultiAgentNetworkExample, + runPersistentMultiAgentNetworkExample, +} from './persistent-multi-agent-network.js'; export { createRaffleExample } from './raffle.js'; export { createReactAgentExample } from './react-agent.js'; export { diff --git a/examples/persistent-multi-agent-network.ts b/examples/persistent-multi-agent-network.ts new file mode 100644 index 0000000..f3f12b6 --- /dev/null +++ b/examples/persistent-multi-agent-network.ts @@ -0,0 +1,111 @@ +import { + createMemoryRunStore, + restoreSession, + startSession, + type PersistedSnapshot, +} from '../src/index.js'; +import { createMultiAgentNetworkExample } from './multi-agent-network.js'; + +type NetworkOptions = Parameters[0]; + +export function createPersistentMultiAgentNetworkExample( + options: NetworkOptions = {} +) { + return createMultiAgentNetworkExample(options); +} + +export async function runPersistentMultiAgentNetworkExample( + input: { topic: string }, + options: NetworkOptions = {} +) { + const machine = createPersistentMultiAgentNetworkExample(options); + const baseStore = createMemoryRunStore(); + let persistedHandoffSnapshot = false; + + const store = { + append: baseStore.append, + loadEvents: baseStore.loadEvents, + loadLatestSnapshot: baseStore.loadLatestSnapshot, + async saveSnapshot( + snapshot: PersistedSnapshot + ) { + const handoffs = + ((snapshot.snapshot.context as { handoffs?: string[] }).handoffs ?? []); + + if (!persistedHandoffSnapshot && handoffs.length === 1) { + persistedHandoffSnapshot = true; + await baseStore.saveSnapshot(snapshot); + return; + } + + if (!persistedHandoffSnapshot && handoffs.length === 0) { + await baseStore.saveSnapshot(snapshot); + } + }, + }; + + const liveRun = await startSession(machine, { + store, + input, + }); + + await waitForTerminal(() => liveRun.getSnapshot().status); + + const restoredRun = await restoreSession(machine, { + sessionId: liveRun.sessionId, + store, + }); + + await waitForMatch( + () => restoredRun.getSnapshot(), + () => liveRun.getSnapshot() + ); + + return { + sessionId: liveRun.sessionId, + liveSnapshot: liveRun.getSnapshot(), + restoredSnapshot: restoredRun.getSnapshot(), + }; +} + +function expectTerminal(status: string) { + if (status !== 'done' && status !== 'error') { + throw new Error(`Snapshot is not terminal yet: ${status}`); + } +} + +async function waitForTerminal( + getStatus: () => string, + timeoutMs = 1000 +) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + expectTerminal(getStatus()); + return; + } catch {} + + await new Promise((resolve) => setTimeout(resolve, 10)); + } + + expectTerminal(getStatus()); +} + +async function waitForMatch( + getActual: () => T, + getExpected: () => T, + timeoutMs = 1000 +) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (JSON.stringify(getActual()) === JSON.stringify(getExpected())) { + return; + } + + await new Promise((resolve) => setTimeout(resolve, 10)); + } + + if (JSON.stringify(getActual()) !== JSON.stringify(getExpected())) { + throw new Error('Snapshots did not converge before timeout.'); + } +} diff --git a/examples/tool-calling.ts b/examples/tool-calling.ts index 8472aae..719fcfb 100644 --- a/examples/tool-calling.ts +++ b/examples/tool-calling.ts @@ -15,15 +15,37 @@ const forecastSchema = z.object({ forecast: z.string(), }); +const toolProgressSchema = z.object({ + toolName: z.string(), + message: z.string(), + step: z.number().int().min(1), +}); + export function createToolCallingExample( - getWeather: (city: string) => Promise> = async ( - city - ) => - generateExampleObject({ + getWeather: ( + city: string, + emitProgress: (event: z.infer) => void + ) => Promise> = async ( + city, + emitProgress + ) => { + emitProgress({ + toolName: 'getWeather', + message: `Looking up current conditions for ${city}.`, + step: 1, + }); + emitProgress({ + toolName: 'getWeather', + message: `Formatting the forecast for ${city}.`, + step: 2, + }); + + return generateExampleObject({ schema: forecastSchema, system: 'You generate plausible demo weather forecasts.', prompt: `Return a short weather forecast for ${city}.`, - }) + }); + } ) { return createAgentMachine({ id: 'tool-calling-example', @@ -35,6 +57,7 @@ export function createToolCallingExample( toolName: z.string(), input: z.object({ city: z.string() }), }), + toolProgress: toolProgressSchema, toolResult: z.object({ toolName: z.string(), output: forecastSchema, @@ -56,7 +79,12 @@ export function createToolCallingExample( input: { city: context.city }, }); - const output = await getWeather(context.city); + const output = await getWeather(context.city, (progress) => { + enq.emit({ + type: 'toolProgress', + ...progress, + }); + }); enq.emit({ type: 'toolResult', @@ -92,6 +120,10 @@ async function main() { console.log(`Calling ${event.toolName}(${event.input.city})`); }); + run.on('toolProgress', (event) => { + console.log(`${event.toolName} [${event.step}] ${event.message}`); + }); + run.on('toolResult', (event) => { console.log(`${event.toolName} -> ${event.output.forecast}`); }); diff --git a/package.json b/package.json index 990cbd6..1db8271 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,9 @@ ], "author": "David Khourshid ", "license": "MIT", + "engines": { + "node": ">=22.18.0" + }, "devDependencies": { "@ai-sdk/openai": "^3.0.25", "@changesets/changelog-github": "^0.5.0", diff --git a/readme.md b/readme.md index 32ea42e..33c0838 100644 --- a/readme.md +++ b/readme.md @@ -15,7 +15,7 @@ The examples in [`examples/`](/Users/davidkpiano/Code/agent/examples) are intent Run them with `node --import tsx examples/.ts`. -Convert a machine file to diagram output with `pnpm agent:convert --format mermaid` or `pnpm agent:convert --format xstate`. Static analysis warnings are printed to stderr. +Convert a machine file to diagram output with `pnpm agent:convert --format mermaid` or `pnpm agent:convert --format xstate`. Static analysis warnings are printed to stderr. For programmatic access, use `analyzeGraph(...)` from `@statelyai/agent/graph`; warnings are returned explicitly instead of being hidden in graph metadata. Each example demonstrates one concept: @@ -23,6 +23,10 @@ Each example demonstrates one concept: - [`examples/hitl.ts`](/Users/davidkpiano/Code/agent/examples/hitl.ts): a human-in-the-loop machine that pauses in a pending state, accepts typed events, and drafts with an LLM after approval - [`examples/error-retry.ts`](/Users/davidkpiano/Code/agent/examples/error-retry.ts): retrying transient invoke failures through explicit internal error events - [`examples/conditional-subflow.ts`](/Users/davidkpiano/Code/agent/examples/conditional-subflow.ts): routing directly into one of multiple child subflows from parent input +- [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts): tool invocation with emitted `toolCall`, incremental `toolProgress`, and final `toolResult` events +- [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts): persisting snapshots and restoring a session in plain runtime code +- [`examples/persistent-multi-agent-network.ts`](/Users/davidkpiano/Code/agent/examples/persistent-multi-agent-network.ts): durable multi-agent handoffs that restore from a persisted mid-network snapshot +- [`examples/cloudflare-durable-network.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-durable-network.ts): a Cloudflare Durable Object runner that starts and resumes a persisted multi-agent network - [`examples/decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts): choosing the next branch with structured branch-specific payloads - [`examples/classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts): routing into a fixed category set when you only need a label - [`examples/adapter.ts`](/Users/davidkpiano/Code/agent/examples/adapter.ts): plugging in a custom adapter that still uses a real OpenAI model under the hood diff --git a/scripts/agent-convert.ts b/scripts/agent-convert.ts index ef5773d..4142c29 100644 --- a/scripts/agent-convert.ts +++ b/scripts/agent-convert.ts @@ -1,7 +1,7 @@ import { writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { toGraph, toMermaid, type AgentGraphWarning } from '../src/graph/index.js'; +import { analyzeGraph, toMermaid, type AgentGraphWarning } from '../src/graph/index.js'; import type { AgentMachine } from '../src/index.js'; import { toXStateVisualization } from '../src/xstate/index.js'; @@ -25,8 +25,8 @@ async function main() { } const machine = await loadMachine(options); - const graph = toGraph(machine); - writeWarnings(graph.data?.warnings ?? []); + const analysis = analyzeGraph(machine); + writeWarnings(analysis.warnings); const output = options.format === 'mermaid' diff --git a/src/examples.test.ts b/src/examples.test.ts index da10335..65512aa 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -5,6 +5,7 @@ import { resolve } from 'node:path'; import { createChatbotExample, + AgentNetworkDurableObject, createDurableObjectRunStore, createAdapterExample, createBranchingExample, @@ -21,6 +22,7 @@ import { createMultiAgentNetworkExample, createNewspaperExample, runPersistenceExample, + runPersistentMultiAgentNetworkExample, createPlanAndExecuteExample, createRaffleExample, createReactAgentExample, @@ -47,6 +49,7 @@ describe('curated examples', () => { expect(existsSync(resolve(examplesDir, 'branching.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'chatbot.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'cloudflare-durable-object.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'cloudflare-durable-network.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'conditional-subflow.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'customer-service-sim.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'email.ts'))).toBe(true); @@ -57,6 +60,7 @@ describe('curated examples', () => { expect(existsSync(resolve(examplesDir, 'multi-agent-network.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'newspaper.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'persistence.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'persistent-multi-agent-network.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'plan-and-execute.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'raffle.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'react-agent.ts'))).toBe(true); @@ -153,6 +157,70 @@ describe('curated examples', () => { ); }); + test('cloudflare durable network example restores and settles a network run', async () => { + const storage = new Map(); + const firstInstance = new AgentNetworkDurableObject({ + storage: { + async get(key) { + return storage.get(key) as never; + }, + async put(key, value) { + storage.set(key, value); + }, + }, + }); + + const startResponse = await firstInstance.fetch( + new Request('https://example.com/start', { + method: 'POST', + body: JSON.stringify({ topic: 'durable networks' }), + }) + ); + const started = await startResponse.json() as { + sessionId: string; + snapshot: { status: string }; + }; + + const resumedInstance = new AgentNetworkDurableObject({ + storage: { + async get(key) { + return storage.get(key) as never; + }, + async put(key, value) { + storage.set(key, value); + }, + }, + }); + const resumeResponse = await resumedInstance.fetch( + new Request( + `https://example.com/resume?sessionId=${started.sessionId}`, + { method: 'POST' } + ) + ); + const resumed = await resumeResponse.json() as { + sessionId: string; + snapshot: { + status: string; + output: { + topic: string; + handoffs: string[]; + }; + }; + }; + + expect(started.sessionId).toBe(resumed.sessionId); + expect(resumed.snapshot.status).toBe('done'); + expect(resumed.snapshot.output).toEqual( + expect.objectContaining({ + topic: 'durable networks', + handoffs: [ + 'researcher:collect the strongest supporting facts', + 'writer:turn the current notes into a concise summary', + ], + }) + ); + }); + test('hitl example exposes typed pending events', async () => { const machine = createHitlExample(); const result = await machine.execute( @@ -276,6 +344,65 @@ describe('curated examples', () => { } }); + test('persistent multi-agent network example restores from a mid-handoff snapshot', async () => { + let step = 0; + const result = await runPersistentMultiAgentNetworkExample( + { topic: 'resumable coordination' }, + { + adapter: { + decide: async () => { + step += 1; + + if (step === 1) { + return { + choice: 'research', + data: { focus: 'collect durable coordination notes' }, + }; + } + + if (step === 2) { + return { + choice: 'write', + data: { angle: 'produce the final coordination memo' }, + }; + } + + return { + choice: 'finalize', + data: {}, + }; + }, + }, + research: async ({ topic, focus }) => ({ + notes: [`${topic}:${focus}:a`, `${topic}:${focus}:b`], + }), + write: async ({ topic, notes, angle }) => ({ + draft: `${topic} | ${angle} | ${notes.join(' / ')}`, + }), + } + ); + + expect(result.restoredSnapshot).toEqual(result.liveSnapshot); + expect(result.liveSnapshot).toEqual( + expect.objectContaining({ + value: 'done', + output: { + topic: 'resumable coordination', + notes: [ + 'resumable coordination:collect durable coordination notes:a', + 'resumable coordination:collect durable coordination notes:b', + ], + draft: + 'resumable coordination | produce the final coordination memo | resumable coordination:collect durable coordination notes:a / resumable coordination:collect durable coordination notes:b', + handoffs: [ + 'researcher:collect durable coordination notes', + 'writer:produce the final coordination memo', + ], + }, + }) + ); + }); + test('branching example fans out plain async work and summarizes it', async () => { const machine = createBranchingExample({ analyzeDocs: async () => 'docs', @@ -668,9 +795,22 @@ describe('curated examples', () => { }); test('tool-calling example emits live tool activity and completes with output', async () => { - const machine = createToolCallingExample(async (city) => ({ - forecast: `Rainy in ${city}`, - })); + const machine = createToolCallingExample(async (city, emitProgress) => { + emitProgress({ + toolName: 'getWeather', + message: `Checking radar for ${city}`, + step: 1, + }); + emitProgress({ + toolName: 'getWeather', + message: `Preparing forecast for ${city}`, + step: 2, + }); + + return { + forecast: `Rainy in ${city}`, + }; + }); const { createMemoryRunStore, startSession } = await import('./index.js'); const run = await startSession(machine, { @@ -682,6 +822,9 @@ describe('curated examples', () => { run.on('toolCall', (event) => { events.push(`call:${event.toolName}`); }); + run.on('toolProgress', (event) => { + events.push(`progress:${event.toolName}:${event.step}`); + }); run.on('toolResult', (event) => { events.push(`result:${event.toolName}`); }); @@ -691,7 +834,12 @@ describe('curated examples', () => { run.onError((event) => reject(event.error)); }); - expect(events).toEqual(['call:getWeather', 'result:getWeather']); + expect(events).toEqual([ + 'call:getWeather', + 'progress:getWeather:1', + 'progress:getWeather:2', + 'result:getWeather', + ]); expect(run.getSnapshot()).toEqual( expect.objectContaining({ output: { forecast: 'Rainy in New York' }, diff --git a/src/graph/index.test.ts b/src/graph/index.test.ts index ba3c897..b9b7090 100644 --- a/src/graph/index.test.ts +++ b/src/graph/index.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'vitest'; import { z } from 'zod'; import { createAgentMachine } from '../index.js'; -import { toGraph, toMermaid } from './index.js'; +import { analyzeGraph, toGraph, toMermaid } from './index.js'; declare function unknownTransition(): { target: 'done' }; @@ -205,7 +205,7 @@ test('reports graph warnings for unsupported transition analysis', () => { }, }); - expect(toGraph(machine).data?.warnings).toEqual([ + expect(analyzeGraph(machine).warnings).toEqual([ { state: 'idle', event: 'go', @@ -213,6 +213,63 @@ test('reports graph warnings for unsupported transition analysis', () => { 'Unsupported helper call: unknownTransition() is not statically resolvable.', }, ]); + expect(toGraph(machine).data).toBeUndefined(); +}); + +test('resolves simple helper calls with arguments in guards and targets', () => { + const machine = createAgentMachine({ + id: 'helper-args-export', + schemas: { + events: { + choose: z.object({ + type: z.literal('choose'), + kind: z.enum(['approved', 'rejected']), + }), + }, + }, + context: () => ({}), + initial: 'idle', + states: { + idle: { + on: { + choose: ({ event }) => { + function goTo( + target: 'approved' | 'rejected', + reason: string + ) { + return { + target, + context: { reason }, + }; + } + + return event.kind === 'approved' + ? goTo('approved', 'explicit approval path') + : goTo('rejected', 'explicit rejection path'); + }, + }, + }, + approved: { type: 'final' }, + rejected: { type: 'final' }, + }, + }); + + expect(toGraph(machine).edges).toEqual([ + expect.objectContaining({ + targetId: 'approved', + data: expect.objectContaining({ + guard: { type: 'event.kind === "approved"' }, + actions: { context: true }, + }), + }), + expect.objectContaining({ + targetId: 'rejected', + data: expect.objectContaining({ + guard: { type: '!(event.kind === "approved")' }, + actions: { context: true }, + }), + }), + ]); }); test('exports a mermaid state diagram from the Stately graph data', () => { diff --git a/src/graph/index.ts b/src/graph/index.ts index 4a7dfe4..842a1f5 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -38,7 +38,6 @@ export interface AgentGraphEdgeData { } export interface AgentGraphData { - warnings?: AgentGraphWarning[]; } export interface AgentGraphWarning { @@ -47,6 +46,11 @@ export interface AgentGraphWarning { message: string; } +export interface AgentGraphAnalysis { + graph: AgentGraph; + warnings: AgentGraphWarning[]; +} + export interface AgentGraph extends StatelyGraph {} export interface AgentGraphNode @@ -80,6 +84,8 @@ type AnalyzableFunction = | ts.FunctionDeclaration; type HelperMap = Map; +type BindingMap = Map; +const printer = ts.createPrinter({ removeComments: true }); /** * Convert an agent machine to a Stately graph-compatible plain JSON object. @@ -88,6 +94,10 @@ type HelperMap = Map; * inferred from static transition objects and transition handler ASTs. */ export function toGraph(machine: AgentMachine): AgentGraph { + return analyzeGraph(machine).graph; +} + +export function analyzeGraph(machine: AgentMachine): AgentGraphAnalysis { const config = (machine as InternalMachine).__config; if (!config) { throw new Error('Machine config metadata is unavailable for graph export'); @@ -138,14 +148,18 @@ export function toGraph(machine: AgentMachine): AgentGraph { } } - return createGraph({ + const graph = createGraph({ id: machine.id, initialNodeId: typeof config.initial === 'string' ? config.initial : undefined, - ...(warnings.length > 0 ? { data: { warnings } } : {}), nodes, edges, }); + + return { + graph, + warnings, + }; } export function toMermaid(machine: AgentMachine): string { @@ -296,7 +310,9 @@ function analyzeTransitionObject(transition: unknown): AnalysisResult { } function analyzeTransitionFunction(fn: Function): AnalysisResult { - const source = fn.toString(); + const source = fn + .toString() + .replace(/__name\([^)]*\);?/g, ''); const file = ts.createSourceFile( 'transition.ts', `const __transition = ${source};`, @@ -320,7 +336,8 @@ function analyzeTransitionFunction(fn: Function): AnalysisResult { transitionFunction.body, [], file, - helpers + helpers, + new Map() ); } @@ -329,7 +346,8 @@ function analyzeTransitionFunction(fn: Function): AnalysisResult { transitionFunction.body.statements, [], file, - helpers + helpers, + new Map() ); } @@ -377,36 +395,39 @@ function collectHelpers(fn: AnalyzableFunction): HelperMap { return helpers; } - for (const statement of fn.body.statements) { + function visit(node: ts.Node) { if ( - ts.isFunctionDeclaration(statement) - && statement.name - && statement.body + node !== fn.body + && isAnalyzableFunction(node) ) { - helpers.set(statement.name.text, statement); - continue; + return; } - if (!ts.isVariableStatement(statement)) { - continue; + if (ts.isFunctionDeclaration(node) && node.name && node.body) { + helpers.set(node.name.text, node); + return; } - for (const declaration of statement.declarationList.declarations) { - if (!ts.isIdentifier(declaration.name) || !declaration.initializer) { - continue; + if (ts.isVariableDeclaration(node)) { + if (!ts.isIdentifier(node.name) || !node.initializer) { + return; } - const initializer = unwrapParenthesized(declaration.initializer); + const initializer = unwrapParenthesized(node.initializer); if ( isAnalyzableFunction(initializer) || ts.isObjectLiteralExpression(initializer) || ts.isConditionalExpression(initializer) ) { - helpers.set(declaration.name.text, initializer); + helpers.set(node.name.text, initializer); } } + + ts.forEachChild(node, visit); } + visit(fn.body); + return helpers; } @@ -414,14 +435,21 @@ function analyzeStatements( statements: ts.NodeArray, guards: string[], file: ts.SourceFile, - helpers: HelperMap + helpers: HelperMap, + bindings: BindingMap ): BlockAnalysis { const candidates: EdgeCandidate[] = []; const warnings: string[] = []; const fallthroughGuards = [...guards]; for (const statement of statements) { - const result = analyzeStatement(statement, fallthroughGuards, file, helpers); + const result = analyzeStatement( + statement, + fallthroughGuards, + file, + helpers, + bindings + ); candidates.push(...result.candidates); warnings.push(...result.warnings); @@ -434,7 +462,9 @@ function analyzeStatements( && isReturnOnlyBranch(statement.thenStatement) && !statement.elseStatement ) { - fallthroughGuards.push(`!(${statement.expression.getText(file)})`); + fallthroughGuards.push( + `!(${renderExpressionText(statement.expression, file, bindings)})` + ); } } @@ -445,7 +475,8 @@ function analyzeStatement( statement: ts.Statement, guards: string[], file: ts.SourceFile, - helpers: HelperMap + helpers: HelperMap, + bindings: BindingMap ): BlockAnalysis { if (ts.isReturnStatement(statement)) { if (!statement.expression) { @@ -460,7 +491,8 @@ function analyzeStatement( statement.expression, guards, file, - helpers + helpers, + bindings ); return { @@ -476,11 +508,11 @@ function analyzeStatement( } if (ts.isIfStatement(statement)) { - return analyzeIfStatement(statement, guards, file, helpers); + return analyzeIfStatement(statement, guards, file, helpers, bindings); } if (ts.isSwitchStatement(statement)) { - return analyzeSwitchStatement(statement, guards, file, helpers); + return analyzeSwitchStatement(statement, guards, file, helpers, bindings); } if ( @@ -502,21 +534,24 @@ function analyzeIfStatement( statement: ts.IfStatement, guards: string[], file: ts.SourceFile, - helpers: HelperMap + helpers: HelperMap, + bindings: BindingMap ): BlockAnalysis { - const condition = statement.expression.getText(file); + const condition = renderExpressionText(statement.expression, file, bindings); const thenResult = analyzeBranch( statement.thenStatement, [...guards, condition], file, - helpers + helpers, + bindings ); const elseResult = statement.elseStatement ? analyzeBranch( statement.elseStatement, [...guards, `!(${condition})`], file, - helpers + helpers, + bindings ) : emptyBlockAnalysis(); @@ -531,11 +566,12 @@ function analyzeSwitchStatement( statement: ts.SwitchStatement, guards: string[], file: ts.SourceFile, - helpers: HelperMap + helpers: HelperMap, + bindings: BindingMap ): BlockAnalysis { const candidates: EdgeCandidate[] = []; const warnings: string[] = []; - const expression = statement.expression.getText(file); + const expression = renderExpressionText(statement.expression, file, bindings); const caseGuards: string[] = []; let allClausesExit = statement.caseBlock.clauses.length > 0; @@ -554,7 +590,8 @@ function analyzeSwitchStatement( clause.statements, clauseGuard ? [...guards, clauseGuard] : guards, file, - helpers + helpers, + bindings ); candidates.push(...result.candidates); warnings.push(...result.warnings); @@ -572,13 +609,14 @@ function analyzeBranch( statement: ts.Statement, guards: string[], file: ts.SourceFile, - helpers: HelperMap + helpers: HelperMap, + bindings: BindingMap ): BlockAnalysis { if (ts.isBlock(statement)) { - return analyzeStatements(statement.statements, guards, file, helpers); + return analyzeStatements(statement.statements, guards, file, helpers, bindings); } - return analyzeStatement(statement, guards, file, helpers); + return analyzeStatement(statement, guards, file, helpers, bindings); } function emptyBlockAnalysis(): BlockAnalysis { @@ -606,25 +644,28 @@ function analyzeTransitionExpression( expression: ts.Expression, guards: string[], file: ts.SourceFile, - helpers: HelperMap + helpers: HelperMap, + bindings: BindingMap ): AnalysisResult { const current = unwrapParenthesized(expression); if (ts.isConditionalExpression(current)) { - const condition = current.condition.getText(file); + const condition = renderExpressionText(current.condition, file, bindings); return mergeAnalysis([ analyzeTransitionExpression( current.whenTrue, [...guards, condition], file, - helpers + helpers, + bindings ), analyzeTransitionExpression( current.whenFalse, [...guards, `!(${condition})`], file, - helpers + helpers, + bindings ), ]); } @@ -632,23 +673,34 @@ function analyzeTransitionExpression( if ( ts.isCallExpression(current) && ts.isIdentifier(current.expression) - && current.arguments.length === 0 ) { - const helper = helpers.get(current.expression.text); + const fallbackHelper = findHelperByName(file, current.expression.text); + const helper = + helpers.get(current.expression.text) + ?? fallbackHelper; if (!helper) { return { candidates: [], warnings: [ - `Unsupported helper call: ${current.expression.text}() is not statically resolvable.`, + `Unsupported helper call: ${current.expression.text}(${current.arguments.map((arg) => renderExpressionText(arg, file, bindings)).join(', ')}) is not statically resolvable.`, ], }; } - return analyzeHelper(helper, guards, file, helpers); + return analyzeHelper( + helper, + current.arguments, + guards, + file, + helpers, + bindings + ); } const object = unwrapParenthesizedObject(current); - const target = object ? getStringProperty(object, 'target') : undefined; + const target = object + ? getStringProperty(object, 'target', file, bindings) + : undefined; if (!target) { return { candidates: [], warnings: [] }; } @@ -666,17 +718,41 @@ function analyzeTransitionExpression( function analyzeHelper( helper: AnalyzableFunction | ts.Expression, + args: ts.NodeArray, guards: string[], file: ts.SourceFile, - helpers: HelperMap + helpers: HelperMap, + bindings: BindingMap ): AnalysisResult { if (isAnalyzableFunction(helper)) { + const helperBindings = createBindings(helper, args, bindings); + if (!helperBindings) { + return { + candidates: [], + warnings: [ + `Unsupported helper call: argument count for ${getHelperName(helper)}(...) could not be matched.`, + ], + }; + } + if (ts.isArrowFunction(helper) && !ts.isBlock(helper.body)) { - return analyzeTransitionExpression(helper.body, guards, file, helpers); + return analyzeTransitionExpression( + helper.body, + guards, + file, + helpers, + helperBindings + ); } if (helper.body && ts.isBlock(helper.body)) { - const result = analyzeStatements(helper.body.statements, guards, file, helpers); + const result = analyzeStatements( + helper.body.statements, + guards, + file, + helpers, + helperBindings + ); return { candidates: result.candidates, warnings: result.warnings, @@ -685,7 +761,14 @@ function analyzeHelper( } if (ts.isExpression(helper)) { - return analyzeTransitionExpression(helper, guards, file, helpers); + if (args.length > 0) { + return { + candidates: [], + warnings: ['Unsupported helper call: non-function helper cannot accept arguments.'], + }; + } + + return analyzeTransitionExpression(helper, guards, file, helpers, bindings); } return { @@ -711,6 +794,50 @@ function unwrapParenthesized(expression: T): ts.Express return current; } +function findHelperByName( + file: ts.SourceFile, + name: string +): AnalyzableFunction | ts.Expression | undefined { + let helper: AnalyzableFunction | ts.Expression | undefined; + + function visit(node: ts.Node) { + if (helper) { + return; + } + + if ( + ts.isFunctionDeclaration(node) + && node.name?.text === name + && node.body + ) { + helper = node; + return; + } + + if (ts.isVariableDeclaration(node)) { + if (!ts.isIdentifier(node.name) || node.name.text !== name || !node.initializer) { + ts.forEachChild(node, visit); + return; + } + + const initializer = unwrapParenthesized(node.initializer); + if ( + isAnalyzableFunction(initializer) + || ts.isObjectLiteralExpression(initializer) + || ts.isConditionalExpression(initializer) + ) { + helper = initializer; + return; + } + } + + ts.forEachChild(node, visit); + } + + visit(file); + return helper; +} + function unwrapParenthesizedObject( expression: ts.Expression ): ts.ObjectLiteralExpression | undefined { @@ -725,28 +852,44 @@ function unwrapParenthesizedObject( function getStringProperty( object: ts.ObjectLiteralExpression, - name: string + name: string, + file: ts.SourceFile, + bindings: BindingMap ): string | undefined { const property = object.properties.find((candidate) => { return ( - ts.isPropertyAssignment(candidate) + (ts.isPropertyAssignment(candidate) + || ts.isShorthandPropertyAssignment(candidate)) && ts.isIdentifier(candidate.name) && candidate.name.text === name ); }); - if (!property || !ts.isPropertyAssignment(property)) { + if (!property) { return undefined; } - const initializer = property.initializer; - return ts.isStringLiteralLike(initializer) ? initializer.text : undefined; + if (ts.isPropertyAssignment(property)) { + return resolveStringExpression(property.initializer, file, bindings); + } + + if (ts.isShorthandPropertyAssignment(property)) { + const binding = bindings.get(property.name.text); + if (!binding) { + return property.name.text; + } + + return resolveStringExpression(binding, file, bindings); + } + + return undefined; } function hasProperty(object: ts.ObjectLiteralExpression, name: string): boolean { return object.properties.some((candidate) => { return ( - ts.isPropertyAssignment(candidate) + (ts.isPropertyAssignment(candidate) + || ts.isShorthandPropertyAssignment(candidate)) && ts.isIdentifier(candidate.name) && candidate.name.text === name ); @@ -761,6 +904,99 @@ function getEdgeLabel(event: string, guard: string | undefined): string { return `${event} [${guard}]`; } +function createBindings( + helper: AnalyzableFunction, + args: ts.NodeArray, + parentBindings: BindingMap +): BindingMap | null { + if (args.length > helper.parameters.length) { + return null; + } + + const bindings = new Map(parentBindings); + helper.parameters.forEach((parameter, index) => { + if (!ts.isIdentifier(parameter.name)) { + return; + } + + const arg = args[index]; + if (arg) { + bindings.set(parameter.name.text, substituteExpression(arg, parentBindings)); + } + }); + + return bindings; +} + +function getHelperName(helper: AnalyzableFunction): string { + if (helper.name) { + return helper.name.text; + } + + return 'helper'; +} + +function resolveStringExpression( + expression: ts.Expression, + file: ts.SourceFile, + bindings: BindingMap +): string | undefined { + const current = substituteExpression(unwrapParenthesized(expression), bindings); + + if (ts.isStringLiteralLike(current)) { + return current.text; + } + + if (ts.isNoSubstitutionTemplateLiteral(current)) { + return current.text; + } + + if (ts.isIdentifier(current)) { + return current.text; + } + + const rendered = renderExpressionText(current, file, bindings); + return /^["'`](.*)["'`]$/s.test(rendered) + ? rendered.slice(1, -1) + : undefined; +} + +function renderExpressionText( + expression: ts.Expression, + file: ts.SourceFile, + bindings: BindingMap +): string { + const substituted = substituteExpression(unwrapParenthesized(expression), bindings); + return printer.printNode(ts.EmitHint.Unspecified, substituted, file); +} + +function substituteExpression( + expression: ts.Expression, + bindings: BindingMap +): ts.Expression { + if (bindings.size === 0) { + return expression; + } + + const transformed = ts.transform(expression, [ + (context) => { + const visit: ts.Visitor = (node) => { + if (ts.isIdentifier(node) && bindings.has(node.text)) { + return substituteExpression(bindings.get(node.text)!, bindings); + } + + return ts.visitEachChild(node, visit, context); + }; + + return (node) => ts.visitNode(node, visit) as ts.Expression; + }, + ]); + + const substituted = transformed.transformed[0] as ts.Expression; + transformed.dispose(); + return substituted; +} + function combineGuardList(guards: string[]): string | undefined { if (guards.length === 0) { return undefined; diff --git a/src/langgraph-equivalents/persistent-multi-agent-network.test.ts b/src/langgraph-equivalents/persistent-multi-agent-network.test.ts new file mode 100644 index 0000000..863d993 --- /dev/null +++ b/src/langgraph-equivalents/persistent-multi-agent-network.test.ts @@ -0,0 +1,63 @@ +import { expect, test } from 'vitest'; +import { runPersistentMultiAgentNetworkExample } from '../../examples/index.js'; + +test('restores a multi-agent handoff workflow from a persisted mid-handoff snapshot', async () => { + let step = 0; + + const result = await runPersistentMultiAgentNetworkExample( + { topic: 'durable agent handoffs' }, + { + adapter: { + decide: async () => { + step += 1; + + if (step === 1) { + return { + choice: 'research', + data: { focus: 'collect the most durable architecture notes' }, + }; + } + + if (step === 2) { + return { + choice: 'write', + data: { angle: 'summarize the handoff-ready findings' }, + }; + } + + return { + choice: 'finalize', + data: {}, + }; + }, + }, + research: async ({ topic, focus }) => ({ + notes: [`${topic}:${focus}:1`, `${topic}:${focus}:2`], + }), + write: async ({ topic, notes, angle }) => ({ + draft: `${topic} | ${angle} | ${notes.join(' / ')}`, + }), + } + ); + + expect(result.restoredSnapshot).toEqual(result.liveSnapshot); + expect(result.restoredSnapshot).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { + topic: 'durable agent handoffs', + notes: [ + 'durable agent handoffs:collect the most durable architecture notes:1', + 'durable agent handoffs:collect the most durable architecture notes:2', + ], + draft: + 'durable agent handoffs | summarize the handoff-ready findings | durable agent handoffs:collect the most durable architecture notes:1 / durable agent handoffs:collect the most durable architecture notes:2', + handoffs: [ + 'researcher:collect the most durable architecture notes', + 'writer:summarize the handoff-ready findings', + ], + }, + }) + ); +}); diff --git a/src/langgraph-equivalents/tool-calling.test.ts b/src/langgraph-equivalents/tool-calling.test.ts index 2a54431..6107041 100644 --- a/src/langgraph-equivalents/tool-calling.test.ts +++ b/src/langgraph-equivalents/tool-calling.test.ts @@ -27,6 +27,11 @@ test('supports tool-call style invokes with live tool events and final output', toolName: z.string(), input: z.object({ city: z.string() }), }), + toolProgress: z.object({ + toolName: z.string(), + message: z.string(), + step: z.number().int().min(1), + }), toolResult: z.object({ toolName: z.string(), output: z.object({ forecast: z.string() }), @@ -49,6 +54,20 @@ test('supports tool-call style invokes with live tool events and final output', input: { city: context.city }, }); + enq.emit({ + type: 'toolProgress', + toolName: 'getWeather', + message: `Fetching weather for ${context.city}`, + step: 1, + }); + + enq.emit({ + type: 'toolProgress', + toolName: 'getWeather', + message: `Formatting response for ${context.city}`, + step: 2, + }); + const output = { forecast: `Sunny in ${context.city}` }; enq.emit({ type: 'toolResult', @@ -79,13 +98,21 @@ test('supports tool-call style invokes with live tool events and final output', run.on('toolCall', (event) => { events.push(`call:${event.toolName}`); }); + run.on('toolProgress', (event) => { + events.push(`progress:${event.toolName}:${event.step}`); + }); run.on('toolResult', (event) => { events.push(`result:${event.toolName}`); }); await once(run.onDone.bind(run)); - expect(events).toEqual(['call:getWeather', 'result:getWeather']); + expect(events).toEqual([ + 'call:getWeather', + 'progress:getWeather:1', + 'progress:getWeather:2', + 'result:getWeather', + ]); expect(run.getSnapshot()).toEqual( expect.objectContaining({ value: 'done', From e7ef00a39fae37515ca059fb5ca46b1a0c218901 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 24 Apr 2026 08:16:24 -0400 Subject: [PATCH 24/34] feat: add durable supervisor and streaming examples --- examples/index.ts | 8 + examples/persistent-streaming.ts | 138 ++++++++++++++++++ examples/persistent-supervisor.ts | 117 +++++++++++++++ readme.md | 2 + src/examples.test.ts | 89 +++++++++++ .../persistent-streaming.test.ts | 26 ++++ .../persistent-supervisor.test.ts | 63 ++++++++ 7 files changed, 443 insertions(+) create mode 100644 examples/persistent-streaming.ts create mode 100644 examples/persistent-supervisor.ts create mode 100644 src/langgraph-equivalents/persistent-streaming.test.ts create mode 100644 src/langgraph-equivalents/persistent-supervisor.test.ts diff --git a/examples/index.ts b/examples/index.ts index d43e598..7b5217e 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -27,6 +27,14 @@ export { createPersistentMultiAgentNetworkExample, runPersistentMultiAgentNetworkExample, } from './persistent-multi-agent-network.js'; +export { + createPersistentStreamingExample, + runPersistentStreamingExample, +} from './persistent-streaming.js'; +export { + createPersistentSupervisorExample, + runPersistentSupervisorExample, +} from './persistent-supervisor.js'; export { createRaffleExample } from './raffle.js'; export { createReactAgentExample } from './react-agent.js'; export { diff --git a/examples/persistent-streaming.ts b/examples/persistent-streaming.ts new file mode 100644 index 0000000..41545d6 --- /dev/null +++ b/examples/persistent-streaming.ts @@ -0,0 +1,138 @@ +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + restoreSession, + startSession, +} from '../src/index.js'; + +const textSchema = z.object({ + text: z.string(), +}); + +const textPartSchema = z.object({ + delta: z.string(), +}); + +export function createPersistentStreamingExample( + writeText: (emitPart: (delta: string) => void) => Promise> = (() => { + const chunks = ['hel', 'lo']; + let cursor = 0; + let attempts = 0; + + return async (emitPart) => { + attempts += 1; + + if (attempts === 1) { + emitPart(chunks[cursor++]!); + await new Promise(() => {}); + } + + while (cursor < chunks.length) { + emitPart(chunks[cursor++]!); + } + + return { text: chunks.join('') }; + }; + })() +) { + return createAgentMachine({ + id: 'persistent-streaming-example', + schemas: { + output: textSchema, + emitted: { + textPart: textPartSchema, + }, + }, + context: () => ({ + finalText: '', + }), + initial: 'writing', + states: { + writing: { + resultSchema: textSchema, + invoke: async (_args, enq) => + writeText((delta) => { + enq.emit({ type: 'textPart', delta }); + }), + onDone: ({ result }) => ({ + target: 'done', + context: { finalText: result.text }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ text: context.finalText }), + }, + }, + }); +} + +export async function runPersistentStreamingExample( + writeText?: (emitPart: (delta: string) => void) => Promise> +) { + const machine = createPersistentStreamingExample(writeText); + const store = createMemoryRunStore(); + const initialRun = await startSession(machine, { store }); + const initialParts: string[] = []; + + initialRun.on('textPart', (event) => { + initialParts.push(event.delta); + }); + + await waitFor( + () => initialParts.length >= 1 && initialRun.getSnapshot().status === 'active' + ); + + const restoredRun = await restoreSession(machine, { + sessionId: initialRun.sessionId, + store, + }); + const restoredParts: string[] = []; + + restoredRun.on('textPart', (event) => { + restoredParts.push(event.delta); + }); + + await once(restoredRun.onDone.bind(restoredRun)); + + return { + sessionId: initialRun.sessionId, + initialParts, + restoredParts, + initialSnapshot: initialRun.getSnapshot(), + restoredSnapshot: restoredRun.getSnapshot(), + journal: await store.loadEvents(initialRun.sessionId), + }; +} + +function once( + subscribe: (handler: (event: T) => void) => () => void +) { + return new Promise((resolve) => { + let off = () => {}; + off = subscribe((event) => { + off(); + resolve(event); + }); + }); +} + +async function waitFor( + predicate: () => boolean, + timeoutMs = 1000 +) { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + if (predicate()) { + return; + } + + await new Promise((resolve) => setTimeout(resolve, 10)); + } + + if (!predicate()) { + throw new Error('Condition did not become true before timeout.'); + } +} diff --git a/examples/persistent-supervisor.ts b/examples/persistent-supervisor.ts new file mode 100644 index 0000000..9bbd343 --- /dev/null +++ b/examples/persistent-supervisor.ts @@ -0,0 +1,117 @@ +import { + createMemoryRunStore, + restoreSession, + startSession, + type PersistedSnapshot, +} from '../src/index.js'; +import { createSupervisorExample } from './supervisor.js'; + +type SupervisorOptions = Parameters[0]; + +export function createPersistentSupervisorExample( + options: SupervisorOptions = {} +) { + return createSupervisorExample(options); +} + +export async function runPersistentSupervisorExample( + input: { request: string }, + options: SupervisorOptions = {} +) { + const machine = createPersistentSupervisorExample(options); + const baseStore = createMemoryRunStore(); + let persistedRetryHandoff = false; + + const store = { + append: baseStore.append, + loadEvents: baseStore.loadEvents, + loadLatestSnapshot: baseStore.loadLatestSnapshot, + async saveSnapshot(snapshot: PersistedSnapshot) { + const context = snapshot.snapshot.context as { + attemptCount?: number; + history?: string[]; + }; + const history = context.history ?? []; + + if ( + !persistedRetryHandoff + && snapshot.snapshot.value === 'handling' + && context.attemptCount === 1 + && history.some((entry) => entry.startsWith('supervisor:retry:')) + ) { + persistedRetryHandoff = true; + await baseStore.saveSnapshot(snapshot); + return; + } + + if (!persistedRetryHandoff) { + await baseStore.saveSnapshot(snapshot); + } + }, + }; + + const liveRun = await startSession(machine, { + store, + input, + }); + + await waitForTerminal(() => liveRun.getSnapshot().status); + + const restoredRun = await restoreSession(machine, { + sessionId: liveRun.sessionId, + store, + }); + + await waitForMatch( + () => restoredRun.getSnapshot(), + () => liveRun.getSnapshot() + ); + + return { + sessionId: liveRun.sessionId, + liveSnapshot: liveRun.getSnapshot(), + restoredSnapshot: restoredRun.getSnapshot(), + }; +} + +function expectTerminal(status: string) { + if (status !== 'done' && status !== 'error') { + throw new Error(`Snapshot is not terminal yet: ${status}`); + } +} + +async function waitForTerminal( + getStatus: () => string, + timeoutMs = 1000 +) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + expectTerminal(getStatus()); + return; + } catch {} + + await new Promise((resolve) => setTimeout(resolve, 10)); + } + + expectTerminal(getStatus()); +} + +async function waitForMatch( + getActual: () => T, + getExpected: () => T, + timeoutMs = 1000 +) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (JSON.stringify(getActual()) === JSON.stringify(getExpected())) { + return; + } + + await new Promise((resolve) => setTimeout(resolve, 10)); + } + + if (JSON.stringify(getActual()) !== JSON.stringify(getExpected())) { + throw new Error('Snapshots did not converge before timeout.'); + } +} diff --git a/readme.md b/readme.md index 33c0838..1c15c23 100644 --- a/readme.md +++ b/readme.md @@ -26,6 +26,8 @@ Each example demonstrates one concept: - [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts): tool invocation with emitted `toolCall`, incremental `toolProgress`, and final `toolResult` events - [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts): persisting snapshots and restoring a session in plain runtime code - [`examples/persistent-multi-agent-network.ts`](/Users/davidkpiano/Code/agent/examples/persistent-multi-agent-network.ts): durable multi-agent handoffs that restore from a persisted mid-network snapshot +- [`examples/persistent-streaming.ts`](/Users/davidkpiano/Code/agent/examples/persistent-streaming.ts): restore a crashed streaming invoke and continue with only new live emitted parts +- [`examples/persistent-supervisor.ts`](/Users/davidkpiano/Code/agent/examples/persistent-supervisor.ts): durable supervisor routing that restores after a persisted retry handoff - [`examples/cloudflare-durable-network.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-durable-network.ts): a Cloudflare Durable Object runner that starts and resumes a persisted multi-agent network - [`examples/decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts): choosing the next branch with structured branch-specific payloads - [`examples/classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts): routing into a fixed category set when you only need a label diff --git a/src/examples.test.ts b/src/examples.test.ts index 65512aa..c9c247c 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -23,6 +23,8 @@ import { createNewspaperExample, runPersistenceExample, runPersistentMultiAgentNetworkExample, + runPersistentStreamingExample, + runPersistentSupervisorExample, createPlanAndExecuteExample, createRaffleExample, createReactAgentExample, @@ -61,6 +63,8 @@ describe('curated examples', () => { expect(existsSync(resolve(examplesDir, 'newspaper.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'persistence.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'persistent-multi-agent-network.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'persistent-streaming.ts'))).toBe(true); + expect(existsSync(resolve(examplesDir, 'persistent-supervisor.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'plan-and-execute.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'raffle.ts'))).toBe(true); expect(existsSync(resolve(examplesDir, 'react-agent.ts'))).toBe(true); @@ -403,6 +407,91 @@ describe('curated examples', () => { ); }); + test('persistent supervisor example restores from a persisted retry handoff', async () => { + let decisions = 0; + + const result = await runPersistentSupervisorExample( + { request: 'Reverse the duplicate subscription charge.' }, + { + adapter: { + decide: async () => { + decisions += 1; + + if (decisions === 1) { + return { + choice: 'retry', + data: { + instruction: 'Retry using the verified billing email on file.', + }, + }; + } + + return { + choice: 'escalate', + data: { + reason: 'Escalate to billing because the account is still ambiguous.', + }, + }; + }, + }, + handle: async ({ attempt, instruction }) => ({ + status: 'blocked' as const, + issue: + attempt === 1 + ? 'Missing account identifier.' + : `Still blocked after retry: ${instruction}`, + }), + maxAttempts: 2, + } + ); + + expect(result.liveSnapshot).toEqual(result.restoredSnapshot); + expect(result.liveSnapshot).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { + request: 'Reverse the duplicate subscription charge.', + status: 'escalated', + resolution: null, + escalationReason: + 'Escalate to billing because the account is still ambiguous.', + attemptCount: 2, + history: [ + 'worker:1:blocked:Missing account identifier.', + 'supervisor:retry:Retry using the verified billing email on file.', + 'worker:2:blocked:Still blocked after retry: Retry using the verified billing email on file.', + 'supervisor:escalate:Escalate to billing because the account is still ambiguous.', + ], + }, + }) + ); + }); + + test('persistent streaming example resumes with only new live parts after restore', async () => { + const result = await runPersistentStreamingExample(); + + expect(result.initialParts).toEqual(['hel']); + expect(result.restoredParts).toEqual(['lo']); + expect(result.initialSnapshot).toEqual( + expect.objectContaining({ + value: 'writing', + status: 'active', + }) + ); + expect(result.restoredSnapshot).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { text: 'hello' }, + }) + ); + expect(result.journal.map((event) => event.type)).toEqual([ + 'xstate.init', + 'xstate.done.invoke.writing', + ]); + }); + test('branching example fans out plain async work and summarizes it', async () => { const machine = createBranchingExample({ analyzeDocs: async () => 'docs', diff --git a/src/langgraph-equivalents/persistent-streaming.test.ts b/src/langgraph-equivalents/persistent-streaming.test.ts new file mode 100644 index 0000000..386e92e --- /dev/null +++ b/src/langgraph-equivalents/persistent-streaming.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from 'vitest'; +import { runPersistentStreamingExample } from '../../examples/index.js'; + +test('restores a streaming workflow without replaying stale emitted parts', async () => { + const result = await runPersistentStreamingExample(); + + expect(result.initialParts).toEqual(['hel']); + expect(result.restoredParts).toEqual(['lo']); + expect(result.initialSnapshot).toEqual( + expect.objectContaining({ + value: 'writing', + status: 'active', + }) + ); + expect(result.restoredSnapshot).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { text: 'hello' }, + }) + ); + expect(result.journal.map((event) => event.type)).toEqual([ + 'xstate.init', + 'xstate.done.invoke.writing', + ]); +}); diff --git a/src/langgraph-equivalents/persistent-supervisor.test.ts b/src/langgraph-equivalents/persistent-supervisor.test.ts new file mode 100644 index 0000000..4885808 --- /dev/null +++ b/src/langgraph-equivalents/persistent-supervisor.test.ts @@ -0,0 +1,63 @@ +import { expect, test } from 'vitest'; +import { runPersistentSupervisorExample } from '../../examples/index.js'; + +test('restores a supervisor handoff workflow from a persisted retry snapshot', async () => { + let decisions = 0; + + const result = await runPersistentSupervisorExample( + { request: 'Reverse the duplicate subscription charge.' }, + { + adapter: { + decide: async () => { + decisions += 1; + + if (decisions === 1) { + return { + choice: 'retry', + data: { + instruction: 'Retry using the verified billing email on file.', + }, + }; + } + + return { + choice: 'escalate', + data: { + reason: 'Escalate to billing because the account is still ambiguous.', + }, + }; + }, + }, + handle: async ({ attempt, instruction }) => ({ + status: 'blocked', + issue: + attempt === 1 + ? 'Missing account identifier.' + : `Still blocked after retry: ${instruction}`, + }), + maxAttempts: 2, + } + ); + + expect(result.restoredSnapshot).toEqual(result.liveSnapshot); + expect(result.restoredSnapshot).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { + request: 'Reverse the duplicate subscription charge.', + status: 'escalated', + resolution: null, + escalationReason: + 'Escalate to billing because the account is still ambiguous.', + attemptCount: 2, + history: [ + 'worker:1:blocked:Missing account identifier.', + 'supervisor:retry:Retry using the verified billing email on file.', + 'worker:2:blocked:Still blocked after retry: Retry using the verified billing email on file.', + 'supervisor:escalate:Escalate to billing because the account is still ambiguous.', + ], + }, + }) + ); +}); From b26a554f4d974d7c7fb8ae9476a0e1f51ef0d340 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 24 Apr 2026 08:32:15 -0400 Subject: [PATCH 25/34] feat: add http session example --- examples/http-session.ts | 68 +++++++++++++++++++++ examples/index.ts | 1 + readme.md | 1 + src/examples.test.ts | 124 ++++++++++++++++++++++++++------------- src/graph/index.test.ts | 60 +++++++++++++++++++ 5 files changed, 214 insertions(+), 40 deletions(-) create mode 100644 examples/http-session.ts diff --git a/examples/http-session.ts b/examples/http-session.ts new file mode 100644 index 0000000..c355654 --- /dev/null +++ b/examples/http-session.ts @@ -0,0 +1,68 @@ +import { + createMemoryRunStore, + restoreSession, + startSession, + type RunStore, +} from '../src/index.js'; +import { createPersistenceExample } from './persistence.js'; + +export interface SessionHttpHandlerOptions { + store?: RunStore; + summarize?: Parameters[0]; +} + +export function createPersistenceSessionHttpHandler( + options: SessionHttpHandlerOptions = {} +) { + const store = options.store ?? createMemoryRunStore(); + const machine = createPersistenceExample(options.summarize); + + return async function handle(request: Request): Promise { + const url = new URL(request.url); + const match = url.pathname.match(/^\/sessions(?:\/([^/]+)(?:\/events)?)?$/); + const sessionId = match?.[1]; + const isEventRoute = url.pathname.endsWith('/events'); + + if (request.method === 'POST' && url.pathname === '/sessions') { + const body = await request.json() as { request: string }; + const run = await startSession(machine, { + store, + input: { request: body.request }, + }); + + return Response.json({ + sessionId: run.sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'GET' && sessionId && !isEventRoute) { + const run = await restoreSession(machine, { + sessionId, + store, + }); + + return Response.json({ + sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'POST' && sessionId && isEventRoute) { + const event = await request.json() as { type: 'approve' }; + const run = await restoreSession(machine, { + sessionId, + store, + }); + + await run.send(event); + + return Response.json({ + sessionId, + snapshot: run.getSnapshot(), + }); + } + + return new Response('Not found', { status: 404 }); + }; +} diff --git a/examples/index.ts b/examples/index.ts index 7b5217e..cbd239d 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -1,6 +1,7 @@ export { createSimpleExample } from './simple.js'; export { createSqlAgentExample } from './sql-agent.js'; export { createHitlExample } from './hitl.js'; +export { createPersistenceSessionHttpHandler } from './http-session.js'; export { createDecideExample } from './decide.js'; export { createClassifyExample } from './classify.js'; export { createAdapterExample } from './adapter.js'; diff --git a/readme.md b/readme.md index 1c15c23..2346575 100644 --- a/readme.md +++ b/readme.md @@ -25,6 +25,7 @@ Each example demonstrates one concept: - [`examples/conditional-subflow.ts`](/Users/davidkpiano/Code/agent/examples/conditional-subflow.ts): routing directly into one of multiple child subflows from parent input - [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts): tool invocation with emitted `toolCall`, incremental `toolProgress`, and final `toolResult` events - [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts): persisting snapshots and restoring a session in plain runtime code +- [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts): runner-agnostic HTTP transport for starting sessions, sending events, and reading snapshots over `Request`/`Response` - [`examples/persistent-multi-agent-network.ts`](/Users/davidkpiano/Code/agent/examples/persistent-multi-agent-network.ts): durable multi-agent handoffs that restore from a persisted mid-network snapshot - [`examples/persistent-streaming.ts`](/Users/davidkpiano/Code/agent/examples/persistent-streaming.ts): restore a crashed streaming invoke and continue with only new live emitted parts - [`examples/persistent-supervisor.ts`](/Users/davidkpiano/Code/agent/examples/persistent-supervisor.ts): durable supervisor routing that restores after a persisted retry handoff diff --git a/src/examples.test.ts b/src/examples.test.ts index c9c247c..80b8d78 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -1,7 +1,5 @@ import { describe, expect, test } from 'vitest'; import { z } from 'zod'; -import { existsSync } from 'node:fs'; -import { resolve } from 'node:path'; import { createChatbotExample, @@ -16,6 +14,7 @@ import { createEmailExample, createErrorRetryExample, createHitlExample, + createPersistenceSessionHttpHandler, createJokeExample, createJugsExample, createMapReduceExample, @@ -40,44 +39,6 @@ import { } from '../examples/index.js'; describe('curated examples', () => { - test('ships the canonical examples directory', () => { - const examplesDir = resolve(process.cwd(), 'examples'); - expect(existsSync(resolve(examplesDir, 'simple.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'sql-agent.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'hitl.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'decide.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'classify.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'adapter.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'branching.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'chatbot.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'cloudflare-durable-object.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'cloudflare-durable-network.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'conditional-subflow.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'customer-service-sim.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'email.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'error-retry.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'joke.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'jugs.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'map-reduce.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'multi-agent-network.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'newspaper.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'persistence.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'persistent-multi-agent-network.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'persistent-streaming.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'persistent-supervisor.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'plan-and-execute.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'raffle.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'react-agent.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'react-agent-from-scratch.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'rewoo.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'reflection.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'river-crossing.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'subflow.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'supervisor.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'tool-calling.ts'))).toBe(true); - expect(existsSync(resolve(examplesDir, 'tutor.ts'))).toBe(true); - }); - test('simple example runs to a final output', async () => { const machine = createSimpleExample(async () => ({ summary: 'A short summary.', @@ -161,6 +122,89 @@ describe('curated examples', () => { ); }); + test('http session example exposes start, send, and status over Request/Response', async () => { + const handle = createPersistenceSessionHttpHandler({ + summarize: async ({ request, approved }) => ({ + summary: `${request} :: approved=${String(approved)}`, + }), + }); + + const startResponse = await handle( + new Request('https://agent.test/sessions', { + method: 'POST', + body: JSON.stringify({ + request: 'Approve the annual budget summary.', + }), + headers: { + 'content-type': 'application/json', + }, + }) + ); + const startBody = await startResponse.json() as { + sessionId: string; + snapshot: { value: string; status: string }; + }; + + expect(startBody.snapshot).toEqual( + expect.objectContaining({ + value: 'review', + status: 'active', + }) + ); + + const sendResponse = await handle( + new Request(`https://agent.test/sessions/${startBody.sessionId}/events`, { + method: 'POST', + body: JSON.stringify({ type: 'approve' }), + headers: { + 'content-type': 'application/json', + }, + }) + ); + const sendBody = await sendResponse.json() as { + snapshot: { + value: string; + status: string; + output: { + request: string; + approved: boolean; + summary: string; + }; + }; + }; + + expect(sendBody.snapshot).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { + request: 'Approve the annual budget summary.', + approved: true, + summary: 'Approve the annual budget summary. :: approved=true', + }, + }) + ); + + const statusResponse = await handle( + new Request(`https://agent.test/sessions/${startBody.sessionId}`, { + method: 'GET', + }) + ); + const statusBody = await statusResponse.json() as { + snapshot: { + value: string; + status: string; + output: { + request: string; + approved: boolean; + summary: string; + }; + }; + }; + + expect(statusBody.snapshot).toEqual(sendBody.snapshot); + }); + test('cloudflare durable network example restores and settles a network run', async () => { const storage = new Map(); const firstInstance = new AgentNetworkDurableObject({ diff --git a/src/graph/index.test.ts b/src/graph/index.test.ts index b9b7090..6afbb11 100644 --- a/src/graph/index.test.ts +++ b/src/graph/index.test.ts @@ -272,6 +272,66 @@ test('resolves simple helper calls with arguments in guards and targets', () => ]); }); +test('resolves one-level helper forwarding with substituted arguments', () => { + const machine = createAgentMachine({ + id: 'helper-forwarding-export', + schemas: { + events: { + choose: z.object({ + type: z.literal('choose'), + kind: z.enum(['approved', 'rejected']), + }), + }, + }, + context: () => ({}), + initial: 'idle', + states: { + idle: { + on: { + choose: ({ event }) => { + function goTo( + target: 'approved' | 'rejected', + reason: string + ) { + return { + target, + context: { reason }, + }; + } + + function route(kind: 'approved' | 'rejected') { + return goTo(kind, `routed:${kind}`); + } + + return event.kind === 'approved' + ? route('approved') + : route('rejected'); + }, + }, + }, + approved: { type: 'final' }, + rejected: { type: 'final' }, + }, + }); + + expect(toGraph(machine).edges).toEqual([ + expect.objectContaining({ + targetId: 'approved', + data: expect.objectContaining({ + guard: { type: 'event.kind === "approved"' }, + actions: { context: true }, + }), + }), + expect.objectContaining({ + targetId: 'rejected', + data: expect.objectContaining({ + guard: { type: '!(event.kind === "approved")' }, + actions: { context: true }, + }), + }), + ]); +}); + test('exports a mermaid state diagram from the Stately graph data', () => { const machine = createAgentMachine({ id: 'mermaid-export', From 72260b623406c616c7c92615dc075c09d049eed4 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 25 Apr 2026 11:49:08 -0400 Subject: [PATCH 26/34] feat: add durable http streaming example --- examples/http-streaming-session.ts | 262 +++++++++++++++++++++++++++++ examples/index.ts | 1 + readme.md | 1 + src/examples.test.ts | 111 ++++++++++++ 4 files changed, 375 insertions(+) create mode 100644 examples/http-streaming-session.ts diff --git a/examples/http-streaming-session.ts b/examples/http-streaming-session.ts new file mode 100644 index 0000000..9e6185c --- /dev/null +++ b/examples/http-streaming-session.ts @@ -0,0 +1,262 @@ +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + restoreSession, + startSession, + type AgentRun, + type RunStore, +} from '../src/index.js'; + +const streamingInputSchema = z.object({ + streamId: z.string(), + text: z.string(), +}); + +const streamingOutputSchema = z.object({ + text: z.string(), +}); + +const textPartSchema = z.object({ + delta: z.string(), +}); + +type StreamingRun = AgentRun< + { streamId: string; text: string; finalText: string }, + string, + {}, + { text: string }, + { textPart: typeof textPartSchema } +>; + +export interface StreamingSessionHttpController { + handle(request: Request): Promise; + advance(streamId: string): void; + dropActiveSession(sessionId: string): void; +} + +export function createStreamingSessionHttpController(options: { + store?: RunStore; +} = {}): StreamingSessionHttpController { + const store = options.store ?? createMemoryRunStore(); + const streamer = createDurableChunkStreamer(); + const machine = createAgentMachine({ + id: 'http-streaming-session-example', + schemas: { + input: streamingInputSchema, + output: streamingOutputSchema, + emitted: { + textPart: textPartSchema, + }, + }, + context: (input) => ({ + streamId: input.streamId, + text: input.text, + finalText: '', + }), + initial: 'writing', + states: { + writing: { + resultSchema: streamingOutputSchema, + invoke: async ({ context }, enq) => + streamer.streamText(context.streamId, context.text, (delta) => { + enq.emit({ type: 'textPart', delta }); + }), + onDone: ({ result }) => ({ + target: 'done', + context: { finalText: result.text }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + text: context.finalText, + }), + }, + }, + }); + const activeRuns = new Map(); + + function trackRun(sessionId: string, run: StreamingRun) { + activeRuns.set(sessionId, run); + run.onDone(() => { + activeRuns.delete(sessionId); + }); + run.onError(() => { + activeRuns.delete(sessionId); + }); + return run; + } + + async function getRun(sessionId: string): Promise { + const existing = activeRuns.get(sessionId); + if (existing) { + return existing; + } + + const restored = await restoreSession(machine, { + sessionId, + store, + }) as StreamingRun; + + return trackRun(sessionId, restored); + } + + return { + advance(streamId) { + streamer.advance(streamId); + }, + + dropActiveSession(sessionId) { + activeRuns.delete(sessionId); + }, + + async handle(request) { + const url = new URL(request.url); + const match = url.pathname.match(/^\/sessions\/([^/]+)(?:\/stream)?$/); + const sessionId = match?.[1]; + const isStreamRoute = url.pathname.endsWith('/stream'); + + if (request.method === 'POST' && url.pathname === '/sessions') { + const body = await request.json() as z.infer; + const run = await startSession(machine, { + store, + input: body, + }) as StreamingRun; + trackRun(run.sessionId, run); + + return Response.json({ + sessionId: run.sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'GET' && sessionId && !isStreamRoute) { + const run = await getRun(sessionId); + return Response.json({ + sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'GET' && sessionId && isStreamRoute) { + const run = await getRun(sessionId); + let cleanup = () => {}; + + const stream = new ReadableStream({ + start(controller) { + const encoder = new TextEncoder(); + const write = (event: string, data: unknown) => { + controller.enqueue( + encoder.encode( + `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` + ) + ); + }; + + if (run.getSnapshot().status === 'done') { + write('done', run.getSnapshot().output); + controller.close(); + return; + } + + if (run.getSnapshot().status === 'error') { + write('error', { error: String(run.getSnapshot().error) }); + controller.close(); + return; + } + + const offPart = run.on('textPart', (event) => { + write('textPart', event); + }); + const offDone = run.onDone((event) => { + write('done', event.output); + cleanup(); + controller.close(); + }); + const offError = run.onError((event) => { + write('error', { error: String(event.error) }); + cleanup(); + controller.close(); + }); + + cleanup = () => { + offPart(); + offDone(); + offError(); + }; + }, + cancel() { + // Subscribers are ephemeral transport clients, not run ownership. + // Closing the stream should detach listeners but leave the run alive. + cleanup(); + }, + }); + + return new Response(stream, { + headers: { + 'content-type': 'text/event-stream', + 'cache-control': 'no-cache', + }, + }); + } + + return new Response('Not found', { status: 404 }); + }, + }; +} + +function createDurableChunkStreamer() { + const cursors = new Map(); + const invocations = new Map(); + const waiters = new Map void>>(); + + return { + advance(streamId: string) { + const current = waiters.get(streamId) ?? []; + waiters.set(streamId, []); + for (const resolve of current) { + resolve(); + } + }, + + async streamText( + streamId: string, + text: string, + emit: (delta: string) => void + ) { + const chunks = splitIntoChunks(text); + const invocation = (invocations.get(streamId) ?? 0) + 1; + invocations.set(streamId, invocation); + let cursor = cursors.get(streamId) ?? 0; + + while (cursor < chunks.length) { + if (invocation < (invocations.get(streamId) ?? 0)) { + await new Promise(() => {}); + } + + await new Promise((resolve) => { + waiters.set(streamId, [...(waiters.get(streamId) ?? []), resolve]); + }); + + if (invocation < (invocations.get(streamId) ?? 0)) { + await new Promise(() => {}); + } + + emit(chunks[cursor]!); + cursor += 1; + cursors.set(streamId, cursor); + } + + return { text }; + }, + }; +} + +function splitIntoChunks(text: string): string[] { + if (text.length <= 3) { + return [text]; + } + + return [text.slice(0, 3), text.slice(3)]; +} diff --git a/examples/index.ts b/examples/index.ts index cbd239d..5971975 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -2,6 +2,7 @@ export { createSimpleExample } from './simple.js'; export { createSqlAgentExample } from './sql-agent.js'; export { createHitlExample } from './hitl.js'; export { createPersistenceSessionHttpHandler } from './http-session.js'; +export { createStreamingSessionHttpController } from './http-streaming-session.js'; export { createDecideExample } from './decide.js'; export { createClassifyExample } from './classify.js'; export { createAdapterExample } from './adapter.js'; diff --git a/readme.md b/readme.md index 2346575..2ee9e36 100644 --- a/readme.md +++ b/readme.md @@ -26,6 +26,7 @@ Each example demonstrates one concept: - [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts): tool invocation with emitted `toolCall`, incremental `toolProgress`, and final `toolResult` events - [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts): persisting snapshots and restoring a session in plain runtime code - [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts): runner-agnostic HTTP transport for starting sessions, sending events, and reading snapshots over `Request`/`Response` +- [`examples/http-streaming-session.ts`](/Users/davidkpiano/Code/agent/examples/http-streaming-session.ts): durable SSE transport that reconnects to a restored run and emits only new streaming parts - [`examples/persistent-multi-agent-network.ts`](/Users/davidkpiano/Code/agent/examples/persistent-multi-agent-network.ts): durable multi-agent handoffs that restore from a persisted mid-network snapshot - [`examples/persistent-streaming.ts`](/Users/davidkpiano/Code/agent/examples/persistent-streaming.ts): restore a crashed streaming invoke and continue with only new live emitted parts - [`examples/persistent-supervisor.ts`](/Users/davidkpiano/Code/agent/examples/persistent-supervisor.ts): durable supervisor routing that restores after a persisted retry handoff diff --git a/src/examples.test.ts b/src/examples.test.ts index 80b8d78..a7ab323 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -15,6 +15,7 @@ import { createErrorRetryExample, createHitlExample, createPersistenceSessionHttpHandler, + createStreamingSessionHttpController, createJokeExample, createJugsExample, createMapReduceExample, @@ -38,6 +39,38 @@ import { createTutorExample, } from '../examples/index.js'; +function createSseReader(response: Response) { + const reader = response.body!.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + return { + async next(): Promise<{ event: string; data: unknown }> { + while (true) { + const match = buffer.match(/^event: ([^\n]+)\ndata: ([^\n]+)\n\n/); + if (match) { + buffer = buffer.slice(match[0].length); + return { + event: match[1]!, + data: JSON.parse(match[2]!), + }; + } + + const chunk = await reader.read(); + if (chunk.done) { + throw new Error('SSE stream closed before the next event was available.'); + } + + buffer += decoder.decode(chunk.value, { stream: true }); + } + }, + + async cancel() { + await reader.cancel(); + }, + }; +} + describe('curated examples', () => { test('simple example runs to a final output', async () => { const machine = createSimpleExample(async () => ({ @@ -205,6 +238,84 @@ describe('curated examples', () => { expect(statusBody.snapshot).toEqual(sendBody.snapshot); }); + test('http streaming session example reconnects with only new SSE parts after restore', async () => { + const controller = createStreamingSessionHttpController(); + + const startResponse = await controller.handle( + new Request('https://agent.test/sessions', { + method: 'POST', + body: JSON.stringify({ + streamId: 'stream-1', + text: 'hello', + }), + headers: { + 'content-type': 'application/json', + }, + }) + ); + const startBody = await startResponse.json() as { + sessionId: string; + }; + + const firstStreamResponse = await controller.handle( + new Request(`https://agent.test/sessions/${startBody.sessionId}/stream`) + ); + const firstReader = createSseReader(firstStreamResponse); + + controller.advance('stream-1'); + + await expect(firstReader.next()).resolves.toEqual({ + event: 'textPart', + data: { + type: 'textPart', + delta: 'hel', + }, + }); + + await firstReader.cancel(); + controller.dropActiveSession(startBody.sessionId); + + const secondStreamResponse = await controller.handle( + new Request(`https://agent.test/sessions/${startBody.sessionId}/stream`) + ); + const secondReader = createSseReader(secondStreamResponse); + + controller.advance('stream-1'); + + await expect(secondReader.next()).resolves.toEqual({ + event: 'textPart', + data: { + type: 'textPart', + delta: 'lo', + }, + }); + await expect(secondReader.next()).resolves.toEqual({ + event: 'done', + data: { + text: 'hello', + }, + }); + + const statusResponse = await controller.handle( + new Request(`https://agent.test/sessions/${startBody.sessionId}`) + ); + const statusBody = await statusResponse.json() as { + snapshot: { + value: string; + status: string; + output: { text: string }; + }; + }; + + expect(statusBody.snapshot).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { text: 'hello' }, + }) + ); + }); + test('cloudflare durable network example restores and settles a network run', async () => { const storage = new Map(); const firstInstance = new AgentNetworkDurableObject({ From 23910f13b31fd507323e45b5d0a29b38ab8c7a85 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 25 Apr 2026 11:57:51 -0400 Subject: [PATCH 27/34] feat: add parity matrix and core workflow examples --- docs/langgraph-parity.md | 80 +++++++++++ examples/chatbot-messages.ts | 133 +++++++++++++++++ examples/index.ts | 2 + examples/rag.ts | 134 ++++++++++++++++++ readme.md | 2 + src/examples.test.ts | 63 ++++++++ .../chatbot-messages.test.ts | 47 ++++++ src/langgraph-equivalents/rag.test.ts | 33 +++++ 8 files changed, 494 insertions(+) create mode 100644 docs/langgraph-parity.md create mode 100644 examples/chatbot-messages.ts create mode 100644 examples/rag.ts create mode 100644 src/langgraph-equivalents/chatbot-messages.test.ts create mode 100644 src/langgraph-equivalents/rag.test.ts diff --git a/docs/langgraph-parity.md b/docs/langgraph-parity.md new file mode 100644 index 0000000..00f04f1 --- /dev/null +++ b/docs/langgraph-parity.md @@ -0,0 +1,80 @@ +# LangGraphJS Parity + +## Scope + +This document tracks where `@statelyai/agent` currently matches the practical end result of `langchain-ai/langgraphjs` for core workflow/runtime behavior. + +It is intentionally scoped to: + +- core orchestration concepts +- durable session behavior +- streaming/runtime transport behavior +- runnable examples and tests in this repo + +It is intentionally not scoped to: + +- LangGraph Platform deployment features +- LangGraph Studio +- LangGraph UI / framework SDK packages +- checkpoint backend packages as separate published adapters + +## External reference + +As of April 25, 2026, the upstream `langgraphjs` repo exposes: + +- core packages under [`libs/`](https://github.com/langchain-ai/langgraphjs/tree/main/libs), including `langgraph`, `langgraph-core`, checkpoint packages, supervisor/swarm helpers, SDKs, and UI packages +- runnable examples under [`examples/`](https://github.com/langchain-ai/langgraphjs/tree/main/examples), including quickstart, plan-and-execute, reflection, rewoo, SQL agent, multi-agent, chatbots, RAG, and UI transport examples + +The parity target here is the core graph/runtime layer, not the whole surrounding product/package ecosystem. + +## Matrix + + + +| LangGraphJS concept | Status | Agent equivalent | +| --- | --- | --- | +| Branching / conditional routing | Covered | [`examples/branching.ts`](/Users/davidkpiano/Code/agent/examples/branching.ts), [`src/langgraph-equivalents/branching.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/branching.test.ts) | +| Subgraphs / nested flows | Covered | [`examples/subflow.ts`](/Users/davidkpiano/Code/agent/examples/subflow.ts), [`examples/conditional-subflow.ts`](/Users/davidkpiano/Code/agent/examples/conditional-subflow.ts), [`src/langgraph-equivalents/subflow.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/subflow.test.ts), [`src/langgraph-equivalents/conditional-subflow.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/conditional-subflow.test.ts) | +| Human-in-the-loop / approval gate | Covered | [`examples/hitl.ts`](/Users/davidkpiano/Code/agent/examples/hitl.ts), [`src/langgraph-equivalents/hitl.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/hitl.test.ts) | +| Durable sessions / restore from snapshots + events | Covered | [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts), [`src/langgraph-equivalents/persistence.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/persistence.test.ts) | +| Streaming emitted parts | Covered | [`examples/persistent-streaming.ts`](/Users/davidkpiano/Code/agent/examples/persistent-streaming.ts), [`src/langgraph-equivalents/streaming.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/streaming.test.ts), [`src/langgraph-equivalents/persistent-streaming.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/persistent-streaming.test.ts) | +| Tool calling with intermediate progress | Covered | [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts), [`src/langgraph-equivalents/tool-calling.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/tool-calling.test.ts) | +| Retry loops / explicit recovery | Covered | [`examples/error-retry.ts`](/Users/davidkpiano/Code/agent/examples/error-retry.ts), [`src/langgraph-equivalents/error-retry.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/error-retry.test.ts) | +| Plan-and-execute | Covered | [`examples/plan-and-execute.ts`](/Users/davidkpiano/Code/agent/examples/plan-and-execute.ts), [`src/langgraph-equivalents/plan-and-execute.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/plan-and-execute.test.ts) | +| Map-reduce style workflows | Covered | [`examples/map-reduce.ts`](/Users/davidkpiano/Code/agent/examples/map-reduce.ts), [`src/langgraph-equivalents/map-reduce.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/map-reduce.test.ts) | +| Reflection loop | Covered | [`examples/reflection.ts`](/Users/davidkpiano/Code/agent/examples/reflection.ts), [`src/langgraph-equivalents/reflection.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/reflection.test.ts) | +| ReWOO-style planner / worker decomposition | Covered | [`examples/rewoo.ts`](/Users/davidkpiano/Code/agent/examples/rewoo.ts), [`src/langgraph-equivalents/rewoo.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/rewoo.test.ts) | +| Supervisor routing | Covered | [`examples/supervisor.ts`](/Users/davidkpiano/Code/agent/examples/supervisor.ts), [`examples/persistent-supervisor.ts`](/Users/davidkpiano/Code/agent/examples/persistent-supervisor.ts), [`src/langgraph-equivalents/supervisor.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/supervisor.test.ts), [`src/langgraph-equivalents/persistent-supervisor.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/persistent-supervisor.test.ts) | +| Multi-agent handoffs | Covered | [`examples/multi-agent-network.ts`](/Users/davidkpiano/Code/agent/examples/multi-agent-network.ts), [`examples/persistent-multi-agent-network.ts`](/Users/davidkpiano/Code/agent/examples/persistent-multi-agent-network.ts), [`src/langgraph-equivalents/multi-agent-network.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/multi-agent-network.test.ts), [`src/langgraph-equivalents/persistent-multi-agent-network.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/persistent-multi-agent-network.test.ts) | +| SQL/tool-heavy agent workflow | Covered | [`examples/sql-agent.ts`](/Users/davidkpiano/Code/agent/examples/sql-agent.ts), [`src/langgraph-equivalents/sql-agent.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/sql-agent.test.ts) | +| ReAct-style agent | Covered | [`examples/react-agent-from-scratch.ts`](/Users/davidkpiano/Code/agent/examples/react-agent-from-scratch.ts), [`examples/react-agent.ts`](/Users/davidkpiano/Code/agent/examples/react-agent.ts), [`src/langgraph-equivalents/prebuilt-react.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/prebuilt-react.test.ts) | +| Message-centric chatbot state | Covered | [`examples/chatbot-messages.ts`](/Users/davidkpiano/Code/agent/examples/chatbot-messages.ts), [`src/langgraph-equivalents/chatbot-messages.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/chatbot-messages.test.ts) | +| Retrieval-augmented generation | Covered | [`examples/rag.ts`](/Users/davidkpiano/Code/agent/examples/rag.ts), [`src/langgraph-equivalents/rag.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/rag.test.ts) | +| HTTP session transport | Covered | [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts), [`src/examples.test.ts`](/Users/davidkpiano/Code/agent/src/examples.test.ts) | +| Durable HTTP streaming transport / reconnect | Covered | [`examples/http-streaming-session.ts`](/Users/davidkpiano/Code/agent/examples/http-streaming-session.ts), [`src/examples.test.ts`](/Users/davidkpiano/Code/agent/src/examples.test.ts) | +| Graph export / visualization support | Covered | [`src/graph/index.ts`](/Users/davidkpiano/Code/agent/src/graph/index.ts), [`src/xstate/index.ts`](/Users/davidkpiano/Code/agent/src/xstate/index.ts), [`src/langgraph-equivalents/graph.test.ts`](/Users/davidkpiano/Code/agent/src/langgraph-equivalents/graph.test.ts) | + +## Intentional differences + +These are currently deliberate, not gaps: + +- Logic stays pure: `(state, event) -> { nextState, effects }`. +- Emitted events are live runtime effects, not durable journal entries. +- Durable behavior is based on first-class snapshot + event persistence rather than in-memory graph execution with optional add-ons. +- `run.on(...)` is reserved for emitted events only; terminal/runtime hooks use dedicated methods like `run.onDone(...)`. +- Parallelism is expected to be expressed in plain JavaScript where possible, rather than forcing a dedicated graph primitive when `Promise.all(...)` is enough. + +## Still missing or intentionally out of scope + +These are the main areas not yet covered by a first-class parity example: + +- swarm-specific helper APIs comparable to `libs/langgraph-swarm` +- published checkpoint backends as separate installable packages +- UI framework transport examples comparable to `examples/ui-react`, `examples/ui-svelte`, etc. +- platform-only features such as threads, cron jobs, Studio, and deployment APIs + +## Recommended next wave + +1. Decide whether swarm/supervisor helper packages should exist as additive libraries or remain plain examples. +2. Decide whether storage adapters should stay example-level or become installable packages. +3. Only after that, consider UI transport helpers if package surface matters beyond examples. diff --git a/examples/chatbot-messages.ts b/examples/chatbot-messages.ts new file mode 100644 index 0000000..85ab4e8 --- /dev/null +++ b/examples/chatbot-messages.ts @@ -0,0 +1,133 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const messageSchema = z.object({ + role: z.enum(['user', 'assistant']), + content: z.string(), +}); + +const replySchema = z.object({ + message: messageSchema, +}); + +export function createChatbotMessagesExample( + reply: (messages: Array>) => Promise> = (messages) => + generateExampleObject({ + schema: replySchema, + system: 'You are a concise assistant in a terminal chat.', + prompt: [ + 'Write the next assistant message for this conversation.', + '', + ...messages.map((message) => `${message.role}: ${message.content}`), + ].join('\n'), + }) +) { + return createAgentMachine({ + id: 'chatbot-messages-example', + schemas: { + output: z.object({ + messages: z.array(messageSchema), + finalMessage: messageSchema.nullable(), + }), + events: { + 'messages.user': z.object({ + message: messageSchema.extend({ + role: z.literal('user'), + }), + }), + 'messages.end': z.object({}), + }, + }, + context: () => ({ + messages: [] as Array>, + finalMessage: null as z.infer | null, + ended: false, + }), + initial: 'waitingForUser', + states: { + waitingForUser: { + on: { + 'messages.user': ({ event, context }) => ({ + target: 'replying', + context: { + messages: [...context.messages, event.message], + }, + }), + 'messages.end': { + target: 'done', + context: { ended: true }, + }, + }, + }, + replying: { + resultSchema: replySchema, + invoke: async ({ context }) => reply(context.messages), + onDone: ({ result, context }) => ({ + target: 'waitingForUser', + context: { + messages: [...context.messages, result.message], + finalMessage: result.message, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + messages: context.messages, + finalMessage: context.finalMessage, + }), + }, + }, + }); +} + +async function main() { + try { + const machine = createChatbotMessagesExample(); + let state = machine.getInitialState(); + let lastPrintedAssistantMessage: string | null = null; + + while (true) { + const result = await machine.execute(state); + + if (result.status === 'done') { + break; + } + + if (result.status !== 'pending') { + throw new Error('Chatbot messages example entered an unexpected error state.'); + } + + if ( + result.context.finalMessage?.role === 'assistant' + && result.context.finalMessage.content !== lastPrintedAssistantMessage + ) { + console.log(`Assistant: ${result.context.finalMessage.content}`); + lastPrintedAssistantMessage = result.context.finalMessage.content; + } + + const content = await prompt('User (blank to exit)'); + state = machine.transition( + result.state, + content + ? { + type: 'messages.user', + message: { role: 'user', content }, + } + : { type: 'messages.end' } + ); + } + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/index.ts b/examples/index.ts index 5971975..652ebf9 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -7,6 +7,7 @@ export { createDecideExample } from './decide.js'; export { createClassifyExample } from './classify.js'; export { createAdapterExample } from './adapter.js'; export { createChatbotExample } from './chatbot.js'; +export { createChatbotMessagesExample } from './chatbot-messages.js'; export { createConditionalSubflowExample } from './conditional-subflow.js'; export { AgentSessionDurableObject, @@ -38,6 +39,7 @@ export { runPersistentSupervisorExample, } from './persistent-supervisor.js'; export { createRaffleExample } from './raffle.js'; +export { createRagExample } from './rag.js'; export { createReactAgentExample } from './react-agent.js'; export { createReactAgentFromScratch, diff --git a/examples/rag.ts b/examples/rag.ts new file mode 100644 index 0000000..abd9772 --- /dev/null +++ b/examples/rag.ts @@ -0,0 +1,134 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const retrievedDocumentSchema = z.object({ + id: z.string(), + content: z.string(), +}); + +const retrievedDocumentsSchema = z.object({ + documents: z.array(retrievedDocumentSchema), +}); + +const answerSchema = z.object({ + answer: z.string(), +}); + +export function createRagExample( + options: { + retrieve?: (question: string) => Promise>; + answer?: (args: { + question: string; + documents: Array>; + }) => Promise>; + } = {} +) { + const retrieve = + options.retrieve ?? + ((question: string) => + Promise.resolve({ + documents: [ + { + id: 'doc-1', + content: `Context about: ${question}`, + }, + { + id: 'doc-2', + content: `Additional supporting detail for: ${question}`, + }, + ], + })); + + const answer = + options.answer ?? + ((args: { + question: string; + documents: Array>; + }) => + generateExampleObject({ + schema: answerSchema, + system: 'Answer the question using only the retrieved documents.', + prompt: [ + `Question: ${args.question}`, + '', + 'Documents:', + ...args.documents.map((document) => `- [${document.id}] ${document.content}`), + ].join('\n'), + })); + + return createAgentMachine({ + id: 'rag-example', + schemas: { + input: z.object({ + question: z.string(), + }), + output: z.object({ + question: z.string(), + documents: z.array(retrievedDocumentSchema), + answer: z.string().nullable(), + }), + }, + context: (input) => ({ + question: input.question, + documents: [] as Array>, + answer: null as string | null, + }), + initial: 'retrieving', + states: { + retrieving: { + resultSchema: retrievedDocumentsSchema, + invoke: async ({ context }) => retrieve(context.question), + onDone: ({ result }) => ({ + target: 'answering', + context: { documents: result.documents }, + }), + }, + answering: { + resultSchema: answerSchema, + invoke: async ({ context }) => + answer({ + question: context.question, + documents: context.documents, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { answer: result.answer }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + question: context.question, + documents: context.documents, + answer: context.answer, + }), + }, + }, + }); +} + +async function main() { + try { + const question = await prompt('Question'); + const machine = createRagExample(); + const result = await machine.execute( + machine.getInitialState({ question }) + ); + + if (result.status === 'done') { + console.log(result.output); + } + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/readme.md b/readme.md index 2ee9e36..4a445fd 100644 --- a/readme.md +++ b/readme.md @@ -22,7 +22,9 @@ Each example demonstrates one concept: - [`examples/simple.ts`](/Users/davidkpiano/Code/agent/examples/simple.ts): the smallest `createAgentMachine(...)` flow with an `invoke` state that makes a real LLM call - [`examples/hitl.ts`](/Users/davidkpiano/Code/agent/examples/hitl.ts): a human-in-the-loop machine that pauses in a pending state, accepts typed events, and drafts with an LLM after approval - [`examples/error-retry.ts`](/Users/davidkpiano/Code/agent/examples/error-retry.ts): retrying transient invoke failures through explicit internal error events +- [`examples/chatbot-messages.ts`](/Users/davidkpiano/Code/agent/examples/chatbot-messages.ts): message-centric chat state with structured `{ role, content }` accumulation across turns - [`examples/conditional-subflow.ts`](/Users/davidkpiano/Code/agent/examples/conditional-subflow.ts): routing directly into one of multiple child subflows from parent input +- [`examples/rag.ts`](/Users/davidkpiano/Code/agent/examples/rag.ts): retrieval-augmented generation with explicit retrieve and answer states - [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts): tool invocation with emitted `toolCall`, incremental `toolProgress`, and final `toolResult` events - [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts): persisting snapshots and restoring a session in plain runtime code - [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts): runner-agnostic HTTP transport for starting sessions, sending events, and reading snapshots over `Request`/`Response` diff --git a/src/examples.test.ts b/src/examples.test.ts index a7ab323..bb160a3 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -11,6 +11,7 @@ import { createConditionalSubflowExample, createCustomerServiceSimExample, createDecideExample, + createChatbotMessagesExample, createEmailExample, createErrorRetryExample, createHitlExample, @@ -27,6 +28,7 @@ import { runPersistentSupervisorExample, createPlanAndExecuteExample, createRaffleExample, + createRagExample, createReactAgentExample, createRewooExample, createReflectionExample, @@ -86,6 +88,67 @@ describe('curated examples', () => { } }); + test('chatbot messages example accumulates structured conversation turns', async () => { + const machine = createChatbotMessagesExample(async (messages) => ({ + message: { + role: 'assistant', + content: `Replying to: ${messages.at(-1)?.content ?? ''}`, + }, + })); + + const afterUserMessage = machine.transition(machine.getInitialState(), { + type: 'messages.user', + message: { + role: 'user', + content: 'Hello there', + }, + }); + const result = await machine.execute(afterUserMessage); + + expect(result.status).toBe('pending'); + if (result.status === 'pending') { + expect(result.context.messages).toEqual([ + { role: 'user', content: 'Hello there' }, + { role: 'assistant', content: 'Replying to: Hello there' }, + ]); + expect(result.context.finalMessage).toEqual({ + role: 'assistant', + content: 'Replying to: Hello there', + }); + } + }); + + test('rag example retrieves context and produces a grounded answer', async () => { + const machine = createRagExample({ + retrieve: async (question) => ({ + documents: [ + { id: 'doc-1', content: `${question} :: first fact` }, + { id: 'doc-2', content: `${question} :: second fact` }, + ], + }), + answer: async ({ question, documents }) => ({ + answer: `${question} => ${documents.map((document) => document.content).join(' | ')}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ question: 'What is LangGraph?' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + question: 'What is LangGraph?', + documents: [ + { id: 'doc-1', content: 'What is LangGraph? :: first fact' }, + { id: 'doc-2', content: 'What is LangGraph? :: second fact' }, + ], + answer: + 'What is LangGraph? => What is LangGraph? :: first fact | What is LangGraph? :: second fact', + }); + } + }); + test('persistence example restores a durable session to the same final snapshot', async () => { const result = await runPersistenceExample( { request: 'Approve the annual budget summary.' }, diff --git a/src/langgraph-equivalents/chatbot-messages.test.ts b/src/langgraph-equivalents/chatbot-messages.test.ts new file mode 100644 index 0000000..602445f --- /dev/null +++ b/src/langgraph-equivalents/chatbot-messages.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from 'vitest'; +import { createChatbotMessagesExample } from '../../examples/index.js'; + +test('message-centric chatbot workflow accumulates structured messages across turns', async () => { + const machine = createChatbotMessagesExample(async (messages) => ({ + message: { + role: 'assistant', + content: `Replying to: ${messages.at(-1)?.content ?? ''}`, + }, + })); + + const afterFirstTurn = machine.transition(machine.getInitialState(), { + type: 'messages.user', + message: { + role: 'user', + content: 'Hello there', + }, + }); + const firstResult = await machine.execute(afterFirstTurn); + + expect(firstResult.status).toBe('pending'); + if (firstResult.status === 'pending') { + expect(firstResult.context.messages).toEqual([ + { role: 'user', content: 'Hello there' }, + { role: 'assistant', content: 'Replying to: Hello there' }, + ]); + + const afterSecondTurn = machine.transition(firstResult.state, { + type: 'messages.user', + message: { + role: 'user', + content: 'Can you expand on that?', + }, + }); + const secondResult = await machine.execute(afterSecondTurn); + + expect(secondResult.status).toBe('pending'); + if (secondResult.status === 'pending') { + expect(secondResult.context.messages).toEqual([ + { role: 'user', content: 'Hello there' }, + { role: 'assistant', content: 'Replying to: Hello there' }, + { role: 'user', content: 'Can you expand on that?' }, + { role: 'assistant', content: 'Replying to: Can you expand on that?' }, + ]); + } + } +}); diff --git a/src/langgraph-equivalents/rag.test.ts b/src/langgraph-equivalents/rag.test.ts new file mode 100644 index 0000000..c896759 --- /dev/null +++ b/src/langgraph-equivalents/rag.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from 'vitest'; +import { createRagExample } from '../../examples/index.js'; + +test('rag workflow retrieves documents and synthesizes a grounded answer', async () => { + const machine = createRagExample({ + retrieve: async (question) => ({ + documents: [ + { id: 'doc-1', content: `${question} :: first fact` }, + { id: 'doc-2', content: `${question} :: second fact` }, + ], + }), + answer: async ({ question, documents }) => ({ + answer: `${question} => ${documents.map((document) => document.content).join(' | ')}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ question: 'What is LangGraph?' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + question: 'What is LangGraph?', + documents: [ + { id: 'doc-1', content: 'What is LangGraph? :: first fact' }, + { id: 'doc-2', content: 'What is LangGraph? :: second fact' }, + ], + answer: + 'What is LangGraph? => What is LangGraph? :: first fact | What is LangGraph? :: second fact', + }); + } +}); From 59161ce74d083bde4d040514a91c2e4a5647f847 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sat, 25 Apr 2026 12:24:58 -0400 Subject: [PATCH 28/34] feat: add ai sdk and cloudflare agents examples --- examples/ai-sdk.ts | 166 +++++ examples/cloudflare-agents.ts | 191 +++++ examples/index.ts | 6 + package.json | 1 + pnpm-lock.yaml | 1325 ++++++++++++++++++++++++++++++++- readme.md | 4 +- src/ai-sdk/index.test.ts | 82 ++ src/ai-sdk/index.ts | 40 +- src/examples.test.ts | 75 ++ 9 files changed, 1875 insertions(+), 15 deletions(-) create mode 100644 examples/ai-sdk.ts create mode 100644 examples/cloudflare-agents.ts create mode 100644 src/ai-sdk/index.test.ts diff --git a/examples/ai-sdk.ts b/examples/ai-sdk.ts new file mode 100644 index 0000000..c60f730 --- /dev/null +++ b/examples/ai-sdk.ts @@ -0,0 +1,166 @@ +import { generateText, Output } from 'ai'; +import { z } from 'zod'; +import { + createAgentMachine, + decide, + decideResultSchema, + type AgentAdapter, +} from '../src/index.js'; +import { createAiSdkAdapter } from '../src/ai-sdk/index.js'; +import { + closePrompt, + createExampleModel, + formatResult, + isMain, + prompt, +} from './_run.js'; + +const routeOptions = { + billing: { + description: 'Handle invoices, refunds, subscription charges, and payment issues.', + schema: z.object({ + confidence: z.number().min(0).max(1), + }), + }, + support: { + description: 'Handle product usage questions and troubleshooting requests.', + schema: z.object({ + confidence: z.number().min(0).max(1), + }), + }, +} as const; + +const replySchema = z.object({ + subject: z.string(), + body: z.string(), +}); + +type Route = keyof typeof routeOptions; + +export function createAiSdkExample(options: { + adapter?: AgentAdapter; + draftReply?: (args: { + route: Route; + confidence: number; + message: string; + }) => Promise>; +} = {}) { + const adapter = + options.adapter ?? + createAiSdkAdapter({ + resolveModel: (model) => createExampleModel(model), + }); + + const draftReply = + options.draftReply ?? + (async ({ + route, + confidence, + message, + }: { + route: Route; + confidence: number; + message: string; + }) => { + const result = await generateText({ + model: createExampleModel('openai/gpt-5.4-nano'), + system: [ + 'Draft a concise support email.', + `Route: ${route}`, + `Classifier confidence: ${confidence.toFixed(2)}`, + 'Return structured output with a subject and body.', + ].join('\n'), + prompt: message, + output: Output.object({ + schema: replySchema, + }), + }); + + return result.output as z.infer; + }); + + return createAgentMachine({ + id: 'ai-sdk-example', + schemas: { + input: z.object({ message: z.string() }), + output: z.object({ + route: z.enum(['billing', 'support']).nullable(), + confidence: z.number().nullable(), + subject: z.string().nullable(), + body: z.string().nullable(), + }), + }, + context: (input) => ({ + message: input.message, + route: null as Route | null, + confidence: null as number | null, + subject: null as string | null, + body: null as string | null, + }), + initial: 'route', + states: { + route: { + resultSchema: decideResultSchema(routeOptions), + invoke: async ({ context }) => + decide({ + adapter, + model: 'openai/gpt-5.4-nano', + prompt: [ + 'Route this inbound customer message.', + '', + context.message, + ].join('\n'), + options: routeOptions, + }), + onDone: ({ result }) => ({ + target: 'drafting', + context: { + route: result.choice, + confidence: result.data.confidence, + }, + }), + }, + drafting: { + resultSchema: replySchema, + invoke: async ({ context }) => + draftReply({ + route: context.route ?? 'support', + confidence: context.confidence ?? 0, + message: context.message, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { + subject: result.subject, + body: result.body, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + route: context.route, + confidence: context.confidence, + subject: context.subject, + body: context.body, + }), + }, + }, + }); +} + +async function main() { + try { + const message = await prompt('Customer message'); + const machine = createAiSdkExample(); + const result = await machine.execute(machine.getInitialState({ message })); + + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/cloudflare-agents.ts b/examples/cloudflare-agents.ts new file mode 100644 index 0000000..8994231 --- /dev/null +++ b/examples/cloudflare-agents.ts @@ -0,0 +1,191 @@ +import { + restoreSession, + startSession, + type JournalEventRecord, + type PersistedSnapshot, + type RunStore, +} from '../src/index.js'; +import { createPersistenceExample } from './persistence.js'; + +type SessionEntry = { + events: JournalEventRecord[]; + snapshot: PersistedSnapshot | null; +}; + +export type CloudflareAgentRunStoreState = { + sessions: Record; +}; + +export interface CloudflareAgentsExampleArtifacts { + ReviewWorkflowAgent: new (...args: any[]) => { + onRequest(request: Request): Promise; + }; + worker: { + fetch(request: Request, env: Record): Promise; + }; +} + +export function createCloudflareAgentRunStore(options: { + getState: () => CloudflareAgentRunStoreState; + setState: ( + nextState: CloudflareAgentRunStoreState + ) => void | Promise; +}): RunStore { + return { + async append(sessionId, event) { + const currentState = options.getState(); + const currentSession = currentState.sessions[sessionId] ?? { + events: [], + snapshot: null, + }; + const sequence = currentSession.events.length + 1; + const nextSession: SessionEntry = { + ...currentSession, + events: [...currentSession.events, { ...event, sequence }], + }; + + await options.setState({ + ...currentState, + sessions: { + ...currentState.sessions, + [sessionId]: nextSession, + }, + }); + + return { sequence }; + }, + + async loadEvents(sessionId, afterSequence = 0) { + return ( + options.getState().sessions[sessionId]?.events.filter( + (event) => event.sequence > afterSequence + ) ?? [] + ); + }, + + async loadLatestSnapshot(sessionId) { + return options.getState().sessions[sessionId]?.snapshot ?? null; + }, + + async saveSnapshot(snapshot) { + const currentState = options.getState(); + const currentSession = currentState.sessions[snapshot.sessionId] ?? { + events: [], + snapshot: null, + }; + + await options.setState({ + ...currentState, + sessions: { + ...currentState.sessions, + [snapshot.sessionId]: { + ...currentSession, + snapshot, + }, + }, + }); + }, + }; +} + +/** + * Cloudflare's `agents` package imports `cloudflare:` modules, so this example + * keeps that import lazy to stay loadable in plain Node. In a real Worker, + * move the `agents` imports to top-level imports. + */ +export async function createCloudflareAgentsExample(): Promise { + const { Agent, routeAgentRequest } = await import('agents'); + const machine = createPersistenceExample(); + + class ReviewWorkflowAgent extends Agent< + Record, + CloudflareAgentRunStoreState + > { + initialState: CloudflareAgentRunStoreState = { + sessions: {}, + }; + + private getStore(): RunStore { + return createCloudflareAgentRunStore({ + getState: () => this.state ?? this.initialState, + setState: (nextState) => this.setState(nextState), + }); + } + + async onRequest(request: Request): Promise { + const url = new URL(request.url); + + if (request.method === 'POST' && url.pathname.endsWith('/start')) { + const body = await request.json() as { request: string }; + const run = await startSession(machine, { + store: this.getStore(), + input: { + request: body.request, + }, + }); + + return Response.json({ + sessionId: run.sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'POST' && url.pathname.endsWith('/events')) { + const body = await request.json() as { + sessionId: string; + event: { type: 'approve' }; + }; + const run = await restoreSession(machine, { + sessionId: body.sessionId, + store: this.getStore(), + }); + + await run.send(body.event); + + return Response.json({ + sessionId: body.sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'GET' && url.pathname.endsWith('/snapshot')) { + const sessionId = requiredSessionId(url); + const run = await restoreSession(machine, { + sessionId, + store: this.getStore(), + }); + + return Response.json({ + sessionId, + snapshot: run.getSnapshot(), + }); + } + + return new Response('Not found', { status: 404 }); + } + } + + const worker = { + async fetch(request: Request, env: Record) { + return ( + await routeAgentRequest(request, env, { + prefix: '/agents', + }) + ) ?? new Response('Not found', { status: 404 }); + }, + }; + + return { + ReviewWorkflowAgent, + worker, + } satisfies CloudflareAgentsExampleArtifacts; +} + +function requiredSessionId(url: URL): string { + const sessionId = url.searchParams.get('sessionId'); + if (!sessionId) { + throw new Error('Missing sessionId'); + } + + return sessionId; +} diff --git a/examples/index.ts b/examples/index.ts index 652ebf9..789af5e 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -6,9 +6,15 @@ export { createStreamingSessionHttpController } from './http-streaming-session.j export { createDecideExample } from './decide.js'; export { createClassifyExample } from './classify.js'; export { createAdapterExample } from './adapter.js'; +export { createAiSdkExample } from './ai-sdk.js'; export { createChatbotExample } from './chatbot.js'; export { createChatbotMessagesExample } from './chatbot-messages.js'; export { createConditionalSubflowExample } from './conditional-subflow.js'; +export { + createCloudflareAgentRunStore, + createCloudflareAgentsExample, + type CloudflareAgentRunStoreState, +} from './cloudflare-agents.js'; export { AgentSessionDurableObject, createDurableObjectRunStore, diff --git a/package.json b/package.json index 1db8271..42e66f7 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.27.9", "@types/node": "^20.16.10", + "agents": "0.11.5", "dotenv": "^16.4.5", "tsdown": "^0.21.7", "tsx": "^4.21.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e25004..4053207 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: '@types/node': specifier: ^20.16.10 version: 20.19.30 + agents: + specifier: 0.11.5 + version: 0.11.5(@babel/core@7.29.0)(@babel/runtime@7.28.6)(@cloudflare/workers-types@4.20260424.1)(ai@6.0.67(zod@4.3.6))(react@19.2.5)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(vite@5.4.21(@types/node@20.19.30))(zod@4.3.6) dotenv: specifier: ^16.4.5 version: 16.6.1 @@ -73,31 +76,149 @@ packages: resolution: {integrity: sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.7.tgz} engines: {node: '>=18'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==, tarball: https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==, tarball: https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==, tarball: https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==, tarball: https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz} + engines: {node: '>=6.9.0'} + '@babel/generator@8.0.0-rc.3': resolution: {integrity: sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==, tarball: https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.3.tgz} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==, tarball: https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==, tarball: https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==, tarball: https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==, tarball: https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==, tarball: https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==, tarball: https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==, tarball: https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==, tarball: https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==, tarball: https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==, tarball: https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==, tarball: https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==, tarball: https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@8.0.0-rc.3': resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==, tarball: https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.3.tgz} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==, tarball: https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.0-rc.3': resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==, tarball: https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.3.tgz} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==, tarball: https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==, tarball: https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==, tarball: https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/parser@8.0.0-rc.3': resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==, tarball: https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.3.tgz} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + '@babel/plugin-proposal-decorators@7.29.0': + resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==, tarball: https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==, tarball: https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime-corejs3@7.29.2': + resolution: {integrity: sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==, tarball: https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.2.tgz} + engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.6': resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==, tarball: https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==, tarball: https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==, tarball: https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==, tarball: https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz} + engines: {node: '>=6.9.0'} + '@babel/types@8.0.0-rc.3': resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==, tarball: https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.3.tgz} engines: {node: ^20.19.0 || >=22.12.0} + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==, tarball: https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz} + '@changesets/apply-release-plan@7.0.14': resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==, tarball: https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.14.tgz} @@ -159,6 +280,9 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==, tarball: https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz} + '@cloudflare/workers-types@4.20260424.1': + resolution: {integrity: sha512-0DLJ9yEk1KKzPbqop80Gw/P1wkKKzawmipULiJWdBXIBCoMvE0OVWms3IrL/Q/G7tfmPop9yF4XlZ69k9JLYng==, tarball: https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260424.1.tgz} + '@emnapi/core@1.9.1': resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==, tarball: https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz} @@ -462,6 +586,12 @@ packages: cpu: [x64] os: [win32] + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==, tarball: https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==, tarball: https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz} engines: {node: '>=18'} @@ -474,6 +604,9 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, tarball: https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, tarball: https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, tarball: https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz} engines: {node: '>=6.0.0'} @@ -490,6 +623,16 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==, tarball: https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==, tarball: https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@napi-rs/wasm-runtime@1.1.2': resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==, tarball: https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz} peerDependencies: @@ -613,6 +756,23 @@ packages: cpu: [x64] os: [win32] + '@rolldown/plugin-babel@0.2.3': + resolution: {integrity: sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==, tarball: https://registry.npmjs.org/@rolldown/plugin-babel/-/plugin-babel-0.2.3.tgz} + engines: {node: '>=22.12.0 || ^24.0.0'} + peerDependencies: + '@babel/core': ^7.29.0 || ^8.0.0-rc.1 + '@babel/plugin-transform-runtime': ^7.29.0 || ^8.0.0-rc.1 + '@babel/runtime': ^7.27.0 || ^8.0.0-rc.1 + rolldown: ^1.0.0-rc.5 + vite: ^8.0.0 + peerDependenciesMeta: + '@babel/plugin-transform-runtime': + optional: true + '@babel/runtime': + optional: true + vite: + optional: true + '@rolldown/pluginutils@1.0.0-rc.12': resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==, tarball: https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz} @@ -828,12 +988,54 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==, tarball: https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==, tarball: https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz} + engines: {node: '>= 0.6'} + + agents@0.11.5: + resolution: {integrity: sha512-1wPkA7OOfEdR4GKwaBmqdnZkOxutN2mCsolVU4ekg5QxrTLnC9Vz9LyZPcGqV2ldyfpUY7R73AUqtig5iYRLvQ==, tarball: https://registry.npmjs.org/agents/-/agents-0.11.5.tgz} + hasBin: true + peerDependencies: + '@cloudflare/ai-chat': '>=0.0.8 <1.0.0' + '@cloudflare/codemode': '>=0.0.7 <1.0.0' + '@tanstack/ai': '>=0.10.2 <1.0.0' + '@x402/core': ^2.0.0 + '@x402/evm': ^2.0.0 + ai: ^6.0.0 + react: ^19.0.0 + vite: '>=6.0.0 <9.0.0' + zod: ^4.0.0 + peerDependenciesMeta: + '@cloudflare/ai-chat': + optional: true + '@cloudflare/codemode': + optional: true + '@tanstack/ai': + optional: true + '@x402/core': + optional: true + '@x402/evm': + optional: true + vite: + optional: true + ai@6.0.67: resolution: {integrity: sha512-xBnTcByHCj3OcG6V8G1s6zvSEqK0Bdiu+IEXYcpGrve1iGFFRgcrKeZtr/WAW/7gupnSvBbDF24BEv1OOfqi1g==, tarball: https://registry.npmjs.org/ai/-/ai-6.0.67.tgz} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==, tarball: https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==, tarball: https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==, tarball: https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz} engines: {node: '>=6'} @@ -842,6 +1044,14 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, tarball: https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz} engines: {node: '>=8'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==, tarball: https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==, tarball: https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz} + engines: {node: '>=12'} + ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==, tarball: https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz} engines: {node: '>=14'} @@ -864,6 +1074,11 @@ packages: resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==, tarball: https://registry.npmjs.org/ast-kit/-/ast-kit-3.0.0-beta.1.tgz} engines: {node: '>=20.19.0'} + baseline-browser-mapping@2.10.21: + resolution: {integrity: sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==, tarball: https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz} + engines: {node: '>=6.0.0'} + hasBin: true + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==, tarball: https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz} engines: {node: '>=4'} @@ -871,10 +1086,23 @@ packages: birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==, tarball: https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==, tarball: https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz} + engines: {node: '>=18'} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, tarball: https://registry.npmjs.org/braces/-/braces-3.0.3.tgz} engines: {node: '>=8'} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==, tarball: https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==, tarball: https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==, tarball: https://registry.npmjs.org/cac/-/cac-6.7.14.tgz} engines: {node: '>=8'} @@ -883,6 +1111,17 @@ packages: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==, tarball: https://registry.npmjs.org/cac/-/cac-7.0.0.tgz} engines: {node: '>=20.19.0'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, tarball: https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, tarball: https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001790: + resolution: {integrity: sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==, tarball: https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==, tarball: https://registry.npmjs.org/chai/-/chai-5.3.3.tgz} engines: {node: '>=18'} @@ -898,6 +1137,40 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==, tarball: https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz} engines: {node: '>=8'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==, tarball: https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz} + engines: {node: '>=20'} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==, tarball: https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==, tarball: https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, tarball: https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==, tarball: https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==, tarball: https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz} + engines: {node: '>= 0.6'} + + core-js-pure@3.49.0: + resolution: {integrity: sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==, tarball: https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==, tarball: https://registry.npmjs.org/cors/-/cors-2.8.6.tgz} + engines: {node: '>= 0.10'} + + cron-schedule@6.0.0: + resolution: {integrity: sha512-BoZaseYGXOo5j5HUwTaegIog3JJbuH4BbrY9A1ArLjXpy+RWb3mV28F/9Gv1dDA7E2L8kngWva4NWisnLTyfgQ==, tarball: https://registry.npmjs.org/cron-schedule/-/cron-schedule-6.0.0.tgz} + engines: {node: '>=20'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, tarball: https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz} engines: {node: '>= 8'} @@ -921,6 +1194,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==, tarball: https://registry.npmjs.org/defu/-/defu-6.1.4.tgz} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, tarball: https://registry.npmjs.org/depd/-/depd-2.0.0.tgz} + engines: {node: '>= 0.8'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==, tarball: https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz} engines: {node: '>=8'} @@ -946,17 +1223,46 @@ packages: oxc-resolver: optional: true + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, tarball: https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, tarball: https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz} + + electron-to-chromium@1.5.344: + resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==, tarball: https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==, tarball: https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz} + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==, tarball: https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz} engines: {node: '>=14'} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, tarball: https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz} + engines: {node: '>= 0.8'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==, tarball: https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz} engines: {node: '>=8.6'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, tarball: https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, tarball: https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==, tarball: https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==, tarball: https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==, tarball: https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz} engines: {node: '>=12'} @@ -967,6 +1273,13 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, tarball: https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, tarball: https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, tarball: https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz} engines: {node: '>=4'} @@ -975,21 +1288,48 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, tarball: https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==, tarball: https://registry.npmjs.org/etag/-/etag-1.8.1.tgz} + engines: {node: '>= 0.6'} + + event-target-polyfill@0.0.4: + resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==, tarball: https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz} + eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==, tarball: https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz} engines: {node: '>=18.0.0'} + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==, tarball: https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==, tarball: https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz} engines: {node: '>=12.0.0'} + express-rate-limit@8.4.0: + resolution: {integrity: sha512-gDK8yiqKxrGta+3WtON59arrrw6GLmadA1qoFgYXzdcch8fmKDID2XqO8itsi3f1wufXYPT51387dN6cvVBS3Q==, tarball: https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.0.tgz} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==, tarball: https://registry.npmjs.org/express/-/express-5.2.1.tgz} + engines: {node: '>= 18'} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==, tarball: https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, tarball: https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==, tarball: https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz} engines: {node: '>=8.6.0'} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==, tarball: https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==, tarball: https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz} @@ -1006,10 +1346,22 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, tarball: https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==, tarball: https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==, tarball: https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz} engines: {node: '>=8'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==, tarball: https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==, tarball: https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz} + engines: {node: '>= 0.8'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==, tarball: https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz} engines: {node: '>=6 <7 || >=8'} @@ -1023,6 +1375,29 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, tarball: https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, tarball: https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, tarball: https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==, tarball: https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, tarball: https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, tarball: https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz} + engines: {node: '>= 0.4'} + get-tsconfig@4.13.7: resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==, tarball: https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz} @@ -1034,12 +1409,32 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==, tarball: https://registry.npmjs.org/globby/-/globby-11.1.0.tgz} engines: {node: '>=10'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, tarball: https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, tarball: https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, tarball: https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==, tarball: https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz} + engines: {node: '>= 0.4'} + + hono@4.12.15: + resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==, tarball: https://registry.npmjs.org/hono/-/hono-4.12.15.tgz} + engines: {node: '>=16.9.0'} + hookable@6.1.0: resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==, tarball: https://registry.npmjs.org/hookable/-/hookable-6.1.0.tgz} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==, tarball: https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz} + engines: {node: '>= 0.8'} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==, tarball: https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz} hasBin: true @@ -1056,6 +1451,17 @@ packages: resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==, tarball: https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.2.5.tgz} engines: {node: '>=20.19.0'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, tarball: https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==, tarball: https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==, tarball: https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz} + engines: {node: '>= 0.10'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, tarball: https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz} engines: {node: '>=0.10.0'} @@ -1068,6 +1474,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, tarball: https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz} engines: {node: '>=0.12.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==, tarball: https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz} + is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==, tarball: https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz} engines: {node: '>=4'} @@ -1079,6 +1488,15 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, tarball: https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz} + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==, tarball: https://registry.npmjs.org/jose/-/jose-6.2.2.tgz} + + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==, tarball: https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, tarball: https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz} + js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==, tarball: https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz} hasBin: true @@ -1092,9 +1510,20 @@ packages: engines: {node: '>=6'} hasBin: true + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==, tarball: https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==, tarball: https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==, tarball: https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, tarball: https://registry.npmjs.org/json5/-/json5-2.2.3.tgz} + engines: {node: '>=6'} + hasBin: true + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==, tarball: https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz} @@ -1108,9 +1537,24 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==, tarball: https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, tarball: https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, tarball: https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, tarball: https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==, tarball: https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==, tarball: https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, tarball: https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz} engines: {node: '>= 8'} @@ -1119,6 +1563,25 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, tarball: https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, tarball: https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==, tarball: https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, tarball: https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==, tarball: https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz} + engines: {node: '>=18'} + + mimetext@3.0.28: + resolution: {integrity: sha512-eQXpbNrtxLCjUtiVbR/qR09dbPgZ2o+KR1uA7QKqGhbn8QV7HIL16mXXsobBL4/8TqoYh1us31kfz+dNfCev9g==, tarball: https://registry.npmjs.org/mimetext/-/mimetext-3.0.28.tgz} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==, tarball: https://registry.npmjs.org/mri/-/mri-1.2.0.tgz} engines: {node: '>=4'} @@ -1131,6 +1594,15 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.9: + resolution: {integrity: sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz} + engines: {node: ^18 || >=20} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==, tarball: https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz} + engines: {node: '>= 0.6'} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==, tarball: https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz} engines: {node: 4.x || >=6.0.0} @@ -1140,9 +1612,27 @@ packages: encoding: optional: true + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==, tarball: https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, tarball: https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, tarball: https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz} + engines: {node: '>= 0.4'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==, tarball: https://registry.npmjs.org/obug/-/obug-2.1.1.tgz} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==, tarball: https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, tarball: https://registry.npmjs.org/once/-/once-1.4.0.tgz} + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==, tarball: https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz} @@ -1169,6 +1659,23 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==, tarball: https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==, tarball: https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz} + engines: {node: '>= 0.8'} + + partyserver@0.4.1: + resolution: {integrity: sha512-StSs0oY8RmTxjGNil7VbCG4gnTN+4rYX20fiUIItAxTPpr/5rPDZT6PIvMROkk9M1Gn7GzE1wuQXwhxceaGhXA==, tarball: https://registry.npmjs.org/partyserver/-/partyserver-0.4.1.tgz} + peerDependencies: + '@cloudflare/workers-types': ^4.20240729.0 + + partysocket@1.1.18: + resolution: {integrity: sha512-SyuvH9VavWOSa14v6dYdp3yfSUDII4BQB1+TkGOFBkjfZKjnDBiba4fhdhwBlqGBkqw4ea3gTA1DYhSffX24Wg==, tarball: https://registry.npmjs.org/partysocket/-/partysocket-1.1.18.tgz} + peerDependencies: + react: '>=17' + peerDependenciesMeta: + react: + optional: true + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, tarball: https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz} engines: {node: '>=8'} @@ -1177,6 +1684,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, tarball: https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz} engines: {node: '>=8'} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==, tarball: https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==, tarball: https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz} engines: {node: '>=8'} @@ -1210,6 +1720,10 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==, tarball: https://registry.npmjs.org/pify/-/pify-4.0.1.tgz} engines: {node: '>=6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==, tarball: https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz} + engines: {node: '>=16.20.0'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==, tarball: https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz} engines: {node: ^10 || ^12 || >=14} @@ -1219,6 +1733,14 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==, tarball: https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz} + engines: {node: '>= 0.10'} + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==, tarball: https://registry.npmjs.org/qs/-/qs-6.15.1.tgz} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==, tarball: https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz} @@ -1228,10 +1750,26 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, tarball: https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz} + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==, tarball: https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==, tarball: https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz} + engines: {node: '>= 0.10'} + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==, tarball: https://registry.npmjs.org/react/-/react-19.2.5.tgz} + engines: {node: '>=0.10.0'} + read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==, tarball: https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz} engines: {node: '>=6'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==, tarball: https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz} + engines: {node: '>=0.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==, tarball: https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz} engines: {node: '>=8'} @@ -1272,12 +1810,20 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==, tarball: https://registry.npmjs.org/router/-/router-2.2.0.tgz} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, tarball: https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz} safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, tarball: https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, tarball: https://registry.npmjs.org/semver/-/semver-6.3.1.tgz} + hasBin: true + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==, tarball: https://registry.npmjs.org/semver/-/semver-7.7.3.tgz} engines: {node: '>=10'} @@ -1288,6 +1834,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==, tarball: https://registry.npmjs.org/send/-/send-1.2.1.tgz} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==, tarball: https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, tarball: https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, tarball: https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz} engines: {node: '>=8'} @@ -1296,6 +1853,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, tarball: https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz} engines: {node: '>=8'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==, tarball: https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==, tarball: https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==, tarball: https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==, tarball: https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==, tarball: https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz} @@ -1320,13 +1893,25 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, tarball: https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==, tarball: https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==, tarball: https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==, tarball: https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz} + engines: {node: '>=18'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, tarball: https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz} engines: {node: '>=8'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==, tarball: https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz} + engines: {node: '>=12'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==, tarball: https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz} engines: {node: '>=4'} @@ -1365,6 +1950,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, tarball: https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==, tarball: https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz} + engines: {node: '>=0.6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, tarball: https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz} @@ -1408,6 +1997,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==, tarball: https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz} + engines: {node: '>= 0.6'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==, tarball: https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz} engines: {node: '>=14.17'} @@ -1423,6 +2016,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==, tarball: https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz} engines: {node: '>= 4.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==, tarball: https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz} + engines: {node: '>= 0.8'} + unrun@0.2.34: resolution: {integrity: sha512-LyaghRBR++r7svhDK6tnDz2XaYHWdneBOA0jbS8wnRsHerI9MFljX4fIiTgbbNbEVzZ0C9P1OjWLLe1OqoaaEw==, tarball: https://registry.npmjs.org/unrun/-/unrun-0.2.34.tgz} engines: {node: '>=20.19.0'} @@ -1433,6 +2030,16 @@ packages: synckit: optional: true + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==, tarball: https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, tarball: https://registry.npmjs.org/vary/-/vary-1.1.2.tgz} + engines: {node: '>= 0.8'} + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==, tarball: https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz} engines: {node: ^18.0.0 || >=20.0.0} @@ -1510,9 +2117,36 @@ packages: engines: {node: '>=8'} hasBin: true + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==, tarball: https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, tarball: https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz} + xstate@5.26.0: resolution: {integrity: sha512-Fvi9VBoqHgsGYLU2NTag8xDTWtKqUC0+ue7EAhBNBb06wf620QEy05upBaEI1VLMzIn63zugLV8nHb69ZUWYAA==, tarball: https://registry.npmjs.org/xstate/-/xstate-5.26.0.tgz} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, tarball: https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, tarball: https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==, tarball: https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==, tarball: https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==, tarball: https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz} + peerDependencies: + zod: ^3.25.28 || ^4 + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==, tarball: https://registry.npmjs.org/zod/-/zod-4.3.6.tgz} @@ -1542,6 +2176,42 @@ snapshots: dependencies: json-schema: 0.4.0 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/generator@8.0.0-rc.3': dependencies: '@babel/parser': 8.0.0-rc.3 @@ -1551,21 +2221,151 @@ snapshots: '@types/jsesc': 2.5.1 jsesc: 3.1.0 - '@babel/helper-string-parser@8.0.0-rc.3': {} + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 - '@babel/helper-validator-identifier@8.0.0-rc.3': {} + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 - '@babel/parser@8.0.0-rc.3': + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-string-parser@8.0.0-rc.3': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-identifier@8.0.0-rc.3': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/parser@8.0.0-rc.3': dependencies: '@babel/types': 8.0.0-rc.3 + '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/runtime-corejs3@7.29.2': + dependencies: + core-js-pure: 3.49.0 + '@babel/runtime@7.28.6': {} + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@8.0.0-rc.3': dependencies: '@babel/helper-string-parser': 8.0.0-rc.3 '@babel/helper-validator-identifier': 8.0.0-rc.3 + '@cfworker/json-schema@4.1.1': {} + '@changesets/apply-release-plan@7.0.14': dependencies: '@changesets/config': 3.1.2 @@ -1725,6 +2525,8 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@cloudflare/workers-types@4.20260424.1': {} + '@emnapi/core@1.9.1': dependencies: '@emnapi/wasi-threads': 1.2.0 @@ -1888,6 +2690,10 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true + '@hono/node-server@1.19.14(hono@4.12.15)': + dependencies: + hono: 4.12.15 + '@inquirer/external-editor@1.0.3(@types/node@20.19.30)': dependencies: chardet: 2.1.1 @@ -1900,6 +2706,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -1925,6 +2736,30 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.15) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.4.0(express@5.2.1) + hono: 4.12.15 + jose: 6.2.2 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.2(zod@4.3.6) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': dependencies: '@emnapi/core': 1.9.1 @@ -2002,6 +2837,15 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': optional: true + '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(vite@5.4.21(@types/node@20.19.30))': + dependencies: + '@babel/core': 7.29.0 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + optionalDependencies: + '@babel/runtime': 7.28.6 + vite: 5.4.21(@types/node@20.19.30) + '@rolldown/pluginutils@1.0.0-rc.12': {} '@rollup/rollup-android-arm-eabi@4.57.1': @@ -2142,6 +2986,36 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + agents@0.11.5(@babel/core@7.29.0)(@babel/runtime@7.28.6)(@cloudflare/workers-types@4.20260424.1)(ai@6.0.67(zod@4.3.6))(react@19.2.5)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(vite@5.4.21(@types/node@20.19.30))(zod@4.3.6): + dependencies: + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) + '@cfworker/json-schema': 4.1.1 + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) + '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.28.6)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(vite@5.4.21(@types/node@20.19.30)) + ai: 6.0.67(zod@4.3.6) + cron-schedule: 6.0.0 + mimetext: 3.0.28 + nanoid: 5.1.9 + partyserver: 0.4.1(@cloudflare/workers-types@4.20260424.1) + partysocket: 1.1.18(react@19.2.5) + react: 19.2.5 + yargs: 18.0.0 + zod: 4.3.6 + optionalDependencies: + vite: 5.4.21(@types/node@20.19.30) + transitivePeerDependencies: + - '@babel/core' + - '@babel/plugin-transform-runtime' + - '@babel/runtime' + - '@cloudflare/workers-types' + - rolldown + - supports-color + ai@6.0.67(zod@4.3.6): dependencies: '@ai-sdk/gateway': 3.0.32(zod@4.3.6) @@ -2150,10 +3024,25 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.6 + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + 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: {} + + ansi-styles@6.2.3: {} + ansis@4.2.0: {} argparse@1.0.10: @@ -2172,20 +3061,58 @@ snapshots: estree-walker: 3.0.3 pathe: 2.0.3 + baseline-browser-mapping@2.10.21: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 birpc@4.0.0: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + braces@3.0.3: dependencies: fill-range: 7.1.1 + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.21 + caniuse-lite: 1.0.30001790 + electron-to-chromium: 1.5.344 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bytes@3.1.2: {} + cac@6.7.14: {} cac@7.0.0: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001790: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -2200,6 +3127,31 @@ snapshots: ci-info@3.9.0: {} + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + core-js-pure@3.49.0: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cron-schedule@6.0.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2216,6 +3168,8 @@ snapshots: defu@6.1.4: {} + depd@2.0.0: {} + detect-indent@6.1.0: {} dir-glob@3.0.1: @@ -2228,15 +3182,37 @@ snapshots: dts-resolver@2.1.3: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.344: {} + + emoji-regex@10.6.0: {} + empathic@2.0.0: {} + encodeurl@2.0.0: {} + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -2292,18 +3268,70 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} + + escape-html@1.0.3: {} + esprima@4.0.1: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + etag@1.8.1: {} + + event-target-polyfill@0.0.4: {} + eventsource-parser@3.0.6: {} + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + expect-type@1.3.0: {} + express-rate-limit@8.4.0(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extendable-error@0.1.7: {} + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2312,6 +3340,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-uri@3.1.0: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -2324,11 +3354,26 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -2344,6 +3389,32 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.5.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-tsconfig@4.13.7: dependencies: resolve-pkg-maps: 1.0.0 @@ -2361,10 +3432,28 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-symbols@1.1.0: {} + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hono@4.12.15: {} + hookable@6.1.0: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + human-id@4.1.3: {} iconv-lite@0.7.2: @@ -2375,6 +3464,12 @@ snapshots: import-without-cache@0.2.5: {} + inherits@2.0.4: {} + + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -2383,6 +3478,8 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -2391,6 +3488,12 @@ snapshots: isexe@2.0.0: {} + jose@6.2.2: {} + + js-base64@3.7.8: {} + + js-tokens@4.0.0: {} + js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -2402,8 +3505,14 @@ snapshots: jsesc@3.1.0: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} + json5@2.2.3: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -2416,10 +3525,20 @@ snapshots: loupe@3.2.1: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -2427,18 +3546,55 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimetext@3.0.28: + dependencies: + '@babel/runtime': 7.28.6 + '@babel/runtime-corejs3': 7.29.2 + js-base64: 3.7.8 + mime-types: 2.1.35 + mri@1.2.0: {} ms@2.1.3: {} nanoid@3.3.11: {} + nanoid@5.1.9: {} + + negotiator@1.0.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 + node-releases@2.0.38: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + obug@2.1.1: {} + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + outdent@0.5.0: {} p-filter@2.1.0: @@ -2461,10 +3617,25 @@ snapshots: dependencies: quansync: 0.2.11 + parseurl@1.3.3: {} + + partyserver@0.4.1(@cloudflare/workers-types@4.20260424.1): + dependencies: + '@cloudflare/workers-types': 4.20260424.1 + nanoid: 5.1.9 + + partysocket@1.1.18(react@19.2.5): + dependencies: + event-target-polyfill: 0.0.4 + optionalDependencies: + react: 19.2.5 + path-exists@4.0.0: {} path-key@3.1.1: {} + path-to-regexp@8.4.2: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -2483,6 +3654,8 @@ snapshots: pify@4.0.1: {} + pkce-challenge@5.0.1: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -2491,12 +3664,32 @@ snapshots: prettier@2.8.8: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} quansync@1.0.0: {} queue-microtask@1.2.3: {} + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react@19.2.5: {} + read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -2504,6 +3697,8 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + require-from-string@2.0.2: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -2583,22 +3778,89 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 safer-buffer@2.1.2: {} + semver@6.3.1: {} + semver@7.7.3: {} semver@7.7.4: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -2616,12 +3878,24 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-bom@3.0.0: {} term-size@2.2.1: {} @@ -2647,6 +3921,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tr46@0.0.3: {} tree-kill@1.2.2: {} @@ -2690,6 +3966,12 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typescript@5.9.3: {} unconfig-core@7.5.0: @@ -2701,6 +3983,8 @@ snapshots: universalify@0.1.2: {} + unpipe@1.0.0: {} + unrun@0.2.34(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): dependencies: rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) @@ -2708,6 +3992,14 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + vary@1.1.2: {} + vite-node@2.1.9(@types/node@20.19.30): dependencies: cac: 6.7.14 @@ -2786,6 +4078,33 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + xstate@5.26.0: {} + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + zod-to-json-schema@3.25.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod@4.3.6: {} diff --git a/readme.md b/readme.md index 4a445fd..c7f208d 100644 --- a/readme.md +++ b/readme.md @@ -11,7 +11,7 @@ Stately Agent is a flexible framework for building AI agents using state machine -The examples in [`examples/`](/Users/davidkpiano/Code/agent/examples) are intentionally small, run in the CLI, and use real OpenAI calls when `OPENAI_API_KEY` is set. +The examples in [`examples/`](/Users/davidkpiano/Code/agent/examples) are intentionally small. Most run in the CLI and use real OpenAI calls when `OPENAI_API_KEY` is set. Runtime-specific examples call out extra environment requirements inline. Run them with `node --import tsx examples/.ts`. @@ -26,9 +26,11 @@ Each example demonstrates one concept: - [`examples/conditional-subflow.ts`](/Users/davidkpiano/Code/agent/examples/conditional-subflow.ts): routing directly into one of multiple child subflows from parent input - [`examples/rag.ts`](/Users/davidkpiano/Code/agent/examples/rag.ts): retrieval-augmented generation with explicit retrieve and answer states - [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts): tool invocation with emitted `toolCall`, incremental `toolProgress`, and final `toolResult` events +- [`examples/ai-sdk.ts`](/Users/davidkpiano/Code/agent/examples/ai-sdk.ts): AI SDK v6 integration using `createAiSdkAdapter(...)` for routing and `generateText(..., { output: Output.object(...) })` for structured drafting - [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts): persisting snapshots and restoring a session in plain runtime code - [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts): runner-agnostic HTTP transport for starting sessions, sending events, and reading snapshots over `Request`/`Response` - [`examples/http-streaming-session.ts`](/Users/davidkpiano/Code/agent/examples/http-streaming-session.ts): durable SSE transport that reconnects to a restored run and emits only new streaming parts +- [`examples/cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts): integrating a persisted agent-machine session store into Cloudflare Agents `onRequest()` routing; requires the Cloudflare Workers runtime - [`examples/persistent-multi-agent-network.ts`](/Users/davidkpiano/Code/agent/examples/persistent-multi-agent-network.ts): durable multi-agent handoffs that restore from a persisted mid-network snapshot - [`examples/persistent-streaming.ts`](/Users/davidkpiano/Code/agent/examples/persistent-streaming.ts): restore a crashed streaming invoke and continue with only new live emitted parts - [`examples/persistent-supervisor.ts`](/Users/davidkpiano/Code/agent/examples/persistent-supervisor.ts): durable supervisor routing that restores after a persisted retry handoff diff --git a/src/ai-sdk/index.test.ts b/src/ai-sdk/index.test.ts new file mode 100644 index 0000000..45c6669 --- /dev/null +++ b/src/ai-sdk/index.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, test } from 'vitest'; +import { z } from 'zod'; +import { createAiSdkAdapter } from './index.js'; + +describe('createAiSdkAdapter', () => { + test('resolves schema-less choices with a custom model resolver', async () => { + const seen: Array<{ model: unknown; prompt: unknown }> = []; + const adapter = createAiSdkAdapter({ + resolveModel: (model) => ({ providerResolved: model }) as never, + generateText: async (options) => { + seen.push({ + model: options.model, + prompt: options.prompt, + }); + + return { + output: 'billing', + } as never; + }, + }); + + const result = await adapter.decide({ + model: 'openai/gpt-5.4-nano', + prompt: 'Refund request for last month.', + options: { + billing: { description: 'Billing support' }, + general: { description: 'General support' }, + }, + }); + + expect(result).toEqual({ + choice: 'billing', + data: {}, + }); + expect(seen).toEqual([ + { + model: { providerResolved: 'openai/gpt-5.4-nano' }, + prompt: 'Refund request for last month.', + }, + ]); + }); + + test('returns structured decision payloads for schema-backed options', async () => { + const adapter = createAiSdkAdapter({ + generateText: async () => + ({ + output: { + decision: 'research', + data: { + query: 'latest cloudflare agents docs', + }, + reasoning: 'Need the newest API details.', + }, + }) as never, + }); + + const result = await adapter.decide({ + model: 'openai/gpt-5.4-nano', + prompt: 'Find the current Cloudflare Agents docs.', + reasoning: true, + options: { + research: { + description: 'Do external research first.', + schema: z.object({ + query: z.string(), + }), + }, + answer: { + description: 'Answer directly.', + }, + }, + }); + + expect(result).toEqual({ + choice: 'research', + data: { + query: 'latest cloudflare agents docs', + }, + reasoning: 'Need the newest API details.', + }); + }); +}); diff --git a/src/ai-sdk/index.ts b/src/ai-sdk/index.ts index 8ba186d..d5cb592 100644 --- a/src/ai-sdk/index.ts +++ b/src/ai-sdk/index.ts @@ -2,12 +2,24 @@ import { generateText, Output } from 'ai'; import { z } from 'zod'; import type { AgentAdapter, StandardSchemaV1 } from '../types.js'; +type AiSdkGenerateText = typeof generateText; +type AiSdkModel = Parameters[0]['model']; + +export interface CreateAiSdkAdapterOptions { + resolveModel?: (model: string) => AiSdkModel; + generateText?: AiSdkGenerateText; +} + /** * Create an adapter that uses the Vercel AI SDK for decide/classify. - * Model strings like 'anthropic/claude-sonnet-4.5' are resolved via the - * AI SDK's model registry. + * By default, model strings are passed straight through to the AI SDK. + * For provider helpers such as `openai(...)`, pass `resolveModel`. */ -export function createAiSdkAdapter(): AgentAdapter { +export function createAiSdkAdapter( + config: CreateAiSdkAdapterOptions = {} +): AgentAdapter { + const generate = config.generateText ?? generateText; + return { async decide({ model, prompt, options, reasoning }) { const optionKeys = Object.keys(options); @@ -18,8 +30,8 @@ export function createAiSdkAdapter(): AgentAdapter { .map(([key, opt]) => `- ${key}: ${opt.description}`) .join('\n'); - const result = await generateText({ - model: resolveModel(model), + const result = await generate({ + model: resolveModel(model, config.resolveModel), system: `You must choose exactly one of the following options:\n${optionDescriptions}`, prompt, output: Output.choice({ @@ -56,8 +68,8 @@ export function createAiSdkAdapter(): AgentAdapter { const systemPrompt = `You must choose exactly one of the following options:\n${optionDescriptions}\n\nRespond with structured output containing the chosen decision and any required data.`; - const result = await generateText({ - model: resolveModel(model), + const result = await generate({ + model: resolveModel(model, config.resolveModel), system: systemPrompt, prompt, output: Output.object({ @@ -96,10 +108,16 @@ function toZodSchema(schema: StandardSchemaV1): z.ZodType { /** * Resolve a model string to an AI SDK model. - * Supports the `provider/model` format via the AI SDK registry. + * Supports custom resolution when users prefer provider helpers such as + * `openai('gpt-5.4-nano')`. */ -function resolveModel(model: string): Parameters[0]['model'] { - // The AI SDK accepts model strings when using a provider registry. - // For now, return as-is — users configure their provider registry externally. +function resolveModel( + model: string, + resolver?: (model: string) => AiSdkModel +): AiSdkModel { + if (resolver) { + return resolver(model); + } + return model as any; } diff --git a/src/examples.test.ts b/src/examples.test.ts index bb160a3..4fa6c8e 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -1,9 +1,12 @@ import { describe, expect, test } from 'vitest'; import { z } from 'zod'; +import { restoreSession, startSession } from './index.js'; import { + createAiSdkExample, createChatbotExample, AgentNetworkDurableObject, + createCloudflareAgentRunStore, createDurableObjectRunStore, createAdapterExample, createBranchingExample, @@ -15,6 +18,7 @@ import { createEmailExample, createErrorRetryExample, createHitlExample, + createPersistenceExample, createPersistenceSessionHttpHandler, createStreamingSessionHttpController, createJokeExample, @@ -88,6 +92,35 @@ describe('curated examples', () => { } }); + test('ai sdk example routes and drafts a structured reply', async () => { + const machine = createAiSdkExample({ + adapter: { + decide: async () => ({ + choice: 'billing', + data: { confidence: 0.93 }, + }), + }, + draftReply: async ({ route, confidence, message }) => ({ + subject: `${route.toUpperCase()} reply`, + body: `${message} :: ${confidence.toFixed(2)}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ message: 'Please refund invoice 123.' }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + route: 'billing', + confidence: 0.93, + subject: 'BILLING reply', + body: 'Please refund invoice 123. :: 0.93', + }); + } + }); + test('chatbot messages example accumulates structured conversation turns', async () => { const machine = createChatbotMessagesExample(async (messages) => ({ message: { @@ -218,6 +251,48 @@ describe('curated examples', () => { ); }); + test('cloudflare agents example store persists durable sessions in synced state', async () => { + let state = { + sessions: {}, + }; + const store = createCloudflareAgentRunStore({ + getState: () => state, + setState: async (nextState) => { + state = nextState; + }, + }); + const machine = createPersistenceExample(async ({ request, approved }) => ({ + summary: `${request} :: approved=${String(approved)}`, + })); + + const run = await startSession(machine, { + store, + input: { + request: 'Approve the Cloudflare rollout.', + }, + }); + + await run.send({ type: 'approve' }); + + const restored = await restoreSession(machine, { + sessionId: run.sessionId, + store, + }); + + expect(restored.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { + request: 'Approve the Cloudflare rollout.', + approved: true, + summary: 'Approve the Cloudflare rollout. :: approved=true', + }, + }) + ); + expect(Object.keys(state.sessions)).toEqual([run.sessionId]); + }); + test('http session example exposes start, send, and status over Request/Response', async () => { const handle = createPersistenceSessionHttpHandler({ summarize: async ({ request, approved }) => ({ From 0f974b29e3954f76667c81fdb432ea5a160941d0 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 26 Apr 2026 13:25:50 -0400 Subject: [PATCH 29/34] feat: add next app router example --- examples/index.ts | 8 ++ examples/next-app-router.ts | 125 ++++++++++++++++++++++++++++++ readme.md | 19 +++++ src/agent-convert-cli.test.ts | 2 +- src/examples.test.ts | 138 ++++++++++++++++++++++++++++++++++ 5 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 examples/next-app-router.ts diff --git a/examples/index.ts b/examples/index.ts index 789af5e..2fab2ea 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -30,6 +30,14 @@ export { createJugsExample } from './jugs.js'; export { createMapReduceExample } from './map-reduce.js'; export { createMultiAgentNetworkExample } from './multi-agent-network.js'; export { createNewspaperExample } from './newspaper.js'; +export { + createNextReviewRouteHandlers, + createNextStreamingRouteHandlers, + dynamic as nextAppRouterDynamic, + maxDuration as nextAppRouterMaxDuration, + runtime as nextAppRouterRuntime, + type NextRouteContext, +} from './next-app-router.js'; export { createPlanAndExecuteExample } from './plan-and-execute.js'; export { createPersistenceExample, runPersistenceExample } from './persistence.js'; export { diff --git a/examples/next-app-router.ts b/examples/next-app-router.ts new file mode 100644 index 0000000..c80fa98 --- /dev/null +++ b/examples/next-app-router.ts @@ -0,0 +1,125 @@ +import { type RunStore } from '../src/index.js'; +import { + createPersistenceSessionHttpHandler, + type SessionHttpHandlerOptions, +} from './http-session.js'; +import { + createStreamingSessionHttpController, + type StreamingSessionHttpController, +} from './http-streaming-session.js'; + +/** + * Suggested route-segment config for Next.js App Router route handlers that + * host long-lived agent sessions and streaming responses. + */ +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 30; + +export interface NextRouteContext> { + params: Promise | TParams; +} + +export interface NextReviewRouteHandlers { + sessions: { + POST(request: Request): Promise; + }; + session: { + GET( + request: Request, + context: NextRouteContext<{ sessionId: string }> + ): Promise; + }; + events: { + POST( + request: Request, + context: NextRouteContext<{ sessionId: string }> + ): Promise; + }; +} + +export interface NextStreamingRouteHandlers { + sessions: { + POST(request: Request): Promise; + }; + session: { + GET( + request: Request, + context: NextRouteContext<{ sessionId: string }> + ): Promise; + }; + stream: { + GET( + request: Request, + context: NextRouteContext<{ sessionId: string }> + ): Promise; + }; + advance(streamId: string): void; + dropActiveSession(sessionId: string): void; +} + +export function createNextReviewRouteHandlers( + options: SessionHttpHandlerOptions = {} +): NextReviewRouteHandlers { + const handle = createPersistenceSessionHttpHandler(options); + + return { + sessions: { + POST(request) { + return handle(rewritePath(request, '/sessions')); + }, + }, + session: { + async GET(request, context) { + const { sessionId } = await context.params; + return handle(rewritePath(request, `/sessions/${sessionId}`)); + }, + }, + events: { + async POST(request, context) { + const { sessionId } = await context.params; + return handle(rewritePath(request, `/sessions/${sessionId}/events`)); + }, + }, + }; +} + +export function createNextStreamingRouteHandlers(options: { + store?: RunStore; +} = {}): NextStreamingRouteHandlers { + const controller = createStreamingSessionHttpController(options); + + return { + sessions: { + POST(request) { + return controller.handle(rewritePath(request, '/sessions')); + }, + }, + session: { + async GET(request, context) { + const { sessionId } = await context.params; + return controller.handle(rewritePath(request, `/sessions/${sessionId}`)); + }, + }, + stream: { + async GET(request, context) { + const { sessionId } = await context.params; + return controller.handle( + rewritePath(request, `/sessions/${sessionId}/stream`) + ); + }, + }, + advance(streamId) { + controller.advance(streamId); + }, + dropActiveSession(sessionId) { + controller.dropActiveSession(sessionId); + }, + }; +} + +function rewritePath(request: Request, pathname: string): Request { + const url = new URL(request.url); + url.pathname = pathname; + return new Request(url, request); +} diff --git a/readme.md b/readme.md index c7f208d..9a65a5c 100644 --- a/readme.md +++ b/readme.md @@ -30,6 +30,7 @@ Each example demonstrates one concept: - [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts): persisting snapshots and restoring a session in plain runtime code - [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts): runner-agnostic HTTP transport for starting sessions, sending events, and reading snapshots over `Request`/`Response` - [`examples/http-streaming-session.ts`](/Users/davidkpiano/Code/agent/examples/http-streaming-session.ts): durable SSE transport that reconnects to a restored run and emits only new streaming parts +- [`examples/next-app-router.ts`](/Users/davidkpiano/Code/agent/examples/next-app-router.ts): Next.js App Router wrappers around the generic HTTP and SSE session transports, including `runtime`, `dynamic`, and `maxDuration` route config values - [`examples/cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts): integrating a persisted agent-machine session store into Cloudflare Agents `onRequest()` routing; requires the Cloudflare Workers runtime - [`examples/persistent-multi-agent-network.ts`](/Users/davidkpiano/Code/agent/examples/persistent-multi-agent-network.ts): durable multi-agent handoffs that restore from a persisted mid-network snapshot - [`examples/persistent-streaming.ts`](/Users/davidkpiano/Code/agent/examples/persistent-streaming.ts): restore a crashed streaming invoke and continue with only new live emitted parts @@ -41,4 +42,22 @@ Each example demonstrates one concept: Use `classify(...)` when the result is just "what kind of thing is this?" Use `decide(...)` when the result is "what should happen next?" and the chosen branch may need structured data. +## Persistence Adapters + + + +Storage adapters are intentionally bring-your-own. Implement the `RunStore` contract with four methods: + +- `append(sessionId, event)` +- `loadEvents(sessionId, afterSequence?)` +- `loadLatestSnapshot(sessionId)` +- `saveSnapshot(snapshot)` + +Use these examples as templates for your storage layer: + +- [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts): the smallest durable session flow with an in-memory store +- [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts): the Request/Response transport shape around a `RunStore` +- [`examples/cloudflare-durable-object.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-durable-object.ts): Durable Object-backed event and snapshot persistence +- [`examples/cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts): syncing a `RunStore` into Cloudflare Agents state + **Read the documentation: [stately.ai/docs/agents](https://stately.ai/docs/agents)** diff --git a/src/agent-convert-cli.test.ts b/src/agent-convert-cli.test.ts index 081108b..a048738 100644 --- a/src/agent-convert-cli.test.ts +++ b/src/agent-convert-cli.test.ts @@ -75,7 +75,7 @@ test('agent:convert writes Mermaid and XState output from machine files', async expect(warningResult.stderr).toContain( '[agent:convert] idle on go: Unsupported helper call: unknownTransition() is not statically resolvable.' ); -}); +}, 20000); async function runConvert(args: string[]) { return execFileAsync('pnpm', ['agent:convert', ...args], { diff --git a/src/examples.test.ts b/src/examples.test.ts index 4fa6c8e..640dc95 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -26,6 +26,8 @@ import { createMapReduceExample, createMultiAgentNetworkExample, createNewspaperExample, + createNextReviewRouteHandlers, + createNextStreamingRouteHandlers, runPersistenceExample, runPersistentMultiAgentNetworkExample, runPersistentStreamingExample, @@ -454,6 +456,142 @@ describe('curated examples', () => { ); }); + test('next app router review example adapts Request/Response handlers to dynamic route params', async () => { + const routes = createNextReviewRouteHandlers({ + summarize: async ({ request, approved }) => ({ + summary: `${request} :: approved=${String(approved)}`, + }), + }); + + const startResponse = await routes.sessions.POST( + new Request('https://agent.test/api/agent', { + method: 'POST', + body: JSON.stringify({ + request: 'Approve the quarterly report.', + }), + headers: { + 'content-type': 'application/json', + }, + }) + ); + const startBody = await startResponse.json() as { + sessionId: string; + snapshot: { value: string; status: string }; + }; + + expect(startBody.snapshot).toEqual( + expect.objectContaining({ + value: 'review', + status: 'active', + }) + ); + + const sendResponse = await routes.events.POST( + new Request(`https://agent.test/api/agent/${startBody.sessionId}/events`, { + method: 'POST', + body: JSON.stringify({ type: 'approve' }), + headers: { + 'content-type': 'application/json', + }, + }), + { + params: Promise.resolve({ + sessionId: startBody.sessionId, + }), + } + ); + const sendBody = await sendResponse.json() as { + snapshot: { + value: string; + status: string; + output: { + request: string; + approved: boolean; + summary: string; + }; + }; + }; + + expect(sendBody.snapshot).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { + request: 'Approve the quarterly report.', + approved: true, + summary: 'Approve the quarterly report. :: approved=true', + }, + }) + ); + }); + + test('next app router streaming example reconnects with only new streamed parts', async () => { + const routes = createNextStreamingRouteHandlers(); + + const startResponse = await routes.sessions.POST( + new Request('https://agent.test/api/agent', { + method: 'POST', + body: JSON.stringify({ + streamId: 'next-stream-1', + text: 'hello', + }), + headers: { + 'content-type': 'application/json', + }, + }) + ); + const startBody = await startResponse.json() as { sessionId: string }; + + const firstStreamResponse = await routes.stream.GET( + new Request(`https://agent.test/api/agent/${startBody.sessionId}/stream`), + { + params: Promise.resolve({ + sessionId: startBody.sessionId, + }), + } + ); + const firstReader = createSseReader(firstStreamResponse); + + routes.advance('next-stream-1'); + + await expect(firstReader.next()).resolves.toEqual({ + event: 'textPart', + data: { + type: 'textPart', + delta: 'hel', + }, + }); + + await firstReader.cancel(); + routes.dropActiveSession(startBody.sessionId); + + const secondStreamResponse = await routes.stream.GET( + new Request(`https://agent.test/api/agent/${startBody.sessionId}/stream`), + { + params: Promise.resolve({ + sessionId: startBody.sessionId, + }), + } + ); + const secondReader = createSseReader(secondStreamResponse); + + routes.advance('next-stream-1'); + + await expect(secondReader.next()).resolves.toEqual({ + event: 'textPart', + data: { + type: 'textPart', + delta: 'lo', + }, + }); + await expect(secondReader.next()).resolves.toEqual({ + event: 'done', + data: { + text: 'hello', + }, + }); + }); + test('cloudflare durable network example restores and settles a network run', async () => { const storage = new Map(); const firstInstance = new AgentNetworkDurableObject({ From 4224da7dfed9c0f7bf3c9e13979c688697a74bcb Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 26 Apr 2026 13:34:24 -0400 Subject: [PATCH 30/34] feat: add next ai sdk ui example --- examples/index.ts | 4 + examples/next-ai-sdk-ui.ts | 214 +++++++++++++++++++++++++++++++++++++ readme.md | 1 + src/examples.test.ts | 48 +++++++++ 4 files changed, 267 insertions(+) create mode 100644 examples/next-ai-sdk-ui.ts diff --git a/examples/index.ts b/examples/index.ts index 2fab2ea..14bc9bf 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -30,6 +30,10 @@ export { createJugsExample } from './jugs.js'; export { createMapReduceExample } from './map-reduce.js'; export { createMultiAgentNetworkExample } from './multi-agent-network.js'; export { createNewspaperExample } from './newspaper.js'; +export { + createNextAiSdkUiRoute, + type AgentUiMessage, +} from './next-ai-sdk-ui.js'; export { createNextReviewRouteHandlers, createNextStreamingRouteHandlers, diff --git a/examples/next-ai-sdk-ui.ts b/examples/next-ai-sdk-ui.ts new file mode 100644 index 0000000..8fab1d7 --- /dev/null +++ b/examples/next-ai-sdk-ui.ts @@ -0,0 +1,214 @@ +import { + convertToModelMessages, + createUIMessageStream, + createUIMessageStreamResponse, + streamText, + type UIMessage, +} from 'ai'; +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from '../src/index.js'; +import { createExampleModel } from './_run.js'; + +const uiMessagesSchema = z.object({ + messages: z.array(z.custom()), +}); + +const streamedTextSchema = z.object({ + text: z.string(), +}); + +const notificationSchema = z.object({ + message: z.string(), + level: z.enum(['info', 'warning', 'error']), +}); + +const sourceSchema = z.object({ + id: z.string(), + url: z.string().url(), + title: z.string(), +}); + +export type AgentUiMessage = UIMessage< + unknown, + { + notification: z.infer; + } +>; + +export function createNextAiSdkUiRoute(options: { + streamReply?: (args: { + messages: UIMessage[]; + onDelta: (delta: string) => void; + }) => Promise>; +} = {}) { + const streamReply = + options.streamReply ?? + (async ({ + messages, + onDelta, + }: { + messages: UIMessage[]; + onDelta: (delta: string) => void; + }) => { + const result = streamText({ + model: createExampleModel('openai/gpt-5.4-nano'), + messages: await convertToModelMessages(messages), + }); + + for await (const delta of result.textStream) { + onDelta(delta); + } + + return { + text: await result.text, + }; + }); + + const machine = createAgentMachine({ + id: 'next-ai-sdk-ui-example', + schemas: { + input: uiMessagesSchema, + output: streamedTextSchema, + emitted: { + notification: notificationSchema, + source: sourceSchema, + textPart: z.object({ + delta: z.string(), + }), + }, + events: { + begin: z.object({}), + }, + }, + context: (input) => ({ + messages: input.messages, + finalText: '', + }), + initial: 'ready', + states: { + ready: { + on: { + begin: { + target: 'drafting', + }, + }, + }, + drafting: { + resultSchema: streamedTextSchema, + invoke: async ({ context }, enq) => { + enq.emit({ + type: 'notification', + message: 'Drafting reply...', + level: 'info', + }); + enq.emit({ + type: 'source', + id: 'agent-docs', + url: 'https://stately.ai/docs/agents', + title: 'Stately Agent documentation', + }); + + return streamReply({ + messages: context.messages, + onDelta: (delta) => { + enq.emit({ + type: 'textPart', + delta, + }); + }, + }); + }, + onDone: ({ result }) => ({ + target: 'done', + context: { + finalText: result.text, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + text: context.finalText, + }), + }, + }, + }); + + return { + async POST(request: Request): Promise { + const { messages } = uiMessagesSchema.parse(await request.json()); + + const stream = createUIMessageStream({ + originalMessages: messages, + execute: async ({ writer }) => { + const run = await startSession(machine, { + store: createMemoryRunStore(), + input: { messages }, + }); + + const textId = 'assistant-response'; + let textStarted = false; + + const offNotification = run.on('notification', (event) => { + writer.write({ + type: 'data-notification', + data: { + message: event.message, + level: event.level, + }, + transient: true, + }); + }); + const offSource = run.on('source', (event) => { + writer.write(({ + type: 'source', + value: { + type: 'source', + sourceType: 'url', + id: event.id, + url: event.url, + title: event.title, + }, + } as unknown) as never); + }); + const offTextPart = run.on('textPart', (event) => { + if (!textStarted) { + writer.write({ + type: 'text-start', + id: textId, + }); + textStarted = true; + } + + writer.write({ + type: 'text-delta', + id: textId, + delta: event.delta, + }); + }); + + try { + await run.send({ type: 'begin' }); + } finally { + offNotification(); + offSource(); + offTextPart(); + } + + if (textStarted) { + writer.write({ + type: 'text-end', + id: textId, + }); + } + }, + }); + + return createUIMessageStreamResponse({ stream }); + }, + }; +} diff --git a/readme.md b/readme.md index 9a65a5c..9bc4e32 100644 --- a/readme.md +++ b/readme.md @@ -31,6 +31,7 @@ Each example demonstrates one concept: - [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts): runner-agnostic HTTP transport for starting sessions, sending events, and reading snapshots over `Request`/`Response` - [`examples/http-streaming-session.ts`](/Users/davidkpiano/Code/agent/examples/http-streaming-session.ts): durable SSE transport that reconnects to a restored run and emits only new streaming parts - [`examples/next-app-router.ts`](/Users/davidkpiano/Code/agent/examples/next-app-router.ts): Next.js App Router wrappers around the generic HTTP and SSE session transports, including `runtime`, `dynamic`, and `maxDuration` route config values +- [`examples/next-ai-sdk-ui.ts`](/Users/davidkpiano/Code/agent/examples/next-ai-sdk-ui.ts): a Next.js App Router chat route that accepts `UIMessage[]` and streams AI SDK UI message parts from machine-emitted notifications, sources, and text deltas - [`examples/cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts): integrating a persisted agent-machine session store into Cloudflare Agents `onRequest()` routing; requires the Cloudflare Workers runtime - [`examples/persistent-multi-agent-network.ts`](/Users/davidkpiano/Code/agent/examples/persistent-multi-agent-network.ts): durable multi-agent handoffs that restore from a persisted mid-network snapshot - [`examples/persistent-streaming.ts`](/Users/davidkpiano/Code/agent/examples/persistent-streaming.ts): restore a crashed streaming invoke and continue with only new live emitted parts diff --git a/src/examples.test.ts b/src/examples.test.ts index 640dc95..cef587c 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -26,6 +26,7 @@ import { createMapReduceExample, createMultiAgentNetworkExample, createNewspaperExample, + createNextAiSdkUiRoute, createNextReviewRouteHandlers, createNextStreamingRouteHandlers, runPersistenceExample, @@ -592,6 +593,53 @@ describe('curated examples', () => { }); }); + test('next ai sdk ui route streams UI message parts from machine emissions', async () => { + const route = createNextAiSdkUiRoute({ + streamReply: async ({ messages, onDelta }) => { + expect(messages.at(-1)).toMatchObject({ + role: 'user', + }); + onDelta('Hel'); + onDelta('lo'); + return { text: 'Hello' }; + }, + }); + + const response = await route.POST( + new Request('https://agent.test/api/chat', { + method: 'POST', + body: JSON.stringify({ + messages: [ + { + id: 'user-1', + role: 'user', + parts: [ + { + type: 'text', + text: 'Say hello.', + }, + ], + }, + ], + }), + headers: { + 'content-type': 'application/json', + }, + }) + ); + const body = await response.text(); + + expect(response.headers.get('x-vercel-ai-ui-message-stream')).toBe('v1'); + expect(body).toContain('"type":"data-notification"'); + expect(body).toContain('"message":"Drafting reply..."'); + expect(body).toContain('"type":"source"'); + expect(body).toContain('"title":"Stately Agent documentation"'); + expect(body).toContain('"type":"text-start"'); + expect(body).toContain('"delta":"Hel"'); + expect(body).toContain('"delta":"lo"'); + expect(body).toContain('"type":"text-end"'); + }); + test('cloudflare durable network example restores and settles a network run', async () => { const storage = new Map(); const firstInstance = new AgentNetworkDurableObject({ From be277fbf3e2275433abbee94c0be598fd790f7c9 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Mon, 27 Apr 2026 05:02:52 -0400 Subject: [PATCH 31/34] feat: reorganize and modernize examples --- docs/crewai-parity.md | 59 +++++ examples/README.md | 77 ++++++ examples/_run.ts | 83 ++++++- examples/apps/cloudflare-agents/README.md | 10 + examples/apps/cloudflare-agents/src/index.ts | 14 ++ .../src/review-workflow-agent.ts | 85 +++++++ examples/apps/next/README.md | 18 ++ examples/apps/next/app/api/chat/route.ts | 9 + .../[sessionId]/events/route.ts | 9 + .../api/review-sessions/[sessionId]/route.ts | 9 + .../next/app/api/review-sessions/route.ts | 9 + .../api/stream-sessions/[sessionId]/route.ts | 9 + .../[sessionId]/stream/route.ts | 9 + .../next/app/api/stream-sessions/route.ts | 9 + examples/apps/next/lib/routes.ts | 16 ++ examples/chatbot-messages.ts | 39 +-- examples/chatbot.ts | 58 +++-- examples/content-creator-flow.ts | 141 +++++++++++ examples/email-auto-responder-flow.ts | 228 ++++++++++++++++++ examples/email.ts | 42 ++-- examples/hitl.ts | 48 ++-- examples/index.ts | 54 +++-- examples/lead-score-flow.ts | 209 ++++++++++++++++ examples/meeting-assistant-flow.ts | 152 ++++++++++++ examples/raffle.ts | 33 ++- examples/react-agent.ts | 12 +- examples/self-evaluation-loop-flow.ts | 133 ++++++++++ examples/sql-agent.ts | 12 +- examples/tool-calling.ts | 12 +- examples/write-a-book-flow.ts | 199 +++++++++++++++ readme.md | 34 +-- .../content-creator-flow.test.ts | 30 +++ .../email-auto-responder-flow.test.ts | 41 ++++ .../lead-score-flow.test.ts | 61 +++++ .../meeting-assistant-flow.test.ts | 41 ++++ .../self-evaluation-loop-flow.test.ts | 39 +++ .../write-a-book-flow.test.ts | 44 ++++ src/examples.test.ts | 58 +++++ 38 files changed, 1997 insertions(+), 148 deletions(-) create mode 100644 docs/crewai-parity.md create mode 100644 examples/README.md create mode 100644 examples/apps/cloudflare-agents/README.md create mode 100644 examples/apps/cloudflare-agents/src/index.ts create mode 100644 examples/apps/cloudflare-agents/src/review-workflow-agent.ts create mode 100644 examples/apps/next/README.md create mode 100644 examples/apps/next/app/api/chat/route.ts create mode 100644 examples/apps/next/app/api/review-sessions/[sessionId]/events/route.ts create mode 100644 examples/apps/next/app/api/review-sessions/[sessionId]/route.ts create mode 100644 examples/apps/next/app/api/review-sessions/route.ts create mode 100644 examples/apps/next/app/api/stream-sessions/[sessionId]/route.ts create mode 100644 examples/apps/next/app/api/stream-sessions/[sessionId]/stream/route.ts create mode 100644 examples/apps/next/app/api/stream-sessions/route.ts create mode 100644 examples/apps/next/lib/routes.ts create mode 100644 examples/content-creator-flow.ts create mode 100644 examples/email-auto-responder-flow.ts create mode 100644 examples/lead-score-flow.ts create mode 100644 examples/meeting-assistant-flow.ts create mode 100644 examples/self-evaluation-loop-flow.ts create mode 100644 examples/write-a-book-flow.ts create mode 100644 src/crewai-equivalents/content-creator-flow.test.ts create mode 100644 src/crewai-equivalents/email-auto-responder-flow.test.ts create mode 100644 src/crewai-equivalents/lead-score-flow.test.ts create mode 100644 src/crewai-equivalents/meeting-assistant-flow.test.ts create mode 100644 src/crewai-equivalents/self-evaluation-loop-flow.test.ts create mode 100644 src/crewai-equivalents/write-a-book-flow.test.ts diff --git a/docs/crewai-parity.md b/docs/crewai-parity.md new file mode 100644 index 0000000..3514e24 --- /dev/null +++ b/docs/crewai-parity.md @@ -0,0 +1,59 @@ +# CrewAI Flows Parity + +## Scope + +This document tracks where `@statelyai/agent` covers the practical workflow patterns shown in the official `crewAIInc/crewAI-examples` Flows directory as of April 26, 2026. + +It is intentionally scoped to: + +- runnable workflow patterns +- state/routing/runtime behavior +- human-in-the-loop and iteration behavior +- examples and tests in this repo + +It is intentionally not scoped to: + +- CrewAI-specific decorators and class APIs +- CrewAI Enterprise triggers/integrations as products +- Python-only configuration formats + +## External reference + +CrewAI’s official examples repo currently lists these Flow examples: + +- Content Creator Flow +- Email Auto Responder Flow +- Lead Score Flow +- Meeting Assistant Flow +- Self Evaluation Loop Flow +- Write a Book with Flows + +Primary sources: + +- [CrewAI examples index](https://docs.crewai.com/en/examples/example) +- [CrewAI Flows docs](https://docs.crewai.com/en/concepts/flows) +- [CrewAI examples repo](https://github.com/crewAIInc/crewAI-examples) + +## Matrix + + + +| CrewAI Flow example | Status | Agent equivalent | +| --- | --- | --- | +| Content Creator Flow | Covered | [`examples/content-creator-flow.ts`](/Users/davidkpiano/Code/agent/examples/content-creator-flow.ts), [`src/crewai-equivalents/content-creator-flow.test.ts`](/Users/davidkpiano/Code/agent/src/crewai-equivalents/content-creator-flow.test.ts) | +| Email Auto Responder Flow | Covered | [`examples/email-auto-responder-flow.ts`](/Users/davidkpiano/Code/agent/examples/email-auto-responder-flow.ts), [`src/crewai-equivalents/email-auto-responder-flow.test.ts`](/Users/davidkpiano/Code/agent/src/crewai-equivalents/email-auto-responder-flow.test.ts) | +| Lead Score Flow | Covered | [`examples/lead-score-flow.ts`](/Users/davidkpiano/Code/agent/examples/lead-score-flow.ts), [`src/crewai-equivalents/lead-score-flow.test.ts`](/Users/davidkpiano/Code/agent/src/crewai-equivalents/lead-score-flow.test.ts) | +| Meeting Assistant Flow | Covered | [`examples/meeting-assistant-flow.ts`](/Users/davidkpiano/Code/agent/examples/meeting-assistant-flow.ts), [`src/crewai-equivalents/meeting-assistant-flow.test.ts`](/Users/davidkpiano/Code/agent/src/crewai-equivalents/meeting-assistant-flow.test.ts) | +| Self Evaluation Loop Flow | Covered | [`examples/self-evaluation-loop-flow.ts`](/Users/davidkpiano/Code/agent/examples/self-evaluation-loop-flow.ts), [`src/crewai-equivalents/self-evaluation-loop-flow.test.ts`](/Users/davidkpiano/Code/agent/src/crewai-equivalents/self-evaluation-loop-flow.test.ts) | +| Write a Book with Flows | Covered | [`examples/write-a-book-flow.ts`](/Users/davidkpiano/Code/agent/examples/write-a-book-flow.ts), [`src/crewai-equivalents/write-a-book-flow.test.ts`](/Users/davidkpiano/Code/agent/src/crewai-equivalents/write-a-book-flow.test.ts) | + +## Notes + +- CrewAI’s `content_creator_flow/` directory in the current examples repo clone is empty, so that equivalence is based on the current official descriptions: multi-format content routing across blog, LinkedIn, and research outputs. +- Several of these patterns overlap with existing generic examples here, but they are still represented as CrewAI-named examples so the parity surface is explicit instead of inferred. + +## Differences + +- Logic remains explicit state-machine logic instead of CrewAI decorator-based method routing. +- Durable sessions are modeled through first-class snapshots and event journals rather than framework-managed persistence hidden behind class methods. +- Fan-out is expressed in plain JavaScript `Promise.all(...)` inside invokes where that is simpler than introducing framework-specific branching primitives. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..da9b6bb --- /dev/null +++ b/examples/README.md @@ -0,0 +1,77 @@ +# Examples + + + +This directory is organized by what a developer is trying to do, not by the underlying primitive. + +## Start Here + +- Building an app route: [`apps/next/`](/Users/davidkpiano/Code/agent/examples/apps/next) or [`apps/cloudflare-agents/`](/Users/davidkpiano/Code/agent/examples/apps/cloudflare-agents) +- Adding durable sessions: [`persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts) and [`http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts) +- Streaming text or tool progress: [`next-ai-sdk-ui.ts`](/Users/davidkpiano/Code/agent/examples/next-ai-sdk-ui.ts), [`http-streaming-session.ts`](/Users/davidkpiano/Code/agent/examples/http-streaming-session.ts), and [`tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts) +- Studying orchestration patterns: start in `Workflow Examples` + +## App-Shaped Examples + +These are the best starting points when you want code that already looks like a real app: + +- [`apps/next/`](/Users/davidkpiano/Code/agent/examples/apps/next): copy-paste Next.js App Router routes +- [`apps/cloudflare-agents/`](/Users/davidkpiano/Code/agent/examples/apps/cloudflare-agents): copy-paste Cloudflare Agents Worker layout +- [`next-ai-sdk-ui.ts`](/Users/davidkpiano/Code/agent/examples/next-ai-sdk-ui.ts): AI SDK UI route helper +- [`next-app-router.ts`](/Users/davidkpiano/Code/agent/examples/next-app-router.ts): App Router session route helpers +- [`cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts): Node-safe Cloudflare Agents helper version + +## Workflow Examples + +These focus on real orchestration patterns: + +- Session-first interactive workflows +- Durable restore and transport patterns +- Multi-step planning, routing, and handoff flows + +- [`persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts) +- [`persistent-streaming.ts`](/Users/davidkpiano/Code/agent/examples/persistent-streaming.ts) +- [`persistent-supervisor.ts`](/Users/davidkpiano/Code/agent/examples/persistent-supervisor.ts) +- [`persistent-multi-agent-network.ts`](/Users/davidkpiano/Code/agent/examples/persistent-multi-agent-network.ts) +- [`content-creator-flow.ts`](/Users/davidkpiano/Code/agent/examples/content-creator-flow.ts) +- [`email-auto-responder-flow.ts`](/Users/davidkpiano/Code/agent/examples/email-auto-responder-flow.ts) +- [`lead-score-flow.ts`](/Users/davidkpiano/Code/agent/examples/lead-score-flow.ts) +- [`meeting-assistant-flow.ts`](/Users/davidkpiano/Code/agent/examples/meeting-assistant-flow.ts) +- [`self-evaluation-loop-flow.ts`](/Users/davidkpiano/Code/agent/examples/self-evaluation-loop-flow.ts) +- [`write-a-book-flow.ts`](/Users/davidkpiano/Code/agent/examples/write-a-book-flow.ts) +- [`plan-and-execute.ts`](/Users/davidkpiano/Code/agent/examples/plan-and-execute.ts) +- [`reflection.ts`](/Users/davidkpiano/Code/agent/examples/reflection.ts) +- [`rewoo.ts`](/Users/davidkpiano/Code/agent/examples/rewoo.ts) +- [`rag.ts`](/Users/davidkpiano/Code/agent/examples/rag.ts) +- [`sql-agent.ts`](/Users/davidkpiano/Code/agent/examples/sql-agent.ts) + +## Runtime / Transport Examples + +- [`http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts) +- [`http-streaming-session.ts`](/Users/davidkpiano/Code/agent/examples/http-streaming-session.ts) +- [`cloudflare-durable-object.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-durable-object.ts) +- [`cloudflare-durable-network.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-durable-network.ts) + +## Reference / Concept Examples + +These are smaller building-block examples: + +- One-shot machine execution: [`simple.ts`](/Users/davidkpiano/Code/agent/examples/simple.ts), [`decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts), [`classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts) +- Interactive session lifecycle: [`chatbot.ts`](/Users/davidkpiano/Code/agent/examples/chatbot.ts), [`chatbot-messages.ts`](/Users/davidkpiano/Code/agent/examples/chatbot-messages.ts), [`hitl.ts`](/Users/davidkpiano/Code/agent/examples/hitl.ts), [`raffle.ts`](/Users/davidkpiano/Code/agent/examples/raffle.ts) + +- [`simple.ts`](/Users/davidkpiano/Code/agent/examples/simple.ts) +- [`decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts) +- [`classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts) +- [`adapter.ts`](/Users/davidkpiano/Code/agent/examples/adapter.ts) +- [`tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts) +- [`hitl.ts`](/Users/davidkpiano/Code/agent/examples/hitl.ts) +- [`branching.ts`](/Users/davidkpiano/Code/agent/examples/branching.ts) +- [`subflow.ts`](/Users/davidkpiano/Code/agent/examples/subflow.ts) +- [`conditional-subflow.ts`](/Users/davidkpiano/Code/agent/examples/conditional-subflow.ts) + +## Parity Tracking + +- [`../docs/langgraph-parity.md`](/Users/davidkpiano/Code/agent/docs/langgraph-parity.md) +- [`../docs/crewai-parity.md`](/Users/davidkpiano/Code/agent/docs/crewai-parity.md) + +The parity docs track end-result coverage. The files here are the runnable equivalents. diff --git a/examples/_run.ts b/examples/_run.ts index 36f0b65..5e62dc1 100644 --- a/examples/_run.ts +++ b/examples/_run.ts @@ -6,7 +6,13 @@ import { createInterface } from 'node:readline/promises'; import { stdin as input, stdout as output } from 'node:process'; import { pathToFileURL } from 'node:url'; import { z } from 'zod'; -import type { AgentAdapter, ExecuteResult, StandardSchemaV1 } from '../src/index.js'; +import type { + AgentAdapter, + AgentRun, + AgentSnapshot, + ExecuteResult, + StandardSchemaV1, +} from '../src/index.js'; export function isMain(moduleUrl: string): boolean { const entry = process.argv[1]; @@ -91,6 +97,81 @@ export function formatResult(result: ExecuteResult) { }; } +export function waitForRunDone< + TContext extends Record, + TValue extends string, + TEvents extends Record, + TOutput, + TEmitted extends Record, +>( + run: AgentRun +): Promise<{ + output: TOutput; + snapshot: AgentSnapshot; +}> { + return new Promise((resolve, reject) => { + const offDone = run.onDone((event) => { + offDone(); + offError(); + resolve(event); + }); + const offError = run.onError((event) => { + offDone(); + offError(); + reject(event.error); + }); + }); +} + +export function waitForRunSnapshot< + TContext extends Record, + TValue extends string, + TEvents extends Record, + TOutput, + TEmitted extends Record, +>( + run: AgentRun, + predicate: ( + snapshot: AgentSnapshot + ) => boolean, + timeoutMs = 1000 +): Promise> { + const current = run.getSnapshot(); + if (predicate(current)) { + return Promise.resolve(current); + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + cleanup(); + reject(new Error('Run snapshot did not reach the expected state in time.')); + }, timeoutMs); + + const cleanup = () => { + clearTimeout(timeout); + offSnapshot(); + offDone(); + offError(); + }; + + const check = (snapshot: AgentSnapshot) => { + if (predicate(snapshot)) { + cleanup(); + resolve(snapshot); + } + }; + + const offSnapshot = run.onSnapshot(check); + const offDone = run.onDone((event) => { + check(event.snapshot); + }); + const offError = run.onError((event) => { + cleanup(); + reject(event.error); + }); + }); +} + export function createOpenAiDecisionAdapter(): AgentAdapter { return { async decide({ model, prompt, options, reasoning }) { diff --git a/examples/apps/cloudflare-agents/README.md b/examples/apps/cloudflare-agents/README.md new file mode 100644 index 0000000..ca2cc9c --- /dev/null +++ b/examples/apps/cloudflare-agents/README.md @@ -0,0 +1,10 @@ +# Cloudflare Agents Worker Example + +These files show the Cloudflare Agents integration in a real Worker layout with top-level `agents` imports, instead of the Node-safe lazy import used in [`examples/cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts). + +Included files: + +- `src/review-workflow-agent.ts`: the Agent class that owns the durable review workflow +- `src/index.ts`: the Worker entrypoint that delegates requests through `routeAgentRequest(...)` + +Use this layout when you want a copy-paste starting point for a real Cloudflare Agents app. diff --git a/examples/apps/cloudflare-agents/src/index.ts b/examples/apps/cloudflare-agents/src/index.ts new file mode 100644 index 0000000..190e940 --- /dev/null +++ b/examples/apps/cloudflare-agents/src/index.ts @@ -0,0 +1,14 @@ +import { routeAgentRequest } from 'agents'; +import { ReviewWorkflowAgent } from './review-workflow-agent.js'; + +export { ReviewWorkflowAgent }; + +export default { + async fetch(request: Request, env: Record) { + return ( + await routeAgentRequest(request, env, { + prefix: '/agents', + }) + ) ?? new Response('Not found', { status: 404 }); + }, +}; diff --git a/examples/apps/cloudflare-agents/src/review-workflow-agent.ts b/examples/apps/cloudflare-agents/src/review-workflow-agent.ts new file mode 100644 index 0000000..5fe1ec7 --- /dev/null +++ b/examples/apps/cloudflare-agents/src/review-workflow-agent.ts @@ -0,0 +1,85 @@ +import { Agent } from 'agents'; +import { restoreSession, startSession, type RunStore } from '../../../../src/index.js'; +import { + createCloudflareAgentRunStore, + type CloudflareAgentRunStoreState, +} from '../../../cloudflare-agents.js'; +import { createPersistenceExample } from '../../../persistence.js'; + +export class ReviewWorkflowAgent extends Agent< + Record, + CloudflareAgentRunStoreState +> { + initialState: CloudflareAgentRunStoreState = { + sessions: {}, + }; + + private getStore(): RunStore { + return createCloudflareAgentRunStore({ + getState: () => this.state ?? this.initialState, + setState: (nextState) => this.setState(nextState), + }); + } + + async onRequest(request: Request): Promise { + const url = new URL(request.url); + const machine = createPersistenceExample(); + + if (request.method === 'POST' && url.pathname.endsWith('/start')) { + const body = await request.json() as { request: string }; + const run = await startSession(machine, { + store: this.getStore(), + input: { + request: body.request, + }, + }); + + return Response.json({ + sessionId: run.sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'POST' && url.pathname.endsWith('/events')) { + const body = await request.json() as { + sessionId: string; + event: { type: 'approve' }; + }; + const run = await restoreSession(machine, { + sessionId: body.sessionId, + store: this.getStore(), + }); + + await run.send(body.event); + + return Response.json({ + sessionId: body.sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'GET' && url.pathname.endsWith('/snapshot')) { + const sessionId = requiredSessionId(url); + const run = await restoreSession(machine, { + sessionId, + store: this.getStore(), + }); + + return Response.json({ + sessionId, + snapshot: run.getSnapshot(), + }); + } + + return new Response('Not found', { status: 404 }); + } +} + +function requiredSessionId(url: URL): string { + const sessionId = url.searchParams.get('sessionId'); + if (!sessionId) { + throw new Error('Missing sessionId'); + } + + return sessionId; +} diff --git a/examples/apps/next/README.md b/examples/apps/next/README.md new file mode 100644 index 0000000..bc1ad10 --- /dev/null +++ b/examples/apps/next/README.md @@ -0,0 +1,18 @@ +# Next App Router Examples + +These files show the same `@statelyai/agent` examples in a shape you can drop directly into a Next.js App Router project. + +Included routes: + +- `app/api/chat/route.ts`: AI SDK UI message streaming route +- `app/api/review-sessions/route.ts`: start a durable review session +- `app/api/review-sessions/[sessionId]/route.ts`: fetch a review session snapshot +- `app/api/review-sessions/[sessionId]/events/route.ts`: send events to a review session +- `app/api/stream-sessions/route.ts`: start a streaming session +- `app/api/stream-sessions/[sessionId]/route.ts`: fetch a streaming session snapshot +- `app/api/stream-sessions/[sessionId]/stream/route.ts`: consume the streaming SSE response + +The route handlers are backed by: + +- [`examples/next-app-router.ts`](/Users/davidkpiano/Code/agent/examples/next-app-router.ts) +- [`examples/next-ai-sdk-ui.ts`](/Users/davidkpiano/Code/agent/examples/next-ai-sdk-ui.ts) diff --git a/examples/apps/next/app/api/chat/route.ts b/examples/apps/next/app/api/chat/route.ts new file mode 100644 index 0000000..0cc706e --- /dev/null +++ b/examples/apps/next/app/api/chat/route.ts @@ -0,0 +1,9 @@ +import { + chatRoute, + dynamic, + maxDuration, + runtime, +} from '../../../lib/routes.js'; + +export { runtime, dynamic, maxDuration }; +export const POST = chatRoute.POST; diff --git a/examples/apps/next/app/api/review-sessions/[sessionId]/events/route.ts b/examples/apps/next/app/api/review-sessions/[sessionId]/events/route.ts new file mode 100644 index 0000000..3234862 --- /dev/null +++ b/examples/apps/next/app/api/review-sessions/[sessionId]/events/route.ts @@ -0,0 +1,9 @@ +import { + dynamic, + maxDuration, + reviewRoutes, + runtime, +} from '../../../../../lib/routes.js'; + +export { runtime, dynamic, maxDuration }; +export const POST = reviewRoutes.events.POST; diff --git a/examples/apps/next/app/api/review-sessions/[sessionId]/route.ts b/examples/apps/next/app/api/review-sessions/[sessionId]/route.ts new file mode 100644 index 0000000..0f98e4b --- /dev/null +++ b/examples/apps/next/app/api/review-sessions/[sessionId]/route.ts @@ -0,0 +1,9 @@ +import { + dynamic, + maxDuration, + reviewRoutes, + runtime, +} from '../../../../lib/routes.js'; + +export { runtime, dynamic, maxDuration }; +export const GET = reviewRoutes.session.GET; diff --git a/examples/apps/next/app/api/review-sessions/route.ts b/examples/apps/next/app/api/review-sessions/route.ts new file mode 100644 index 0000000..30f3729 --- /dev/null +++ b/examples/apps/next/app/api/review-sessions/route.ts @@ -0,0 +1,9 @@ +import { + dynamic, + maxDuration, + reviewRoutes, + runtime, +} from '../../../lib/routes.js'; + +export { runtime, dynamic, maxDuration }; +export const POST = reviewRoutes.sessions.POST; diff --git a/examples/apps/next/app/api/stream-sessions/[sessionId]/route.ts b/examples/apps/next/app/api/stream-sessions/[sessionId]/route.ts new file mode 100644 index 0000000..bce7ee0 --- /dev/null +++ b/examples/apps/next/app/api/stream-sessions/[sessionId]/route.ts @@ -0,0 +1,9 @@ +import { + dynamic, + maxDuration, + runtime, + streamingRoutes, +} from '../../../../lib/routes.js'; + +export { runtime, dynamic, maxDuration }; +export const GET = streamingRoutes.session.GET; diff --git a/examples/apps/next/app/api/stream-sessions/[sessionId]/stream/route.ts b/examples/apps/next/app/api/stream-sessions/[sessionId]/stream/route.ts new file mode 100644 index 0000000..89ceefe --- /dev/null +++ b/examples/apps/next/app/api/stream-sessions/[sessionId]/stream/route.ts @@ -0,0 +1,9 @@ +import { + dynamic, + maxDuration, + runtime, + streamingRoutes, +} from '../../../../../lib/routes.js'; + +export { runtime, dynamic, maxDuration }; +export const GET = streamingRoutes.stream.GET; diff --git a/examples/apps/next/app/api/stream-sessions/route.ts b/examples/apps/next/app/api/stream-sessions/route.ts new file mode 100644 index 0000000..296725f --- /dev/null +++ b/examples/apps/next/app/api/stream-sessions/route.ts @@ -0,0 +1,9 @@ +import { + dynamic, + maxDuration, + runtime, + streamingRoutes, +} from '../../../lib/routes.js'; + +export { runtime, dynamic, maxDuration }; +export const POST = streamingRoutes.sessions.POST; diff --git a/examples/apps/next/lib/routes.ts b/examples/apps/next/lib/routes.ts new file mode 100644 index 0000000..348f3ec --- /dev/null +++ b/examples/apps/next/lib/routes.ts @@ -0,0 +1,16 @@ +import { + createNextReviewRouteHandlers, + createNextStreamingRouteHandlers, + dynamic as nextDynamic, + maxDuration as nextMaxDuration, + runtime as nextRuntime, +} from '../../../next-app-router.js'; +import { createNextAiSdkUiRoute } from '../../../next-ai-sdk-ui.js'; + +export const runtime = nextRuntime; +export const dynamic = nextDynamic; +export const maxDuration = nextMaxDuration; + +export const reviewRoutes = createNextReviewRouteHandlers(); +export const streamingRoutes = createNextStreamingRouteHandlers(); +export const chatRoute = createNextAiSdkUiRoute(); diff --git a/examples/chatbot-messages.ts b/examples/chatbot-messages.ts index 85ab4e8..bb1c977 100644 --- a/examples/chatbot-messages.ts +++ b/examples/chatbot-messages.ts @@ -1,10 +1,15 @@ import { z } from 'zod'; -import { createAgentMachine } from '../src/index.js'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from '../src/index.js'; import { closePrompt, generateExampleObject, isMain, prompt, + waitForRunSnapshot, } from './_run.js'; const messageSchema = z.object({ @@ -90,31 +95,37 @@ export function createChatbotMessagesExample( async function main() { try { const machine = createChatbotMessagesExample(); - let state = machine.getInitialState(); + const run = await startSession(machine, { + store: createMemoryRunStore(), + }); let lastPrintedAssistantMessage: string | null = null; while (true) { - const result = await machine.execute(state); + const snapshot = await waitForRunSnapshot( + run, + (nextSnapshot) => nextSnapshot.status !== 'active' + ); - if (result.status === 'done') { + if (snapshot.status === 'done') { + console.log({ + status: snapshot.status, + value: snapshot.value, + context: snapshot.context, + output: snapshot.output, + }); break; } - if (result.status !== 'pending') { - throw new Error('Chatbot messages example entered an unexpected error state.'); - } - if ( - result.context.finalMessage?.role === 'assistant' - && result.context.finalMessage.content !== lastPrintedAssistantMessage + snapshot.context.finalMessage?.role === 'assistant' + && snapshot.context.finalMessage.content !== lastPrintedAssistantMessage ) { - console.log(`Assistant: ${result.context.finalMessage.content}`); - lastPrintedAssistantMessage = result.context.finalMessage.content; + console.log(`Assistant: ${snapshot.context.finalMessage.content}`); + lastPrintedAssistantMessage = snapshot.context.finalMessage.content; } const content = await prompt('User (blank to exit)'); - state = machine.transition( - result.state, + await run.send( content ? { type: 'messages.user', diff --git a/examples/chatbot.ts b/examples/chatbot.ts index ab390c1..8f53f30 100644 --- a/examples/chatbot.ts +++ b/examples/chatbot.ts @@ -1,12 +1,19 @@ import { z } from 'zod'; -import { createAgentMachine, decide, decideResultSchema, type AgentAdapter } from '../src/index.js'; +import { + createAgentMachine, + createMemoryRunStore, + decide, + decideResultSchema, + startSession, + type AgentAdapter, +} from '../src/index.js'; import { closePrompt, createOpenAiDecisionAdapter, - formatResult, generateExampleObject, isMain, prompt, + waitForRunSnapshot, } from './_run.js'; const replySchema = z.object({ @@ -122,41 +129,46 @@ export function createChatbotExample( async function main() { try { const machine = createChatbotExample(); - let state = machine.getInitialState(); + const run = await startSession(machine, { + store: createMemoryRunStore(), + }); let lastPrintedAssistantMessage: string | null = null; while (true) { - const result = await machine.execute(state); + const snapshot = await waitForRunSnapshot( + run, + (nextSnapshot) => nextSnapshot.status !== 'active' + ); - if (result.status === 'done') { + if (snapshot.status === 'done') { if ( - result.output && - typeof result.output === 'object' && - 'lastAssistantMessage' in result.output && - result.output.lastAssistantMessage && - result.output.lastAssistantMessage !== lastPrintedAssistantMessage + snapshot.output && + typeof snapshot.output === 'object' && + 'lastAssistantMessage' in snapshot.output && + snapshot.output.lastAssistantMessage && + snapshot.output.lastAssistantMessage !== lastPrintedAssistantMessage ) { - console.log(`Assistant: ${result.output.lastAssistantMessage}`); + console.log(`Assistant: ${snapshot.output.lastAssistantMessage}`); } - console.log(formatResult(result)); + console.log({ + status: snapshot.status, + value: snapshot.value, + context: snapshot.context, + output: snapshot.output, + }); break; } - if (result.status !== 'pending') { - throw new Error('Chatbot example entered an unexpected error state.'); - } - - if ( - result.context.lastAssistantMessage && - result.context.lastAssistantMessage !== lastPrintedAssistantMessage + if ( + snapshot.context.lastAssistantMessage && + snapshot.context.lastAssistantMessage !== lastPrintedAssistantMessage ) { - console.log(`Assistant: ${result.context.lastAssistantMessage}`); - lastPrintedAssistantMessage = result.context.lastAssistantMessage; + console.log(`Assistant: ${snapshot.context.lastAssistantMessage}`); + lastPrintedAssistantMessage = snapshot.context.lastAssistantMessage; } const message = await prompt('User (blank to exit)'); - state = machine.transition( - result.state, + await run.send( message ? { type: 'user.message', message } : { type: 'user.exit' } diff --git a/examples/content-creator-flow.ts b/examples/content-creator-flow.ts new file mode 100644 index 0000000..efc483d --- /dev/null +++ b/examples/content-creator-flow.ts @@ -0,0 +1,141 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const routeSchema = z.object({ + route: z.enum(['blog', 'linkedin', 'research']), +}); + +const contentSchema = z.object({ + title: z.string(), + body: z.string(), +}); + +type ContentRoute = z.infer['route']; + +export function createContentCreatorFlowExample(options: { + routeRequest?: (request: string) => Promise>; + createBlog?: (request: string) => Promise>; + createLinkedInPost?: (request: string) => Promise>; + createResearchReport?: (request: string) => Promise>; +} = {}) { + const routeRequest = + options.routeRequest ?? + ((request: string) => + generateExampleObject({ + schema: routeSchema, + system: + 'Route content requests to blog, linkedin, or research. Choose research for analysis-heavy requests, linkedin for short professional posts, and blog for longer educational pieces.', + prompt: request, + })); + + const createBlog = + options.createBlog ?? + ((request: string) => + generateExampleObject({ + schema: contentSchema, + system: 'Write a concise professional blog post.', + prompt: request, + })); + + const createLinkedInPost = + options.createLinkedInPost ?? + ((request: string) => + generateExampleObject({ + schema: contentSchema, + system: 'Write a concise professional LinkedIn post.', + prompt: request, + })); + + const createResearchReport = + options.createResearchReport ?? + ((request: string) => + generateExampleObject({ + schema: contentSchema, + system: 'Write a concise research-style briefing with findings and implications.', + prompt: request, + })); + + return createAgentMachine({ + id: 'content-creator-flow-example', + schemas: { + input: z.object({ + request: z.string(), + }), + output: z.object({ + route: z.enum(['blog', 'linkedin', 'research']).nullable(), + title: z.string().nullable(), + body: z.string().nullable(), + }), + }, + context: (input) => ({ + request: input.request, + route: null as ContentRoute | null, + title: null as string | null, + body: null as string | null, + }), + initial: 'routing', + states: { + routing: { + resultSchema: routeSchema, + invoke: async ({ context }) => routeRequest(context.request), + onDone: ({ result }) => ({ + target: 'creating', + context: { + route: result.route, + }, + }), + }, + creating: { + resultSchema: contentSchema, + invoke: async ({ context }) => { + switch (context.route) { + case 'linkedin': + return createLinkedInPost(context.request); + case 'research': + return createResearchReport(context.request); + case 'blog': + default: + return createBlog(context.request); + } + }, + onDone: ({ result }) => ({ + target: 'done', + context: { + title: result.title, + body: result.body, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + route: context.route, + title: context.title, + body: context.body, + }), + }, + }, + }); +} + +async function main() { + try { + const request = await prompt('Content request'); + const machine = createContentCreatorFlowExample(); + const result = await machine.execute(machine.getInitialState({ request })); + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/email-auto-responder-flow.ts b/examples/email-auto-responder-flow.ts new file mode 100644 index 0000000..8ab30ea --- /dev/null +++ b/examples/email-auto-responder-flow.ts @@ -0,0 +1,228 @@ +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + restoreSession, + startSession, + type RunStore, +} from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const incomingEmailSchema = z.object({ + id: z.string(), + subject: z.string(), + body: z.string(), + sender: z.string(), +}); + +const draftResponseSchema = z.object({ + draft: z.string(), +}); + +type IncomingEmail = z.infer; + +export function createEmailAutoResponderFlowExample( + createDraft: (email: IncomingEmail) => Promise> = ( + email + ) => + generateExampleObject({ + schema: draftResponseSchema, + system: 'Write a concise professional email reply draft.', + prompt: [ + `Sender: ${email.sender}`, + `Subject: ${email.subject}`, + '', + email.body, + ].join('\n'), + }) +) { + return createAgentMachine({ + id: 'email-auto-responder-flow-example', + schemas: { + input: z.object({}), + output: z.object({ + processedIds: z.array(z.string()), + drafts: z.record(z.string(), z.string()), + }), + events: { + 'emails.received': z.object({ + emails: z.array(incomingEmailSchema), + }), + stop: z.object({}), + }, + }, + context: () => ({ + queue: [] as IncomingEmail[], + currentEmail: null as IncomingEmail | null, + processedIds: [] as string[], + drafts: {} as Record, + }), + initial: 'waiting', + states: { + waiting: { + on: { + 'emails.received': ({ context, event }) => { + const nextQueue = [...context.queue, ...event.emails].filter( + (email) => + !context.processedIds.includes(email.id) + && email.id !== context.currentEmail?.id + ); + const [currentEmail, ...queue] = nextQueue; + + if (!currentEmail) { + return { + context: { + queue, + }, + }; + } + + return { + target: 'drafting', + context: { + currentEmail, + queue, + }, + }; + }, + stop: { + target: 'done', + }, + }, + }, + drafting: { + on: { + 'emails.received': ({ context, event }) => ({ + context: { + queue: [...context.queue, ...event.emails].filter( + (email) => + !context.processedIds.includes(email.id) + && email.id !== context.currentEmail?.id + ), + }, + }), + stop: { + target: 'done', + }, + }, + resultSchema: draftResponseSchema, + invoke: async ({ context }) => createDraft(context.currentEmail!), + onDone: ({ result, context }) => { + const currentEmail = context.currentEmail!; + const processedIds = [...context.processedIds, currentEmail.id]; + const drafts = { + ...context.drafts, + [currentEmail.id]: result.draft, + }; + const [nextEmail, ...queue] = context.queue; + + if (nextEmail) { + return { + target: 'drafting', + context: { + currentEmail: nextEmail, + queue, + processedIds, + drafts, + }, + }; + } + + return { + target: 'waiting', + context: { + currentEmail: null, + queue: [], + processedIds, + drafts, + }, + }; + }, + }, + done: { + type: 'final', + output: ({ context }) => ({ + processedIds: context.processedIds, + drafts: context.drafts, + }), + }, + }, + }); +} + +export async function runEmailAutoResponderFlowExample( + emails: IncomingEmail[], + options: { + createDraft?: (email: IncomingEmail) => Promise>; + store?: RunStore; + } = {} +) { + const machine = createEmailAutoResponderFlowExample(options.createDraft); + const store = options.store ?? createMemoryRunStore(); + const run = await startSession(machine, { + store, + input: {}, + }); + + await run.send({ + type: 'emails.received', + emails, + }); + + return { + sessionId: run.sessionId, + snapshot: run.getSnapshot(), + restoredSnapshot: ( + await restoreSession(machine, { + sessionId: run.sessionId, + store, + }) + ).getSnapshot(), + }; +} + +async function main() { + try { + const sender = await prompt('Sender'); + const subject = await prompt('Subject'); + const body = await prompt('Body'); + const result = await runEmailAutoResponderFlowExample([ + { + id: 'email-1', + sender, + subject, + body, + }, + ]); + + console.log(formatResult({ + status: + result.snapshot.status === 'done' + ? 'done' + : result.snapshot.status === 'error' + ? 'error' + : 'pending', + state: { + value: result.snapshot.value, + context: result.snapshot.context, + status: result.snapshot.status, + input: result.snapshot.input, + }, + output: result.snapshot.output, + context: result.snapshot.context, + error: result.snapshot.error, + } as never)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/email.ts b/examples/email.ts index 053959a..972228b 100644 --- a/examples/email.ts +++ b/examples/email.ts @@ -1,13 +1,20 @@ import { z } from 'zod'; -import { createAgentMachine, decide, decideResultSchema, type AgentAdapter } from '../src/index.js'; +import { + createAgentMachine, + createMemoryRunStore, + decide, + decideResultSchema, + startSession, + type AgentAdapter, +} from '../src/index.js'; import { closePrompt, createOpenAiDecisionAdapter, - formatResult, generateExampleObject, generateExampleText, isMain, prompt, + waitForRunSnapshot, } from './_run.js'; const draftSchema = z.object({ @@ -219,28 +226,35 @@ async function main() { const email = await prompt('Incoming email'); const instructions = await prompt('Instructions'); const machine = createEmailExample(); - let state = machine.getInitialState({ email, instructions }); + const run = await startSession(machine, { + store: createMemoryRunStore(), + input: { email, instructions }, + }); while (true) { - const result = await machine.execute(state); + const snapshot = await waitForRunSnapshot( + run, + (nextSnapshot) => nextSnapshot.status !== 'active' + ); - if (result.status === 'done') { - console.log(formatResult(result)); + if (snapshot.status === 'done') { + console.log({ + status: snapshot.status, + value: snapshot.value, + context: snapshot.context, + output: snapshot.output, + }); break; } - if (result.status !== 'pending') { - throw new Error('Email example entered an unexpected error state.'); - } - - if (result.value === 'clarifying') { - console.log(result.context.questions.join('\n')); + if (snapshot.value === 'clarifying') { + console.log(snapshot.context.questions.join('\n')); const answer = await prompt('Clarification'); - state = machine.transition(result.state, { type: 'user.answer', answer }); + await run.send({ type: 'user.answer', answer }); continue; } - state = result.state; + throw new Error('Email example entered an unexpected pending state.'); } } finally { closePrompt(); diff --git a/examples/hitl.ts b/examples/hitl.ts index 1074b87..7e88077 100644 --- a/examples/hitl.ts +++ b/examples/hitl.ts @@ -1,11 +1,15 @@ import { z } from 'zod'; -import { createAgentMachine } from '../src/index.js'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from '../src/index.js'; import { closePrompt, - formatResult, generateExampleObject, isMain, prompt, + waitForRunSnapshot, } from './_run.js'; const draftSchema = z.object({ @@ -88,33 +92,49 @@ async function main() { try { const task = await prompt('Task'); const machine = createHitlExample(); - let state = await machine.invoke(machine.getInitialState({ task })); + const run = await startSession(machine, { + store: createMemoryRunStore(), + input: { task }, + }); + + while (true) { + const snapshot = await waitForRunSnapshot( + run, + (nextSnapshot) => nextSnapshot.status !== 'active' + ); + + if (snapshot.status === 'done') { + console.log({ + status: snapshot.status, + value: snapshot.value, + context: snapshot.context, + output: snapshot.output, + }); + break; + } - while (state.status === 'pending') { const message = await prompt('Add note, or type /approve or /cancel'); if (message === '/approve') { - state = machine.transition(state, { type: 'user.approve' }); - break; + await run.send({ type: 'user.approve' }); + continue; } if (message === '/cancel') { - state = machine.transition(state, { type: 'user.cancel' }); - break; + await run.send({ type: 'user.cancel' }); + continue; } - state = machine.transition(state, { + await run.send({ type: 'user.message', message, }); console.log({ - status: state.status, - value: state.value, - context: state.context, + status: run.getSnapshot().status, + value: run.getSnapshot().value, + context: run.getSnapshot().context, }); } - - console.log(formatResult(await machine.execute(state))); } finally { closePrompt(); } diff --git a/examples/index.ts b/examples/index.ts index 14bc9bf..f698113 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -1,15 +1,7 @@ -export { createSimpleExample } from './simple.js'; -export { createSqlAgentExample } from './sql-agent.js'; -export { createHitlExample } from './hitl.js'; +// Runtime and deployment examples export { createPersistenceSessionHttpHandler } from './http-session.js'; export { createStreamingSessionHttpController } from './http-streaming-session.js'; -export { createDecideExample } from './decide.js'; -export { createClassifyExample } from './classify.js'; -export { createAdapterExample } from './adapter.js'; export { createAiSdkExample } from './ai-sdk.js'; -export { createChatbotExample } from './chatbot.js'; -export { createChatbotMessagesExample } from './chatbot-messages.js'; -export { createConditionalSubflowExample } from './conditional-subflow.js'; export { createCloudflareAgentRunStore, createCloudflareAgentsExample, @@ -22,14 +14,6 @@ export { type DurableObjectStorageLike, } from './cloudflare-durable-object.js'; export { AgentNetworkDurableObject } from './cloudflare-durable-network.js'; -export { createCustomerServiceSimExample } from './customer-service-sim.js'; -export { createEmailExample } from './email.js'; -export { createErrorRetryExample } from './error-retry.js'; -export { createJokeExample } from './joke.js'; -export { createJugsExample } from './jugs.js'; -export { createMapReduceExample } from './map-reduce.js'; -export { createMultiAgentNetworkExample } from './multi-agent-network.js'; -export { createNewspaperExample } from './newspaper.js'; export { createNextAiSdkUiRoute, type AgentUiMessage, @@ -42,7 +26,6 @@ export { runtime as nextAppRouterRuntime, type NextRouteContext, } from './next-app-router.js'; -export { createPlanAndExecuteExample } from './plan-and-execute.js'; export { createPersistenceExample, runPersistenceExample } from './persistence.js'; export { createPersistentMultiAgentNetworkExample, @@ -56,6 +39,18 @@ export { createPersistentSupervisorExample, runPersistentSupervisorExample, } from './persistent-supervisor.js'; + +// Workflow examples +export { createContentCreatorFlowExample } from './content-creator-flow.js'; +export { + createEmailAutoResponderFlowExample, + runEmailAutoResponderFlowExample, +} from './email-auto-responder-flow.js'; +export { createErrorRetryExample } from './error-retry.js'; +export { createLeadScoreFlowExample } from './lead-score-flow.js'; +export { createMeetingAssistantFlowExample } from './meeting-assistant-flow.js'; +export { createMultiAgentNetworkExample } from './multi-agent-network.js'; +export { createPlanAndExecuteExample } from './plan-and-execute.js'; export { createRaffleExample } from './raffle.js'; export { createRagExample } from './rag.js'; export { createReactAgentExample } from './react-agent.js'; @@ -67,9 +62,28 @@ export { } from './react-agent-from-scratch.js'; export { createRewooExample } from './rewoo.js'; export { createReflectionExample } from './reflection.js'; -export { createRiverCrossingExample } from './river-crossing.js'; +export { createSelfEvaluationLoopFlowExample } from './self-evaluation-loop-flow.js'; +export { createSupervisorExample } from './supervisor.js'; +export { createWriteABookFlowExample } from './write-a-book-flow.js'; +export { createSqlAgentExample } from './sql-agent.js'; + +// Reference and concept examples +export { createAdapterExample } from './adapter.js'; export { createBranchingExample } from './branching.js'; +export { createChatbotExample } from './chatbot.js'; +export { createChatbotMessagesExample } from './chatbot-messages.js'; +export { createClassifyExample } from './classify.js'; +export { createConditionalSubflowExample } from './conditional-subflow.js'; +export { createCustomerServiceSimExample } from './customer-service-sim.js'; +export { createDecideExample } from './decide.js'; +export { createEmailExample } from './email.js'; +export { createHitlExample } from './hitl.js'; +export { createJokeExample } from './joke.js'; +export { createJugsExample } from './jugs.js'; +export { createMapReduceExample } from './map-reduce.js'; +export { createNewspaperExample } from './newspaper.js'; +export { createRiverCrossingExample } from './river-crossing.js'; +export { createSimpleExample } from './simple.js'; export { createSubflowExample } from './subflow.js'; -export { createSupervisorExample } from './supervisor.js'; export { createToolCallingExample } from './tool-calling.js'; export { createTutorExample } from './tutor.js'; diff --git a/examples/lead-score-flow.ts b/examples/lead-score-flow.ts new file mode 100644 index 0000000..459f643 --- /dev/null +++ b/examples/lead-score-flow.ts @@ -0,0 +1,209 @@ +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from '../src/index.js'; +import { + closePrompt, + isMain, + prompt, + waitForRunSnapshot, +} from './_run.js'; + +const leadSchema = z.object({ + id: z.string(), + company: z.string(), + contact: z.string(), +}); + +const scoredLeadSchema = leadSchema.extend({ + score: z.number().min(0).max(100), + rationale: z.string(), +}); + +const scoringSchema = z.object({ + scoredLeads: z.array(scoredLeadSchema), +}); + +const emailDraftSchema = z.object({ + leadId: z.string(), + draft: z.string(), +}); + +const emailBatchSchema = z.object({ + drafts: z.array(emailDraftSchema), +}); + +type Lead = z.infer; + +export function createLeadScoreFlowExample(options: { + scoreLeads?: (args: { + leads: Lead[]; + reviewNote: string | null; + }) => Promise>; + writeEmails?: (leads: z.infer[]) => Promise>; +} = {}) { + const scoreLeads = + options.scoreLeads ?? + (async ({ leads, reviewNote }) => ({ + scoredLeads: leads + .map((lead, index) => ({ + ...lead, + score: Math.max(0, 90 - index * 10 - (reviewNote ? 5 : 0)), + rationale: reviewNote + ? `Adjusted after review: ${reviewNote}` + : `Initial score for ${lead.company}`, + })) + .sort((a, b) => b.score - a.score), + })); + + const writeEmails = + options.writeEmails ?? + (async (leads) => ({ + drafts: leads.map((lead) => ({ + leadId: lead.id, + draft: `Hi ${lead.contact}, I would love to talk about ${lead.company}.`, + })), + })); + + return createAgentMachine({ + id: 'lead-score-flow-example', + schemas: { + input: z.object({ + leads: z.array(leadSchema), + }), + output: z.object({ + scoredLeads: z.array(scoredLeadSchema), + topLeads: z.array(scoredLeadSchema), + emailDrafts: z.array(emailDraftSchema), + reviewCount: z.number(), + }), + events: { + 'review.approve': z.object({}), + 'review.requestChanges': z.object({ + note: z.string(), + }), + }, + }, + context: (input) => ({ + leads: input.leads, + scoredLeads: [] as z.infer[], + topLeads: [] as z.infer[], + emailDrafts: [] as z.infer[], + reviewNote: null as string | null, + reviewCount: 0, + }), + initial: 'scoring', + states: { + scoring: { + resultSchema: scoringSchema, + invoke: async ({ context }) => + scoreLeads({ + leads: context.leads, + reviewNote: context.reviewNote, + }), + onDone: ({ result, context }) => ({ + target: 'reviewing', + context: { + scoredLeads: result.scoredLeads, + topLeads: result.scoredLeads.slice(0, 3), + reviewNote: null, + reviewCount: context.reviewCount + 1, + }, + }), + }, + reviewing: { + on: { + 'review.approve': { + target: 'writing', + }, + 'review.requestChanges': ({ event }) => ({ + target: 'scoring', + context: { + reviewNote: event.note, + }, + }), + }, + }, + writing: { + resultSchema: emailBatchSchema, + invoke: async ({ context }) => writeEmails(context.scoredLeads), + onDone: ({ result }) => ({ + target: 'done', + context: { + emailDrafts: result.drafts, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + scoredLeads: context.scoredLeads, + topLeads: context.topLeads, + emailDrafts: context.emailDrafts, + reviewCount: context.reviewCount, + }), + }, + }, + }); +} + +async function main() { + try { + const companies = (await prompt('Comma-separated company names')) + .split(',') + .map((value) => value.trim()) + .filter(Boolean); + const machine = createLeadScoreFlowExample(); + const run = await startSession(machine, { + store: createMemoryRunStore(), + input: { + leads: companies.map((company, index) => ({ + id: `lead-${index + 1}`, + company, + contact: `Contact ${index + 1}`, + })), + }, + }); + + while (true) { + const snapshot = await waitForRunSnapshot( + run, + (nextSnapshot) => nextSnapshot.status !== 'active' + ); + + if (snapshot.status === 'done') { + console.log({ + status: snapshot.status, + value: snapshot.value, + context: snapshot.context, + output: snapshot.output, + }); + break; + } + + if (snapshot.value === 'reviewing') { + console.log(snapshot.context.topLeads); + const answer = await prompt('Type /approve or provide a review note'); + await run.send( + answer === '/approve' + ? { type: 'review.approve' } + : { + type: 'review.requestChanges', + note: answer, + } + ); + continue; + } + + throw new Error('Lead score flow entered an unexpected pending state.'); + } + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/meeting-assistant-flow.ts b/examples/meeting-assistant-flow.ts new file mode 100644 index 0000000..b55e26b --- /dev/null +++ b/examples/meeting-assistant-flow.ts @@ -0,0 +1,152 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const taskSchema = z.object({ + title: z.string(), + owner: z.string(), +}); + +const extractionSchema = z.object({ + summary: z.string(), + tasks: z.array(taskSchema), +}); + +const fanOutSchema = z.object({ + trelloCardIds: z.array(z.string()), + csvPath: z.string(), + slackMessageId: z.string(), +}); + +export function createMeetingAssistantFlowExample(options: { + extractTasks?: (notes: string) => Promise>; + addTasksToTrello?: (tasks: z.infer[]) => Promise<{ trelloCardIds: string[] }>; + saveTasksToCsv?: (tasks: z.infer[]) => Promise<{ csvPath: string }>; + sendSlackNotification?: (args: { + summary: string; + tasks: z.infer[]; + }) => Promise<{ slackMessageId: string }>; +} = {}) { + const extractTasks = + options.extractTasks ?? + ((notes: string) => + generateExampleObject({ + schema: extractionSchema, + system: 'Extract a concise meeting summary and explicit action items.', + prompt: notes, + })); + + const addTasksToTrello = + options.addTasksToTrello ?? + (async (tasks) => ({ + trelloCardIds: tasks.map((_, index) => `card-${index + 1}`), + })); + + const saveTasksToCsv = + options.saveTasksToCsv ?? + (async () => ({ + csvPath: 'new_tasks.csv', + })); + + const sendSlackNotification = + options.sendSlackNotification ?? + (async () => ({ + slackMessageId: 'slack-message-1', + })); + + return createAgentMachine({ + id: 'meeting-assistant-flow-example', + schemas: { + input: z.object({ + notes: z.string(), + }), + output: z.object({ + summary: z.string().nullable(), + tasks: z.array(taskSchema), + trelloCardIds: z.array(z.string()), + csvPath: z.string().nullable(), + slackMessageId: z.string().nullable(), + }), + }, + context: (input) => ({ + notes: input.notes, + summary: null as string | null, + tasks: [] as z.infer[], + trelloCardIds: [] as string[], + csvPath: null as string | null, + slackMessageId: null as string | null, + }), + initial: 'extracting', + states: { + extracting: { + resultSchema: extractionSchema, + invoke: async ({ context }) => extractTasks(context.notes), + onDone: ({ result }) => ({ + target: 'dispatching', + context: { + summary: result.summary, + tasks: result.tasks, + }, + }), + }, + dispatching: { + resultSchema: fanOutSchema, + invoke: async ({ context }) => { + const [trello, csv, slack] = await Promise.all([ + addTasksToTrello(context.tasks), + saveTasksToCsv(context.tasks), + sendSlackNotification({ + summary: context.summary ?? '', + tasks: context.tasks, + }), + ]); + + return { + trelloCardIds: trello.trelloCardIds, + csvPath: csv.csvPath, + slackMessageId: slack.slackMessageId, + }; + }, + onDone: ({ result }) => ({ + target: 'done', + context: { + trelloCardIds: result.trelloCardIds, + csvPath: result.csvPath, + slackMessageId: result.slackMessageId, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + summary: context.summary, + tasks: context.tasks, + trelloCardIds: context.trelloCardIds, + csvPath: context.csvPath, + slackMessageId: context.slackMessageId, + }), + }, + }, + }); +} + +async function main() { + try { + const notes = await prompt('Meeting notes'); + const machine = createMeetingAssistantFlowExample(); + const result = await machine.execute(machine.getInitialState({ notes })); + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/raffle.ts b/examples/raffle.ts index b649860..f02b3a8 100644 --- a/examples/raffle.ts +++ b/examples/raffle.ts @@ -1,11 +1,15 @@ import { z } from 'zod'; -import { createAgentMachine } from '../src/index.js'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from '../src/index.js'; import { closePrompt, - formatResult, generateExampleObject, isMain, prompt, + waitForRunSnapshot, } from './_run.js'; const winnerSchema = z.object({ @@ -93,23 +97,28 @@ export function createRaffleExample( async function main() { try { const machine = createRaffleExample(); - let state = machine.getInitialState(); + const run = await startSession(machine, { + store: createMemoryRunStore(), + }); while (true) { - const result = await machine.execute(state); + const snapshot = await waitForRunSnapshot( + run, + (nextSnapshot) => nextSnapshot.status !== 'active' + ); - if (result.status === 'done') { - console.log(formatResult(result)); + if (snapshot.status === 'done') { + console.log({ + status: snapshot.status, + value: snapshot.value, + context: snapshot.context, + output: snapshot.output, + }); break; } - if (result.status !== 'pending') { - throw new Error('Raffle example entered an unexpected error state.'); - } - const entry = await prompt('Entry (blank to draw)'); - state = machine.transition( - result.state, + await run.send( entry ? { type: 'user.entry', entry } : { type: 'user.draw' } ); } diff --git a/examples/react-agent.ts b/examples/react-agent.ts index 0d15e45..8e20a84 100644 --- a/examples/react-agent.ts +++ b/examples/react-agent.ts @@ -10,6 +10,7 @@ import { generateExampleText, isMain, prompt, + waitForRunDone, } from './_run.js'; const reactModelResultSchema = z.discriminatedUnion('kind', [ @@ -87,15 +88,8 @@ async function main() { console.log(`${event.toolName} -> ${String(event.output)}`); }); - await new Promise((resolve, reject) => { - run.onDone((event) => { - console.log(event.output); - resolve(); - }); - run.onError((event) => { - reject(event.error); - }); - }); + const done = await waitForRunDone(run); + console.log(done.output); } finally { closePrompt(); } diff --git a/examples/self-evaluation-loop-flow.ts b/examples/self-evaluation-loop-flow.ts new file mode 100644 index 0000000..1827ddb --- /dev/null +++ b/examples/self-evaluation-loop-flow.ts @@ -0,0 +1,133 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + isMain, + prompt, +} from './_run.js'; + +const postSchema = z.object({ + post: z.string(), +}); + +const evaluationSchema = z.object({ + valid: z.boolean(), + feedback: z.string().nullable(), +}); + +export function createSelfEvaluationLoopFlowExample(options: { + generatePost?: (args: { + topic: string; + feedback: string | null; + attempt: number; + }) => Promise>; + evaluatePost?: (post: string) => Promise>; + maxAttempts?: number; +} = {}) { + const generatePost = + options.generatePost ?? + ((args: { topic: string; feedback: string | null; attempt: number }) => + generateExampleObject({ + schema: postSchema, + system: 'Write a playful X post in a Shakespearean tone with no emojis and under 280 characters.', + prompt: [ + `Topic: ${args.topic}`, + `Attempt: ${args.attempt}`, + args.feedback ? `Feedback to address: ${args.feedback}` : 'Feedback: none', + ].join('\n'), + })); + + const evaluatePost = + options.evaluatePost ?? + ((post: string) => + generateExampleObject({ + schema: evaluationSchema, + system: + 'Validate whether the X post is under 280 characters, uses no emojis, and stays playful. Return feedback only when it should be revised.', + prompt: post, + })); + + return createAgentMachine({ + id: 'self-evaluation-loop-flow-example', + schemas: { + input: z.object({ + topic: z.string(), + }), + output: z.object({ + post: z.string().nullable(), + valid: z.boolean(), + feedback: z.string().nullable(), + attempt: z.number(), + }), + }, + context: (input) => ({ + topic: input.topic, + post: null as string | null, + valid: false, + feedback: null as string | null, + attempt: 1, + maxAttempts: options.maxAttempts ?? 3, + }), + initial: 'generating', + states: { + generating: { + resultSchema: postSchema, + invoke: async ({ context }) => + generatePost({ + topic: context.topic, + feedback: context.feedback, + attempt: context.attempt, + }), + onDone: ({ result }) => ({ + target: 'evaluating', + context: { + post: result.post, + }, + }), + }, + evaluating: { + resultSchema: evaluationSchema, + invoke: async ({ context }) => evaluatePost(context.post ?? ''), + onDone: ({ result, context }) => ({ + target: + result.valid || context.attempt >= context.maxAttempts + ? 'done' + : 'generating', + context: { + valid: result.valid, + feedback: result.feedback, + attempt: result.valid + ? context.attempt + : context.attempt + 1, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + post: context.post, + valid: context.valid, + feedback: context.feedback, + attempt: context.attempt, + }), + }, + }, + }); +} + +async function main() { + try { + const topic = await prompt('Topic'); + const machine = createSelfEvaluationLoopFlowExample(); + const result = await machine.execute(machine.getInitialState({ topic })); + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/sql-agent.ts b/examples/sql-agent.ts index 14776b9..d48b0c6 100644 --- a/examples/sql-agent.ts +++ b/examples/sql-agent.ts @@ -13,6 +13,7 @@ import { generateExampleObject, isMain, prompt, + waitForRunDone, } from './_run.js'; const sqlValueSchema = z.union([z.string(), z.number(), z.null()]); @@ -260,15 +261,8 @@ async function main() { console.log(`${event.toolName} -> ${JSON.stringify(event.output)}`); }); - await new Promise((resolve, reject) => { - run.onDone((event) => { - console.log(event.output); - resolve(); - }); - run.onError((event) => { - reject(event.error); - }); - }); + const done = await waitForRunDone(run); + console.log(done.output); } finally { closePrompt(); } diff --git a/examples/tool-calling.ts b/examples/tool-calling.ts index 719fcfb..36e2d12 100644 --- a/examples/tool-calling.ts +++ b/examples/tool-calling.ts @@ -9,6 +9,7 @@ import { generateExampleObject, isMain, prompt, + waitForRunDone, } from './_run.js'; const forecastSchema = z.object({ @@ -128,15 +129,8 @@ async function main() { console.log(`${event.toolName} -> ${event.output.forecast}`); }); - await new Promise((resolve, reject) => { - run.onDone((event) => { - console.log(event.output); - resolve(); - }); - run.onError((event) => { - reject(event.error); - }); - }); + const done = await waitForRunDone(run); + console.log(done.output); } finally { closePrompt(); } diff --git a/examples/write-a-book-flow.ts b/examples/write-a-book-flow.ts new file mode 100644 index 0000000..d22742d --- /dev/null +++ b/examples/write-a-book-flow.ts @@ -0,0 +1,199 @@ +import { z } from 'zod'; +import { createAgentMachine } from '../src/index.js'; +import { + closePrompt, + formatResult, + generateExampleObject, + generateExampleText, + isMain, + prompt, +} from './_run.js'; + +const chapterOutlineSchema = z.object({ + title: z.string(), + brief: z.string(), +}); + +const outlineSchema = z.object({ + title: z.string(), + chapters: z.array(chapterOutlineSchema), +}); + +const chapterSchema = z.object({ + title: z.string(), + content: z.string(), +}); + +const chapterBatchSchema = z.object({ + chapters: z.array(chapterSchema), +}); + +const manuscriptSchema = z.object({ + manuscript: z.string(), +}); + +type ChapterOutline = z.infer; + +export function createWriteABookFlowExample(options: { + createOutline?: (args: { + topic: string; + goal: string; + }) => Promise>; + writeChapter?: (args: { + title: string; + brief: string; + goal: string; + topic: string; + }) => Promise>; + compileManuscript?: (args: { + title: string; + chapters: z.infer[]; + }) => Promise>; +} = {}) { + const createOutline = + options.createOutline ?? + ((args: { topic: string; goal: string }) => + generateExampleObject({ + schema: outlineSchema, + system: 'Create a concise non-fiction book outline.', + prompt: [`Topic: ${args.topic}`, `Goal: ${args.goal}`].join('\n'), + })); + + const writeChapter = + options.writeChapter ?? + ((args: { + title: string; + brief: string; + goal: string; + topic: string; + }) => + generateExampleObject({ + schema: chapterSchema, + system: 'Write a concise but coherent book chapter.', + prompt: [ + `Book topic: ${args.topic}`, + `Book goal: ${args.goal}`, + `Chapter title: ${args.title}`, + `Chapter brief: ${args.brief}`, + ].join('\n'), + })); + + const compileManuscript = + options.compileManuscript ?? + ((args: { title: string; chapters: z.infer[] }) => + generateExampleObject({ + schema: manuscriptSchema, + system: 'Compile chapters into a single clean markdown manuscript.', + prompt: [ + `Title: ${args.title}`, + '', + ...args.chapters.map( + (chapter) => `## ${chapter.title}\n\n${chapter.content}` + ), + ].join('\n'), + })); + + return createAgentMachine({ + id: 'write-a-book-flow-example', + schemas: { + input: z.object({ + topic: z.string(), + goal: z.string(), + }), + output: z.object({ + title: z.string().nullable(), + outline: z.array(chapterOutlineSchema), + chapters: z.array(chapterSchema), + manuscript: z.string().nullable(), + }), + }, + context: (input) => ({ + topic: input.topic, + goal: input.goal, + title: null as string | null, + outline: [] as ChapterOutline[], + chapters: [] as z.infer[], + manuscript: null as string | null, + }), + initial: 'outlining', + states: { + outlining: { + resultSchema: outlineSchema, + invoke: async ({ context }) => + createOutline({ + topic: context.topic, + goal: context.goal, + }), + onDone: ({ result }) => ({ + target: 'writing', + context: { + title: result.title, + outline: result.chapters, + }, + }), + }, + writing: { + resultSchema: chapterBatchSchema, + invoke: async ({ context }) => { + const chapters = await Promise.all( + context.outline.map((chapter) => + writeChapter({ + title: chapter.title, + brief: chapter.brief, + goal: context.goal, + topic: context.topic, + }) + ) + ); + + return { chapters }; + }, + onDone: ({ result }) => ({ + target: 'compiling', + context: { + chapters: result.chapters, + }, + }), + }, + compiling: { + resultSchema: manuscriptSchema, + invoke: async ({ context }) => + compileManuscript({ + title: context.title ?? 'Untitled Book', + chapters: context.chapters, + }), + onDone: ({ result }) => ({ + target: 'done', + context: { + manuscript: result.manuscript, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + title: context.title, + outline: context.outline, + chapters: context.chapters, + manuscript: context.manuscript, + }), + }, + }, + }); +} + +async function main() { + try { + const topic = await prompt('Book topic'); + const goal = await prompt('Book goal'); + const machine = createWriteABookFlowExample(); + const result = await machine.execute(machine.getInitialState({ topic, goal })); + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/readme.md b/readme.md index 9bc4e32..6e977ce 100644 --- a/readme.md +++ b/readme.md @@ -13,36 +13,24 @@ Stately Agent is a flexible framework for building AI agents using state machine The examples in [`examples/`](/Users/davidkpiano/Code/agent/examples) are intentionally small. Most run in the CLI and use real OpenAI calls when `OPENAI_API_KEY` is set. Runtime-specific examples call out extra environment requirements inline. +If you want examples grouped by intent instead of a flat list, start with [`examples/README.md`](/Users/davidkpiano/Code/agent/examples/README.md). It separates app-shaped examples, workflow examples, runtime integrations, and lower-level reference examples. + Run them with `node --import tsx examples/.ts`. Convert a machine file to diagram output with `pnpm agent:convert --format mermaid` or `pnpm agent:convert --format xstate`. Static analysis warnings are printed to stderr. For programmatic access, use `analyzeGraph(...)` from `@statelyai/agent/graph`; warnings are returned explicitly instead of being hidden in graph metadata. -Each example demonstrates one concept: - -- [`examples/simple.ts`](/Users/davidkpiano/Code/agent/examples/simple.ts): the smallest `createAgentMachine(...)` flow with an `invoke` state that makes a real LLM call -- [`examples/hitl.ts`](/Users/davidkpiano/Code/agent/examples/hitl.ts): a human-in-the-loop machine that pauses in a pending state, accepts typed events, and drafts with an LLM after approval -- [`examples/error-retry.ts`](/Users/davidkpiano/Code/agent/examples/error-retry.ts): retrying transient invoke failures through explicit internal error events -- [`examples/chatbot-messages.ts`](/Users/davidkpiano/Code/agent/examples/chatbot-messages.ts): message-centric chat state with structured `{ role, content }` accumulation across turns -- [`examples/conditional-subflow.ts`](/Users/davidkpiano/Code/agent/examples/conditional-subflow.ts): routing directly into one of multiple child subflows from parent input -- [`examples/rag.ts`](/Users/davidkpiano/Code/agent/examples/rag.ts): retrieval-augmented generation with explicit retrieve and answer states -- [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts): tool invocation with emitted `toolCall`, incremental `toolProgress`, and final `toolResult` events -- [`examples/ai-sdk.ts`](/Users/davidkpiano/Code/agent/examples/ai-sdk.ts): AI SDK v6 integration using `createAiSdkAdapter(...)` for routing and `generateText(..., { output: Output.object(...) })` for structured drafting -- [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts): persisting snapshots and restoring a session in plain runtime code -- [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts): runner-agnostic HTTP transport for starting sessions, sending events, and reading snapshots over `Request`/`Response` -- [`examples/http-streaming-session.ts`](/Users/davidkpiano/Code/agent/examples/http-streaming-session.ts): durable SSE transport that reconnects to a restored run and emits only new streaming parts -- [`examples/next-app-router.ts`](/Users/davidkpiano/Code/agent/examples/next-app-router.ts): Next.js App Router wrappers around the generic HTTP and SSE session transports, including `runtime`, `dynamic`, and `maxDuration` route config values -- [`examples/next-ai-sdk-ui.ts`](/Users/davidkpiano/Code/agent/examples/next-ai-sdk-ui.ts): a Next.js App Router chat route that accepts `UIMessage[]` and streams AI SDK UI message parts from machine-emitted notifications, sources, and text deltas -- [`examples/cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts): integrating a persisted agent-machine session store into Cloudflare Agents `onRequest()` routing; requires the Cloudflare Workers runtime -- [`examples/persistent-multi-agent-network.ts`](/Users/davidkpiano/Code/agent/examples/persistent-multi-agent-network.ts): durable multi-agent handoffs that restore from a persisted mid-network snapshot -- [`examples/persistent-streaming.ts`](/Users/davidkpiano/Code/agent/examples/persistent-streaming.ts): restore a crashed streaming invoke and continue with only new live emitted parts -- [`examples/persistent-supervisor.ts`](/Users/davidkpiano/Code/agent/examples/persistent-supervisor.ts): durable supervisor routing that restores after a persisted retry handoff -- [`examples/cloudflare-durable-network.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-durable-network.ts): a Cloudflare Durable Object runner that starts and resumes a persisted multi-agent network -- [`examples/decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts): choosing the next branch with structured branch-specific payloads -- [`examples/classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts): routing into a fixed category set when you only need a label -- [`examples/adapter.ts`](/Users/davidkpiano/Code/agent/examples/adapter.ts): plugging in a custom adapter that still uses a real OpenAI model under the hood +Start here: + +- App-shaped integrations: [`examples/apps/next/`](/Users/davidkpiano/Code/agent/examples/apps/next), [`examples/apps/cloudflare-agents/`](/Users/davidkpiano/Code/agent/examples/apps/cloudflare-agents), [`examples/next-ai-sdk-ui.ts`](/Users/davidkpiano/Code/agent/examples/next-ai-sdk-ui.ts), [`examples/cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts) +- Durable sessions and transports: [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts), [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts), [`examples/http-streaming-session.ts`](/Users/davidkpiano/Code/agent/examples/http-streaming-session.ts) +- Core workflow patterns: [`examples/rag.ts`](/Users/davidkpiano/Code/agent/examples/rag.ts), [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts), [`examples/error-retry.ts`](/Users/davidkpiano/Code/agent/examples/error-retry.ts), [`examples/persistent-supervisor.ts`](/Users/davidkpiano/Code/agent/examples/persistent-supervisor.ts) +- CrewAI-style equivalents: [`examples/content-creator-flow.ts`](/Users/davidkpiano/Code/agent/examples/content-creator-flow.ts), [`examples/email-auto-responder-flow.ts`](/Users/davidkpiano/Code/agent/examples/email-auto-responder-flow.ts), [`examples/lead-score-flow.ts`](/Users/davidkpiano/Code/agent/examples/lead-score-flow.ts), [`examples/meeting-assistant-flow.ts`](/Users/davidkpiano/Code/agent/examples/meeting-assistant-flow.ts), [`examples/self-evaluation-loop-flow.ts`](/Users/davidkpiano/Code/agent/examples/self-evaluation-loop-flow.ts), [`examples/write-a-book-flow.ts`](/Users/davidkpiano/Code/agent/examples/write-a-book-flow.ts) +- Reference examples: [`examples/simple.ts`](/Users/davidkpiano/Code/agent/examples/simple.ts), [`examples/decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts), [`examples/classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts), [`examples/adapter.ts`](/Users/davidkpiano/Code/agent/examples/adapter.ts) Use `classify(...)` when the result is just "what kind of thing is this?" Use `decide(...)` when the result is "what should happen next?" and the chosen branch may need structured data. +CrewAI Flow parity is tracked in [`docs/crewai-parity.md`](/Users/davidkpiano/Code/agent/docs/crewai-parity.md), the same way LangGraph parity is tracked separately. + ## Persistence Adapters diff --git a/src/crewai-equivalents/content-creator-flow.test.ts b/src/crewai-equivalents/content-creator-flow.test.ts new file mode 100644 index 0000000..fd4ab29 --- /dev/null +++ b/src/crewai-equivalents/content-creator-flow.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from 'vitest'; +import { createContentCreatorFlowExample } from '../../examples/index.js'; + +describe('CrewAI content creator flow equivalent', () => { + test('routes a request and generates specialized content', async () => { + const machine = createContentCreatorFlowExample({ + routeRequest: async () => ({ route: 'linkedin' }), + createLinkedInPost: async (request) => ({ + title: 'LinkedIn launch post', + body: `LinkedIn: ${request}`, + }), + }); + + const result = await machine.execute( + machine.getInitialState({ + request: 'Announce our AI workflow launch in a short professional post.', + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + route: 'linkedin', + title: 'LinkedIn launch post', + body: + 'LinkedIn: Announce our AI workflow launch in a short professional post.', + }); + } + }); +}); diff --git a/src/crewai-equivalents/email-auto-responder-flow.test.ts b/src/crewai-equivalents/email-auto-responder-flow.test.ts new file mode 100644 index 0000000..accd299 --- /dev/null +++ b/src/crewai-equivalents/email-auto-responder-flow.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, test } from 'vitest'; +import { runEmailAutoResponderFlowExample } from '../../examples/index.js'; + +describe('CrewAI email auto responder flow equivalent', () => { + test('processes new emails and restores the same durable snapshot', async () => { + const result = await runEmailAutoResponderFlowExample( + [ + { + id: 'email-1', + sender: 'buyer@example.com', + subject: 'Pricing question', + body: 'Can you send pricing details?', + }, + { + id: 'email-2', + sender: 'founder@example.com', + subject: 'Partnership', + body: 'Interested in discussing a partnership.', + }, + ], + { + createDraft: async (email) => ({ + draft: `Draft for ${email.subject}`, + }), + } + ); + + expect(result.snapshot).toEqual(result.restoredSnapshot); + expect(result.snapshot).toEqual( + expect.objectContaining({ + value: 'waiting', + status: 'pending', + }) + ); + expect(result.snapshot.context.processedIds).toEqual(['email-1', 'email-2']); + expect(result.snapshot.context.drafts).toEqual({ + 'email-1': 'Draft for Pricing question', + 'email-2': 'Draft for Partnership', + }); + }); +}); diff --git a/src/crewai-equivalents/lead-score-flow.test.ts b/src/crewai-equivalents/lead-score-flow.test.ts new file mode 100644 index 0000000..8462101 --- /dev/null +++ b/src/crewai-equivalents/lead-score-flow.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from 'vitest'; +import { createLeadScoreFlowExample } from '../../examples/index.js'; + +describe('CrewAI lead score flow equivalent', () => { + test('supports human review before generating outreach emails', async () => { + const machine = createLeadScoreFlowExample({ + scoreLeads: async ({ leads, reviewNote }) => ({ + scoredLeads: leads.map((lead, index) => ({ + ...lead, + score: 100 - index * 10 - (reviewNote ? 3 : 0), + rationale: reviewNote ?? 'initial', + })), + }), + writeEmails: async (leads) => ({ + drafts: leads.map((lead) => ({ + leadId: lead.id, + draft: `Email for ${lead.company}`, + })), + }), + }); + + const initial = machine.getInitialState({ + leads: [ + { id: 'lead-1', company: 'Acme', contact: 'Ana' }, + { id: 'lead-2', company: 'Beta', contact: 'Ben' }, + { id: 'lead-3', company: 'Gamma', contact: 'Gia' }, + ], + }); + const firstPass = await machine.execute(initial); + expect(firstPass.status).toBe('pending'); + if (firstPass.status !== 'pending') { + return; + } + + const rescored = machine.transition(firstPass.state, { + type: 'review.requestChanges', + note: 'Prefer companies already asking for demos.', + }); + const secondPass = await machine.execute(rescored); + expect(secondPass.status).toBe('pending'); + if (secondPass.status !== 'pending') { + return; + } + + const approved = machine.transition(secondPass.state, { + type: 'review.approve', + }); + const finalResult = await machine.execute(approved); + + expect(finalResult.status).toBe('done'); + if (finalResult.status === 'done') { + expect(finalResult.output.reviewCount).toBe(2); + expect(finalResult.output.topLeads).toHaveLength(3); + expect(finalResult.output.emailDrafts).toEqual([ + { leadId: 'lead-1', draft: 'Email for Acme' }, + { leadId: 'lead-2', draft: 'Email for Beta' }, + { leadId: 'lead-3', draft: 'Email for Gamma' }, + ]); + } + }); +}); diff --git a/src/crewai-equivalents/meeting-assistant-flow.test.ts b/src/crewai-equivalents/meeting-assistant-flow.test.ts new file mode 100644 index 0000000..c9a87f6 --- /dev/null +++ b/src/crewai-equivalents/meeting-assistant-flow.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, test } from 'vitest'; +import { createMeetingAssistantFlowExample } from '../../examples/index.js'; + +describe('CrewAI meeting assistant flow equivalent', () => { + test('fans one meeting summary into multiple side effects', async () => { + const machine = createMeetingAssistantFlowExample({ + extractTasks: async () => ({ + summary: 'Agreed on launch scope and follow-ups.', + tasks: [ + { title: 'Send launch checklist', owner: 'Ana' }, + { title: 'Prepare customer email', owner: 'Ben' }, + ], + }), + addTasksToTrello: async (tasks) => ({ + trelloCardIds: tasks.map((_, index) => `card-${index + 1}`), + }), + saveTasksToCsv: async () => ({ csvPath: 'new_tasks.csv' }), + sendSlackNotification: async () => ({ slackMessageId: 'slack-123' }), + }); + + const result = await machine.execute( + machine.getInitialState({ + notes: 'Meeting notes go here.', + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output).toEqual({ + summary: 'Agreed on launch scope and follow-ups.', + tasks: [ + { title: 'Send launch checklist', owner: 'Ana' }, + { title: 'Prepare customer email', owner: 'Ben' }, + ], + trelloCardIds: ['card-1', 'card-2'], + csvPath: 'new_tasks.csv', + slackMessageId: 'slack-123', + }); + } + }); +}); diff --git a/src/crewai-equivalents/self-evaluation-loop-flow.test.ts b/src/crewai-equivalents/self-evaluation-loop-flow.test.ts new file mode 100644 index 0000000..4bbb51d --- /dev/null +++ b/src/crewai-equivalents/self-evaluation-loop-flow.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from 'vitest'; +import { createSelfEvaluationLoopFlowExample } from '../../examples/index.js'; + +describe('CrewAI self evaluation loop equivalent', () => { + test('iterates until the generated post passes evaluation', async () => { + const attempts: string[] = []; + const machine = createSelfEvaluationLoopFlowExample({ + generatePost: async ({ feedback, attempt }) => { + const post = + attempt === 1 + ? 'A very long post with too much detail and maybe an emoji :)' + : `Refined post after: ${feedback}`; + attempts.push(post); + return { post }; + }, + evaluatePost: async (post) => + post.includes('Refined') + ? { valid: true, feedback: null } + : { + valid: false, + feedback: 'Shorten it and remove emoji-like punctuation.', + }, + }); + + const result = await machine.execute( + machine.getInitialState({ + topic: 'Flying cars', + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output.valid).toBe(true); + expect(result.output.attempt).toBe(2); + expect(attempts).toHaveLength(2); + expect(result.output.post).toContain('Refined post after'); + } + }); +}); diff --git a/src/crewai-equivalents/write-a-book-flow.test.ts b/src/crewai-equivalents/write-a-book-flow.test.ts new file mode 100644 index 0000000..6ee2c5c --- /dev/null +++ b/src/crewai-equivalents/write-a-book-flow.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from 'vitest'; +import { createWriteABookFlowExample } from '../../examples/index.js'; + +describe('CrewAI write a book flow equivalent', () => { + test('outlines a book, writes chapters in parallel, and compiles a manuscript', async () => { + const machine = createWriteABookFlowExample({ + createOutline: async () => ({ + title: 'The Workflow Book', + chapters: [ + { title: 'Chapter 1', brief: 'Introduction' }, + { title: 'Chapter 2', brief: 'Execution' }, + ], + }), + writeChapter: async ({ title, brief }) => ({ + title, + content: `${title}: ${brief}`, + }), + compileManuscript: async ({ title, chapters }) => ({ + manuscript: [ + `# ${title}`, + ...chapters.map((chapter) => `## ${chapter.title}\n${chapter.content}`), + ].join('\n\n'), + }), + }); + + const result = await machine.execute( + machine.getInitialState({ + topic: 'Workflow systems', + goal: 'Teach developers how to build durable AI workflows.', + }) + ); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.output.title).toBe('The Workflow Book'); + expect(result.output.outline).toHaveLength(2); + expect(result.output.chapters).toEqual([ + { title: 'Chapter 1', content: 'Chapter 1: Introduction' }, + { title: 'Chapter 2', content: 'Chapter 2: Execution' }, + ]); + expect(result.output.manuscript).toContain('# The Workflow Book'); + } + }); +}); diff --git a/src/examples.test.ts b/src/examples.test.ts index cef587c..fc1efd7 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -593,6 +593,64 @@ describe('curated examples', () => { }); }); + test('next app-shaped route files import cleanly', async () => { + const [ + routesModule, + chatRouteModule, + reviewSessionsRouteModule, + reviewSessionRouteModule, + reviewEventsRouteModule, + streamingSessionsRouteModule, + streamingSessionRouteModule, + streamingStreamRouteModule, + ] = await Promise.all([ + import(new URL('../examples/apps/next/lib/routes.ts', import.meta.url).href), + import(new URL('../examples/apps/next/app/api/chat/route.ts', import.meta.url).href), + import( + new URL('../examples/apps/next/app/api/review-sessions/route.ts', import.meta.url).href + ), + import( + new URL( + '../examples/apps/next/app/api/review-sessions/[sessionId]/route.ts', + import.meta.url + ).href + ), + import( + new URL( + '../examples/apps/next/app/api/review-sessions/[sessionId]/events/route.ts', + import.meta.url + ).href + ), + import( + new URL('../examples/apps/next/app/api/stream-sessions/route.ts', import.meta.url).href + ), + import( + new URL( + '../examples/apps/next/app/api/stream-sessions/[sessionId]/route.ts', + import.meta.url + ).href + ), + import( + new URL( + '../examples/apps/next/app/api/stream-sessions/[sessionId]/stream/route.ts', + import.meta.url + ).href + ), + ]); + + expect(routesModule.runtime).toBe('nodejs'); + expect(typeof routesModule.chatRoute.POST).toBe('function'); + expect(typeof routesModule.reviewRoutes.sessions.POST).toBe('function'); + expect(typeof routesModule.streamingRoutes.stream.GET).toBe('function'); + expect(typeof chatRouteModule.POST).toBe('function'); + expect(typeof reviewSessionsRouteModule.POST).toBe('function'); + expect(typeof reviewSessionRouteModule.GET).toBe('function'); + expect(typeof reviewEventsRouteModule.POST).toBe('function'); + expect(typeof streamingSessionsRouteModule.POST).toBe('function'); + expect(typeof streamingSessionRouteModule.GET).toBe('function'); + expect(typeof streamingStreamRouteModule.GET).toBe('function'); + }); + test('next ai sdk ui route streams UI message parts from machine emissions', async () => { const route = createNextAiSdkUiRoute({ streamReply: async ({ messages, onDelta }) => { From 4bde82c2a8ca17a3ce2c4dcc7dd2c4beb2778fb4 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 5 May 2026 11:04:05 -0400 Subject: [PATCH 32/34] feat: add runtime adapter subpaths --- examples/README.md | 6 +- examples/_run.ts | 78 +------ .../src/review-workflow-agent.ts | 2 +- examples/cloudflare-agents.ts | 79 +------ examples/cloudflare-durable-object.ts | 87 +------- examples/http-session.ts | 61 +---- examples/http-streaming-session.ts | 132 +---------- examples/next-app-router.ts | 19 +- package.json | 40 ++++ readme.md | 21 +- src/cloudflare/index.test.ts | 88 ++++++++ src/cloudflare/index.ts | 147 ++++++++++++ src/http/index.test.ts | 157 +++++++++++++ src/http/index.ts | 209 ++++++++++++++++++ src/index.ts | 1 + src/next/index.test.ts | 89 ++++++++ src/next/index.ts | 81 +++++++ src/runtime/index.test.ts | 64 ++++++ src/runtime/index.ts | 80 +++++++ tsdown.config.ts | 4 + 20 files changed, 1019 insertions(+), 426 deletions(-) create mode 100644 src/cloudflare/index.test.ts create mode 100644 src/cloudflare/index.ts create mode 100644 src/http/index.test.ts create mode 100644 src/http/index.ts create mode 100644 src/next/index.test.ts create mode 100644 src/next/index.ts create mode 100644 src/runtime/index.test.ts create mode 100644 src/runtime/index.ts diff --git a/examples/README.md b/examples/README.md index da9b6bb..ed4b3f3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,8 +18,8 @@ These are the best starting points when you want code that already looks like a - [`apps/next/`](/Users/davidkpiano/Code/agent/examples/apps/next): copy-paste Next.js App Router routes - [`apps/cloudflare-agents/`](/Users/davidkpiano/Code/agent/examples/apps/cloudflare-agents): copy-paste Cloudflare Agents Worker layout - [`next-ai-sdk-ui.ts`](/Users/davidkpiano/Code/agent/examples/next-ai-sdk-ui.ts): AI SDK UI route helper -- [`next-app-router.ts`](/Users/davidkpiano/Code/agent/examples/next-app-router.ts): App Router session route helpers -- [`cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts): Node-safe Cloudflare Agents helper version +- [`next-app-router.ts`](/Users/davidkpiano/Code/agent/examples/next-app-router.ts): App Router session routes backed by `@statelyai/agent/next` and `@statelyai/agent/http` +- [`cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts): Node-safe Cloudflare Agents example backed by `@statelyai/agent/cloudflare` ## Workflow Examples @@ -52,6 +52,8 @@ These focus on real orchestration patterns: - [`cloudflare-durable-object.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-durable-object.ts) - [`cloudflare-durable-network.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-durable-network.ts) +The reusable pieces behind these examples are exported from `@statelyai/agent/http`, `@statelyai/agent/next`, and `@statelyai/agent/cloudflare`. + ## Reference / Concept Examples These are smaller building-block examples: diff --git a/examples/_run.ts b/examples/_run.ts index 5e62dc1..d3abce0 100644 --- a/examples/_run.ts +++ b/examples/_run.ts @@ -8,11 +8,10 @@ import { pathToFileURL } from 'node:url'; import { z } from 'zod'; import type { AgentAdapter, - AgentRun, - AgentSnapshot, ExecuteResult, StandardSchemaV1, } from '../src/index.js'; +export { waitForRunDone, waitForRunSnapshot } from '../src/runtime/index.js'; export function isMain(moduleUrl: string): boolean { const entry = process.argv[1]; @@ -97,81 +96,6 @@ export function formatResult(result: ExecuteResult) { }; } -export function waitForRunDone< - TContext extends Record, - TValue extends string, - TEvents extends Record, - TOutput, - TEmitted extends Record, ->( - run: AgentRun -): Promise<{ - output: TOutput; - snapshot: AgentSnapshot; -}> { - return new Promise((resolve, reject) => { - const offDone = run.onDone((event) => { - offDone(); - offError(); - resolve(event); - }); - const offError = run.onError((event) => { - offDone(); - offError(); - reject(event.error); - }); - }); -} - -export function waitForRunSnapshot< - TContext extends Record, - TValue extends string, - TEvents extends Record, - TOutput, - TEmitted extends Record, ->( - run: AgentRun, - predicate: ( - snapshot: AgentSnapshot - ) => boolean, - timeoutMs = 1000 -): Promise> { - const current = run.getSnapshot(); - if (predicate(current)) { - return Promise.resolve(current); - } - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - cleanup(); - reject(new Error('Run snapshot did not reach the expected state in time.')); - }, timeoutMs); - - const cleanup = () => { - clearTimeout(timeout); - offSnapshot(); - offDone(); - offError(); - }; - - const check = (snapshot: AgentSnapshot) => { - if (predicate(snapshot)) { - cleanup(); - resolve(snapshot); - } - }; - - const offSnapshot = run.onSnapshot(check); - const offDone = run.onDone((event) => { - check(event.snapshot); - }); - const offError = run.onError((event) => { - cleanup(); - reject(event.error); - }); - }); -} - export function createOpenAiDecisionAdapter(): AgentAdapter { return { async decide({ model, prompt, options, reasoning }) { diff --git a/examples/apps/cloudflare-agents/src/review-workflow-agent.ts b/examples/apps/cloudflare-agents/src/review-workflow-agent.ts index 5fe1ec7..d64d72c 100644 --- a/examples/apps/cloudflare-agents/src/review-workflow-agent.ts +++ b/examples/apps/cloudflare-agents/src/review-workflow-agent.ts @@ -3,7 +3,7 @@ import { restoreSession, startSession, type RunStore } from '../../../../src/ind import { createCloudflareAgentRunStore, type CloudflareAgentRunStoreState, -} from '../../../cloudflare-agents.js'; +} from '../../../../src/cloudflare/index.js'; import { createPersistenceExample } from '../../../persistence.js'; export class ReviewWorkflowAgent extends Agent< diff --git a/examples/cloudflare-agents.ts b/examples/cloudflare-agents.ts index 8994231..2b6cbdc 100644 --- a/examples/cloudflare-agents.ts +++ b/examples/cloudflare-agents.ts @@ -1,19 +1,17 @@ import { restoreSession, startSession, - type JournalEventRecord, - type PersistedSnapshot, type RunStore, } from '../src/index.js'; +import { + createCloudflareAgentRunStore, + type CloudflareAgentRunStoreState, +} from '../src/cloudflare/index.js'; import { createPersistenceExample } from './persistence.js'; -type SessionEntry = { - events: JournalEventRecord[]; - snapshot: PersistedSnapshot | null; -}; - -export type CloudflareAgentRunStoreState = { - sessions: Record; +export { + createCloudflareAgentRunStore, + type CloudflareAgentRunStoreState, }; export interface CloudflareAgentsExampleArtifacts { @@ -25,69 +23,6 @@ export interface CloudflareAgentsExampleArtifacts { }; } -export function createCloudflareAgentRunStore(options: { - getState: () => CloudflareAgentRunStoreState; - setState: ( - nextState: CloudflareAgentRunStoreState - ) => void | Promise; -}): RunStore { - return { - async append(sessionId, event) { - const currentState = options.getState(); - const currentSession = currentState.sessions[sessionId] ?? { - events: [], - snapshot: null, - }; - const sequence = currentSession.events.length + 1; - const nextSession: SessionEntry = { - ...currentSession, - events: [...currentSession.events, { ...event, sequence }], - }; - - await options.setState({ - ...currentState, - sessions: { - ...currentState.sessions, - [sessionId]: nextSession, - }, - }); - - return { sequence }; - }, - - async loadEvents(sessionId, afterSequence = 0) { - return ( - options.getState().sessions[sessionId]?.events.filter( - (event) => event.sequence > afterSequence - ) ?? [] - ); - }, - - async loadLatestSnapshot(sessionId) { - return options.getState().sessions[sessionId]?.snapshot ?? null; - }, - - async saveSnapshot(snapshot) { - const currentState = options.getState(); - const currentSession = currentState.sessions[snapshot.sessionId] ?? { - events: [], - snapshot: null, - }; - - await options.setState({ - ...currentState, - sessions: { - ...currentState.sessions, - [snapshot.sessionId]: { - ...currentSession, - snapshot, - }, - }, - }); - }, - }; -} - /** * Cloudflare's `agents` package imports `cloudflare:` modules, so this example * keeps that import lazy to stay loadable in plain Node. In a real Worker, diff --git a/examples/cloudflare-durable-object.ts b/examples/cloudflare-durable-object.ts index 5ff0527..23a040b 100644 --- a/examples/cloudflare-durable-object.ts +++ b/examples/cloudflare-durable-object.ts @@ -1,75 +1,20 @@ -import { - createPersistenceExample, -} from './persistence.js'; import { restoreSession, startSession, - type AgentSnapshot, - type JournalEvent, - type JournalEventRecord, - type PersistedSnapshot, type RunStore, } from '../src/index.js'; - -export interface DurableObjectStorageLike { - get(key: string): Promise; - put(key: string, value: T): Promise; -} - -export interface DurableObjectStateLike { - storage: DurableObjectStorageLike; -} - -export function createDurableObjectRunStore( - storage: DurableObjectStorageLike -): RunStore { - return { - async append(sessionId, event) { - const key = journalKey(sessionId); - const current = (await storage.get(key)) ?? []; - const sequence = - current.length === 0 - ? 1 - : current[current.length - 1]!.sequence + 1; - - await storage.put(key, [...current, { ...event, sequence }]); - return { sequence }; - }, - - async loadEvents(sessionId, afterSequence = 0) { - const current = - (await storage.get[]>( - journalKey(sessionId) - )) ?? []; - - return current - .filter((event) => event.sequence > afterSequence) - .sort((a, b) => a.sequence - b.sequence); - }, - - async loadLatestSnapshot(sessionId) { - const snapshots = - (await storage.get[]>( - snapshotsKey(sessionId) - )) ?? []; - - return ( - [...snapshots].sort( - (a, b) => - a.afterSequence - b.afterSequence || a.createdAt - b.createdAt - ).at(-1) ?? null - ); - }, - - async saveSnapshot(snapshot) { - const key = snapshotsKey(snapshot.sessionId); - const current = - (await storage.get[]>(key)) ?? []; - - await storage.put(key, [...current, snapshot]); - }, - }; -} +import { + createDurableObjectRunStore, + type DurableObjectStateLike, + type DurableObjectStorageLike, +} from '../src/cloudflare/index.js'; +import { createPersistenceExample } from './persistence.js'; + +export { + createDurableObjectRunStore, + type DurableObjectStateLike, + type DurableObjectStorageLike, +}; export class AgentSessionDurableObject { private readonly store: RunStore; @@ -137,11 +82,3 @@ function requiredSessionId(url: URL): string { return sessionId; } - -function journalKey(sessionId: string): string { - return `sessions/${sessionId}/journal`; -} - -function snapshotsKey(sessionId: string): string { - return `sessions/${sessionId}/snapshots`; -} diff --git a/examples/http-session.ts b/examples/http-session.ts index c355654..f765193 100644 --- a/examples/http-session.ts +++ b/examples/http-session.ts @@ -1,9 +1,5 @@ -import { - createMemoryRunStore, - restoreSession, - startSession, - type RunStore, -} from '../src/index.js'; +import { createSessionHttpHandler } from '../src/http/index.js'; +import { type RunStore } from '../src/index.js'; import { createPersistenceExample } from './persistence.js'; export interface SessionHttpHandlerOptions { @@ -14,55 +10,8 @@ export interface SessionHttpHandlerOptions { export function createPersistenceSessionHttpHandler( options: SessionHttpHandlerOptions = {} ) { - const store = options.store ?? createMemoryRunStore(); const machine = createPersistenceExample(options.summarize); - - return async function handle(request: Request): Promise { - const url = new URL(request.url); - const match = url.pathname.match(/^\/sessions(?:\/([^/]+)(?:\/events)?)?$/); - const sessionId = match?.[1]; - const isEventRoute = url.pathname.endsWith('/events'); - - if (request.method === 'POST' && url.pathname === '/sessions') { - const body = await request.json() as { request: string }; - const run = await startSession(machine, { - store, - input: { request: body.request }, - }); - - return Response.json({ - sessionId: run.sessionId, - snapshot: run.getSnapshot(), - }); - } - - if (request.method === 'GET' && sessionId && !isEventRoute) { - const run = await restoreSession(machine, { - sessionId, - store, - }); - - return Response.json({ - sessionId, - snapshot: run.getSnapshot(), - }); - } - - if (request.method === 'POST' && sessionId && isEventRoute) { - const event = await request.json() as { type: 'approve' }; - const run = await restoreSession(machine, { - sessionId, - store, - }); - - await run.send(event); - - return Response.json({ - sessionId, - snapshot: run.getSnapshot(), - }); - } - - return new Response('Not found', { status: 404 }); - }; + return createSessionHttpHandler(machine, { + store: options.store, + }); } diff --git a/examples/http-streaming-session.ts b/examples/http-streaming-session.ts index 9e6185c..736ba97 100644 --- a/examples/http-streaming-session.ts +++ b/examples/http-streaming-session.ts @@ -1,10 +1,8 @@ import { z } from 'zod'; +import { createSessionHttpController } from '../src/http/index.js'; import { createAgentMachine, createMemoryRunStore, - restoreSession, - startSession, - type AgentRun, type RunStore, } from '../src/index.js'; @@ -21,14 +19,6 @@ const textPartSchema = z.object({ delta: z.string(), }); -type StreamingRun = AgentRun< - { streamId: string; text: string; finalText: string }, - string, - {}, - { text: string }, - { textPart: typeof textPartSchema } ->; - export interface StreamingSessionHttpController { handle(request: Request): Promise; advance(streamId: string): void; @@ -75,32 +65,7 @@ export function createStreamingSessionHttpController(options: { }, }, }); - const activeRuns = new Map(); - - function trackRun(sessionId: string, run: StreamingRun) { - activeRuns.set(sessionId, run); - run.onDone(() => { - activeRuns.delete(sessionId); - }); - run.onError(() => { - activeRuns.delete(sessionId); - }); - return run; - } - - async function getRun(sessionId: string): Promise { - const existing = activeRuns.get(sessionId); - if (existing) { - return existing; - } - - const restored = await restoreSession(machine, { - sessionId, - store, - }) as StreamingRun; - - return trackRun(sessionId, restored); - } + const controller = createSessionHttpController(machine, { store }); return { advance(streamId) { @@ -108,100 +73,11 @@ export function createStreamingSessionHttpController(options: { }, dropActiveSession(sessionId) { - activeRuns.delete(sessionId); + controller.dropActiveSession(sessionId); }, async handle(request) { - const url = new URL(request.url); - const match = url.pathname.match(/^\/sessions\/([^/]+)(?:\/stream)?$/); - const sessionId = match?.[1]; - const isStreamRoute = url.pathname.endsWith('/stream'); - - if (request.method === 'POST' && url.pathname === '/sessions') { - const body = await request.json() as z.infer; - const run = await startSession(machine, { - store, - input: body, - }) as StreamingRun; - trackRun(run.sessionId, run); - - return Response.json({ - sessionId: run.sessionId, - snapshot: run.getSnapshot(), - }); - } - - if (request.method === 'GET' && sessionId && !isStreamRoute) { - const run = await getRun(sessionId); - return Response.json({ - sessionId, - snapshot: run.getSnapshot(), - }); - } - - if (request.method === 'GET' && sessionId && isStreamRoute) { - const run = await getRun(sessionId); - let cleanup = () => {}; - - const stream = new ReadableStream({ - start(controller) { - const encoder = new TextEncoder(); - const write = (event: string, data: unknown) => { - controller.enqueue( - encoder.encode( - `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` - ) - ); - }; - - if (run.getSnapshot().status === 'done') { - write('done', run.getSnapshot().output); - controller.close(); - return; - } - - if (run.getSnapshot().status === 'error') { - write('error', { error: String(run.getSnapshot().error) }); - controller.close(); - return; - } - - const offPart = run.on('textPart', (event) => { - write('textPart', event); - }); - const offDone = run.onDone((event) => { - write('done', event.output); - cleanup(); - controller.close(); - }); - const offError = run.onError((event) => { - write('error', { error: String(event.error) }); - cleanup(); - controller.close(); - }); - - cleanup = () => { - offPart(); - offDone(); - offError(); - }; - }, - cancel() { - // Subscribers are ephemeral transport clients, not run ownership. - // Closing the stream should detach listeners but leave the run alive. - cleanup(); - }, - }); - - return new Response(stream, { - headers: { - 'content-type': 'text/event-stream', - 'cache-control': 'no-cache', - }, - }); - } - - return new Response('Not found', { status: 404 }); + return controller.handle(request); }, }; } diff --git a/examples/next-app-router.ts b/examples/next-app-router.ts index c80fa98..5a9e826 100644 --- a/examples/next-app-router.ts +++ b/examples/next-app-router.ts @@ -1,4 +1,11 @@ import { type RunStore } from '../src/index.js'; +import type { NextRouteContext } from '../src/next/index.js'; +export { + dynamic, + maxDuration, + runtime, +} from '../src/next/index.js'; +export type { NextRouteContext } from '../src/next/index.js'; import { createPersistenceSessionHttpHandler, type SessionHttpHandlerOptions, @@ -8,18 +15,6 @@ import { type StreamingSessionHttpController, } from './http-streaming-session.js'; -/** - * Suggested route-segment config for Next.js App Router route handlers that - * host long-lived agent sessions and streaming responses. - */ -export const runtime = 'nodejs'; -export const dynamic = 'force-dynamic'; -export const maxDuration = 30; - -export interface NextRouteContext> { - params: Promise | TParams; -} - export interface NextReviewRouteHandlers { sessions: { POST(request: Request): Promise; diff --git a/package.json b/package.json index 42e66f7..4457cd0 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,16 @@ "default": "./dist/ai-sdk.cjs" } }, + "./cloudflare": { + "import": { + "types": "./dist/cloudflare.d.mts", + "default": "./dist/cloudflare.mjs" + }, + "require": { + "types": "./dist/cloudflare.d.cts", + "default": "./dist/cloudflare.cjs" + } + }, "./graph": { "import": { "types": "./dist/graph.d.mts", @@ -37,6 +47,36 @@ "default": "./dist/graph.cjs" } }, + "./http": { + "import": { + "types": "./dist/http.d.mts", + "default": "./dist/http.mjs" + }, + "require": { + "types": "./dist/http.d.cts", + "default": "./dist/http.cjs" + } + }, + "./next": { + "import": { + "types": "./dist/next.d.mts", + "default": "./dist/next.mjs" + }, + "require": { + "types": "./dist/next.d.cts", + "default": "./dist/next.cjs" + } + }, + "./runtime": { + "import": { + "types": "./dist/runtime.d.mts", + "default": "./dist/runtime.mjs" + }, + "require": { + "types": "./dist/runtime.d.cts", + "default": "./dist/runtime.cjs" + } + }, "./xstate": { "import": { "types": "./dist/xstate.d.mts", diff --git a/readme.md b/readme.md index 6e977ce..e597b31 100644 --- a/readme.md +++ b/readme.md @@ -31,6 +31,21 @@ Use `classify(...)` when the result is just "what kind of thing is this?" Use `d CrewAI Flow parity is tracked in [`docs/crewai-parity.md`](/Users/davidkpiano/Code/agent/docs/crewai-parity.md), the same way LangGraph parity is tracked separately. +## Runtime Adapters + + + +The core package exports session helpers from `@statelyai/agent` and `@statelyai/agent/runtime`: + +- `waitForRunDone(run)`: await terminal success or reject on session error +- `waitForRunSnapshot(run, predicate)`: await the next snapshot that matches a predicate + +Use the framework adapters when a machine needs to run inside an app runtime: + +- `@statelyai/agent/http`: `createSessionHttpController(...)`, `createSessionHttpHandler(...)`, and `createRunSseResponse(...)` +- `@statelyai/agent/next`: `createNextSessionRouteHandlers(...)` plus App Router config exports +- `@statelyai/agent/cloudflare`: `createDurableObjectRunStore(...)` and `createCloudflareAgentRunStore(...)` + ## Persistence Adapters @@ -45,8 +60,8 @@ Storage adapters are intentionally bring-your-own. Implement the `RunStore` cont Use these examples as templates for your storage layer: - [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts): the smallest durable session flow with an in-memory store -- [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts): the Request/Response transport shape around a `RunStore` -- [`examples/cloudflare-durable-object.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-durable-object.ts): Durable Object-backed event and snapshot persistence -- [`examples/cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts): syncing a `RunStore` into Cloudflare Agents state +- [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts): the Request/Response transport shape around `@statelyai/agent/http` +- [`examples/cloudflare-durable-object.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-durable-object.ts): Durable Object-backed event and snapshot persistence with `@statelyai/agent/cloudflare` +- [`examples/cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts): syncing a `RunStore` into Cloudflare Agents state with `@statelyai/agent/cloudflare` **Read the documentation: [stately.ai/docs/agents](https://stately.ai/docs/agents)** diff --git a/src/cloudflare/index.test.ts b/src/cloudflare/index.test.ts new file mode 100644 index 0000000..67f0010 --- /dev/null +++ b/src/cloudflare/index.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from 'vitest'; +import { + createCloudflareAgentRunStore, + createDurableObjectRunStore, + type CloudflareAgentRunStoreState, +} from './index.js'; + +describe('cloudflare adapter', () => { + test('creates a Durable Object RunStore', async () => { + const storage = new Map(); + const store = createDurableObjectRunStore({ + async get(key) { + return storage.get(key) as never; + }, + async put(key, value) { + storage.set(key, value); + }, + }); + + await store.append('session-1', { type: 'start', at: 1 }); + await store.saveSnapshot({ + sessionId: 'session-1', + afterSequence: 1, + createdAt: 2, + snapshot: { + sessionId: 'session-1', + createdAt: 2, + value: 'done', + status: 'done', + context: {}, + input: {}, + }, + }); + + await expect(store.loadEvents('session-1')).resolves.toEqual([ + { + type: 'start', + at: 1, + sequence: 1, + }, + ]); + await expect(store.loadLatestSnapshot('session-1')).resolves.toEqual( + expect.objectContaining({ + afterSequence: 1, + }) + ); + }); + + test('creates a Cloudflare Agents state-backed RunStore', async () => { + let state: CloudflareAgentRunStoreState = { + sessions: {}, + }; + const store = createCloudflareAgentRunStore({ + getState: () => state, + setState: (nextState) => { + state = nextState; + }, + }); + + await store.append('session-1', { type: 'approve', at: 1 }); + await store.saveSnapshot({ + sessionId: 'session-1', + afterSequence: 1, + createdAt: 2, + snapshot: { + sessionId: 'session-1', + createdAt: 2, + value: 'done', + status: 'done', + context: {}, + input: {}, + }, + }); + + await expect(store.loadEvents('session-1')).resolves.toEqual([ + { + type: 'approve', + at: 1, + sequence: 1, + }, + ]); + await expect(store.loadLatestSnapshot('session-1')).resolves.toEqual( + expect.objectContaining({ + afterSequence: 1, + }) + ); + }); +}); diff --git a/src/cloudflare/index.ts b/src/cloudflare/index.ts new file mode 100644 index 0000000..008f0bc --- /dev/null +++ b/src/cloudflare/index.ts @@ -0,0 +1,147 @@ +import type { + AgentSnapshot, + JournalEvent, + JournalEventRecord, + PersistedSnapshot, + RunStore, +} from '../types.js'; + +export interface DurableObjectStorageLike { + get(key: string): Promise; + put(key: string, value: T): Promise; +} + +export interface DurableObjectStateLike { + storage: DurableObjectStorageLike; +} + +export function createDurableObjectRunStore( + storage: DurableObjectStorageLike +): RunStore { + return { + async append(sessionId, event) { + const key = journalKey(sessionId); + const current = (await storage.get(key)) ?? []; + const sequence = + current.length === 0 + ? 1 + : current[current.length - 1]!.sequence + 1; + + await storage.put(key, [...current, { ...event, sequence }]); + return { sequence }; + }, + + async loadEvents(sessionId, afterSequence = 0) { + const current = + (await storage.get[]>( + journalKey(sessionId) + )) ?? []; + + return current + .filter((event) => event.sequence > afterSequence) + .sort((a, b) => a.sequence - b.sequence); + }, + + async loadLatestSnapshot(sessionId) { + const snapshots = + (await storage.get[]>( + snapshotsKey(sessionId) + )) ?? []; + + return ( + [...snapshots].sort( + (a, b) => + a.afterSequence - b.afterSequence || a.createdAt - b.createdAt + ).at(-1) ?? null + ); + }, + + async saveSnapshot(snapshot) { + const key = snapshotsKey(snapshot.sessionId); + const current = + (await storage.get[]>(key)) ?? []; + + await storage.put(key, [...current, snapshot]); + }, + }; +} + +type SessionEntry = { + events: JournalEventRecord[]; + snapshot: PersistedSnapshot | null; +}; + +export type CloudflareAgentRunStoreState = { + sessions: Record; +}; + +export function createCloudflareAgentRunStore(options: { + getState: () => CloudflareAgentRunStoreState; + setState: ( + nextState: CloudflareAgentRunStoreState + ) => void | Promise; +}): RunStore { + return { + async append(sessionId, event) { + const currentState = options.getState(); + const currentSession = currentState.sessions[sessionId] ?? { + events: [], + snapshot: null, + }; + const sequence = currentSession.events.length + 1; + const nextSession: SessionEntry = { + ...currentSession, + events: [...currentSession.events, { ...event, sequence }], + }; + + await options.setState({ + ...currentState, + sessions: { + ...currentState.sessions, + [sessionId]: nextSession, + }, + }); + + return { sequence }; + }, + + async loadEvents(sessionId, afterSequence = 0) { + return ( + options.getState().sessions[sessionId]?.events.filter( + (event) => event.sequence > afterSequence + ) ?? [] + ); + }, + + async loadLatestSnapshot(sessionId) { + return options.getState().sessions[sessionId]?.snapshot ?? null; + }, + + async saveSnapshot(snapshot) { + const currentState = options.getState(); + const currentSession = currentState.sessions[snapshot.sessionId] ?? { + events: [], + snapshot: null, + }; + + await options.setState({ + ...currentState, + sessions: { + ...currentState.sessions, + [snapshot.sessionId]: { + ...currentSession, + snapshot, + }, + }, + }); + }, + }; +} + +function journalKey(sessionId: string): string { + return `sessions/${sessionId}/journal`; +} + +function snapshotsKey(sessionId: string): string { + return `sessions/${sessionId}/snapshots`; +} diff --git a/src/http/index.test.ts b/src/http/index.test.ts new file mode 100644 index 0000000..5ddbf5f --- /dev/null +++ b/src/http/index.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, test } from 'vitest'; +import { z } from 'zod'; +import { createAgentMachine } from '../index.js'; +import { createSessionHttpController } from './index.js'; + +function createSseReader(response: Response) { + const reader = response.body!.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + return { + async next(): Promise<{ event: string; data: unknown }> { + while (true) { + const match = buffer.match(/^event: ([^\n]+)\ndata: ([^\n]+)\n\n/); + if (match) { + buffer = buffer.slice(match[0].length); + return { + event: match[1]!, + data: JSON.parse(match[2]!), + }; + } + + const chunk = await reader.read(); + if (chunk.done) { + throw new Error('SSE stream closed before the next event was available.'); + } + + buffer += decoder.decode(chunk.value, { stream: true }); + } + }, + + async cancel() { + await reader.cancel(); + }, + }; +} + +describe('http adapter', () => { + test('starts sessions, sends events, reads snapshots, and streams emitted events', async () => { + const machine = createAgentMachine({ + id: 'http-adapter-test', + schemas: { + input: z.object({ + text: z.string(), + }), + events: { + begin: z.object({}), + }, + emitted: { + textPart: z.object({ + delta: z.string(), + }), + }, + }, + context: (input) => ({ + text: input.text, + finalText: '', + }), + initial: 'waiting', + states: { + waiting: { + on: { + begin: { + target: 'writing', + }, + }, + }, + writing: { + resultSchema: z.object({ + text: z.string(), + }), + invoke: async ({ context }, enq) => { + enq.emit({ type: 'textPart', delta: context.text }); + return { text: context.text }; + }, + onDone: ({ result }) => ({ + target: 'done', + context: { + finalText: result.text, + }, + }), + }, + done: { + type: 'final', + output: ({ context }) => ({ + text: context.finalText, + }), + }, + }, + }); + const controller = createSessionHttpController(machine); + + const startResponse = await controller.handle( + new Request('https://agent.test/sessions', { + method: 'POST', + body: JSON.stringify({ text: 'hello' }), + }) + ); + const startBody = await startResponse.json() as { + sessionId: string; + snapshot: { value: string; status: string }; + }; + + expect(startBody.snapshot).toEqual( + expect.objectContaining({ + value: 'waiting', + status: 'active', + }) + ); + + const streamResponse = await controller.handle( + new Request(`https://agent.test/sessions/${startBody.sessionId}/stream`) + ); + const reader = createSseReader(streamResponse); + + const sendPromise = controller.handle( + new Request(`https://agent.test/sessions/${startBody.sessionId}/events`, { + method: 'POST', + body: JSON.stringify({ type: 'begin' }), + }) + ); + + await expect(reader.next()).resolves.toEqual({ + event: 'textPart', + data: { + type: 'textPart', + delta: 'hello', + }, + }); + await expect(reader.next()).resolves.toEqual({ + event: 'done', + data: { + text: 'hello', + }, + }); + + const sendResponse = await sendPromise; + expect(sendResponse.status).toBe(200); + + const statusResponse = await controller.handle( + new Request(`https://agent.test/sessions/${startBody.sessionId}`) + ); + const statusBody = await statusResponse.json() as { + snapshot: { value: string; status: string; output: unknown }; + }; + + expect(statusBody.snapshot).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + output: { + text: 'hello', + }, + }) + ); + }); +}); diff --git a/src/http/index.ts b/src/http/index.ts new file mode 100644 index 0000000..c1a0ee9 --- /dev/null +++ b/src/http/index.ts @@ -0,0 +1,209 @@ +import { + createMemoryRunStore, +} from '../runtime/memory-store.js'; +import { + restoreSession, + startSession, +} from '../runtime/session.js'; +import type { + AgentMachine, + AgentRun, + RunStore, + TransitionEvent, +} from '../types.js'; + +type AnyMachine = AgentMachine; +type RunFor = + TMachine extends AgentMachine< + any, + infer TContext, + infer TEvents, + infer TStates, + infer TOutput, + infer TEmitted + > + ? AgentRun + : AgentRun; + +type InputFor = + TMachine extends AgentMachine + ? TInput + : unknown; + +type EventsFor = + TMachine extends AgentMachine + ? TEvents + : {}; + +export interface SessionHttpController { + handle(request: Request): Promise; + getRun(sessionId: string): Promise>; + dropActiveSession(sessionId: string): void; +} + +export interface SessionHttpControllerOptions { + store?: RunStore; + parseInput?: (request: Request) => Promise>; + parseEvent?: ( + request: Request + ) => Promise>>; +} + +export function createSessionHttpController( + machine: TMachine, + options: SessionHttpControllerOptions = {} +): SessionHttpController { + const store = options.store ?? createMemoryRunStore(); + const activeRuns = new Map>(); + const parseInput = + options.parseInput ?? ((request) => request.json() as Promise>); + const parseEvent = + options.parseEvent ?? + ((request) => request.json() as Promise>>); + + function trackRun(run: RunFor): RunFor { + activeRuns.set(run.sessionId, run); + run.onDone(() => { + activeRuns.delete(run.sessionId); + }); + run.onError(() => { + activeRuns.delete(run.sessionId); + }); + return run; + } + + async function getRun(sessionId: string): Promise> { + const existing = activeRuns.get(sessionId); + if (existing) { + return existing; + } + + const restored = await restoreSession(machine, { + sessionId, + store, + }) as RunFor; + + return trackRun(restored); + } + + return { + getRun, + + dropActiveSession(sessionId) { + activeRuns.delete(sessionId); + }, + + async handle(request) { + const url = new URL(request.url); + const match = url.pathname.match(/^\/sessions(?:\/([^/]+)(?:\/(events|stream))?)?$/); + const sessionId = match?.[1]; + const childRoute = match?.[2]; + + if (request.method === 'POST' && url.pathname === '/sessions') { + const run = await startSession(machine, { + store, + input: await parseInput(request), + }) as RunFor; + + trackRun(run); + + return Response.json({ + sessionId: run.sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'GET' && sessionId && !childRoute) { + const run = await getRun(sessionId); + + return Response.json({ + sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'POST' && sessionId && childRoute === 'events') { + const run = await getRun(sessionId); + await run.send(await parseEvent(request)); + + return Response.json({ + sessionId, + snapshot: run.getSnapshot(), + }); + } + + if (request.method === 'GET' && sessionId && childRoute === 'stream') { + return createRunSseResponse(await getRun(sessionId)); + } + + return new Response('Not found', { status: 404 }); + }, + }; +} + +export function createSessionHttpHandler( + machine: TMachine, + options: SessionHttpControllerOptions = {} +): (request: Request) => Promise { + const controller = createSessionHttpController(machine, options); + return (request) => controller.handle(request); +} + +export function createRunSseResponse( + run: AgentRun +): Response { + let cleanup = () => {}; + + const stream = new ReadableStream({ + start(controller) { + const encoder = new TextEncoder(); + const write = (event: string, data: unknown) => { + controller.enqueue( + encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) + ); + }; + + if (run.getSnapshot().status === 'done') { + write('done', run.getSnapshot().output); + controller.close(); + return; + } + + if (run.getSnapshot().status === 'error') { + write('error', { error: String(run.getSnapshot().error) }); + controller.close(); + return; + } + + const offEmitted = run.onEmitted((event) => { + write(event.type, event); + }); + const offDone = run.onDone((event) => { + write('done', event.output); + cleanup(); + controller.close(); + }); + const offError = run.onError((event) => { + write('error', { error: String(event.error) }); + cleanup(); + controller.close(); + }); + + cleanup = () => { + offEmitted(); + offDone(); + offError(); + }; + }, + cancel() { + cleanup(); + }, + }); + + return new Response(stream, { + headers: { + 'content-type': 'text/event-stream', + 'cache-control': 'no-cache', + }, + }); +} diff --git a/src/index.ts b/src/index.ts index 12162ba..b28d902 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ export { classify, classifyResultSchema } from './classify.js'; export { createAdapter } from './adapter.js'; export { createMemoryRunStore } from './runtime/memory-store.js'; export { restoreSession, startSession } from './runtime/session.js'; +export { waitForRunDone, waitForRunSnapshot } from './runtime/index.js'; // Types export type { diff --git a/src/next/index.test.ts b/src/next/index.test.ts new file mode 100644 index 0000000..37984f4 --- /dev/null +++ b/src/next/index.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, test } from 'vitest'; +import { z } from 'zod'; +import { createAgentMachine } from '../index.js'; +import { + createNextSessionRouteHandlers, + dynamic, + maxDuration, + runtime, +} from './index.js'; + +describe('next adapter', () => { + test('adapts generic session handlers to App Router route params', async () => { + const machine = createAgentMachine({ + id: 'next-adapter-test', + schemas: { + input: z.object({ + request: z.string(), + }), + events: { + approve: z.object({}), + }, + }, + context: (input) => ({ + request: input.request, + approved: false, + }), + initial: 'review', + states: { + review: { + on: { + approve: { + target: 'done', + context: { + approved: true, + }, + }, + }, + }, + done: { + type: 'final', + output: ({ context }) => ({ + request: context.request, + approved: context.approved, + }), + }, + }, + }); + const routes = createNextSessionRouteHandlers(machine); + + expect(runtime).toBe('nodejs'); + expect(dynamic).toBe('force-dynamic'); + expect(maxDuration).toBe(30); + + const startResponse = await routes.sessions.POST( + new Request('https://agent.test/api/sessions', { + method: 'POST', + body: JSON.stringify({ request: 'Ship it.' }), + }) + ); + const startBody = await startResponse.json() as { + sessionId: string; + }; + + const sendResponse = await routes.events.POST( + new Request(`https://agent.test/api/sessions/${startBody.sessionId}/events`, { + method: 'POST', + body: JSON.stringify({ type: 'approve' }), + }), + { + params: Promise.resolve({ + sessionId: startBody.sessionId, + }), + } + ); + const sendBody = await sendResponse.json() as { + snapshot: { value: string; output: unknown }; + }; + + expect(sendBody.snapshot).toEqual( + expect.objectContaining({ + value: 'done', + output: { + request: 'Ship it.', + approved: true, + }, + }) + ); + }); +}); diff --git a/src/next/index.ts b/src/next/index.ts new file mode 100644 index 0000000..6fa6431 --- /dev/null +++ b/src/next/index.ts @@ -0,0 +1,81 @@ +import { + createSessionHttpController, + type SessionHttpController, + type SessionHttpControllerOptions, +} from '../http/index.js'; +import type { AgentMachine } from '../types.js'; + +type AnyMachine = AgentMachine; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 30; + +export interface NextRouteContext> { + params: Promise | TParams; +} + +export interface NextSessionRouteHandlers { + sessions: { + POST(request: Request): Promise; + }; + session: { + GET( + request: Request, + context: NextRouteContext<{ sessionId: string }> + ): Promise; + }; + events: { + POST( + request: Request, + context: NextRouteContext<{ sessionId: string }> + ): Promise; + }; + stream: { + GET( + request: Request, + context: NextRouteContext<{ sessionId: string }> + ): Promise; + }; + controller: SessionHttpController; +} + +export function createNextSessionRouteHandlers( + machine: TMachine, + options: SessionHttpControllerOptions = {} +): NextSessionRouteHandlers { + const controller = createSessionHttpController(machine, options); + + return { + sessions: { + POST(request) { + return controller.handle(rewritePath(request, '/sessions')); + }, + }, + session: { + async GET(request, context) { + const { sessionId } = await context.params; + return controller.handle(rewritePath(request, `/sessions/${sessionId}`)); + }, + }, + events: { + async POST(request, context) { + const { sessionId } = await context.params; + return controller.handle(rewritePath(request, `/sessions/${sessionId}/events`)); + }, + }, + stream: { + async GET(request, context) { + const { sessionId } = await context.params; + return controller.handle(rewritePath(request, `/sessions/${sessionId}/stream`)); + }, + }, + controller, + }; +} + +function rewritePath(request: Request, pathname: string): Request { + const url = new URL(request.url); + url.pathname = pathname; + return new Request(url, request); +} diff --git a/src/runtime/index.test.ts b/src/runtime/index.test.ts new file mode 100644 index 0000000..10f1569 --- /dev/null +++ b/src/runtime/index.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, test } from 'vitest'; +import { z } from 'zod'; +import { + createAgentMachine, + createMemoryRunStore, + startSession, +} from '../index.js'; +import { waitForRunDone, waitForRunSnapshot } from './index.js'; + +describe('runtime helpers', () => { + test('waitForRunSnapshot and waitForRunDone observe session lifecycle', async () => { + const machine = createAgentMachine({ + id: 'runtime-helper-test', + schemas: { + events: { + finish: z.object({ value: z.string() }), + }, + }, + context: () => ({ + value: null as string | null, + }), + initial: 'waiting', + states: { + waiting: { + on: { + finish: ({ event }) => ({ + target: 'done', + context: { + value: event.value, + }, + }), + }, + }, + done: { + type: 'final', + output: ({ context }) => ({ + value: context.value, + }), + }, + }, + }); + + const run = await startSession(machine, { + store: createMemoryRunStore(), + }); + const waiting = await waitForRunSnapshot( + run, + (snapshot) => snapshot.status === 'pending' + ); + + expect(waiting.value).toBe('waiting'); + + const donePromise = waitForRunDone(run); + await run.send({ type: 'finish', value: 'ok' }); + + await expect(donePromise).resolves.toEqual( + expect.objectContaining({ + output: { + value: 'ok', + }, + }) + ); + }); +}); diff --git a/src/runtime/index.ts b/src/runtime/index.ts new file mode 100644 index 0000000..0233285 --- /dev/null +++ b/src/runtime/index.ts @@ -0,0 +1,80 @@ +import type { + AgentRun, + AgentSnapshot, + StandardSchemaV1, +} from '../types.js'; + +export function waitForRunDone< + TContext extends Record, + TValue extends string, + TEvents extends Record, + TOutput, + TEmitted extends Record, +>( + run: AgentRun +): Promise<{ + output: TOutput; + snapshot: AgentSnapshot; +}> { + return new Promise((resolve, reject) => { + const offDone = run.onDone((event) => { + offDone(); + offError(); + resolve(event); + }); + const offError = run.onError((event) => { + offDone(); + offError(); + reject(event.error); + }); + }); +} + +export function waitForRunSnapshot< + TContext extends Record, + TValue extends string, + TEvents extends Record, + TOutput, + TEmitted extends Record, +>( + run: AgentRun, + predicate: ( + snapshot: AgentSnapshot + ) => boolean, + timeoutMs = 1000 +): Promise> { + const current = run.getSnapshot(); + if (predicate(current)) { + return Promise.resolve(current); + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + cleanup(); + reject(new Error('Run snapshot did not reach the expected state in time.')); + }, timeoutMs); + + const cleanup = () => { + clearTimeout(timeout); + offSnapshot(); + offDone(); + offError(); + }; + + const check = (snapshot: AgentSnapshot) => { + if (predicate(snapshot)) { + cleanup(); + resolve(snapshot); + } + }; + + const offSnapshot = run.onSnapshot(check); + const offDone = run.onDone((event) => { + check(event.snapshot); + }); + const offError = run.onError((event) => { + cleanup(); + reject(event.error); + }); + }); +} diff --git a/tsdown.config.ts b/tsdown.config.ts index 333bb5f..05fe1c3 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -4,7 +4,11 @@ export default defineConfig({ entry: { index: 'src/index.ts', 'ai-sdk': 'src/ai-sdk/index.ts', + cloudflare: 'src/cloudflare/index.ts', graph: 'src/graph/index.ts', + http: 'src/http/index.ts', + next: 'src/next/index.ts', + runtime: 'src/runtime/index.ts', xstate: 'src/xstate/index.ts', }, format: ['esm', 'cjs'], From 858ce6f4166dabaed332421cfe3453959dc71245 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Fri, 15 May 2026 00:10:46 +0100 Subject: [PATCH 33/34] Add first-class messages and always transitions --- .changeset/sharp-messages-always.md | 9 + examples/README.md | 1 + examples/_run.ts | 2 + examples/chatbot-messages.ts | 36 ++- examples/hitl.ts | 23 +- examples/index.ts | 1 + examples/spec-agent-loop.ts | 274 ++++++++++++++++++ readme.md | 2 +- src/agent.test.ts | 84 +++++- src/cloudflare/index.test.ts | 2 + src/examples.test.ts | 3 +- src/graph/index.test.ts | 36 +++ src/graph/index.ts | 27 +- src/index.ts | 7 + .../chatbot-messages.test.ts | 4 +- src/langgraph-equivalents/error-retry.test.ts | 1 + src/machine.ts | 68 ++++- src/persistence.test.ts | 6 + src/session-runtime.test.ts | 46 +++ src/session-types.test.ts | 1 + src/types.ts | 27 +- src/utils.ts | 42 ++- src/xstate/index.test.ts | 34 +++ src/xstate/index.ts | 17 ++ 24 files changed, 696 insertions(+), 57 deletions(-) create mode 100644 .changeset/sharp-messages-always.md create mode 100644 examples/spec-agent-loop.ts diff --git a/.changeset/sharp-messages-always.md b/.changeset/sharp-messages-always.md new file mode 100644 index 0000000..31172bf --- /dev/null +++ b/.changeset/sharp-messages-always.md @@ -0,0 +1,9 @@ +--- +"@statelyai/agent": minor +--- + +Add first-class session messages and deterministic always transitions. + +Agent states and snapshots now carry `messages` alongside `context`. State hooks receive messages, transition results can replace messages, and helper functions are exported for appending user, assistant, and system messages. + +Machines can now define `always` transitions for deterministic eventless routing. Runtime sessions journal these transitions as internal events so persistence and restore remain replayable. diff --git a/examples/README.md b/examples/README.md index ed4b3f3..724fc56 100644 --- a/examples/README.md +++ b/examples/README.md @@ -38,6 +38,7 @@ These focus on real orchestration patterns: - [`lead-score-flow.ts`](/Users/davidkpiano/Code/agent/examples/lead-score-flow.ts) - [`meeting-assistant-flow.ts`](/Users/davidkpiano/Code/agent/examples/meeting-assistant-flow.ts) - [`self-evaluation-loop-flow.ts`](/Users/davidkpiano/Code/agent/examples/self-evaluation-loop-flow.ts) +- [`spec-agent-loop.ts`](/Users/davidkpiano/Code/agent/examples/spec-agent-loop.ts) - [`write-a-book-flow.ts`](/Users/davidkpiano/Code/agent/examples/write-a-book-flow.ts) - [`plan-and-execute.ts`](/Users/davidkpiano/Code/agent/examples/plan-and-execute.ts) - [`reflection.ts`](/Users/davidkpiano/Code/agent/examples/reflection.ts) diff --git a/examples/_run.ts b/examples/_run.ts index d3abce0..510f2af 100644 --- a/examples/_run.ts +++ b/examples/_run.ts @@ -76,6 +76,7 @@ export function formatResult(result: ExecuteResult) { status: result.status, value: result.state.value, context: result.context, + messages: result.messages, output: result.output, }; } @@ -85,6 +86,7 @@ export function formatResult(result: ExecuteResult) { status: result.status, value: result.value, context: result.context, + messages: result.messages, events: Object.keys(result.events), }; } diff --git a/examples/chatbot-messages.ts b/examples/chatbot-messages.ts index bb1c977..7c5cd80 100644 --- a/examples/chatbot-messages.ts +++ b/examples/chatbot-messages.ts @@ -3,6 +3,7 @@ import { createAgentMachine, createMemoryRunStore, startSession, + type AgentMessage, } from '../src/index.js'; import { closePrompt, @@ -13,7 +14,7 @@ import { } from './_run.js'; const messageSchema = z.object({ - role: z.enum(['user', 'assistant']), + role: z.string(), content: z.string(), }); @@ -22,7 +23,7 @@ const replySchema = z.object({ }); export function createChatbotMessagesExample( - reply: (messages: Array>) => Promise> = (messages) => + reply: (messages: AgentMessage[]) => Promise> = (messages) => generateExampleObject({ schema: replySchema, system: 'You are a concise assistant in a terminal chat.', @@ -50,19 +51,17 @@ export function createChatbotMessagesExample( }, }, context: () => ({ - messages: [] as Array>, finalMessage: null as z.infer | null, ended: false, }), + messages: [], initial: 'waitingForUser', states: { waitingForUser: { on: { - 'messages.user': ({ event, context }) => ({ + 'messages.user': ({ event, messages }) => ({ target: 'replying', - context: { - messages: [...context.messages, event.message], - }, + messages: messages.concat(event.message), }), 'messages.end': { target: 'done', @@ -72,19 +71,19 @@ export function createChatbotMessagesExample( }, replying: { resultSchema: replySchema, - invoke: async ({ context }) => reply(context.messages), - onDone: ({ result, context }) => ({ + invoke: async ({ messages }) => reply(messages), + onDone: ({ result, messages }) => ({ target: 'waitingForUser', + messages: messages.concat(result.message), context: { - messages: [...context.messages, result.message], finalMessage: result.message, }, }), }, done: { type: 'final', - output: ({ context }) => ({ - messages: context.messages, + output: ({ context, messages }) => ({ + messages, finalMessage: context.finalMessage, }), }, @@ -111,17 +110,22 @@ async function main() { status: snapshot.status, value: snapshot.value, context: snapshot.context, + messages: snapshot.messages, output: snapshot.output, }); break; } + const finalMessage = snapshot.context.finalMessage as + | z.infer + | null; + if ( - snapshot.context.finalMessage?.role === 'assistant' - && snapshot.context.finalMessage.content !== lastPrintedAssistantMessage + finalMessage?.role === 'assistant' + && finalMessage.content !== lastPrintedAssistantMessage ) { - console.log(`Assistant: ${snapshot.context.finalMessage.content}`); - lastPrintedAssistantMessage = snapshot.context.finalMessage.content; + console.log(`Assistant: ${finalMessage.content}`); + lastPrintedAssistantMessage = finalMessage.content; } const content = await prompt('User (blank to exit)'); diff --git a/examples/hitl.ts b/examples/hitl.ts index 7e88077..919d894 100644 --- a/examples/hitl.ts +++ b/examples/hitl.ts @@ -3,6 +3,7 @@ import { createAgentMachine, createMemoryRunStore, startSession, + type AgentMessage, } from '../src/index.js'; import { closePrompt, @@ -19,15 +20,15 @@ const draftSchema = z.object({ export function createHitlExample( draftReply: (args: { task: string; - notes: string[]; - }) => Promise> = async ({ task, notes }) => { + messages: AgentMessage[]; + }) => Promise> = async ({ task, messages }) => { return generateExampleObject({ schema: draftSchema, prompt: [ `Task: ${task}`, '', 'Use the notes below to draft a concise response:', - ...notes.map((note, index) => `${index + 1}. ${note}`), + ...messages.map((message, index) => `${index + 1}. ${message.content}`), ].join('\n'), }); } @@ -48,17 +49,15 @@ export function createHitlExample( }, context: (input) => ({ task: input.task, - notes: [] as string[], draft: null as string | null, }), + messages: [], initial: 'gathering', states: { gathering: { on: { - 'user.message': ({ context, event }) => ({ - context: { - notes: context.notes.concat(event.message), - }, + 'user.message': ({ messages, event }) => ({ + messages: messages.concat({ role: 'user', content: event.message }), }), 'user.approve': { target: 'drafting' }, 'user.cancel': { target: 'cancelled' }, @@ -66,13 +65,14 @@ export function createHitlExample( }, drafting: { resultSchema: draftSchema, - invoke: async ({ context }) => + invoke: async ({ context, messages }) => draftReply({ task: context.task, - notes: context.notes, + messages, }), - onDone: ({ result }) => ({ + onDone: ({ result, messages }) => ({ target: 'done', + messages: messages.concat({ role: 'assistant', content: result.draft }), context: { draft: result.draft }, }), }, @@ -108,6 +108,7 @@ async function main() { status: snapshot.status, value: snapshot.value, context: snapshot.context, + messages: snapshot.messages, output: snapshot.output, }); break; diff --git a/examples/index.ts b/examples/index.ts index f698113..d0b02a4 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -63,6 +63,7 @@ export { export { createRewooExample } from './rewoo.js'; export { createReflectionExample } from './reflection.js'; export { createSelfEvaluationLoopFlowExample } from './self-evaluation-loop-flow.js'; +export { createSpecAgentLoopExample } from './spec-agent-loop.js'; export { createSupervisorExample } from './supervisor.js'; export { createWriteABookFlowExample } from './write-a-book-flow.js'; export { createSqlAgentExample } from './sql-agent.js'; diff --git a/examples/spec-agent-loop.ts b/examples/spec-agent-loop.ts new file mode 100644 index 0000000..8d86de9 --- /dev/null +++ b/examples/spec-agent-loop.ts @@ -0,0 +1,274 @@ +import { z } from 'zod'; +import { + appendMessages, + assistantMessage, + createAgentMachine, + createMemoryRunStore, + startSession, + userMessage, +} from '../src/index.js'; +import { + closePrompt, + generateExampleText, + isMain, + prompt, + waitForRunSnapshot, +} from './_run.js'; + +const generationSchema = z.object({ + rawText: z.string(), + specYaml: z.string(), + questions: z.array(z.string()), + status: z.enum(['needs_user', 'complete']), +}); + +const validationSchema = z.object({ + ok: z.boolean(), + errors: z.array(z.string()), +}); + +type Generation = z.infer; + +export function createSpecAgentLoopExample( + options: { + generate?: (args: { + specName: string; + messages: Array<{ role: string; content: string }>; + }) => Promise; + validate?: (yaml: string) => z.infer; + maxRepairTurns?: number; + } = {} +) { + const generate = + options.generate ?? + (({ specName, messages }) => + generateExampleText({ + system: [ + 'Write a small YAML spec.', + 'Respond exactly with , , and tags.', + 'Use complete only when the YAML has no __HOLE__ markers.', + ].join('\n'), + prompt: [ + `Spec name: ${specName}`, + '', + ...messages.map((message) => `${message.role}: ${message.content}`), + ].join('\n'), + })); + + const validate = + options.validate ?? + ((yaml: string) => { + const errors: string[] = []; + if (!yaml.trim()) errors.push('Missing YAML'); + if (/__HOLE__|TODO|TBD|UNKNOWN/i.test(yaml)) errors.push('YAML has holes'); + if (!/^name:/m.test(yaml)) errors.push('Missing name'); + return { ok: errors.length === 0, errors }; + }); + + return createAgentMachine({ + id: 'spec-agent-loop-example', + schemas: { + input: z.object({ + specName: z.string(), + prompt: z.string(), + }), + events: { + 'user.answer': z.object({ answer: z.string() }), + 'user.accept': z.object({}), + 'user.quit': z.object({}), + }, + output: z.object({ + specYaml: z.string(), + accepted: z.boolean(), + }), + }, + context: (input) => ({ + specName: input.specName, + specYaml: '', + questions: [] as string[], + status: 'needs_user' as Generation['status'], + validation: { ok: false, errors: [] as string[] }, + repairTurns: 0, + maxRepairTurns: options.maxRepairTurns ?? 3, + accepted: false, + }), + messages: (input) => [ + userMessage(`Create an initial spec from this prompt:\n\n${input.prompt}`), + ], + initial: 'generating', + states: { + generating: { + resultSchema: generationSchema, + invoke: async ({ context, messages }) => + parseTaggedResponse( + await generate({ + specName: context.specName, + messages, + }) + ), + onDone: ({ result, messages }) => ({ + target: result.specYaml ? 'validating' : 'repairing', + context: { + specYaml: result.specYaml, + questions: result.questions, + status: result.status, + }, + messages: appendMessages(messages, assistantMessage(result.rawText)), + }), + }, + validating: { + resultSchema: validationSchema, + invoke: async ({ context }) => validate(context.specYaml), + onDone: ({ result }) => ({ + target: 'routing', + context: { validation: result }, + }), + }, + routing: { + always: ({ context, messages }) => { + if (context.validation.ok && context.status === 'complete') { + return { target: 'awaitingAcceptance' }; + } + + if (!context.validation.ok && context.status === 'complete') { + return { + target: + context.repairTurns < context.maxRepairTurns + ? 'generating' + : 'awaitingUser', + context: { repairTurns: context.repairTurns + 1 }, + messages: appendMessages( + messages, + userMessage( + [ + 'You marked the spec complete, but deterministic validation failed.', + ...context.validation.errors.map((error) => `- ${error}`), + ].join('\n') + ) + ), + }; + } + + return { target: 'awaitingUser' }; + }, + }, + repairing: { + always: ({ context, messages }) => ({ + target: + context.repairTurns < context.maxRepairTurns + ? 'generating' + : 'awaitingUser', + context: { repairTurns: context.repairTurns + 1 }, + messages: appendMessages( + messages, + userMessage('Return the full YAML spec using the required tags.') + ), + }), + }, + awaitingUser: { + on: { + 'user.answer': ({ event, messages }) => ({ + target: 'generating', + context: { repairTurns: 0 }, + messages: appendMessages( + messages, + userMessage(`User answered/refined:\n\n${event.answer}`) + ), + }), + 'user.quit': { target: 'done' }, + }, + }, + awaitingAcceptance: { + on: { + 'user.accept': { + target: 'done', + context: { accepted: true }, + }, + 'user.answer': ({ event, messages }) => ({ + target: 'generating', + messages: appendMessages( + messages, + userMessage(`Spec validates, but refine:\n\n${event.answer}`) + ), + }), + }, + }, + done: { + type: 'final', + output: ({ context }) => ({ + specYaml: context.specYaml, + accepted: context.accepted, + }), + }, + }, + }); +} + +function parseTaggedResponse(text: string): Generation { + const specYaml = text.match(/([\s\S]*?)<\/SPEC_YAML>/)?.[1]?.trim() ?? ''; + const questionText = text.match(/([\s\S]*?)<\/QUESTIONS>/)?.[1]?.trim() ?? ''; + const statusRaw = text.match(/([\s\S]*?)<\/STATUS>/)?.[1]?.trim(); + + return { + rawText: text.trim(), + specYaml, + questions: questionText + .split('\n') + .map((line) => line.replace(/^[-*\d. )]+/, '').trim()) + .filter(Boolean), + status: statusRaw === 'complete' ? 'complete' : 'needs_user', + }; +} + +async function main() { + try { + const specName = await prompt('Spec name'); + const initialPrompt = await prompt('Describe the spec'); + const machine = createSpecAgentLoopExample(); + const run = await startSession(machine, { + store: createMemoryRunStore(), + input: { specName, prompt: initialPrompt }, + }); + + while (true) { + const snapshot = await waitForRunSnapshot( + run, + (nextSnapshot) => nextSnapshot.status !== 'active' + ); + + if (snapshot.status === 'done') { + console.log(snapshot.output); + break; + } + + console.log({ + value: snapshot.value, + validation: snapshot.context.validation, + questions: snapshot.context.questions, + }); + + if (snapshot.value === 'awaitingAcceptance') { + const answer = await prompt('Accept? [Y/n]'); + await run.send( + !answer || /^y(es)?$/i.test(answer) + ? { type: 'user.accept' } + : { type: 'user.answer', answer } + ); + continue; + } + + const answer = await prompt('Answer, refine, or /quit'); + await run.send( + answer === '/quit' + ? { type: 'user.quit' } + : { type: 'user.answer', answer } + ); + } + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/readme.md b/readme.md index e597b31..8193cc0 100644 --- a/readme.md +++ b/readme.md @@ -23,7 +23,7 @@ Start here: - App-shaped integrations: [`examples/apps/next/`](/Users/davidkpiano/Code/agent/examples/apps/next), [`examples/apps/cloudflare-agents/`](/Users/davidkpiano/Code/agent/examples/apps/cloudflare-agents), [`examples/next-ai-sdk-ui.ts`](/Users/davidkpiano/Code/agent/examples/next-ai-sdk-ui.ts), [`examples/cloudflare-agents.ts`](/Users/davidkpiano/Code/agent/examples/cloudflare-agents.ts) - Durable sessions and transports: [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts), [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts), [`examples/http-streaming-session.ts`](/Users/davidkpiano/Code/agent/examples/http-streaming-session.ts) -- Core workflow patterns: [`examples/rag.ts`](/Users/davidkpiano/Code/agent/examples/rag.ts), [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts), [`examples/error-retry.ts`](/Users/davidkpiano/Code/agent/examples/error-retry.ts), [`examples/persistent-supervisor.ts`](/Users/davidkpiano/Code/agent/examples/persistent-supervisor.ts) +- Core workflow patterns: [`examples/rag.ts`](/Users/davidkpiano/Code/agent/examples/rag.ts), [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts), [`examples/error-retry.ts`](/Users/davidkpiano/Code/agent/examples/error-retry.ts), [`examples/spec-agent-loop.ts`](/Users/davidkpiano/Code/agent/examples/spec-agent-loop.ts), [`examples/persistent-supervisor.ts`](/Users/davidkpiano/Code/agent/examples/persistent-supervisor.ts) - CrewAI-style equivalents: [`examples/content-creator-flow.ts`](/Users/davidkpiano/Code/agent/examples/content-creator-flow.ts), [`examples/email-auto-responder-flow.ts`](/Users/davidkpiano/Code/agent/examples/email-auto-responder-flow.ts), [`examples/lead-score-flow.ts`](/Users/davidkpiano/Code/agent/examples/lead-score-flow.ts), [`examples/meeting-assistant-flow.ts`](/Users/davidkpiano/Code/agent/examples/meeting-assistant-flow.ts), [`examples/self-evaluation-loop-flow.ts`](/Users/davidkpiano/Code/agent/examples/self-evaluation-loop-flow.ts), [`examples/write-a-book-flow.ts`](/Users/davidkpiano/Code/agent/examples/write-a-book-flow.ts) - Reference examples: [`examples/simple.ts`](/Users/davidkpiano/Code/agent/examples/simple.ts), [`examples/decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts), [`examples/classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts), [`examples/adapter.ts`](/Users/davidkpiano/Code/agent/examples/adapter.ts) diff --git a/src/agent.test.ts b/src/agent.test.ts index 7813732..1a24258 100644 --- a/src/agent.test.ts +++ b/src/agent.test.ts @@ -701,6 +701,83 @@ describe('type: choice', () => { }); }); +describe('messages and always', () => { + test('messages are passed through invoke, onDone, always, and output', async () => { + const machine = createAgentMachine({ + id: 'messages-always', + schemas: { + input: z.object({ prompt: z.string() }), + output: z.object({ + messages: z.array(z.object({ role: z.string(), content: z.string() })), + attempts: z.number(), + }), + }, + context: () => ({ + attempts: 0, + accepted: false, + }), + messages: (input) => [{ role: 'user', content: input.prompt }], + initial: 'generating', + states: { + generating: { + resultSchema: z.object({ text: z.string() }), + invoke: async ({ messages }) => ({ + text: `reply to ${messages.at(-1)?.content}`, + }), + onDone: ({ result, context, messages }) => ({ + target: 'checking', + context: { attempts: context.attempts + 1 }, + messages: messages.concat({ + role: 'assistant', + content: result.text, + }), + }), + }, + checking: { + always: ({ context, messages }) => + context.attempts >= 2 + ? { + target: 'done', + context: { accepted: true }, + messages: messages.concat({ + role: 'system', + content: 'accepted', + }), + } + : { + target: 'generating', + messages: messages.concat({ + role: 'user', + content: 'repair', + }), + }, + }, + done: { + type: 'final', + output: ({ context, messages }) => ({ + messages, + attempts: context.attempts, + }), + }, + }, + }); + + const result = await machine.execute(machine.getInitialState({ prompt: 'draft' })); + + expect(result.status).toBe('done'); + if (result.status === 'done') { + expect(result.messages.map((message) => message.content)).toEqual([ + 'draft', + 'reply to draft', + 'repair', + 'reply to repair', + 'accepted', + ]); + expect(result.output.attempts).toBe(2); + } + }); +}); + describe('classify', () => { test('result has typed category', async () => { const machine = createClassifyMachine( @@ -1388,12 +1465,13 @@ describe('edge cases', () => { test('done state returns as-is', async () => { const machine = createSimpleMachine(); const done = { - value: 'done', + value: 'done' as const, input: {}, context: { count: 1 }, - status: 'done', + messages: [], + status: 'done' as const, output: { result: 1 }, - } as const; + }; expect(await machine.invoke(done)).toEqual(done); }); }); diff --git a/src/cloudflare/index.test.ts b/src/cloudflare/index.test.ts index 67f0010..18b8e5f 100644 --- a/src/cloudflare/index.test.ts +++ b/src/cloudflare/index.test.ts @@ -28,6 +28,7 @@ describe('cloudflare adapter', () => { value: 'done', status: 'done', context: {}, + messages: [], input: {}, }, }); @@ -68,6 +69,7 @@ describe('cloudflare adapter', () => { value: 'done', status: 'done', context: {}, + messages: [], input: {}, }, }); diff --git a/src/examples.test.ts b/src/examples.test.ts index fc1efd7..cdad249 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -143,7 +143,7 @@ describe('curated examples', () => { expect(result.status).toBe('pending'); if (result.status === 'pending') { - expect(result.context.messages).toEqual([ + expect(result.messages).toEqual([ { role: 'user', content: 'Hello there' }, { role: 'assistant', content: 'Replying to: Hello there' }, ]); @@ -234,6 +234,7 @@ describe('curated examples', () => { snapshot: { value: 'done', context: {}, + messages: [], status: 'done', createdAt: 2, sessionId: 'session-1', diff --git a/src/graph/index.test.ts b/src/graph/index.test.ts index 6afbb11..d65d2c9 100644 --- a/src/graph/index.test.ts +++ b/src/graph/index.test.ts @@ -111,6 +111,42 @@ test('exports finite states and transition edges as Stately graph JSON', () => { }); }); +test('exports always transitions and message updates', () => { + const machine = createAgentMachine({ + id: 'always-graph', + context: () => ({}), + initial: 'checking', + states: { + checking: { + always: ({ messages }) => ({ + target: 'done', + messages: messages.concat({ role: 'assistant', content: 'ok' }), + }), + }, + done: { + type: 'final', + }, + }, + }); + + expect(toGraph(machine).edges).toEqual([ + { + type: 'edge', + id: 'checking::0', + sourceId: 'checking', + targetId: 'done', + label: 'always', + data: { + event: '', + source: 'always', + actions: { + messages: true, + }, + }, + }, + ]); +}); + test('infers switch, early-return, and helper-call transition branches', () => { const machine = createAgentMachine({ id: 'ast-rich-export', diff --git a/src/graph/index.ts b/src/graph/index.ts index 842a1f5..f9b4cab 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -27,13 +27,14 @@ export interface AgentGraphNodeData { export interface AgentGraphEdgeData { event?: string; - source?: 'event' | 'invoke.done'; + source?: 'event' | 'invoke.done' | 'always'; guard?: { type: string; }; actions?: { context?: boolean; input?: boolean; + messages?: boolean; }; } @@ -67,6 +68,7 @@ type EdgeCandidate = { guard?: string; hasContext?: boolean; hasInput?: boolean; + hasMessages?: boolean; }; type AnalysisResult = { @@ -131,6 +133,19 @@ export function analyzeGraph(machine: AgentMachine): AgentGraphAnalysis { warnings.push(...formatWarnings(sourceId, event, result.warnings)); } + if (stateConfig.always) { + const event = ''; + const result = getTransitionEdges({ + sourceId, + event, + source: 'always', + transition: stateConfig.always, + ordinalOffset: edges.length, + }); + edges.push(...result.edges); + warnings.push(...formatWarnings(sourceId, 'always', result.warnings)); + } + if (!stateConfig.on) { continue; } @@ -270,11 +285,12 @@ function getTransitionEdges(args: { }, } : {}), - ...((candidate.hasContext || candidate.hasInput) + ...((candidate.hasContext || candidate.hasInput || candidate.hasMessages) ? { actions: { ...(candidate.hasContext ? { context: true } : {}), ...(candidate.hasInput ? { input: true } : {}), + ...(candidate.hasMessages ? { messages: true } : {}), }, } : {}), @@ -301,6 +317,7 @@ function analyzeTransitionObject(transition: unknown): AnalysisResult { target, hasContext: 'context' in transition, hasInput: 'input' in transition, + hasMessages: 'messages' in transition, }], warnings: [], }; @@ -711,6 +728,7 @@ function analyzeTransitionExpression( guard: combineGuardList(guards), hasContext: object ? hasProperty(object, 'context') : false, hasInput: object ? hasProperty(object, 'input') : false, + hasMessages: object ? hasProperty(object, 'messages') : false, }], warnings: [], }; @@ -897,11 +915,12 @@ function hasProperty(object: ts.ObjectLiteralExpression, name: string): boolean } function getEdgeLabel(event: string, guard: string | undefined): string { + const label = event || 'always'; if (!guard) { - return event; + return label; } - return `${event} [${guard}]`; + return `${label} [${guard}]`; } function createBindings( diff --git a/src/index.ts b/src/index.ts index b28d902..4df7514 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,11 +8,18 @@ export { createAdapter } from './adapter.js'; export { createMemoryRunStore } from './runtime/memory-store.js'; export { restoreSession, startSession } from './runtime/session.js'; export { waitForRunDone, waitForRunSnapshot } from './runtime/index.js'; +export { + appendMessages, + assistantMessage, + systemMessage, + userMessage, +} from './utils.js'; // Types export type { AgentAdapter, AgentMachine, + AgentMessage, AgentRun, AgentSnapshot, AgentState, diff --git a/src/langgraph-equivalents/chatbot-messages.test.ts b/src/langgraph-equivalents/chatbot-messages.test.ts index 602445f..1174b1c 100644 --- a/src/langgraph-equivalents/chatbot-messages.test.ts +++ b/src/langgraph-equivalents/chatbot-messages.test.ts @@ -20,7 +20,7 @@ test('message-centric chatbot workflow accumulates structured messages across tu expect(firstResult.status).toBe('pending'); if (firstResult.status === 'pending') { - expect(firstResult.context.messages).toEqual([ + expect(firstResult.messages).toEqual([ { role: 'user', content: 'Hello there' }, { role: 'assistant', content: 'Replying to: Hello there' }, ]); @@ -36,7 +36,7 @@ test('message-centric chatbot workflow accumulates structured messages across tu expect(secondResult.status).toBe('pending'); if (secondResult.status === 'pending') { - expect(secondResult.context.messages).toEqual([ + expect(secondResult.messages).toEqual([ { role: 'user', content: 'Hello there' }, { role: 'assistant', content: 'Replying to: Hello there' }, { role: 'user', content: 'Can you expand on that?' }, diff --git a/src/langgraph-equivalents/error-retry.test.ts b/src/langgraph-equivalents/error-retry.test.ts index 8472fd0..d595a37 100644 --- a/src/langgraph-equivalents/error-retry.test.ts +++ b/src/langgraph-equivalents/error-retry.test.ts @@ -80,6 +80,7 @@ test('restores a durable retry snapshot and continues from the next attempt', as snapshot: { value: retryState.value, context: retryState.context, + messages: retryState.messages, status: retryState.status, input: retryState.input, createdAt: 1, diff --git a/src/machine.ts b/src/machine.ts index 5371bc0..01d85bf 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -1,5 +1,6 @@ import type { AgentMachine, + AgentMessage, AgentSnapshot, AgentState, EmittedPart, @@ -18,6 +19,7 @@ import { formatSchemaIssues, getAvailableEvents, getInput, + isAlwaysEventType, isDoneInvokeEventType, isErrorInvokeEventType, resolveInitial, @@ -50,19 +52,26 @@ type StateNodeDef< resultSchema?: StandardSchemaV1; invoke?: (args: { context: TContext; + messages: AgentMessage[]; input: NoInfer; signal?: AbortSignal; }, enq: { emit(part: EmittedPart): void }) => Promise; - onDone?: (args: { result: OnDoneResult; context: TContext }) => TransitionResult; + onDone?: (args: { result: OnDoneResult; context: TContext; messages: AgentMessage[] }) => TransitionResult; + always?: TransitionResult | ((args: { + context: TContext; + messages: AgentMessage[]; + input: NoInfer; + }, enq: { emit(part: EmittedPart): void }) => TransitionResult); on?: { [E in keyof TEvents & string]?: TransitionResult | ((args: { event: EventFor; context: TContext; + messages: AgentMessage[]; }, enq: { emit(part: EmittedPart): void }) => TransitionResult) }; events?: Record; - output?: (args: { context: TContext }) => NoInfer; + output?: (args: { context: TContext; messages: AgentMessage[] }) => NoInfer; model?: string; adapter?: import('./types.js').AgentAdapter; - prompt?: string | ((args: { context: TContext; input: NoInfer }) => string); + prompt?: string | ((args: { context: TContext; messages: AgentMessage[]; input: NoInfer }) => string); options?: Record; reasoning?: boolean; }; @@ -105,6 +114,7 @@ export function createAgentMachine< output?: StandardSchemaV1; }; context: (input: NoInfer) => NoInfer; + messages?: AgentMessage[] | ((input: NoInfer) => AgentMessage[]); adapter?: import('./types.js').AgentAdapter; initial: | (keyof TInputMap & keyof TResultMap & string) @@ -134,6 +144,7 @@ export function createAgentMachine< output?: StandardSchemaV1; }; context: (input: NoInfer) => TContext; + messages?: AgentMessage[] | ((input: NoInfer) => AgentMessage[]); adapter?: import('./types.js').AgentAdapter; initial: | (keyof TInputMap & keyof TResultMap & string) @@ -162,6 +173,7 @@ export function createAgentMachine< output?: StandardSchemaV1; }; context: (...args: any[]) => TContext; + messages?: AgentMessage[] | ((input: unknown) => AgentMessage[]); adapter?: import('./types.js').AgentAdapter; initial: | (keyof TInputMap & keyof TResultMap & string) @@ -221,6 +233,10 @@ export function createAgentMachine( } const context = cfg.context(validatedInput); + const messages = + typeof cfg.messages === 'function' + ? cfg.messages(validatedInput) + : cfg.messages ?? []; const init = resolveInitial(cfg.initial, { context, input: {} }); if (!init.target) { @@ -230,6 +246,7 @@ export function createAgentMachine( return { value: init.target, context: init.context ? { ...context, ...init.context } : context, + messages: init.messages ?? messages, status: 'active', input: init.input ? { [init.target]: init.input } : {}, }; @@ -238,6 +255,7 @@ export function createAgentMachine( function resolveState(raw: { value: string; context: Record; + messages?: AgentMessage[]; input?: Record>; sessionId?: string; createdAt?: number; @@ -248,6 +266,7 @@ export function createAgentMachine( return { value: raw.value, context: raw.context, + messages: raw.messages ?? [], status: raw.status ?? 'active', input: raw.input ?? {}, sessionId: raw.sessionId, @@ -289,6 +308,7 @@ export function createAgentMachine( context: result.context ? { ...state.context, ...result.context } : state.context, + messages: result.messages ?? state.messages, }; } @@ -296,14 +316,21 @@ export function createAgentMachine( handler: | TransitionResult | ((args: { - event: { type: string; [k: string]: unknown }; - context: Record; - }, enq: { emit(part: EmittedPart): void }) => TransitionResult), + event: { type: string; [k: string]: unknown }; + context: Record; + messages: AgentMessage[]; + input: Record; + }, enq: { emit(part: EmittedPart): void }) => TransitionResult), status = state.status ): { next: AgentState; emitted: EmittedPart[] } { const result: TransitionResult = typeof handler === 'function' - ? handler({ context: state.context, event }, enqueue) + ? handler({ + context: state.context, + messages: state.messages, + input: getInput(state.value, state.input), + event, + }, enqueue) : handler; return { @@ -322,6 +349,7 @@ export function createAgentMachine( const trans = sc.onDone({ result: validatedResult, context: state.context, + messages: state.messages, }); return { @@ -338,6 +366,17 @@ export function createAgentMachine( return { next: { ...state, status: 'pending' }, emitted }; } + if (isAlwaysEventType(state.value, event.type)) { + if (!sc.always) { + throw new Error(`No always transition in state '${state.value}'`); + } + + return resolveHandlerResult( + sc.always, + state.status + ); + } + if (isErrorInvokeEventType(state.value, event.type)) { const internalHandler = sc.on?.[event.type]; if (internalHandler !== undefined) { @@ -452,7 +491,7 @@ export function createAgentMachine( const input = getInput(state.value, state.input); const prompt = typeof sc.prompt === 'function' - ? sc.prompt({ context: state.context, input }) + ? sc.prompt({ context: state.context, messages: state.messages, input }) : sc.prompt; try { @@ -487,6 +526,7 @@ export function createAgentMachine( const result = await sc.invoke!( { context: state.context, + messages: state.messages, input: getInput(state.value, state.input), }, createEnqueue(onEmit) @@ -516,6 +556,13 @@ export function createAgentMachine( } const sc = resolveStateConfig(cfg, state.value); + if (sc.always) { + return { + type: `xstate.always.${state.value}`, + at: Date.now(), + }; + } + if (sc.type === 'choice') { return createChoiceEvent(state); } @@ -560,7 +607,7 @@ export function createAgentMachine( if (sc.type === 'final') { const rawOutput = sc.output - ? sc.output({ context: state.context }) + ? sc.output({ context: state.context, messages: state.messages }) : undefined; const output = cfg.schemas?.output ? validateSchemaSync(cfg.schemas.output, rawOutput) @@ -597,6 +644,7 @@ export function createAgentMachine( state: current, output: current.output, context: current.context, + messages: current.messages, }; case 'pending': return { @@ -605,6 +653,7 @@ export function createAgentMachine( value: current.value, events: getAvailableEvents(cfg, current.value), context: current.context, + messages: current.messages, }; case 'error': return { @@ -642,6 +691,7 @@ export function createAgentMachine( return { value: s.value, context: s.context, + messages: s.messages, status: s.status, sessionId: runtime.sessionId, createdAt: runtime.createdAt, diff --git a/src/persistence.test.ts b/src/persistence.test.ts index ae505e7..7cd2f17 100644 --- a/src/persistence.test.ts +++ b/src/persistence.test.ts @@ -48,6 +48,7 @@ test('loads the most replay-advanced saved snapshot', async () => { snapshot: { value: 'idle', context: { count: 1 }, + messages: [], status: 'active', createdAt: 100, sessionId: 'session-1', @@ -64,6 +65,7 @@ test('loads the most replay-advanced saved snapshot', async () => { snapshot: { value: 'done', context: { count: 2 }, + messages: [], status: 'done', createdAt: 300, sessionId: 'session-1', @@ -81,6 +83,7 @@ test('loads the most replay-advanced saved snapshot', async () => { snapshot: { value: 'done', context: { count: 2 }, + messages: [], status: 'done', createdAt: 300, sessionId: 'session-1', @@ -102,6 +105,7 @@ test('loads the most replay-advanced snapshot even if saved earlier', async () = snapshot: { value: 'done', context: { count: 5 }, + messages: [], status: 'done', createdAt: 500, sessionId: 'session-1', @@ -116,6 +120,7 @@ test('loads the most replay-advanced snapshot even if saved earlier', async () = snapshot: { value: 'review', context: { count: 2 }, + messages: [], status: 'active', createdAt: 200, sessionId: 'session-1', @@ -130,6 +135,7 @@ test('loads the most replay-advanced snapshot even if saved earlier', async () = snapshot: { value: 'done', context: { count: 5 }, + messages: [], status: 'done', createdAt: 500, sessionId: 'session-1', diff --git a/src/session-runtime.test.ts b/src/session-runtime.test.ts index 7b7d292..c8daed0 100644 --- a/src/session-runtime.test.ts +++ b/src/session-runtime.test.ts @@ -147,6 +147,49 @@ test('serializes concurrent sends so each event applies from the latest snapshot ); }); +test('journals always transitions and persists messages', async () => { + const machine = createAgentMachine({ + id: 'always-session', + context: () => ({ ready: false }), + messages: () => [{ role: 'user', content: 'start' }], + initial: 'checking', + states: { + checking: { + always: ({ messages }) => ({ + target: 'done', + context: { ready: true }, + messages: messages.concat({ role: 'assistant', content: 'done' }), + }), + }, + done: { + type: 'final', + output: ({ context, messages }) => ({ ...context, messages }), + }, + }, + }); + const store = createMemoryRunStore(); + const run = await startSession(machine, { store }); + + await vi.waitFor(() => { + expect(run.getSnapshot()).toEqual( + expect.objectContaining({ + value: 'done', + status: 'done', + context: { ready: true }, + messages: [ + { role: 'user', content: 'start' }, + { role: 'assistant', content: 'done' }, + ], + }) + ); + }); + + await expect(store.loadEvents(run.sessionId)).resolves.toEqual([ + expect.objectContaining({ sequence: 1, type: 'xstate.init' }), + expect.objectContaining({ sequence: 2, type: 'xstate.always.checking' }), + ]); +}); + test('rejects reserved internal events from run.send', async () => { const machine = createAgentMachine({ id: 'reserved-events', @@ -181,4 +224,7 @@ test('rejects reserved internal events from run.send', async () => { await expect( run.send({ type: 'xstate.error.invoke.worker' }) ).rejects.toThrow(/reserved internal event/i); + await expect( + run.send({ type: 'xstate.always.ready' }) + ).rejects.toThrow(/reserved internal event/i); }); diff --git a/src/session-types.test.ts b/src/session-types.test.ts index ea4b8a7..af88cc5 100644 --- a/src/session-types.test.ts +++ b/src/session-types.test.ts @@ -5,6 +5,7 @@ test('AgentSnapshot includes durable session fields', () => { const snapshot: AgentSnapshot<{ count: number }, 'idle'> = { value: 'idle', context: { count: 1 }, + messages: [], status: 'active', createdAt: 123, sessionId: 'session-1', diff --git a/src/types.ts b/src/types.ts index b6809f9..2967dbe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,6 +33,12 @@ export type TransitionEvent< export type EmittedPart = { type: string; [key: string]: unknown }; +export type AgentMessage = { + role: string; + content: string; + [key: string]: unknown; +}; + export interface InvokeEnqueue { emit(part: EmittedPart): void; } @@ -71,12 +77,14 @@ export type TransitionResult< | { target?: undefined; context?: Partial; + messages?: AgentMessage[]; input?: never; } | { [K in TTarget]: { target: K; context?: Partial; + messages?: AgentMessage[]; } & (K extends keyof TInputByTarget ? IsExactlyUnknown extends true ? { input?: never } @@ -90,6 +98,7 @@ export interface InitialTransitionResult< > { target: TTarget; context?: Partial; + messages?: AgentMessage[]; input?: Record; } @@ -105,17 +114,19 @@ export interface StateConfig< resultSchema?: StandardSchemaV1; invoke?: (args: { context: TContext; + messages: AgentMessage[]; input: Record; signal?: AbortSignal; }, enq: InvokeEnqueue) => Promise; - onDone?: (args: { result: any; context: TContext }) => TransitionResult; - on?: Record | ((args: { event: any; context: TContext }, enq: InvokeEnqueue) => TransitionResult)>; + onDone?: (args: { result: any; context: TContext; messages: AgentMessage[] }) => TransitionResult; + always?: TransitionResult | ((args: { context: TContext; messages: AgentMessage[]; input: Record }, enq: InvokeEnqueue) => TransitionResult); + on?: Record | ((args: { event: any; context: TContext; messages: AgentMessage[] }, enq: InvokeEnqueue) => TransitionResult)>; events?: Record; - output?: (args: { context: TContext }) => unknown; + output?: (args: { context: TContext; messages: AgentMessage[] }) => unknown; // choice-specific model?: string; adapter?: AgentAdapter; - prompt?: string | ((args: { context: TContext; input: Record }) => string); + prompt?: string | ((args: { context: TContext; messages: AgentMessage[]; input: Record }) => string); options?: Record; reasoning?: boolean; } @@ -140,6 +151,7 @@ export interface AgentState< > { value: TValue; context: TContext; + messages: AgentMessage[]; status: 'active' | 'pending' | 'done' | 'error'; input: Record>; sessionId?: string; @@ -156,8 +168,8 @@ export type ExecuteResult< TEvents extends Record = {}, TOutput = unknown, > = - | { status: 'done'; state: AgentState; output: TOutput; context: TContext } - | { status: 'pending'; state: AgentState; value: TValue; events: Record; context: TContext } + | { status: 'done'; state: AgentState; output: TOutput; context: TContext; messages: AgentMessage[] } + | { status: 'pending'; state: AgentState; value: TValue; events: Record; context: TContext; messages: AgentMessage[] } | { status: 'error'; state: AgentState; error: unknown }; // ─── Snapshot ─── @@ -169,6 +181,7 @@ export interface AgentSnapshot< > { value: TValue; context: TContext; + messages: AgentMessage[]; status: AgentState['status']; createdAt: number; sessionId: string; @@ -201,6 +214,7 @@ export interface AgentMachine< | { value: string; context: TContext; + messages?: AgentMessage[]; input?: Record>; sessionId?: string; createdAt?: number; @@ -304,6 +318,7 @@ export interface MachineConfig< output?: StandardSchemaV1; }; context: (input: TInput) => TContext; + messages?: AgentMessage[] | ((input: TInput) => AgentMessage[]); adapter?: AgentAdapter; initial: | (keyof TStates & string) diff --git a/src/utils.ts b/src/utils.ts index 2665900..b357bae 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import type { + AgentMessage, AgentState, InitialTransitionResult, MachineConfig, @@ -50,17 +51,19 @@ export type StateConfigAny = { invoke?: ( args: { context: Record; + messages: AgentMessage[]; input: Record; }, enq: { emit(part: { type: string; [key: string]: unknown }): void } ) => Promise; - onDone?: (args: { result: unknown; context: Record }) => TransitionResult; - on?: Record; context: Record }, enq: { emit(part: { type: string; [key: string]: unknown }): void }) => TransitionResult)>; - output?: (args: { context: Record }) => unknown; + onDone?: (args: { result: unknown; context: Record; messages: AgentMessage[] }) => TransitionResult; + always?: TransitionResult | ((args: { context: Record; messages: AgentMessage[]; input: Record }, enq: { emit(part: { type: string; [key: string]: unknown }): void }) => TransitionResult); + on?: Record; context: Record; messages: AgentMessage[] }, enq: { emit(part: { type: string; [key: string]: unknown }): void }) => TransitionResult)>; + output?: (args: { context: Record; messages: AgentMessage[] }) => unknown; resultSchema?: StandardSchemaV1; model?: string; adapter?: { decide: (...args: unknown[]) => Promise }; - prompt?: string | ((args: { context: Record; input: Record }) => string); + prompt?: string | ((args: { context: Record; messages: AgentMessage[]; input: Record }) => string); options?: Record; reasoning?: boolean; events?: Record; @@ -110,6 +113,10 @@ export function applyTransition( newState.context = { ...state.context, ...transition.context }; } + if (transition.messages) { + newState.messages = transition.messages; + } + if (transition.target) { newState.value = transition.target; newState.status = 'active'; @@ -204,11 +211,19 @@ export function isErrorInvokeEventType( return eventType === `xstate.error.invoke.${stateValue}`; } +export function isAlwaysEventType( + stateValue: string, + eventType: string +): boolean { + return eventType === `xstate.always.${stateValue}`; +} + export function isReservedInternalEventType(eventType: string): boolean { return ( eventType === 'xstate.init' || eventType.startsWith('xstate.done.invoke.') || eventType.startsWith('xstate.error.invoke.') + || eventType.startsWith('xstate.always.') ); } @@ -223,3 +238,22 @@ export function serializeError(error: unknown): unknown { return error; } + +export function appendMessages( + messages: readonly AgentMessage[], + ...nextMessages: AgentMessage[] +): AgentMessage[] { + return messages.concat(nextMessages); +} + +export function userMessage(content: string, extras: Record = {}): AgentMessage { + return { role: 'user', content, ...extras }; +} + +export function assistantMessage(content: string, extras: Record = {}): AgentMessage { + return { role: 'assistant', content, ...extras }; +} + +export function systemMessage(content: string, extras: Record = {}): AgentMessage { + return { role: 'system', content, ...extras }; +} diff --git a/src/xstate/index.test.ts b/src/xstate/index.test.ts index 5b00afe..af49f28 100644 --- a/src/xstate/index.test.ts +++ b/src/xstate/index.test.ts @@ -116,3 +116,37 @@ test('exports a serializable XState config for visualization', () => { }); expect(toXStateMachine(machine)).toEqual(toXStateVisualization(machine)); }); + +test('exports always transitions for visualization', () => { + const machine = createAgentMachine({ + id: 'xstate-always', + context: () => ({}), + initial: 'checking', + states: { + checking: { + always: ({ messages }) => ({ + target: 'done', + messages: messages.concat({ role: 'assistant', content: 'ok' }), + }), + }, + done: { + type: 'final', + }, + }, + }); + + expect(toXStateVisualization(machine).states.checking).toEqual({ + always: { + target: 'done', + actions: ['assignMessages'], + meta: { + agent: { + event: '', + updates: { + messages: true, + }, + }, + }, + }, + }); +}); diff --git a/src/xstate/index.ts b/src/xstate/index.ts index 14311df..9d9b40c 100644 --- a/src/xstate/index.ts +++ b/src/xstate/index.ts @@ -17,6 +17,7 @@ export interface XStateMachineConfig { export interface XStateStateConfig { type?: 'final'; on?: Record; + always?: XStateTransitionConfig | XStateTransitionConfig[]; invoke?: { id: string; src: string; @@ -43,6 +44,7 @@ export interface XStateTransitionConfig { updates?: { context?: boolean; input?: boolean; + messages?: boolean; }; }; }; @@ -90,6 +92,7 @@ export function toXStateVisualization(machine: AgentMachine): XStateMachineConfi const regularEdges = graph.edges.filter((edge) => edge.sourceId === stateId && edge.data.source !== 'invoke.done' + && edge.data.source !== 'always' ); for (const [event, edges] of groupEdgesByEvent(regularEdges)) { @@ -102,6 +105,18 @@ export function toXStateVisualization(machine: AgentMachine): XStateMachineConfi xstateState.on[event] = formatted; } + if (stateConfig.always) { + const alwaysEdges = graph.edges.filter((edge) => + edge.sourceId === stateId + && edge.data.source === 'always' + ); + + const formattedAlways = formatTransitions(alwaysEdges); + if (formattedAlways) { + xstateState.always = formattedAlways; + } + } + if (stateConfig.onDone) { const doneEdges = graph.edges.filter((edge) => edge.sourceId === stateId @@ -180,6 +195,7 @@ function formatTransition(edge: AgentGraphEdge): XStateTransitionConfig { const actions = [ ...(edge.data.actions?.context ? ['assignContext'] : []), ...(edge.data.actions?.input ? ['assignInput'] : []), + ...(edge.data.actions?.messages ? ['assignMessages'] : []), ]; return { @@ -194,6 +210,7 @@ function formatTransition(edge: AgentGraphEdge): XStateTransitionConfig { updates: { ...(edge.data.actions.context ? { context: true } : {}), ...(edge.data.actions.input ? { input: true } : {}), + ...(edge.data.actions.messages ? { messages: true } : {}), }, } : {}), From c169a3dfb12dc57d2c37a7fd8aca9f7cf42ab908 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 17 May 2026 21:09:49 +0100 Subject: [PATCH 34/34] Add generative state schemas --- examples/README.md | 1 + examples/_run.ts | 28 +- examples/adapter.ts | 15 +- examples/ai-sdk.ts | 24 +- examples/branching.ts | 12 +- examples/chatbot-messages.ts | 8 +- examples/chatbot.ts | 20 +- examples/classify.ts | 10 +- examples/conditional-subflow.ts | 27 +- examples/content-creator-flow.ts | 14 +- examples/customer-service-sim.ts | 16 +- examples/decide.ts | 12 +- examples/email-auto-responder-flow.ts | 6 +- examples/email.ts | 18 +- examples/error-retry.ts | 6 +- examples/hitl.ts | 8 +- examples/http-streaming-session.ts | 6 +- examples/index.ts | 5 + examples/joke.ts | 63 +- examples/jugs.ts | 23 +- examples/lead-score-flow.ts | 14 +- examples/map-reduce.ts | 18 +- examples/meeting-assistant-flow.ts | 18 +- examples/multi-agent-network.ts | 46 +- examples/newspaper.ts | 32 +- examples/next-ai-sdk-ui.ts | 6 +- examples/persistence.ts | 6 +- examples/persistent-streaming.ts | 6 +- examples/plan-and-execute.ts | 21 +- examples/raffle.ts | 12 +- examples/rag.ts | 52 +- examples/react-agent-from-scratch.ts | 33 +- examples/reflection.ts | 20 +- examples/rewoo.ts | 23 +- examples/river-crossing.ts | 25 +- examples/self-evaluation-loop-flow.ts | 18 +- examples/simple.ts | 23 +- examples/spec-agent-loop.ts | 20 +- examples/sql-agent.ts | 27 +- examples/subflow.ts | 18 +- examples/supervisor.ts | 31 +- examples/tool-calling.ts | 6 +- examples/tutor.ts | 14 +- examples/workflow-guardrails.ts | 363 +++++++++++ examples/write-a-book-flow.ts | 20 +- readme.md | 2 +- src/adapter.ts | 2 +- src/agent.test.ts | 564 ++++++++++++++---- src/ai-sdk/index.test.ts | 24 +- src/ai-sdk/index.ts | 36 +- src/decide.ts | 6 +- src/examples.test.ts | 76 ++- src/graph/index.test.ts | 7 +- src/http/index.test.ts | 8 +- src/index.ts | 6 + src/invoke-events.test.ts | 8 +- src/langgraph-equivalents/branching.test.ts | 14 +- src/langgraph-equivalents/graph.test.ts | 40 +- src/langgraph-equivalents/hitl.test.ts | 6 +- src/langgraph-equivalents/map-reduce.test.ts | 18 +- src/langgraph-equivalents/persistence.test.ts | 6 +- src/langgraph-equivalents/rag.test.ts | 11 +- src/langgraph-equivalents/streaming.test.ts | 6 +- src/langgraph-equivalents/subflow.test.ts | 18 +- .../tool-calling.test.ts | 6 +- src/machine.ts | 302 +++++++--- src/restore.test.ts | 6 +- src/session-runtime.test.ts | 6 +- src/streaming.test.ts | 12 +- src/target-types.assert.ts | 20 +- src/types.ts | 77 ++- src/utils.ts | 48 +- src/xstate/index.test.ts | 7 +- src/xstate/index.ts | 4 - 74 files changed, 1799 insertions(+), 741 deletions(-) create mode 100644 examples/workflow-guardrails.ts diff --git a/examples/README.md b/examples/README.md index 724fc56..09f186e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,6 +39,7 @@ These focus on real orchestration patterns: - [`meeting-assistant-flow.ts`](/Users/davidkpiano/Code/agent/examples/meeting-assistant-flow.ts) - [`self-evaluation-loop-flow.ts`](/Users/davidkpiano/Code/agent/examples/self-evaluation-loop-flow.ts) - [`spec-agent-loop.ts`](/Users/davidkpiano/Code/agent/examples/spec-agent-loop.ts) +- [`workflow-guardrails.ts`](/Users/davidkpiano/Code/agent/examples/workflow-guardrails.ts) - [`write-a-book-flow.ts`](/Users/davidkpiano/Code/agent/examples/write-a-book-flow.ts) - [`plan-and-execute.ts`](/Users/davidkpiano/Code/agent/examples/plan-and-execute.ts) - [`reflection.ts`](/Users/davidkpiano/Code/agent/examples/reflection.ts) diff --git a/examples/_run.ts b/examples/_run.ts index 510f2af..f632bf0 100644 --- a/examples/_run.ts +++ b/examples/_run.ts @@ -8,6 +8,7 @@ import { pathToFileURL } from 'node:url'; import { z } from 'zod'; import type { AgentAdapter, + DecideAdapter, ExecuteResult, StandardSchemaV1, } from '../src/index.js'; @@ -98,7 +99,7 @@ export function formatResult(result: ExecuteResult) { }; } -export function createOpenAiDecisionAdapter(): AgentAdapter { +export function createOpenAiDecisionAdapter(): DecideAdapter { return { async decide({ model, prompt, options, reasoning }) { const optionKeys = Object.keys(options); @@ -106,7 +107,7 @@ export function createOpenAiDecisionAdapter(): AgentAdapter { const allSchemaLess = Object.values(options).every((option) => !option.schema); if (allSchemaLess && !reasoning) { - const choiceResult = await generateText({ + const choiceResult = await generateText({ model: createExampleModel(model), system: [ 'Choose exactly one option.', @@ -173,6 +174,29 @@ export function createOpenAiDecisionAdapter(): AgentAdapter { }; } +export function createOpenAiGenerationAdapter(): AgentAdapter { + return { + async generateText({ model, system, prompt, messages, outputSchema }) { + const result = await generateText({ + model: createExampleModel(model), + system, + prompt, + messages: messages as any, + ...(outputSchema + ? { + output: Output.object({ + schema: toZodSchema(outputSchema), + }), + } + : {}), + }); + + const output = result as { output?: unknown; text?: string }; + return output.output ?? output.text ?? result; + }, + }; +} + export async function generateExampleObject(options: { schema: StandardSchemaV1; prompt: string; diff --git a/examples/adapter.ts b/examples/adapter.ts index a88e6c1..02f368a 100644 --- a/examples/adapter.ts +++ b/examples/adapter.ts @@ -1,10 +1,9 @@ import { z } from 'zod'; import { - createAdapter, createAgentMachine, decide, decideResultSchema, - type AgentAdapter, + type DecideAdapter, } from '../src/index.js'; import { closePrompt, @@ -15,9 +14,7 @@ import { } from './_run.js'; export function createAdapterExample( - adapter: AgentAdapter = createAdapter({ - decide: createOpenAiDecisionAdapter().decide, - }) + adapter: DecideAdapter = createOpenAiDecisionAdapter() ) { const routeOptions = { billing: { @@ -47,7 +44,7 @@ export function createAdapterExample( initial: 'route', states: { route: { - resultSchema: decideResultSchema(routeOptions), + schemas: { output: decideResultSchema(routeOptions) }, invoke: async ({ context }) => decide({ adapter, @@ -62,12 +59,12 @@ export function createAdapterExample( options: routeOptions, reasoning: false, }), - onDone: ({ result }) => { + onDone: ({ output }) => { return { target: 'done', context: { - route: result.choice, - confidence: result.data.confidence, + route: output.choice, + confidence: output.data.confidence, }, }; }, diff --git a/examples/ai-sdk.ts b/examples/ai-sdk.ts index c60f730..b83b170 100644 --- a/examples/ai-sdk.ts +++ b/examples/ai-sdk.ts @@ -4,9 +4,9 @@ import { createAgentMachine, decide, decideResultSchema, - type AgentAdapter, + type DecideAdapter, } from '../src/index.js'; -import { createAiSdkAdapter } from '../src/ai-sdk/index.js'; +import { createAiSdkDecisionAdapter } from '../src/ai-sdk/index.js'; import { closePrompt, createExampleModel, @@ -38,7 +38,7 @@ const replySchema = z.object({ type Route = keyof typeof routeOptions; export function createAiSdkExample(options: { - adapter?: AgentAdapter; + adapter?: DecideAdapter; draftReply?: (args: { route: Route; confidence: number; @@ -47,7 +47,7 @@ export function createAiSdkExample(options: { } = {}) { const adapter = options.adapter ?? - createAiSdkAdapter({ + createAiSdkDecisionAdapter({ resolveModel: (model) => createExampleModel(model), }); @@ -100,7 +100,7 @@ export function createAiSdkExample(options: { initial: 'route', states: { route: { - resultSchema: decideResultSchema(routeOptions), + schemas: { output: decideResultSchema(routeOptions) }, invoke: async ({ context }) => decide({ adapter, @@ -112,27 +112,27 @@ export function createAiSdkExample(options: { ].join('\n'), options: routeOptions, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'drafting', context: { - route: result.choice, - confidence: result.data.confidence, + route: output.choice, + confidence: output.data.confidence, }, }), }, drafting: { - resultSchema: replySchema, + schemas: { output: replySchema }, invoke: async ({ context }) => draftReply({ route: context.route ?? 'support', confidence: context.confidence ?? 0, message: context.message, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', context: { - subject: result.subject, - body: result.body, + subject: output.subject, + body: output.body, }, }), }, diff --git a/examples/branching.ts b/examples/branching.ts index d50d78d..3f89c88 100644 --- a/examples/branching.ts +++ b/examples/branching.ts @@ -52,7 +52,7 @@ export function createBranchingExample( initial: 'analyzing', states: { analyzing: { - resultSchema: branchResultSchema, + schemas: { output: branchResultSchema }, invoke: async ({ context }) => { const [docs, issues, code] = await Promise.all([ (options.analyzeDocs @@ -77,13 +77,13 @@ export function createBranchingExample( return { docs, issues, code }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'summarizing', - context: result, + context: output, }), }, summarizing: { - resultSchema: summarySchema, + schemas: { output: summarySchema }, invoke: async ({ context }) => (options.summarize ?? (({ docs, issues, code }) => @@ -104,9 +104,9 @@ export function createBranchingExample( issues: context.issues ?? '', code: context.code ?? '', }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { summary: result.summary }, + context: { summary: output.summary }, }), }, done: { diff --git a/examples/chatbot-messages.ts b/examples/chatbot-messages.ts index 7c5cd80..fa6e2ca 100644 --- a/examples/chatbot-messages.ts +++ b/examples/chatbot-messages.ts @@ -70,13 +70,13 @@ export function createChatbotMessagesExample( }, }, replying: { - resultSchema: replySchema, + schemas: { output: replySchema }, invoke: async ({ messages }) => reply(messages), - onDone: ({ result, messages }) => ({ + onDone: ({ output, messages }) => ({ target: 'waitingForUser', - messages: messages.concat(result.message), + messages: messages.concat(output.message), context: { - finalMessage: result.message, + finalMessage: output.message, }, }), }, diff --git a/examples/chatbot.ts b/examples/chatbot.ts index 8f53f30..b062664 100644 --- a/examples/chatbot.ts +++ b/examples/chatbot.ts @@ -5,7 +5,7 @@ import { decide, decideResultSchema, startSession, - type AgentAdapter, + type DecideAdapter, } from '../src/index.js'; import { closePrompt, @@ -22,7 +22,7 @@ const replySchema = z.object({ export function createChatbotExample( options: { - adapter?: AgentAdapter; + adapter?: DecideAdapter; reply?: (transcript: string[]) => Promise>; } = {} ) { @@ -85,7 +85,7 @@ export function createChatbotExample( }, }, deciding: { - resultSchema: decideResultSchema(decisionOptions), + schemas: { output: decideResultSchema(decisionOptions) }, invoke: async ({ context }) => decide({ adapter, @@ -98,19 +98,19 @@ export function createChatbotExample( ].join('\n'), options: decisionOptions, }), - onDone: ({ result }) => ({ - target: result.choice === 'end' ? 'done' : 'replying', - context: result.choice === 'end' ? { ended: true } : {}, + onDone: ({ output }) => ({ + target: output.choice === 'end' ? 'done' : 'replying', + context: output.choice === 'end' ? { ended: true } : {}, }), }, replying: { - resultSchema: replySchema, + schemas: { output: replySchema }, invoke: async ({ context }) => reply(context.transcript), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'listening', context: { - lastAssistantMessage: result.response, - transcript: [...context.transcript, `Assistant: ${result.response}`], + lastAssistantMessage: output.response, + transcript: [...context.transcript, `Assistant: ${output.response}`], }, }), }, diff --git a/examples/classify.ts b/examples/classify.ts index eb4eaab..dcbc96a 100644 --- a/examples/classify.ts +++ b/examples/classify.ts @@ -3,7 +3,7 @@ import { createAgentMachine, classify, classifyResultSchema, - type AgentAdapter, + type DecideAdapter, } from '../src/index.js'; import { closePrompt, @@ -14,7 +14,7 @@ import { } from './_run.js'; export function createClassifyExample( - adapter: AgentAdapter = createOpenAiDecisionAdapter() + adapter: DecideAdapter = createOpenAiDecisionAdapter() ) { const categories = { billing: { description: 'Payments, invoices, refunds, and charges.' }, @@ -35,7 +35,7 @@ export function createClassifyExample( initial: 'routing', states: { routing: { - resultSchema: classifyResultSchema(categories), + schemas: { output: classifyResultSchema(categories) }, invoke: async ({ context }) => classify({ adapter, @@ -43,9 +43,9 @@ export function createClassifyExample( prompt: `Classify this support request:\n\n${context.request}`, into: categories, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { category: result.category }, + context: { category: output.category }, }), }, done: { diff --git a/examples/conditional-subflow.ts b/examples/conditional-subflow.ts index 52793f0..391adbc 100644 --- a/examples/conditional-subflow.ts +++ b/examples/conditional-subflow.ts @@ -40,7 +40,7 @@ export function createConditionalSubflowExample( initial: 'researching', states: { researching: { - resultSchema: researchSchema, + schemas: { output: researchSchema }, invoke: async ({ context }) => (options.research ?? ((topic) => @@ -49,9 +49,9 @@ export function createConditionalSubflowExample( system: 'Return concise research bullets.', prompt: `Return 2 to 4 bullets about ${topic}.`, })))(context.topic), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { bullets: result.bullets }, + context: { bullets: output.bullets }, }), }, done: { @@ -78,7 +78,7 @@ export function createConditionalSubflowExample( initial: 'drafting', states: { drafting: { - resultSchema: draftSchema, + schemas: { output: draftSchema }, invoke: async ({ context }) => (options.draft ?? (({ topic, bullets }) => @@ -94,9 +94,9 @@ export function createConditionalSubflowExample( topic: context.topic, bullets: context.bullets, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { draft: result.draft }, + context: { draft: output.draft }, }), }, done: { @@ -132,7 +132,7 @@ export function createConditionalSubflowExample( : { target: 'drafting', input: { bullets: context.bullets } }, states: { researching: { - resultSchema: researchSchema, + schemas: { output: researchSchema }, invoke: async ({ context }) => { const result = await researchMachine.execute( researchMachine.getInitialState({ topic: context.topic }) @@ -144,16 +144,15 @@ export function createConditionalSubflowExample( return result.output; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { bullets: result.bullets }, + context: { bullets: output.bullets }, }), }, drafting: { - inputSchema: z.object({ + schemas: { input: z.object({ bullets: z.array(z.string()), - }), - resultSchema: draftSchema, + }), output: draftSchema }, invoke: async ({ context, input }) => { const result = await draftMachine.execute( draftMachine.getInitialState({ @@ -168,9 +167,9 @@ export function createConditionalSubflowExample( return result.output; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { draft: result.draft }, + context: { draft: output.draft }, }), }, done: { diff --git a/examples/content-creator-flow.ts b/examples/content-creator-flow.ts index efc483d..b6d579d 100644 --- a/examples/content-creator-flow.ts +++ b/examples/content-creator-flow.ts @@ -83,17 +83,17 @@ export function createContentCreatorFlowExample(options: { initial: 'routing', states: { routing: { - resultSchema: routeSchema, + schemas: { output: routeSchema }, invoke: async ({ context }) => routeRequest(context.request), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'creating', context: { - route: result.route, + route: output.route, }, }), }, creating: { - resultSchema: contentSchema, + schemas: { output: contentSchema }, invoke: async ({ context }) => { switch (context.route) { case 'linkedin': @@ -105,11 +105,11 @@ export function createContentCreatorFlowExample(options: { return createBlog(context.request); } }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', context: { - title: result.title, - body: result.body, + title: output.title, + body: output.body, }, }), }, diff --git a/examples/customer-service-sim.ts b/examples/customer-service-sim.ts index 3bade57..a7f1e2a 100644 --- a/examples/customer-service-sim.ts +++ b/examples/customer-service-sim.ts @@ -87,12 +87,12 @@ export function createCustomerServiceSimExample( initial: 'service', states: { service: { - resultSchema: serviceReplySchema, + schemas: { output: serviceReplySchema }, invoke: async ({ context }) => serviceReply(context), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: context.turnCount + 1 >= context.maxTurns ? 'done' : 'customer', context: { - transcript: [...context.transcript, `Agent: ${result.response}`], + transcript: [...context.transcript, `Agent: ${output.response}`], outcome: context.turnCount + 1 >= context.maxTurns ? 'max-turns-reached' @@ -101,14 +101,14 @@ export function createCustomerServiceSimExample( }), }, customer: { - resultSchema: customerReplySchema, + schemas: { output: customerReplySchema }, invoke: async ({ context }) => customerReply(context), - onDone: ({ result, context }) => ({ - target: result.done ? 'done' : 'service', + onDone: ({ output, context }) => ({ + target: output.done ? 'done' : 'service', context: { - transcript: [...context.transcript, `Customer: ${result.response}`], + transcript: [...context.transcript, `Customer: ${output.response}`], turnCount: context.turnCount + 1, - outcome: result.outcome, + outcome: output.outcome, }, }), }, diff --git a/examples/decide.ts b/examples/decide.ts index 723a3ef..a484150 100644 --- a/examples/decide.ts +++ b/examples/decide.ts @@ -3,7 +3,7 @@ import { createAgentMachine, decide, decideResultSchema, - type AgentAdapter, + type DecideAdapter, } from '../src/index.js'; import { closePrompt, @@ -13,7 +13,7 @@ import { prompt, } from './_run.js'; -export function createDecideExample(adapter: AgentAdapter = createOpenAiDecisionAdapter()) { +export function createDecideExample(adapter: DecideAdapter = createOpenAiDecisionAdapter()) { const triageOptions = { reply: { description: 'Reply directly to the customer.', @@ -46,7 +46,7 @@ export function createDecideExample(adapter: AgentAdapter = createOpenAiDecision initial: 'triage', states: { triage: { - resultSchema: decideResultSchema(triageOptions), + schemas: { output: decideResultSchema(triageOptions) }, invoke: async ({ context }) => decide({ adapter, @@ -59,11 +59,11 @@ export function createDecideExample(adapter: AgentAdapter = createOpenAiDecision ].join('\n'), options: triageOptions, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', context: { - action: result.choice, - payload: result.data, + action: output.choice, + payload: output.data, }, }), }, diff --git a/examples/email-auto-responder-flow.ts b/examples/email-auto-responder-flow.ts index 8ab30ea..2d1bf85 100644 --- a/examples/email-auto-responder-flow.ts +++ b/examples/email-auto-responder-flow.ts @@ -111,14 +111,14 @@ export function createEmailAutoResponderFlowExample( target: 'done', }, }, - resultSchema: draftResponseSchema, + schemas: { output: draftResponseSchema }, invoke: async ({ context }) => createDraft(context.currentEmail!), - onDone: ({ result, context }) => { + onDone: ({ output, context }) => { const currentEmail = context.currentEmail!; const processedIds = [...context.processedIds, currentEmail.id]; const drafts = { ...context.drafts, - [currentEmail.id]: result.draft, + [currentEmail.id]: output.draft, }; const [nextEmail, ...queue] = context.queue; diff --git a/examples/email.ts b/examples/email.ts index 972228b..391b753 100644 --- a/examples/email.ts +++ b/examples/email.ts @@ -5,7 +5,7 @@ import { decide, decideResultSchema, startSession, - type AgentAdapter, + type DecideAdapter, } from '../src/index.js'; import { closePrompt, @@ -29,7 +29,7 @@ type EmailTools = { export function createEmailExample( options: { - adapter?: AgentAdapter; + adapter?: DecideAdapter; tools?: Partial; compose?: ( input: { @@ -146,7 +146,7 @@ export function createEmailExample( initial: 'checking', states: { checking: { - resultSchema: decideResultSchema(checkingOptions), + schemas: { output: decideResultSchema(checkingOptions) }, invoke: async ({ context }) => decide({ adapter, @@ -161,14 +161,14 @@ export function createEmailExample( ].join('\n'), options: checkingOptions, }), - onDone: ({ result, context }) => { + onDone: ({ output, context }) => { if ( - result.choice === 'askForClarification' + output.choice === 'askForClarification' && context.clarifications.length === 0 ) { return { target: 'clarifying', - context: { questions: result.data.questions }, + context: { questions: output.data.questions }, }; } @@ -190,7 +190,7 @@ export function createEmailExample( }, }, drafting: { - resultSchema: draftSchema, + schemas: { output: draftSchema }, invoke: async ({ context }) => { const contactName = await tools.lookupContactName(context.email); const availability = await tools.lookupAvailability(); @@ -205,9 +205,9 @@ export function createEmailExample( signature, }); }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { replyEmail: result.replyEmail }, + context: { replyEmail: output.replyEmail }, }), }, done: { diff --git a/examples/error-retry.ts b/examples/error-retry.ts index dba62e9..ce422f6 100644 --- a/examples/error-retry.ts +++ b/examples/error-retry.ts @@ -56,16 +56,16 @@ export function createErrorRetryExample( initial: 'answering', states: { answering: { - resultSchema: answerSchema, + schemas: { output: answerSchema }, invoke: async ({ context }) => answer({ question: context.question, attempt: context.attempt, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', context: { - answer: result.answer, + answer: output.answer, }, }), on: { diff --git a/examples/hitl.ts b/examples/hitl.ts index 919d894..9d80bf2 100644 --- a/examples/hitl.ts +++ b/examples/hitl.ts @@ -64,16 +64,16 @@ export function createHitlExample( }, }, drafting: { - resultSchema: draftSchema, + schemas: { output: draftSchema }, invoke: async ({ context, messages }) => draftReply({ task: context.task, messages, }), - onDone: ({ result, messages }) => ({ + onDone: ({ output, messages }) => ({ target: 'done', - messages: messages.concat({ role: 'assistant', content: result.draft }), - context: { draft: result.draft }, + messages: messages.concat({ role: 'assistant', content: output.draft }), + context: { draft: output.draft }, }), }, done: { diff --git a/examples/http-streaming-session.ts b/examples/http-streaming-session.ts index 736ba97..2f2a394 100644 --- a/examples/http-streaming-session.ts +++ b/examples/http-streaming-session.ts @@ -47,14 +47,14 @@ export function createStreamingSessionHttpController(options: { initial: 'writing', states: { writing: { - resultSchema: streamingOutputSchema, + schemas: { output: streamingOutputSchema }, invoke: async ({ context }, enq) => streamer.streamText(context.streamId, context.text, (delta) => { enq.emit({ type: 'textPart', delta }); }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { finalText: result.text }, + context: { finalText: output.text }, }), }, done: { diff --git a/examples/index.ts b/examples/index.ts index d0b02a4..c73aafb 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -64,6 +64,11 @@ export { createRewooExample } from './rewoo.js'; export { createReflectionExample } from './reflection.js'; export { createSelfEvaluationLoopFlowExample } from './self-evaluation-loop-flow.js'; export { createSpecAgentLoopExample } from './spec-agent-loop.js'; +export { + createGuardrailedBugfixWorkflowExample, + createGuardrailedIncidentResponseExample, + createUnguardedIncidentResponseExample, +} from './workflow-guardrails.js'; export { createSupervisorExample } from './supervisor.js'; export { createWriteABookFlowExample } from './write-a-book-flow.js'; export { createSqlAgentExample } from './sql-agent.js'; diff --git a/examples/joke.ts b/examples/joke.ts index fc4bdaf..012bcf1 100644 --- a/examples/joke.ts +++ b/examples/joke.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { createAgentMachine } from '../src/index.js'; +import { createAgentMachine, type AgentAdapter } from '../src/index.js'; import { closePrompt, + createOpenAiGenerationAdapter, formatResult, - generateExampleObject, isMain, prompt, } from './_run.js'; @@ -18,38 +18,11 @@ const ratingSchema = z.object({ }); export function createJokeExample( - options: { - tellJoke?: (topic: string) => Promise>; - rateJoke?: ( - topic: string, - joke: string - ) => Promise>; - } = {} + adapter: AgentAdapter = createOpenAiGenerationAdapter() ) { - const tellJoke = - options.tellJoke ?? - ((topic: string) => - generateExampleObject({ - schema: jokeSchema, - system: 'You write short, clean jokes.', - prompt: `Write one short joke about ${topic}.`, - })); - const rateJoke = - options.rateJoke ?? - ((topic: string, joke: string) => - generateExampleObject({ - schema: ratingSchema, - system: 'You are a joke critic. Be fair and concise.', - prompt: [ - `Topic: ${topic}`, - `Joke: ${joke}`, - '', - 'Rate the joke from 1 to 10 and explain briefly.', - ].join('\n'), - })); - return createAgentMachine({ id: 'joke-example', + adapter, schemas: { input: z.object({ topic: z.string() }), output: z.object({ @@ -70,22 +43,30 @@ export function createJokeExample( initial: 'telling', states: { telling: { - resultSchema: jokeSchema, - invoke: async ({ context }) => tellJoke(context.topic), - onDone: ({ result }) => ({ + schemas: { output: jokeSchema }, + system: 'You write short, clean jokes.', + prompt: ({ context }) => `Write one short joke about ${context.topic}.`, + onDone: ({ output }) => ({ target: 'rating', - context: { joke: result.joke }, + context: { joke: output.joke }, }), }, rating: { - resultSchema: ratingSchema, - invoke: async ({ context }) => rateJoke(context.topic, context.joke ?? ''), - onDone: ({ result }) => ({ + schemas: { output: ratingSchema }, + system: 'You are a joke critic. Be fair and concise.', + prompt: ({ context }) => + [ + `Topic: ${context.topic}`, + `Joke: ${context.joke ?? ''}`, + '', + 'Rate the joke from 1 to 10 and explain briefly.', + ].join('\n'), + onDone: ({ output }) => ({ target: 'done', context: { - rating: result.rating, - explanation: result.explanation, - accepted: result.rating >= 7, + rating: output.rating, + explanation: output.explanation, + accepted: output.rating >= 7, }, }), }, diff --git a/examples/jugs.ts b/examples/jugs.ts index ae68c7e..dfce867 100644 --- a/examples/jugs.ts +++ b/examples/jugs.ts @@ -73,12 +73,12 @@ export function createJugsExample() { initial: 'choosing', states: { choosing: { - resultSchema: moveSchema, + schemas: { output: moveSchema }, invoke: async ({ context }) => chooseWaterJugMove(context.jug3, context.jug5), - onDone: ({ result, context }) => { - const nextReasoning = [...context.reasoning, result.reasoning]; + onDone: ({ output, context }) => { + const nextReasoning = [...context.reasoning, output.reasoning]; - if (result.move === 'done') { + if (output.move === 'done') { return { target: 'done' as const, context: { reasoning: nextReasoning }, @@ -87,28 +87,27 @@ export function createJugsExample() { return { target: 'applying' as const, - input: { move: result.move }, + input: { move: output.move }, context: { reasoning: nextReasoning }, }; }, }, applying: { - inputSchema: z.object({ + schemas: { input: z.object({ move: moveSchema.shape.move.exclude(['done']), - }), - resultSchema: applySchema, + }), output: applySchema }, invoke: async ({ context, input }) => applyWaterJugMove( context.jug3, context.jug5, input.move as 'fill5' | 'pour5to3' | 'empty3' ), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'choosing', context: { - jug3: result.jug3, - jug5: result.jug5, - steps: [...context.steps, result.step], + jug3: output.jug3, + jug5: output.jug5, + steps: [...context.steps, output.step], }, }), }, diff --git a/examples/lead-score-flow.ts b/examples/lead-score-flow.ts index 459f643..c0c6879 100644 --- a/examples/lead-score-flow.ts +++ b/examples/lead-score-flow.ts @@ -97,17 +97,17 @@ export function createLeadScoreFlowExample(options: { initial: 'scoring', states: { scoring: { - resultSchema: scoringSchema, + schemas: { output: scoringSchema }, invoke: async ({ context }) => scoreLeads({ leads: context.leads, reviewNote: context.reviewNote, }), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'reviewing', context: { - scoredLeads: result.scoredLeads, - topLeads: result.scoredLeads.slice(0, 3), + scoredLeads: output.scoredLeads, + topLeads: output.scoredLeads.slice(0, 3), reviewNote: null, reviewCount: context.reviewCount + 1, }, @@ -127,12 +127,12 @@ export function createLeadScoreFlowExample(options: { }, }, writing: { - resultSchema: emailBatchSchema, + schemas: { output: emailBatchSchema }, invoke: async ({ context }) => writeEmails(context.scoredLeads), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', context: { - emailDrafts: result.drafts, + emailDrafts: output.drafts, }, }), }, diff --git a/examples/map-reduce.ts b/examples/map-reduce.ts index 2f6c255..25abe38 100644 --- a/examples/map-reduce.ts +++ b/examples/map-reduce.ts @@ -47,7 +47,7 @@ export function createMapReduceExample( initial: 'planning', states: { planning: { - resultSchema: subjectsSchema, + schemas: { output: subjectsSchema }, invoke: async ({ context }) => (options.planSubjects ?? ((topic) => @@ -56,13 +56,13 @@ export function createMapReduceExample( system: 'You break a topic into a few concrete subtopics.', prompt: `List 2 to 4 specific subtopics worth covering for: ${topic}`, })))(context.topic), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'mapping', - context: { subjects: result.subjects }, + context: { subjects: output.subjects }, }), }, mapping: { - resultSchema: jokesSchema, + schemas: { output: jokesSchema }, invoke: async ({ context }) => { const jokes = await Promise.all( context.subjects.map((subject) => @@ -77,13 +77,13 @@ export function createMapReduceExample( return { jokes }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'reducing', - context: { jokes: result.jokes }, + context: { jokes: output.jokes }, }), }, reducing: { - resultSchema: bestJokeSchema, + schemas: { output: bestJokeSchema }, invoke: async ({ context }) => (options.chooseBest ?? ((jokes) => @@ -92,9 +92,9 @@ export function createMapReduceExample( system: 'You pick the strongest joke from a list.', prompt: ['Choose the best joke from this list:', ...jokes].join('\n'), })))(context.jokes), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { bestJoke: result.bestJoke }, + context: { bestJoke: output.bestJoke }, }), }, done: { diff --git a/examples/meeting-assistant-flow.ts b/examples/meeting-assistant-flow.ts index b55e26b..ae8171e 100644 --- a/examples/meeting-assistant-flow.ts +++ b/examples/meeting-assistant-flow.ts @@ -85,18 +85,18 @@ export function createMeetingAssistantFlowExample(options: { initial: 'extracting', states: { extracting: { - resultSchema: extractionSchema, + schemas: { output: extractionSchema }, invoke: async ({ context }) => extractTasks(context.notes), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'dispatching', context: { - summary: result.summary, - tasks: result.tasks, + summary: output.summary, + tasks: output.tasks, }, }), }, dispatching: { - resultSchema: fanOutSchema, + schemas: { output: fanOutSchema }, invoke: async ({ context }) => { const [trello, csv, slack] = await Promise.all([ addTasksToTrello(context.tasks), @@ -113,12 +113,12 @@ export function createMeetingAssistantFlowExample(options: { slackMessageId: slack.slackMessageId, }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', context: { - trelloCardIds: result.trelloCardIds, - csvPath: result.csvPath, - slackMessageId: result.slackMessageId, + trelloCardIds: output.trelloCardIds, + csvPath: output.csvPath, + slackMessageId: output.slackMessageId, }, }), }, diff --git a/examples/multi-agent-network.ts b/examples/multi-agent-network.ts index 65c0501..da5533d 100644 --- a/examples/multi-agent-network.ts +++ b/examples/multi-agent-network.ts @@ -3,7 +3,7 @@ import { createAgentMachine, decide, decideResultSchema, - type AgentAdapter, + type DecideAdapter, } from '../src/index.js'; import { closePrompt, @@ -42,7 +42,7 @@ const draftHandoffSchema = z.object({ export function createMultiAgentNetworkExample( options: { - adapter?: AgentAdapter; + adapter?: DecideAdapter; research?: (args: { topic: string; focus: string; @@ -120,15 +120,15 @@ export function createMultiAgentNetworkExample( initial: 'researching', states: { researching: { - resultSchema: researchNotesSchema, + schemas: { output: researchNotesSchema }, invoke: async ({ context }) => research({ topic: context.topic, focus: context.focus, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { notes: result.notes }, + context: { notes: output.notes }, }), }, done: { @@ -161,16 +161,16 @@ export function createMultiAgentNetworkExample( initial: 'writing', states: { writing: { - resultSchema: draftSchema, + schemas: { output: draftSchema }, invoke: async ({ context }) => write({ topic: context.topic, notes: context.notes, angle: context.angle, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { draft: result.draft }, + context: { draft: output.draft }, }), }, done: { @@ -202,7 +202,7 @@ export function createMultiAgentNetworkExample( initial: 'coordinating', states: { coordinating: { - resultSchema: decideResultSchema(coordinatorOptions), + schemas: { output: decideResultSchema(coordinatorOptions) }, invoke: async ({ context }) => decide({ adapter, @@ -224,21 +224,21 @@ export function createMultiAgentNetworkExample( ].join('\n'), options: coordinatorOptions, }), - onDone: ({ result }) => { - if (result.choice === 'research') { + onDone: ({ output }) => { + if (output.choice === 'research') { return { target: 'researching', input: { - focus: result.data.focus ?? 'gather the most useful supporting facts', + focus: output.data.focus ?? 'gather the most useful supporting facts', }, }; } - if (result.choice === 'write') { + if (output.choice === 'write') { return { target: 'writing', input: { - angle: result.data.angle ?? 'produce the clearest concise draft', + angle: output.data.angle ?? 'produce the clearest concise draft', }, }; } @@ -249,8 +249,7 @@ export function createMultiAgentNetworkExample( }, }, researching: { - inputSchema: researchParamsSchema, - resultSchema: researchHandoffSchema, + schemas: { input: researchParamsSchema, output: researchHandoffSchema }, invoke: async ({ context, input }) => { const result = await researchAgent.execute( researchAgent.getInitialState({ @@ -268,17 +267,16 @@ export function createMultiAgentNetworkExample( handoff: `researcher:${input.focus}`, }; }, - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'coordinating', context: { - notes: result.notes, - handoffs: [...context.handoffs, result.handoff], + notes: output.notes, + handoffs: [...context.handoffs, output.handoff], }, }), }, writing: { - inputSchema: writeParamsSchema, - resultSchema: draftHandoffSchema, + schemas: { input: writeParamsSchema, output: draftHandoffSchema }, invoke: async ({ context, input }) => { const result = await writerAgent.execute( writerAgent.getInitialState({ @@ -297,11 +295,11 @@ export function createMultiAgentNetworkExample( handoff: `writer:${input.angle}`, }; }, - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'coordinating', context: { - draft: result.draft, - handoffs: [...context.handoffs, result.handoff], + draft: output.draft, + handoffs: [...context.handoffs, output.handoff], }, }), }, diff --git a/examples/newspaper.ts b/examples/newspaper.ts index ec74eb3..7734598 100644 --- a/examples/newspaper.ts +++ b/examples/newspaper.ts @@ -111,49 +111,49 @@ export function createNewspaperExample( initial: 'searching', states: { searching: { - resultSchema: searchSchema, + schemas: { output: searchSchema }, invoke: async ({ context }) => search(context.topic), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'curating', - context: { searchResults: result.searchResults }, + context: { searchResults: output.searchResults }, }), }, curating: { - resultSchema: searchSchema, + schemas: { output: searchSchema }, invoke: async ({ context }) => curate(context.topic, context.searchResults), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'writing', - context: { searchResults: result.searchResults }, + context: { searchResults: output.searchResults }, }), }, writing: { - resultSchema: articleSchema, + schemas: { output: articleSchema }, invoke: async ({ context }) => write(context.topic, context.searchResults), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'critiquing', - context: { article: result.article }, + context: { article: output.article }, }), }, critiquing: { - resultSchema: critiqueSchema, + schemas: { output: critiqueSchema }, invoke: async ({ context }) => critique(context.article ?? '', context.revisionCount), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: - !result.critique || context.revisionCount >= context.maxRevisions + !output.critique || context.revisionCount >= context.maxRevisions ? 'done' : 'revising', - context: { critique: result.critique }, + context: { critique: output.critique }, }), }, revising: { - resultSchema: articleSchema, + schemas: { output: articleSchema }, invoke: async ({ context }) => revise(context.article ?? '', context.critique ?? ''), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'critiquing', context: { - article: result.article, + article: output.article, revisionCount: context.revisionCount + 1, }, }), diff --git a/examples/next-ai-sdk-ui.ts b/examples/next-ai-sdk-ui.ts index 8fab1d7..ccc2a31 100644 --- a/examples/next-ai-sdk-ui.ts +++ b/examples/next-ai-sdk-ui.ts @@ -98,7 +98,7 @@ export function createNextAiSdkUiRoute(options: { }, }, drafting: { - resultSchema: streamedTextSchema, + schemas: { output: streamedTextSchema }, invoke: async ({ context }, enq) => { enq.emit({ type: 'notification', @@ -122,10 +122,10 @@ export function createNextAiSdkUiRoute(options: { }, }); }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', context: { - finalText: result.text, + finalText: output.text, }, }), }, diff --git a/examples/persistence.ts b/examples/persistence.ts index b74ac48..f43c0db 100644 --- a/examples/persistence.ts +++ b/examples/persistence.ts @@ -63,15 +63,15 @@ export function createPersistenceExample( }, }, summarizing: { - resultSchema: summarySchema, + schemas: { output: summarySchema }, invoke: async ({ context }) => summarize({ request: context.request, approved: context.approved, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { summary: result.summary }, + context: { summary: output.summary }, }), }, done: { diff --git a/examples/persistent-streaming.ts b/examples/persistent-streaming.ts index 41545d6..f5f158f 100644 --- a/examples/persistent-streaming.ts +++ b/examples/persistent-streaming.ts @@ -50,14 +50,14 @@ export function createPersistentStreamingExample( initial: 'writing', states: { writing: { - resultSchema: textSchema, + schemas: { output: textSchema }, invoke: async (_args, enq) => writeText((delta) => { enq.emit({ type: 'textPart', delta }); }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { finalText: result.text }, + context: { finalText: output.text }, }), }, done: { diff --git a/examples/plan-and-execute.ts b/examples/plan-and-execute.ts index 86f7d08..49e97bc 100644 --- a/examples/plan-and-execute.ts +++ b/examples/plan-and-execute.ts @@ -98,27 +98,26 @@ export function createPlanAndExecuteExample( initial: 'planning', states: { planning: { - resultSchema: planSchema, + schemas: { output: planSchema }, invoke: async ({ context }) => planner(context.goal), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'executing', - context: { plan: result.plan }, + context: { plan: output.plan }, input: { index: 0 } }), }, executing: { - inputSchema: z.object({ + schemas: { input: z.object({ index: z.number().int().min(0), - }), - resultSchema: stepResultSchema, + }), output: stepResultSchema }, invoke: async ({ context, input }) => executeStep({ goal: context.goal, step: context.plan[input.index] ?? '', priorResults: context.stepResults, }), - onDone: ({ result, context }) => { - const nextStepResults = [...context.stepResults, result.result]; + onDone: ({ output, context }) => { + const nextStepResults = [...context.stepResults, output.result]; const nextIndex = nextStepResults.length; if (nextIndex < context.plan.length) { @@ -136,16 +135,16 @@ export function createPlanAndExecuteExample( }, }, synthesizing: { - resultSchema: finalAnswerSchema, + schemas: { output: finalAnswerSchema }, invoke: async ({ context }) => synthesize({ goal: context.goal, plan: context.plan, stepResults: context.stepResults, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { answer: result.answer }, + context: { answer: output.answer }, }), }, done: { diff --git a/examples/raffle.ts b/examples/raffle.ts index f02b3a8..b052784 100644 --- a/examples/raffle.ts +++ b/examples/raffle.ts @@ -68,15 +68,15 @@ export function createRaffleExample( }, }, drawing: { - resultSchema: winnerSchema, + schemas: { output: winnerSchema }, invoke: async ({ context }) => pickWinner(context.entries), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', context: { - winner: result.winningEntry, - firstRunnerUp: result.firstRunnerUp, - secondRunnerUp: result.secondRunnerUp, - explanation: result.explanation, + winner: output.winningEntry, + firstRunnerUp: output.firstRunnerUp, + secondRunnerUp: output.secondRunnerUp, + explanation: output.explanation, }, }), }, diff --git a/examples/rag.ts b/examples/rag.ts index abd9772..08e4376 100644 --- a/examples/rag.ts +++ b/examples/rag.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { createAgentMachine } from '../src/index.js'; +import { createAgentMachine, type AgentAdapter } from '../src/index.js'; import { closePrompt, - generateExampleObject, + createOpenAiGenerationAdapter, isMain, prompt, } from './_run.js'; @@ -22,11 +22,8 @@ const answerSchema = z.object({ export function createRagExample( options: { + adapter?: AgentAdapter; retrieve?: (question: string) => Promise>; - answer?: (args: { - question: string; - documents: Array>; - }) => Promise>; } = {} ) { const retrieve = @@ -45,25 +42,9 @@ export function createRagExample( ], })); - const answer = - options.answer ?? - ((args: { - question: string; - documents: Array>; - }) => - generateExampleObject({ - schema: answerSchema, - system: 'Answer the question using only the retrieved documents.', - prompt: [ - `Question: ${args.question}`, - '', - 'Documents:', - ...args.documents.map((document) => `- [${document.id}] ${document.content}`), - ].join('\n'), - })); - return createAgentMachine({ id: 'rag-example', + adapter: options.adapter ?? createOpenAiGenerationAdapter(), schemas: { input: z.object({ question: z.string(), @@ -82,23 +63,26 @@ export function createRagExample( initial: 'retrieving', states: { retrieving: { - resultSchema: retrievedDocumentsSchema, + schemas: { output: retrievedDocumentsSchema }, invoke: async ({ context }) => retrieve(context.question), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'answering', - context: { documents: result.documents }, + context: { documents: output.documents }, }), }, answering: { - resultSchema: answerSchema, - invoke: async ({ context }) => - answer({ - question: context.question, - documents: context.documents, - }), - onDone: ({ result }) => ({ + schemas: { output: answerSchema }, + system: 'Answer the question using only the retrieved documents.', + prompt: ({ context }) => + [ + `Question: ${context.question}`, + '', + 'Documents:', + ...context.documents.map((document) => `- [${document.id}] ${document.content}`), + ].join('\n'), + onDone: ({ output }) => ({ target: 'done', - context: { answer: result.answer }, + context: { answer: output.answer }, }), }, done: { diff --git a/examples/react-agent-from-scratch.ts b/examples/react-agent-from-scratch.ts index bd82736..c0b22c0 100644 --- a/examples/react-agent-from-scratch.ts +++ b/examples/react-agent-from-scratch.ts @@ -100,7 +100,7 @@ export function createReactAgentFromScratch(options: { initial: 'agent', states: { agent: { - resultSchema: modelResultSchema, + schemas: { output: modelResultSchema }, invoke: async ({ context }, enq) => { if (context.stepCount >= maxSteps) { return { @@ -120,8 +120,8 @@ export function createReactAgentFromScratch(options: { return result; }, - onDone: ({ result, context }) => { - if (result.kind === 'final') { + onDone: ({ output, context }) => { + if (output.kind === 'final') { return { target: 'done' as const, context: { @@ -130,7 +130,7 @@ export function createReactAgentFromScratch(options: { ...context.messages, { role: 'assistant', - content: result.message, + content: output.message, } satisfies ReactAgentMessage, ], }, @@ -142,35 +142,34 @@ export function createReactAgentFromScratch(options: { context: { stepCount: context.stepCount + 1, pendingToolCall: { - toolName: result.toolName, - input: result.input, + toolName: output.toolName, + input: output.input, }, messages: [ ...context.messages, { role: 'assistant', content: - result.message - ?? `Calling tool ${result.toolName} with ${JSON.stringify(result.input)}`, + output.message + ?? `Calling tool ${output.toolName} with ${JSON.stringify(output.input)}`, } satisfies ReactAgentMessage, ], }, input: { - toolName: result.toolName, - input: result.input, + toolName: output.toolName, + input: output.input, }, }; }, }, tool: { - inputSchema: z.object({ + schemas: { input: z.object({ toolName: z.string(), input: z.record(z.string(), z.unknown()), - }), - resultSchema: z.object({ + }), output: z.object({ toolName: z.string(), output: z.unknown(), - }), + }) }, invoke: async ({ input }, enq) => { const tool = toolsByName.get(input.toolName); @@ -197,7 +196,7 @@ export function createReactAgentFromScratch(options: { output, }; }, - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'agent' as const, context: { pendingToolCall: null, @@ -205,8 +204,8 @@ export function createReactAgentFromScratch(options: { ...context.messages, { role: 'tool', - name: result.toolName, - content: serializeToolOutput(result.output), + name: output.toolName, + content: serializeToolOutput(output.output), } satisfies ReactAgentMessage, ], }, diff --git a/examples/reflection.ts b/examples/reflection.ts index 529faa9..f7f5ee7 100644 --- a/examples/reflection.ts +++ b/examples/reflection.ts @@ -94,41 +94,41 @@ export function createReflectionExample( initial: 'drafting', states: { drafting: { - resultSchema: draftSchema, + schemas: { output: draftSchema }, invoke: async ({ context }) => draft(context.task), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'reflecting', - context: { draft: result.draft }, + context: { draft: output.draft }, }), }, reflecting: { - resultSchema: feedbackSchema, + schemas: { output: feedbackSchema }, invoke: async ({ context }) => reflect({ task: context.task, draft: context.draft ?? '', revisionCount: context.revisionCount, }), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: - !result.feedback || context.revisionCount >= context.maxRevisions + !output.feedback || context.revisionCount >= context.maxRevisions ? 'done' : 'revising', - context: { feedback: result.feedback }, + context: { feedback: output.feedback }, }), }, revising: { - resultSchema: draftSchema, + schemas: { output: draftSchema }, invoke: async ({ context }) => revise({ task: context.task, draft: context.draft ?? '', feedback: context.feedback ?? '', }), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'reflecting', context: { - draft: result.draft, + draft: output.draft, revisionCount: context.revisionCount + 1, }, }), diff --git a/examples/rewoo.ts b/examples/rewoo.ts index fba6b82..45f8547 100644 --- a/examples/rewoo.ts +++ b/examples/rewoo.ts @@ -136,22 +136,21 @@ export function createRewooExample( initial: 'planning', states: { planning: { - resultSchema: rewooPlanSchema, + schemas: { output: rewooPlanSchema }, invoke: async ({ context }) => plan(context.objective), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'executing', - context: { steps: result.steps }, + context: { steps: output.steps }, input: { index: 0 }, }), }, executing: { - inputSchema: z.object({ + schemas: { input: z.object({ index: z.number().int().min(0), - }), - resultSchema: z.object({ + }), output: z.object({ stepId: z.string(), result: z.string(), - }), + }) }, invoke: async ({ context, input }) => { const step = context.steps[input.index]; @@ -172,10 +171,10 @@ export function createRewooExample( result: outcome.result, }; }, - onDone: ({ result, context }) => { + onDone: ({ output, context }) => { const nextResultsById = { ...context.resultsById, - [result.stepId]: result.result, + [output.stepId]: output.result, }; const nextIndex = Object.keys(nextResultsById).length; @@ -194,16 +193,16 @@ export function createRewooExample( }, }, solving: { - resultSchema: rewooAnswerSchema, + schemas: { output: rewooAnswerSchema }, invoke: async ({ context }) => solve({ objective: context.objective, steps: context.steps, resultsById: context.resultsById, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { answer: result.answer }, + context: { answer: output.answer }, }), }, done: { diff --git a/examples/river-crossing.ts b/examples/river-crossing.ts index c628267..497c2e5 100644 --- a/examples/river-crossing.ts +++ b/examples/river-crossing.ts @@ -111,17 +111,17 @@ export function createRiverCrossingExample() { initial: 'choosing', states: { choosing: { - resultSchema: crossingMoveSchema, + schemas: { output: crossingMoveSchema }, invoke: async ({ context }) => chooseCrossingMove( [...context.leftBank], [...context.rightBank], context.farmerPosition ), - onDone: ({ result, context }) => { - const nextReasoning = [...context.reasoning, result.reasoning]; + onDone: ({ output, context }) => { + const nextReasoning = [...context.reasoning, output.reasoning]; - if (result.move === 'done') { + if (output.move === 'done') { return { target: 'done' as const, context: { reasoning: nextReasoning }, @@ -130,16 +130,15 @@ export function createRiverCrossingExample() { return { target: 'moving' as const, - input: { move: result.move }, + input: { move: output.move }, context: { reasoning: nextReasoning }, }; }, }, moving: { - inputSchema: z.object({ + schemas: { input: z.object({ move: crossingMoveSchema.shape.move.exclude(['done']), - }), - resultSchema: crossingStateSchema, + }), output: crossingStateSchema }, invoke: async ({ context, input }) => moveItem( [...context.leftBank], @@ -147,13 +146,13 @@ export function createRiverCrossingExample() { context.farmerPosition, input.move as 'takeGoat' | 'takeWolf' | 'takeCabbage' | 'returnEmpty' ), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'choosing', context: { - leftBank: result.leftBank, - rightBank: result.rightBank, - farmerPosition: result.farmerPosition, - steps: [...context.steps, result.step], + leftBank: output.leftBank, + rightBank: output.rightBank, + farmerPosition: output.farmerPosition, + steps: [...context.steps, output.step], }, }), }, diff --git a/examples/self-evaluation-loop-flow.ts b/examples/self-evaluation-loop-flow.ts index 1827ddb..130e6eb 100644 --- a/examples/self-evaluation-loop-flow.ts +++ b/examples/self-evaluation-loop-flow.ts @@ -73,32 +73,32 @@ export function createSelfEvaluationLoopFlowExample(options: { initial: 'generating', states: { generating: { - resultSchema: postSchema, + schemas: { output: postSchema }, invoke: async ({ context }) => generatePost({ topic: context.topic, feedback: context.feedback, attempt: context.attempt, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'evaluating', context: { - post: result.post, + post: output.post, }, }), }, evaluating: { - resultSchema: evaluationSchema, + schemas: { output: evaluationSchema }, invoke: async ({ context }) => evaluatePost(context.post ?? ''), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: - result.valid || context.attempt >= context.maxAttempts + output.valid || context.attempt >= context.maxAttempts ? 'done' : 'generating', context: { - valid: result.valid, - feedback: result.feedback, - attempt: result.valid + valid: output.valid, + feedback: output.feedback, + attempt: output.valid ? context.attempt : context.attempt + 1, }, diff --git a/examples/simple.ts b/examples/simple.ts index 812fb22..ef4e86d 100644 --- a/examples/simple.ts +++ b/examples/simple.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { createAgentMachine } from '../src/index.js'; +import { createAgentMachine, type AgentAdapter } from '../src/index.js'; import { closePrompt, + createOpenAiGenerationAdapter, formatResult, - generateExampleObject, isMain, prompt, } from './_run.js'; @@ -13,17 +13,11 @@ const summarySchema = z.object({ }); export function createSimpleExample( - summarize: (text: string) => Promise> = async ( - text - ) => { - return generateExampleObject({ - schema: summarySchema, - prompt: `Summarize this text in one sentence:\n\n${text}`, - }); - } + adapter: AgentAdapter = createOpenAiGenerationAdapter() ) { return createAgentMachine({ id: 'simple-example', + adapter, schemas: { input: z.object({ text: z.string() }), output: z.object({ summary: z.string().nullable() }), @@ -35,11 +29,12 @@ export function createSimpleExample( initial: 'summarizing', states: { summarizing: { - resultSchema: summarySchema, - invoke: async ({ context }) => summarize(context.text), - onDone: ({ result }) => ({ + schemas: { output: summarySchema }, + prompt: ({ context }) => + `Summarize this text in one sentence:\n\n${context.text}`, + onDone: ({ output }) => ({ target: 'done', - context: { summary: result.summary }, + context: { summary: output.summary }, }), }, done: { diff --git a/examples/spec-agent-loop.ts b/examples/spec-agent-loop.ts index 8d86de9..2968cb4 100644 --- a/examples/spec-agent-loop.ts +++ b/examples/spec-agent-loop.ts @@ -98,7 +98,7 @@ export function createSpecAgentLoopExample( initial: 'generating', states: { generating: { - resultSchema: generationSchema, + schemas: { output: generationSchema }, invoke: async ({ context, messages }) => parseTaggedResponse( await generate({ @@ -106,22 +106,22 @@ export function createSpecAgentLoopExample( messages, }) ), - onDone: ({ result, messages }) => ({ - target: result.specYaml ? 'validating' : 'repairing', + onDone: ({ output, messages }) => ({ + target: output.specYaml ? 'validating' : 'repairing', context: { - specYaml: result.specYaml, - questions: result.questions, - status: result.status, + specYaml: output.specYaml, + questions: output.questions, + status: output.status, }, - messages: appendMessages(messages, assistantMessage(result.rawText)), + messages: appendMessages(messages, assistantMessage(output.rawText)), }), }, validating: { - resultSchema: validationSchema, + schemas: { output: validationSchema }, invoke: async ({ context }) => validate(context.specYaml), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'routing', - context: { validation: result }, + context: { validation: output }, }), }, routing: { diff --git a/examples/sql-agent.ts b/examples/sql-agent.ts index d48b0c6..e5f35f3 100644 --- a/examples/sql-agent.ts +++ b/examples/sql-agent.ts @@ -5,7 +5,7 @@ import { decide, decideResultSchema, startSession, - type AgentAdapter, + type DecideAdapter, } from '../src/index.js'; import { closePrompt, @@ -49,7 +49,7 @@ const queryExecutionSchema = z.discriminatedUnion('status', [ export function createSqlAgentExample( options: { - adapter?: AgentAdapter; + adapter?: DecideAdapter; executeQuery?: (args: { question: string; schema: string; @@ -135,7 +135,7 @@ export function createSqlAgentExample( initial: 'planning', states: { planning: { - resultSchema: decideResultSchema(planningOptions), + schemas: { output: decideResultSchema(planningOptions) }, invoke: async ({ context }) => decide({ adapter, @@ -159,12 +159,12 @@ export function createSqlAgentExample( ].join('\n'), options: planningOptions, }), - onDone: ({ result }) => { - if (result.choice === 'query') { + onDone: ({ output }) => { + if (output.choice === 'query') { return { target: 'querying', input: { - query: result.data.query, + query: output.data.query, }, }; } @@ -172,16 +172,15 @@ export function createSqlAgentExample( return { target: 'done', context: { - answer: result.data.answer, + answer: output.data.answer, }, }; }, }, querying: { - inputSchema: z.object({ + schemas: { input: z.object({ query: z.string(), - }), - resultSchema: queryExecutionSchema, + }), output: queryExecutionSchema }, invoke: async ({ context, input }, enq) => { enq.emit({ type: 'toolCall', @@ -217,15 +216,15 @@ export function createSqlAgentExample( return resolvedOutput; }, - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'planning', context: { queryHistory: [ ...context.queryHistory, - result.query, + output.query, ], - latestRows: result.status === 'success' ? result.rows : null, - latestError: result.status === 'error' ? result.error : null, + latestRows: output.status === 'success' ? output.rows : null, + latestError: output.status === 'error' ? output.error : null, }, }), }, diff --git a/examples/subflow.ts b/examples/subflow.ts index 5946a3a..cb6639c 100644 --- a/examples/subflow.ts +++ b/examples/subflow.ts @@ -38,7 +38,7 @@ export function createSubflowExample( initial: 'researching', states: { researching: { - resultSchema: researchSchema, + schemas: { output: researchSchema }, invoke: async ({ context }) => (options.research ?? ((topic) => @@ -47,9 +47,9 @@ export function createSubflowExample( system: 'You research a topic and return concise bullet points.', prompt: `Return 2 to 4 concise research bullets about ${topic}.`, })))(context.topic), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { bullets: result.bullets }, + context: { bullets: output.bullets }, }), }, done: { @@ -76,7 +76,7 @@ export function createSubflowExample( initial: 'researching', states: { researching: { - resultSchema: researchSchema, + schemas: { output: researchSchema }, invoke: async ({ context }) => { const result = await childMachine.execute( childMachine.getInitialState({ topic: context.topic }) @@ -90,13 +90,13 @@ export function createSubflowExample( bullets: result.output.bullets, }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'writing', - context: { bullets: result.bullets }, + context: { bullets: output.bullets }, }), }, writing: { - resultSchema: draftSchema, + schemas: { output: draftSchema }, invoke: async ({ context }) => (options.write ?? (({ topic, bullets }) => @@ -112,9 +112,9 @@ export function createSubflowExample( topic: context.topic, bullets: context.bullets, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { draft: result.draft }, + context: { draft: output.draft }, }), }, done: { diff --git a/examples/supervisor.ts b/examples/supervisor.ts index cdbf1e2..d54887b 100644 --- a/examples/supervisor.ts +++ b/examples/supervisor.ts @@ -3,7 +3,7 @@ import { createAgentMachine, decide, decideResultSchema, - type AgentAdapter, + type DecideAdapter, } from '../src/index.js'; import { closePrompt, @@ -47,7 +47,7 @@ const supervisorOptions = { export function createSupervisorExample( options: { - adapter?: AgentAdapter; + adapter?: DecideAdapter; handle?: (args: { request: string; attempt: number; @@ -120,8 +120,7 @@ export function createSupervisorExample( }), states: { handling: { - inputSchema: handlingParamsSchema, - resultSchema: workerResultSchema, + schemas: { input: handlingParamsSchema, output: workerResultSchema }, invoke: async ({ context, input }) => handle({ request: context.request, @@ -129,18 +128,18 @@ export function createSupervisorExample( instruction: input.instruction ?? null, priorIssues: context.priorIssues, }), - onDone: ({ result, context, }) => { + onDone: ({ output, context, }) => { const nextAttemptCount = context.attemptCount + 1; - if (result.status === 'resolved') { + if (output.status === 'resolved') { return { target: 'done', context: { attemptCount: nextAttemptCount, - resolution: result.response, + resolution: output.response, history: [ ...context.history, - `worker:${nextAttemptCount}:resolved:${result.response}`, + `worker:${nextAttemptCount}:resolved:${output.response}`, ], }, }; @@ -150,18 +149,18 @@ export function createSupervisorExample( target: 'supervising', context: { attemptCount: nextAttemptCount, - latestIssue: result.issue, - priorIssues: [...context.priorIssues, result.issue], + latestIssue: output.issue, + priorIssues: [...context.priorIssues, output.issue], history: [ ...context.history, - `worker:${nextAttemptCount}:blocked:${result.issue}`, + `worker:${nextAttemptCount}:blocked:${output.issue}`, ], }, }; }, }, supervising: { - resultSchema: decideResultSchema(supervisorOptions), + schemas: { output: decideResultSchema(supervisorOptions) }, invoke: async ({ context }) => decide({ adapter, @@ -183,10 +182,10 @@ export function createSupervisorExample( ].join('\n'), options: supervisorOptions, }), - onDone: ({ result, context }) => { - if (result.choice === 'retry') { + onDone: ({ output, context }) => { + if (output.choice === 'retry') { const instruction = - result.data.instruction + output.data.instruction ?? 'Retry once with a more concrete plan and any available context.'; return { @@ -205,7 +204,7 @@ export function createSupervisorExample( } const reason = - result.data.reason + output.data.reason ?? `Escalated after ${context.attemptCount} unsuccessful attempts.`; return { diff --git a/examples/tool-calling.ts b/examples/tool-calling.ts index 36e2d12..2753b17 100644 --- a/examples/tool-calling.ts +++ b/examples/tool-calling.ts @@ -72,7 +72,7 @@ export function createToolCallingExample( initial: 'checkingWeather', states: { checkingWeather: { - resultSchema: forecastSchema, + schemas: { output: forecastSchema }, invoke: async ({ context }, enq) => { enq.emit({ type: 'toolCall', @@ -95,9 +95,9 @@ export function createToolCallingExample( return output; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { forecast: result.forecast }, + context: { forecast: output.forecast }, }), }, done: { diff --git a/examples/tutor.ts b/examples/tutor.ts index 16193e3..9ef2009 100644 --- a/examples/tutor.ts +++ b/examples/tutor.ts @@ -57,23 +57,23 @@ export function createTutorExample( initial: 'teaching', states: { teaching: { - resultSchema: feedbackSchema, + schemas: { output: feedbackSchema }, invoke: async ({ context }) => teach(context.conversation.at(-1)?.replace(/^User:\s*/, '') ?? ''), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'responding', - context: { feedback: result.instruction }, + context: { feedback: output.instruction }, }), }, responding: { - resultSchema: responseSchema, + schemas: { output: responseSchema }, invoke: async ({ context }) => respond(context.conversation.at(-1)?.replace(/^User:\s*/, '') ?? ''), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'done', context: { - response: result.response, - conversation: [...context.conversation, `Tutor: ${result.response}`], + response: output.response, + conversation: [...context.conversation, `Tutor: ${output.response}`], }, }), }, diff --git a/examples/workflow-guardrails.ts b/examples/workflow-guardrails.ts new file mode 100644 index 0000000..f67ada1 --- /dev/null +++ b/examples/workflow-guardrails.ts @@ -0,0 +1,363 @@ +import { z } from 'zod'; +import { createAgentMachine, type AgentAdapter } from '../src/index.js'; +import { closePrompt, formatResult, isMain, prompt } from './_run.js'; + +type WorkflowTool = (input?: Record) => Promise; + +const taskInputSchema = z.object({ task: z.string().optional() }).optional(); + +const planSchema = z.object({ plan: z.string() }); +const implementationSchema = z.object({ summary: z.string() }); +const testSchema = z.object({ + passed: z.boolean(), + output: z.string().optional(), +}); +const diagnosisSchema = z.object({ diagnosis: z.string() }); +const rootCauseSchema = z.object({ rootCause: z.string() }); +const proposalSchema = z.object({ proposal: z.string() }); +const fixSchema = z.object({ applied: z.boolean(), summary: z.string() }); +const verificationSchema = z.object({ + verified: z.boolean(), + summary: z.string(), +}); + +const readOnlyCodingTools = { + Read: async () => undefined, + Grep: async () => undefined, + Glob: async () => undefined, + LS: async () => undefined, + Bash: async () => undefined, +} satisfies Record; + +const editingCodingTools = { + ...readOnlyCodingTools, + Edit: async () => undefined, + Write: async () => undefined, +} satisfies Record; + +const incidentReadTools = { + Read: async () => undefined, + Bash: async () => undefined, + Grep: async () => undefined, +} satisfies Record; + +const incidentWriteTools = { + Read: async () => undefined, + Bash: async () => undefined, +} satisfies Record; + +const allIncidentTools = { + list_services: async () => undefined, + get_service: async () => undefined, + list_volumes: async () => undefined, + get_volume: async () => undefined, + get_logs: async () => undefined, + get_env: async () => undefined, + update_env: async () => undefined, + restart_service: async () => undefined, + delete_volume: async () => undefined, + delete_service: async () => undefined, + test_connection: async () => undefined, +} satisfies Record; + +function createSequenceAdapter(results: unknown[]): AgentAdapter { + let index = 0; + + return { + async generateText() { + const result = results[index] ?? results.at(-1); + index += 1; + return result; + }, + }; +} + +export function createGuardrailedBugfixWorkflowExample(options: { + adapter?: AgentAdapter; +} = {}) { + return createAgentMachine({ + id: 'guardrailed-bugfix-workflow', + schemas: { input: taskInputSchema }, + adapter: + options.adapter + ?? createSequenceAdapter([ + { plan: 'Read the failing test, inspect the implementation, then make the smallest fix.' }, + { summary: 'Applied a targeted code change.' }, + { passed: true, output: 'All tests passed.' }, + ]), + context: (input) => ({ + task: input?.task ?? 'Fix the failing tests.', + plan: null as string | null, + changeSummary: null as string | null, + testOutput: null as string | null, + }), + messages: (input) => [ + { role: 'user', content: input?.task ?? 'Fix the failing tests.' }, + ], + initial: 'planning', + states: { + planning: { + schemas: { output: planSchema }, + prompt: + 'Read relevant files and produce a brief fix plan. Do not edit anything yet.', + tools: readOnlyCodingTools, + onDone: ({ output }) => ({ + target: 'implementing', + context: { plan: output.plan }, + }), + }, + implementing: { + schemas: { output: implementationSchema }, + prompt: ({ snapshot }) => + [ + 'Implement the fix. Make targeted, minimal edits.', + `Current state: ${snapshot.value}`, + `Plan: ${snapshot.context.plan ?? 'none'}`, + ].join('\n'), + tools: editingCodingTools, + onDone: ({ output }) => ({ + target: 'testing', + context: { changeSummary: output.summary }, + }), + }, + testing: { + schemas: { output: testSchema }, + prompt: ({ snapshot }) => + [ + 'Run the tests to verify the fix.', + `Current state: ${snapshot.value}`, + `Change summary: ${snapshot.context.changeSummary ?? 'none'}`, + ].join('\n'), + tools: { + Read: readOnlyCodingTools.Read, + Bash: readOnlyCodingTools.Bash, + }, + onDone: ({ output }) => + output.passed + ? { + target: 'completed', + context: { testOutput: output.output ?? null }, + } + : { + target: 'implementing', + context: { testOutput: output.output ?? null }, + }, + }, + completed: { + type: 'final', + output: ({ context }) => ({ + plan: context.plan, + changeSummary: context.changeSummary, + testOutput: context.testOutput, + }), + }, + }, + }); +} + +export function createGuardrailedIncidentResponseExample(options: { + adapter?: AgentAdapter; +} = {}) { + return createAgentMachine({ + id: 'guardrailed-incident-response', + schemas: { + input: taskInputSchema, + events: { + APPROVED: z.object({ type: z.literal('APPROVED') }), + REJECTED: z.object({ type: z.literal('REJECTED') }), + }, + }, + adapter: + options.adapter + ?? createSequenceAdapter([ + { diagnosis: 'The web service cannot connect to its database.' }, + { rootCause: 'The staging database credential is stale.' }, + { proposal: 'Update the staging DB password and restart the web service.' }, + { applied: true, summary: 'Updated the staging DB password and restarted the service.' }, + { verified: true, summary: 'Connection test passed and service is healthy.' }, + ]), + context: (input) => ({ + task: + input?.task + ?? 'The staging environment is down. Diagnose and repair without destructive actions.', + diagnosis: null as string | null, + rootCause: null as string | null, + proposal: null as string | null, + fixSummary: null as string | null, + verification: null as string | null, + }), + messages: (input) => [ + { + role: 'user', + content: + input?.task + ?? 'The staging environment is down. Diagnose and repair without destructive actions.', + }, + ], + initial: 'diagnosing', + states: { + diagnosing: { + schemas: { output: diagnosisSchema }, + prompt: + 'Check service status and logs. Identify the likely failure. Do not modify anything.', + tools: { + ...incidentReadTools, + list_services: allIncidentTools.list_services, + get_service: allIncidentTools.get_service, + get_logs: allIncidentTools.get_logs, + get_volume: allIncidentTools.get_volume, + list_volumes: allIncidentTools.list_volumes, + }, + onDone: ({ output }) => ({ + target: 'investigating', + context: { diagnosis: output.diagnosis }, + }), + }, + investigating: { + schemas: { output: rootCauseSchema }, + prompt: + 'Investigate the root cause. Check environment variables, test connections, and read logs. Still read-only.', + tools: { + ...incidentReadTools, + get_env: allIncidentTools.get_env, + test_connection: allIncidentTools.test_connection, + get_logs: allIncidentTools.get_logs, + }, + onDone: ({ output }) => ({ + target: 'proposing', + context: { rootCause: output.rootCause }, + }), + }, + proposing: { + schemas: { output: proposalSchema }, + prompt: + 'Propose the fix. Describe exactly what should change and why. Do not execute the fix yet.', + tools: { Read: incidentReadTools.Read }, + onDone: ({ output }) => ({ + target: 'awaitingApproval', + context: { proposal: output.proposal }, + }), + }, + awaitingApproval: { + prompt: ({ snapshot }) => + [ + `Await approval while in ${snapshot.value}.`, + snapshot.context.proposal ?? '', + ].join('\n'), + tools: { Read: incidentReadTools.Read }, + on: { + APPROVED: { target: 'executingFix' }, + REJECTED: { target: 'proposing' }, + }, + }, + executingFix: { + schemas: { output: fixSchema }, + prompt: + 'Execute the approved fix. API actions allowed: update_env, restart_service. Do not delete volumes or services.', + tools: { + ...incidentWriteTools, + update_env: allIncidentTools.update_env, + restart_service: allIncidentTools.restart_service, + }, + onDone: ({ output }) => ({ + target: 'verifying', + context: { fixSummary: output.summary }, + }), + }, + verifying: { + schemas: { output: verificationSchema }, + prompt: + 'Verify the fix. Test the connection, check service status, and review logs.', + tools: { + ...incidentWriteTools, + test_connection: allIncidentTools.test_connection, + get_service: allIncidentTools.get_service, + get_logs: allIncidentTools.get_logs, + }, + onDone: ({ output }) => + output.verified + ? { + target: 'completed', + context: { verification: output.summary }, + } + : { + target: 'proposing', + context: { verification: output.summary }, + }, + }, + completed: { + type: 'final', + output: ({ context }) => ({ + diagnosis: context.diagnosis, + rootCause: context.rootCause, + proposal: context.proposal, + fixSummary: context.fixSummary, + verification: context.verification, + }), + }, + }, + }); +} + +export function createUnguardedIncidentResponseExample(options: { + adapter?: AgentAdapter; +} = {}) { + return createAgentMachine({ + id: 'unguarded-incident-response', + schemas: { input: taskInputSchema }, + adapter: + options.adapter + ?? createSequenceAdapter([ + { applied: true, summary: 'Used whatever API actions were available to repair the service.' }, + ]), + context: (input) => ({ + task: + input?.task + ?? 'The staging environment is down. Fix it with all API actions available.', + fixSummary: null as string | null, + }), + messages: (input) => [ + { + role: 'user', + content: + input?.task + ?? 'The staging environment is down. Fix it with all API actions available.', + }, + ], + initial: 'working', + states: { + working: { + schemas: { output: fixSchema }, + prompt: 'Fix the staging environment issue. All tools and API actions are available.', + tools: { + ...incidentReadTools, + ...allIncidentTools, + }, + onDone: ({ output }) => ({ + target: 'completed', + context: { fixSummary: output.summary }, + }), + }, + completed: { + type: 'final', + output: ({ context }) => ({ fixSummary: context.fixSummary }), + }, + }, + }); +} + +async function main() { + try { + const task = await prompt('Task'); + const machine = createGuardrailedBugfixWorkflowExample(); + const result = await machine.execute(machine.getInitialState({ task })); + + console.log(formatResult(result)); + } finally { + closePrompt(); + } +} + +if (isMain(import.meta.url)) { + void main(); +} diff --git a/examples/write-a-book-flow.ts b/examples/write-a-book-flow.ts index d22742d..f171020 100644 --- a/examples/write-a-book-flow.ts +++ b/examples/write-a-book-flow.ts @@ -118,22 +118,22 @@ export function createWriteABookFlowExample(options: { initial: 'outlining', states: { outlining: { - resultSchema: outlineSchema, + schemas: { output: outlineSchema }, invoke: async ({ context }) => createOutline({ topic: context.topic, goal: context.goal, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'writing', context: { - title: result.title, - outline: result.chapters, + title: output.title, + outline: output.chapters, }, }), }, writing: { - resultSchema: chapterBatchSchema, + schemas: { output: chapterBatchSchema }, invoke: async ({ context }) => { const chapters = await Promise.all( context.outline.map((chapter) => @@ -148,24 +148,24 @@ export function createWriteABookFlowExample(options: { return { chapters }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'compiling', context: { - chapters: result.chapters, + chapters: output.chapters, }, }), }, compiling: { - resultSchema: manuscriptSchema, + schemas: { output: manuscriptSchema }, invoke: async ({ context }) => compileManuscript({ title: context.title ?? 'Untitled Book', chapters: context.chapters, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', context: { - manuscript: result.manuscript, + manuscript: output.manuscript, }, }), }, diff --git a/readme.md b/readme.md index 8193cc0..8a9acb4 100644 --- a/readme.md +++ b/readme.md @@ -25,7 +25,7 @@ Start here: - Durable sessions and transports: [`examples/persistence.ts`](/Users/davidkpiano/Code/agent/examples/persistence.ts), [`examples/http-session.ts`](/Users/davidkpiano/Code/agent/examples/http-session.ts), [`examples/http-streaming-session.ts`](/Users/davidkpiano/Code/agent/examples/http-streaming-session.ts) - Core workflow patterns: [`examples/rag.ts`](/Users/davidkpiano/Code/agent/examples/rag.ts), [`examples/tool-calling.ts`](/Users/davidkpiano/Code/agent/examples/tool-calling.ts), [`examples/error-retry.ts`](/Users/davidkpiano/Code/agent/examples/error-retry.ts), [`examples/spec-agent-loop.ts`](/Users/davidkpiano/Code/agent/examples/spec-agent-loop.ts), [`examples/persistent-supervisor.ts`](/Users/davidkpiano/Code/agent/examples/persistent-supervisor.ts) - CrewAI-style equivalents: [`examples/content-creator-flow.ts`](/Users/davidkpiano/Code/agent/examples/content-creator-flow.ts), [`examples/email-auto-responder-flow.ts`](/Users/davidkpiano/Code/agent/examples/email-auto-responder-flow.ts), [`examples/lead-score-flow.ts`](/Users/davidkpiano/Code/agent/examples/lead-score-flow.ts), [`examples/meeting-assistant-flow.ts`](/Users/davidkpiano/Code/agent/examples/meeting-assistant-flow.ts), [`examples/self-evaluation-loop-flow.ts`](/Users/davidkpiano/Code/agent/examples/self-evaluation-loop-flow.ts), [`examples/write-a-book-flow.ts`](/Users/davidkpiano/Code/agent/examples/write-a-book-flow.ts) -- Reference examples: [`examples/simple.ts`](/Users/davidkpiano/Code/agent/examples/simple.ts), [`examples/decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts), [`examples/classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts), [`examples/adapter.ts`](/Users/davidkpiano/Code/agent/examples/adapter.ts) +- Reference examples: [`examples/simple.ts`](/Users/davidkpiano/Code/agent/examples/simple.ts), [`examples/decide.ts`](/Users/davidkpiano/Code/agent/examples/decide.ts), [`examples/classify.ts`](/Users/davidkpiano/Code/agent/examples/classify.ts), [`examples/adapter.ts`](/Users/davidkpiano/Code/agent/examples/adapter.ts), [`examples/workflow-guardrails.ts`](/Users/davidkpiano/Code/agent/examples/workflow-guardrails.ts) Use `classify(...)` when the result is just "what kind of thing is this?" Use `decide(...)` when the result is "what should happen next?" and the chosen branch may need structured data. diff --git a/src/adapter.ts b/src/adapter.ts index 9cc3e54..5edb2a3 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -1,7 +1,7 @@ import type { AgentAdapter } from './types.js'; /** - * Create a custom adapter for AI primitives (classify/decide). + * Create a custom adapter for model execution. */ export function createAdapter(impl: AgentAdapter): AgentAdapter { return impl; diff --git a/src/agent.test.ts b/src/agent.test.ts index 1a24258..ec37e3e 100644 --- a/src/agent.test.ts +++ b/src/agent.test.ts @@ -8,7 +8,7 @@ import { decide, decideResultSchema, } from './index.js'; -import type { AgentAdapter } from './types.js'; +import type { DecideAdapter } from './types.js'; // ─── Test helpers ─── @@ -18,7 +18,7 @@ function mockAdapter( data?: Record; reasoning?: string; }> -): AgentAdapter { +): DecideAdapter { let index = 0; return { decide: async () => { @@ -53,14 +53,14 @@ function createSimpleMachine() { }, }, running: { - resultSchema: z.object({ value: z.number() }), + schemas: { output: z.object({ value: z.number() }) }, invoke: async ({ context }) => { // context.count is typed as number ✓ return { value: context.count + 1 }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { count: result.value }, + context: { count: output.value }, }), }, done: { @@ -109,16 +109,16 @@ function createHitlMachine() { }, }, processing: { - resultSchema: z.object({ output: z.string() }), + schemas: { output: z.object({ output: z.string() }) }, invoke: async ({ context }) => { // context.messages is typed ✓ return { output: `Processed: ${context.messages.map((m) => m.content).join(', ')}`, }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'reviewing', - context: { result: result.output }, + context: { result: output.output }, }), }, reviewing: { @@ -151,7 +151,7 @@ function createHitlMachine() { // ─── Decide machine ─── -function createDecideMachine(adapter: AgentAdapter) { +function createDecideMachine(adapter: DecideAdapter) { const options = { billing: { description: 'Billing issues' }, technical: { description: 'Technical issues' }, @@ -168,7 +168,7 @@ function createDecideMachine(adapter: AgentAdapter) { initial: 'classifying', states: { classifying: { - resultSchema: decideResultSchema(options), + schemas: { output: decideResultSchema(options) }, invoke: async ({ context }) => decide({ adapter, @@ -176,19 +176,19 @@ function createDecideMachine(adapter: AgentAdapter) { prompt: `Classify: ${context.issue}`, options, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'handling', - context: { category: result.choice }, + context: { category: output.choice }, }), }, handling: { - resultSchema: z.object({ resolution: z.string() }), + schemas: { output: z.object({ resolution: z.string() }) }, invoke: async ({ context }) => ({ resolution: `Handled ${context.category} issue`, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { resolution: result.resolution }, + context: { resolution: output.resolution }, }), }, done: { @@ -204,7 +204,7 @@ function createDecideMachine(adapter: AgentAdapter) { // ─── Classify machine ─── -function createClassifyMachine(adapter: AgentAdapter) { +function createClassifyMachine(adapter: DecideAdapter) { const categories = { billing: { description: 'Billing, payments, refunds' }, technical: { description: 'Technical issues, bugs' }, @@ -220,7 +220,7 @@ function createClassifyMachine(adapter: AgentAdapter) { initial: 'classifyIntent', states: { classifyIntent: { - resultSchema: classifyResultSchema(categories), + schemas: { output: classifyResultSchema(categories) }, invoke: async ({ context }) => classify({ adapter, @@ -228,9 +228,9 @@ function createClassifyMachine(adapter: AgentAdapter) { prompt: `Classify: "${context.issue}"`, into: categories, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { category: result.category }, + context: { category: output.category }, }), }, done: { @@ -519,10 +519,10 @@ describe('decide', () => { initial: 'choosing', states: { choosing: { - resultSchema: decideResultSchema({ + schemas: { output: decideResultSchema({ a: { description: 'A' }, b: { description: 'B' }, - }), + }) }, invoke: async ({ context }) => decide({ adapter: { decide: spy }, @@ -530,9 +530,9 @@ describe('decide', () => { prompt: `About ${context.topic}`, options: { a: { description: 'A' }, b: { description: 'B' } }, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { choice: result.choice }, + context: { choice: output.choice }, }), }, done: { type: 'final' }, @@ -551,10 +551,10 @@ describe('decide', () => { initial: 'choosing', states: { choosing: { - resultSchema: decideResultSchema({ + schemas: { output: decideResultSchema({ state: { description: 'State' }, machine: { description: 'Machine' }, - }), + }) }, invoke: async () => decide({ adapter: mockAdapter([{ choice: 'state' }]), @@ -565,9 +565,9 @@ describe('decide', () => { machine: { description: 'Machine' }, }, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { choice: result.choice }, + context: { choice: output.choice }, }), }, done: { type: 'final' }, @@ -584,13 +584,13 @@ describe('decide', () => { initial: 'choosing', states: { choosing: { - resultSchema: decideResultSchema({ + schemas: { output: decideResultSchema({ withData: { description: 'Has data', schema: z.object({ items: z.array(z.string()) }), }, withoutData: { description: 'No data' }, - }), + }) }, invoke: async () => decide({ adapter: { @@ -609,13 +609,13 @@ describe('decide', () => { withoutData: { description: 'No data' }, }, }), - onDone: ({ result }) => { + onDone: ({ output }) => { return { target: 'done', context: { items: - result.choice === 'withData' - ? (result.data.items ?? null) + output.choice === 'withData' + ? (output.data.items ?? null) : null, }, }; @@ -629,27 +629,29 @@ describe('decide', () => { }); }); -describe('type: choice', () => { - test('inline choice state with typed context', async () => { +describe('decide helper', () => { + test('explicit decide invoke with typed context', async () => { const adapter = mockAdapter([{ choice: 'technical' }]); const machine = createAgentMachine({ - id: 'choice-test', + id: 'decide-helper-test', context: () => ({ issue: 'App crashes', result: null as string | null }), - adapter, initial: 'routing', states: { routing: { - type: 'choice', - resultSchema: choiceResultSchema, - model: 'test-model', - prompt: ({ context }) => `Route: ${context.issue}`, // context typed ✓ - options: { - billing: { description: 'Billing' }, - technical: { description: 'Technical' }, - }, - onDone: ({ result, context }) => ({ + schemas: { output: choiceResultSchema }, + invoke: async ({ context }) => + decide({ + adapter, + model: 'test-model', + prompt: `Route: ${context.issue}`, + options: { + billing: { description: 'Billing' }, + technical: { description: 'Technical' }, + }, + }), + onDone: ({ output, context }) => ({ target: 'done', - context: { result: `${result.choice}: ${context.issue}` }, + context: { result: `${output.choice}: ${context.issue}` }, }), }, done: { type: 'final', output: ({ context }) => ({ result: context.result }) }, @@ -663,27 +665,28 @@ describe('type: choice', () => { } }); - test('choice with event preemption', async () => { + test('invoke state with event transition', () => { let called = false; - const adapter: AgentAdapter = { + const adapter: DecideAdapter = { decide: async () => { called = true; - // Slow adapter — in real use, event would preempt return { choice: 'a', data: {} }; }, }; const machine = createAgentMachine({ - id: 'choice-preempt', + id: 'invoke-event-transition', context: () => ({}), - adapter, initial: 'choosing', states: { choosing: { - type: 'choice', - resultSchema: choiceResultSchema, - model: 'test', - prompt: 'pick', - options: { a: { description: 'A' } }, + schemas: { output: choiceResultSchema }, + invoke: async () => + decide({ + adapter, + model: 'test', + prompt: 'pick', + options: { a: { description: 'A' } }, + }), onDone: () => ({ target: 'done' }), on: { cancel: () => ({ target: 'cancelled' }), @@ -694,14 +697,340 @@ describe('type: choice', () => { }, }); - // Can send event to choice state (preemption) const state = machine.getInitialState(); const next = machine.transition(state, { type: 'cancel' }); expect(next.value).toBe('cancelled'); + expect(called).toBe(false); }); }); describe('messages and always', () => { + test('states expose resolved generation fields', () => { + const search = async () => 'result'; + const machine = createAgentMachine({ + id: 'generation-fields', + schemas: { + input: z.object({ task: z.string() }), + }, + context: (input) => ({ task: input.task, phase: 'read' }), + messages: (input) => [{ role: 'user', content: input.task }], + initial: 'planning', + states: { + planning: { + model: 'test-model', + system: 'Plan carefully.', + prompt: ({ context }) => `Plan: ${context.task}`, + tools: { search }, + toolChoice: 'auto', + on: { + ready: { + target: 'implementing', + context: { phase: 'write' }, + messages: [ + { + role: 'system', + content: 'Writing is allowed now.', + }, + ], + }, + }, + }, + implementing: { + prompt: ({ context }) => `Implement: ${context.task}`, + tools: { + writeFile: async () => 'ok', + }, + on: { + done: { target: 'done' }, + }, + }, + done: { type: 'final' }, + }, + }); + + const planning = machine.getInitialState({ task: 'Fix bug' }); + expect(planning.prompt).toBe('Plan: Fix bug'); + expect(planning.model).toBe('test-model'); + expect(planning.system).toBe('Plan carefully.'); + expect(Object.keys(planning.tools ?? {})).toEqual(['search', 'event.ready']); + expect(planning.toolChoice).toBe('auto'); + + const implementing = machine.transition(planning, { type: 'ready' }); + expect(implementing.prompt).toBe('Implement: Fix bug'); + expect(implementing.model).toBeUndefined(); + expect(Object.keys(implementing.tools ?? {})).toEqual([ + 'writeFile', + 'event.done', + ]); + expect(implementing.messages.at(-1)).toEqual({ + role: 'system', + content: 'Writing is allowed now.', + }); + }); + + test('generation fields resolve from the unresolved snapshot', () => { + const read = async () => 'read'; + const write = async () => 'write'; + const seenSnapshots: Array<{ + value: string; + hasPrompt: boolean; + hasTools: boolean; + }> = []; + const machine = createAgentMachine({ + id: 'snapshot-resolvers', + schemas: { + input: z.object({ task: z.string(), mode: z.enum(['read', 'write']) }), + }, + context: (input) => ({ task: input.task, mode: input.mode }), + messages: (input) => [{ role: 'user', content: `Task: ${input.task}` }], + initial: 'working', + states: { + working: { + model: ({ snapshot }) => + snapshot.context.mode === 'write' ? 'write-model' : 'read-model', + system: ({ snapshot }) => `State: ${snapshot.value}`, + prompt: ({ snapshot }) => { + seenSnapshots.push({ + value: snapshot.value, + hasPrompt: 'prompt' in snapshot, + hasTools: 'tools' in snapshot, + }); + + return [ + `Mode: ${snapshot.context.mode}`, + `Messages: ${snapshot.messages.length}`, + `Task: ${snapshot.context.task}`, + ].join('\n'); + }, + tools: ({ snapshot }) => + snapshot.context.mode === 'write' ? { read, write } : { read }, + toolChoice: ({ snapshot }) => + snapshot.context.mode === 'write' ? 'required' : 'auto', + on: { + done: { target: 'done' }, + }, + }, + done: { type: 'final' }, + }, + }); + + const state = machine.getInitialState({ task: 'Fix bug', mode: 'write' }); + + expect(state.model).toBe('write-model'); + expect(state.system).toBe('State: working'); + expect(state.prompt).toBe('Mode: write\nMessages: 1\nTask: Fix bug'); + expect(Object.keys(state.tools ?? {})).toEqual(['read', 'write', 'event.done']); + expect(state.toolChoice).toBe('required'); + expect(seenSnapshots).toEqual([ + { + value: 'working', + hasPrompt: false, + hasTools: false, + }, + ]); + }); + + test('event tools are namespaced and use event schemas', async () => { + const userTool = async () => 'user tool'; + const machine = createAgentMachine({ + id: 'event-tools', + schemas: { + events: { + PLAN_READY: z.object({ + type: z.literal('PLAN_READY'), + rationale: z.string(), + }), + }, + }, + context: () => ({}), + initial: 'planning', + states: { + planning: { + tools: { PLAN_READY: userTool }, + on: { + PLAN_READY: { target: 'done' }, + }, + }, + done: { type: 'final' }, + }, + }); + + const state = machine.getInitialState(); + expect(state.tools?.PLAN_READY).toBe(userTool); + expect(state.tools?.['event.PLAN_READY']).toMatchObject({ + description: "Transition with event 'PLAN_READY'.", + schemas: { input: expect.any(Object) }, + }); + + const eventTool = state.tools?.['event.PLAN_READY'] as { + execute(input: Record): Promise>; + }; + await expect( + eventTool.execute({ rationale: 'plan is ready' }) + ).resolves.toEqual({ + type: 'PLAN_READY', + rationale: 'plan is ready', + }); + }); + + test('prompt states with no user tools still expose event tools', () => { + const machine = createAgentMachine({ + id: 'event-only-tools', + context: () => ({}), + initial: 'waiting', + states: { + waiting: { + prompt: 'Wait for completion.', + on: { + done: { target: 'done' }, + }, + }, + done: { type: 'final' }, + }, + }); + + expect(Object.keys(machine.getInitialState().tools ?? {})).toEqual([ + 'event.done', + ]); + }); + + test('on events become prefixed event tools in prompt states by default', () => { + const machine = createAgentMachine({ + id: 'prefixed-event-tools', + context: () => ({}), + initial: 'planning', + states: { + planning: { + prompt: 'Plan and choose a transition.', + on: { + PLAN_READY: { target: 'done' }, + FAIL: { target: 'failed' }, + }, + }, + done: { type: 'final' }, + failed: { type: 'final' }, + }, + }); + + expect(Object.keys(machine.getInitialState().tools ?? {})).toEqual([ + 'event.PLAN_READY', + 'event.FAIL', + ]); + }); + + test('non-generative states do not expose on events as tools', () => { + const machine = createAgentMachine({ + id: 'non-generative-events', + context: () => ({}), + initial: 'waiting', + states: { + waiting: { + on: { + APPROVED: { target: 'done' }, + }, + }, + done: { type: 'final' }, + }, + }); + + const waiting = machine.getInitialState(); + expect(waiting.tools).toBeUndefined(); + + const done = machine.transition(waiting, { type: 'APPROVED' }); + expect(done.value).toBe('done'); + }); + + test('external events are valid transitions but excluded from event tools', () => { + const machine = createAgentMachine({ + id: 'external-events', + externalEvents: ['APPROVED', 'REJECTED'], + schemas: { + events: { + PLAN_READY: z.object({}), + APPROVED: z.object({}), + REJECTED: z.object({}), + }, + }, + context: () => ({}), + initial: 'planning', + states: { + planning: { + prompt: 'Prepare a plan.', + on: { + PLAN_READY: { target: 'awaitingApproval' }, + }, + }, + awaitingApproval: { + prompt: 'Wait for approval.', + on: { + APPROVED: { target: 'done' }, + REJECTED: { target: 'planning' }, + }, + }, + done: { type: 'final' }, + }, + }); + + const planning = machine.getInitialState(); + expect(Object.keys(planning.tools ?? {})).toEqual(['event.PLAN_READY']); + + const awaitingApproval = machine.transition(planning, { + type: 'PLAN_READY', + }); + expect(awaitingApproval.value).toBe('awaitingApproval'); + expect(awaitingApproval.tools).toBeUndefined(); + + const done = machine.transition(awaitingApproval, { type: 'APPROVED' }); + expect(done.value).toBe('done'); + }); + + test('invoke cannot be combined with generation fields', () => { + expect(() => + createAgentMachine({ + id: 'invoke-generation-conflict', + context: () => ({}), + initial: 'working', + states: { + working: { + prompt: 'Generate something.', + invoke: async () => ({}), + }, + }, + }) + ).toThrow( + "State 'working' cannot combine invoke with prompt, system, tools, or toolChoice" + ); + }); + + test('snapshots omit executable generation fields', async () => { + const machine = createAgentMachine({ + id: 'snapshot-generation-fields', + context: () => ({}), + initial: 'waiting', + states: { + waiting: { + prompt: 'Use the tool.', + tools: { search: async () => 'result' }, + on: { done: { target: 'done' } }, + }, + done: { type: 'final' }, + }, + }); + + const state = machine.getInitialState(); + expect(state.prompt).toBe('Use the tool.'); + expect(state.tools).toBeDefined(); + + const snapshots = []; + for await (const snapshot of machine.stream(state)) { + snapshots.push(snapshot); + break; + } + + expect(snapshots[0]).not.toHaveProperty('prompt'); + expect(snapshots[0]).not.toHaveProperty('tools'); + }); + test('messages are passed through invoke, onDone, always, and output', async () => { const machine = createAgentMachine({ id: 'messages-always', @@ -720,16 +1049,16 @@ describe('messages and always', () => { initial: 'generating', states: { generating: { - resultSchema: z.object({ text: z.string() }), + schemas: { output: z.object({ text: z.string() }) }, invoke: async ({ messages }) => ({ text: `reply to ${messages.at(-1)?.content}`, }), - onDone: ({ result, context, messages }) => ({ + onDone: ({ output, context, messages }) => ({ target: 'checking', context: { attempts: context.attempts + 1 }, messages: messages.concat({ role: 'assistant', - content: result.text, + content: output.text, }), }), }, @@ -957,16 +1286,16 @@ describe('type inference', () => { initial: 'work', states: { work: { - resultSchema: z.object({ doubled: z.number() }), + schemas: { output: z.object({ doubled: z.number() }) }, invoke: async ({ context }) => { context.n satisfies number; // @ts-expect-error — 'nope' does not exist context.nope; return { doubled: context.n * 2 }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { n: result.doubled }, + context: { n: output.doubled }, }), }, done: { type: 'final' }, @@ -1149,17 +1478,16 @@ describe('type inference', () => { ).toThrow(); }); - // ─── inputSchema per state ─── + // ─── schemas.input per state ─── - test('input typed per state from inputSchema', async () => { + test('input typed per state from schemas.input', async () => { const machine = createAgentMachine({ id: 't', context: () => ({ result: '' }), initial: 'a', states: { a: { - inputSchema: z.object({ count: z.number() }), - resultSchema: z.object({ doubled: z.number() }), + schemas: { input: z.object({ count: z.number() }), output: z.object({ doubled: z.number() }) }, invoke: async ({ input }) => { input.count satisfies number; // @ts-expect-error — count is number not string @@ -1168,15 +1496,14 @@ describe('type inference', () => { input.name; return { doubled: input.count * 2 }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'b', input: { name: 'hello' }, - context: { result: String(result.doubled) }, + context: { result: String(output.doubled) }, }), }, b: { - inputSchema: z.object({ name: z.string() }), - resultSchema: z.object({ greeting: z.string() }), + schemas: { input: z.object({ name: z.string() }), output: z.object({ greeting: z.string() }) }, invoke: async ({ input }) => { input.name satisfies string; // @ts-expect-error — name is string not number @@ -1185,9 +1512,9 @@ describe('type inference', () => { input.count; return { greeting: `hi ${input.name}` }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { result: result.greeting }, + context: { result: output.greeting }, }), }, done: { @@ -1205,7 +1532,7 @@ describe('type inference', () => { expect(r.status === 'done' && r.output).toEqual({ result: 'hi hello' }); }); - test('no inputSchema → input is Record', () => { + test('no schemas.input → input is Record', () => { createAgentMachine({ id: 't', context: () => ({}), @@ -1221,33 +1548,65 @@ describe('type inference', () => { }); }); - // ─── type: 'choice' context typing ─── + test('state resolver snapshot is typed from context and input', () => { + createAgentMachine({ + id: 't', + schemas: { + input: z.object({ task: z.string() }), + }, + context: (input) => ({ task: input.task, count: 1 }), + initial: 'working', + states: { + working: { + schemas: { input: z.object({ attempt: z.number() }) }, + prompt: ({ snapshot, context, input }) => { + snapshot.value satisfies string; + snapshot.context.task satisfies string; + context.count satisfies number; + input.attempt satisfies number; + // @ts-expect-error — resolved prompt is not present while resolving + snapshot.prompt; + // @ts-expect-error — attempt is number not string + input.attempt satisfies string; + return `${snapshot.value}: ${context.task}`; + }, + on: { + done: { target: 'done' }, + }, + }, + done: { type: 'final' }, + }, + }); + }); + + // ─── decide helper context typing ─── - test('type: choice gets typed context in prompt and onDone', () => { + test('decide helper gets typed context in invoke and onDone', () => { const adapter = mockAdapter([{ choice: 'a' }]); const machine = createAgentMachine({ id: 't', context: () => ({ topic: 'cats', result: '' }), - adapter, initial: 'choosing', states: { choosing: { - type: 'choice', - resultSchema: choiceResultSchema, - model: 'test', - prompt: ({ context }) => { + schemas: { output: choiceResultSchema }, + invoke: async ({ context }) => { context.topic satisfies string; // @ts-expect-error — 'nope' does not exist context.nope; - return `About ${context.topic}`; + return decide({ + adapter, + model: 'test', + prompt: `About ${context.topic}`, + options: { a: { description: 'A' } }, + }); }, - options: { a: { description: 'A' } }, - onDone: ({ result, context }) => { - result.choice satisfies string; + onDone: ({ output, context }) => { + output.choice satisfies string; // @ts-expect-error - result.nope; + output.nope; context.topic satisfies string; - return { target: 'done', context: { result: result.choice } }; + return { target: 'done', context: { result: output.choice } }; }, }, done: { type: 'final' }, @@ -1297,26 +1656,26 @@ describe('type inference', () => { machine.getInitialState(undefined); }); - // ─── resultSchema ─── + // ─── schemas.output ─── - test('resultSchema types invoke return and onDone result', () => { + test('schemas.output types invoke return and onDone output', () => { createAgentMachine({ id: 't', context: () => ({ total: 0 }), initial: 'work', states: { work: { - resultSchema: z.object({ value: z.number() }), + schemas: { output: z.object({ value: z.number() }) }, invoke: async () => { - // return type must match resultSchema + // return type must match schemas.output return { value: 42 }; }, - onDone: ({ result }) => { - // result is typed from resultSchema - result.value satisfies number; + onDone: ({ output }) => { + // output is typed from schemas.output + output.value satisfies number; // @ts-expect-error — 'nope' does not exist on result - result.nope; - return { target: 'done', context: { total: result.value } }; + output.nope; + return { target: 'done', context: { total: output.value } }; }, }, done: { type: 'final' }, @@ -1324,7 +1683,7 @@ describe('type inference', () => { }); }); - test('no resultSchema → onDone result is inferred from invoke', () => { + test('no schemas.output → onDone output is inferred from invoke', () => { createAgentMachine({ id: 't', context: () => ({}), @@ -1332,10 +1691,10 @@ describe('type inference', () => { states: { work: { invoke: async () => ({ anything: true }), - onDone: ({ result }) => { - result.anything satisfies boolean; + onDone: ({ output }) => { + output.anything satisfies boolean; // @ts-expect-error — 'choice' does not exist on invoke result - result.choice; + output.choice; return { target: 'done' }; }, }, @@ -1479,8 +1838,9 @@ describe('edge cases', () => { describe('createAdapter', () => { test('creates custom adapter', () => { const a = createAdapter({ - decide: async () => ({ choice: 'a', data: {} }), + generateText: async () => 'ok', }); - expect(a.decide).toBeDefined(); + expect(a.generateText).toBeDefined(); + expect('decide' in a).toBe(false); }); }); diff --git a/src/ai-sdk/index.test.ts b/src/ai-sdk/index.test.ts index 45c6669..23a573e 100644 --- a/src/ai-sdk/index.test.ts +++ b/src/ai-sdk/index.test.ts @@ -1,11 +1,11 @@ import { describe, expect, test } from 'vitest'; import { z } from 'zod'; -import { createAiSdkAdapter } from './index.js'; +import { createAiSdkAdapter, createAiSdkDecisionAdapter } from './index.js'; describe('createAiSdkAdapter', () => { test('resolves schema-less choices with a custom model resolver', async () => { const seen: Array<{ model: unknown; prompt: unknown }> = []; - const adapter = createAiSdkAdapter({ + const adapter = createAiSdkDecisionAdapter({ resolveModel: (model) => ({ providerResolved: model }) as never, generateText: async (options) => { seen.push({ @@ -41,7 +41,7 @@ describe('createAiSdkAdapter', () => { }); test('returns structured decision payloads for schema-backed options', async () => { - const adapter = createAiSdkAdapter({ + const adapter = createAiSdkDecisionAdapter({ generateText: async () => ({ output: { @@ -79,4 +79,22 @@ describe('createAiSdkAdapter', () => { reasoning: 'Need the newest API details.', }); }); + + test('creates a generation-only machine adapter', async () => { + const adapter = createAiSdkAdapter({ + generateText: async (options) => + ({ + text: `generated ${options.prompt}`, + }) as never, + }); + + await expect( + adapter.generateText?.({ + model: 'openai/gpt-5.4-nano', + messages: [], + prompt: 'reply', + }) + ).resolves.toBe('generated reply'); + expect('decide' in adapter).toBe(false); + }); }); diff --git a/src/ai-sdk/index.ts b/src/ai-sdk/index.ts index d5cb592..43b1bf8 100644 --- a/src/ai-sdk/index.ts +++ b/src/ai-sdk/index.ts @@ -1,6 +1,6 @@ import { generateText, Output } from 'ai'; import { z } from 'zod'; -import type { AgentAdapter, StandardSchemaV1 } from '../types.js'; +import type { AgentAdapter, DecideAdapter, StandardSchemaV1 } from '../types.js'; type AiSdkGenerateText = typeof generateText; type AiSdkModel = Parameters[0]['model']; @@ -11,7 +11,7 @@ export interface CreateAiSdkAdapterOptions { } /** - * Create an adapter that uses the Vercel AI SDK for decide/classify. + * Create an adapter that uses the Vercel AI SDK for generative states. * By default, model strings are passed straight through to the AI SDK. * For provider helpers such as `openai(...)`, pass `resolveModel`. */ @@ -20,6 +20,38 @@ export function createAiSdkAdapter( ): AgentAdapter { const generate = config.generateText ?? generateText; + return { + async generateText({ model, system, prompt, messages, tools, toolChoice, outputSchema }) { + const result = await generate({ + model: resolveModel(model ?? 'default', config.resolveModel), + system, + prompt, + messages: messages as any, + tools: tools as any, + toolChoice: toolChoice as any, + ...(outputSchema + ? { + output: Output.object({ + schema: toZodSchema(outputSchema), + }), + } + : {}), + }); + + const output = result as { output?: unknown; text?: string }; + return output.output ?? output.text ?? result; + }, + }; +} + +/** + * Create a decision helper adapter for decide(...) and classify(...). + */ +export function createAiSdkDecisionAdapter( + config: CreateAiSdkAdapterOptions = {} +): DecideAdapter { + const generate = config.generateText ?? generateText; + return { async decide({ model, prompt, options, reasoning }) { const optionKeys = Object.keys(options); diff --git a/src/decide.ts b/src/decide.ts index 62cec7d..10f62d7 100644 --- a/src/decide.ts +++ b/src/decide.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { validateSchemaSync } from './utils.js'; import type { - AgentAdapter, + DecideAdapter, DecideOptions, DecideResultFor, StandardSchemaV1, @@ -39,9 +39,9 @@ export async function decide< } export function requireAdapter( - adapter: AgentAdapter | undefined, + adapter: DecideAdapter | undefined, label: string -): AgentAdapter { +): DecideAdapter { if (!adapter) { throw new Error(`No adapter configured for ${label}`); } diff --git a/src/examples.test.ts b/src/examples.test.ts index cdad249..4233fd1 100644 --- a/src/examples.test.ts +++ b/src/examples.test.ts @@ -42,6 +42,9 @@ import { createRiverCrossingExample, createSimpleExample, createSqlAgentExample, + createGuardrailedBugfixWorkflowExample, + createGuardrailedIncidentResponseExample, + createUnguardedIncidentResponseExample, createSubflowExample, createSupervisorExample, createToolCallingExample, @@ -82,9 +85,9 @@ function createSseReader(response: Response) { describe('curated examples', () => { test('simple example runs to a final output', async () => { - const machine = createSimpleExample(async () => ({ - summary: 'A short summary.', - })); + const machine = createSimpleExample({ + generateText: async () => ({ summary: 'A short summary.' }), + }); const result = await machine.execute( machine.getInitialState({ text: 'Longer source text.' }) ); @@ -162,9 +165,14 @@ describe('curated examples', () => { { id: 'doc-2', content: `${question} :: second fact` }, ], }), - answer: async ({ question, documents }) => ({ - answer: `${question} => ${documents.map((document) => document.content).join(' | ')}`, - }), + adapter: { + generateText: async ({ prompt }) => ({ + answer: String(prompt) + .replace('Question: ', '') + .replace('\n\nDocuments:\n- [doc-1] ', ' => ') + .replace('\n- [doc-2] ', ' | '), + }), + }, }); const result = await machine.execute( @@ -1264,9 +1272,12 @@ describe('curated examples', () => { }); test('joke example produces a rating and acceptance flag', async () => { + const results = [ + { joke: 'A short joke about ducks.' }, + { rating: 9, explanation: 'It works.' }, + ]; const machine = createJokeExample({ - tellJoke: async () => ({ joke: 'A short joke about ducks.' }), - rateJoke: async () => ({ rating: 9, explanation: 'It works.' }), + generateText: async () => results.shift(), }); const result = await machine.execute( @@ -1889,3 +1900,52 @@ describe('curated examples', () => { } }); }); + +describe('guardrailed workflow examples', () => { + test('bugfix example exposes per-state prompts and tools', () => { + const machine = createGuardrailedBugfixWorkflowExample(); + + const planning = machine.getInitialState({ task: 'Fix divide().' }); + expect(planning.value).toBe('planning'); + expect(Object.keys(planning.tools ?? {})).toEqual( + expect.arrayContaining([ + 'Read', + 'Grep', + 'Glob', + 'LS', + 'Bash', + ]) + ); + + expect(Object.keys(planning.tools ?? {})).not.toContain('Edit'); + }); + + test('incident response example withholds destructive tools', async () => { + const machine = createGuardrailedIncidentResponseExample(); + + const diagnose = machine.getInitialState({}); + expect(diagnose.value).toBe('diagnosing'); + expect(Object.keys(diagnose.tools ?? {})).toContain('get_logs'); + expect(Object.keys(diagnose.tools ?? {})).not.toContain('delete_volume'); + + const result = await machine.execute(diagnose); + expect(result.status).toBe('pending'); + if (result.status !== 'pending') { + throw new Error('Expected approval state'); + } + + expect(result.state.value).toBe('awaitingApproval'); + expect(Object.keys(result.state.tools ?? {})).toEqual( + expect.arrayContaining(['Read', 'event.APPROVED', 'event.REJECTED']) + ); + }); + + test('unguarded incident response example exposes every API action', () => { + const machine = createUnguardedIncidentResponseExample(); + const state = machine.getInitialState({}); + + expect(state.value).toBe('working'); + expect(Object.keys(state.tools ?? {})).toContain('delete_volume'); + expect(Object.keys(state.tools ?? {})).toContain('restart_service'); + }); +}); diff --git a/src/graph/index.test.ts b/src/graph/index.test.ts index d65d2c9..d0b3fa6 100644 --- a/src/graph/index.test.ts +++ b/src/graph/index.test.ts @@ -39,12 +39,11 @@ test('exports finite states and transition edges as Stately graph JSON', () => { }, }, working: { - inputSchema: z.object({ + schemas: { input: z.object({ index: z.number(), - }), - resultSchema: z.object({ + }), output: z.object({ ok: z.boolean(), - }), + }) }, invoke: async () => ({ ok: true }), onDone: () => ({ target: 'done', diff --git a/src/http/index.test.ts b/src/http/index.test.ts index 5ddbf5f..5357153 100644 --- a/src/http/index.test.ts +++ b/src/http/index.test.ts @@ -66,17 +66,17 @@ describe('http adapter', () => { }, }, writing: { - resultSchema: z.object({ + schemas: { output: z.object({ text: z.string(), - }), + }) }, invoke: async ({ context }, enq) => { enq.emit({ type: 'textPart', delta: context.text }); return { text: context.text }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', context: { - finalText: result.text, + finalText: output.text, }, }), }, diff --git a/src/index.ts b/src/index.ts index 4df7514..9ced35d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,11 +21,15 @@ export type { AgentMachine, AgentMessage, AgentRun, + AgentResolverSnapshot, AgentSnapshot, AgentState, + AgentToolChoice, + AgentTools, ClassifyOptions, ClassifyResultFor, DecideOptions, + DecideAdapter, DecideResultFor, EmittedPart, EmittedUnion, @@ -38,11 +42,13 @@ export type { JournalEventRecord, MachineConfig, PersistedSnapshot, + ResolvableStateValue, RestoreSessionOptions, RunStore, SessionOptions, StandardSchemaV1, StateConfig, + StateResolverArgs, Trace, TransitionEvent, TransitionResult, diff --git a/src/invoke-events.test.ts b/src/invoke-events.test.ts index 0fe92df..5498bd4 100644 --- a/src/invoke-events.test.ts +++ b/src/invoke-events.test.ts @@ -24,11 +24,11 @@ test('invoke success is journaled as an internal machine event', async () => { initial: 'processing', states: { processing: { - resultSchema: z.object({ value: z.string() }), + schemas: { output: z.object({ value: z.string() }) }, invoke: async () => ({ value: 'ok' }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { result: result.value }, + context: { result: output.value }, }), }, done: { @@ -105,7 +105,7 @@ test('invalid invoke results fail without journaling a done event', async () => initial: 'processing', states: { processing: { - resultSchema: z.object({ value: z.string() }), + schemas: { output: z.object({ value: z.string() }) }, invoke: async () => ({ value: 42 } as unknown as { value: string }), }, }, diff --git a/src/langgraph-equivalents/branching.test.ts b/src/langgraph-equivalents/branching.test.ts index a06f91f..784594c 100644 --- a/src/langgraph-equivalents/branching.test.ts +++ b/src/langgraph-equivalents/branching.test.ts @@ -18,11 +18,11 @@ test('supports branching-style orchestration with plain async fan-out inside inv initial: 'analyzing', states: { analyzing: { - resultSchema: z.object({ + schemas: { output: z.object({ docs: z.string(), issues: z.string(), code: z.string(), - }), + }) }, invoke: async ({ context }) => { const [docs, issues, code] = await Promise.all([ Promise.resolve(`docs about ${context.topic}`), @@ -32,20 +32,20 @@ test('supports branching-style orchestration with plain async fan-out inside inv return { docs, issues, code }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'summarizing', - context: result, + context: output, }), }, summarizing: { // paramsschema could help here, the summary has lots of string | null - resultSchema: z.object({ summary: z.string() }), + schemas: { output: z.object({ summary: z.string() }) }, invoke: async ({ context }) => ({ summary: [context.docs, context.issues, context.code].join(' | '), }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { summary: result.summary }, + context: { summary: output.summary }, }), }, done: { diff --git a/src/langgraph-equivalents/graph.test.ts b/src/langgraph-equivalents/graph.test.ts index 33b1b45..d6de3f1 100644 --- a/src/langgraph-equivalents/graph.test.ts +++ b/src/langgraph-equivalents/graph.test.ts @@ -9,27 +9,27 @@ test('supports multi-step workflow accumulation like a sequential state graph', initial: 'node1', states: { node1: { - resultSchema: z.object({ messages: z.array(z.string()) }), + schemas: { output: z.object({ messages: z.array(z.string()) }) }, invoke: async () => ({ messages: ['from node1'] }), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'node2', - context: { messages: [...context.messages, ...result.messages] }, + context: { messages: [...context.messages, ...output.messages] }, }), }, node2: { - resultSchema: z.object({ messages: z.array(z.string()) }), + schemas: { output: z.object({ messages: z.array(z.string()) }) }, invoke: async () => ({ messages: ['from node2'] }), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'node3', - context: { messages: [...context.messages, ...result.messages] }, + context: { messages: [...context.messages, ...output.messages] }, }), }, node3: { - resultSchema: z.object({ messages: z.array(z.string()) }), + schemas: { output: z.object({ messages: z.array(z.string()) }) }, invoke: async () => ({ messages: ['from node3'] }), - onDone: ({ result, context }) => ({ + onDone: ({ output, context }) => ({ target: 'done', - context: { messages: [...context.messages, ...result.messages] }, + context: { messages: [...context.messages, ...output.messages] }, }), }, done: { @@ -63,9 +63,9 @@ test('supports conditional routing with explicit machine transitions', async () initial: 'routeRequest', states: { routeRequest: { - resultSchema: z.object({ + schemas: { output: z.object({ route: z.enum(['billing', 'general']), - }), + }) }, invoke: async ({ context }) => { const route = context.request.toLowerCase().includes('refund') ? 'billing' @@ -73,25 +73,25 @@ test('supports conditional routing with explicit machine transitions', async () return { route } as const; }, - onDone: ({ result }) => ({ - target: result.route, - context: { route: result.route }, + onDone: ({ output }) => ({ + target: output.route, + context: { route: output.route }, }), }, billing: { - resultSchema: z.object({ handledBy: z.literal('billing') }), + schemas: { output: z.object({ handledBy: z.literal('billing') }) }, invoke: async () => ({ handledBy: 'billing' as const }), // why do we need to cast to const here? - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { handledBy: result.handledBy }, + context: { handledBy: output.handledBy }, }), }, general: { - resultSchema: z.object({ handledBy: z.literal('general') }), + schemas: { output: z.object({ handledBy: z.literal('general') }) }, invoke: async () => ({ handledBy: 'general' as const }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { handledBy: result.handledBy }, + context: { handledBy: output.handledBy }, }), }, done: { diff --git a/src/langgraph-equivalents/hitl.test.ts b/src/langgraph-equivalents/hitl.test.ts index 3cf8f6e..9f82a76 100644 --- a/src/langgraph-equivalents/hitl.test.ts +++ b/src/langgraph-equivalents/hitl.test.ts @@ -20,13 +20,13 @@ test('supports human-in-the-loop review with explicit pending states and externa initial: 'drafting', states: { drafting: { - resultSchema: z.object({ draft: z.string() }), + schemas: { output: z.object({ draft: z.string() }) }, invoke: async ({ context }) => ({ draft: `Draft for ${context.task}${context.notes.length ? ` (${context.notes.join(', ')})` : ''}`, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'review', - context: { draft: result.draft }, + context: { draft: output.draft }, }), }, review: { diff --git a/src/langgraph-equivalents/map-reduce.test.ts b/src/langgraph-equivalents/map-reduce.test.ts index 34e0762..7503c33 100644 --- a/src/langgraph-equivalents/map-reduce.test.ts +++ b/src/langgraph-equivalents/map-reduce.test.ts @@ -17,17 +17,17 @@ test('supports map-reduce style orchestration with dynamic work items inside inv initial: 'planning', states: { planning: { - resultSchema: z.object({ subjects: z.array(z.string()) }), + schemas: { output: z.object({ subjects: z.array(z.string()) }) }, invoke: async ({ context }) => ({ subjects: [`${context.topic} basics`, `${context.topic} advanced`], }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'mapping', - context: { subjects: result.subjects }, + context: { subjects: output.subjects }, }), }, mapping: { - resultSchema: z.object({ jokes: z.array(z.string()) }), + schemas: { output: z.object({ jokes: z.array(z.string()) }) }, invoke: async ({ context }) => { const jokes = await Promise.all( context.subjects.map(async (subject) => `joke about ${subject}`) @@ -35,19 +35,19 @@ test('supports map-reduce style orchestration with dynamic work items inside inv return { jokes }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'reducing', - context: { jokes: result.jokes }, + context: { jokes: output.jokes }, }), }, reducing: { - resultSchema: z.object({ bestJoke: z.string() }), + schemas: { output: z.object({ bestJoke: z.string() }) }, invoke: async ({ context }) => ({ bestJoke: context.jokes[0] ?? '', }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { bestJoke: result.bestJoke }, + context: { bestJoke: output.bestJoke }, }), }, done: { diff --git a/src/langgraph-equivalents/persistence.test.ts b/src/langgraph-equivalents/persistence.test.ts index be6f53f..efa31ae 100644 --- a/src/langgraph-equivalents/persistence.test.ts +++ b/src/langgraph-equivalents/persistence.test.ts @@ -25,13 +25,13 @@ test('persists and restores a long-running approval workflow', async () => { }, }, summarize: { - resultSchema: z.object({ summary: z.string() }), + schemas: { output: z.object({ summary: z.string() }) }, invoke: async ({ context }) => ({ summary: context.approved ? 'approved summary' : 'rejected summary', }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { summary: result.summary }, + context: { summary: output.summary }, }), }, done: { diff --git a/src/langgraph-equivalents/rag.test.ts b/src/langgraph-equivalents/rag.test.ts index c896759..8cc5f8a 100644 --- a/src/langgraph-equivalents/rag.test.ts +++ b/src/langgraph-equivalents/rag.test.ts @@ -9,9 +9,14 @@ test('rag workflow retrieves documents and synthesizes a grounded answer', async { id: 'doc-2', content: `${question} :: second fact` }, ], }), - answer: async ({ question, documents }) => ({ - answer: `${question} => ${documents.map((document) => document.content).join(' | ')}`, - }), + adapter: { + generateText: async ({ prompt }) => ({ + answer: String(prompt) + .replace('Question: ', '') + .replace('\n\nDocuments:\n- [doc-1] ', ' => ') + .replace('\n- [doc-2] ', ' | '), + }), + }, }); const result = await machine.execute( diff --git a/src/langgraph-equivalents/streaming.test.ts b/src/langgraph-equivalents/streaming.test.ts index 348488a..e35d997 100644 --- a/src/langgraph-equivalents/streaming.test.ts +++ b/src/langgraph-equivalents/streaming.test.ts @@ -30,15 +30,15 @@ test('streams live invoke output while preserving durable state history', async initial: 'write', states: { write: { - resultSchema: z.object({ text: z.string() }), + schemas: { output: z.object({ text: z.string() }) }, invoke: async (_args, enq) => { enq.emit({ type: 'textPart', delta: 'hello' }); enq.emit({ type: 'textPart', delta: ' world' }); return { text: 'hello world' }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { text: result.text }, + context: { text: output.text }, }), }, done: { diff --git a/src/langgraph-equivalents/subflow.test.ts b/src/langgraph-equivalents/subflow.test.ts index 4da8014..15b0370 100644 --- a/src/langgraph-equivalents/subflow.test.ts +++ b/src/langgraph-equivalents/subflow.test.ts @@ -15,13 +15,13 @@ test('supports subflow composition by executing a child machine inside a parent initial: 'researching', states: { researching: { - resultSchema: z.object({ bullets: z.array(z.string()) }), + schemas: { output: z.object({ bullets: z.array(z.string()) }) }, invoke: async ({ context }) => ({ bullets: [`fact about ${context.topic}`, `another fact about ${context.topic}`], }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { bullets: result.bullets }, + context: { bullets: output.bullets }, }), }, done: { @@ -44,7 +44,7 @@ test('supports subflow composition by executing a child machine inside a parent initial: 'researching', states: { researching: { - resultSchema: z.object({ bullets: z.array(z.string()) }), + schemas: { output: z.object({ bullets: z.array(z.string()) }) }, invoke: async ({ context }) => { const result = await childMachine.execute( childMachine.getInitialState({ topic: context.topic }) @@ -58,19 +58,19 @@ test('supports subflow composition by executing a child machine inside a parent bullets: (result.output as { bullets: string[] }).bullets, }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'writing', - context: { bullets: result.bullets }, + context: { bullets: output.bullets }, }), }, writing: { - resultSchema: z.object({ draft: z.string() }), + schemas: { output: z.object({ draft: z.string() }) }, invoke: async ({ context }) => ({ draft: `${context.topic}: ${context.bullets.join('; ')}`, }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { draft: result.draft }, + context: { draft: output.draft }, }), }, done: { diff --git a/src/langgraph-equivalents/tool-calling.test.ts b/src/langgraph-equivalents/tool-calling.test.ts index 6107041..1b781f0 100644 --- a/src/langgraph-equivalents/tool-calling.test.ts +++ b/src/langgraph-equivalents/tool-calling.test.ts @@ -46,7 +46,7 @@ test('supports tool-call style invokes with live tool events and final output', initial: 'checkingWeather', states: { checkingWeather: { - resultSchema: z.object({ forecast: z.string() }), + schemas: { output: z.object({ forecast: z.string() }) }, invoke: async ({ context }, enq) => { enq.emit({ type: 'toolCall', @@ -77,9 +77,9 @@ test('supports tool-call style invokes with live tool events and final output', return output; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { forecast: result.forecast }, + context: { forecast: output.forecast }, }), }, done: { diff --git a/src/machine.ts b/src/machine.ts index 01d85bf..8e84b8f 100644 --- a/src/machine.ts +++ b/src/machine.ts @@ -1,6 +1,9 @@ import type { AgentMachine, AgentMessage, + AgentResolverSnapshot, + AgentToolChoice, + AgentTools, AgentSnapshot, AgentState, EmittedPart, @@ -22,6 +25,7 @@ import { isAlwaysEventType, isDoneInvokeEventType, isErrorInvokeEventType, + isReservedInternalEventType, resolveInitial, resolveStateConfig, serializeError, @@ -30,13 +34,31 @@ import { import type { StateConfigAny } from './utils.js'; // ─── Type helpers ─── -/** Result type for onDone: typed from invoke return or resultSchema when present */ -type OnDoneResult = NoInfer; +/** Output type for onDone: typed from invoke return or state schemas.output when present */ +type OnDoneOutput = NoInfer; type EventFor = E extends keyof TEvents & string ? { type: E } & EventPayload> : { type: E & string; [k: string]: unknown }; +type StateResolverArgs< + TContext extends Record, + TInput, +> = { + snapshot: AgentResolverSnapshot; + context: TContext; + messages: AgentMessage[]; + input: NoInfer; +}; + +type ResolvableStateValue< + TValue, + TContext extends Record, + TInput, +> = + | TValue + | ((args: StateResolverArgs) => TValue); + type StateNodeDef< TState, TContext extends Record, @@ -47,16 +69,18 @@ type StateNodeDef< TInputMap extends Record, TOutput, > = { - type?: 'final' | 'choice'; - inputSchema?: StandardSchemaV1; - resultSchema?: StandardSchemaV1; + type?: 'final'; + schemas?: { + input?: StandardSchemaV1; + output?: StandardSchemaV1; + }; invoke?: (args: { context: TContext; messages: AgentMessage[]; input: NoInfer; signal?: AbortSignal; }, enq: { emit(part: EmittedPart): void }) => Promise; - onDone?: (args: { result: OnDoneResult; context: TContext; messages: AgentMessage[] }) => TransitionResult; + onDone?: (args: { output: OnDoneOutput; context: TContext; messages: AgentMessage[] }) => TransitionResult; always?: TransitionResult | ((args: { context: TContext; messages: AgentMessage[]; @@ -69,11 +93,12 @@ type StateNodeDef< }, enq: { emit(part: EmittedPart): void }) => TransitionResult) }; events?: Record; output?: (args: { context: TContext; messages: AgentMessage[] }) => NoInfer; - model?: string; + model?: ResolvableStateValue; adapter?: import('./types.js').AgentAdapter; - prompt?: string | ((args: { context: TContext; messages: AgentMessage[]; input: NoInfer }) => string); - options?: Record; - reasoning?: boolean; + prompt?: ResolvableStateValue; + system?: ResolvableStateValue; + tools?: ResolvableStateValue; + toolChoice?: ResolvableStateValue; }; type StatesMap< @@ -116,6 +141,7 @@ export function createAgentMachine< context: (input: NoInfer) => NoInfer; messages?: AgentMessage[] | ((input: NoInfer) => AgentMessage[]); adapter?: import('./types.js').AgentAdapter; + externalEvents?: readonly (keyof TEvents & string)[]; initial: | (keyof TInputMap & keyof TResultMap & string) | ((args: { context: NoInfer }) => { @@ -146,6 +172,7 @@ export function createAgentMachine< context: (input: NoInfer) => TContext; messages?: AgentMessage[] | ((input: NoInfer) => AgentMessage[]); adapter?: import('./types.js').AgentAdapter; + externalEvents?: readonly (keyof TEvents & string)[]; initial: | (keyof TInputMap & keyof TResultMap & string) | ((args: { context: TContext }) => { @@ -175,6 +202,7 @@ export function createAgentMachine< context: (...args: any[]) => TContext; messages?: AgentMessage[] | ((input: unknown) => AgentMessage[]); adapter?: import('./types.js').AgentAdapter; + externalEvents?: readonly (keyof TEvents & string)[]; initial: | (keyof TInputMap & keyof TResultMap & string) | ((args: { context: TContext }) => { @@ -190,8 +218,30 @@ export function createAgentMachine( machineConfig: MachineConfig ): AgentMachine { const cfg = machineConfig as MachineConfig; + assertValidConfig(cfg); type SnapshotRuntime = { sessionId: string; createdAt: number }; + const EVENT_TOOL_PREFIX = 'event.'; + + function assertValidConfig(config: MachineConfig) { + for (const [stateValue, stateConfig] of Object.entries(config.states)) { + if (!stateConfig.invoke) { + continue; + } + + const hasGenerationFields = + stateConfig.prompt !== undefined + || stateConfig.system !== undefined + || stateConfig.tools !== undefined + || stateConfig.toolChoice !== undefined; + + if (hasGenerationFields) { + throw new Error( + `State '${stateValue}' cannot combine invoke with prompt, system, tools, or toolChoice` + ); + } + } + } function createSnapshotRuntime(state: AgentState) { if (state.sessionId && state.createdAt !== undefined) { @@ -217,11 +267,109 @@ export function createAgentMachine( state: AgentState, runtime: SnapshotRuntime ): AgentState { - return { + return resolveStateFields({ ...state, sessionId: runtime.sessionId, createdAt: runtime.createdAt, + }); + } + + function withoutResolvedFields(state: AgentState): AgentState { + const { + model: _model, + prompt: _prompt, + system: _system, + tools: _tools, + toolChoice: _toolChoice, + ...rest + } = state; + + return rest; + } + + function resolveStateFields(state: AgentState): AgentState { + const base = withoutResolvedFields(state); + const sc = resolveStateConfig(cfg, base.value); + const input = getInput(base.value, base.input); + const args = { + snapshot: base, + context: base.context, + messages: base.messages, + input, + }; + + const model = + typeof sc.model === 'function' + ? sc.model(args) + : sc.model; + const prompt = + typeof sc.prompt === 'function' + ? sc.prompt(args) + : sc.prompt; + const system = + typeof sc.system === 'function' + ? sc.system(args) + : sc.system; + const tools = + typeof sc.tools === 'function' + ? sc.tools(args) + : sc.tools; + const toolChoice = + typeof sc.toolChoice === 'function' + ? sc.toolChoice(args) + : sc.toolChoice; + const eventTools = getEventTools(base.value); + const resolvedTools = { + ...(tools ?? {}), + ...eventTools, }; + + return { + ...base, + ...(model !== undefined ? { model } : {}), + ...(prompt !== undefined ? { prompt } : {}), + ...(system !== undefined ? { system } : {}), + ...(Object.keys(resolvedTools).length > 0 ? { tools: resolvedTools } : {}), + ...(toolChoice !== undefined ? { toolChoice } : {}), + }; + } + + function getEventTools(value: string): AgentTools { + const sc = resolveStateConfig(cfg, value); + if (!sc.on || !isGenerativeState(sc)) { + return {}; + } + + const tools: AgentTools = {}; + const externalEvents = new Set(cfg.externalEvents ?? []); + + for (const eventType of Object.keys(sc.on)) { + if (isReservedInternalEventType(eventType) || externalEvents.has(eventType)) { + continue; + } + + const schema = findEventSchema(cfg, value, eventType); + + tools[`${EVENT_TOOL_PREFIX}${eventType}`] = { + description: `Transition with event '${eventType}'.`, + ...(schema ? { schemas: { input: schema },} : {}), + execute: async (input: unknown = {}) => ({ + ...(input && typeof input === 'object' ? input : {}), + type: eventType, + }), + }; + } + + return tools; + } + + function isGenerativeState(sc: StateConfigAny): boolean { + return ( + sc.prompt !== undefined + || sc.system !== undefined + || sc.tools !== undefined + || sc.toolChoice !== undefined + ); } function getInitialState(...args: [input?: unknown]): AgentState { @@ -243,13 +391,13 @@ export function createAgentMachine( throw new Error('Initial transition must specify a target state'); } - return { + return resolveStateFields({ value: init.target, context: init.context ? { ...context, ...init.context } : context, messages: init.messages ?? messages, status: 'active', input: init.input ? { [init.target]: init.input } : {}, - }; + }); } function resolveState(raw: { @@ -263,7 +411,7 @@ export function createAgentMachine( output?: unknown; error?: unknown; }): AgentState { - return { + return resolveStateFields({ value: raw.value, context: raw.context, messages: raw.messages ?? [], @@ -273,7 +421,7 @@ export function createAgentMachine( createdAt: raw.createdAt, output: raw.output, error: raw.error, - }; + }); } function transition( @@ -299,17 +447,17 @@ export function createAgentMachine( status = state.status ): AgentState { if (result.target) { - return applyTransition(state, result); + return resolveStateFields(applyTransition(withoutResolvedFields(state), result)); } - return { - ...state, + return resolveStateFields({ + ...withoutResolvedFields(state), status, context: result.context ? { ...state.context, ...result.context } : state.context, messages: result.messages ?? state.messages, - }; + }); } function resolveHandlerResult( @@ -341,13 +489,13 @@ export function createAgentMachine( if (isDoneInvokeEventType(state.value, event.type)) { const result = 'output' in event ? event.output : undefined; - const validatedResult = sc.resultSchema - ? validateSchemaSync(sc.resultSchema, result) + const validatedOutput = sc.schemas?.output + ? validateSchemaSync(sc.schemas.output, result) : result; if (sc.onDone) { const trans = sc.onDone({ - result: validatedResult, + output: validatedOutput, context: state.context, messages: state.messages, }); @@ -363,7 +511,7 @@ export function createAgentMachine( return resolveHandlerResult(internalHandler, 'pending'); } - return { next: { ...state, status: 'pending' }, emitted }; + return { next: resolveStateFields({ ...withoutResolvedFields(state), status: 'pending' }), emitted }; } if (isAlwaysEventType(state.value, event.type)) { @@ -384,11 +532,11 @@ export function createAgentMachine( } return { - next: { - ...state, + next: resolveStateFields({ + ...withoutResolvedFields(state), status: 'error', error: 'error' in event ? event.error : undefined, - }, + }), emitted, }; } @@ -410,11 +558,11 @@ export function createAgentMachine( result: unknown ): unknown { const sc = resolveStateConfig(cfg, value); - if (!sc.resultSchema) { + if (!sc.schemas?.output) { return result; } - return validateSchemaSync(sc.resultSchema, result); + return validateSchemaSync(sc.schemas.output, result); } function validateEventPayload( @@ -477,30 +625,20 @@ export function createAgentMachine( }; } - async function createChoiceEvent(state: AgentState): Promise { - const sc = resolveStateConfig(cfg, state.value); - const adapter = sc.adapter ?? cfg.adapter; - if (!adapter) { - return { - type: `xstate.error.invoke.${state.value}`, - error: { message: `No adapter for '${state.value}'` }, - at: Date.now(), - }; - } - - const input = getInput(state.value, state.input); - const prompt = - typeof sc.prompt === 'function' - ? sc.prompt({ context: state.context, messages: state.messages, input }) - : sc.prompt; - + async function createInvokeEvent( + state: AgentState, + sc: StateConfigAny, + onEmit?: (part: EmittedPart) => void + ): Promise { try { - const result = await adapter.decide({ - model: sc.model!, - prompt: prompt as string, - options: sc.options!, - reasoning: sc.reasoning, - }); + const result = await sc.invoke!( + { + context: state.context, + messages: state.messages, + input: getInput(state.value, state.input), + }, + createEnqueue(onEmit) + ); const validatedResult = validateReplayableResult(state.value, result); return { @@ -517,20 +655,30 @@ export function createAgentMachine( } } - async function createInvokeEvent( - state: AgentState, - sc: StateConfigAny, - onEmit?: (part: EmittedPart) => void - ): Promise { + async function createGenerateEvent(state: AgentState): Promise { + const sc = resolveStateConfig(cfg, state.value); + const adapter = sc.adapter ?? cfg.adapter; + if (!adapter?.generateText) { + return { + type: `xstate.error.invoke.${state.value}`, + error: { message: `No generateText adapter for '${state.value}'` }, + at: Date.now(), + }; + } + try { - const result = await sc.invoke!( - { - context: state.context, - messages: state.messages, - input: getInput(state.value, state.input), - }, - createEnqueue(onEmit) - ); + const messages = state.prompt + ? state.messages.concat({ role: 'user', content: state.prompt }) + : state.messages; + const result = await adapter.generateText({ + model: state.model, + system: state.system, + prompt: state.prompt, + messages, + tools: state.tools, + toolChoice: state.toolChoice, + outputSchema: sc.schemas?.output, + }); const validatedResult = validateReplayableResult(state.value, result); return { @@ -563,14 +711,22 @@ export function createAgentMachine( }; } - if (sc.type === 'choice') { - return createChoiceEvent(state); - } - if (sc.invoke) { return createInvokeEvent(state, sc, onEmit); } + if ( + sc.onDone + && ( + sc.prompt !== undefined + || sc.system !== undefined + || sc.tools !== undefined + || sc.toolChoice !== undefined + ) + ) { + return createGenerateEvent(state); + } + return null; } @@ -612,7 +768,7 @@ export function createAgentMachine( const output = cfg.schemas?.output ? validateSchemaSync(cfg.schemas.output, rawOutput) : rawOutput; - return { ...state, status: 'done', output }; + return resolveStateFields({ ...withoutResolvedFields(state), status: 'done', output }); } const effectEvent = await getEffectEvent(state); @@ -621,14 +777,14 @@ export function createAgentMachine( } if (sc.on) { - return { ...state, status: 'pending' }; + return resolveStateFields({ ...withoutResolvedFields(state), status: 'pending' }); } - return { - ...state, + return resolveStateFields({ + ...withoutResolvedFields(state), status: 'error', error: `State '${state.value}' has no invoke, events, or final type`, - }; + }); } async function execute(state: AgentState): Promise { diff --git a/src/restore.test.ts b/src/restore.test.ts index 2dda9cf..b840845 100644 --- a/src/restore.test.ts +++ b/src/restore.test.ts @@ -22,13 +22,13 @@ test('restoreSession reconstructs from the latest snapshot plus replay tail', as }, }, processing: { - resultSchema: z.object({ value: z.string() }), + schemas: { output: z.object({ value: z.string() }) }, invoke: async ({ context }) => ({ value: context.approved ? 'approved' : 'rejected', }), - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { result: result.value }, + context: { result: output.value }, }), }, done: { diff --git a/src/session-runtime.test.ts b/src/session-runtime.test.ts index c8daed0..8844a1f 100644 --- a/src/session-runtime.test.ts +++ b/src/session-runtime.test.ts @@ -98,15 +98,15 @@ test('serializes concurrent sends so each event applies from the latest snapshot }, }, working: { - resultSchema: z.object({ count: z.number() }), + schemas: { output: z.object({ count: z.number() }) }, invoke: async ({ context }) => { const gate = gates[invocations++]!; await gate.promise; return { count: context.count }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'ready', - context: { count: result.count }, + context: { count: output.count }, }), }, }, diff --git a/src/streaming.test.ts b/src/streaming.test.ts index 3bdf276..a2c490a 100644 --- a/src/streaming.test.ts +++ b/src/streaming.test.ts @@ -30,16 +30,16 @@ test('returns a live run before initial invoke output and emits ephemeral parts' initial: 'writing', states: { writing: { - resultSchema: z.object({ text: z.string() }), + schemas: { output: z.object({ text: z.string() }) }, invoke: async (_args, enq) => { enq.emit({ type: 'textPart', delta: 'hel' }); enq.emit({ type: 'textPart', delta: 'lo' }); return { text: 'hello' }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { finalText: result.text }, + context: { finalText: output.text }, }), }, done: { @@ -110,15 +110,15 @@ test('does not replay prior events to late subscribers', async () => { initial: 'writing', states: { writing: { - resultSchema: z.object({ text: z.string() }), + schemas: { output: z.object({ text: z.string() }) }, invoke: async (_args, enq) => { enq.emit({ type: 'textPart', delta: 'hel' }); enq.emit({ type: 'textPart', delta: 'lo' }); return { text: 'hello' }; }, - onDone: ({ result }) => ({ + onDone: ({ output }) => ({ target: 'done', - context: { finalText: result.text }, + context: { finalText: output.text }, }), }, done: { diff --git a/src/target-types.assert.ts b/src/target-types.assert.ts index 80fde55..6cd6cae 100644 --- a/src/target-types.assert.ts +++ b/src/target-types.assert.ts @@ -44,9 +44,9 @@ createAgentMachine({ }, }, working: { - inputSchema: z.object({ + schemas: { input: z.object({ index: z.number(), - }), + }) }, }, }, schemas: { @@ -140,16 +140,16 @@ createAgentMachine({ states: { idle: { on: { - // @ts-expect-error input should be required when the target has inputSchema + // @ts-expect-error input should be required when the target has schemas.input advance: () => ({ target: 'working', }), }, }, working: { - inputSchema: z.object({ + schemas: { input: z.object({ index: z.number(), - }), + }) }, }, }, schemas: { @@ -194,7 +194,7 @@ createAgentMachine({ states: { idle: { on: { - // @ts-expect-error input should be rejected when the target has no inputSchema + // @ts-expect-error input should be rejected when the target has no schemas.input advance: () => ({ target: 'done', input: { @@ -233,9 +233,9 @@ createAgentMachine({ }, }, working: { - inputSchema: z.object({ + schemas: { input: z.object({ index: z.number(), - }), + }) }, }, }, schemas: { @@ -264,9 +264,9 @@ createAgentMachine({ }, }, working: { - inputSchema: z.object({ + schemas: { input: z.object({ index: z.number(), - }), + }) }, }, }, schemas: { diff --git a/src/types.ts b/src/types.ts index 2967dbe..7c88a04 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,6 +39,41 @@ export type AgentMessage = { [key: string]: unknown; }; +export type AgentTools = Record; + +export type AgentToolChoice = + | string + | number + | boolean + | null + | readonly unknown[] + | { [key: string]: unknown }; + +export type AgentResolverSnapshot< + TContext extends Record = Record, +> = Omit< + AgentState, + 'model' | 'prompt' | 'system' | 'tools' | 'toolChoice' +>; + +export type StateResolverArgs< + TContext extends Record, + TInput = Record, +> = { + snapshot: AgentResolverSnapshot; + context: TContext; + messages: AgentMessage[]; + input: TInput; +}; + +export type ResolvableStateValue< + TValue, + TContext extends Record, + TInput = Record, +> = + | TValue + | ((args: StateResolverArgs) => TValue); + export interface InvokeEnqueue { emit(part: EmittedPart): void; } @@ -55,6 +90,18 @@ export type { JournalEventRecord, PersistedSnapshot, RunStore } from './runtime/ // ─── Adapter ─── export interface AgentAdapter { + generateText?: (options: { + model?: string; + system?: string; + prompt?: string; + messages: AgentMessage[]; + tools?: AgentTools; + toolChoice?: unknown; + outputSchema?: StandardSchemaV1; + }) => Promise; +} + +export interface DecideAdapter { decide: (options: { model: string; prompt: string; @@ -109,26 +156,28 @@ export interface StateConfig< TTarget extends string = string, TInputByTarget extends Record = {}, > { - type?: 'final' | 'choice'; - inputSchema?: StandardSchemaV1; - resultSchema?: StandardSchemaV1; + type?: 'final'; + schemas?: { + input?: StandardSchemaV1; + output?: StandardSchemaV1; + }; invoke?: (args: { context: TContext; messages: AgentMessage[]; input: Record; signal?: AbortSignal; }, enq: InvokeEnqueue) => Promise; - onDone?: (args: { result: any; context: TContext; messages: AgentMessage[] }) => TransitionResult; + onDone?: (args: { output: any; context: TContext; messages: AgentMessage[] }) => TransitionResult; always?: TransitionResult | ((args: { context: TContext; messages: AgentMessage[]; input: Record }, enq: InvokeEnqueue) => TransitionResult); on?: Record | ((args: { event: any; context: TContext; messages: AgentMessage[] }, enq: InvokeEnqueue) => TransitionResult)>; events?: Record; output?: (args: { context: TContext; messages: AgentMessage[] }) => unknown; - // choice-specific - model?: string; + model?: ResolvableStateValue; adapter?: AgentAdapter; - prompt?: string | ((args: { context: TContext; messages: AgentMessage[]; input: Record }) => string); - options?: Record; - reasoning?: boolean; + prompt?: ResolvableStateValue; + system?: ResolvableStateValue; + tools?: ResolvableStateValue; + toolChoice?: ResolvableStateValue; } type OutputForState = TState extends { @@ -158,6 +207,11 @@ export interface AgentState< createdAt?: number; output?: TOutput; error?: unknown; + model?: string; + prompt?: string; + system?: string; + tools?: AgentTools; + toolChoice?: unknown; } // ─── Execute Result ─── @@ -320,6 +374,7 @@ export interface MachineConfig< context: (input: TInput) => TContext; messages?: AgentMessage[] | ((input: TInput) => AgentMessage[]); adapter?: AgentAdapter; + externalEvents?: readonly string[]; initial: | (keyof TStates & string) | ((args: { context: TContext }) => { target: keyof TStates & string; input?: Record }); @@ -339,7 +394,7 @@ export type DecideResultFor< export interface DecideOptions< TOptions extends Record = Record, > { - adapter?: AgentAdapter; + adapter?: DecideAdapter; model: string; prompt: string; options: TOptions; @@ -355,7 +410,7 @@ export interface ClassifyResultFor< export interface ClassifyOptions< TCategories extends Record = Record, > { - adapter?: AgentAdapter; + adapter?: DecideAdapter; model: string; prompt: string; into: TCategories; diff --git a/src/utils.ts b/src/utils.ts index b357bae..6984215 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ import type { AgentMessage, AgentState, + AgentToolChoice, InitialTransitionResult, MachineConfig, StandardSchemaResult, @@ -47,7 +48,7 @@ export function resolveStateConfig( /** Loose state config for internal runtime use */ export type StateConfigAny = { - type?: 'final' | 'choice'; + type?: 'final'; invoke?: ( args: { context: Record; @@ -56,16 +57,47 @@ export type StateConfigAny = { }, enq: { emit(part: { type: string; [key: string]: unknown }): void } ) => Promise; - onDone?: (args: { result: unknown; context: Record; messages: AgentMessage[] }) => TransitionResult; + onDone?: (args: { output: unknown; context: Record; messages: AgentMessage[] }) => TransitionResult; always?: TransitionResult | ((args: { context: Record; messages: AgentMessage[]; input: Record }, enq: { emit(part: { type: string; [key: string]: unknown }): void }) => TransitionResult); on?: Record; context: Record; messages: AgentMessage[] }, enq: { emit(part: { type: string; [key: string]: unknown }): void }) => TransitionResult)>; output?: (args: { context: Record; messages: AgentMessage[] }) => unknown; - resultSchema?: StandardSchemaV1; - model?: string; - adapter?: { decide: (...args: unknown[]) => Promise }; - prompt?: string | ((args: { context: Record; messages: AgentMessage[]; input: Record }) => string); - options?: Record; - reasoning?: boolean; + schemas?: { + input?: StandardSchemaV1; + output?: StandardSchemaV1; + }; + model?: string | ((args: { + snapshot: AgentState; + context: Record; + messages: AgentMessage[]; + input: Record; + }) => string); + adapter?: { + generateText?: (...args: unknown[]) => Promise; + }; + prompt?: string | ((args: { + snapshot: AgentState; + context: Record; + messages: AgentMessage[]; + input: Record; + }) => string); + system?: string | ((args: { + snapshot: AgentState; + context: Record; + messages: AgentMessage[]; + input: Record; + }) => string); + tools?: Record | ((args: { + snapshot: AgentState; + context: Record; + messages: AgentMessage[]; + input: Record; + }) => Record); + toolChoice?: AgentToolChoice | ((args: { + snapshot: AgentState; + context: Record; + messages: AgentMessage[]; + input: Record; + }) => unknown); events?: Record; }; diff --git a/src/xstate/index.test.ts b/src/xstate/index.test.ts index af49f28..ee15627 100644 --- a/src/xstate/index.test.ts +++ b/src/xstate/index.test.ts @@ -33,12 +33,11 @@ test('exports a serializable XState config for visualization', () => { }, }, working: { - inputSchema: z.object({ + schemas: { input: z.object({ index: z.number(), - }), - resultSchema: z.object({ + }), output: z.object({ ok: z.boolean(), - }), + }) }, invoke: async () => ({ ok: true }), onDone: () => ({ target: 'done', diff --git a/src/xstate/index.ts b/src/xstate/index.ts index 9d9b40c..32ffda8 100644 --- a/src/xstate/index.ts +++ b/src/xstate/index.ts @@ -77,10 +77,6 @@ export function toXStateVisualization(machine: AgentMachine): XStateMachineConfi } const meta: NonNullable['agent'] = {}; - if (stateConfig.type === 'choice') { - meta.type = 'choice'; - } - if (stateConfig.invoke) { meta.invoke = true; xstateState.invoke = {