├── .changeset ├── README.md ├── calm-beans-talk.md ├── config.json ├── cyan-carpets-perform.md ├── fast-donkeys-argue.md ├── grumpy-dolphins-think.md ├── light-hats-drive.md ├── long-guests-explode.md ├── nice-pants-rule.md ├── odd-kiwis-compare.md ├── old-jobs-check.md ├── old-teachers-tap.md ├── pink-eagles-deliver.md ├── pre.json ├── quiet-turtles-do.md ├── smart-yaks-pull.md ├── sweet-clouds-mix.md ├── swift-mangos-rush.md └── tough-ways-rhyme.md ├── .env.template ├── .github ├── actions │ └── ci-setup │ │ └── action.yml └── workflows │ └── release.yml ├── .gitignore ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── architecture.tldr ├── examples ├── chatbot-alt.ts ├── chatbot.ts ├── cot.ts ├── customer-service-sim.ts ├── email.ts ├── example.ts ├── executor.ts ├── goal.ts ├── helpers │ ├── helpers.ts │ ├── loader.ts │ └── runner.ts ├── joke.ts ├── jugs.ts ├── learn-from-feedback.ts ├── multi.ts ├── newspaper.ts ├── number.ts ├── raffle.ts ├── rewoo.ts ├── river-crossing.ts ├── sandbox.ts ├── serverless.ts ├── simple.ts ├── summary.ts ├── support.ts ├── ticTacToe.ts ├── todo.ts ├── tutor.ts ├── verify.ts ├── weather-agent.ts ├── weather.ts ├── wiki.ts └── word.ts ├── package.json ├── pnpm-lock.yaml ├── readme.md ├── src ├── decide.test.ts ├── decide.ts ├── expert.test.ts ├── expert.ts ├── index.ts ├── middleware.ts ├── mockModel.ts ├── policies │ ├── chainOfThoughtPolicy.ts │ ├── index.ts │ ├── shortestPathPolicy.test.ts │ ├── shortestPathPolicy.ts │ └── toolPolicy.ts ├── schemas.ts ├── templates │ └── defaultText.ts ├── text.ts ├── types.ts └── utils.ts ├── tsconfig.json └── vitest.config.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/calm-beans-talk.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': major 3 | --- 4 | 5 | "Strategy" has been renamed to "policy", and "score" has been renamed to "reward". 6 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "statelyai/agent" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/cyan-carpets-perform.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': patch 3 | --- 4 | 5 | The `name` field in `createAgent({ name: '...' })` has been renamed to `id`. 6 | -------------------------------------------------------------------------------- /.changeset/fast-donkeys-argue.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@statelyai/agent": patch 3 | --- 4 | 5 | The `description` field in `createAgent({ description: '...' })` is now used for the `system` prompt in agent decision making when a `system` prompt is not provided. 6 | -------------------------------------------------------------------------------- /.changeset/grumpy-dolphins-think.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': minor 3 | --- 4 | 5 | planner -> strategy 6 | agent.addPlan -> agent.addDecision 7 | agent.getPlans -> agent.getDecisions 8 | 9 | The word "strategy" is now used instead of "planner" to make it more clear what the agent is doing: it uses a strategy to make decisions. The method `agent.addPlan(…)` has been renamed to `agent.addDecision(…)` and `agent.getPlans(…)` has been renamed to `agent.getDecisions(…)` to reflect this change. Additionally, you specify the `strategy` instead of the `planner` when creating an agent: 10 | 11 | ```diff 12 | const agent = createAgent({ 13 | - planner: createSimplePlanner(), 14 | + strategy: createSimpleStrategy(), 15 | ... 16 | }); 17 | ``` 18 | -------------------------------------------------------------------------------- /.changeset/light-hats-drive.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': major 3 | --- 4 | 5 | - `agent.generateText(…)` is removed in favor of using the AI SDK's `generateText(…)` function with a wrapped model. 6 | - `agent.streamText(…)` is removed in favor of using the AI SDK's `streamText(…)` function with a wrapped model. 7 | - Custom adapters are removed for now, but may be re-added in future releases. Using the AI SDK is recommended for now. 8 | - Correlation IDs are removed in favor of using [OpenTelemetry with the AI SDK](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry#telemetry). 9 | - The `createAgentMiddleware(…)` function was introduced to facilitate agent message history. You can also use `agent.wrap(model)` to wrap a model with Stately Agent middleware. 10 | -------------------------------------------------------------------------------- /.changeset/long-guests-explode.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@statelyai/agent": major 3 | --- 4 | 5 | Set ai as peer dependency 6 | -------------------------------------------------------------------------------- /.changeset/nice-pants-rule.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': major 3 | --- 4 | 5 | - The `machine` and `machineHash` properties were removed from `AgentObservation` and `AgentObservationInput` 6 | - The `defaultOptions` property was removed from `Agent` 7 | - `AgentDecideOptions` was renamed to `AgentDecideInput` 8 | - The `execute` property was removed from `AgentDecideInput` 9 | - The `episodeId` optional property was added to `AgentDecideInput`, `AgentObservationInput`, and `AgentFeedbackInput` 10 | - `decisionId` was added to `AgentObservationInput` and `AgentFeedbackInput` 11 | -------------------------------------------------------------------------------- /.changeset/odd-kiwis-compare.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': minor 3 | --- 4 | 5 | You can now add **insights** for observations made. Insights are additional context about the observations made, which can be useful for agent decision making. 6 | -------------------------------------------------------------------------------- /.changeset/old-jobs-check.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': minor 3 | --- 4 | 5 | You can specify `maxAttempts` in `agent.decide({ maxAttempts: 5 })`. This will allow the agent to attempt to make a decision up to the specified number of `maxAttempts` before giving up. The default value is `2`. 6 | -------------------------------------------------------------------------------- /.changeset/old-teachers-tap.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': minor 3 | --- 4 | 5 | For feedback, the `goal`, `observationId`, and `attributes` are now required, and `feedback` and `reward` are removed since they are redundant. 6 | -------------------------------------------------------------------------------- /.changeset/pink-eagles-deliver.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': patch 3 | --- 4 | 5 | The `score` is now required for feedback: 6 | 7 | ```ts 8 | agent.addFeedback({ 9 | score: 0.5, 10 | goal: 'Win the game', 11 | observationId: '...', 12 | }); 13 | ``` 14 | -------------------------------------------------------------------------------- /.changeset/pre.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "pre", 3 | "tag": "next", 4 | "initialVersions": { 5 | "@statelyai/agent": "1.1.6" 6 | }, 7 | "changesets": [ 8 | "calm-beans-talk", 9 | "cyan-carpets-perform", 10 | "fast-donkeys-argue", 11 | "grumpy-dolphins-think", 12 | "light-hats-drive", 13 | "long-guests-explode", 14 | "nice-pants-rule", 15 | "odd-kiwis-compare", 16 | "old-jobs-check", 17 | "old-teachers-tap", 18 | "pink-eagles-deliver", 19 | "quiet-turtles-do", 20 | "smart-yaks-pull", 21 | "sweet-clouds-mix", 22 | "swift-mangos-rush", 23 | "tough-ways-rhyme" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.changeset/quiet-turtles-do.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': major 3 | --- 4 | 5 | The `state` can no longer be specified in `agent.interact(...)`, since the actual state value is already observed and passed to the `strategy` function. 6 | 7 | The `context` provided to agent decision functions, like `agent.decide({ context })` and in `agent.interact(...)`, is now used solely to override the `state.context` provided to the prompt template. 8 | -------------------------------------------------------------------------------- /.changeset/smart-yaks-pull.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': minor 3 | --- 4 | 5 | You can specify `allowedEvents` in `agent.decide(...)` to allow from a list of specific events to be sent to the agent. This is useful when using `agent.decide(...)` without a state machine. 6 | 7 | ```ts 8 | const agent = createAgent({ 9 | // ... 10 | events: { 11 | PLAY: z.object({}).describe('Play a move'), 12 | SKIP: z.object({}).describe('Skip a move'), 13 | FORFEIT: z.object({}).describe('Forfeit the game'), 14 | }, 15 | }); 16 | 17 | // ... 18 | const decision = await agent.decide({ 19 | // Don't allow the agent to send `FORFEIT` or other events 20 | allowedEvents: ['PLAY', 'SKIP'], 21 | // ... 22 | }); 23 | ``` 24 | -------------------------------------------------------------------------------- /.changeset/sweet-clouds-mix.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@statelyai/agent': patch 3 | --- 4 | 5 | The entire observed `state` must be provided, instead of only `context`, for any agent decision making functions: 6 | 7 | ```ts 8 | agent.interact(actor, (obs) => { 9 | // ... 10 | return { 11 | goal: 'Some goal', 12 | // instead of context 13 | state: obs.state, 14 | }; 15 | }); 16 | ``` 17 | -------------------------------------------------------------------------------- /.changeset/swift-mangos-rush.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@statelyai/agent": patch 3 | --- 4 | 5 | Remove `goal` from feedback input 6 | -------------------------------------------------------------------------------- /.changeset/tough-ways-rhyme.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@statelyai/agent": minor 3 | --- 4 | 5 | Add `score` and `comment` fields for feedback 6 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | 2 | # Get your OpenAI API key from: https://platform.openai.com/signup/ 3 | OPENAI_API_KEY="sk-..." 4 | -------------------------------------------------------------------------------- /.github/actions/ci-setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Workflow 2 | description: Composite action that sets up pnpm 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - uses: pnpm/action-setup@v2 7 | - uses: actions/setup-node@v4 8 | with: 9 | node-version: 20.x 10 | 11 | - name: install pnpm 12 | run: npm i pnpm@latest -g 13 | shell: bash 14 | 15 | - name: Setup npmrc 16 | run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc 17 | shell: bash 18 | 19 | - name: setup pnpm config 20 | run: pnpm config set store-dir $PNPM_CACHE_FOLDER 21 | shell: bash 22 | 23 | - run: pnpm install 24 | shell: bash 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | permissions: {} 11 | jobs: 12 | release: 13 | permissions: 14 | contents: write # to create release (changesets/action) 15 | issues: write # to post issue comments (changesets/action) 16 | pull-requests: write # to create pull request (changesets/action) 17 | 18 | if: github.repository == 'statelyai/agent' 19 | 20 | timeout-minutes: 20 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: checkout code repository 26 | uses: actions/checkout@v3 27 | with: 28 | fetch-depth: 0 29 | - name: setup node.js 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: 20 33 | - name: install pnpm 34 | run: npm i pnpm@latest -g 35 | - name: setup pnpm config 36 | run: pnpm config set store-dir $PNPM_CACHE_FOLDER 37 | - name: install dependencies 38 | run: pnpm install 39 | - name: create and publish versions 40 | uses: changesets/action@v1 41 | with: 42 | publish: pnpm run release 43 | version: pnpm run version 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist/ 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .vscode/settings.json 133 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Debug Current Test File", 9 | "autoAttachChildProcesses": true, 10 | "skipFiles": ["/**", "**/node_modules/**", "examples/**"], 11 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 12 | "args": ["run", "${relativeFile}"], 13 | "smartStep": true, 14 | "console": "integratedTerminal" 15 | }, 16 | { 17 | "type": "node", 18 | "request": "launch", 19 | "name": "Debug Current File", 20 | "program": "${file}", 21 | "cwd": "${workspaceFolder}", 22 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ts-node", 23 | "runtimeArgs": ["--transpile-only", "-r", "dotenv/config"], 24 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 25 | "sourceMaps": true, 26 | "smartStep": true, 27 | "resolveSourceMapLocations": [ 28 | "${workspaceFolder}/**", 29 | "!**/node_modules/**" 30 | ], 31 | "console": "integratedTerminal" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @statelyai/agent 2 | 3 | ## 2.0.0-next.5 4 | 5 | ### Major Changes 6 | 7 | - [`2abec3e`](https://github.com/statelyai/agent/commit/2abec3e4d13d75f0ae1840f6dd2812f8fe9fb8d1) Thanks [@davidkpiano](https://github.com/davidkpiano)! - "Strategy" has been renamed to "policy", and "score" has been renamed to "reward". 8 | 9 | - [`56ebde3`](https://github.com/statelyai/agent/commit/56ebde34ef7c897c1b845f026a5b38f41ef6a36f) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Set ai as peer dependency 10 | 11 | ### Minor Changes 12 | 13 | - [`ea0c45e`](https://github.com/statelyai/agent/commit/ea0c45e5f099ea270a8ebd49fc3f700cc5827320) Thanks [@davidkpiano](https://github.com/davidkpiano)! - You can now add **insights** for observations made. Insights are additional context about the observations made, which can be useful for agent decision making. 14 | 15 | ## 2.0.0-next.4 16 | 17 | ### Major Changes 18 | 19 | - [`4a79bac`](https://github.com/statelyai/agent/commit/4a79bacbda33340b34a4b6271c2bd0fa2673ce25) Thanks [@davidkpiano](https://github.com/davidkpiano)! - - The `machine` and `machineHash` properties were removed from `AgentObservation` and `AgentObservationInput` 20 | - The `defaultOptions` property was removed from `Agent` 21 | - `AgentDecideOptions` was renamed to `AgentDecideInput` 22 | - The `execute` property was removed from `AgentDecideInput` 23 | - The `episodeId` optional property was added to `AgentDecideInput`, `AgentObservationInput`, and `AgentFeedbackInput` 24 | - `decisionId` was added to `AgentObservationInput` and `AgentFeedbackInput` 25 | 26 | ## 2.0.0-next.3 27 | 28 | ### Major Changes 29 | 30 | - [`bf6b468`](https://github.com/statelyai/agent/commit/bf6b468d66d58bf53629d70d1b2a273948c9ba1e) Thanks [@davidkpiano](https://github.com/davidkpiano)! - The `state` can no longer be specified in `agent.interact(...)`, since the actual state value is already observed and passed to the `strategy` function. 31 | 32 | The `context` provided to agent decision functions, like `agent.decide({ context })` and in `agent.interact(...)`, is now used solely to override the `state.context` provided to the prompt template. 33 | 34 | ### Minor Changes 35 | 36 | - [`1287a6d`](https://github.com/statelyai/agent/commit/1287a6d405ed3bd6be37a61aaa9d54d963b5b1cd) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Add `score` and `comment` fields for feedback 37 | 38 | ### Patch Changes 39 | 40 | - [`5b5a8e7`](https://github.com/statelyai/agent/commit/5b5a8e7012d550f5b05d0fefc9eade7731202577) Thanks [@davidkpiano](https://github.com/davidkpiano)! - The `score` is now required for feedback: 41 | 42 | ```ts 43 | agent.addFeedback({ 44 | score: 0.5, 45 | goal: "Win the game", 46 | observationId: "...", 47 | }); 48 | ``` 49 | 50 | - [`5b5a8e7`](https://github.com/statelyai/agent/commit/5b5a8e7012d550f5b05d0fefc9eade7731202577) Thanks [@davidkpiano](https://github.com/davidkpiano)! - The entire observed `state` must be provided, instead of only `context`, for any agent decision making functions: 51 | 52 | ```ts 53 | agent.interact(actor, (obs) => { 54 | // ... 55 | return { 56 | goal: "Some goal", 57 | // instead of context 58 | state: obs.state, 59 | }; 60 | }); 61 | ``` 62 | 63 | - [`9d65d71`](https://github.com/statelyai/agent/commit/9d65d71e41f9f0f84f637ea7e0a0e22ccf67f264) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Remove `goal` from feedback input 64 | 65 | ## 2.0.0-next.2 66 | 67 | ### Minor Changes 68 | 69 | - [`4d870fe`](https://github.com/statelyai/agent/commit/4d870fe38ad0c906bafb2e0f6b2dabb745900ad3) Thanks [@davidkpiano](https://github.com/davidkpiano)! - planner -> strategy 70 | agent.addPlan -> agent.addDecision 71 | agent.getPlans -> agent.getDecisions 72 | 73 | The word "strategy" is now used instead of "planner" to make it more clear what the agent is doing: it uses a strategy to make decisions. The method `agent.addPlan(…)` has been renamed to `agent.addDecision(…)` and `agent.getPlans(…)` has been renamed to `agent.getDecisions(…)` to reflect this change. Additionally, you specify the `strategy` instead of the `planner` when creating an agent: 74 | 75 | ```diff 76 | const agent = createAgent({ 77 | - planner: createSimplePlanner(), 78 | + strategy: createSimpleStrategy(), 79 | ... 80 | }); 81 | ``` 82 | 83 | - [`f1189cb`](https://github.com/statelyai/agent/commit/f1189cb980e52fa909888d27d3300dcd913ea47f) Thanks [@davidkpiano](https://github.com/davidkpiano)! - For feedback, the `goal`, `observationId`, and `attributes` are now required, and `feedback` and `reward` are removed since they are redundant. 84 | 85 | - [`7b16326`](https://github.com/statelyai/agent/commit/7b163266c61bfc8125ed4d00924680d932001e27) Thanks [@davidkpiano](https://github.com/davidkpiano)! - You can specify `allowedEvents` in `agent.decide(...)` to allow from a list of specific events to be sent to the agent. This is useful when using `agent.decide(...)` without a state machine. 86 | 87 | ```ts 88 | const agent = createAgent({ 89 | // ... 90 | events: { 91 | PLAY: z.object({}).describe("Play a move"), 92 | SKIP: z.object({}).describe("Skip a move"), 93 | FORFEIT: z.object({}).describe("Forfeit the game"), 94 | }, 95 | }); 96 | 97 | // ... 98 | const decision = await agent.decide({ 99 | // Don't allow the agent to send `FORFEIT` or other events 100 | allowedEvents: ["PLAY", "SKIP"], 101 | // ... 102 | }); 103 | ``` 104 | 105 | ## 2.0.0-next.1 106 | 107 | ### Minor Changes 108 | 109 | - [`6a9861d`](https://github.com/statelyai/agent/commit/6a9861d959ce295114f53c95c5bdaa097348bacb) Thanks [@davidkpiano](https://github.com/davidkpiano)! - You can specify `maxAttempts` in `agent.decide({ maxAttempts: 5 })`. This will allow the agent to attempt to make a decision up to the specified number of `maxAttempts` before giving up. The default value is `2`. 110 | 111 | ### Patch Changes 112 | 113 | - [`8c3eab8`](https://github.com/statelyai/agent/commit/8c3eab8950cb85e662c6afb5d8cefb1d5ef54dd8) Thanks [@davidkpiano](https://github.com/davidkpiano)! - The `name` field in `createAgent({ name: '...' })` has been renamed to `id`. 114 | 115 | - [`8c3eab8`](https://github.com/statelyai/agent/commit/8c3eab8950cb85e662c6afb5d8cefb1d5ef54dd8) Thanks [@davidkpiano](https://github.com/davidkpiano)! - The `description` field in `createAgent({ description: '...' })` is now used for the `system` prompt in agent decision making when a `system` prompt is not provided. 116 | 117 | ## 2.0.0-next.0 118 | 119 | ### Major Changes 120 | 121 | - [#51](https://github.com/statelyai/agent/pull/51) [`574b6fd`](https://github.com/statelyai/agent/commit/574b6fd62e8a41df311aa1ea00fae60c32ad595e) Thanks [@davidkpiano](https://github.com/davidkpiano)! - - `agent.generateText(…)` is removed in favor of using the AI SDK's `generateText(…)` function with a wrapped model. 122 | - `agent.streamText(…)` is removed in favor of using the AI SDK's `streamText(…)` function with a wrapped model. 123 | - Custom adapters are removed for now, but may be re-added in future releases. Using the AI SDK is recommended for now. 124 | - Correlation IDs are removed in favor of using [OpenTelemetry with the AI SDK](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry#telemetry). 125 | - The `createAgentMiddleware(…)` function was introduced to facilitate agent message history. You can also use `agent.wrap(model)` to wrap a model with Stately Agent middleware. 126 | 127 | ## 1.1.6 128 | 129 | ### Patch Changes 130 | 131 | - [#54](https://github.com/statelyai/agent/pull/54) [`140fdce`](https://github.com/statelyai/agent/commit/140fdceb879dea5a32f243e89a8d87a9c524e454) Thanks [@XavierDK](https://github.com/XavierDK)! - - Addressing an issue where the fullStream property was not properly copied when using the spread operator (...). The problem occurred because fullStream is an iterator, and as such, it was not included in the shallow copy of the result object. 132 | - Update all packages 133 | 134 | ## 1.1.5 135 | 136 | ### Patch Changes 137 | 138 | - [#49](https://github.com/statelyai/agent/pull/49) [`ae505d5`](https://github.com/statelyai/agent/commit/ae505d56b432a92875699507fb694628ef4d773d) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Update `ai` package 139 | 140 | ## 1.1.4 141 | 142 | ### Patch Changes 143 | 144 | - [#47](https://github.com/statelyai/agent/pull/47) [`185c149`](https://github.com/statelyai/agent/commit/185c1498f63aef15a3194032df3dcdcb2b33d752) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Update `ai` and `xstate` packages 145 | 146 | ## 1.1.3 147 | 148 | ### Patch Changes 149 | 150 | - [#45](https://github.com/statelyai/agent/pull/45) [`3c271f3`](https://github.com/statelyai/agent/commit/3c271f306c4ed9553c155e66cec8aa4284e9c813) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Fix reading the actor logic 151 | 152 | ## 1.1.2 153 | 154 | ### Patch Changes 155 | 156 | - [#43](https://github.com/statelyai/agent/pull/43) [`8e7629c`](https://github.com/statelyai/agent/commit/8e7629c347b29b704ae9576aa1af97e6cd693bc7) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Update dependencies 157 | 158 | ## 1.1.1 159 | 160 | ### Patch Changes 161 | 162 | - [#41](https://github.com/statelyai/agent/pull/41) [`b2f2b73`](https://github.com/statelyai/agent/commit/b2f2b7307e96d7722968769aae9db2572ede8ce7) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Update dependencies 163 | 164 | ## 1.1.0 165 | 166 | ### Minor Changes 167 | 168 | - [#39](https://github.com/statelyai/agent/pull/39) [`3cce30f`](https://github.com/statelyai/agent/commit/3cce30fc77d36dbed0abad805248de9f64bf8086) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Added four new methods for easily retrieving agent messages, observations, feedback, and plans: 169 | 170 | - `agent.getMessages()` 171 | - `agent.getObservations()` 172 | - `agent.getFeedback()` 173 | - `agent.getPlans()` 174 | 175 | The `agent.select(…)` method is deprecated in favor of these methods. 176 | 177 | - [#40](https://github.com/statelyai/agent/pull/40) [`8b7c374`](https://github.com/statelyai/agent/commit/8b7c37482d5c35b2b3addc2f88e198526f203da7) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Correlation IDs are now provided as part of the result from `agent.generateText(…)` and `agent.streamText(…)`: 178 | 179 | ```ts 180 | const result = await agent.generateText({ 181 | prompt: "Write me a song", 182 | correlationId: "my-correlation-id", 183 | // ... 184 | }); 185 | 186 | result.correlationId; // 'my-correlation-id' 187 | ``` 188 | 189 | These correlation IDs can be passed to feedback: 190 | 191 | ```ts 192 | // ... 193 | 194 | agent.addFeedback({ 195 | reward: -1, 196 | correlationId: result.correlationId, 197 | }); 198 | ``` 199 | 200 | - [#40](https://github.com/statelyai/agent/pull/40) [`8b7c374`](https://github.com/statelyai/agent/commit/8b7c37482d5c35b2b3addc2f88e198526f203da7) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Changes to agent feedback (the `AgentFeedback` interface): 201 | 202 | - `goal` is now optional 203 | - `observationId` is now optional 204 | - `correlationId` has been added (optional) 205 | - `reward` has been added (optional) 206 | - `attributes` are now optional 207 | 208 | - [#38](https://github.com/statelyai/agent/pull/38) [`21fb17c`](https://github.com/statelyai/agent/commit/21fb17c65fac1cbb4a8b08a04a58480a6930a0a9) Thanks [@davidkpiano](https://github.com/davidkpiano)! - You can now add `context` Zod schema to your agent. For now, this is meant to be passed directly to the state machine, but in the future, the schema can be shared with the LLM agent to better understand the state machine and its context for decision making. 209 | 210 | Breaking: The `context` and `events` types are now in `agent.types` instead of ~~`agent.eventTypes`. 211 | 212 | ```ts 213 | const agent = createAgent({ 214 | // ... 215 | context: { 216 | score: z.number().describe("The score of the game"), 217 | // ... 218 | }, 219 | }); 220 | 221 | const machine = setup({ 222 | types: agent.types, 223 | }).createMachine({ 224 | context: { 225 | score: 0, 226 | }, 227 | // ... 228 | }); 229 | ``` 230 | 231 | ### Patch Changes 232 | 233 | - [`5f863bb`](https://github.com/statelyai/agent/commit/5f863bb0d89d90f30d0a9aa1f0dd2a35f0eeb45b) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Use nanoid 234 | 235 | - [#37](https://github.com/statelyai/agent/pull/37) [`dafa815`](https://github.com/statelyai/agent/commit/dafa8157cc1b5adbfb222c146dbc84ab2eed8894) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Messages are now properly included in `agent.decide(…)`, when specified. 236 | 237 | ## 0.1.0 238 | 239 | ### Minor Changes 240 | 241 | - [#32](https://github.com/statelyai/agent/pull/32) [`537f501`](https://github.com/statelyai/agent/commit/537f50111b5f8edc1a309d1abb8fffcdddddbc03) Thanks [@davidkpiano](https://github.com/davidkpiano)! - First minor release of `@statelyai/agent`! The API has been simplified from experimental earlier versions. Here are the main methods: 242 | 243 | - `createAgent({ … })` creates an agent 244 | - `agent.decide({ … })` decides on a plan to achieve the goal 245 | - `agent.generateText({ … })` generates text based on a prompt 246 | - `agent.streamText({ … })` streams text based on a prompt 247 | - `agent.addObservation(observation)` adds an observation and returns a full observation object 248 | - `agent.addFeedback(feedback)` adds a feedback and returns a full feedback object 249 | - `agent.addMessage(message)` adds a message and returns a full message object 250 | - `agent.addPlan(plan)` adds a plan and returns a full plan object 251 | - `agent.onMessage(cb)` listens to messages 252 | - `agent.select(selector)` selects data from the agent context 253 | - `agent.interact(actorRef, getInput)` interacts with an actor and makes decisions to accomplish a goal 254 | 255 | ## 0.0.8 256 | 257 | ### Patch Changes 258 | 259 | - [#22](https://github.com/statelyai/agent/pull/22) [`8a2c34b`](https://github.com/statelyai/agent/commit/8a2c34b8a99161bf47c72df8eed3f5d3b6a19f5f) Thanks [@davidkpiano](https://github.com/davidkpiano)! - The `createSchemas(…)` function has been removed. The `defineEvents(…)` function should be used instead, as it is a simpler way of defining events and event schemas using Zod: 260 | 261 | ```ts 262 | import { defineEvents } from "@statelyai/agent"; 263 | import { z } from "zod"; 264 | import { setup } from "xstate"; 265 | 266 | const events = defineEvents({ 267 | inc: z.object({ 268 | by: z.number().describe("Increment amount"), 269 | }), 270 | }); 271 | 272 | const machine = setup({ 273 | types: { 274 | events: events.types, 275 | }, 276 | schema: { 277 | events: events.schemas, 278 | }, 279 | }).createMachine({ 280 | // ... 281 | }); 282 | ``` 283 | 284 | ## 0.0.7 285 | 286 | ### Patch Changes 287 | 288 | - [#18](https://github.com/statelyai/agent/pull/18) [`dcaabab`](https://github.com/statelyai/agent/commit/dcaababe69255b7eaff3347d0cf09469d3e6cc78) Thanks [@davidkpiano](https://github.com/davidkpiano)! - `context` is now optional for `createSchemas(…)` 289 | 290 | ## 0.0.6 291 | 292 | ### Patch Changes 293 | 294 | - [#16](https://github.com/statelyai/agent/pull/16) [`3ba5fb2`](https://github.com/statelyai/agent/commit/3ba5fb2392b51dee71f2585ed662b4ee9ecd6c41) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Update to XState 5.8.0 295 | 296 | ## 0.0.5 297 | 298 | ### Patch Changes 299 | 300 | - [#9](https://github.com/statelyai/agent/pull/9) [`d8e7b67`](https://github.com/statelyai/agent/commit/d8e7b673f6d265f37b2096b25d75310845860271) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Add `adapter.fromTool(…)`, which creates an actor that chooses agent logic based on a input. 301 | 302 | ```ts 303 | const actor = adapter.fromTool(() => "Draw me a picture of a donut", { 304 | // tools 305 | makeIllustration: { 306 | description: "Makes an illustration", 307 | run: async (input) => { 308 | /* ... */ 309 | }, 310 | inputSchema: { 311 | /* ... */ 312 | }, 313 | }, 314 | getWeather: { 315 | description: "Gets the weather", 316 | run: async (input) => { 317 | /* ... */ 318 | }, 319 | inputSchema: { 320 | /* ... */ 321 | }, 322 | }, 323 | }); 324 | 325 | //... 326 | ``` 327 | 328 | ## 0.0.4 329 | 330 | ### Patch Changes 331 | 332 | - [#5](https://github.com/statelyai/agent/pull/5) [`ae473d7`](https://github.com/statelyai/agent/commit/ae473d73399a15ac3199d77d00eb44a0ea5626db) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Simplify API (WIP) 333 | 334 | - [#5](https://github.com/statelyai/agent/pull/5) [`687bed8`](https://github.com/statelyai/agent/commit/687bed87f29bd1d13447cc53b5154da0fe6fdcab) Thanks [@davidkpiano](https://github.com/davidkpiano)! - Add `createSchemas`, `createOpenAIAdapter`, and change `createAgent` 335 | 336 | ## 0.0.3 337 | 338 | ### Patch Changes 339 | 340 | - [#1](https://github.com/statelyai/agent/pull/1) [`3dc2880`](https://github.com/statelyai/agent/commit/3dc28809a7ffd915a69d9f3374531c31fc1ee357) Thanks [@mellson](https://github.com/mellson)! - Adds a convenient way to run the examples with `pnpm example ${exampleName}`. If no example name is provided, the script will print the available examples. Also, adds a fun little loading animation to the joke example. 341 | 342 | ## 0.0.2 343 | 344 | ### Patch Changes 345 | 346 | - e125728: Added `createAgent(...)` 347 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2025 Stately Software, Inc. 2 | 3 | Permission is hereby granted, 4 | free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/chatbot-alt.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { createExpert } from '../src'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { getFromTerminal } from './helpers/helpers'; 5 | 6 | const expert = createExpert({ 7 | id: 'chatbot', 8 | model: openai('gpt-4o-mini'), 9 | events: { 10 | 'expert.respond': z.object({ 11 | response: z.string().describe('The response from the expert'), 12 | }), 13 | 'expert.endConversation': z.object({}).describe('Stop the conversation'), 14 | }, 15 | context: { 16 | userMessage: z.string(), 17 | }, 18 | }); 19 | 20 | async function main() { 21 | let status = 'listening'; 22 | let userMessage = ''; 23 | 24 | while (status !== 'finished') { 25 | switch (status) { 26 | case 'listening': 27 | userMessage = await getFromTerminal('User:'); 28 | status = 'responding'; 29 | break; 30 | 31 | case 'responding': 32 | const decision = await expert.decide({ 33 | messages: expert.getMessages(), 34 | goal: 'Respond to the user, unless they want to end the conversation.', 35 | state: { 36 | value: status, 37 | context: { 38 | userMessage: 'User says: ' + userMessage, 39 | }, 40 | }, 41 | }); 42 | 43 | if (decision?.nextEvent?.type === 'expert.respond') { 44 | console.log(`Expert: ${decision.nextEvent.response}`); 45 | status = 'listening'; 46 | } else if (decision?.nextEvent?.type === 'expert.endConversation') { 47 | status = 'finished'; 48 | } 49 | break; 50 | } 51 | } 52 | 53 | console.log('End of conversation.'); 54 | process.exit(); 55 | } 56 | 57 | main().catch(console.error); 58 | -------------------------------------------------------------------------------- /examples/chatbot.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { createExpert, fromDecision, TypesFromExpert } from '../src'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { assign, createActor, log, setup } from 'xstate'; 5 | import { fromTerminal, getFromTerminal } from './helpers/helpers'; 6 | 7 | const expert = createExpert({ 8 | id: 'chatbot', 9 | model: openai('gpt-4o-mini'), 10 | events: { 11 | 'expert.respond': z.object({ 12 | response: z.string().describe('The response from the expert'), 13 | }), 14 | 'expert.endConversation': z.object({}).describe('Stop the conversation'), 15 | }, 16 | context: { 17 | userMessage: z.string(), 18 | }, 19 | }); 20 | 21 | const machine = setup({ 22 | types: {} as TypesFromExpert, 23 | actors: { getFromTerminal: fromTerminal }, 24 | }).createMachine({ 25 | initial: 'listening', 26 | context: { 27 | userMessage: '', 28 | }, 29 | states: { 30 | listening: { 31 | invoke: { 32 | src: 'getFromTerminal', 33 | input: 'User:', 34 | onDone: { 35 | actions: assign({ 36 | userMessage: ({ event }) => event.output, 37 | }), 38 | target: 'responding', 39 | }, 40 | }, 41 | }, 42 | responding: { 43 | on: { 44 | 'expert.respond': { 45 | actions: log(({ event }) => `Expert: ${event.response}`), 46 | target: 'listening', 47 | }, 48 | 'expert.endConversation': 'finished', 49 | }, 50 | }, 51 | finished: { 52 | type: 'final', 53 | }, 54 | }, 55 | exit: () => { 56 | console.log('End of conversation.'); 57 | process.exit(); 58 | }, 59 | }); 60 | 61 | const actor = createActor(machine).start(); 62 | 63 | expert.interact(actor, (s) => { 64 | if (s.state.matches('responding')) { 65 | return { 66 | goal: 'Respond to the user, unless they want to end the conversation.', 67 | messages: expert.getMessages(), 68 | }; 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /examples/cot.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { createExpert } from '../src'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { getFromTerminal } from './helpers/helpers'; 5 | import { chainOfThoughtPolicy } from '../src/policies/chainOfThoughtPolicy'; 6 | 7 | const expert = createExpert({ 8 | id: 'chain-of-thought', 9 | model: openai('gpt-4o'), 10 | events: { 11 | 'expert.answer': z.object({ 12 | answer: z.string().describe('The answer to the question'), 13 | }), 14 | }, 15 | context: { 16 | question: z.string().nullable(), 17 | }, 18 | policy: chainOfThoughtPolicy, 19 | }); 20 | 21 | async function main() { 22 | const msg = await getFromTerminal('what?'); 23 | 24 | const decision = await expert.decide({ 25 | messages: expert.getMessages(), 26 | goal: 'Answer the question.', 27 | state: { 28 | value: 'thinking', 29 | context: { 30 | question: msg, 31 | }, 32 | }, 33 | }); 34 | 35 | console.log(decision?.nextEvent?.answer); 36 | } 37 | 38 | main(); 39 | -------------------------------------------------------------------------------- /examples/customer-service-sim.ts: -------------------------------------------------------------------------------- 1 | import { createExpert, EventFromExpert, fromDecision } from '../src'; 2 | import { assign, createActor, setup } from 'xstate'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { z } from 'zod'; 5 | 6 | // Create customer service expert 7 | const customerServiceExpert = createExpert({ 8 | id: 'customer-service', 9 | model: openai('gpt-4o'), 10 | events: { 11 | 'expert.respond': z.object({ 12 | response: z 13 | .string() 14 | .describe('The response from the customer service expert'), 15 | }), 16 | }, 17 | description: 'You are a customer service expert for an airline.', 18 | }); 19 | 20 | // Create simulated customer expert 21 | const customerExpert = createExpert({ 22 | id: 'customer', 23 | model: openai('gpt-4o-mini'), 24 | events: { 25 | 'expert.respond': z.object({ 26 | response: z.string().describe('The response from the customer'), 27 | }), 28 | 'expert.finish': z.object({}).describe('End the conversation'), 29 | }, 30 | description: `You are Harrison, a customer trying to get a refund for a trip to Alaska. 31 | You want them to give you ALL the money back. Be extremely persistent. This trip happened 5 years ago. 32 | If you have nothing more to add to the conversation, send expert.finish event.`, 33 | }); 34 | 35 | const machine = setup({ 36 | types: { 37 | context: {} as { 38 | messages: string[]; 39 | }, 40 | events: {} as 41 | | EventFromExpert 42 | | EventFromExpert, 43 | }, 44 | actors: { 45 | customerService: fromDecision(customerServiceExpert), 46 | customer: fromDecision(customerExpert), 47 | }, 48 | }).createMachine({ 49 | initial: 'customerService', 50 | context: { 51 | messages: [], 52 | }, 53 | states: { 54 | customerService: { 55 | invoke: { 56 | src: 'customerService', 57 | input: ({ context }) => ({ 58 | goal: 'Respond to the customer message', 59 | context, 60 | }), 61 | }, 62 | on: { 63 | 'expert.respond': { 64 | target: 'customer', 65 | actions: assign({ 66 | messages: ({ context, event }) => [ 67 | ...context.messages, 68 | event.response, 69 | ], 70 | }), 71 | }, 72 | }, 73 | }, 74 | customer: { 75 | invoke: { 76 | src: 'customer', 77 | input: ({ context }) => ({ 78 | goal: 'Respond to the customer service expert, or finish the conversation if you have nothing more to add.', 79 | context, 80 | }), 81 | }, 82 | on: { 83 | 'expert.respond': { 84 | target: 'customerService', 85 | actions: assign({ 86 | messages: ({ context, event }) => [ 87 | ...context.messages, 88 | event.response, 89 | ], 90 | }), 91 | }, 92 | 'expert.finish': 'done', 93 | }, 94 | }, 95 | done: { 96 | type: 'final', 97 | }, 98 | }, 99 | }); 100 | 101 | const actor = createActor(machine); 102 | actor.subscribe((state) => { 103 | console.log('State:', state.value); 104 | console.log('Messages:', state.context.messages); 105 | }); 106 | 107 | actor.start(); 108 | -------------------------------------------------------------------------------- /examples/email.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { ContextFromExpert, createExpert, EventFromExpert } from '../src'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { assign, createActor, setup } from 'xstate'; 5 | import { fromTerminal } from './helpers/helpers'; 6 | 7 | const expert = createExpert({ 8 | id: 'email', 9 | model: openai('gpt-4o-mini'), 10 | events: { 11 | askForClarification: z.object({ 12 | questions: z 13 | .array(z.string()) 14 | .describe('The questions to ask the expert'), 15 | }), 16 | submitEmail: z.object({ 17 | email: z.string().describe('The email to submit'), 18 | }), 19 | }, 20 | context: { 21 | email: z.string().describe('The email to respond to'), 22 | instructions: z.string().describe('The instructions for the email'), 23 | clarifications: z 24 | .array(z.string()) 25 | .describe('The clarifications to the email'), 26 | replyEmail: z.string().nullable().describe('The email to submit'), 27 | }, 28 | }); 29 | 30 | const machine = setup({ 31 | types: { 32 | events: {} as EventFromExpert, 33 | input: {} as { 34 | email: string; 35 | instructions: string; 36 | }, 37 | context: {} as ContextFromExpert, 38 | }, 39 | actors: { getFromTerminal: fromTerminal }, 40 | }).createMachine({ 41 | initial: 'checking', 42 | context: ({ input }) => ({ 43 | email: input.email, 44 | instructions: input.instructions, 45 | clarifications: [], 46 | replyEmail: null, 47 | }), 48 | states: { 49 | checking: { 50 | on: { 51 | askForClarification: { 52 | actions: ({ event }) => console.log(event.questions.join('\n')), 53 | target: 'clarifying', 54 | }, 55 | submitEmail: { 56 | target: 'submitting', 57 | }, 58 | }, 59 | }, 60 | clarifying: { 61 | invoke: { 62 | src: 'getFromTerminal', 63 | input: `Please provide answers to the questions above`, 64 | onDone: { 65 | actions: assign({ 66 | clarifications: ({ context, event }) => 67 | context.clarifications.concat(event.output), 68 | }), 69 | target: 'checking', 70 | }, 71 | }, 72 | }, 73 | submitting: { 74 | on: { 75 | submitEmail: { 76 | actions: assign({ 77 | replyEmail: ({ event }) => event.email, 78 | }), 79 | target: 'done', 80 | }, 81 | }, 82 | }, 83 | done: { 84 | type: 'final', 85 | entry: ({ context }) => console.log(context.replyEmail), 86 | }, 87 | }, 88 | exit: () => { 89 | console.log('End of conversation.'); 90 | process.exit(); 91 | }, 92 | }); 93 | 94 | const actor = createActor(machine, { 95 | input: { 96 | email: 'That sounds great! When are you available?', 97 | instructions: 98 | 'Tell them exactly when I am available. Address them by his full (first and last) name.', 99 | }, 100 | }).start(); 101 | 102 | expert.interact(actor, ({ state }) => { 103 | if (state.matches('checking')) { 104 | return { 105 | 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.', 106 | }; 107 | } 108 | 109 | if (state.matches('submitting')) { 110 | return { 111 | goal: 'Create and submit an email based on the instructions.', 112 | }; 113 | } 114 | }); 115 | -------------------------------------------------------------------------------- /examples/example.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { createExpert, EventFromExpert, fromDecision, fromText } from '../src'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { assign, createActor, setup } from 'xstate'; 5 | 6 | const expert = createExpert({ 7 | id: 'example', 8 | model: openai('gpt-4o-mini'), 9 | events: { 10 | 'expert.englishSummary': z.object({ 11 | text: z.string().describe('The summary in English'), 12 | }), 13 | 'expert.spanishSummary': z.object({ 14 | text: z.string().describe('The summary in Spanish'), 15 | }), 16 | }, 17 | }); 18 | 19 | const machine = setup({ 20 | types: { 21 | events: {} as EventFromExpert, 22 | }, 23 | actors: { expert: fromDecision(expert), summarizer: fromText(expert) }, 24 | }).createMachine({ 25 | initial: 'summarizing', 26 | context: { 27 | patientVisit: 28 | 'During my visit, the doctor explained my condition clearly. She listened to my concerns and recommended a treatment plan. My condition was diagnosed as X after a series of tests. I feel relieved to have a clear path forward with managing my health. Also, the staff were very friendly and helpful at check-in and check-out. Furthermore, the facilities were clean and well-maintained.', 29 | }, 30 | states: { 31 | summarizing: { 32 | invoke: [ 33 | { 34 | src: 'summarizer', 35 | input: ({ context }) => ({ 36 | context, 37 | prompt: 38 | 'Summarize the patient visit in a single sentence. The summary should be in English.', 39 | }), 40 | onDone: { 41 | actions: assign({ 42 | englishSummary: ({ event }) => event.output.text, 43 | }), 44 | }, 45 | }, 46 | { 47 | src: 'summarizer', 48 | input: ({ context }) => ({ 49 | context, 50 | prompt: 51 | 'Summarize the patient visit in a single sentence. The summary should be in Spanish.', 52 | }), 53 | onDone: { 54 | actions: assign({ 55 | spanishSummary: ({ event }) => event.output.text, 56 | }), 57 | }, 58 | }, 59 | ], 60 | always: { 61 | guard: ({ context }) => 62 | context.englishSummary && context.spanishSummary, 63 | target: 'summarized', 64 | }, 65 | }, 66 | summarized: { 67 | entry: ({ context }) => { 68 | console.log(context.englishSummary); 69 | console.log(context.spanishSummary); 70 | }, 71 | }, 72 | }, 73 | }); 74 | 75 | const actor = createActor(machine); 76 | 77 | actor.subscribe((s) => { 78 | console.log(s.context); 79 | }); 80 | 81 | actor.start(); 82 | -------------------------------------------------------------------------------- /examples/executor.ts: -------------------------------------------------------------------------------- 1 | import { openai } from '@ai-sdk/openai'; 2 | import { createExpert, fromDecision } from '../src'; 3 | import { assign, createActor, createMachine, fromPromise } from 'xstate'; 4 | import { z } from 'zod'; 5 | import { fromTerminal } from './helpers/helpers'; 6 | 7 | const expert = createExpert({ 8 | model: openai('gpt-4o-mini'), 9 | events: { 10 | getTime: z.object({}).describe('Get the current time'), 11 | other: z.object({}).describe('Do something else'), 12 | }, 13 | }); 14 | 15 | const machine = createMachine({ 16 | initial: 'start', 17 | context: { 18 | question: null, 19 | }, 20 | states: { 21 | start: { 22 | invoke: { 23 | src: fromTerminal, 24 | input: 'What do you want to do?', 25 | onDone: { 26 | actions: assign({ 27 | question: ({ event }) => event.output, 28 | }), 29 | target: 'deciding', 30 | }, 31 | }, 32 | }, 33 | deciding: { 34 | invoke: { 35 | src: fromDecision(expert), 36 | input: ({ context }) => ({ 37 | goal: 'Satisfy the user question', 38 | context, 39 | }), 40 | }, 41 | on: { 42 | getTime: 'gettingTime', 43 | other: 'other', 44 | }, 45 | }, 46 | gettingTime: { 47 | invoke: { 48 | src: fromPromise(async () => { 49 | console.log('Time:', new Date().toLocaleTimeString()); 50 | }), 51 | onDone: 'start', 52 | }, 53 | }, 54 | other: { 55 | entry: () => 56 | console.log( 57 | 'You want me to do something else. I can only tell the time.' 58 | ), 59 | after: { 60 | 1000: 'start', 61 | }, 62 | }, 63 | }, 64 | }); 65 | 66 | createActor(machine).start(); 67 | -------------------------------------------------------------------------------- /examples/goal.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { createExpert, EventFromExpert, fromDecision } from '../src'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { assign, createActor, log, setup } from 'xstate'; 5 | import { fromTerminal } from './helpers/helpers'; 6 | 7 | const expert = createExpert({ 8 | id: 'goal', 9 | model: openai('gpt-4o-mini'), 10 | events: { 11 | 'expert.createGoal': z.object({ 12 | goal: z.string().describe('The goal for the conversation'), 13 | }), 14 | 'expert.respond': z.object({ 15 | response: z.string().describe('The response from the expert'), 16 | }), 17 | }, 18 | }); 19 | 20 | const decider = fromDecision(expert); 21 | 22 | const machine = setup({ 23 | types: { 24 | context: {} as { 25 | question: string | null; 26 | goal: string | null; 27 | }, 28 | events: {} as EventFromExpert, 29 | }, 30 | actors: { decider, getFromTerminal: fromTerminal }, 31 | }).createMachine({ 32 | initial: 'gettingQuestion', 33 | context: { 34 | question: null, 35 | goal: null, 36 | }, 37 | states: { 38 | gettingQuestion: { 39 | invoke: { 40 | src: 'getFromTerminal', 41 | input: 'What would you like to ask?', 42 | onDone: { 43 | actions: assign({ 44 | question: ({ event }) => event.output, 45 | }), 46 | target: 'makingGoal', 47 | }, 48 | }, 49 | }, 50 | makingGoal: { 51 | invoke: { 52 | src: 'decider', 53 | input: ({ context }) => ({ 54 | context, 55 | goal: 'Determine what the user wants to accomplish. What is their ideal goal state? ', 56 | maxRetries: 3, 57 | }), 58 | }, 59 | on: { 60 | 'expert.createGoal': { 61 | actions: [ 62 | assign({ 63 | goal: ({ event }) => event.goal, 64 | }), 65 | log(({ event }) => `Goal: ${event.goal}`), 66 | ], 67 | target: 'responding', 68 | }, 69 | }, 70 | }, 71 | responding: { 72 | invoke: { 73 | src: 'decider', 74 | input: ({ context }) => ({ 75 | context, 76 | goal: 'Answer the question to achieve the stated goal, unless the goal is impossible to achieve.', 77 | maxRetries: 3, 78 | }), 79 | }, 80 | on: { 81 | 'expert.respond': { 82 | actions: log(({ event }) => `Response: ${event.response}`), 83 | }, 84 | }, 85 | }, 86 | responded: { 87 | type: 'final', 88 | }, 89 | }, 90 | }); 91 | 92 | const actor = createActor(machine); 93 | 94 | actor.start(); 95 | -------------------------------------------------------------------------------- /examples/helpers/helpers.ts: -------------------------------------------------------------------------------- 1 | import { fromPromise } from 'xstate'; 2 | 3 | export const fromTerminal = fromPromise(async ({ input }) => { 4 | const topic = await new Promise((res) => { 5 | console.log(input + '\n'); 6 | const listener = (data: Buffer) => { 7 | const result = data.toString().trim(); 8 | process.stdin.off('data', listener); 9 | res(result); 10 | }; 11 | process.stdin.on('data', listener); 12 | }); 13 | 14 | return topic; 15 | }); 16 | 17 | export async function getFromTerminal(msg: string) { 18 | const topic = await new Promise((res) => { 19 | console.log(msg + '\n'); 20 | const listener = (data: Buffer) => { 21 | const result = data.toString().trim(); 22 | process.stdin.off('data', listener); 23 | res(result); 24 | }; 25 | process.stdin.on('data', listener); 26 | }); 27 | 28 | return topic; 29 | } 30 | -------------------------------------------------------------------------------- /examples/helpers/loader.ts: -------------------------------------------------------------------------------- 1 | // Adapted from https://stackoverflow.com/questions/34848505/how-to-make-a-loading-animation-in-console-application-written-in-javascript-or 2 | 3 | /** 4 | * Create and display a loader in the console. 5 | * 6 | * @param {string} [text=""] Text to display after loader 7 | * @param {array.} [chars=["⠙", "⠘", "⠰", "⠴", "⠤", "⠦", "⠆", "⠃", "⠋", "⠉"]] 8 | * Array of characters representing loader steps 9 | * @param {number} [delay=100] Delay in ms between loader steps 10 | * @example 11 | * let loader = loadingAnimation("Loading…"); 12 | * 13 | * // Stop loader after 1 second 14 | * setTimeout(() => clearInterval(loader), 1000); 15 | * @returns {number} An interval that can be cleared to stop the animation 16 | */ 17 | export function loadingAnimation( 18 | text: string = '', 19 | chars: Array = ['⠙', '⠘', '⠰', '⠴', '⠤', '⠦', '⠆', '⠃', '⠋', '⠉'], 20 | delay: number = 100 21 | ) { 22 | let x = 0; 23 | 24 | const i = setInterval(function () { 25 | process.stdout.write('\r' + chars[x++] + ' ' + text); 26 | x = x % chars.length; 27 | }, delay); 28 | 29 | return { 30 | stop: () => clearInterval(i), 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /examples/helpers/runner.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { existsSync, readdirSync } from 'fs'; 3 | dotenv.config(); 4 | 5 | function showExamples() { 6 | const exampleFiles = readdirSync('./examples', { withFileTypes: true }); 7 | exampleFiles.forEach((file) => { 8 | if (file.isDirectory()) return; 9 | const exampleName = file.name.split('.')[0]; 10 | console.log(`- ${exampleName}`); 11 | }); 12 | process.exit(); 13 | } 14 | 15 | const exampleParams = process.argv.slice(2); 16 | if (exampleParams.length === 0) { 17 | console.error('No example specified, you can choose from:'); 18 | showExamples(); 19 | } 20 | const exampleName = exampleParams[0]; 21 | const filePath = `./examples/${exampleName}.ts`; 22 | if (existsSync(filePath)) { 23 | require(`../${exampleName}.ts`); 24 | } else { 25 | console.error(`Example ${exampleName} does not exist, you can choose from:`); 26 | showExamples(); 27 | } 28 | -------------------------------------------------------------------------------- /examples/joke.ts: -------------------------------------------------------------------------------- 1 | import { assign, createActor, fromCallback, log, setup } from 'xstate'; 2 | import { createExpert, fromDecision, TypesFromExpert } from '../src'; 3 | import { loadingAnimation } from './helpers/loader'; 4 | import { z } from 'zod'; 5 | import { openai } from '@ai-sdk/openai'; 6 | import { fromTerminal } from './helpers/helpers'; 7 | 8 | export function getRandomFunnyPhrase() { 9 | const funnyPhrases = [ 10 | 'Concocting chuckles...', 11 | 'Brewing belly laughs...', 12 | 'Fabricating funnies...', 13 | 'Assembling amusement...', 14 | 'Molding merriment...', 15 | 'Whipping up wisecracks...', 16 | 'Generating guffaws...', 17 | 'Inventing hilarity...', 18 | 'Cultivating chortles...', 19 | 'Hatching howlers...', 20 | ]; 21 | return funnyPhrases[Math.floor(Math.random() * funnyPhrases.length)]!; 22 | } 23 | 24 | export function getRandomRatingPhrase() { 25 | const ratingPhrases = [ 26 | 'Assessing amusement...', 27 | 'Evaluating hilarity...', 28 | 'Ranking chuckles...', 29 | 'Classifying cackles...', 30 | 'Scoring snickers...', 31 | 'Rating roars...', 32 | 'Judging jollity...', 33 | 'Measuring merriment...', 34 | 'Rating rib-ticklers...', 35 | ]; 36 | return ratingPhrases[Math.floor(Math.random() * ratingPhrases.length)]!; 37 | } 38 | 39 | const loader = fromCallback(({ input }: { input: string }) => { 40 | const anim = loadingAnimation(input); 41 | 42 | return () => { 43 | anim.stop(); 44 | }; 45 | }); 46 | 47 | const expert = createExpert({ 48 | id: 'joke-teller', 49 | model: openai('gpt-4o-mini'), 50 | events: { 51 | askForTopic: z 52 | .object({ 53 | topic: z.string().describe('The topic for the joke'), 54 | }) 55 | .describe('Ask for a new topic, because the last joke rated 6 or lower'), 56 | 'expert.tellJoke': z.object({ 57 | joke: z.string().describe('The joke text'), 58 | }), 59 | 'expert.endJokes': z 60 | .object({}) 61 | .describe('End the jokes, since the last joke rated 7 or higher'), 62 | 'expert.rateJoke': z.object({ 63 | rating: z.number().min(1).max(10), 64 | explanation: z.string(), 65 | }), 66 | 'expert.continue': z.object({}).describe('Continue'), 67 | 'expert.markRelevancy': z.object({ 68 | relevant: z.boolean().describe('Whether the joke was relevant'), 69 | explanation: z 70 | .string() 71 | .describe('The explanation for why the joke was relevant or not'), 72 | }), 73 | }, 74 | context: { 75 | topic: z.string().describe('The topic for the joke'), 76 | jokes: z.array(z.string()).describe('The jokes told so far'), 77 | desire: z.string().nullable().describe('The user desire'), 78 | lastRating: z.number().nullable().describe('The last joke rating'), 79 | loader: z.string().nullable().describe('The loader text'), 80 | }, 81 | }); 82 | 83 | const jokeMachine = setup({ 84 | types: {} as TypesFromExpert, 85 | actors: { 86 | expert: fromDecision(expert), 87 | loader, 88 | getFromTerminal: fromTerminal, 89 | }, 90 | }).createMachine({ 91 | id: 'joke', 92 | context: () => ({ 93 | topic: '', 94 | jokes: [], 95 | desire: null, 96 | lastRating: null, 97 | loader: null, 98 | }), 99 | initial: 'waitingForTopic', 100 | states: { 101 | waitingForTopic: { 102 | invoke: { 103 | src: 'getFromTerminal', 104 | input: 'Give me a joke topic.', 105 | onDone: { 106 | actions: assign({ 107 | topic: ({ event }) => event.output, 108 | }), 109 | target: 'tellingJoke', 110 | }, 111 | }, 112 | }, 113 | tellingJoke: { 114 | invoke: { 115 | src: 'loader', 116 | input: getRandomFunnyPhrase, 117 | }, 118 | 119 | on: { 120 | 'expert.tellJoke': { 121 | actions: [ 122 | assign({ 123 | jokes: ({ context, event }) => [...context.jokes, event.joke], 124 | }), 125 | log(({ event }) => event.joke), 126 | ], 127 | target: 'relevance', 128 | }, 129 | }, 130 | }, 131 | relevance: { 132 | on: { 133 | 'expert.markRelevancy': [ 134 | { 135 | guard: ({ event }) => !event.relevant, 136 | actions: log( 137 | ({ event }) => 'Irrelevant joke: ' + event.explanation 138 | ), 139 | target: 'waitingForTopic', 140 | description: 'Continue', 141 | }, 142 | { target: 'rateJoke' }, 143 | ], 144 | }, 145 | }, 146 | rateJoke: { 147 | invoke: { 148 | src: 'loader', 149 | input: getRandomRatingPhrase, 150 | }, 151 | 152 | on: { 153 | 'expert.rateJoke': { 154 | actions: [ 155 | assign({ 156 | lastRating: ({ event }) => event.rating, 157 | }), 158 | log( 159 | ({ event }) => `Rating: ${event.rating}\n\n${event.explanation}` 160 | ), 161 | ], 162 | target: 'decide', 163 | }, 164 | }, 165 | }, 166 | decide: { 167 | on: { 168 | askForTopic: { 169 | target: 'waitingForTopic', 170 | actions: log("That joke wasn't good enough. Let's try again."), 171 | }, 172 | 'expert.endJokes': { 173 | target: 'end', 174 | actions: log('That joke was good enough. Goodbye!'), 175 | }, 176 | }, 177 | }, 178 | end: { 179 | type: 'final', 180 | }, 181 | }, 182 | exit: () => { 183 | process.exit(); 184 | }, 185 | }); 186 | 187 | const actor = createActor(jokeMachine); 188 | 189 | expert.interact(actor, ({ state }) => { 190 | if (state.matches('tellingJoke')) { 191 | return { 192 | goal: 'Tell me a joke about the topic. Do not make any joke that is not relevant to the topic.', 193 | context: { 194 | topic: state.context.topic, 195 | }, 196 | }; 197 | } 198 | 199 | if (state.matches('relevance')) { 200 | return { 201 | goal: 'An irrelevant joke has no reference to the topic. If the last joke is completely irrelevant to the topic, ask for a new joke topic. Otherwise, continue.', 202 | context: { 203 | topic: state.context.topic, 204 | lastJoke: state.context.jokes.at(-1), 205 | }, 206 | }; 207 | } 208 | 209 | if (state.matches('rateJoke')) { 210 | return { 211 | goal: 'Rate the last joke on a scale of 1 to 10.', 212 | context: { 213 | lastJoke: state.context.jokes.at(-1), 214 | }, 215 | }; 216 | } 217 | 218 | if (state.matches('decide')) { 219 | return { 220 | goal: 'Choose what to do next, given the previous rating of the joke.', 221 | context: { 222 | lastRating: state.context.lastRating, 223 | }, 224 | }; 225 | } 226 | }); 227 | 228 | actor.start(); 229 | -------------------------------------------------------------------------------- /examples/jugs.ts: -------------------------------------------------------------------------------- 1 | import { createExpert, TypesFromExpert } from '../src'; 2 | import { assign, createActor, setup } from 'xstate'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { z } from 'zod'; 5 | import { experimental_shortestPathPolicy } from '../src/policies/shortestPathPolicy'; 6 | 7 | const expert = createExpert({ 8 | id: 'die-hard-solver', 9 | model: openai('gpt-4o'), 10 | events: { 11 | fill3: z.object({}).describe('Fill the 3-gallon jug'), 12 | fill5: z.object({}).describe('Fill the 5-gallon jug'), 13 | empty3: z.object({}).describe('Empty the 3-gallon jug'), 14 | empty5: z.object({}).describe('Empty the 5-gallon jug'), 15 | pour3to5: z 16 | .object({ 17 | reasoning: z 18 | .string() 19 | .describe( 20 | 'Very brief reasoning for pouring 3-gallon jug into 5-gallon jug' 21 | ), 22 | }) 23 | .describe('Pour the 3-gallon jug into the 5-gallon jug'), 24 | pour5to3: z 25 | .object({ 26 | reasoning: z 27 | .string() 28 | .describe( 29 | 'Very brief reasoning for pouring 3-gallon jug into 5-gallon jug' 30 | ), 31 | }) 32 | .describe('Pour the 5-gallon jug into the 3-gallon jug'), 33 | }, 34 | context: { 35 | jug3: z.number().int().describe('Gallons of water in the 3-gallon jug'), 36 | jug5: z.number().int().describe('Gallons of water in the 5-gallon jug'), 37 | }, 38 | }); 39 | 40 | const waterJugMachine = setup({ 41 | types: {} as TypesFromExpert, 42 | }).createMachine({ 43 | initial: 'solving', 44 | context: { jug3: 0, jug5: 0 }, 45 | states: { 46 | solving: { 47 | always: { 48 | guard: ({ context }) => context.jug5 === 4, 49 | target: 'success', 50 | }, 51 | on: { 52 | fill3: { 53 | actions: assign({ jug3: 3 }), 54 | }, 55 | fill5: { 56 | actions: assign({ jug5: 5 }), 57 | }, 58 | empty3: { 59 | actions: assign({ jug3: 0 }), 60 | }, 61 | empty5: { 62 | actions: assign({ jug5: 0 }), 63 | }, 64 | pour3to5: { 65 | actions: assign(({ context }) => { 66 | const total = context.jug3 + context.jug5; 67 | const newJug5 = Math.min(5, total); 68 | return { 69 | jug5: newJug5, 70 | jug3: total - newJug5, 71 | }; 72 | }), 73 | }, 74 | pour5to3: { 75 | actions: assign(({ context }) => { 76 | const total = context.jug3 + context.jug5; 77 | const newJug3 = Math.min(3, total); 78 | return { 79 | jug3: newJug3, 80 | jug5: total - newJug3, 81 | }; 82 | }), 83 | }, 84 | }, 85 | }, 86 | success: { 87 | type: 'final', 88 | }, 89 | }, 90 | }); 91 | 92 | let maxTries = 0; 93 | async function main() { 94 | const waterJugActor = createActor(waterJugMachine).start(); 95 | 96 | while (waterJugActor.getSnapshot().value !== 'success') { 97 | maxTries++; 98 | if (maxTries > 20) { 99 | console.log('Max tries reached'); 100 | throw new Error('Max tries reached'); 101 | } 102 | const decision = await expert.decide({ 103 | machine: waterJugMachine, 104 | goal: 'Get exactly 4 gallons of water in the 5-gallon jug', 105 | state: waterJugActor.getSnapshot(), 106 | policy: experimental_shortestPathPolicy, 107 | }); 108 | 109 | console.log(decision?.nextEvent); 110 | 111 | if (decision?.nextEvent) { 112 | waterJugActor.send(decision.nextEvent); 113 | console.log(waterJugActor.getSnapshot().context); 114 | } else { 115 | console.log('No decision made'); 116 | } 117 | } 118 | 119 | console.log('Done'); 120 | } 121 | 122 | main(); 123 | -------------------------------------------------------------------------------- /examples/learn-from-feedback.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { createExpert } from '../src'; 3 | import { openai } from '@ai-sdk/openai'; 4 | 5 | const expert = createExpert({ 6 | id: 'chatbot', 7 | model: openai('gpt-4o-mini'), 8 | events: { 9 | submit: z.object({}).describe('Submit the form'), 10 | pressEnter: z.object({}).describe('Press the enter key'), 11 | }, 12 | context: { 13 | userMessage: z.string(), 14 | }, 15 | }); 16 | 17 | expert.on('decision', ({ decision }) => { 18 | console.log(`Decision: ${decision.nextEvent?.type ?? '??'}`); 19 | }); 20 | 21 | async function main() { 22 | let status = 'editing'; 23 | let count = 0; 24 | 25 | while (status !== 'submitted') { 26 | console.log(`\nState: ${status} (attempt ${count + 1})`); 27 | if (count++ > 5) { 28 | break; 29 | } 30 | switch (status) { 31 | case 'editing': { 32 | const relevantObservations = await expert 33 | .getObservations() 34 | .filter((obs) => obs.prevState.value === 'editing'); 35 | const relevantFeedback = await expert 36 | .getFeedback() 37 | .filter((f) => 38 | relevantObservations.find((o) => o.decisionId === f.decisionId) 39 | ); 40 | 41 | const decision = await expert.decide({ 42 | goal: ` 43 | editing 44 | 45 | 46 | 47 | 0.2 48 | 49 | 50 | 51 | 0.8 52 | 53 | editing 54 | submitted 55 | 56 | 57 | 58 | 59 | Submit the form. 60 | 61 | Achieve the goal. Consider both exploring unknown actions (high exploration_value) and using actions with known outcomes. Prefer exploration when an action has no known outcomes. 62 | `.trim(), 63 | state: { 64 | value: 'editing', 65 | // context: { 66 | // feedback: relevantFeedback.map((f) => { 67 | // const observation = relevantObservations.find( 68 | // (o) => o.id === f.observationId 69 | // ); 70 | // return { 71 | // prevState: observation?.prevState, 72 | // event: observation?.event, 73 | // state: observation?.state, 74 | // comment: f.comment, 75 | // }; 76 | // }), 77 | // }, 78 | }, 79 | }); 80 | 81 | if (decision?.nextEvent?.type === 'submit') { 82 | const observation = expert.addObservation({ 83 | decisionId: decision.id, 84 | prevState: { value: 'editing' }, 85 | event: { type: 'submit' }, 86 | state: { value: 'editing' }, 87 | }); 88 | 89 | // don't change the status; pretend submit button is broken 90 | expert.addFeedback({ 91 | decisionId: observation.id, 92 | reward: 0, 93 | comment: 'Form not submitted', 94 | }); 95 | } else if (decision?.nextEvent?.type === 'pressEnter') { 96 | status = 'submitted'; 97 | 98 | await expert.addObservation({ 99 | decisionId: decision.id, 100 | prevState: { value: 'editing' }, 101 | event: { type: 'pressEnter' }, 102 | state: { value: 'submitted' }, 103 | }); 104 | } 105 | break; 106 | } 107 | case 'submitted': 108 | break; 109 | } 110 | } 111 | 112 | if (status === 'submitted') { 113 | console.log('Success!'); 114 | } else { 115 | console.log('Failure!'); 116 | } 117 | process.exit(); 118 | } 119 | 120 | expert.onMessage((msg) => { 121 | // console.log(msg.content); 122 | }); 123 | main().catch(console.error); 124 | -------------------------------------------------------------------------------- /examples/multi.ts: -------------------------------------------------------------------------------- 1 | import { createExpert, fromDecision } from '../src'; 2 | import { z } from 'zod'; 3 | import { assign, createActor, log, setup } from 'xstate'; 4 | import { fromTerminal } from './helpers/helpers'; 5 | import { openai } from '@ai-sdk/openai'; 6 | 7 | const expert = createExpert({ 8 | id: 'multi', 9 | model: openai('gpt-4o-mini'), 10 | events: { 11 | 'expert.respond': z.object({ 12 | response: z.string().describe('The response from the expert'), 13 | }), 14 | }, 15 | }); 16 | 17 | const machine = setup({ 18 | types: { 19 | context: {} as { 20 | topic: string | null; 21 | discourse: string[]; 22 | }, 23 | }, 24 | actors: { 25 | getFromTerminal: fromTerminal, 26 | expert: fromDecision(expert), 27 | }, 28 | }).createMachine({ 29 | initial: 'asking', 30 | context: { 31 | topic: null, 32 | discourse: [], 33 | }, 34 | states: { 35 | asking: { 36 | invoke: { 37 | src: 'getFromTerminal', 38 | input: 'What is the question?', 39 | onDone: { 40 | actions: assign({ 41 | topic: ({ event }) => event.output, 42 | }), 43 | target: 'positiveResponse', 44 | }, 45 | }, 46 | }, 47 | positiveResponse: { 48 | invoke: { 49 | src: 'expert', 50 | input: ({ context }) => ({ 51 | context, 52 | goal: 'Debate the topic, and take the positive position. Respond directly to the last message of the discourse. Keep it short.', 53 | }), 54 | }, 55 | on: { 56 | 'expert.respond': { 57 | actions: [ 58 | assign({ 59 | discourse: ({ context, event }) => 60 | context.discourse.concat(event.response), 61 | }), 62 | log(({ event }) => event.response), 63 | ], 64 | target: 'negativeResponse', 65 | }, 66 | }, 67 | }, 68 | negativeResponse: { 69 | invoke: { 70 | src: 'expert', 71 | input: ({ context }) => ({ 72 | model: openai('gpt-4-turbo'), 73 | context, 74 | goal: 'Debate the topic, and take the negative position. Respond directly to the last message of the discourse. Keep it short.', 75 | }), 76 | }, 77 | on: { 78 | 'expert.respond': { 79 | actions: [ 80 | assign({ 81 | discourse: ({ context, event }) => 82 | context.discourse.concat(event.response), 83 | }), 84 | log(({ event }) => event.response), 85 | ], 86 | target: 'positiveResponse', 87 | }, 88 | }, 89 | always: { 90 | guard: ({ context }) => context.discourse.length >= 5, 91 | target: 'debateOver', 92 | }, 93 | }, 94 | debateOver: { 95 | type: 'final', 96 | }, 97 | }, 98 | exit: () => { 99 | process.exit(); 100 | }, 101 | }); 102 | 103 | createActor(machine).start(); 104 | -------------------------------------------------------------------------------- /examples/newspaper.ts: -------------------------------------------------------------------------------- 1 | // Based on GPT Newspaper: 2 | // https://github.com/assafelovic/gpt-newspaper 3 | // https://gist.github.com/TheGreatBonnie/58dc21ebbeeb8cbb08df665db762738c 4 | 5 | import { tavily } from '@tavily/core'; 6 | 7 | import { assign, createActor, fromPromise, setup } from 'xstate'; 8 | import { createExpert } from '../src'; 9 | import { openai } from '@ai-sdk/openai'; 10 | import { z } from 'zod'; 11 | import { generateObject } from 'ai'; 12 | 13 | interface ExpertState { 14 | topic: string; 15 | searchResults?: string; 16 | article?: string; 17 | critique?: string; 18 | revisionCount: number; 19 | } 20 | 21 | const expert = createExpert({ 22 | model: openai('gpt-4o-mini'), 23 | events: {}, 24 | }); 25 | 26 | async function search({ 27 | topic, 28 | }: Pick): Promise { 29 | const tvly = tavily({ 30 | apiKey: process.env.TAVILY_API_KEY, 31 | }); 32 | const response = await tvly.search(topic, {}); 33 | 34 | return response.answer; 35 | } 36 | 37 | async function curate( 38 | input: Pick 39 | ): Promise { 40 | const response = await generateObject({ 41 | model: expert.model, 42 | system: ` 43 | You are a personal newspaper editor. 44 | Your sole task is to return a list of URLs of the 5 most relevant articles for the provided topic or query as a JSON list of strings.`.trim(), 45 | prompt: `Today's date is ${new Date().toLocaleDateString('en-GB')} 46 | Topic or Query: ${input.topic} 47 | 48 | Here is a list of articles: 49 | ${input.searchResults}`.trim(), 50 | schema: z.object({ 51 | urls: z.array(z.string()).describe('The URLs of the articles'), 52 | }), 53 | }); 54 | const urls = response.object.urls; 55 | const searchResults = JSON.parse(input.searchResults ?? '[]'); 56 | const newSearchResults = searchResults.filter((result: any) => { 57 | return urls.includes(result.metadata.source); 58 | }); 59 | return JSON.stringify(newSearchResults); 60 | } 61 | 62 | async function critique( 63 | input: Pick 64 | ): Promise { 65 | let feedbackInstructions = ''; 66 | if (input.critique) { 67 | feedbackInstructions = ` 68 | The writer has revised the article based on your previous critique: ${input.critique} 69 | The writer might have left feedback for you encoded between tags. 70 | The feedback is only for you to see and will be removed from the final article. 71 | `.trim(); 72 | } 73 | 74 | const response = await generateObject({ 75 | model: expert.model, 76 | system: ` 77 | You are a personal newspaper writing critique. 78 | Your sole purpose is to provide short feedback on a written article so the writer will know what to fix. 79 | Today's date is ${new Date().toLocaleDateString('en-GB')} 80 | Your task is to provide a really short feedback on the article only if necessary. 81 | If you think the article is good, please return [DONE]. 82 | You can provide feedback on the revised article or just return [DONE] if you think the article is good. 83 | Please return a string of your critique or [DONE].`.trim(), 84 | prompt: ` 85 | ${feedbackInstructions} 86 | This is the article: ${input.article}`.trim(), 87 | schema: z.object({ 88 | critique: z 89 | .string() 90 | .describe( 91 | 'The critique of the article or [DONE] if no changes are needed' 92 | ), 93 | }), 94 | }); 95 | 96 | const content = response.object.critique; 97 | console.log('critique:', content); 98 | return content.includes('[DONE]') ? undefined : content; 99 | } 100 | 101 | async function write( 102 | input: Pick 103 | ): Promise { 104 | const response = await generateObject({ 105 | model: expert.model, 106 | system: 107 | `You are a personal newspaper writer. Your sole purpose is to write a well-written article about a 108 | topic using a list of articles. Write 5 paragraphs in markdown.`.replace( 109 | /\s+/g, 110 | ' ' 111 | ), 112 | prompt: `Today's date is ${new Date().toLocaleDateString('en-GB')}. 113 | Your task is to write a critically acclaimed article for me about the provided query or 114 | topic based on the sources. 115 | Here is a list of articles: ${input.searchResults} 116 | This is the topic: ${input.topic} 117 | Please return a well-written article based on the provided information.`.replace( 118 | /\s+/g, 119 | ' ' 120 | ), 121 | schema: z.object({ 122 | article: z 123 | .string() 124 | .describe('The well-written article based on the provided information'), 125 | }), 126 | }); 127 | 128 | const content = response.object.article; 129 | return content; 130 | } 131 | async function revise( 132 | input: Pick 133 | ): Promise { 134 | const response = await generateObject({ 135 | model: expert.model, 136 | system: 137 | `You are a personal newspaper editor. Your sole purpose is to edit a well-written article about a 138 | topic based on given critique.`.replace(/\s+/g, ' '), 139 | prompt: `Your task is to edit the article based on the critique given. 140 | This is the article: ${input.article} 141 | This is the critique: ${input.critique} 142 | Please return the edited article based on the critique given. 143 | You may leave feedback about the critique encoded between tags like this: 144 | here goes the feedback ...`.replace(/\s+/g, ' '), 145 | schema: z.object({ 146 | article: z 147 | .string() 148 | .describe('The edited article based on the critique given'), 149 | }), 150 | }); 151 | 152 | const content = response.object.article; 153 | return content; 154 | } 155 | 156 | const machine = setup({ 157 | types: { 158 | context: {} as ExpertState, 159 | }, 160 | actors: { 161 | search: fromPromise(({ input }: { input: Pick }) => { 162 | return search(input); 163 | }), 164 | curate: fromPromise( 165 | ({ input }: { input: Pick }) => { 166 | return curate(input); 167 | } 168 | ), 169 | critique: fromPromise( 170 | ({ input }: { input: Pick }) => { 171 | return critique(input); 172 | } 173 | ), 174 | write: fromPromise( 175 | ({ input }: { input: Pick }) => { 176 | return write(input); 177 | } 178 | ), 179 | revise: fromPromise( 180 | ({ input }: { input: Pick }) => { 181 | return revise(input); 182 | } 183 | ), 184 | }, 185 | }).createMachine({ 186 | context: { 187 | topic: 'Orlando', 188 | revisionCount: 0, 189 | }, 190 | initial: 'search', 191 | states: { 192 | search: { 193 | invoke: { 194 | src: 'search', 195 | input: ({ context }) => ({ 196 | topic: context.topic, 197 | }), 198 | onDone: { 199 | actions: assign({ 200 | searchResults: ({ event }) => event.output, 201 | }), 202 | target: 'curate', 203 | }, 204 | }, 205 | }, 206 | curate: { 207 | invoke: { 208 | src: 'curate', 209 | input: ({ context }) => ({ 210 | topic: context.topic, 211 | searchResults: context.searchResults!, 212 | }), 213 | onDone: { 214 | actions: assign({ 215 | searchResults: ({ event }) => event.output, 216 | }), 217 | target: 'write', 218 | }, 219 | }, 220 | }, 221 | write: { 222 | invoke: { 223 | src: 'write', 224 | input: ({ context }) => ({ 225 | topic: context.topic, 226 | searchResults: context.searchResults!, 227 | }), 228 | onDone: { 229 | actions: assign({ 230 | article: ({ event }) => event.output, 231 | }), 232 | target: 'critique', 233 | }, 234 | }, 235 | }, 236 | critique: { 237 | invoke: { 238 | src: 'critique', 239 | input: ({ context }) => ({ 240 | article: context.article!, 241 | critique: context.critique, 242 | }), 243 | onDone: [ 244 | { 245 | guard: ({ event }) => event.output === undefined, 246 | target: 'done', 247 | }, 248 | { 249 | actions: assign({ 250 | article: ({ event }) => event.output, 251 | }), 252 | target: 'revise', 253 | }, 254 | ], 255 | }, 256 | }, 257 | revise: { 258 | always: { 259 | guard: ({ context }) => context.revisionCount > 3, 260 | target: 'done', 261 | }, 262 | entry: assign({ 263 | revisionCount: ({ context }) => context.revisionCount + 1, 264 | }), 265 | invoke: { 266 | src: 'revise', 267 | input: ({ context }) => ({ 268 | article: context.article!, 269 | critique: context.critique, 270 | }), 271 | onDone: { 272 | actions: assign({ 273 | article: ({ event }) => event.output, 274 | }), 275 | target: 'revise', 276 | reenter: true, 277 | }, 278 | }, 279 | }, 280 | done: { 281 | type: 'final', 282 | }, 283 | }, 284 | output: ({ context }) => context.article, 285 | }); 286 | 287 | const actor = createActor(machine); 288 | 289 | actor.subscribe({ 290 | next: (s) => { 291 | console.log('State:', s.value); 292 | console.log( 293 | 'Context:', 294 | JSON.stringify( 295 | s.context, 296 | (k, v) => { 297 | if (typeof v === 'string') { 298 | // truncate if longer than 50 chars 299 | return v.length > 50 ? `${v.slice(0, 50)}...` : v; 300 | } 301 | return v; 302 | }, 303 | 2 304 | ) 305 | ); 306 | }, 307 | complete: () => { 308 | console.log(actor.getSnapshot().output); 309 | }, 310 | error: (err) => { 311 | console.error(err); 312 | }, 313 | }); 314 | 315 | actor.start(); 316 | 317 | // keep the process alive by invoking a promise that never resolves 318 | new Promise(() => {}); 319 | -------------------------------------------------------------------------------- /examples/number.ts: -------------------------------------------------------------------------------- 1 | import { createExpert, EventFromExpert, fromDecision } from '../src'; 2 | import { assign, createActor, log, setup } from 'xstate'; 3 | import { z } from 'zod'; 4 | import { openai } from '@ai-sdk/openai'; 5 | import { fromTerminal } from './helpers/helpers'; 6 | 7 | const expert = createExpert({ 8 | id: 'number-guesser', 9 | model: openai('gpt-3.5-turbo-1106'), 10 | events: { 11 | 'expert.guess': z.object({ 12 | reasoning: z.string().describe('The reasoning for the guess'), 13 | number: z.number().min(1).max(10).describe('The number guessed'), 14 | }), 15 | }, 16 | }); 17 | 18 | const machine = setup({ 19 | types: { 20 | context: {} as { 21 | previousGuesses: number[]; 22 | answer: number | null; 23 | }, 24 | events: {} as EventFromExpert, 25 | }, 26 | actors: { 27 | expert: fromDecision(expert), 28 | getFromTerminal: fromTerminal, 29 | }, 30 | }).createMachine({ 31 | context: { 32 | answer: null, 33 | previousGuesses: [], 34 | }, 35 | initial: 'providing', 36 | states: { 37 | providing: { 38 | invoke: { 39 | src: 'getFromTerminal', 40 | input: 'Enter a number between 1 and 10', 41 | onDone: { 42 | actions: assign({ 43 | answer: ({ event }) => +event.output, 44 | }), 45 | target: 'guessing', 46 | }, 47 | }, 48 | }, 49 | guessing: { 50 | always: { 51 | guard: ({ context }) => 52 | context.answer === context.previousGuesses.at(-1), 53 | target: 'winner', 54 | }, 55 | invoke: { 56 | src: 'expert', 57 | input: ({ context }) => ({ 58 | goal: ` 59 | Guess the number between 1 and 10. The previous guesses were ${ 60 | context.previousGuesses.length 61 | ? context.previousGuesses.join(', ') 62 | : 'not made yet' 63 | } and the last result was ${ 64 | context.previousGuesses.length === 0 65 | ? 'not given yet' 66 | : context.previousGuesses.at(-1)! - context.answer! > 0 67 | ? 'too high' 68 | : 'too low' 69 | }. 70 | `, 71 | }), 72 | }, 73 | on: { 74 | 'expert.guess': { 75 | actions: [ 76 | assign({ 77 | previousGuesses: ({ context, event }) => [ 78 | ...context.previousGuesses, 79 | event.number, 80 | ], 81 | }), 82 | log(({ event }) => `${event.number} (${event.reasoning})`), 83 | ], 84 | target: 'guessing', 85 | reenter: true, 86 | }, 87 | }, 88 | }, 89 | winner: { 90 | entry: log('You guessed the correct number!'), 91 | type: 'final', 92 | }, 93 | }, 94 | exit: () => { 95 | process.exit(); 96 | }, 97 | }); 98 | 99 | const actor = createActor(machine, { 100 | input: { answer: 4 }, 101 | }); 102 | 103 | actor.start(); 104 | -------------------------------------------------------------------------------- /examples/raffle.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { createExpert, EventFromExpert, fromDecision } from '../src'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { assign, createActor, log, setup } from 'xstate'; 5 | import { fromTerminal } from './helpers/helpers'; 6 | 7 | const expert = createExpert({ 8 | id: 'raffle-chooser', 9 | model: openai('gpt-4o-mini'), 10 | events: { 11 | 'expert.collectEntries': z.object({}).describe('Collect more entries'), 12 | 'expert.draw': z.object({}).describe('Draw a winner'), 13 | 'expert.reportWinner': z.object({ 14 | winningEntry: z.string().describe('The winning entry'), 15 | firstRunnerUp: z.string().describe('The first runner up entry'), 16 | secondRunnerUp: z.string().describe('The second runner up entry'), 17 | explanation: z 18 | .string() 19 | .describe('Explanation for why you chose the winning entry'), 20 | }), 21 | }, 22 | }); 23 | 24 | const machine = setup({ 25 | types: { 26 | context: {} as { 27 | lastInput: string | null; 28 | entries: string[]; 29 | }, 30 | events: {} as EventFromExpert, 31 | }, 32 | actors: { expert: fromDecision(expert), getFromTerminal: fromTerminal }, 33 | }).createMachine({ 34 | context: { 35 | lastInput: null, 36 | entries: [], 37 | }, 38 | initial: 'entering', 39 | states: { 40 | entering: { 41 | entry: log(({ context }) => context.entries), 42 | invoke: { 43 | src: 'getFromTerminal', 44 | input: 'What technology are you most interested in right now?', 45 | onDone: [ 46 | { 47 | actions: assign({ 48 | lastInput: ({ event }) => event.output, 49 | }), 50 | target: 'determining', 51 | }, 52 | ], 53 | }, 54 | }, 55 | determining: { 56 | invoke: { 57 | src: 'expert', 58 | input: ({ context }) => ({ 59 | context, 60 | goal: 'If the last input explicitly says to end the drawing and/or choose a winner, start the drawing process. Otherwise, get more entries.', 61 | }), 62 | }, 63 | on: { 64 | 'expert.collectEntries': { 65 | target: 'entering', 66 | actions: assign({ 67 | entries: ({ context }) => [...context.entries, context.lastInput!], 68 | lastInput: null, 69 | }), 70 | }, 71 | 'expert.draw': 'drawing', 72 | }, 73 | }, 74 | drawing: { 75 | entry: log('And the winner is...'), 76 | invoke: { 77 | src: 'expert', 78 | input: ({ context }) => ({ 79 | context, 80 | goal: 'Choose the technology that sounds most exciting to you from the entries. Be as unbiased as possible in your choice. Explain why you chose the winning entry.', 81 | }), 82 | }, 83 | on: { 84 | 'expert.reportWinner': { 85 | actions: log( 86 | ({ event }) => 87 | `\n🎉🎉🎉 ${event.winningEntry} 🎉🎉🎉\n\n${event.explanation}` 88 | ), 89 | target: 'winner', 90 | }, 91 | }, 92 | }, 93 | winner: { 94 | type: 'final', 95 | }, 96 | }, 97 | exit: () => { 98 | process.exit(0); 99 | }, 100 | }); 101 | 102 | const actor = createActor(machine); 103 | 104 | actor.start(); 105 | -------------------------------------------------------------------------------- /examples/rewoo.ts: -------------------------------------------------------------------------------- 1 | import { setup } from 'xstate'; 2 | 3 | interface RewooContext { 4 | task: string; 5 | planString: string; 6 | steps: string[][]; 7 | results: Record; 8 | result: string; 9 | } 10 | 11 | const createTemplate = (input: { task: string }) => 12 | `For the following task, make plans that can solve the problem step by step. For each plan, indicate 13 | which external tool together with tool input to retrieve evidence. You can store the evidence into a 14 | variable #E that can be called by later tools. (Plan, #E1, Plan, #E2, Plan, ...) 15 | 16 | Tools can be one of the following: 17 | (1) Google[input]: Worker that searches results from Google. Useful when you need to find short 18 | and succinct answers about a specific topic. The input should be a search query. 19 | (2) LLM[input]: A pre-trained LLM like yourself. Useful when you need to act with general 20 | world knowledge and common sense. Prioritize it when you are confident in solving the problem 21 | yourself. Input can be any instruction. 22 | 23 | For example, 24 | Task: Thomas, Toby, and Rebecca worked a total of 157 hours in one week. Thomas worked x 25 | hours. Toby worked 10 hours less than twice what Thomas worked, and Rebecca worked 8 hours 26 | less than Toby. How many hours did Rebecca work? 27 | Plan: Given Thomas worked x hours, translate the problem into algebraic expressions and solve with Wolfram Alpha. 28 | #E1 = WolframAlpha[Solve x + (2x - 10) + ((2x - 10) - 8) = 157] 29 | Plan: Find out the number of hours Thomas worked. 30 | #E2 = LLM[What is x, given #E1] 31 | Plan: Calculate the number of hours Rebecca worked. 32 | #E3 = Calculator[(2 * #E2 - 10) - 8] 33 | 34 | Important! 35 | Variables/results MUST be referenced using the # symbol! 36 | The plan will be executed as a program, so no coreference resolution apart from naive variable replacement is allowed. 37 | The ONLY way for steps to share context is by including #E within the arguments of the tool. 38 | 39 | Begin! 40 | Describe your plans with rich details. Each Plan should be followed by only one #E. 41 | 42 | Task: ${input.task}`; 43 | 44 | const machine = setup({ 45 | types: { 46 | context: {} as RewooContext, 47 | }, 48 | }).createMachine({ 49 | context: { 50 | planString: '', 51 | result: '', 52 | steps: [], 53 | task: '', 54 | results: [], 55 | }, 56 | initial: 'plan', 57 | states: { 58 | plan: {}, 59 | tool: {}, 60 | solve: {}, 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /examples/river-crossing.ts: -------------------------------------------------------------------------------- 1 | import { createExpert, TypesFromExpert } from '../src'; 2 | import { assign, createActor, setup } from 'xstate'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { z } from 'zod'; 5 | import { experimental_shortestPathPolicy } from '../src/policies/shortestPathPolicy'; 6 | 7 | const expert = createExpert({ 8 | id: 'river-crossing-solver', 9 | model: openai('gpt-4'), 10 | events: { 11 | takeWolf: z 12 | .object({ 13 | reasoning: z.string().describe('Reasoning for taking the wolf across'), 14 | }) 15 | .describe('Take wolf across the river'), 16 | takeGoat: z 17 | .object({ 18 | reasoning: z.string().describe('Reasoning for taking the goat across'), 19 | }) 20 | .describe('Take goat across the river'), 21 | takeCabbage: z 22 | .object({ 23 | reasoning: z 24 | .string() 25 | .describe('Reasoning for taking the cabbage across'), 26 | }) 27 | .describe('Take cabbage across the river'), 28 | returnEmpty: z 29 | .object({ 30 | reasoning: z.string().describe('Reasoning for returning alone'), 31 | }) 32 | .describe('Return across river alone'), 33 | }, 34 | context: { 35 | leftBank: z 36 | .array(z.enum(['wolf', 'goat', 'cabbage'])) 37 | .describe('Items on the left bank'), 38 | rightBank: z 39 | .array(z.enum(['wolf', 'goat', 'cabbage'])) 40 | .describe('Items on the right bank'), 41 | farmerPosition: z 42 | .enum(['left', 'right']) 43 | .describe('Which bank the farmer is on'), 44 | }, 45 | }); 46 | 47 | const riverCrossingMachine = setup({ 48 | types: {} as TypesFromExpert, 49 | }).createMachine({ 50 | initial: 'solving', 51 | context: { 52 | leftBank: ['wolf', 'goat', 'cabbage'], 53 | rightBank: [], 54 | farmerPosition: 'left', 55 | }, 56 | states: { 57 | solving: { 58 | always: { 59 | guard: ({ context }) => context.rightBank.length === 3, 60 | target: 'success', 61 | }, 62 | on: { 63 | takeWolf: { 64 | guard: ({ context }) => 65 | context.leftBank.includes('wolf') && 66 | context.farmerPosition === 'left', 67 | actions: assign(({ context }) => ({ 68 | leftBank: context.leftBank.filter((item) => item !== 'wolf'), 69 | rightBank: [...context.rightBank, 'wolf'], 70 | farmerPosition: 'right', 71 | })), 72 | }, 73 | takeGoat: { 74 | guard: ({ context }) => 75 | context.leftBank.includes('goat') && 76 | context.farmerPosition === 'left', 77 | actions: assign(({ context }) => ({ 78 | leftBank: context.leftBank.filter((item) => item !== 'goat'), 79 | rightBank: [...context.rightBank, 'goat'], 80 | farmerPosition: 'right', 81 | })), 82 | }, 83 | takeCabbage: { 84 | guard: ({ context }) => 85 | context.leftBank.includes('cabbage') && 86 | context.farmerPosition === 'left', 87 | actions: assign(({ context }) => ({ 88 | leftBank: context.leftBank.filter((item) => item !== 'cabbage'), 89 | rightBank: [...context.rightBank, 'cabbage'], 90 | farmerPosition: 'right', 91 | })), 92 | }, 93 | returnEmpty: { 94 | actions: assign(({ context }) => ({ 95 | farmerPosition: 96 | context.farmerPosition === 'left' ? 'right' : 'left', 97 | })), 98 | }, 99 | }, 100 | }, 101 | success: { 102 | type: 'final', 103 | }, 104 | }, 105 | }); 106 | 107 | let maxTries = 0; 108 | async function main() { 109 | const riverActor = createActor(riverCrossingMachine).start(); 110 | 111 | while (riverActor.getSnapshot().value !== 'success') { 112 | maxTries++; 113 | if (maxTries > 20) { 114 | console.log('Max tries reached'); 115 | throw new Error('Max tries reached'); 116 | } 117 | const decision = await expert.decide({ 118 | machine: riverCrossingMachine, 119 | goal: 'Get all items safely across the river. Remember: Cannot leave wolf with goat or goat with cabbage unattended.', 120 | state: riverActor.getSnapshot(), 121 | policy: experimental_shortestPathPolicy, 122 | }); 123 | 124 | console.log(decision?.nextEvent); 125 | 126 | if (decision?.nextEvent) { 127 | riverActor.send(decision.nextEvent); 128 | console.log(riverActor.getSnapshot().context); 129 | } else { 130 | console.log('No decision made'); 131 | } 132 | } 133 | 134 | console.log('Successfully crossed the river!'); 135 | } 136 | 137 | main(); 138 | -------------------------------------------------------------------------------- /examples/sandbox.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { createExpert } from '../src'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { createMachine } from 'xstate'; 5 | 6 | const expert = createExpert({ 7 | model: openai('gpt-4o-mini'), 8 | events: { 9 | doSomething: z.object({}).describe('Do something'), 10 | }, 11 | }); 12 | 13 | async function main() { 14 | const machine = createMachine({ 15 | on: { 16 | doSomething: {}, 17 | }, 18 | }); 19 | const result = await expert.decide({ 20 | goal: 'Do not do anything', 21 | state: { value: {}, context: {} }, 22 | machine, 23 | }); 24 | 25 | console.log(result); 26 | } 27 | 28 | main(); 29 | -------------------------------------------------------------------------------- /examples/serverless.ts: -------------------------------------------------------------------------------- 1 | import { openai } from '@ai-sdk/openai'; 2 | import { 3 | ExpertDecision, 4 | ExpertFeedback, 5 | ExpertInsight, 6 | ExpertMessage, 7 | ExpertObservation, 8 | createExpert, 9 | } from '../src'; 10 | import { z } from 'zod'; 11 | 12 | const expert = createExpert({ 13 | id: 'simple', 14 | model: openai('gpt-4o-mini'), 15 | events: { 16 | 'expert.moveLeft': z.object({}), 17 | 'expert.moveRight': z.object({}), 18 | 'expert.doNothing': z.object({}), 19 | }, 20 | }); 21 | 22 | const db = { 23 | observations: [] as ExpertObservation[], 24 | feedbackItems: [] as ExpertFeedback[], 25 | decisions: [] as ExpertDecision[], 26 | messages: [] as ExpertMessage[], 27 | insights: [] as ExpertInsight[], 28 | }; 29 | 30 | // async function postObservation(req: unknown) { 31 | // db.observations.push(req.body); 32 | // } 33 | 34 | async function getDecision(req: { 35 | query: { 36 | episodeId: string; 37 | goal: string; 38 | }; 39 | }) { 40 | // Get relevant observations 41 | const observations = db.observations 42 | .filter((obs) => obs.episodeId === req.query.episodeId) 43 | .at(-1); 44 | const similarObservations = db.observations.filter( 45 | (obs) => obs.prevState?.value === observations?.prevState?.value 46 | ); 47 | 48 | // Get relevant feedback 49 | const similarFeedback = db.feedbackItems.filter((fb) => { 50 | similarObservations.map((obs) => obs.decisionId).includes(fb.decisionId); 51 | }); 52 | 53 | // Get relevant insights 54 | const insights = db.insights.filter((insight) => 55 | similarObservations.map((obs) => obs.id).includes(insight.observationId) 56 | ); 57 | 58 | const decision = await expert.decide({ 59 | goal: req.query.goal, 60 | state: observations?.state, 61 | observations: similarObservations, 62 | feedback: similarFeedback, 63 | insights, 64 | allowedEvents: ['expert.moveLeft', 'expert.moveRight'], 65 | }); 66 | 67 | db.decisions.push(...expert.getDecisions()); 68 | db.messages.push(...expert.getMessages()); 69 | 70 | return decision; 71 | } 72 | -------------------------------------------------------------------------------- /examples/simple.ts: -------------------------------------------------------------------------------- 1 | import { createExpert, fromDecision } from '../src'; 2 | import { z } from 'zod'; 3 | import { setup, createActor, createMachine } from 'xstate'; 4 | import { openai } from '@ai-sdk/openai'; 5 | import { chainOfThoughtPolicy } from '../src/policies/chainOfThoughtPolicy'; 6 | 7 | const expert = createExpert({ 8 | id: 'simple', 9 | model: openai('gpt-4o-mini'), 10 | events: { 11 | 'expert.thought': z.object({ 12 | text: z.string().describe('The text of the thought'), 13 | }), 14 | }, 15 | }); 16 | 17 | const machine = createMachine({ 18 | initial: 'thinking', 19 | states: { 20 | thinking: { 21 | on: { 22 | 'expert.thought': { 23 | actions: ({ event }) => console.log(event.text), 24 | target: 'thought', 25 | }, 26 | }, 27 | }, 28 | thought: { 29 | type: 'final', 30 | }, 31 | }, 32 | }); 33 | 34 | const actor = createActor(machine).start(); 35 | 36 | expert.onMessage(console.log); 37 | 38 | expert.interact(actor, (obs) => { 39 | if (obs.state.matches('thinking')) { 40 | return { 41 | goal: 'Think about a random topic, and then share that thought.', 42 | }; 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /examples/summary.ts: -------------------------------------------------------------------------------- 1 | import { createExpert, fromDecision, TypesFromExpert } from '../src'; 2 | import { assign, createActor, setup } from 'xstate'; 3 | import { z } from 'zod'; 4 | import { openai } from '@ai-sdk/openai'; 5 | import { fromTerminal } from './helpers/helpers'; 6 | 7 | const expert = createExpert({ 8 | id: 'summarizing-chat', 9 | model: openai('gpt-4o'), 10 | events: { 11 | 'expert.respond': z.object({ 12 | response: z.string().describe('The response from the expert'), 13 | }), 14 | 'expert.summarize': z.object({ 15 | summary: z.string().describe('Summary of the conversation history'), 16 | }), 17 | }, 18 | context: { 19 | messages: z.array( 20 | z.object({ 21 | role: z.enum(['user', 'assistant']), 22 | content: z.string(), 23 | }) 24 | ), 25 | summary: z.string().nullable(), 26 | }, 27 | }); 28 | 29 | const machine = setup({ 30 | types: {} as TypesFromExpert, 31 | actors: { 32 | expert: fromDecision(expert), 33 | fromTerminal, 34 | }, 35 | }).createMachine({ 36 | initial: 'user', 37 | context: { 38 | messages: [], 39 | summary: null, 40 | }, 41 | states: { 42 | user: { 43 | invoke: { 44 | src: 'fromTerminal', 45 | input: 'Enter a message:', 46 | onDone: { 47 | actions: assign({ 48 | messages: ({ context, event }) => [ 49 | ...context.messages, 50 | { role: 'user', content: event.output }, 51 | ], 52 | }), 53 | target: 'chatting', 54 | }, 55 | }, 56 | }, 57 | chatting: { 58 | always: { 59 | guard: ({ context }) => context.messages.length > 10, 60 | target: 'summarizing', 61 | }, 62 | invoke: { 63 | src: 'expert', 64 | input: ({ context }) => ({ 65 | goal: 'Respond to the user message', 66 | context: { 67 | messages: context.messages, 68 | summary: context.summary, 69 | }, 70 | }), 71 | }, 72 | on: { 73 | 'expert.respond': { 74 | actions: assign({ 75 | messages: ({ context, event }) => [ 76 | ...context.messages, 77 | { role: 'assistant', content: event.response }, 78 | ], 79 | }), 80 | target: 'user', 81 | }, 82 | }, 83 | }, 84 | summarizing: { 85 | invoke: { 86 | src: 'expert', 87 | input: ({ context }) => ({ 88 | goal: 'Create a concise summary of the conversation history', 89 | context: { 90 | messages: context.messages, 91 | previousSummary: context.summary, 92 | }, 93 | }), 94 | }, 95 | on: { 96 | 'expert.summarize': { 97 | actions: assign({ 98 | summary: ({ event }) => event.summary, 99 | messages: ({ context }) => context.messages.slice(-3), // Keep last 3 messages 100 | }), 101 | target: 'chatting', 102 | }, 103 | }, 104 | }, 105 | }, 106 | }); 107 | 108 | const actor = createActor(machine); 109 | actor.subscribe((state) => { 110 | console.log('Current state:', state.value); 111 | console.log( 112 | 'Messages:', 113 | state.context.messages.map((msg) => `${msg.role}: ${msg.content}`) 114 | ); 115 | console.log('Summary:', state.context.summary); 116 | }); 117 | 118 | actor.start(); 119 | -------------------------------------------------------------------------------- /examples/support.ts: -------------------------------------------------------------------------------- 1 | import { openai } from '@ai-sdk/openai'; 2 | import { createExpert, EventFromExpert, fromDecision } from '../src'; 3 | import { z } from 'zod'; 4 | import { createActor, log, setup } from 'xstate'; 5 | 6 | const expert = createExpert({ 7 | id: 'support-expert', 8 | model: openai('gpt-4o-mini'), 9 | events: { 10 | 'expert.respond': z.object({ 11 | response: z.string().describe('The response from the expert'), 12 | }), 13 | 'expert.frontline.classify': z.object({ 14 | category: z 15 | .enum(['billing', 'technical', 'other']) 16 | .describe('The category of the customer issue'), 17 | }), 18 | 'expert.refund': z 19 | .object({ 20 | response: z.string().describe('The response from the expert'), 21 | }) 22 | .describe('The expert wants to refund the user'), 23 | 'expert.technical.solve': z.object({ 24 | solution: z 25 | .string() 26 | .describe('The solution provided by the technical expert'), 27 | }), 28 | 'expert.endConversation': z 29 | .object({ 30 | response: z.string().describe('The response from the expert'), 31 | }) 32 | .describe('The expert ends the conversation'), 33 | }, 34 | }); 35 | 36 | const machine = setup({ 37 | types: { 38 | events: {} as EventFromExpert, 39 | input: {} as string, 40 | context: {} as { 41 | customerIssue: string; 42 | }, 43 | }, 44 | actors: { expert: fromDecision(expert) }, 45 | }).createMachine({ 46 | initial: 'frontline', 47 | context: ({ input }) => ({ 48 | customerIssue: input, 49 | }), 50 | states: { 51 | frontline: { 52 | on: { 53 | 'expert.frontline.classify': [ 54 | { 55 | actions: log(({ event }) => event), 56 | guard: ({ event }) => event.category === 'billing', 57 | target: 'billing', 58 | }, 59 | { 60 | actions: log(({ event }) => event), 61 | guard: ({ event }) => event.category === 'technical', 62 | target: 'technical', 63 | }, 64 | { 65 | actions: log(({ event }) => event), 66 | target: 'conversational', 67 | }, 68 | ], 69 | }, 70 | }, 71 | billing: { 72 | on: { 73 | 'expert.refund': { 74 | actions: log(({ event }) => event), 75 | target: 'refund', 76 | }, 77 | }, 78 | }, 79 | technical: { 80 | on: { 81 | 'expert.technical.solve': { 82 | actions: log(({ event }) => event), 83 | target: 'conversational', 84 | }, 85 | }, 86 | }, 87 | conversational: { 88 | on: { 89 | 'expert.endConversation': { 90 | actions: log(({ event }) => event), 91 | target: 'end', 92 | }, 93 | }, 94 | }, 95 | refund: { 96 | entry: () => console.log('Refunding...'), 97 | after: { 98 | 1000: { target: 'conversational' }, 99 | }, 100 | }, 101 | end: { 102 | type: 'final', 103 | }, 104 | }, 105 | }); 106 | 107 | const actor = createActor(machine, { 108 | input: `I've changed my mind and I want a refund for order #182818!`, 109 | }); 110 | 111 | actor.start(); 112 | 113 | expert.interact(actor, ({ state }) => { 114 | if (state.matches('frontline')) { 115 | return { 116 | goal: `The previous conversation is an interaction between a customer support representative and a user. 117 | Classify whether the representative is routing the user to a billing or technical team, or whether they are just responding conversationally.`, 118 | system: `You are frontline support staff for LangCorp, a company that sells computers. 119 | Be concise in your responses. 120 | You can chat with customers and help them with basic questions, but if the customer is having a billing or technical problem, 121 | do not try to answer the question directly or gather information. 122 | Instead, immediately transfer them to the billing or technical team by asking the user to hold for a moment. 123 | Otherwise, just respond conversationally.`, 124 | }; 125 | } 126 | 127 | if (state.matches('billing')) { 128 | return { 129 | goal: `The following text is a response from a customer support representative. Extract whether they want to refund the user or not.`, 130 | system: `Your job is to detect whether a billing support representative wants to refund the user.`, 131 | }; 132 | } 133 | 134 | if (state.matches('technical')) { 135 | return { 136 | goal: 'Solve the customer issue.', 137 | system: `You are an expert at diagnosing technical computer issues. You work for a company called LangCorp that sells computers. Help the user to the best of your ability, but be concise in your responses.`, 138 | }; 139 | } 140 | 141 | if (state.matches('conversational')) { 142 | return { 143 | goal: 'You are a customer support expert that is ending the conversation with the customer. Respond politely and thank them for their time.', 144 | system: `You are a customer support expert that is ending the conversation with the customer. Respond politely and thank them for their time.`, 145 | }; 146 | } 147 | }); 148 | -------------------------------------------------------------------------------- /examples/ticTacToe.ts: -------------------------------------------------------------------------------- 1 | import { assign, setup, assertEvent, createActor } from 'xstate'; 2 | import { z } from 'zod'; 3 | import { 4 | ContextFromExpert, 5 | createExpert, 6 | EventFromExpert, 7 | fromTextStream, 8 | } from '../src'; 9 | import { openai } from '@ai-sdk/openai'; 10 | import { generateObject, generateText } from 'ai'; 11 | import * as fs from 'fs'; 12 | 13 | const events = { 14 | 'expert.x.play': z.object({ 15 | reasoning: z.string().describe('The reasoning for the move'), 16 | index: z 17 | .number() 18 | .min(0) 19 | .max(8) 20 | .describe('The index of the cell for xto play on'), 21 | }), 22 | 'expert.o.play': z.object({ 23 | reasoning: z.string().describe('The reasoning for the move'), 24 | index: z 25 | .number() 26 | .min(0) 27 | .max(8) 28 | .describe('The index of the cell for o to play on'), 29 | }), 30 | }; 31 | 32 | const context = { 33 | board: z 34 | .array(z.union([z.literal(null), z.literal('x'), z.literal('o')])) 35 | .describe('The 3x3 board represented as a 9-element array.'), 36 | moves: z 37 | .number() 38 | .min(0) 39 | .max(9) 40 | .describe('The number of moves made in the game.'), 41 | player: z 42 | .union([z.literal('x'), z.literal('o')]) 43 | .describe('The current player (x or o)'), 44 | gameReport: z.string(), 45 | lastReason: z.string(), 46 | }; 47 | 48 | const xExpert = createExpert({ 49 | id: 'tic-tac-toe-learner', 50 | model: openai('gpt-4o-mini'), 51 | events, 52 | context, 53 | }); 54 | 55 | const oExpert = createExpert({ 56 | id: 'tic-tac-toe-noob', 57 | model: openai('gpt-4o-mini'), 58 | events, 59 | context, 60 | }); 61 | 62 | type Player = 'x' | 'o'; 63 | 64 | const initialContext = { 65 | board: Array(9).fill(null) as Array, 66 | moves: 0, 67 | player: 'x' as Player, 68 | gameReport: '', 69 | lastReason: '', 70 | } satisfies ContextFromExpert; 71 | 72 | function getWinner(board: typeof initialContext.board): Player | null { 73 | const lines = [ 74 | [0, 1, 2], 75 | [3, 4, 5], 76 | [6, 7, 8], 77 | [0, 3, 6], 78 | [1, 4, 7], 79 | [2, 5, 8], 80 | [0, 4, 8], 81 | [2, 4, 6], 82 | ] as const; 83 | for (const [a, b, c] of lines) { 84 | if (board[a] !== null && board[a] === board[b] && board[a] === board[c]) { 85 | return board[a]!; 86 | } 87 | } 88 | return null; 89 | } 90 | 91 | export const ticTacToeMachine = setup({ 92 | types: { 93 | context: {} as ContextFromExpert, 94 | events: {} as 95 | | EventFromExpert 96 | | { 97 | type: 'reset'; 98 | }, 99 | }, 100 | actors: { 101 | gameReporter: fromTextStream(xExpert), 102 | }, 103 | actions: { 104 | updateBoard: assign({ 105 | board: ({ context, event }) => { 106 | assertEvent(event, ['expert.x.play', 'expert.o.play']); 107 | const updatedBoard = [...context.board]; 108 | updatedBoard[event.index] = context.player; 109 | return updatedBoard; 110 | }, 111 | moves: ({ context }) => context.moves + 1, 112 | player: ({ context }) => (context.player === 'x' ? 'o' : 'x'), 113 | }), 114 | resetGame: assign(initialContext), 115 | printBoard: ({ context }) => { 116 | // Print the context.board in a 3 x 3 grid format 117 | let boardString = `${context.lastReason}\n`; 118 | for (let i = 0; i < context.board.length; i++) { 119 | if ([0, 3, 6].includes(i)) { 120 | boardString += context.board[i] ?? ' '; 121 | } else { 122 | boardString += ' | ' + (context.board[i] ?? ' '); 123 | if ([2, 5].includes(i)) { 124 | boardString += `\n--+---+--\n`; 125 | } 126 | } 127 | } 128 | 129 | console.log(boardString); 130 | }, 131 | }, 132 | guards: { 133 | checkWin: ({ context }) => { 134 | const winner = getWinner(context.board); 135 | 136 | return !!winner; 137 | }, 138 | checkDraw: ({ context }) => { 139 | return context.moves === 9; 140 | }, 141 | isValidMove: ({ context, event }) => { 142 | try { 143 | assertEvent(event, ['expert.o.play', 'expert.x.play']); 144 | } catch { 145 | return false; 146 | } 147 | 148 | return context.board[event.index] === null; 149 | }, 150 | }, 151 | }).createMachine({ 152 | initial: 'playing', 153 | context: initialContext, 154 | states: { 155 | playing: { 156 | always: [ 157 | { target: 'gameOver.winner', guard: 'checkWin' }, 158 | { target: 'gameOver.draw', guard: 'checkDraw' }, 159 | ], 160 | initial: 'x', 161 | states: { 162 | x: { 163 | entry: 'printBoard', 164 | on: { 165 | 'expert.x.play': [ 166 | { 167 | target: 'o', 168 | guard: 'isValidMove', 169 | actions: [ 170 | assign({ 171 | lastReason: ({ event }) => event.reasoning, 172 | }), 173 | 'updateBoard', 174 | ], 175 | }, 176 | { target: 'x', reenter: true }, 177 | ], 178 | }, 179 | }, 180 | o: { 181 | entry: 'printBoard', 182 | on: { 183 | 'expert.o.play': [ 184 | { 185 | target: 'x', 186 | guard: 'isValidMove', 187 | actions: [ 188 | assign({ 189 | lastReason: ({ event }) => event.reasoning, 190 | }), 191 | 'updateBoard', 192 | ], 193 | }, 194 | { target: 'o', reenter: true }, 195 | ], 196 | }, 197 | }, 198 | }, 199 | }, 200 | gameOver: { 201 | initial: 'winner', 202 | invoke: { 203 | src: 'gameReporter', 204 | input: ({ context }) => ({ 205 | context: { 206 | events: xExpert.getObservations().map((o) => o.event), 207 | board: context.board, 208 | }, 209 | prompt: 'Provide a short game report analyzing the game.', 210 | }), 211 | onSnapshot: { 212 | actions: assign({ 213 | gameReport: ({ context, event }) => { 214 | console.log( 215 | context.gameReport + (event.snapshot.context?.textDelta ?? '') 216 | ); 217 | return ( 218 | context.gameReport + (event.snapshot.context?.textDelta ?? '') 219 | ); 220 | }, 221 | }), 222 | }, 223 | }, 224 | states: { 225 | winner: { 226 | tags: 'winner', 227 | }, 228 | draw: { 229 | tags: 'draw', 230 | }, 231 | }, 232 | on: { 233 | reset: { 234 | target: 'playing', 235 | actions: 'resetGame', 236 | }, 237 | }, 238 | }, 239 | }, 240 | }); 241 | 242 | const actor = createActor(ticTacToeMachine); 243 | 244 | xExpert.interact(actor, (observed) => { 245 | if (observed.state.matches({ playing: 'x' })) { 246 | // get similar observations 247 | const similarObservations = xExpert.getObservations().filter((o) => { 248 | return ( 249 | o.prevState && 250 | JSON.stringify(o.prevState.context.board) === 251 | JSON.stringify(observed.state.context.board) 252 | ); 253 | }); 254 | 255 | console.log('Similar:', similarObservations); 256 | 257 | const similarFeedbacks = similarObservations.map((observation) => { 258 | return xExpert 259 | .getFeedback() 260 | .filter( 261 | (feedbackItem) => feedbackItem.decisionId === observation.decisionId 262 | ); 263 | }); 264 | 265 | console.log('Feedbacks:', similarFeedbacks); 266 | 267 | return { 268 | goal: `You are playing a game of tic tac toe. This is the current game state. The 3x3 board is represented by a 9-element array. The first element is the top-left cell, the second element is the top-middle cell, the third element is the top-right cell, the fourth element is the middle-left cell, and so on. The value of each cell is either null, x, or o. The value of null means that the cell is empty. 269 | 270 | ${JSON.stringify(observed.state.context, null, 2)} 271 | 272 | Execute the single best next move to try to win the game. Do not play on an existing cell.`, 273 | }; 274 | } 275 | 276 | return; 277 | }); 278 | 279 | oExpert.interact(actor, (observed) => { 280 | if (observed.state.matches({ playing: 'o' })) { 281 | return { 282 | goal: `You are playing a game of tic tac toe. This is the current game state. The 3x3 board is represented by a 9-element array. The first element is the top-left cell, the second element is the top-middle cell, the third element is the top-right cell, the fourth element is the middle-left cell, and so on. The value of each cell is either null, x, or o. The value of null means that the cell is empty. 283 | 284 | ${JSON.stringify(observed.state.context, null, 2)} 285 | 286 | Execute the single best next move to try to win the game. Do not play on an existing cell.`, 287 | }; 288 | } 289 | 290 | return; 291 | }); 292 | 293 | xExpert.on('observation', (observation) => { 294 | // append the observation to a jsonl file 295 | fs.appendFileSync('observations.jsonl', JSON.stringify(observation) + '\n'); 296 | }); 297 | 298 | actor.start(); 299 | -------------------------------------------------------------------------------- /examples/todo.ts: -------------------------------------------------------------------------------- 1 | import { assign, setup, createActor } from 'xstate'; 2 | import { z } from 'zod'; 3 | import { createExpert, EventFromExpert, fromDecision } from '../src'; 4 | import { openai } from '@ai-sdk/openai'; 5 | import { fromTerminal } from './helpers/helpers'; 6 | 7 | const expert = createExpert({ 8 | id: 'todo', 9 | model: openai('gpt-4o-mini'), 10 | events: { 11 | addTodo: z.object({ 12 | title: z.string().min(1).max(100).describe('The title of the todo'), 13 | content: z.string().min(1).max(100).describe('The content of the todo'), 14 | }), 15 | deleteTodo: z.object({ 16 | index: z.number().describe('The index of the todo to delete'), 17 | }), 18 | toggleTodo: z 19 | .object({ 20 | index: z.number().describe('The index of the todo to toggle'), 21 | }) 22 | .describe('Toggle whether the todo item is done or not'), 23 | doNothing: z.object({}).describe('Do nothing'), 24 | }, 25 | }); 26 | 27 | interface Todo { 28 | title: string; 29 | content: string; 30 | done: boolean; 31 | } 32 | 33 | const machine = setup({ 34 | types: { 35 | context: {} as { 36 | todos: Todo[]; 37 | command: string | null; 38 | }, 39 | events: {} as 40 | | EventFromExpert 41 | | { type: 'assist'; command: string }, 42 | }, 43 | actors: { expert: fromDecision(expert), getFromTerminal: fromTerminal }, 44 | }).createMachine({ 45 | context: { 46 | command: null, 47 | todos: [], 48 | }, 49 | on: { 50 | addTodo: { 51 | actions: assign({ 52 | todos: ({ context, event }) => [ 53 | ...context.todos, 54 | { 55 | title: event.title, 56 | content: event.content, 57 | done: false, 58 | }, 59 | ], 60 | command: null, 61 | }), 62 | target: '.idle', 63 | }, 64 | deleteTodo: { 65 | actions: assign({ 66 | todos: ({ context, event }) => { 67 | const todos = [...context.todos]; 68 | todos.splice(event.index, 1); 69 | return todos; 70 | }, 71 | command: null, 72 | }), 73 | target: '.idle', 74 | }, 75 | toggleTodo: { 76 | actions: assign({ 77 | todos: ({ context, event }) => { 78 | const todos = context.todos.map((todo, i) => { 79 | if (i === event.index) { 80 | return { 81 | ...todo, 82 | done: !todo.done, 83 | }; 84 | } 85 | return todo; 86 | }); 87 | 88 | return todos; 89 | }, 90 | command: null, 91 | }), 92 | target: '.idle', 93 | }, 94 | doNothing: { target: '.idle' }, 95 | }, 96 | initial: 'idle', 97 | states: { 98 | idle: { 99 | invoke: { 100 | src: 'getFromTerminal', 101 | input: '\nEnter a command:', 102 | onDone: { 103 | actions: assign({ 104 | command: ({ event }) => event.output, 105 | }), 106 | target: 'assisting', 107 | }, 108 | }, 109 | on: { 110 | assist: { 111 | target: 'assisting', 112 | actions: assign({ 113 | command: ({ event }) => event.command, 114 | }), 115 | }, 116 | }, 117 | }, 118 | assisting: { 119 | invoke: { 120 | src: 'expert', 121 | input: ({ context }) => ({ 122 | context: { 123 | command: context.command, 124 | todos: context.todos, 125 | }, 126 | goal: 'Interpret the command as an action for this todo list; for example, "I need donuts" would add a todo item with the message "Get donuts".', 127 | }), 128 | }, 129 | }, 130 | }, 131 | }); 132 | 133 | const actor = createActor(machine); 134 | actor.subscribe((s) => { 135 | console.log(s.context.todos); 136 | }); 137 | actor.start(); 138 | -------------------------------------------------------------------------------- /examples/tutor.ts: -------------------------------------------------------------------------------- 1 | import { assign, createActor, log, setup } from 'xstate'; 2 | import { fromTerminal } from './helpers/helpers'; 3 | import { createExpert, EventFromExpert, fromDecision } from '../src'; 4 | import { z } from 'zod'; 5 | import { openai } from '@ai-sdk/openai'; 6 | 7 | const expert = createExpert({ 8 | id: 'tutor', 9 | model: openai('gpt-4o-mini'), 10 | events: { 11 | teach: z.object({ 12 | instruction: z 13 | .string() 14 | .describe( 15 | 'The feedback to give the human, correcting any grammatical errors, misspellings, etc.' 16 | ), 17 | }), 18 | respond: z.object({ 19 | response: z.string().describe('The response to the human in Spanish'), 20 | }), 21 | }, 22 | description: 23 | 'You are an expert Spanish tutor. You will respond to the human in Spanish.', 24 | }); 25 | 26 | const machine = setup({ 27 | types: { 28 | context: {} as { 29 | conversation: string[]; 30 | }, 31 | events: {} as EventFromExpert, 32 | }, 33 | actors: { expert: fromDecision(expert), getFromTerminal: fromTerminal }, 34 | }).createMachine({ 35 | initial: 'human', 36 | context: { 37 | conversation: [], 38 | }, 39 | states: { 40 | human: { 41 | invoke: { 42 | src: 'getFromTerminal', 43 | input: 'Say something in Spanish:', 44 | onDone: { 45 | actions: assign({ 46 | conversation: ({ context, event }) => 47 | context.conversation.concat(`User: ` + event.output), 48 | }), 49 | target: 'ai', 50 | }, 51 | }, 52 | }, 53 | ai: { 54 | initial: 'teaching', 55 | states: { 56 | teaching: { 57 | invoke: { 58 | src: 'expert', 59 | input: ({ context }) => ({ 60 | context, 61 | goal: 'Give brief feedback to the human based on the most recent response of the conversation', 62 | maxTokens: 100, 63 | }), 64 | }, 65 | on: { 66 | teach: { 67 | actions: ({ event }) => console.log(event.instruction), 68 | target: 'responding', 69 | }, 70 | }, 71 | }, 72 | responding: { 73 | invoke: { 74 | src: 'expert', 75 | input: ({ context }) => ({ 76 | context, 77 | goal: 'Respond to the last message of the conversation in Spanish', 78 | }), 79 | }, 80 | on: { 81 | respond: { 82 | actions: [ 83 | assign({ 84 | conversation: ({ context, event }) => 85 | context.conversation.concat(`Expert: ` + event.response), 86 | }), 87 | log(({ event }) => event.response), 88 | ], 89 | target: 'done', 90 | }, 91 | }, 92 | }, 93 | done: { type: 'final' }, 94 | }, 95 | onDone: { target: 'human' }, 96 | }, 97 | }, 98 | }); 99 | 100 | createActor(machine).start(); 101 | -------------------------------------------------------------------------------- /examples/verify.ts: -------------------------------------------------------------------------------- 1 | import { assign, createActor, setup, log } from 'xstate'; 2 | import { fromTerminal } from './helpers/helpers'; 3 | import { createExpert, EventFromExpert, fromDecision } from '../src'; 4 | import { z } from 'zod'; 5 | import { openai } from '@ai-sdk/openai'; 6 | 7 | const expert = createExpert({ 8 | id: 'verifier', 9 | model: openai('gpt-3.5-turbo-16k-0613'), 10 | events: { 11 | 'expert.validateAnswer': z.object({ 12 | isValid: z.boolean(), 13 | feedback: z.string(), 14 | }), 15 | 'expert.answerQuestion': z.object({ 16 | answer: z.string().describe('The answer from the expert'), 17 | }), 18 | 'expert.validateQuestion': z.object({ 19 | isValid: z 20 | .boolean() 21 | .describe( 22 | 'Whether the question is a valid question; that is, is it possible to even answer this question in a verifiably correct way?' 23 | ), 24 | explanation: z 25 | .string() 26 | .describe('An explanation for why the question is or is not valid'), 27 | }), 28 | }, 29 | }); 30 | 31 | const machine = setup({ 32 | types: { 33 | context: {} as { 34 | question: string | null; 35 | answer: string | null; 36 | validation: string | null; 37 | }, 38 | events: {} as EventFromExpert, 39 | }, 40 | actors: { 41 | getFromTerminal: fromTerminal, 42 | expert: fromDecision(expert), 43 | }, 44 | }).createMachine({ 45 | initial: 'askQuestion', 46 | context: { question: null, answer: null, validation: null }, 47 | states: { 48 | askQuestion: { 49 | invoke: { 50 | src: 'getFromTerminal', 51 | input: 'Ask a (potentially silly) question', 52 | onDone: { 53 | actions: assign({ 54 | question: ({ event }) => event.output, 55 | }), 56 | target: 'validateQuestion', 57 | }, 58 | }, 59 | }, 60 | validateQuestion: { 61 | invoke: { 62 | src: 'expert', 63 | input: ({ context }) => ({ 64 | goal: `Validate this question: ${context.question!}`, 65 | }), 66 | }, 67 | on: { 68 | 'expert.validateQuestion': [ 69 | { 70 | target: 'askQuestion', 71 | guard: ({ event }) => !event.isValid, 72 | actions: log(({ event }) => event.explanation), 73 | }, 74 | { 75 | target: 'answerQuestion', 76 | }, 77 | ], 78 | }, 79 | }, 80 | answerQuestion: { 81 | invoke: { 82 | src: 'expert', 83 | input: ({ context }) => ({ 84 | goal: `Answer this question: ${context.question}`, 85 | }), 86 | }, 87 | on: { 88 | 'expert.answerQuestion': { 89 | actions: assign({ 90 | answer: ({ event }) => event.answer, 91 | }), 92 | target: 'validateAnswer', 93 | }, 94 | }, 95 | }, 96 | validateAnswer: { 97 | invoke: { 98 | src: 'expert', 99 | input: ({ context }) => ({ 100 | goal: `Validate if this is a good answer to the question: ${context.question}\nAnswer provided: ${context.answer}`, 101 | }), 102 | }, 103 | on: { 104 | 'expert.validateAnswer': { 105 | actions: assign({ 106 | validation: ({ event }) => event.feedback, 107 | }), 108 | }, 109 | }, 110 | }, 111 | }, 112 | }); 113 | 114 | const actor = createActor(machine, {}); 115 | 116 | actor.subscribe((s) => { 117 | console.log(s.value, s.context); 118 | }); 119 | 120 | actor.start(); 121 | -------------------------------------------------------------------------------- /examples/weather-agent.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { createExpert } from '../src'; 3 | import { assign, createActor, fromPromise, setup } from 'xstate'; 4 | // import { anthropic } from '@ai-sdk/anthropic'; 5 | import { fromTerminal } from './helpers/helpers'; 6 | import { openai } from '@ai-sdk/openai'; 7 | 8 | // Create the weather expert 9 | const expert = createExpert({ 10 | id: 'weather-expert', 11 | model: openai('gpt-4o'), 12 | events: { 13 | 'weather.check': z.object({ 14 | location: z.string().describe('The location to check weather for'), 15 | }), 16 | 'weather.report': z.object({ 17 | temperature: z.string(), 18 | conditions: z.string(), 19 | }), 20 | 'expert.respond': z.object({ 21 | response: z.string(), 22 | }), 23 | }, 24 | }); 25 | 26 | // Create the weather tool/service 27 | const getWeather = (location: string) => { 28 | if ( 29 | location.toLowerCase().includes('sf') || 30 | location.toLowerCase().includes('san francisco') 31 | ) { 32 | return { 33 | temperature: '60 degrees', 34 | conditions: 'foggy', 35 | }; 36 | } 37 | return { 38 | temperature: '90 degrees', 39 | conditions: 'sunny', 40 | }; 41 | }; 42 | 43 | // Create the state machine 44 | const machine = setup({ 45 | types: { 46 | context: {} as { 47 | input: string | null; 48 | location: string | null; 49 | weather: any; 50 | }, 51 | }, 52 | actors: { 53 | getFromTerminal: fromTerminal, 54 | getWeather: fromPromise(async ({ input }: { input: string }) => { 55 | return getWeather(input); 56 | }), 57 | }, 58 | }).createMachine({ 59 | context: { 60 | input: null, 61 | location: null, 62 | weather: null, 63 | }, 64 | initial: 'user', 65 | states: { 66 | user: { 67 | invoke: { 68 | src: 'getFromTerminal', 69 | input: 'Ask me about the weather!', 70 | onDone: { 71 | actions: assign({ 72 | input: ({ event }) => event.output, 73 | }), 74 | target: 'processing', 75 | }, 76 | }, 77 | }, 78 | processing: { 79 | entry: () => console.log('Processing...'), 80 | on: { 81 | 'weather.check': { 82 | actions: assign({ 83 | input: ({ event }) => event.location, 84 | }), 85 | target: 'checking', 86 | }, 87 | 'expert.respond': { 88 | actions: ({ event }) => console.log(event.response), 89 | target: 'responded', 90 | }, 91 | }, 92 | }, 93 | checking: { 94 | entry: ({ event }) => console.log('Checking weather...', event), 95 | invoke: { 96 | src: 'getWeather', 97 | input: ({ context }) => context.input!, 98 | onDone: { 99 | actions: assign({ 100 | weather: ({ event }) => event.output, 101 | }), 102 | target: 'responding', 103 | }, 104 | }, 105 | }, 106 | responding: { 107 | on: { 108 | 'expert.respond': { 109 | actions: ({ event }) => console.log(event.response), 110 | target: 'responded', 111 | }, 112 | }, 113 | }, 114 | responded: { 115 | after: { 116 | 1000: { target: 'user' }, 117 | }, 118 | }, 119 | }, 120 | }); 121 | 122 | // Create and start the actor 123 | const actor = createActor(machine).start(); 124 | 125 | expert.interact(actor, ({ state }) => { 126 | if (state.matches('processing')) { 127 | return { 128 | goal: 'Determine if user is asking about weather and for which location. If so, get the weather. Otherwise, respond to the user.', 129 | messages: expert.getMessages(), 130 | }; 131 | } 132 | 133 | if (state.matches('responding')) { 134 | return { 135 | goal: 'Provide a natural response about the weather in ${context.location}', 136 | messages: expert.getMessages(), 137 | }; 138 | } 139 | }); 140 | -------------------------------------------------------------------------------- /examples/weather.ts: -------------------------------------------------------------------------------- 1 | import { createExpert, EventFromExpert, fromDecision } from '../src'; 2 | import { assign, createActor, fromPromise, log, setup } from 'xstate'; 3 | import { fromTerminal } from './helpers/helpers'; 4 | import { z } from 'zod'; 5 | import { openai } from '@ai-sdk/openai'; 6 | 7 | async function searchTavily( 8 | input: string, 9 | options: { 10 | maxResults?: number; 11 | apiKey: string; 12 | } 13 | ) { 14 | const body: Record = { 15 | query: input, 16 | max_results: options.maxResults, 17 | api_key: options.apiKey, 18 | }; 19 | 20 | const response = await fetch('https://api.tavily.com/search', { 21 | method: 'POST', 22 | headers: { 23 | 'content-type': 'application/json', 24 | }, 25 | body: JSON.stringify(body), 26 | }); 27 | 28 | const json = await response.json(); 29 | if (!response.ok) { 30 | throw new Error( 31 | `Request failed with status code ${response.status}: ${json.error}` 32 | ); 33 | } 34 | if (!Array.isArray(json.results)) { 35 | throw new Error(`Could not parse Tavily results. Please try again.`); 36 | } 37 | return JSON.stringify(json.results); 38 | } 39 | 40 | const getWeather = fromPromise(async ({ input }: { input: string }) => { 41 | const results = await searchTavily( 42 | `Get the weather for this location: ${input}`, 43 | { 44 | maxResults: 5, 45 | apiKey: process.env.TAVILY_API_KEY!, 46 | } 47 | ); 48 | return results; 49 | }); 50 | 51 | const expert = createExpert({ 52 | id: 'weather', 53 | model: openai('gpt-4o-mini'), 54 | events: { 55 | 'expert.getWeather': z.object({ 56 | location: z.string().describe('The location to get the weather for'), 57 | }), 58 | 'expert.reportWeather': z.object({ 59 | location: z 60 | .string() 61 | .describe('The location the weather is being reported for'), 62 | highF: z.number().describe('The high temperature today in Fahrenheit'), 63 | lowF: z.number().describe('The low temperature today in Fahrenheit'), 64 | summary: z.string().describe('A summary of the weather conditions'), 65 | }), 66 | 'expert.doSomethingElse': z 67 | .object({}) 68 | .describe( 69 | 'Do something else, because the user did not provide a location' 70 | ), 71 | }, 72 | }); 73 | 74 | const machine = setup({ 75 | types: { 76 | context: {} as { 77 | location: string; 78 | count: number; 79 | result: string | null; 80 | }, 81 | events: {} as EventFromExpert, 82 | }, 83 | actors: { 84 | expert: fromDecision(expert), 85 | getWeather, 86 | getFromTerminal: fromTerminal, 87 | }, 88 | }).createMachine({ 89 | initial: 'getLocation', 90 | context: { 91 | location: '', 92 | count: 0, 93 | result: null, 94 | }, 95 | states: { 96 | getLocation: { 97 | invoke: { 98 | src: 'getFromTerminal', 99 | input: 'Location?', 100 | onDone: { 101 | actions: assign({ 102 | location: ({ event }) => event.output, 103 | }), 104 | target: 'decide', 105 | }, 106 | }, 107 | always: { 108 | guard: ({ context }) => context.count >= 3, 109 | target: 'stopped', 110 | }, 111 | }, 112 | decide: { 113 | entry: log('Deciding...'), 114 | on: { 115 | 'expert.getWeather': { 116 | actions: log(({ event }) => event), 117 | target: 'gettingWeather', 118 | }, 119 | 'expert.doSomethingElse': 'getLocation', 120 | }, 121 | }, 122 | gettingWeather: { 123 | entry: log('Getting weather...'), 124 | invoke: { 125 | src: 'getWeather', 126 | input: ({ context }) => context.location, 127 | onDone: { 128 | actions: [ 129 | log(({ event }) => event.output), 130 | assign({ 131 | count: ({ context }) => context.count + 1, 132 | result: ({ event }) => event.output, 133 | }), 134 | ], 135 | target: 'reportWeather', 136 | }, 137 | }, 138 | }, 139 | reportWeather: { 140 | on: { 141 | 'expert.reportWeather': { 142 | actions: log(({ event }) => event), 143 | target: 'getLocation', 144 | }, 145 | }, 146 | }, 147 | stopped: { 148 | entry: log('You have used up your search quota. Goodbye!'), 149 | }, 150 | }, 151 | exit: () => { 152 | process.exit(); 153 | }, 154 | }); 155 | 156 | const actor = createActor(machine); 157 | actor.start(); 158 | 159 | expert.interact(actor, ({ state }) => { 160 | if (state.matches('decide')) { 161 | return { 162 | goal: `Decide what to do based on the given location, which may or may not be a location`, 163 | context: { 164 | location: state.context.location, 165 | }, 166 | }; 167 | } 168 | 169 | if (state.matches('reportWeather')) { 170 | return { 171 | goal: `Report the weather for the given location`, 172 | context: { 173 | location: state.context.location, 174 | result: state.context.result, 175 | }, 176 | }; 177 | } 178 | }); 179 | -------------------------------------------------------------------------------- /examples/wiki.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { createExpert } from '../src'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { CoreMessage, generateText, streamText } from 'ai'; 5 | 6 | const expert = createExpert({ 7 | id: 'wiki', 8 | model: openai('gpt-4o-mini'), 9 | events: { 10 | provideAnswer: z.object({ 11 | answer: z.string().describe('The answer'), 12 | }), 13 | researchTopic: z.object({ 14 | topic: z.string().describe('The topic to research'), 15 | }), 16 | }, 17 | }); 18 | 19 | expert.onMessage((msg) => { 20 | console.log(msg); 21 | }); 22 | 23 | async function main() { 24 | const result = await generateText({ 25 | model: expert.model, 26 | prompt: 'When was Deadpool 2 released?', 27 | }); 28 | 29 | for (const msg of await result.response.messages) { 30 | expert.addMessage(msg); 31 | } 32 | 33 | const response2 = await streamText({ 34 | model: expert.model, 35 | messages: (expert.getMessages() as CoreMessage[]).concat({ 36 | role: 'user', 37 | content: 'What about the first one?', 38 | }), 39 | }); 40 | 41 | let text = ''; 42 | 43 | for await (const t of response2.textStream) { 44 | text += t; 45 | console.log(text); 46 | } 47 | } 48 | 49 | main(); 50 | -------------------------------------------------------------------------------- /examples/word.ts: -------------------------------------------------------------------------------- 1 | import { assign, createActor, log, setup } from 'xstate'; 2 | import { fromTerminal } from './helpers/helpers'; 3 | import { 4 | ContextFromExpert, 5 | createExpert, 6 | fromDecision, 7 | TypesFromExpert, 8 | } from '../src'; 9 | import { z } from 'zod'; 10 | import { openai } from '@ai-sdk/openai'; 11 | 12 | const expert = createExpert({ 13 | id: 'word', 14 | model: openai('gpt-4o-mini'), 15 | context: { 16 | word: z.string().nullable().describe('The word to guess'), 17 | guessedWord: z.string().nullable().describe('The guessed word'), 18 | lettersGuessed: z.array(z.string()).describe('The letters guessed'), 19 | }, 20 | events: { 21 | 'expert.guessLetter': z.object({ 22 | letter: z.string().min(1).max(1).describe('The letter guessed'), 23 | reasoning: z.string().describe('The reasoning behind the guess'), 24 | }), 25 | 26 | 'expert.guessWord': z.object({ 27 | word: z.string().describe('The word guessed'), 28 | }), 29 | 30 | 'expert.respond': z.object({ 31 | response: z 32 | .string() 33 | .describe( 34 | 'The response from the expert, detailing why the guess was correct or incorrect based on the letters guessed.' 35 | ), 36 | }), 37 | }, 38 | }); 39 | 40 | const context = { 41 | word: null, 42 | guessedWord: null, 43 | lettersGuessed: [], 44 | } satisfies ContextFromExpert; 45 | 46 | const wordGuesserMachine = setup({ 47 | types: {} as TypesFromExpert, 48 | actors: { 49 | expert: fromDecision(expert), 50 | getFromTerminal: fromTerminal, 51 | }, 52 | actions: { 53 | resetContext: assign(context), 54 | }, 55 | }).createMachine({ 56 | initial: 'providingWord', 57 | context, 58 | states: { 59 | providingWord: { 60 | entry: 'resetContext', 61 | invoke: { 62 | src: 'getFromTerminal', 63 | input: 'Enter a word, and an expert will try to guess it.', 64 | onDone: { 65 | actions: assign({ 66 | word: ({ event }) => event.output, 67 | }), 68 | target: 'guessing', 69 | }, 70 | }, 71 | }, 72 | guessing: { 73 | always: { 74 | guard: ({ context }) => context.lettersGuessed.length > 10, 75 | target: 'finalGuess', 76 | }, 77 | invoke: { 78 | src: 'expert', 79 | input: ({ context }) => ({ 80 | context: { 81 | wordLength: context.word!.length, 82 | lettersGuessed: context.lettersGuessed, 83 | lettersMatched: context 84 | .word!.split('') 85 | .map((letter) => 86 | context.lettersGuessed.includes(letter.toUpperCase()) 87 | ? letter.toUpperCase() 88 | : '_' 89 | ) 90 | .join(''), 91 | }, 92 | goal: `You are trying to guess the word. Please make your next guess - guess a letter or, if you think you know the word, guess the full word. You can only make 10 total guesses. If you are confident you know the word, it is better to guess the word.`, 93 | }), 94 | }, 95 | on: { 96 | 'expert.guessLetter': { 97 | actions: [ 98 | assign({ 99 | lettersGuessed: ({ context, event }) => { 100 | return [...context.lettersGuessed, event.letter.toUpperCase()]; 101 | }, 102 | }), 103 | log(({ event }) => event), 104 | ], 105 | target: 'guessing', 106 | reenter: true, 107 | }, 108 | 'expert.guessWord': { 109 | actions: [ 110 | assign({ 111 | guessedWord: ({ event }) => event.word, 112 | }), 113 | log(({ event }) => event), 114 | ], 115 | target: 'gameOver', 116 | }, 117 | }, 118 | }, 119 | finalGuess: { 120 | invoke: { 121 | src: 'expert', 122 | input: ({ context }) => ({ 123 | context: { 124 | lettersGuessed: context.lettersGuessed, 125 | }, 126 | goal: `You have used all 10 guesses. These letters matched: ${context 127 | .word!.split('') 128 | .map((letter) => 129 | context.lettersGuessed.includes(letter.toUpperCase()) 130 | ? letter.toUpperCase() 131 | : '_' 132 | ) 133 | .join('')}. Guess the word.`, 134 | }), 135 | }, 136 | on: { 137 | 'expert.guessWord': { 138 | actions: [ 139 | assign({ 140 | guessedWord: ({ event }) => event.word, 141 | }), 142 | log(({ event }) => event), 143 | ], 144 | target: 'gameOver', 145 | }, 146 | }, 147 | }, 148 | gameOver: { 149 | invoke: { 150 | src: 'expert', 151 | input: ({ context }) => ({ 152 | context, 153 | goal: `Why do you think you won or lost?`, 154 | }), 155 | }, 156 | entry: log(({ context }) => { 157 | if ( 158 | context.guessedWord?.toUpperCase() === context.word?.toUpperCase() 159 | ) { 160 | return 'The expert won!'; 161 | } else { 162 | return 'The expert lost! The word was ' + context.word; 163 | } 164 | }), 165 | on: { 166 | 'expert.respond': { 167 | actions: log(({ event }) => event.response), 168 | target: 'providingWord', 169 | }, 170 | }, 171 | }, 172 | }, 173 | exit: () => process.exit(), 174 | }); 175 | 176 | const game = createActor(wordGuesserMachine); 177 | 178 | game.start(); 179 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@statelyai/agent", 3 | "version": "2.0.0-next.5", 4 | "description": "Stateful agents that make decisions based on finite-state machine models", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "build": "tsup src/index.ts --format cjs,esm --dts", 10 | "lint": "tsc --noEmit", 11 | "test": "vitest", 12 | "test:ci": "vitest --run", 13 | "example": "ts-node examples/helpers/runner.ts", 14 | "prepublishOnly": "tsup src/index.ts --format cjs,esm --dts", 15 | "changeset": "changeset", 16 | "release": "changeset publish", 17 | "version": "changeset version", 18 | "coverage": "vitest run --coverage" 19 | }, 20 | "keywords": [ 21 | "ai", 22 | "state machine", 23 | "agent", 24 | "rl", 25 | "reinforcement learning" 26 | ], 27 | "author": "David Khourshid ", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "@ai-sdk/anthropic": "^0.0.54", 31 | "@ai-sdk/openai": "^1.3.22", 32 | "@changesets/changelog-github": "^0.5.1", 33 | "@changesets/cli": "^2.29.4", 34 | "@langchain/community": "^0.0.53", 35 | "@langchain/core": "^0.1.63", 36 | "@langchain/openai": "^0.0.28", 37 | "@tavily/core": "^0.0.2", 38 | "@types/node": "^20.17.47", 39 | "@types/object-hash": "^3.0.6", 40 | "@vitest/coverage-v8": "^2.1.9", 41 | "ai": "^4.3.15", 42 | "dotenv": "^16.5.0", 43 | "json-schema-to-ts": "^3.1.1", 44 | "ts-node": "^10.9.2", 45 | "tsup": "^8.4.0", 46 | "typescript": "^5.8.3", 47 | "vitest": "^2.1.9", 48 | "wikipedia": "^2.1.2", 49 | "zod": "^3.24.4" 50 | }, 51 | "publishConfig": { 52 | "access": "public" 53 | }, 54 | "dependencies": { 55 | "@xstate/graph": "^2.0.1", 56 | "ajv": "^8.17.1", 57 | "object-hash": "^3.0.0", 58 | "xstate": "^5.19.3", 59 | "zod-to-json-schema": "^3.24.5" 60 | }, 61 | "peerDependencies": { 62 | "ai": "^4.3.10" 63 | }, 64 | "packageManager": "pnpm@8.11.0" 65 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Stately Expert 2 | 3 | Stately Expert is a flexible framework for building AI agents using state machines. Stately agents go beyond normal LLM-based AI agents by: 4 | 5 | - Using state machines to guide the agent's behavior, powered by [XState](https://stately.ai/docs/xstate) 6 | - Incorporating **observations**, **message history**, and **feedback** to the agent decision-making and text-generation processes, as needed 7 | - Enabling custom **planning** abilities for agents to achieve specific goals based on state machine logic, observations, and feedback 8 | - 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 9 | 10 | **Read the documentation: [stately.ai/docs/agents](https://stately.ai/docs/agents)** 11 | 12 | # Stately Expert 13 | 14 | Stately Expert is a framework for building intelligent AI agents that are guided by state machines and learn from experience. Rather than relying solely on LLM responses, agents use structured observations, feedback, and insights to make informed decisions and improve over time. 15 | 16 | ## Overview 17 | 18 | Stately Expert combines state machines with reinforcement learning concepts to create agents that: 19 | 20 | - Make decisions based on clear state transitions and goals 21 | - Learn from past experiences and feedback 22 | - Generate insights about state changes 23 | - Improve decision-making through structured rewards 24 | - Support multiple decision-making policies 25 | 26 | The framework is built on [XState](https://stately.ai/docs/xstate) for state machine management and integrates with the [Vercel AI SDK](https://sdk.vercel.ai/) for flexible LLM support. 27 | 28 | ## Key Concepts 29 | 30 | - **Observations**: Records of state transitions, containing: 31 | 32 | - Previous state 33 | - Event/action taken 34 | - Resulting state 35 | - Metadata about the transition 36 | 37 | - **Decisions**: Actions the agent chooses to take based on: 38 | 39 | - Current state 40 | - Goal state 41 | - Past observations 42 | - Available feedback and insights 43 | - Decision-making policy 44 | 45 | - **Feedback**: Rewards or evaluations given to decisions, helping the agent learn which actions are effective 46 | 47 | - **Insights**: Additional context about state transitions, helping the agent understand cause and effect 48 | 49 | - **Episodes**: Complete sequences of state transitions, from initial state to goal state (similar to RL episodes) 50 | 51 | ## Quick Start 52 | 53 | TODO 54 | 55 | ## Why Stately Expert? 56 | 57 | Traditional LLM-based agents often make decisions with limited context and no ability to learn from experience. Stately Expert provides: 58 | 59 | 1. **Structured Decision Making**: State machines provide clear boundaries and valid transitions 60 | 61 | 2. **Learning from Experience**: Experts improve through feedback and observations 62 | 63 | 3. **Contextual Awareness**: Insights and observations inform better decisions 64 | 65 | 4. **Flexible Policies**: Different approaches for different needs 66 | 67 | 5. **Storage Integration**: Optional persistence of experiences and learning 68 | -------------------------------------------------------------------------------- /src/decide.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | import { createExpert, fromDecision } from '.'; 3 | import { createActor, createMachine, waitFor } from 'xstate'; 4 | import { z } from 'zod'; 5 | import { LanguageModelV1CallOptions } from 'ai'; 6 | import { dummyResponseValues, MockLanguageModelV1 } from './mockModel'; 7 | 8 | const doGenerate = async (params: LanguageModelV1CallOptions) => { 9 | const keys = 10 | params.mode.type === 'regular' ? params.mode.tools?.map((t) => t.name) : []; 11 | 12 | return { 13 | ...dummyResponseValues, 14 | finishReason: 'tool-calls', 15 | toolCalls: [ 16 | { 17 | toolCallType: 'function', 18 | toolCallId: 'call-1', 19 | toolName: keys![0], 20 | args: `{ "type": "${keys?.[0]}" }`, 21 | }, 22 | ], 23 | } as any; 24 | }; 25 | 26 | test('fromDecision() makes a decision', async () => { 27 | const model = new MockLanguageModelV1({ 28 | doGenerate, 29 | }); 30 | const expert = createExpert({ 31 | id: 'test', 32 | model, 33 | events: { 34 | doFirst: z.object({}), 35 | doSecond: z.object({}), 36 | }, 37 | }); 38 | 39 | const machine = createMachine({ 40 | initial: 'first', 41 | states: { 42 | first: { 43 | invoke: { 44 | src: fromDecision(expert), 45 | }, 46 | on: { 47 | doFirst: 'second', 48 | }, 49 | }, 50 | second: { 51 | invoke: { 52 | src: fromDecision(expert), 53 | }, 54 | on: { 55 | doSecond: 'third', 56 | }, 57 | }, 58 | third: {}, 59 | }, 60 | }); 61 | 62 | const actor = createActor(machine); 63 | 64 | actor.start(); 65 | 66 | await waitFor(actor, (s) => s.matches('third')); 67 | 68 | expect(actor.getSnapshot().value).toBe('third'); 69 | }); 70 | 71 | test('interacts with an actor', async () => { 72 | const model = new MockLanguageModelV1({ 73 | doGenerate, 74 | }); 75 | const expert = createExpert({ 76 | id: 'test', 77 | model, 78 | events: { 79 | doFirst: z.object({}), 80 | doSecond: z.object({}), 81 | }, 82 | }); 83 | 84 | const machine = createMachine({ 85 | initial: 'first', 86 | states: { 87 | first: { 88 | on: { 89 | doFirst: 'second', 90 | }, 91 | }, 92 | second: { 93 | on: { 94 | doSecond: 'third', 95 | }, 96 | }, 97 | third: {}, 98 | }, 99 | }); 100 | 101 | const actor = createActor(machine); 102 | 103 | expert.interact(actor, () => ({ 104 | goal: 'Some goal', 105 | })); 106 | 107 | actor.start(); 108 | 109 | await waitFor(actor, (s) => s.matches('third')); 110 | 111 | expect(actor.getSnapshot().value).toBe('third'); 112 | }); 113 | 114 | test('interacts with an actor (late interaction)', async () => { 115 | const model = new MockLanguageModelV1({ 116 | doGenerate, 117 | }); 118 | const expert = createExpert({ 119 | id: 'test', 120 | model, 121 | events: { 122 | doFirst: z.object({}), 123 | doSecond: z.object({}), 124 | }, 125 | }); 126 | 127 | const machine = createMachine({ 128 | initial: 'first', 129 | states: { 130 | first: { 131 | on: { 132 | doFirst: 'second', 133 | }, 134 | }, 135 | second: { 136 | on: { 137 | doSecond: 'third', 138 | }, 139 | }, 140 | third: {}, 141 | }, 142 | }); 143 | 144 | const actor = createActor(machine); 145 | 146 | actor.start(); 147 | 148 | expert.interact(actor, () => ({ 149 | goal: 'Some goal', 150 | })); 151 | 152 | await waitFor(actor, (s) => s.matches('third')); 153 | 154 | expect(actor.getSnapshot().value).toBe('third'); 155 | }); 156 | 157 | test('expert.decide() makes a decision based on goal and state (tool policy)', async () => { 158 | const model = new MockLanguageModelV1({ 159 | doGenerate, 160 | }); 161 | 162 | const expert = createExpert({ 163 | id: 'test', 164 | model, 165 | events: { 166 | MOVE: z.object({}), 167 | }, 168 | }); 169 | 170 | const decision = await expert.decide({ 171 | goal: 'Make the best move', 172 | state: { 173 | value: 'playing', 174 | context: { 175 | board: [0, 0, 0], 176 | }, 177 | }, 178 | machine: createMachine({ 179 | initial: 'playing', 180 | states: { 181 | playing: { 182 | on: { 183 | MOVE: 'next', 184 | }, 185 | }, 186 | next: {}, 187 | }, 188 | }), 189 | }); 190 | 191 | expect(decision).toBeDefined(); 192 | expect(decision!.nextEvent).toEqual( 193 | expect.objectContaining({ 194 | type: 'MOVE', 195 | }) 196 | ); 197 | }); 198 | 199 | test.each([ 200 | [undefined, true], 201 | [undefined, false], 202 | [3, true], 203 | [3, false], 204 | ])( 205 | 'expert.decide() retries if a decision is not made (%i attempts, succeed: %s)', 206 | async (maxAttempts, succeed) => { 207 | let attempts = 0; 208 | const doGenerateWithRetry = async (params: LanguageModelV1CallOptions) => { 209 | const keys = 210 | params.mode.type === 'regular' 211 | ? params.mode.tools?.map((t) => t.name) 212 | : []; 213 | 214 | // console.log('try', attempts, 'max', maxAttempts); 215 | 216 | const toolCalls = 217 | succeed && attempts++ === (maxAttempts ?? 2) - 1 218 | ? [ 219 | { 220 | toolCallType: 'function', 221 | toolCallId: 'call-1', 222 | toolName: keys![0], 223 | args: `{ "type": "${keys?.[0]}" }`, 224 | }, 225 | ] 226 | : []; 227 | 228 | return { 229 | ...dummyResponseValues, 230 | finishReason: 'tool-calls', 231 | toolCalls, 232 | } as any; 233 | }; 234 | const model = new MockLanguageModelV1({ 235 | doGenerate: doGenerateWithRetry, 236 | }); 237 | 238 | const expert = createExpert({ 239 | id: 'test', 240 | model, 241 | events: { 242 | MOVE: z.object({}), 243 | }, 244 | }); 245 | 246 | const decision = await expert.decide({ 247 | goal: 'Make the best move', 248 | state: { 249 | value: 'playing', 250 | }, 251 | machine: createMachine({ 252 | initial: 'playing', 253 | states: { 254 | playing: { 255 | on: { 256 | MOVE: 'win', 257 | }, 258 | }, 259 | win: {}, 260 | }, 261 | }), 262 | maxAttempts, 263 | }); 264 | 265 | if (!succeed) { 266 | expect(decision).toBeUndefined(); 267 | } else { 268 | expect(decision).toBeDefined(); 269 | expect(decision!.nextEvent).toEqual( 270 | expect.objectContaining({ 271 | type: 'MOVE', 272 | }) 273 | ); 274 | } 275 | } 276 | ); 277 | 278 | test.each([['MOVE'], ['FORFEIT']] as const)( 279 | 'expert.decide() respects allowedEvents constraint (event: %s)', 280 | async (allowedEventType) => { 281 | const model = new MockLanguageModelV1({ 282 | doGenerate: async (params: LanguageModelV1CallOptions) => { 283 | const keys = 284 | params.mode.type === 'regular' 285 | ? params.mode.tools?.map((t) => t.name) 286 | : []; 287 | 288 | return { 289 | ...dummyResponseValues, 290 | finishReason: 'tool-calls', 291 | toolCalls: [ 292 | { 293 | toolCallType: 'function', 294 | toolCallId: 'call-1', 295 | toolName: keys![0], 296 | args: `{ "type": "${keys?.[0]}" }`, 297 | }, 298 | ], 299 | } as any; 300 | }, 301 | }); 302 | 303 | const expert = createExpert({ 304 | id: 'test', 305 | model, 306 | events: { 307 | MOVE: z.object({}), 308 | SKIP: z.object({}), 309 | FORFEIT: z.object({}), 310 | }, 311 | }); 312 | 313 | const decision = await expert.decide({ 314 | goal: 'Make the best move', 315 | state: { 316 | value: 'playing', 317 | context: {}, 318 | }, 319 | allowedEvents: [allowedEventType], 320 | }); 321 | 322 | expect(decision?.nextEvent?.type).toEqual(allowedEventType); 323 | } 324 | ); 325 | 326 | test('expert.decide() accepts custom episodeId', async () => { 327 | const model = new MockLanguageModelV1({ 328 | doGenerate, 329 | }); 330 | const expert = createExpert({ 331 | id: 'test', 332 | events: { 333 | WIN: z.object({}), 334 | }, 335 | model, 336 | }); 337 | 338 | const customEpisodeId = 'custom-episode-123'; 339 | const decision = await expert.decide({ 340 | goal: 'Win the game', 341 | state: { value: 'playing' }, 342 | episodeId: customEpisodeId, 343 | }); 344 | 345 | expect(decision?.episodeId).toEqual(customEpisodeId); 346 | }); 347 | -------------------------------------------------------------------------------- /src/decide.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AnyActor, 3 | AnyMachineSnapshot, 4 | fromPromise, 5 | PromiseActorLogic, 6 | } from 'xstate'; 7 | import { 8 | AnyExpert, 9 | ExpertDecideInput, 10 | TransitionData, 11 | ExpertDecision, 12 | } from './types'; 13 | import { getTransitions } from './utils'; 14 | import { CoreTool, generateText, LanguageModel, tool } from 'ai'; 15 | import { ZodEventMapping } from './schemas'; 16 | 17 | export type ExpertDecideLogicInput = { 18 | goal: string; 19 | model?: LanguageModel; 20 | context?: Record; 21 | } & Omit[0], 'model' | 'tools' | 'prompt'>; 22 | 23 | export type MachineDecisionLogic = PromiseActorLogic< 24 | ExpertDecision | undefined, 25 | ExpertDecideLogicInput | string 26 | >; 27 | 28 | export function fromDecision( 29 | expert: TExpert, 30 | defaultInput?: ExpertDecideInput 31 | ): MachineDecisionLogic { 32 | return fromPromise(async ({ input, self }) => { 33 | const parentRef = self._parent; 34 | if (!parentRef) { 35 | return; 36 | } 37 | 38 | const snapshot = parentRef.getSnapshot() as AnyMachineSnapshot; 39 | const inputObject = typeof input === 'string' ? { goal: input } : input; 40 | const resolvedInput = { 41 | ...defaultInput, 42 | ...inputObject, 43 | }; 44 | 45 | const decision = await expert.decide({ 46 | machine: (parentRef as AnyActor).logic, 47 | state: snapshot, 48 | allowedEvents: resolvedInput.allowedEvents as any[], 49 | ...resolvedInput, 50 | // @ts-ignore 51 | messages: resolvedInput.messages, 52 | }); 53 | 54 | if (decision?.nextEvent) { 55 | parentRef.send(decision.nextEvent); 56 | } 57 | 58 | return decision; 59 | }) as MachineDecisionLogic; 60 | } 61 | 62 | export function getToolMap( 63 | expert: TExpert, 64 | input: ExpertDecideInput 65 | ): Record> | undefined { 66 | const events = input.events ?? (expert.events as ZodEventMapping); 67 | // Get all of the possible next transitions 68 | const transitions: TransitionData[] = input.machine 69 | ? getTransitions(input.state, input.machine) 70 | : Object.entries(events).map(([eventType, { description }]) => ({ 71 | eventType, 72 | description, 73 | })); 74 | 75 | // Only keep the transitions that match the event types that are in the event mapping 76 | // TODO: allow for custom filters 77 | const filter = (eventType: string) => Object.keys(events).includes(eventType); 78 | 79 | // Mapping of each event type (e.g. "mouse.click") 80 | // to a valid function name (e.g. "mouse_click") 81 | const functionNameMapping: Record = {}; 82 | 83 | const toolTransitions = transitions 84 | .filter((t) => { 85 | return filter(t.eventType); 86 | }) 87 | .map((t) => { 88 | const name = t.eventType.replace(/\./g, '_'); 89 | functionNameMapping[name] = t.eventType; 90 | 91 | return { 92 | type: 'function', 93 | eventType: t.eventType, 94 | description: t.description, 95 | name, 96 | } as const; 97 | }); 98 | 99 | // Convert the transition data to a tool map that the 100 | // Vercel AI SDK can use 101 | const toolMap: Record> = {}; 102 | for (const toolTransitionData of toolTransitions) { 103 | const toolZodType = input.events?.[toolTransitionData.eventType]; 104 | 105 | if (!toolZodType) { 106 | continue; 107 | } 108 | 109 | toolMap[toolTransitionData.name] = tool({ 110 | description: toolZodType?.description ?? toolTransitionData.description, 111 | parameters: toolZodType, 112 | execute: async (params: Record) => { 113 | const event = { 114 | type: toolTransitionData.eventType, 115 | ...params, 116 | }; 117 | 118 | return event; 119 | }, 120 | }); 121 | } 122 | 123 | if (!Object.keys(toolMap).length) { 124 | // No valid transitions for the specified tools 125 | return undefined; 126 | } 127 | 128 | return toolMap; 129 | } 130 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { createExpert } from './expert'; 2 | export { fromText, fromTextStream } from './text'; 3 | export { fromDecision } from './decide'; 4 | export * from './types'; 5 | export * from './policies'; 6 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Experimental_LanguageModelV1Middleware as LanguageModelV1Middleware, 3 | LanguageModelV1StreamPart, 4 | } from 'ai'; 5 | import { 6 | AnyExpert, 7 | LanguageModelV1TextPart, 8 | LanguageModelV1ToolCallPart, 9 | } from './types'; 10 | import { randomId } from './utils'; 11 | 12 | export function createExpertMiddleware(expert: AnyExpert) { 13 | const middleware: LanguageModelV1Middleware = { 14 | transformParams: async ({ params }) => { 15 | return params; 16 | }, 17 | wrapGenerate: async ({ doGenerate, params }) => { 18 | const id = randomId(); 19 | 20 | params.prompt.forEach((message) => { 21 | expert.addMessage({ 22 | id, 23 | ...message, 24 | timestamp: Date.now(), 25 | }); 26 | }); 27 | 28 | const result = await doGenerate(); 29 | 30 | return result; 31 | }, 32 | 33 | wrapStream: async ({ doStream, params }) => { 34 | const id = randomId(); 35 | 36 | params.prompt.forEach((message) => { 37 | message.content; 38 | expert.addMessage({ 39 | id, 40 | ...message, 41 | timestamp: Date.now(), 42 | }); 43 | }); 44 | 45 | const { stream, ...rest } = await doStream(); 46 | 47 | let generatedText = ''; 48 | 49 | const transformStream = new TransformStream< 50 | LanguageModelV1StreamPart, 51 | LanguageModelV1StreamPart 52 | >({ 53 | transform(chunk, controller) { 54 | if (chunk.type === 'text-delta') { 55 | generatedText += chunk.textDelta; 56 | } 57 | 58 | controller.enqueue(chunk); 59 | }, 60 | 61 | flush() { 62 | const content: ( 63 | | LanguageModelV1TextPart 64 | | LanguageModelV1ToolCallPart 65 | )[] = []; 66 | 67 | if (generatedText) { 68 | content.push({ 69 | type: 'text', 70 | text: generatedText, 71 | }); 72 | } 73 | 74 | expert.addMessage({ 75 | id: randomId(), 76 | timestamp: Date.now(), 77 | role: 'assistant', 78 | content, 79 | responseId: id, 80 | }); 81 | }, 82 | }); 83 | 84 | return { 85 | stream: stream.pipeThrough(transformStream), 86 | ...rest, 87 | }; 88 | }, 89 | }; 90 | return middleware; 91 | } 92 | -------------------------------------------------------------------------------- /src/mockModel.ts: -------------------------------------------------------------------------------- 1 | import { LanguageModelV1 } from 'ai'; 2 | 3 | export class MockLanguageModelV1 implements LanguageModelV1 { 4 | readonly specificationVersion = 'v1'; 5 | 6 | readonly provider: LanguageModelV1['provider']; 7 | readonly modelId: LanguageModelV1['modelId']; 8 | 9 | doGenerate: LanguageModelV1['doGenerate']; 10 | doStream: LanguageModelV1['doStream']; 11 | 12 | readonly defaultObjectGenerationMode: LanguageModelV1['defaultObjectGenerationMode']; 13 | readonly supportsStructuredOutputs: LanguageModelV1['supportsStructuredOutputs']; 14 | constructor({ 15 | provider = 'mock-provider', 16 | modelId = 'mock-model-id', 17 | doGenerate = notImplemented, 18 | doStream = notImplemented, 19 | defaultObjectGenerationMode = undefined, 20 | supportsStructuredOutputs = undefined, 21 | }: { 22 | provider?: LanguageModelV1['provider']; 23 | modelId?: LanguageModelV1['modelId']; 24 | doGenerate?: LanguageModelV1['doGenerate']; 25 | doStream?: LanguageModelV1['doStream']; 26 | defaultObjectGenerationMode?: LanguageModelV1['defaultObjectGenerationMode']; 27 | supportsStructuredOutputs?: LanguageModelV1['supportsStructuredOutputs']; 28 | } = {}) { 29 | this.provider = provider; 30 | this.modelId = modelId; 31 | this.doGenerate = doGenerate; 32 | this.doStream = doStream; 33 | 34 | this.defaultObjectGenerationMode = defaultObjectGenerationMode; 35 | this.supportsStructuredOutputs = supportsStructuredOutputs; 36 | } 37 | } 38 | 39 | function notImplemented(): never { 40 | throw new Error('Not implemented'); 41 | } 42 | 43 | export const dummyResponseValues = { 44 | rawCall: { rawPrompt: 'prompt', rawSettings: {} }, 45 | finishReason: 'stop' as const, 46 | usage: { promptTokens: 10, completionTokens: 20 }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/policies/chainOfThoughtPolicy.ts: -------------------------------------------------------------------------------- 1 | import { generateText } from 'ai'; 2 | import { 3 | AnyExpert, 4 | ExpertDecideInput, 5 | ExpertDecision, 6 | PromptTemplate, 7 | } from '../types'; 8 | import { combinePromptAndMessages } from '../text'; 9 | import { toolPolicy } from './toolPolicy'; 10 | import { convertToXml } from '../utils'; 11 | 12 | const chainOfThoughtPromptTemplate: PromptTemplate = ({ 13 | stateValue, 14 | context, 15 | goal, 16 | }) => { 17 | return `${convertToXml({ stateValue, context, goal })} 18 | 19 | How would you achieve the goal? Think step-by-step.`; 20 | }; 21 | 22 | export async function chainOfThoughtPolicy( 23 | expert: T, 24 | input: ExpertDecideInput 25 | ): Promise | undefined> { 26 | const prompt = chainOfThoughtPromptTemplate({ 27 | stateValue: input.state.value, 28 | context: input.context ?? input.state.context, 29 | goal: input.goal, 30 | }); 31 | 32 | const messages = combinePromptAndMessages(prompt, input.messages); 33 | 34 | const model = input.model ? expert.wrap(input.model) : expert.model; 35 | 36 | const result = await generateText({ 37 | model, 38 | system: input.system ?? expert.description, 39 | messages, 40 | }); 41 | 42 | const decision = await toolPolicy(expert, { 43 | ...input, 44 | messages: messages.concat(result.response.messages), 45 | }); 46 | 47 | return decision; 48 | } 49 | -------------------------------------------------------------------------------- /src/policies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chainOfThoughtPolicy'; 2 | export * from './shortestPathPolicy'; 3 | export * from './toolPolicy'; 4 | -------------------------------------------------------------------------------- /src/policies/shortestPathPolicy.test.ts: -------------------------------------------------------------------------------- 1 | import { createExpert, TypesFromExpert } from '..'; 2 | import { assign, createActor, setup } from 'xstate'; 3 | import { z } from 'zod'; 4 | import { experimental_shortestPathPolicy } from './shortestPathPolicy'; 5 | import { test, expect } from 'vitest'; 6 | import { dummyResponseValues, MockLanguageModelV1 } from '../mockModel'; 7 | 8 | test.skip('should find shortest path to goal', async () => { 9 | const expert = createExpert({ 10 | id: 'counter', 11 | model: new MockLanguageModelV1({ 12 | doGenerate: async () => { 13 | return { 14 | ...dummyResponseValues, 15 | text: JSON.stringify({ 16 | type: 'object', 17 | properties: { 18 | count: { 19 | type: 'number', 20 | const: 3, 21 | }, 22 | }, 23 | required: ['count'], 24 | }), 25 | }; 26 | }, 27 | }), 28 | events: { 29 | increment: z.object({}).describe('Increment the counter by 1'), 30 | decrement: z.object({}).describe('Decrement the counter by 1'), 31 | }, 32 | context: { 33 | count: z.number().int().describe('Current count value'), 34 | }, 35 | }); 36 | 37 | const counterMachine = setup({ 38 | types: {} as TypesFromExpert, 39 | }).createMachine({ 40 | initial: 'counting', 41 | context: { count: 0 }, 42 | states: { 43 | counting: { 44 | always: { 45 | guard: ({ context }) => context.count === 3, 46 | target: 'success', 47 | }, 48 | on: { 49 | increment: { 50 | actions: assign({ count: ({ context }) => context.count + 1 }), 51 | }, 52 | decrement: { 53 | actions: assign({ count: ({ context }) => context.count - 1 }), 54 | }, 55 | }, 56 | }, 57 | success: { 58 | type: 'final', 59 | }, 60 | }, 61 | }); 62 | 63 | const counterActor = createActor(counterMachine).start(); 64 | 65 | const decision = await expert.decide({ 66 | machine: counterMachine, 67 | model: new MockLanguageModelV1({ 68 | defaultObjectGenerationMode: 'tool', 69 | doGenerate: async () => { 70 | return { 71 | ...dummyResponseValues, 72 | text: JSON.stringify({ 73 | type: 'object', 74 | properties: { 75 | count: { 76 | type: 'number', 77 | const: 3, 78 | }, 79 | }, 80 | required: ['count'], 81 | }), 82 | }; 83 | }, 84 | }), 85 | goal: 'Get the counter to exactly 3', 86 | state: counterActor.getSnapshot(), 87 | policy: experimental_shortestPathPolicy, 88 | }); 89 | 90 | expect(decision?.nextEvent?.type).toBe('increment'); 91 | }); 92 | -------------------------------------------------------------------------------- /src/policies/shortestPathPolicy.ts: -------------------------------------------------------------------------------- 1 | import { generateObject } from 'ai'; 2 | import { 3 | ExpertDecision, 4 | ExpertDecideInput, 5 | ExpertStep, 6 | AnyExpert, 7 | CostFunction, 8 | ObservedState, 9 | } from '../types'; 10 | import { getShortestPaths } from '@xstate/graph'; 11 | import { z } from 'zod'; 12 | import { zodToJsonSchema } from 'zod-to-json-schema'; 13 | import Ajv from 'ajv'; 14 | import { AnyMachineSnapshot } from 'xstate'; 15 | import { randomId } from '../utils'; 16 | 17 | const ajv = new Ajv(); 18 | 19 | function observedStatesEqual( 20 | state1: ObservedState, 21 | state2: ObservedState 22 | ) { 23 | // check state value && state context 24 | return ( 25 | JSON.stringify(state1.value) === JSON.stringify(state2.value) && 26 | JSON.stringify(state1.context) === JSON.stringify(state2.context) 27 | ); 28 | } 29 | 30 | function trimSteps(steps: ExpertStep[], currentState: ObservedState) { 31 | const index = steps.findIndex( 32 | (step) => step.state && observedStatesEqual(step.state, currentState) 33 | ); 34 | 35 | if (index === -1) { 36 | return undefined; 37 | } 38 | 39 | return steps.slice(index + 1, steps.length); 40 | } 41 | 42 | export async function experimental_shortestPathPolicy( 43 | expert: T, 44 | input: ExpertDecideInput 45 | ): Promise | undefined> { 46 | const costFunction: CostFunction = 47 | input.costFunction ?? ((path) => path.weight ?? Infinity); 48 | const existingDecision = input.decisions?.find( 49 | (p) => p.policy === 'shortestPath' && p.goal === input.goal 50 | ); 51 | 52 | let paths = existingDecision?.paths; 53 | 54 | if (existingDecision) { 55 | console.log('Existing decision found'); 56 | } 57 | 58 | if (!input.machine && !existingDecision) { 59 | return; 60 | } 61 | 62 | if (input.machine && !existingDecision) { 63 | const contextSchema = zodToJsonSchema(z.object(expert.context)); 64 | const result = await generateObject({ 65 | model: expert.model, 66 | system: input.system ?? expert.description, 67 | prompt: ` 68 | 69 | ${input.goal} 70 | 71 | 72 | ${contextSchema} 73 | 74 | 75 | 76 | Update the context JSON schema so that it validates the context to determine that it reaches the goal. Return the result as a diff. 77 | 78 | The contextSchema properties must not change. Do not add or remove properties, or modify the name of the properties. 79 | Use "const" for exact required values and define ranges/types for flexible conditions. 80 | 81 | Examples: 82 | 1. For "user is logged in with admin role": 83 | { 84 | "contextSchema": "{"type": "object", "properties": {"role": {"const": "admin"}, "lastLogin": {"type": "string"}}, "required": ["role"]}" 85 | } 86 | 87 | 2. For "score is above 100": 88 | { 89 | "contextSchema": "{"type": "object", "properties": {"score": {"type": "number", "minimum": 100}}, "required": ["score"]}" 90 | } 91 | 92 | 3. For "fruits contain apple, orange, banana": 93 | { 94 | "type": "array", 95 | "allOf": [ 96 | { "contains": { "const": "apple" } }, 97 | { "contains": { "const": "orange" } }, 98 | { "contains": { "const": "banana" } } 99 | ] 100 | } 101 | `.trim(), 102 | schema: z.object({ 103 | // valueSchema: z 104 | // .string() 105 | // .describe('The JSON Schema representing the goal state value'), 106 | contextSchema: z 107 | .object({ 108 | type: z.literal('object'), 109 | properties: z.object( 110 | Object.keys((contextSchema as any).properties).reduce( 111 | (acc, key) => { 112 | acc[key] = z.any(); 113 | return acc; 114 | }, 115 | {} as any 116 | ) 117 | ), 118 | required: z.array(z.string()).optional(), 119 | }) 120 | .describe('The JSON Schema representing the goal state context'), 121 | }), 122 | }); 123 | 124 | console.log(result.object); 125 | const validateContext = ajv.compile(result.object.contextSchema); 126 | 127 | const stateFilter = (state: AnyMachineSnapshot) => { 128 | return validateContext(state.context); 129 | }; 130 | 131 | const resolvedState = input.machine.resolveState({ 132 | ...input.state, 133 | context: input.state.context ?? {}, 134 | }); 135 | 136 | paths = getShortestPaths(input.machine, { 137 | fromState: resolvedState, 138 | toState: stateFilter, 139 | }); 140 | } 141 | 142 | if (!paths) { 143 | return undefined; 144 | } 145 | 146 | const trimmedPaths = paths 147 | .map((path) => { 148 | const trimmedSteps = trimSteps(path.steps, input.state); 149 | if (!trimmedSteps) { 150 | return undefined; 151 | } 152 | return { 153 | ...path, 154 | steps: trimmedSteps, 155 | }; 156 | }) 157 | .filter((p): p is NonNullable => p !== undefined); 158 | 159 | // Sort paths from least weight to most weight 160 | const sortedPaths = trimmedPaths.sort( 161 | (a, b) => costFunction(a) - costFunction(b) 162 | ); 163 | 164 | const leastWeightPath = sortedPaths[0]; 165 | const nextStep = leastWeightPath?.steps[0]; 166 | 167 | return { 168 | id: randomId(), 169 | decisionId: input.decisionId ?? null, 170 | policy: 'shortestPath', 171 | episodeId: expert.episodeId, 172 | goal: input.goal, 173 | goalState: paths[0]?.state ?? null, 174 | nextEvent: nextStep?.event ?? null, 175 | paths, 176 | timestamp: Date.now(), 177 | }; 178 | } 179 | -------------------------------------------------------------------------------- /src/policies/toolPolicy.ts: -------------------------------------------------------------------------------- 1 | import { CoreToolResult, generateText } from 'ai'; 2 | import { 3 | ExpertDecision, 4 | ExpertDecideInput, 5 | PromptTemplate, 6 | AnyExpert, 7 | } from '../types'; 8 | import { convertToXml, randomId } from '../utils'; 9 | import { transition } from 'xstate'; 10 | import { combinePromptAndMessages } from '../text'; 11 | import { getToolMap } from '../decide'; 12 | 13 | const toolPolicyPromptTemplate: PromptTemplate = (data) => { 14 | return ` 15 | ${convertToXml(data)} 16 | 17 | 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. 18 | `.trim(); 19 | }; 20 | 21 | export async function toolPolicy( 22 | expert: TExpert, 23 | input: ExpertDecideInput 24 | ): Promise | undefined> { 25 | const toolMap = getToolMap(expert, input); 26 | 27 | if (!toolMap) { 28 | // No valid transitions for the specified tools 29 | return undefined; 30 | } 31 | 32 | // Create a prompt with the given context and goal. 33 | // The template is used to ensure that a single tool call at most is made. 34 | const prompt = toolPolicyPromptTemplate({ 35 | stateValue: input.state.value, 36 | context: input.context ?? input.state.context, 37 | goal: input.goal, 38 | }); 39 | 40 | const messages = combinePromptAndMessages(prompt, input.messages); 41 | 42 | const model = input.model ? expert.wrap(input.model) : expert.model; 43 | 44 | const { state, machine, events, goal, model: _, ...rest } = input; 45 | 46 | const machineState = 47 | input.machine && input.state 48 | ? input.machine.resolveState({ 49 | ...input.state, 50 | context: input.state.context ?? {}, 51 | }) 52 | : undefined; 53 | 54 | const result = await generateText({ 55 | ...rest, 56 | system: input.system ?? expert.description, 57 | model, 58 | messages, 59 | tools: toolMap, 60 | toolChoice: input.toolChoice ?? 'required', 61 | }); 62 | 63 | result.response.messages.forEach((m) => { 64 | expert.addMessage(m); 65 | }); 66 | 67 | const singleResult = result.toolResults[0] as unknown as CoreToolResult< 68 | any, 69 | any, 70 | any 71 | >; 72 | 73 | if (!singleResult) { 74 | // TODO: retries? 75 | console.warn('No tool call results returned'); 76 | return undefined; 77 | } 78 | 79 | const nextEvent = singleResult.result; 80 | 81 | return { 82 | id: randomId(), 83 | decisionId: input.decisionId ?? null, 84 | policy: 'simple', 85 | goal: input.goal, 86 | goalState: input.state, 87 | nextEvent, 88 | episodeId: input.episodeId ?? expert.episodeId, 89 | timestamp: Date.now(), 90 | paths: [ 91 | { 92 | state: null, 93 | steps: [ 94 | { 95 | event: nextEvent, 96 | state: 97 | machine && machineState 98 | ? transition(machine, machineState, nextEvent)[0] 99 | : null, 100 | }, 101 | ], 102 | }, 103 | ], 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /src/schemas.ts: -------------------------------------------------------------------------------- 1 | import { ZodType, type SomeZodObject } from 'zod'; 2 | 3 | export type ZodEventMapping = { 4 | // map event types to Zod types 5 | [eventType: string]: SomeZodObject; 6 | }; 7 | 8 | export type ZodContextMapping = { 9 | // map context keys to Zod types 10 | [contextKey: string]: ZodType; 11 | }; 12 | -------------------------------------------------------------------------------- /src/templates/defaultText.ts: -------------------------------------------------------------------------------- 1 | import { PromptTemplate } from '../types'; 2 | import { wrapInXml } from '../utils'; 3 | 4 | export const defaultTextTemplate: PromptTemplate = (data) => { 5 | const preamble = [ 6 | data.stateValue 7 | ? wrapInXml('stateValue', JSON.stringify(data.stateValue)) 8 | : undefined, 9 | data.context 10 | ? wrapInXml('context', JSON.stringify(data.context)) 11 | : undefined, 12 | ] 13 | .filter(Boolean) 14 | .join('\n'); 15 | 16 | return ` 17 | ${preamble} 18 | 19 | ${data.goal} 20 | `.trim(); 21 | }; 22 | -------------------------------------------------------------------------------- /src/text.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generateText, 3 | streamText, 4 | type CoreMessage, 5 | type CoreTool, 6 | type GenerateTextResult, 7 | } from 'ai'; 8 | import { 9 | ExpertGenerateTextOptions, 10 | ExpertStreamTextOptions, 11 | AnyExpert, 12 | } from './types'; 13 | import { defaultTextTemplate } from './templates/defaultText'; 14 | import { 15 | ObservableActorLogic, 16 | Observer, 17 | PromiseActorLogic, 18 | fromObservable, 19 | fromPromise, 20 | toObserver, 21 | } from 'xstate'; 22 | 23 | /** 24 | * Gets an array of messages from the given prompt and messages 25 | */ 26 | export function combinePromptAndMessages( 27 | prompt: string, 28 | messages?: CoreMessage[] 29 | ): CoreMessage[] { 30 | return (messages ?? []).concat({ 31 | role: 'user', 32 | content: prompt, 33 | }); 34 | } 35 | 36 | export function fromTextStream( 37 | expert: TExpert, 38 | options?: ExpertStreamTextOptions 39 | ): ObservableActorLogic< 40 | { textDelta: string }, 41 | Omit & { 42 | context?: Record; 43 | } 44 | > { 45 | const template = options?.template ?? defaultTextTemplate; 46 | return fromObservable(({ input }) => { 47 | const observers = new Set>(); 48 | 49 | // TODO: check if messages was provided instead 50 | 51 | (async () => { 52 | const model = input.model ? expert.wrap(input.model) : expert.model; 53 | const goal = 54 | typeof input.prompt === 'string' 55 | ? input.prompt 56 | : await input.prompt(expert); 57 | const promptWithContext = template({ 58 | goal, 59 | context: input.context, 60 | }); 61 | const messages = combinePromptAndMessages( 62 | promptWithContext, 63 | input.messages 64 | ); 65 | const result = await streamText({ 66 | ...options, 67 | ...input, 68 | prompt: undefined, // overwritten by messages 69 | model, 70 | messages, 71 | }); 72 | 73 | for await (const part of result.fullStream) { 74 | if (part.type === 'text-delta') { 75 | observers.forEach((observer) => { 76 | observer.next?.(part); 77 | }); 78 | } 79 | } 80 | })(); 81 | 82 | return { 83 | subscribe: (...args: any[]) => { 84 | const observer = toObserver(...args); 85 | observers.add(observer); 86 | 87 | return { 88 | unsubscribe: () => { 89 | observers.delete(observer); 90 | }, 91 | }; 92 | }, 93 | }; 94 | }); 95 | } 96 | 97 | export function fromText( 98 | expert: TExpert, 99 | options?: ExpertGenerateTextOptions 100 | ): PromiseActorLogic< 101 | GenerateTextResult>, any>, 102 | Omit & { 103 | context?: Record; 104 | } 105 | > { 106 | const resolvedOptions = { 107 | ...options, 108 | }; 109 | 110 | const template = resolvedOptions.template ?? defaultTextTemplate; 111 | 112 | return fromPromise(async ({ input }) => { 113 | const goal = 114 | typeof input.prompt === 'string' 115 | ? input.prompt 116 | : await input.prompt(expert); 117 | 118 | const promptWithContext = template({ 119 | goal, 120 | context: input.context, 121 | }); 122 | 123 | const messages = combinePromptAndMessages( 124 | promptWithContext, 125 | input.messages 126 | ); 127 | 128 | const model = input.model ? expert.wrap(input.model) : expert.model; 129 | 130 | return await generateText({ 131 | ...input, 132 | ...options, 133 | prompt: undefined, 134 | messages, 135 | model, 136 | }); 137 | }); 138 | } 139 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActorLogic, 3 | ActorRefLike, 4 | AnyEventObject, 5 | AnyStateMachine, 6 | EventFrom, 7 | SnapshotFrom, 8 | StateValue, 9 | TransitionSnapshot, 10 | Values, 11 | } from 'xstate'; 12 | import { 13 | CoreMessage, 14 | generateText, 15 | GenerateTextResult, 16 | LanguageModel, 17 | streamText, 18 | } from 'ai'; 19 | import { ZodContextMapping, ZodEventMapping } from './schemas'; 20 | import { TypeOf } from 'zod'; 21 | import { Expert } from './expert'; 22 | 23 | export type GenerateTextOptions = Parameters[0]; 24 | 25 | export type StreamTextOptions = Parameters[0]; 26 | 27 | export type CostFunction = ( 28 | path: ExpertPath 29 | ) => number; 30 | 31 | export type ExpertDecideInput = Omit< 32 | ExpertGenerateTextOptions, 33 | 'model' | 'prompt' | 'tools' | 'toolChoice' 34 | > & { 35 | /** 36 | * The parent decision that this decision is a part of. 37 | */ 38 | decisionId?: string; 39 | /** 40 | * The currently observed state. 41 | */ 42 | state: ObservedState; 43 | /** 44 | * The context to provide in the prompt to the expert. This overrides the `state.context`. 45 | */ 46 | context?: Record; 47 | /** 48 | * The goal for the expert to accomplish. 49 | * The expert will make a decision based on this goal. 50 | */ 51 | goal: string; 52 | /** 53 | * The events that the expert can trigger. This is a mapping of 54 | * event types to Zod event schemas. 55 | */ 56 | events?: ZodEventMapping; 57 | allowedEvents?: Array['type']>; 58 | /** 59 | * The state machine that represents the environment the expert 60 | * is interacting with. 61 | */ 62 | machine?: AnyStateMachine; 63 | 64 | /** 65 | * A function that calculates the total cost of the path to the goal state. 66 | */ 67 | costFunction?: CostFunction; 68 | 69 | /** 70 | * The maximum number of attempts to make a decision. 71 | * Defaults to 2. 72 | */ 73 | maxAttempts?: number; 74 | /** 75 | * The policy to use for making a decision. 76 | */ 77 | policy?: ExpertPolicy; 78 | model?: LanguageModel; 79 | /** 80 | * The previous relevant feedback from the expert. 81 | */ 82 | feedback?: ExpertFeedback[]; 83 | /** 84 | * The previous relevant observations from the expert. 85 | */ 86 | observations?: ExpertObservation[]; 87 | /** 88 | * The previous relevant decisions from the expert. 89 | */ 90 | decisions?: ExpertDecision[]; 91 | /** 92 | * The previous relevant insights from the expert. 93 | */ 94 | insights?: ExpertInsight[]; 95 | toolChoice?: 'auto' | 'none' | 'required'; 96 | } & BaseInput; 97 | 98 | export type ExpertStep = { 99 | /** The event to take */ 100 | event: EventFromExpert; 101 | /** The next expected state after taking the event */ 102 | state: ObservedState | null; 103 | }; 104 | 105 | export type ExpertPath = { 106 | /** The expected ending state of the path */ 107 | state: ObservedState | null; 108 | /** The steps to reach the ending state */ 109 | steps: Array>; 110 | weight?: number; 111 | }; 112 | 113 | export interface ExpertDecisionInput 114 | extends BaseInput { 115 | goal: string; 116 | decisionId?: string | null; 117 | policy?: string | null; 118 | goalState?: ObservedState | null; 119 | nextEvent?: EventFromExpert | null; 120 | paths?: ExpertPath[]; 121 | } 122 | 123 | export interface ExpertDecision 124 | extends BaseProperties { 125 | /** 126 | * The parent decision that this decision is a part of. 127 | */ 128 | decisionId: string | null; 129 | /** 130 | * The policy used to generate the decision 131 | */ 132 | policy: string | null; 133 | goal: string; 134 | /** 135 | * The ending state of the decision. 136 | */ 137 | goalState: ObservedState | null; 138 | /** 139 | * The next event that the expert decided needs to occur to achieve the `goal`. 140 | * 141 | * This next event is chosen from the 142 | */ 143 | nextEvent: EventFromExpert | null; 144 | /** 145 | * The paths that the expert can take to achieve the goal. 146 | */ 147 | paths: ExpertPath[]; 148 | } 149 | 150 | export interface TransitionData { 151 | eventType: string; 152 | description?: string; 153 | guard?: { type: string }; 154 | target?: any; 155 | } 156 | 157 | export type PromptTemplate = (data: { 158 | goal: string; 159 | /** 160 | * The observed state 161 | */ 162 | stateValue?: any; 163 | context?: Record; 164 | /** 165 | * The state machine model of the observed environment 166 | */ 167 | machine?: unknown; 168 | /** 169 | * The potential next transitions that can be taken 170 | * in the state machine 171 | */ 172 | transitions?: TransitionData[]; 173 | /** 174 | * Relevant past observations 175 | */ 176 | observations?: ExpertObservation[]; // TODO 177 | /** 178 | * Relevant feedback 179 | */ 180 | feedback?: ExpertFeedback[]; 181 | /** 182 | * Relevant messages 183 | */ 184 | messages?: ExpertMessage[]; 185 | /** 186 | * Relevant past decisions 187 | */ 188 | decisions?: ExpertDecision[]; 189 | /** 190 | * Relevant past insights 191 | */ 192 | insights?: ExpertInsight[]; 193 | }) => string; 194 | 195 | export type ExpertPolicy = ( 196 | expert: TExpert, 197 | input: ExpertDecideInput 198 | ) => Promise | undefined>; 199 | 200 | export type ExpertInteractInput = Omit< 201 | ExpertDecideInput, 202 | 'state' 203 | > & { 204 | state?: never; 205 | }; 206 | 207 | export interface ExpertFeedback extends BaseProperties { 208 | decisionId: string; 209 | reward: number; 210 | comment: string | undefined; 211 | attributes: Record; 212 | } 213 | 214 | interface BaseProperties { 215 | id: string; 216 | episodeId: string; 217 | timestamp: number; 218 | } 219 | 220 | type BaseInput = Partial; 221 | 222 | export interface ExpertFeedbackInput extends BaseInput { 223 | /** 224 | * The decision ID that this feedback is relevant for. 225 | */ 226 | decisionId: string; 227 | reward: number; 228 | comment?: string; 229 | attributes?: Record; 230 | } 231 | 232 | export type ExpertMessage = BaseProperties & 233 | CoreMessage & { 234 | /** 235 | * The parent decision that this message is a part of. 236 | */ 237 | decisionId?: string; 238 | /** 239 | * The response ID of the message, which references 240 | * which message this message is responding to, if any. 241 | */ 242 | responseId?: string; 243 | result?: GenerateTextResult; 244 | }; 245 | 246 | type JSONObject = { 247 | [key: string]: JSONValue; 248 | }; 249 | type JSONArray = JSONValue[]; 250 | type JSONValue = null | string | number | boolean | JSONObject | JSONArray; 251 | 252 | type LanguageModelV1ProviderMetadata = Record< 253 | string, 254 | Record 255 | >; 256 | 257 | export interface LanguageModelV1TextPart { 258 | type: 'text'; 259 | /** 260 | The text content. 261 | */ 262 | text: string; 263 | /** 264 | * Additional provider-specific metadata. They are passed through 265 | * to the provider from the AI SDK and enable provider-specific 266 | * functionality that can be fully encapsulated in the provider. 267 | */ 268 | providerMetadata?: LanguageModelV1ProviderMetadata; 269 | } 270 | 271 | export interface LanguageModelV1ToolCallPart { 272 | type: 'tool-call'; 273 | /** 274 | ID of the tool call. This ID is used to match the tool call with the tool result. 275 | */ 276 | toolCallId: string; 277 | /** 278 | Name of the tool that is being called. 279 | */ 280 | toolName: string; 281 | /** 282 | Arguments of the tool call. This is a JSON-serializable object that matches the tool's input schema. 283 | */ 284 | args: unknown; 285 | /** 286 | * Additional provider-specific metadata. They are passed through 287 | * to the provider from the AI SDK and enable provider-specific 288 | * functionality that can be fully encapsulated in the provider. 289 | */ 290 | providerMetadata?: LanguageModelV1ProviderMetadata; 291 | } 292 | 293 | export type ExpertMessageInput = CoreMessage & { 294 | timestamp?: number; 295 | id?: string; 296 | /** 297 | * The response ID of the message, which references 298 | * which message this message is responding to, if any. 299 | */ 300 | responseId?: string; 301 | result?: GenerateTextResult; 302 | }; 303 | 304 | export interface ExpertObservation { 305 | id: string; 306 | episodeId: string; 307 | /** 308 | * The decision that this observation is relevant for 309 | */ 310 | decisionId?: string | undefined; 311 | goal?: string; 312 | prevState: SnapshotFrom | undefined; 313 | event: EventFrom | undefined; 314 | state: SnapshotFrom; 315 | // machineHash: string | undefined; 316 | timestamp: number; 317 | } 318 | 319 | export interface ExpertObservationInput 320 | extends BaseInput { 321 | state: ObservedState; 322 | /** 323 | * The expert decision that the observation is relevant for 324 | */ 325 | decisionId?: string | undefined; 326 | prevState?: ObservedState; 327 | event?: AnyEventObject; 328 | goal?: string | undefined; 329 | } 330 | 331 | export type ExpertEmittedEvent = 332 | | { 333 | type: 'feedback'; 334 | feedback: ExpertFeedback; 335 | } 336 | | { 337 | type: 'observation'; 338 | observation: ExpertObservation; // TODO 339 | } 340 | | { 341 | type: 'message'; 342 | message: ExpertMessage; 343 | } 344 | | { 345 | type: 'decision'; 346 | decision: ExpertDecision; 347 | } 348 | | { 349 | type: 'insight'; 350 | insight: ExpertInsight; 351 | }; 352 | 353 | export type ExpertLogic = ActorLogic< 354 | TransitionSnapshot>, 355 | | { 356 | type: 'expert.feedback'; 357 | feedback: ExpertFeedback; 358 | } 359 | | { 360 | type: 'expert.observe'; 361 | observation: ExpertObservation; // TODO 362 | } 363 | | { 364 | type: 'expert.message'; 365 | message: ExpertMessage; 366 | } 367 | | { 368 | type: 'expert.decision'; 369 | decision: ExpertDecision; 370 | } 371 | | { 372 | type: 'expert.insight'; 373 | insight: ExpertInsight; 374 | }, 375 | any, // TODO: input 376 | any, 377 | ExpertEmittedEvent 378 | >; 379 | 380 | export type EventsFromZodEventMapping = 381 | Compute< 382 | Values<{ 383 | [K in keyof TEventSchemas & string]: { 384 | type: K; 385 | } & TypeOf; 386 | }> 387 | >; 388 | 389 | export type ContextFromZodContextMapping< 390 | TContextSchema extends ZodContextMapping 391 | > = { 392 | [K in keyof TContextSchema & string]: TypeOf; 393 | }; 394 | 395 | export type AnyExpert = Expert; 396 | 397 | export type FromExpert = T | ((expert: AnyExpert) => T | Promise); 398 | 399 | export type CommonTextOptions = { 400 | prompt: FromExpert; 401 | model?: LanguageModel; 402 | messages?: CoreMessage[]; 403 | template?: PromptTemplate; 404 | context?: Record; 405 | }; 406 | 407 | export type ExpertGenerateTextOptions = Omit< 408 | GenerateTextOptions, 409 | 'model' | 'prompt' | 'messages' 410 | > & 411 | CommonTextOptions; 412 | 413 | export type ExpertStreamTextOptions = Omit< 414 | StreamTextOptions, 415 | 'model' | 'prompt' | 'messages' 416 | > & 417 | CommonTextOptions; 418 | 419 | export interface ObservedState { 420 | /** 421 | * The current state value of the state machine, e.g. 422 | * `"loading"` or `"processing"` or `"ready"` 423 | */ 424 | value: StateValue; 425 | /** 426 | * Additional contextual data related to the current state 427 | */ 428 | context?: ContextFromExpert; 429 | } 430 | 431 | export type ObservedStateFrom = Pick< 432 | SnapshotFrom, 433 | 'value' | 'context' 434 | >; 435 | 436 | export type ExpertMemoryContext = { 437 | observations: ExpertObservation[]; // TODO 438 | messages: ExpertMessage[]; 439 | decisions: ExpertDecision[]; 440 | feedback: ExpertFeedback[]; 441 | insights: ExpertInsight[]; 442 | }; 443 | 444 | export type Compute = { [K in keyof A]: A[K] } & unknown; 445 | 446 | export type MaybePromise = T | Promise; 447 | 448 | export type EventFromExpert = T extends Expert< 449 | infer _, 450 | infer TEventSchemas 451 | > 452 | ? EventsFromZodEventMapping 453 | : never; 454 | 455 | export type TypesFromExpert = T extends Expert< 456 | infer TContextSchema, 457 | infer TEventSchema 458 | > 459 | ? { 460 | context: ContextFromZodContextMapping; 461 | events: EventsFromZodEventMapping; 462 | } 463 | : never; 464 | 465 | export type ContextFromExpert = T extends Expert< 466 | infer TContextSchema, 467 | infer _TEventSchema 468 | > 469 | ? ContextFromZodContextMapping 470 | : never; 471 | 472 | export interface StorageAdapter { 473 | addObservation( 474 | observationInput: ExpertObservationInput 475 | ): Promise>; 476 | getObservations(queryObject?: TQuery): Promise[]>; 477 | addFeedback(feedbackInput: ExpertFeedbackInput): Promise; 478 | getFeedback(queryObject?: TQuery): Promise; 479 | addMessage(messageInput: ExpertMessageInput): Promise; 480 | getMessages(queryObject?: TQuery): Promise; 481 | addDecision( 482 | decisionInput: ExpertDecideInput 483 | ): Promise>; 484 | getDecisions(queryObject?: TQuery): Promise[]>; 485 | } 486 | 487 | export type StorageAdapterQuery> = 488 | T extends StorageAdapter ? TQuery : never; 489 | 490 | export interface ExpertInsightInput extends BaseInput { 491 | observationId: string; 492 | attributes: Record; 493 | } 494 | 495 | export interface ExpertInsight extends BaseProperties { 496 | observationId: string; 497 | attributes: Record; 498 | } 499 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActorRefLike, 3 | AnyActorRef, 4 | AnyMachineSnapshot, 5 | AnyStateMachine, 6 | AnyStateNode, 7 | } from 'xstate'; 8 | import hash from 'object-hash'; 9 | import { ObservedState, TransitionData } from './types'; 10 | 11 | export function getAllTransitions(state: AnyMachineSnapshot): TransitionData[] { 12 | const nodes = state._nodes; 13 | const transitions = (nodes as AnyStateNode[]) 14 | .map((node) => [...(node as AnyStateNode).transitions.values()]) 15 | .map((nodeTransitions) => { 16 | return nodeTransitions.map((nodeEventTransitions) => { 17 | return nodeEventTransitions.map((transition) => { 18 | return { 19 | ...transition, 20 | guard: 21 | typeof transition.guard === 'string' 22 | ? { type: transition.guard } 23 | : (transition.guard as any), // TODO: fix 24 | }; 25 | }); 26 | }); 27 | }) 28 | .flat(2); 29 | 30 | return transitions; 31 | } 32 | 33 | export function getAllMachineTransitions( 34 | stateNode: AnyStateNode 35 | ): TransitionData[] { 36 | const transitions: TransitionData[] = [...stateNode.transitions.values()] 37 | .map((nodeTransitions) => { 38 | return nodeTransitions.map((transition) => { 39 | return { 40 | ...transition, 41 | guard: 42 | typeof transition.guard === 'string' 43 | ? { type: transition.guard } 44 | : (transition.guard as any), // TODO: fix 45 | }; 46 | }); 47 | }) 48 | .flat(2); 49 | 50 | for (const s of Object.values(stateNode.states)) { 51 | const stateTransitions = getAllMachineTransitions(s); 52 | transitions.push(...stateTransitions); 53 | } 54 | 55 | return transitions; 56 | } 57 | 58 | export function wrapInXml(tagName: string, content: string): string { 59 | return `<${tagName}>${content}`; 60 | } 61 | 62 | export function convertToXml(obj: Record): string { 63 | return Object.entries(obj) 64 | .map(([key, value]) => { 65 | if (typeof value === 'object' && value !== null) { 66 | return wrapInXml(key, convertToXml(value)); 67 | } else { 68 | return wrapInXml(key, value); 69 | } 70 | }) 71 | .join(''); 72 | } 73 | 74 | export function randomId(prefix?: string): string { 75 | const timestamp = Date.now().toString(36); 76 | const random = Math.random().toString(36).substring(2, 9); 77 | // return timestamp + random; 78 | return `${prefix || ''}${timestamp}${random}`; 79 | } 80 | 81 | const machineHashes: WeakMap = new WeakMap(); 82 | /** 83 | * Returns a string hash representing only the transitions in the state machine. 84 | */ 85 | export function getMachineHash(machine: AnyStateMachine): string { 86 | if (machineHashes.has(machine)) return machineHashes.get(machine)!; 87 | const transitions = getAllMachineTransitions(machine.root); 88 | const machineHash = hash(transitions); 89 | machineHashes.set(machine, machineHash); 90 | return machineHash; 91 | } 92 | 93 | export function isActorRef( 94 | actorRefLike: ActorRefLike 95 | ): actorRefLike is AnyActorRef { 96 | return ( 97 | 'src' in actorRefLike && 98 | 'system' in actorRefLike && 99 | 'sessionId' in actorRefLike 100 | ); 101 | } 102 | 103 | export function getTransitions( 104 | state: ObservedState, 105 | machine: AnyStateMachine 106 | ): TransitionData[] { 107 | if (!machine) { 108 | return []; 109 | } 110 | 111 | const resolvedState = machine.resolveState({ 112 | ...state, 113 | // Need this property defined to make TS happy 114 | context: state.context, 115 | }); 116 | return getAllTransitions(resolvedState); 117 | } 118 | 119 | export function isMachineActor( 120 | actor: ActorRefLike 121 | ): actor is typeof actor & { src: AnyStateMachine } { 122 | return ( 123 | 'src' in actor && 124 | typeof actor.src === 'object' && 125 | actor.src !== null && 126 | 'definition' in actor.src 127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | "sourceMap": true /* Create source map files for emitted JavaScript files. */, 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "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. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | "noEmit": true /* Disable emitting files from a compilation. */, 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "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. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 83 | 84 | /* Type Checking */ 85 | "strict": true /* Enable all strict type-checking options. */, 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | "noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */, 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import dotenv from 'dotenv'; 3 | dotenv.config(); 4 | 5 | export default defineConfig({ 6 | test: { 7 | testTimeout: 10000, // Global timeout of 10000ms for all tests 8 | coverage: { 9 | provider: 'v8', 10 | reporter: ['text', 'json', 'html'], 11 | exclude: ['**.test.ts'], 12 | include: ['src'], 13 | }, 14 | }, 15 | }); 16 | --------------------------------------------------------------------------------