├── .changeset └── config.json ├── .github └── workflows │ ├── preview.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── CLAUDE.md ├── LICENSE ├── README.md ├── demo ├── browser │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── llamaindex.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── cloudflare │ ├── .gitignore │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ ├── worker-configuration.d.ts │ └── wrangler.jsonc ├── deno │ ├── deno.json │ ├── deno.lock │ ├── main.ts │ └── main_test.ts ├── hono │ └── app.ts ├── next │ └── next-env.d.ts ├── node │ ├── basic.ts │ ├── file-parse-promise.ts │ ├── file-parse-rxjs.ts │ ├── llama-parse-workflow.ts │ ├── mcp-file-parse-tool.ts │ ├── name-ask-readline.ts │ └── tool-call-agent.ts ├── package.json ├── tsconfig.json ├── waku │ ├── .gitignore │ ├── openapi-ts.config.ts │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── images │ │ │ └── favicon.png │ ├── src │ │ ├── components │ │ │ ├── RAG.tsx │ │ │ ├── footer.tsx │ │ │ └── header.tsx │ │ ├── pages │ │ │ ├── _layout.tsx │ │ │ ├── about.tsx │ │ │ ├── api │ │ │ │ └── store.ts │ │ │ └── index.tsx │ │ ├── schema │ │ │ └── index.ts │ │ ├── styles.css │ │ └── workflow │ │ │ ├── basic.ts │ │ │ ├── events.ts │ │ │ └── llama-parse.ts │ └── tsconfig.json └── workflows │ ├── file-parse-agent.ts │ ├── human-in-the-loop.ts │ ├── llama-parse-workflow.ts │ └── tool-call-agent.ts ├── docs ├── package.json └── workflows │ ├── advanced-events.mdx │ ├── basic-workflow.mdx │ ├── index.mdx │ ├── llamaindex-integration.mdx │ ├── meta.json │ └── streaming.mdx ├── lint-staged.config.ts ├── package.json ├── packages ├── core │ ├── CHANGELOG.md │ ├── LICENSE │ ├── jsr.json │ ├── package.json │ ├── src │ │ ├── async-context │ │ │ ├── index.browser.ts │ │ │ └── index.ts │ │ ├── core │ │ │ ├── context.ts │ │ │ ├── event.ts │ │ │ ├── index.ts │ │ │ ├── stream.ts │ │ │ ├── utils.ts │ │ │ └── workflow.ts │ │ ├── hono.ts │ │ ├── mcp.ts │ │ ├── middleware │ │ │ ├── snapshot.ts │ │ │ ├── snapshot │ │ │ │ └── stable-hash.ts │ │ │ ├── state.ts │ │ │ ├── store.ts │ │ │ ├── trace-events.ts │ │ │ ├── trace-events │ │ │ │ ├── create-handler-decorator.ts │ │ │ │ └── run-once.ts │ │ │ └── validation.ts │ │ ├── next.ts │ │ ├── observable.ts │ │ ├── stream │ │ │ ├── consumer.ts │ │ │ ├── filter.ts │ │ │ ├── find.ts │ │ │ ├── run.ts │ │ │ └── until.ts │ │ └── util │ │ │ ├── p-retry.ts │ │ │ └── zod.ts │ ├── tests │ │ ├── core │ │ │ ├── abort-signal.spec.ts │ │ │ ├── context-api.spec.ts │ │ │ ├── event.spec.ts │ │ │ ├── listener.spec.ts │ │ │ ├── readable-stream.spec.ts │ │ │ ├── run-helpers.spec.ts │ │ │ ├── shared │ │ │ │ └── events.ts │ │ │ ├── stream.spec.ts │ │ │ └── sub-workflow.spec.ts │ │ ├── integration │ │ │ └── stream-library.spec.ts │ │ ├── middleware │ │ │ ├── full-workflow.spec.ts │ │ │ ├── with-snapshot.spec.ts │ │ │ ├── with-state.spec.ts │ │ │ ├── with-trace-events.spec.ts │ │ │ └── with-validation.spec.ts │ │ └── util │ │ │ └── zod.spec.ts │ ├── tsconfig.browser.build.json │ ├── tsconfig.browser.json │ ├── tsconfig.browser.test.json │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── tsconfig.test.json │ ├── tsdown.config.ts │ └── vitest.config.ts └── http │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ ├── client.ts │ └── server.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tests └── cjs │ ├── CHANGELOG.md │ ├── package.json │ ├── tsconfig.json │ ├── vitest.config.js │ └── workflow-cjs.test.ts ├── tsconfig.json ├── tsconfig.lint-staged.json ├── turbo.json └── vitest.workspace.ts /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [], 11 | "bumpVersionsWithWorkspaceProtocolOnly": true 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/preview.yml: -------------------------------------------------------------------------------- 1 | name: Publish Preview 2 | on: [pull_request] 3 | env: 4 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 5 | VERCEL_PROJECT_ID: ${{ secrets.LLAMAINDEX_VERCEL_PROJECT_ID }} 6 | TURBO_TOKEN: ${{ secrets.VERCEL_TOKEN }} 7 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | pre_release: 15 | name: Pre Release 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout Repo 20 | uses: actions/checkout@v4 21 | 22 | - uses: pnpm/action-setup@v4 23 | 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 22 28 | cache: "pnpm" 29 | 30 | - name: Install dependencies 31 | run: pnpm install 32 | 33 | - name: Run build 34 | run: pnpx turbo run build --filter="./packages/*" 35 | 36 | - name: Pre Release 37 | run: pnpx pkg-pr-new publish --pnpm ./packages/* 38 | 39 | pre_release_doc: 40 | name: Pre Release Doc 41 | runs-on: ubuntu-latest 42 | outputs: 43 | deployment_url: ${{ steps.deploy.outputs.deployment_url }} 44 | 45 | steps: 46 | - name: Checkout Repo 47 | uses: actions/checkout@v4 48 | with: 49 | path: workflows-ts 50 | repository: "${{ github.repository }}" 51 | fetch-depth: "1" 52 | - name: Checkout LITS 53 | uses: actions/checkout@v4 54 | with: 55 | path: llamaindex 56 | repository: "run-llama/LlamaIndexTS" 57 | fetch-depth: "1" 58 | - uses: pnpm/action-setup@v4 59 | with: 60 | package_json_file: "workflows-ts/package.json" 61 | - uses: pnpm/action-setup@v4 62 | with: 63 | package_json_file: "llamaindex/package.json" 64 | - name: Setup Node.js for workflows-ts 65 | uses: actions/setup-node@v4 66 | with: 67 | node-version: 22 68 | cache: "pnpm" 69 | cache-dependency-path: "workflows-ts/pnpm-lock.yaml" 70 | - name: Setup Node.js for LlamaIndex 71 | uses: actions/setup-node@v4 72 | with: 73 | node-version: 22 74 | cache: "pnpm" 75 | cache-dependency-path: "llamaindex/pnpm-lock.yaml" 76 | - name: Install dependencies for workflows-ts 77 | run: pnpm install 78 | working-directory: workflows-ts 79 | - name: Install dependencies for llamaindex 80 | run: pnpm install 81 | working-directory: llamaindex 82 | - name: Link workflows-ts docs to LlamaIndex 83 | run: | 84 | pnpm link ${{github.workspace}}/workflows/docs 85 | working-directory: llamaindex/apps/next 86 | - name: Install Vercel CLI 87 | run: npm install --global vercel@latest 88 | - name: Pull Vercel Environment Information 89 | run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} 90 | working-directory: llamaindex 91 | - name: Build Project Artifacts 92 | run: vercel build --token=${{ secrets.VERCEL_TOKEN }} 93 | working-directory: llamaindex 94 | - name: Deploy Project Artifacts to Vercel 95 | id: deploy 96 | run: | 97 | vercel deploy --prebuilt --archive=tgz --token=${{ secrets.VERCEL_TOKEN }} > deploy.log 98 | URL=$(cat deploy.log | grep -o 'https://[^ ]*.llamaindex.ai' | head -n1) 99 | echo "deployment_url=$URL" >> $GITHUB_OUTPUT 100 | working-directory: llamaindex 101 | 102 | add-comment: 103 | name: Add Comment 104 | runs-on: ubuntu-latest 105 | needs: pre_release_doc 106 | permissions: 107 | issues: write 108 | pull-requests: write 109 | steps: 110 | - name: Comment URL to PR 111 | uses: actions/github-script@v6 112 | id: comment-deployment-url-script 113 | env: 114 | DEPLOYMENT_URL: ${{ needs.pre_release_doc.outputs.deployment_url }} 115 | with: 116 | script: | 117 | // Get pull requests that are open for current ref. 118 | const pullRequests = await github.rest.pulls.list({ 119 | owner: context.repo.owner, 120 | repo: context.repo.repo, 121 | state: 'open', 122 | head: `${context.repo.owner}:${context.ref.replace('refs/heads/', '')}` 123 | }) 124 | 125 | // Set issue number for following calls from context (if on pull request event) or from above variable. 126 | const issueNumber = context.issue.number || pullRequests.data[0].number 127 | 128 | // Retrieve existing bot comments for the PR 129 | const {data: comments} = await github.rest.issues.listComments({ 130 | owner: context.repo.owner, 131 | repo: context.repo.repo, 132 | issue_number: issueNumber, 133 | }) 134 | const botComment = comments.find(comment => { 135 | return comment.user.type === 'Bot' && comment.body.includes('Deployed at') 136 | }) 137 | 138 | const output = "Deployed at " + process.env.DEPLOYMENT_URL 139 | 140 | // If we have a comment, update it, otherwise create a new one 141 | if (botComment) { 142 | github.rest.issues.updateComment({ 143 | owner: context.repo.owner, 144 | repo: context.repo.repo, 145 | comment_id: botComment.id, 146 | body: output 147 | }) 148 | } else { 149 | github.rest.issues.createComment({ 150 | issue_number: issueNumber, 151 | owner: context.repo.owner, 152 | repo: context.repo.repo, 153 | body: output 154 | }) 155 | } 156 | -------------------------------------------------------------------------------- /.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 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v4 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 22 21 | cache: "pnpm" 22 | - name: Install dependencies 23 | run: pnpm install 24 | - name: Create Release Pull Request or Publish to npm 25 | id: changesets 26 | uses: changesets/action@v1 27 | with: 28 | publish: pnpm run publish 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | 33 | - name: Update jsr.json 34 | run: | 35 | jq --arg version "$(jq -r '.version' ./packages/core/package.json)" '.version = $version' ./packages/core/jsr.json > tmp.json && mv tmp.json ./packages/core/jsr.json 36 | pnpx prettier --write ./packages/core/jsr.json 37 | 38 | - name: Remove unsued changelog 39 | run: | 40 | rm -rf ./docs/CHANGELOG.md 41 | 42 | - name: Commit lock file 43 | continue-on-error: true 44 | uses: stefanzweifel/git-auto-commit-action@v5 45 | with: 46 | commit_message: "chore: update jsr.json" 47 | branch: changeset-release/main 48 | file_pattern: "**/jsr.json **/CHANGELOG.md" 49 | # Blocked by upstream issue: https://github.com/jsr-io/jsr/issues/543 50 | # 51 | # publish: 52 | # name: Publish to jsr 53 | # runs-on: ubuntu-latest 54 | # permissions: 55 | # contents: read 56 | # id-token: write 57 | # steps: 58 | # - uses: actions/checkout@v4 59 | # - uses: pnpm/action-setup@v4 60 | # - name: Setup Node.js 61 | # uses: actions/setup-node@v4 62 | # with: 63 | # node-version: 22 64 | # cache: "pnpm" 65 | # - name: Install dependencies 66 | # run: pnpm install 67 | # - run: pnpm dlx jsr publish --allow-slow-types 68 | # working-directory: ./packages/core 69 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | node-version: [20.x, 22.x, 24.x] 21 | name: Test on Node.js ${{ matrix.node-version }} 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: pnpm/action-setup@v4 26 | - name: Setup Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | cache: "pnpm" 31 | - name: Install dependencies 32 | run: pnpm install 33 | - name: Build packages 34 | run: pnpx turbo run build --filter="./packages/*" 35 | - name: Run tests 36 | run: pnpx turbo run test 37 | deno-test: 38 | name: Test on Deno 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: denoland/setup-deno@v2 43 | with: 44 | deno-version: v2.x 45 | - uses: pnpm/action-setup@v4 46 | - name: Setup Node.js 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: 22 50 | cache: "pnpm" 51 | - name: Install dependencies 52 | run: pnpm install 53 | - name: Run build 54 | run: pnpx turbo run build --filter="./packages/*" 55 | - name: Run Deno tests 56 | run: deno test 57 | working-directory: ./demo/deno 58 | lint: 59 | name: Lint 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v4 63 | - uses: pnpm/action-setup@v4 64 | - name: Setup Node.js 65 | uses: actions/setup-node@v4 66 | with: 67 | node-version: 22 68 | cache: "pnpm" 69 | - name: Install dependencies 70 | run: pnpm install 71 | - name: Run lint 72 | run: pnpm run lint 73 | type: 74 | name: Type Check 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v4 78 | - uses: pnpm/action-setup@v4 79 | - name: Setup Node.js 80 | uses: actions/setup-node@v4 81 | with: 82 | node-version: 22 83 | cache: "pnpm" 84 | - name: Install dependencies 85 | run: pnpm install 86 | - name: Run build 87 | run: pnpx turbo run build --filter="./packages/*" 88 | - name: Run type check 89 | run: pnpm run typecheck 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional stylelint cache 59 | .stylelintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variable files 77 | .env 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | .env.local 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | .parcel-cache 86 | 87 | # Next.js build output 88 | .next 89 | out 90 | 91 | # Nuxt.js build / generate output 92 | .nuxt 93 | dist 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | .cache 107 | 108 | # Docusaurus cache and generated files 109 | .docusaurus 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # TernJS port file 121 | .tern-port 122 | 123 | # Stores VSCode versions used for testing VSCode extensions 124 | .vscode-test 125 | 126 | # yarn v2 127 | .yarn/cache 128 | .yarn/unplugged 129 | .yarn/build-state.yml 130 | .yarn/install-state.gz 131 | .pnp.* 132 | 133 | packages/core/core 134 | packages/core/async-context 135 | packages/core/interrupter 136 | packages/core/middleware 137 | packages/core/util 138 | packages/core/stream 139 | packages/core/README.md 140 | 141 | lib 142 | .turbo 143 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | export NODE_OPTIONS="--experimental-strip-types" 2 | 3 | lint-staged --config lint-staged.config.ts 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | prefer-workspace-packages = true 2 | link-workspace-packages = true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | lib 3 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Commands 6 | 7 | ### Development 8 | 9 | - **Build all packages**: `pnpm run build` (uses Turbo for monorepo builds) 10 | - **Type check**: `pnpm run typecheck` (TypeScript build check across all packages) 11 | - **Lint**: `pnpm run lint` (Prettier formatting check) 12 | - **Fix formatting**: `pnpm run lint:fix` (Auto-format with Prettier) 13 | 14 | ### Package-specific commands 15 | 16 | - **Core package build**: `cd packages/core && pnpm run build` 17 | - **Core package dev**: `cd packages/core && pnpm run dev` (watch mode) 18 | - **Core package test**: `cd packages/core && pnpm run test` (Vitest) 19 | - **HTTP package build**: `cd packages/http && pnpm run build` (uses Bunchee) 20 | - **LlamaIndex package test**: `cd packages/llamaindex && pnpm run test` 21 | 22 | ### Testing 23 | 24 | - **Run tests**: `vitest run` from package directories 25 | - **Test workspace**: Tests are configured in `vitest.workspace.ts` for all packages 26 | 27 | ## Architecture 28 | 29 | `@llamaindex/workflow-core` is an **event-driven workflow engine** with these core concepts: 30 | 31 | ### Core Components 32 | 33 | - **WorkflowEvent**: Type-safe event definitions created with `workflowEvent()` 34 | - **Workflow**: Event handler registry created with `createWorkflow()` 35 | - **WorkflowContext**: Execution environment with stream access and event sending 36 | - **WorkflowStream**: Extended ReadableStream with filtering, mapping, and event processing 37 | 38 | ### Event Flow Pattern 39 | 40 | ```typescript 41 | // 1. Define events 42 | const startEvent = workflowEvent(); 43 | const stopEvent = workflowEvent(); 44 | 45 | // 2. Register handlers 46 | workflow.handle([startEvent], (start) => { 47 | return stopEvent.with(parseInt(start.data)); 48 | }); 49 | 50 | // 3. Execute workflow 51 | const { stream, sendEvent } = workflow.createContext(); 52 | sendEvent(startEvent.with("42")); 53 | ``` 54 | 55 | ### Middleware System 56 | 57 | - **State**: `withState()` adds stateful context 58 | - **Validation**: `withValidation()` provides type-safe event handling 59 | - **Trace Events**: `withTraceEvents()` enables debugging and handler decorators 60 | - **Snapshot**: `withSnapshot()` allows workflow state save/restore 61 | 62 | ### Key Directories 63 | 64 | - `packages/core/src/core/`: Core workflow engine (event, workflow, context, stream) 65 | - `packages/core/src/middleware/`: Middleware implementations 66 | - `packages/core/src/stream/`: Stream utilities and helpers 67 | - `packages/http/`: HTTP protocol adapter 68 | - `packages/llamaindex/`: LlamaIndex integration 69 | - `demo/`: Example implementations for various frameworks 70 | 71 | ### Monorepo Structure 72 | 73 | - Uses pnpm workspaces with Turbo for build orchestration 74 | - Packages are independently versioned and published 75 | - Demo projects showcase integrations with Next.js, Hono, Deno, browser environments 76 | 77 | ### Browser Compatibility 78 | 79 | Important: Call `getContext()` at the top level of handlers due to browser async context limitations. Calling it after await will fail in browsers. 80 | 81 | ### Testing 82 | 83 | - Uses Vitest for testing across all packages 84 | - Tests are colocated with source files (`.test.ts` files) 85 | - Browser-specific tests use `happy-dom` environment 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 LlamaIndex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/browser/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /demo/browser/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 13 | 14 | ```js 15 | export default tseslint.config({ 16 | extends: [ 17 | // Remove ...tseslint.configs.recommended and replace with this 18 | ...tseslint.configs.recommendedTypeChecked, 19 | // Alternatively, use this for stricter rules 20 | ...tseslint.configs.strictTypeChecked, 21 | // Optionally, add this for stylistic rules 22 | ...tseslint.configs.stylisticTypeChecked, 23 | ], 24 | languageOptions: { 25 | // other options... 26 | parserOptions: { 27 | project: ["./tsconfig.node.json", "./tsconfig.app.json"], 28 | tsconfigRootDir: import.meta.dirname, 29 | }, 30 | }, 31 | }); 32 | ``` 33 | 34 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 35 | 36 | ```js 37 | // eslint.config.js 38 | import reactX from "eslint-plugin-react-x"; 39 | import reactDom from "eslint-plugin-react-dom"; 40 | 41 | export default tseslint.config({ 42 | plugins: { 43 | // Add the react-x and react-dom plugins 44 | "react-x": reactX, 45 | "react-dom": reactDom, 46 | }, 47 | rules: { 48 | // other rules... 49 | // Enable its recommended typescript rules 50 | ...reactX.configs["recommended-typescript"].rules, 51 | ...reactDom.configs.recommended.rules, 52 | }, 53 | }); 54 | ``` 55 | -------------------------------------------------------------------------------- /demo/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS + Llamaindex 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@llamaindex/workflow-core": "latest", 13 | "react": "^19.1.0", 14 | "react-dom": "^19.1.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^19.1.8", 18 | "@types/react-dom": "^19.1.6", 19 | "@vitejs/plugin-react-swc": "^3.10.2", 20 | "globals": "^16.2.0", 21 | "typescript": "~5.8.3", 22 | "vite": "^7.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/browser/public/llamaindex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /demo/browser/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #4051b5); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /demo/browser/src/App.tsx: -------------------------------------------------------------------------------- 1 | import reactLogo from "./assets/react.svg"; 2 | import llamaindexLogo from "/llamaindex.svg"; 3 | import "./App.css"; 4 | import { 5 | createWorkflow, 6 | getContext, 7 | workflowEvent, 8 | } from "@llamaindex/workflow-core"; 9 | import { runWorkflow } from "@llamaindex/workflow-core/stream/run"; 10 | import { Suspense } from "react"; 11 | 12 | const startEvent = workflowEvent(); 13 | const stopEvent = workflowEvent(); 14 | 15 | const workflow = createWorkflow(); 16 | 17 | workflow.handle([startEvent], () => { 18 | const context = getContext(); 19 | setTimeout(() => { 20 | context.sendEvent(stopEvent.with("Hello, World!")); 21 | }, 1000); 22 | }); 23 | 24 | const promise = runWorkflow(workflow, startEvent.with(), stopEvent); 25 | 26 | function App() { 27 | return ( 28 | <> 29 |
30 | 31 | Vite logo 32 | 33 | 34 | React logo 35 | 36 |
37 |

React + Llamaindex Flow

38 |
39 |

40 | 41 | {promise.then(({ data }) => data)} 42 | 43 |

44 |

45 | Edit src/App.tsx and save to test HMR 46 |

47 |
48 | 49 | ); 50 | } 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /demo/browser/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/browser/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | @media (prefers-color-scheme: light) { 58 | :root { 59 | color: #213547; 60 | background-color: #ffffff; 61 | } 62 | a:hover { 63 | color: #747bff; 64 | } 65 | button { 66 | background-color: #f9f9f9; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /demo/browser/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App.tsx"; 5 | 6 | createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /demo/browser/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/browser/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "isolatedModules": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /demo/browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "tsBuildInfoFile": "./lib/.tsbuildinfo" 6 | }, 7 | "files": [], 8 | "references": [ 9 | { "path": "./tsconfig.app.json" }, 10 | { "path": "./tsconfig.node.json" } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /demo/browser/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "target": "ES2022", 6 | "lib": ["ES2023"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /demo/browser/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /demo/cloudflare/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | 173 | .wrangler 174 | -------------------------------------------------------------------------------- /demo/cloudflare/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudflare-workers-openapi", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types" 10 | }, 11 | "dependencies": { 12 | "@llamaindex/workflow-core": "latest", 13 | "hono": "^4.8.3" 14 | }, 15 | "devDependencies": { 16 | "@cloudflare/workers-types": "^4.20250620.0", 17 | "@types/node": "24.0.4", 18 | "@types/service-worker-mock": "^2.0.1", 19 | "wrangler": "^4.21.2" 20 | }, 21 | "packageManager": "pnpm@9.15.5+sha512.845196026aab1cc3f098a0474b64dfbab2afe7a1b4e91dd86895d8e4aa32a7a6d03049e2d0ad770bbe4de023a7122fb68c1a1d6e0d033c7076085f9d5d4800d4" 22 | } 23 | -------------------------------------------------------------------------------- /demo/cloudflare/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; 3 | import { createHonoHandler } from "@llamaindex/workflow-core/hono"; 4 | import { html } from "hono/html"; 5 | 6 | const app = new Hono(); 7 | 8 | const startEvent = workflowEvent(); 9 | const stopEvent = workflowEvent(); 10 | const workflow = createWorkflow(); 11 | 12 | workflow.handle([startEvent], ({ data }) => { 13 | return stopEvent.with(`hello, ${data}!`); 14 | }); 15 | 16 | app.post( 17 | "/workflow", 18 | createHonoHandler( 19 | workflow, 20 | async (ctx) => startEvent.with(await ctx.req.text()), 21 | stopEvent, 22 | ), 23 | ); 24 | 25 | app.get("/", (c) => { 26 | return c.html( 27 | html` 28 | 29 | 30 | Workflow Demo 31 | 32 | 33 |

Hello!

34 | 35 |
36 | 37 | Name 38 | 39 |
40 | 54 | 55 | `, 56 | ); 57 | }); 58 | 59 | export default app; 60 | -------------------------------------------------------------------------------- /demo/cloudflare/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "baseUrl": "src", 7 | "declaration": true, 8 | "sourceMap": true, 9 | "esModuleInterop": true, 10 | "inlineSourceMap": false, 11 | "lib": ["esnext"], 12 | "listEmittedFiles": false, 13 | "listFiles": false, 14 | "moduleResolution": "bundler", 15 | "noFallthroughCasesInSwitch": true, 16 | "pretty": true, 17 | "resolveJsonModule": true, 18 | "skipLibCheck": true, 19 | "strict": false, 20 | "traceResolution": false, 21 | "outDir": "./dist", 22 | "tsBuildInfoFile": "./dist/.tsbuildinfo", 23 | "target": "esnext", 24 | "module": "esnext", 25 | "types": [ 26 | "@types/node", 27 | "@types/service-worker-mock", 28 | "@cloudflare/workers-types/2023-07-01" 29 | ] 30 | }, 31 | "exclude": ["node_modules", "dist", "tests"], 32 | "include": ["src", "scripts"] 33 | } 34 | -------------------------------------------------------------------------------- /demo/cloudflare/worker-configuration.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by Wrangler 2 | // After adding bindings to `wrangler.jsonc`, regenerate this interface via `npm run cf-typegen` 3 | interface Env {} 4 | -------------------------------------------------------------------------------- /demo/cloudflare/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | /** 2 | * For more details on how to configure Wrangler, refer to: 3 | * https://developers.cloudflare.com/workers/wrangler/configuration/ 4 | */ 5 | { 6 | "$schema": "node_modules/wrangler/config-schema.json", 7 | "name": "workspace-worker", 8 | "main": "src/index.ts", 9 | "compatibility_flags": ["nodejs_als"], 10 | "compatibility_date": "2025-03-16", 11 | } 12 | -------------------------------------------------------------------------------- /demo/deno/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "dev": "deno run --watch main.ts" 4 | }, 5 | "imports": { 6 | "@std/assert": "jsr:@std/assert@1", 7 | "@llamaindex/workflow-core": "npm:@llamaindex/workflow-core" 8 | }, 9 | "links": ["../../packages/core"], 10 | "unstable": ["npm-patch"], 11 | "nodeModulesDir": "auto" 12 | } 13 | -------------------------------------------------------------------------------- /demo/deno/deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "specifiers": { 4 | "jsr:@std/assert@1": "1.0.13", 5 | "jsr:@std/internal@^1.0.6": "1.0.6", 6 | "npm:@llamaindex/workflow-core@*": "0.4.5" 7 | }, 8 | "jsr": { 9 | "@std/assert@1.0.13": { 10 | "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", 11 | "dependencies": [ 12 | "jsr:@std/internal" 13 | ] 14 | }, 15 | "@std/internal@1.0.6": { 16 | "integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4" 17 | } 18 | }, 19 | "npm": { 20 | "@llamaindex/workflow-core@0.4.5": {} 21 | }, 22 | "workspace": { 23 | "dependencies": [ 24 | "jsr:@std/assert@1", 25 | "npm:@llamaindex/workflow-core@*" 26 | ], 27 | "links": { 28 | "npm:@llamaindex/workflow-core@0.4.5": { 29 | "peerDependencies": [ 30 | "npm:@modelcontextprotocol/sdk@^1.7.0", 31 | "npm:hono@^4.7.4", 32 | "npm:next@^15.2.2", 33 | "npm:p-retry@^6.2.1", 34 | "npm:rxjs@^7.8.2", 35 | "npm:zod@^3.24.2" 36 | ], 37 | "peerDependenciesMeta": { 38 | "@modelcontextprotocol/sdk": { 39 | "optional": true 40 | }, 41 | "hono": { 42 | "optional": true 43 | }, 44 | "next": { 45 | "optional": true 46 | }, 47 | "p-retry": { 48 | "optional": true 49 | }, 50 | "rxjs": { 51 | "optional": true 52 | }, 53 | "zod": { 54 | "optional": true 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /demo/deno/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createWorkflow, 3 | workflowEvent, 4 | getContext, 5 | } from "@llamaindex/workflow-core"; 6 | 7 | const workflow = createWorkflow(); 8 | 9 | export const startEvent = workflowEvent(); 10 | export const endEvent = workflowEvent(); 11 | 12 | workflow.handle([startEvent], () => { 13 | const { sendEvent } = getContext(); 14 | setTimeout(() => { 15 | sendEvent(endEvent.with("Hello World!")); 16 | }); 17 | }); 18 | 19 | export { workflow }; 20 | 21 | if (import.meta.main) { 22 | const { sendEvent } = workflow.createContext(); 23 | sendEvent(startEvent.with()); 24 | } 25 | -------------------------------------------------------------------------------- /demo/deno/main_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "@std/assert"; 2 | import { workflow, startEvent, endEvent } from "./main.ts"; 3 | 4 | Deno.test(function workflowRun() { 5 | const { sendEvent, stream } = workflow.createContext(); 6 | sendEvent(startEvent.with()); 7 | stream 8 | .pipeThrough( 9 | new TransformStream({ 10 | transform: (event, controller) => { 11 | if (endEvent.include(event)) { 12 | controller.enqueue(event.data); 13 | } 14 | }, 15 | }), 16 | ) 17 | .pipeTo( 18 | new WritableStream({ 19 | write: (data) => { 20 | assertEquals(data, "Hello World!"); 21 | }, 22 | }), 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /demo/hono/app.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { serve } from "@hono/node-server"; 3 | import { createHonoHandler } from "@llamaindex/workflow-core/hono"; 4 | import { 5 | toolCallWorkflow, 6 | startEvent, 7 | stopEvent, 8 | } from "../workflows/tool-call-agent.js"; 9 | 10 | const app = new Hono(); 11 | 12 | app.post( 13 | "/workflow", 14 | createHonoHandler( 15 | toolCallWorkflow, 16 | async (ctx) => startEvent.with(await ctx.req.text()), 17 | stopEvent, 18 | ), 19 | ); 20 | 21 | const serializableMemoryMap = new Map(); 22 | 23 | app.post("/human-in-the-loop", async (ctx) => { 24 | const { workflow, stopEvent, startEvent, humanInteractionEvent } = 25 | await import("../workflows/human-in-the-loop"); 26 | const json = await ctx.req.json(); 27 | let context: ReturnType; 28 | if (json.requestId) { 29 | const data = json.data; 30 | const serializable = serializableMemoryMap.get(json.requestId); 31 | context = workflow.resume(data, serializable); 32 | } else { 33 | context = workflow.createContext(); 34 | context.sendEvent(startEvent.with(json.data)); 35 | } 36 | 37 | const { onRequest, stream } = context; 38 | return new Promise((resolve) => { 39 | // listen to human interaction 40 | onRequest(humanInteractionEvent, async (reason) => { 41 | context.snapshot().then(([re, sd]) => { 42 | const requestId = crypto.randomUUID(); 43 | serializableMemoryMap.set(requestId, sd); 44 | resolve( 45 | Response.json({ 46 | requestId: requestId, 47 | reason: reason, 48 | data: re.map((r) => 49 | r === humanInteractionEvent 50 | ? "request human in the loop" 51 | : "UNKNOWN", 52 | ), 53 | }), 54 | ); 55 | }); 56 | }); 57 | 58 | // consume stream 59 | stream 60 | .until(stopEvent) 61 | .toArray() 62 | .then((events) => { 63 | const stopEvent = events.at(-1)!; 64 | resolve(Response.json(stopEvent.data)); 65 | }); 66 | }); 67 | }); 68 | 69 | serve(app, ({ port }) => { 70 | console.log(`Server started at http://localhost:${port}`); 71 | }); 72 | -------------------------------------------------------------------------------- /demo/next/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /demo/node/basic.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createWorkflow, 3 | workflowEvent, 4 | getContext, 5 | } from "@llamaindex/workflow-core"; 6 | import { pipeline } from "node:stream/promises"; 7 | import { collect } from "@llamaindex/workflow-core/stream/consumer"; 8 | 9 | //#region define workflow events 10 | const startEvent = workflowEvent(); 11 | const branchAEvent = workflowEvent(); 12 | const branchBEvent = workflowEvent(); 13 | const branchCEvent = workflowEvent(); 14 | const branchCompleteEvent = workflowEvent(); 15 | const allCompleteEvent = workflowEvent(); 16 | const stopEvent = workflowEvent(); 17 | //#endregion 18 | 19 | //#region defines workflow 20 | const workflow = createWorkflow(); 21 | workflow.handle([startEvent], async () => { 22 | // emit 3 different events, handled separately 23 | const { sendEvent, stream } = getContext(); 24 | sendEvent(branchAEvent.with("Branch A")); 25 | sendEvent(branchBEvent.with("Branch B")); 26 | sendEvent(branchCEvent.with("Branch C")); 27 | 28 | const results = await stream.filter(branchCompleteEvent).take(3).toArray(); 29 | 30 | return allCompleteEvent.with(results.map((e) => e.data).join(", ")); 31 | }); 32 | 33 | workflow.handle([branchAEvent], (branchA) => { 34 | return branchCompleteEvent.with(branchA.data); 35 | }); 36 | 37 | workflow.handle([branchBEvent], (branchB) => { 38 | return branchCompleteEvent.with(branchB.data); 39 | }); 40 | 41 | workflow.handle([branchCEvent], (branchC) => { 42 | return branchCompleteEvent.with(branchC.data); 43 | }); 44 | 45 | workflow.handle([allCompleteEvent], (allComplete) => { 46 | return stopEvent.with(allComplete.data); 47 | }); 48 | 49 | //#endregion 50 | 51 | const { stream, sendEvent } = workflow.createContext(); 52 | sendEvent(startEvent.with("initial data")); 53 | 54 | const result = await pipeline(stream, async function (source) { 55 | for await (const event of source) { 56 | if (stopEvent.include(event)) { 57 | return `Result: ${event.data}`; 58 | } 59 | } 60 | }); 61 | 62 | console.log(result); // Result: Branch A, Branch B, Branch C 63 | -------------------------------------------------------------------------------- /demo/node/file-parse-promise.ts: -------------------------------------------------------------------------------- 1 | import { 2 | fileParseWorkflow, 3 | startEvent, 4 | stopEvent, 5 | } from "../workflows/file-parse-agent.js"; 6 | 7 | const directory = ".."; 8 | 9 | const { state, sendEvent, stream } = fileParseWorkflow.createContext(); 10 | 11 | sendEvent(startEvent.with(directory)); 12 | 13 | await stream.until(stopEvent).toArray(); 14 | 15 | console.log(state.output); 16 | -------------------------------------------------------------------------------- /demo/node/file-parse-rxjs.ts: -------------------------------------------------------------------------------- 1 | import { 2 | fileParseWorkflow, 3 | messageEvent, 4 | startEvent, 5 | } from "../workflows/file-parse-agent.js"; 6 | import { filter, map } from "rxjs"; 7 | import { eventSource } from "@llamaindex/workflow-core"; 8 | import { toObservable } from "@llamaindex/workflow-core/observable"; 9 | 10 | const directory = ".."; 11 | 12 | const { stream, sendEvent } = fileParseWorkflow.createContext(); 13 | 14 | toObservable(stream) 15 | .pipe( 16 | filter((ev) => eventSource(ev) === messageEvent), 17 | map((ev) => ev.data), 18 | ) 19 | .subscribe((data) => { 20 | console.log(data); 21 | }); 22 | 23 | sendEvent(startEvent.with(directory)); 24 | -------------------------------------------------------------------------------- /demo/node/llama-parse-workflow.ts: -------------------------------------------------------------------------------- 1 | import { 2 | llamaParseWorkflow, 3 | startEvent, 4 | stopEvent, 5 | } from "../workflows/llama-parse-workflow.js"; 6 | import { runWorkflow } from "@llamaindex/workflow-core/stream/run"; 7 | 8 | runWorkflow( 9 | llamaParseWorkflow, 10 | startEvent.with({ 11 | inputFile: process.argv[2], 12 | apiKey: process.env.LLAMA_CLOUD_API!, 13 | }), 14 | stopEvent, 15 | ).then(({ data }) => { 16 | console.log(data.markdown); 17 | }); 18 | -------------------------------------------------------------------------------- /demo/node/mcp-file-parse-tool.ts: -------------------------------------------------------------------------------- 1 | import { mcpTool } from "@llamaindex/workflow-core/mcp"; 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { z } from "zod"; 4 | import { fileParseWorkflow } from "../workflows/file-parse-agent.js"; 5 | import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; 6 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 7 | 8 | const server = new McpServer({ 9 | name: "Demo", 10 | version: "1.0.0", 11 | }); 12 | 13 | export const startEvent = workflowEvent<{ 14 | filePath: string; 15 | }>(); 16 | export const stopEvent = workflowEvent<{ 17 | content: { type: "text"; text: string }[]; 18 | }>(); 19 | 20 | const wrappedWorkflow = createWorkflow(); 21 | 22 | wrappedWorkflow.handle([startEvent], async ({ data: { filePath } }) => { 23 | const { stream, sendEvent, state } = fileParseWorkflow.createContext(); 24 | sendEvent(startEvent.with({ filePath })); 25 | await stream.until(stopEvent).toArray(); 26 | return stopEvent.with({ 27 | content: [ 28 | { 29 | type: "text", 30 | text: state.output, 31 | }, 32 | ], 33 | }); 34 | }); 35 | 36 | server.tool( 37 | "list directory", 38 | { 39 | filePath: z.string(), 40 | }, 41 | mcpTool(wrappedWorkflow, startEvent, stopEvent), 42 | ); 43 | 44 | const transport = new StdioServerTransport(); 45 | 46 | server.connect(transport).then(() => { 47 | console.log("Connected"); 48 | }); 49 | -------------------------------------------------------------------------------- /demo/node/name-ask-readline.ts: -------------------------------------------------------------------------------- 1 | import { input } from "@inquirer/prompts"; 2 | import { 3 | workflow, 4 | stopEvent, 5 | startEvent, 6 | humanInteractionEvent, 7 | } from "../workflows/human-in-the-loop"; 8 | 9 | const name = await input({ 10 | message: "What is your name?", 11 | }); 12 | const { onRequest, stream, sendEvent } = workflow.createContext(); 13 | 14 | sendEvent(startEvent.with(name)); 15 | 16 | onRequest(humanInteractionEvent, async (reason) => { 17 | console.log("Requesting human interaction..."); 18 | const name = await input({ 19 | message: JSON.parse(reason).message, 20 | }); 21 | console.log("Human interaction completed."); 22 | sendEvent(humanInteractionEvent.with(name)); 23 | }); 24 | 25 | stream.on(stopEvent, ({ data }) => { 26 | console.log("AI analysis: ", data); 27 | }); 28 | 29 | await stream.until(stopEvent).toArray(); 30 | -------------------------------------------------------------------------------- /demo/node/tool-call-agent.ts: -------------------------------------------------------------------------------- 1 | import { runWorkflow } from "@llamaindex/workflow-core/stream/run"; 2 | import { 3 | toolCallWorkflow, 4 | startEvent, 5 | stopEvent, 6 | } from "../workflows/tool-call-agent.js"; 7 | 8 | runWorkflow( 9 | toolCallWorkflow, 10 | startEvent.with("what is weather today, im in san francisco"), 11 | stopEvent, 12 | ).then(({ data }) => { 13 | console.log("AI response", data); 14 | }); 15 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "type": "module", 4 | "private": true, 5 | "dependencies": { 6 | "@hono/node-server": "^1.14.4", 7 | "@inquirer/prompts": "^7.5.3", 8 | "@llamaindex/workflow-core": "latest", 9 | "@modelcontextprotocol/sdk": "^1.13.1", 10 | "hono": "^4.8.3", 11 | "openai": "^5.7.0", 12 | "p-retry": "^6.2.1", 13 | "rxjs": "^7.8.2", 14 | "zod": "^3.25.67" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^24.0.4", 18 | "tsx": "^4.20.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "esModuleInterop": true, 6 | "moduleResolution": "bundler", 7 | "outDir": "./lib", 8 | "tsBuildInfoFile": "./lib/.tsbuildinfo", 9 | "lib": ["DOM", "DOM.AsyncIterable", "DOM.Iterable", "esnext"], 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["hono", "node", "workflows"], 15 | "references": [ 16 | { 17 | "path": "./browser/tsconfig.json" 18 | }, 19 | { 20 | "path": "./cloudflare/tsconfig.json" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /demo/waku/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env* 4 | *.tsbuildinfo 5 | .cache 6 | .DS_Store 7 | *.pem 8 | src/pages.gen.ts 9 | -------------------------------------------------------------------------------- /demo/waku/openapi-ts.config.ts: -------------------------------------------------------------------------------- 1 | import { defaultPlugins, defineConfig } from "@hey-api/openapi-ts"; 2 | 3 | export default defineConfig({ 4 | input: "https://api.cloud.llamaindex.ai/api/openapi.json", 5 | output: "src/lib/api", 6 | plugins: [ 7 | ...defaultPlugins, 8 | "@hey-api/client-fetch", 9 | "zod", 10 | "@hey-api/schemas", 11 | "@hey-api/sdk", 12 | { 13 | enums: "javascript", 14 | identifierCase: "PascalCase", 15 | name: "@hey-api/typescript", 16 | }, 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /demo/waku/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waku-project", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "postinstall": "openapi-ts", 8 | "dev": "waku dev", 9 | "build": "waku build", 10 | "start": "waku start" 11 | }, 12 | "dependencies": { 13 | "@llamaindex/workflow-core": "latest", 14 | "@llamaindex/workflow-http": "latest", 15 | "@neondatabase/serverless": "^1.0.1", 16 | "lucide-react": "^0.523.0", 17 | "openai": "^5.7.0", 18 | "react": "19.1.0", 19 | "react-dom": "19.1.0", 20 | "react-server-dom-webpack": "19.1.0", 21 | "stable-hash": "^0.0.6", 22 | "waku": "0.23.2" 23 | }, 24 | "devDependencies": { 25 | "@hey-api/client-fetch": "^0.13.1", 26 | "@hey-api/openapi-ts": "^0.77.0", 27 | "@tailwindcss/postcss": "4.1.10", 28 | "@types/react": "19.1.8", 29 | "@types/react-dom": "19.1.6", 30 | "postcss": "8.5.6", 31 | "tailwindcss": "4.1.10", 32 | "typescript": "5.8.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demo/waku/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /demo/waku/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/run-llama/workflows-ts/6017be435975a983e66be679ecb7be0bedf8edd7/demo/waku/public/images/favicon.png -------------------------------------------------------------------------------- /demo/waku/src/components/RAG.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { createClient } from "@llamaindex/workflow-http/client"; 3 | import * as events from "../workflow/events"; 4 | import { useState, useCallback } from "react"; 5 | 6 | const { fetch } = createClient("/api/store", events); 7 | 8 | export const RAG = () => { 9 | const [list, setList] = useState([]); 10 | const [file, setFile] = useState(null); 11 | 12 | const handleFileInput = useCallback( 13 | (e: React.ChangeEvent) => { 14 | if (e.target.files) { 15 | const selectedFiles = Array.from(e.target.files); 16 | setFile(selectedFiles[0]!); 17 | } 18 | }, 19 | [], 20 | ); 21 | 22 | return ( 23 |
24 |
25 | 26 | 42 |
43 | 44 |
{ 46 | const search = form.get("search") as string; 47 | fetch({ 48 | search, 49 | }).then((stream) => { 50 | stream.forEach((event) => { 51 | console.log(event); 52 | if (event.data) { 53 | setList((prev) => [...prev, `${event.data}`]); 54 | } 55 | }); 56 | }); 57 | }} 58 | > 59 | 65 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /demo/waku/src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | export const Footer = () => { 2 | return ( 3 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /demo/waku/src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "waku"; 2 | 3 | export const Header = () => { 4 | return ( 5 |
6 |

7 | Waku starter 8 |

9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /demo/waku/src/pages/_layout.tsx: -------------------------------------------------------------------------------- 1 | import "../styles.css"; 2 | 3 | import type { ReactNode } from "react"; 4 | 5 | import { Header } from "../components/header"; 6 | import { Footer } from "../components/footer"; 7 | 8 | type RootLayoutProps = { children: ReactNode }; 9 | 10 | export default async function RootLayout({ children }: RootLayoutProps) { 11 | const data = await getData(); 12 | 13 | return ( 14 |
15 | 16 | 17 |
18 |
19 | {children} 20 |
21 |
22 |
23 | ); 24 | } 25 | 26 | const getData = async () => { 27 | const data = { 28 | description: "An internet website!", 29 | icon: "/images/favicon.png", 30 | }; 31 | 32 | return data; 33 | }; 34 | 35 | export const getConfig = async () => { 36 | return { 37 | render: "static", 38 | } as const; 39 | }; 40 | -------------------------------------------------------------------------------- /demo/waku/src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "waku"; 2 | 3 | export default async function AboutPage() { 4 | const data = await getData(); 5 | 6 | return ( 7 |
8 | {data.title} 9 |

{data.headline}

10 |

{data.body}

11 | 12 | Return home 13 | 14 |
15 | ); 16 | } 17 | 18 | const getData = async () => { 19 | const data = { 20 | title: "About", 21 | headline: "About Waku", 22 | body: "The minimal React framework", 23 | }; 24 | 25 | return data; 26 | }; 27 | 28 | export const getConfig = async () => { 29 | return { 30 | render: "static", 31 | } as const; 32 | }; 33 | -------------------------------------------------------------------------------- /demo/waku/src/pages/api/store.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from "@llamaindex/workflow-http/server"; 2 | import { workflow } from "../../workflow/basic"; 3 | import { upload } from "../../workflow/llama-parse"; 4 | import { storeEvent, stopEvent, searchEvent } from "../../workflow/events"; 5 | 6 | process.on("unhandledRejection", (reason) => { 7 | console.error("Unhandled Rejection at:", reason); 8 | }); 9 | 10 | export const POST = createServer( 11 | workflow, 12 | async (data, sendEvent) => { 13 | if (data.file) { 14 | const file = data.file; 15 | const job = await upload({ 16 | file, 17 | }); 18 | const text = await job.markdown(); 19 | sendEvent(storeEvent.with(text)); 20 | } else if (data.search) { 21 | const search = data.search; 22 | sendEvent(searchEvent.with(search)); 23 | } 24 | }, 25 | (stream) => stream.until(stopEvent), 26 | ); 27 | -------------------------------------------------------------------------------- /demo/waku/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { RAG } from "../components/RAG"; 2 | 3 | export default async function HomePage() { 4 | const data = await getData(); 5 | 6 | return ( 7 |
8 | {data.title} 9 |

{data.headline}

10 |

{data.body}

11 | 12 |
13 | ); 14 | } 15 | 16 | const getData = async () => { 17 | const data = { 18 | title: "Waku", 19 | headline: "Waku", 20 | body: "Hello world!", 21 | }; 22 | 23 | return data; 24 | }; 25 | 26 | export const getConfig = async () => { 27 | return { 28 | render: "static", 29 | } as const; 30 | }; 31 | -------------------------------------------------------------------------------- /demo/waku/src/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { FailPageMode, ParserLanguages, ParsingMode } from "../lib/api"; 2 | 3 | import { z } from "zod"; 4 | 5 | type Language = (typeof ParserLanguages)[keyof typeof ParserLanguages]; 6 | const VALUES: [Language, ...Language[]] = [ 7 | ParserLanguages.EN, 8 | ...Object.values(ParserLanguages), 9 | ]; 10 | const languageSchema = z.enum(VALUES); 11 | 12 | const PARSE_PRESETS = [ 13 | "fast", 14 | "balanced", 15 | "premium", 16 | "structured", 17 | "auto", 18 | "scientific", 19 | "invoice", 20 | "slides", 21 | "_carlyle", 22 | ] as const; 23 | 24 | export const parsePresetSchema = z.enum(PARSE_PRESETS); 25 | 26 | export const parseFormSchema = z.object({ 27 | adaptive_long_table: z.boolean().optional(), 28 | annotate_links: z.boolean().optional(), 29 | auto_mode: z.boolean().optional(), 30 | auto_mode_trigger_on_image_in_page: z.boolean().optional(), 31 | auto_mode_trigger_on_table_in_page: z.boolean().optional(), 32 | auto_mode_trigger_on_text_in_page: z.string().optional(), 33 | auto_mode_trigger_on_regexp_in_page: z.string().optional(), 34 | auto_mode_configuration_json: z.string().optional(), 35 | azure_openai_api_version: z.string().optional(), 36 | azure_openai_deployment_name: z.string().optional(), 37 | azure_openai_endpoint: z.string().optional(), 38 | azure_openai_key: z.string().optional(), 39 | bbox_bottom: z.number().min(0).max(1).optional(), 40 | bbox_left: z.number().min(0).max(1).optional(), 41 | bbox_right: z.number().min(0).max(1).optional(), 42 | bbox_top: z.number().min(0).max(1).optional(), 43 | disable_ocr: z.boolean().optional(), 44 | disable_reconstruction: z.boolean().optional(), 45 | disable_image_extraction: z.boolean().optional(), 46 | do_not_cache: z.coerce.boolean().optional(), 47 | do_not_unroll_columns: z.coerce.boolean().optional(), 48 | extract_charts: z.boolean().optional(), 49 | guess_xlsx_sheet_name: z.boolean().optional(), 50 | html_make_all_elements_visible: z.boolean().optional(), 51 | html_remove_fixed_elements: z.boolean().optional(), 52 | html_remove_navigation_elements: z.boolean().optional(), 53 | http_proxy: z 54 | .string() 55 | .url( 56 | 'Set a valid URL for the HTTP proxy, e.g., "http://proxy.example.com:8080"', 57 | ) 58 | .refine( 59 | (url) => { 60 | try { 61 | const parsedUrl = new URL(url); 62 | return ( 63 | parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:" 64 | ); 65 | } catch { 66 | return false; 67 | } 68 | }, 69 | { 70 | message: "Invalid HTTP proxy URL", 71 | }, 72 | ) 73 | .optional(), 74 | input_s3_path: z.string().optional(), 75 | input_s3_region: z.string().optional(), 76 | input_url: z.string().optional(), 77 | invalidate_cache: z.boolean().optional(), 78 | language: z.array(languageSchema).optional(), 79 | extract_layout: z.boolean().optional(), 80 | max_pages: z.number().nullable().optional(), 81 | output_pdf_of_document: z.boolean().optional(), 82 | output_s3_path_prefix: z.string().optional(), 83 | output_s3_region: z.string().optional(), 84 | page_prefix: z.string().optional(), 85 | page_separator: z.string().optional(), 86 | page_suffix: z.string().optional(), 87 | preserve_layout_alignment_across_pages: z.boolean().optional(), 88 | skip_diagonal_text: z.boolean().optional(), 89 | spreadsheet_extract_sub_tables: z.boolean().optional(), 90 | structured_output: z.boolean().optional(), 91 | structured_output_json_schema: z.string().optional(), 92 | structured_output_json_schema_name: z.string().optional(), 93 | take_screenshot: z.boolean().optional(), 94 | target_pages: z.string().optional(), 95 | vendor_multimodal_api_key: z.string().optional(), 96 | vendor_multimodal_model_name: z.string().optional(), 97 | model: z.string().optional(), 98 | webhook_url: z.string().url().optional(), 99 | parse_mode: z.nativeEnum(ParsingMode).nullable().optional(), 100 | system_prompt: z.string().optional(), 101 | system_prompt_append: z.string().optional(), 102 | user_prompt: z.string().optional(), 103 | job_timeout_in_seconds: z.number().optional(), 104 | job_timeout_extra_time_per_page_in_seconds: z.number().optional(), 105 | strict_mode_image_extraction: z.boolean().optional(), 106 | strict_mode_image_ocr: z.boolean().optional(), 107 | strict_mode_reconstruction: z.boolean().optional(), 108 | strict_mode_buggy_font: z.boolean().optional(), 109 | save_images: z.boolean().optional(), 110 | ignore_document_elements_for_layout_detection: z.boolean().optional(), 111 | output_tables_as_HTML: z.boolean().optional(), 112 | use_vendor_multimodal_model: z.boolean().optional(), 113 | bounding_box: z.string().optional(), 114 | gpt4o_mode: z.boolean().optional(), 115 | gpt4o_api_key: z.string().optional(), 116 | complemental_formatting_instruction: z.string().optional(), 117 | content_guideline_instruction: z.string().optional(), 118 | premium_mode: z.boolean().optional(), 119 | is_formatting_instruction: z.boolean().optional(), 120 | continuous_mode: z.boolean().optional(), 121 | parsing_instruction: z.string().optional(), 122 | fast_mode: z.boolean().optional(), 123 | formatting_instruction: z.string().optional(), 124 | preset: parsePresetSchema.optional(), 125 | compact_markdown_table: z.boolean().optional(), 126 | markdown_table_multiline_header_separator: z.string().optional(), 127 | page_error_tolerance: z.number().min(0).max(1).optional(), 128 | replace_failed_page_mode: z.nativeEnum(FailPageMode).nullable().optional(), 129 | replace_failed_page_with_error_message_prefix: z.string().optional(), 130 | replace_failed_page_with_error_message_suffix: z.string().optional(), 131 | }); 132 | -------------------------------------------------------------------------------- /demo/waku/src/styles.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,400;0,700;1,400;1,700&display=swap") 2 | layer(base); 3 | @import "tailwindcss"; 4 | -------------------------------------------------------------------------------- /demo/waku/src/workflow/basic.ts: -------------------------------------------------------------------------------- 1 | import { neon } from "@neondatabase/serverless"; 2 | import { createWorkflow, getContext } from "@llamaindex/workflow-core"; 3 | import { storeEvent, stopEvent, searchEvent } from "./events"; 4 | import { getEnv } from "waku"; 5 | import { OpenAI } from "openai"; 6 | 7 | const openai = new OpenAI({ 8 | apiKey: getEnv("OPENAI_API_KEY")!, 9 | }); 10 | const sql = neon(getEnv("DATABASE_URL")!); 11 | 12 | export const workflow = createWorkflow(); 13 | 14 | workflow.handle([storeEvent], async ({ data }) => { 15 | const embeddingResponse = await openai.embeddings.create({ 16 | model: "text-embedding-ada-002", 17 | input: data, 18 | }); 19 | const embedding = embeddingResponse.data[0]!.embedding; 20 | await sql` 21 | INSERT INTO text_vectors (text, embedding) 22 | VALUES (${data}, ${JSON.stringify(embedding)}) 23 | `; 24 | return stopEvent.with("success"); 25 | }); 26 | 27 | workflow.handle([searchEvent], async ({ data }) => { 28 | const { signal } = getContext(); 29 | signal.addEventListener("abort", () => { 30 | console.error("error", signal.reason); 31 | }); 32 | const embeddingResponse = await openai.embeddings.create({ 33 | model: "text-embedding-ada-002", 34 | input: data, 35 | }); 36 | const embedding = embeddingResponse.data[0]!.embedding; 37 | const result = await sql` 38 | SELECT text 39 | FROM text_vectors 40 | ORDER BY embedding <=> ${JSON.stringify(embedding)} 41 | LIMIT 5 42 | `; 43 | return stopEvent.with(result.map((row) => row.text).join("\n")); 44 | }); 45 | -------------------------------------------------------------------------------- /demo/waku/src/workflow/events.ts: -------------------------------------------------------------------------------- 1 | import { workflowEvent } from "@llamaindex/workflow-core"; 2 | import { zodEvent } from "@llamaindex/workflow-core/util/zod"; 3 | import { parseFormSchema } from "../schema"; 4 | import { z } from "zod"; 5 | 6 | export const searchEvent = workflowEvent({ 7 | debugLabel: "search", 8 | uniqueId: "search", 9 | }); 10 | export const storeEvent = workflowEvent({ 11 | debugLabel: "store", 12 | uniqueId: "store", 13 | }); 14 | export const stopEvent = workflowEvent({ 15 | debugLabel: "stop", 16 | uniqueId: "stop", 17 | }); 18 | 19 | export const startEvent = zodEvent( 20 | parseFormSchema.merge( 21 | z.object({ 22 | file: z 23 | .string() 24 | .or(z.instanceof(File)) 25 | .or(z.instanceof(Blob)) 26 | .or(z.instanceof(Uint8Array)) 27 | .optional() 28 | .describe("input"), 29 | }), 30 | ), 31 | { 32 | debugLabel: "llama-parse", 33 | uniqueId: "llama-parse", 34 | }, 35 | ); 36 | export const checkStatusEvent = workflowEvent({ 37 | debugLabel: "check-status", 38 | uniqueId: "check-status", 39 | }); 40 | export const checkStatusSuccessEvent = workflowEvent({ 41 | debugLabel: "check-status-success", 42 | uniqueId: "check-status-success", 43 | }); 44 | export const requestMarkdownEvent = workflowEvent({ 45 | debugLabel: "markdown-request", 46 | uniqueId: "markdown-request", 47 | }); 48 | export const requestTextEvent = workflowEvent({ 49 | debugLabel: "text-request", 50 | uniqueId: "text-request", 51 | }); 52 | export const requestJsonEvent = workflowEvent({ 53 | debugLabel: "json-request", 54 | uniqueId: "json-request", 55 | }); 56 | 57 | export const markdownResultEvent = workflowEvent({ 58 | debugLabel: "markdown-result", 59 | uniqueId: "markdown-result", 60 | }); 61 | export const textResultEvent = workflowEvent({ 62 | debugLabel: "text-result", 63 | uniqueId: "text-result", 64 | }); 65 | export const jsonResultEvent = workflowEvent({ 66 | debugLabel: "json-result", 67 | uniqueId: "json-result", 68 | }); 69 | -------------------------------------------------------------------------------- /demo/waku/src/workflow/llama-parse.ts: -------------------------------------------------------------------------------- 1 | import { createClient, createConfig } from "@hey-api/client-fetch"; 2 | import { 3 | createWorkflow, 4 | workflowEvent, 5 | type InferWorkflowEventData, 6 | } from "@llamaindex/workflow-core"; 7 | import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state"; 8 | import { withTraceEvents } from "@llamaindex/workflow-core/middleware/trace-events"; 9 | import { pRetryHandler } from "@llamaindex/workflow-core/util/p-retry"; 10 | import { zodEvent } from "@llamaindex/workflow-core/util/zod"; 11 | import hash from "stable-hash"; 12 | import { z } from "zod"; 13 | import fs from "node:fs/promises"; 14 | import path from "node:path"; 15 | import { getEnv } from "waku"; 16 | import { parseFormSchema } from "../schema"; 17 | import { 18 | type BodyUploadFileApiV1ParsingUploadPost, 19 | getJobApiV1ParsingJobJobIdGet, 20 | getJobJsonResultApiV1ParsingJobJobIdResultJsonGet, 21 | getJobResultApiV1ParsingJobJobIdResultMarkdownGet, 22 | getJobTextResultApiV1ParsingJobJobIdResultTextGet, 23 | type StatusEnum, 24 | uploadFileApiV1ParsingUploadPost, 25 | } from "../lib/api"; 26 | import { 27 | checkStatusEvent, 28 | checkStatusSuccessEvent, 29 | requestMarkdownEvent, 30 | startEvent, 31 | markdownResultEvent, 32 | requestTextEvent, 33 | textResultEvent, 34 | requestJsonEvent, 35 | jsonResultEvent, 36 | } from "./events"; 37 | 38 | export type LlamaParseWorkflowParams = { 39 | region?: "us" | "eu" | "us-staging"; 40 | apiKey?: string; 41 | }; 42 | 43 | const URLS = { 44 | us: "https://api.cloud.llamaindex.ai", 45 | eu: "https://api.cloud.eu.llamaindex.ai", 46 | "us-staging": "https://api.staging.llamaindex.ai", 47 | } as const; 48 | 49 | const { withState, getContext } = createStatefulMiddleware( 50 | (params: LlamaParseWorkflowParams) => { 51 | const apiKey = params.apiKey ?? getEnv("LLAMA_CLOUD_API_KEY"); 52 | const region = params.region ?? "us"; 53 | if (!apiKey) { 54 | throw new Error("LLAMA_CLOUD_API_KEY is not set"); 55 | } 56 | return { 57 | cache: {} as Record, 58 | client: createClient( 59 | createConfig({ 60 | baseUrl: URLS[region], 61 | headers: { 62 | Authorization: `Bearer ${apiKey}`, 63 | }, 64 | }), 65 | ), 66 | }; 67 | }, 68 | ); 69 | 70 | const llamaParseWorkflow = withState(withTraceEvents(createWorkflow())); 71 | 72 | llamaParseWorkflow.handle([startEvent], async ({ data: form }) => { 73 | const { state } = getContext(); 74 | const finalForm = { ...form }; 75 | if ("file" in form) { 76 | // support loads from the file system 77 | const file = form?.file; 78 | const isFilePath = typeof file === "string"; 79 | const data = isFilePath ? await fs.readFile(file) : file; 80 | const filename: string | undefined = isFilePath 81 | ? path.basename(file) 82 | : undefined; 83 | finalForm.file = data 84 | ? globalThis.File && filename 85 | ? new File([data], filename) 86 | : new Blob([data]) 87 | : undefined; 88 | } 89 | const { 90 | data: { id, status }, 91 | } = await uploadFileApiV1ParsingUploadPost({ 92 | throwOnError: true, 93 | body: { 94 | ...finalForm, 95 | } as BodyUploadFileApiV1ParsingUploadPost, 96 | client: state.client, 97 | }); 98 | state.cache[id] = status; 99 | return checkStatusEvent.with(id); 100 | }); 101 | 102 | llamaParseWorkflow.handle( 103 | [checkStatusEvent], 104 | pRetryHandler( 105 | async ({ data: uuid }) => { 106 | const { state } = getContext(); 107 | if (state.cache[uuid] === "SUCCESS") { 108 | return checkStatusSuccessEvent.with(uuid); 109 | } 110 | const { 111 | data: { status }, 112 | } = await getJobApiV1ParsingJobJobIdGet({ 113 | throwOnError: true, 114 | path: { 115 | job_id: uuid, 116 | }, 117 | client: state.client, 118 | }); 119 | state.cache[uuid] = status; 120 | if (status === "SUCCESS") { 121 | return checkStatusSuccessEvent.with(uuid); 122 | } 123 | throw new Error(`LLamaParse status: ${status}`); 124 | }, 125 | { 126 | retries: 100, 127 | }, 128 | ), 129 | ); 130 | 131 | //#region sub workflow 132 | llamaParseWorkflow.handle([requestMarkdownEvent], async ({ data: job_id }) => { 133 | const { state } = getContext(); 134 | const { data } = await getJobResultApiV1ParsingJobJobIdResultMarkdownGet({ 135 | throwOnError: true, 136 | path: { 137 | job_id, 138 | }, 139 | client: state.client, 140 | }); 141 | return markdownResultEvent.with(data.markdown); 142 | }); 143 | 144 | llamaParseWorkflow.handle([requestTextEvent], async ({ data: job_id }) => { 145 | const { state } = getContext(); 146 | const { data } = await getJobTextResultApiV1ParsingJobJobIdResultTextGet({ 147 | throwOnError: true, 148 | path: { 149 | job_id, 150 | }, 151 | client: state.client, 152 | }); 153 | return textResultEvent.with(data.text); 154 | }); 155 | 156 | llamaParseWorkflow.handle([requestJsonEvent], async ({ data: job_id }) => { 157 | const { state } = getContext(); 158 | const { data } = await getJobJsonResultApiV1ParsingJobJobIdResultJsonGet({ 159 | throwOnError: true, 160 | path: { 161 | job_id, 162 | }, 163 | client: state.client, 164 | }); 165 | return jsonResultEvent.with(data.pages); 166 | }); 167 | //#endregion 168 | 169 | const cacheMap = new Map< 170 | string, 171 | ReturnType 172 | >(); 173 | 174 | export type ParseJob = { 175 | get jobId(): string; 176 | get signal(): AbortSignal; 177 | get context(): ReturnType; 178 | get form(): InferWorkflowEventData; 179 | 180 | markdown(): Promise; 181 | text(): Promise; 182 | //eslint-disable-next-line @typescript-eslint/no-explicit-any 183 | json(): Promise; 184 | }; 185 | 186 | export const upload = async ( 187 | params: InferWorkflowEventData & LlamaParseWorkflowParams, 188 | ): Promise => { 189 | //#region cache 190 | const key = hash({ apiKey: params.apiKey, region: params.region }); 191 | if (!cacheMap.has(key)) { 192 | const context = llamaParseWorkflow.createContext(params); 193 | cacheMap.set(key, context); 194 | } 195 | //#endregion 196 | 197 | //#region upload event 198 | const context = cacheMap.get(key)!; 199 | const { stream, sendEvent } = context; 200 | const ev = startEvent.with(params); 201 | sendEvent(ev); 202 | 203 | const uploadThread = await llamaParseWorkflow 204 | .substream(ev, stream) 205 | .until((ev) => checkStatusSuccessEvent.include(ev)) 206 | .toArray(); 207 | //#region 208 | const jobId: string = uploadThread.at(-1)!.data; 209 | return { 210 | get signal() { 211 | // lazy load 212 | return context.signal; 213 | }, 214 | get jobId() { 215 | return jobId; 216 | }, 217 | get form() { 218 | return ev.data; 219 | }, 220 | get context() { 221 | return context; 222 | }, 223 | async markdown(): Promise { 224 | const requestEv = requestMarkdownEvent.with(jobId); 225 | const { sendEvent, stream } = llamaParseWorkflow.createContext(params); 226 | sendEvent(requestEv); 227 | const markdownThread = await stream.until(markdownResultEvent).toArray(); 228 | return markdownThread.at(-1)!.data; 229 | }, 230 | async text(): Promise { 231 | const requestEv = requestTextEvent.with(jobId); 232 | const { sendEvent, stream } = llamaParseWorkflow.createContext(params); 233 | sendEvent(requestEv); 234 | const textThread = await stream.until(textResultEvent).toArray(); 235 | console.log("textThread", textThread); 236 | return textThread.at(-1)!.data; 237 | }, 238 | //eslint-disable-next-line @typescript-eslint/no-explicit-any 239 | async json(): Promise { 240 | const requestEv = requestJsonEvent.with(jobId); 241 | const { sendEvent, stream } = llamaParseWorkflow.createContext(params); 242 | sendEvent(requestEv); 243 | const jsonThread = await stream 244 | .until((ev) => jsonResultEvent.include(ev)) 245 | .toArray(); 246 | return jsonThread.at(-1)!.data; 247 | }, 248 | }; 249 | }; 250 | -------------------------------------------------------------------------------- /demo/waku/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "target": "esnext", 6 | "downlevelIteration": true, 7 | "esModuleInterop": true, 8 | "module": "esnext", 9 | "moduleResolution": "bundler", 10 | "skipLibCheck": true, 11 | "noUncheckedIndexedAccess": true, 12 | "exactOptionalPropertyTypes": true, 13 | "jsx": "react-jsx" 14 | }, 15 | "include": ["./src"] 16 | } 17 | -------------------------------------------------------------------------------- /demo/workflows/file-parse-agent.ts: -------------------------------------------------------------------------------- 1 | import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; 2 | import { readdir, stat } from "node:fs/promises"; 3 | import { resolve } from "node:path"; 4 | import { AsyncLocalStorage } from "node:async_hooks"; 5 | import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state"; 6 | 7 | export const messageEvent = workflowEvent({ 8 | debugLabel: "message", 9 | }); 10 | 11 | export const startEvent = workflowEvent({ 12 | debugLabel: "start", 13 | }); 14 | const readDirEvent = workflowEvent<[string, number]>({ 15 | debugLabel: "readDir", 16 | }); 17 | const readFileEvent = workflowEvent<[string, number]>({ 18 | debugLabel: "readFile", 19 | }); 20 | const readResultEvent = workflowEvent({ 21 | debugLabel: "readResult", 22 | }); 23 | export const stopEvent = workflowEvent({ 24 | debugLabel: "stop", 25 | }); 26 | 27 | const { withState, getContext } = createStatefulMiddleware(() => ({ 28 | output: "", 29 | apiKey: "", 30 | })); 31 | 32 | export const fileParseWorkflow = withState(createWorkflow()); 33 | 34 | const locks: { 35 | finish: boolean; 36 | }[] = []; 37 | 38 | fileParseWorkflow.handle([startEvent], async ({ data: dir }) => { 39 | const { stream, sendEvent } = getContext(); 40 | sendEvent(readDirEvent.with([dir, 0])); 41 | await stream 42 | .until(() => locks.length > 0 && locks.every((l) => l.finish)) 43 | .toArray(); 44 | return stopEvent.with(); 45 | }); 46 | 47 | const als = new AsyncLocalStorage<{ 48 | finish: boolean; 49 | }>(); 50 | fileParseWorkflow.handle([readDirEvent], async ({ data: [dir, tab] }) => { 51 | getContext().sendEvent(messageEvent.with(dir)); 52 | const { sendEvent } = getContext(); 53 | const items = await readdir(dir); 54 | getContext().state.output += " ".repeat(tab) + dir + "\n"; 55 | await Promise.all( 56 | items.map(async (item) => { 57 | const filePath = resolve(dir, item); 58 | if (filePath.includes("node_modules")) { 59 | return; 60 | } 61 | const s = await stat(filePath); 62 | let lock = { 63 | finish: false, 64 | }; 65 | if (s.isFile()) { 66 | als.run(lock, () => sendEvent(readFileEvent.with([filePath, tab + 2]))); 67 | locks.push(lock); 68 | } else if (s.isDirectory()) { 69 | als.run(lock, () => sendEvent(readDirEvent.with([filePath, tab + 2]))); 70 | locks.push(lock); 71 | } 72 | }), 73 | ); 74 | const lock = als.getStore(); 75 | if (lock) { 76 | lock.finish = true; 77 | } 78 | return readResultEvent.with(); 79 | }); 80 | 81 | fileParseWorkflow.handle([readFileEvent], async ({ data: [filePath, tab] }) => { 82 | const lock = als.getStore(); 83 | if (lock) { 84 | lock.finish = true; 85 | } 86 | getContext().sendEvent(messageEvent.with(filePath)); 87 | getContext().state.output += " ".repeat(tab) + filePath + "\n"; 88 | return readResultEvent.with(); 89 | }); 90 | -------------------------------------------------------------------------------- /demo/workflows/human-in-the-loop.ts: -------------------------------------------------------------------------------- 1 | import { 2 | withSnapshot, 3 | request, 4 | } from "@llamaindex/workflow-core/middleware/snapshot"; 5 | import { 6 | createWorkflow, 7 | workflowEvent, 8 | getContext, 9 | } from "@llamaindex/workflow-core"; 10 | import { OpenAI } from "openai"; 11 | 12 | const openai = new OpenAI(); 13 | 14 | const workflow = withSnapshot(createWorkflow()); 15 | 16 | const startEvent = workflowEvent({ 17 | debugLabel: "start", 18 | }); 19 | const humanInteractionEvent = workflowEvent({ 20 | debugLabel: "humanInteraction", 21 | }); 22 | const stopEvent = workflowEvent({ 23 | debugLabel: "stop", 24 | }); 25 | 26 | workflow.handle([startEvent], async ({ data }) => { 27 | const response = await openai.chat.completions.create({ 28 | stream: false, 29 | model: "gpt-4.1", 30 | messages: [ 31 | { 32 | role: "system", 33 | content: `You are a helpful assistant. 34 | If user doesn't provide his/her name, call ask_name tool to ask for user's name. 35 | Otherwise, analyze user's name with a good meaning and return the analysis. 36 | 37 | For example, alex is from "Alexander the Great", who was a king of the ancient Greek kingdom of Macedon and one of history's greatest military minds.`, 38 | }, 39 | { 40 | role: "user", 41 | content: data, 42 | }, 43 | ], 44 | tools: [ 45 | { 46 | type: "function", 47 | function: { 48 | name: "ask_name", 49 | description: "Ask for user's name", 50 | parameters: { 51 | type: "object", 52 | properties: { 53 | message: { 54 | type: "string", 55 | description: "The message to ask for user's name", 56 | }, 57 | }, 58 | required: ["message"], 59 | }, 60 | }, 61 | }, 62 | ], 63 | }); 64 | const tools = response.choices[0].message.tool_calls; 65 | if (tools && tools.length > 0) { 66 | const askName = tools.find((tool) => tool.function.name === "ask_name"); 67 | if (askName) { 68 | return request(humanInteractionEvent, askName.function.arguments); 69 | } 70 | } 71 | return stopEvent.with(response.choices[0].message.content!); 72 | }); 73 | 74 | workflow.handle([humanInteractionEvent], async ({ data }) => { 75 | const { sendEvent } = getContext(); 76 | // going back to the start event 77 | sendEvent(startEvent.with(data)); 78 | }); 79 | 80 | export { workflow, startEvent, humanInteractionEvent, stopEvent }; 81 | -------------------------------------------------------------------------------- /demo/workflows/llama-parse-workflow.ts: -------------------------------------------------------------------------------- 1 | import { workflowEvent, createWorkflow } from "@llamaindex/workflow-core"; 2 | import { z } from "zod"; 3 | import { zodEvent } from "@llamaindex/workflow-core/util/zod"; 4 | import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state"; 5 | import { pRetryHandler } from "@llamaindex/workflow-core/util/p-retry"; 6 | 7 | export const startEvent = zodEvent( 8 | z.object({ 9 | inputFile: z.string().describe("input"), 10 | apiKey: z.string().describe("apiKey"), 11 | }), 12 | ); 13 | const checkStatusEvent = workflowEvent(); 14 | const checkStatusSuccessEvent = workflowEvent(); 15 | export const stopEvent = zodEvent( 16 | z.object({ 17 | markdown: z.string().describe("markdown"), 18 | }), 19 | ); 20 | 21 | const { withState, getContext } = createStatefulMiddleware( 22 | () => 23 | ({}) as { 24 | apiKey: string; 25 | }, 26 | ); 27 | 28 | export const llamaParseWorkflow = withState(createWorkflow()); 29 | 30 | llamaParseWorkflow.handle( 31 | [startEvent], 32 | async ({ data: { inputFile, apiKey } }) => { 33 | getContext().state.apiKey = apiKey; 34 | const { stream, sendEvent } = getContext(); 35 | const { openAsBlob } = await import("node:fs"); 36 | const blob = await openAsBlob(inputFile); 37 | const formData = new FormData(); 38 | formData.append("file", blob); 39 | const { id } = await fetch( 40 | "https://api.cloud.llamaindex.ai/api/v1/parsing/upload", 41 | { 42 | method: "POST", 43 | headers: { 44 | Authorization: `Bearer ${apiKey}`, 45 | }, 46 | body: formData, 47 | }, 48 | ).then((res) => res.json()); 49 | sendEvent(checkStatusEvent.with(id)); 50 | await stream.until(checkStatusSuccessEvent).toArray(); 51 | return fetch( 52 | `https://api.cloud.llamaindex.ai/api/v1/parsing/job/${id}/result/markdown`, 53 | { 54 | method: "GET", 55 | headers: { 56 | Authorization: `Bearer ${apiKey}`, 57 | }, 58 | }, 59 | ).then(async (res) => stopEvent.with(await res.json())); 60 | }, 61 | ); 62 | 63 | llamaParseWorkflow.handle( 64 | [checkStatusEvent], 65 | pRetryHandler( 66 | async ({ data: uuid }) => { 67 | const { status } = await fetch( 68 | `https://api.cloud.llamaindex.ai/api/v1/parsing/job/${uuid}`, 69 | { 70 | method: "GET", 71 | headers: { 72 | Authorization: `Bearer ${getContext().state.apiKey}`, 73 | }, 74 | }, 75 | ).then((res) => res.json()); 76 | if (status === "SUCCESS") { 77 | return checkStatusSuccessEvent.with(); 78 | } 79 | throw new Error(`LLamaParse status: ${status}`); 80 | }, 81 | { 82 | retries: 100, 83 | }, 84 | ), 85 | ); 86 | -------------------------------------------------------------------------------- /demo/workflows/tool-call-agent.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI } from "openai"; 2 | import { 3 | createWorkflow, 4 | getContext, 5 | workflowEvent, 6 | } from "@llamaindex/workflow-core"; 7 | import type { 8 | ChatCompletionMessageToolCall, 9 | ChatCompletionTool, 10 | } from "openai/resources/chat/completions/completions"; 11 | 12 | const llm = new OpenAI(); 13 | const tools = [ 14 | { 15 | function: { 16 | name: "get_weather", 17 | description: "Get Weather Weather", 18 | parameters: { 19 | type: "object", 20 | properties: { 21 | location: { 22 | type: "string", 23 | description: "City and country e.g. Bogotá, Colombia", 24 | }, 25 | }, 26 | required: ["location"], 27 | }, 28 | }, 29 | type: "function", 30 | }, 31 | ] satisfies ChatCompletionTool[]; 32 | 33 | export const startEvent = workflowEvent(); 34 | const chatEvent = workflowEvent(); 35 | const toolCallEvent = workflowEvent(); 36 | const toolCallResultEvent = workflowEvent(); 37 | export const stopEvent = workflowEvent(); 38 | export const toolCallWorkflow = createWorkflow(); 39 | toolCallWorkflow.handle([startEvent], async ({ data }) => { 40 | console.log("start event"); 41 | const context = getContext(); 42 | context.sendEvent(chatEvent.with(data)); 43 | }); 44 | toolCallWorkflow.handle([toolCallEvent], async () => { 45 | console.log("tool call event"); 46 | return toolCallResultEvent.with("Today is sunny."); 47 | }); 48 | toolCallWorkflow.handle([chatEvent], async ({ data }) => { 49 | console.log("chat event"); 50 | const { choices } = await llm.chat.completions.create({ 51 | model: "gpt-4-turbo", 52 | tools, 53 | messages: [ 54 | { 55 | role: "system", 56 | content: "You are a helpful assistant.", 57 | }, 58 | { 59 | role: "user", 60 | content: data, 61 | }, 62 | ], 63 | }); 64 | const { sendEvent, stream } = getContext(); 65 | if ( 66 | choices[0]?.message?.tool_calls?.length && 67 | choices[0].message.tool_calls.length > 0 68 | ) { 69 | console.log("sending choices", choices[0].message.tool_calls); 70 | const result = ( 71 | await Promise.all( 72 | choices[0].message.tool_calls.map(async (tool_call) => { 73 | sendEvent(toolCallEvent.with(tool_call)); 74 | return stream.until(toolCallResultEvent).toArray(); 75 | }), 76 | ) 77 | ) 78 | .map((list) => list.at(-1)!) 79 | .map(({ data }) => data) 80 | .join("\n"); 81 | console.log("toolcall result", result); 82 | sendEvent(chatEvent.with(result)); 83 | } else { 84 | console.log("no choices"); 85 | return stopEvent.with(choices[0]!.message.content!); 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@llamaindex/workflow-docs", 3 | "version": "0.1.1", 4 | "files": [ 5 | "workflows" 6 | ], 7 | "publishConfig": { 8 | "access": "public" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/workflows/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: LlamaIndex Workflows 3 | description: LlamaIndex Workflows is a simple and lightweight engine for JavaScript and TypeScript apps. 4 | --- 5 | 6 | LlamaIndex Workflows are a library for event-driven programming in JavaScript and TypeScript. 7 | It provides a simple and lightweight orchestration solution for building complex workflows with minimal boilerplate. 8 | 9 | It combines [event-driven](#) programming, [async context](#) and [streaming](#) to create a flexible and efficient way to handle data processing tasks. 10 | 11 | The essential concepts of Workflows are: 12 | 13 | - **Events**: are the core building blocks of Workflows. They represent data that flows through the system. 14 | - **Handlers**: are functions that process events and can produce new events. 15 | - **Context**: is the environment in which events are processed. It provides access to the event stream and allows sending new events. 16 | - **Workflow**: is the collection of events, handlers, and context that define the processing logic. 17 | 18 | ## Getting Started 19 | 20 | ```shell 21 | npm i @llamaindex/workflow-core 22 | 23 | yarn add @llamaindex/workflow-core 24 | 25 | pnpm add @llamaindex/workflow-core 26 | 27 | bun add @llamaindex/workflow-core 28 | 29 | deno add npm:@llamaindex/workflow-core 30 | ``` 31 | 32 | ## First Example 33 | 34 | With [workflowEvent](#) and [createWorkflow](#), you can create a simple workflow that processes events. 35 | 36 | ```ts 37 | import { OpenAI } from "openai"; 38 | import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; 39 | 40 | const openai = new OpenAI({ 41 | apiKey: process.env.OPENAI_API_KEY, 42 | }); 43 | 44 | const startEvent = workflowEvent(); 45 | const stopEvent = workflowEvent(); 46 | 47 | const workflow = createWorkflow(); 48 | 49 | workflow.handle([startEvent], async (event) => { 50 | const response = await openai.chat.completions.create({ 51 | // ... 52 | messages: [{ role: "user", content: event.data }], 53 | }); 54 | 55 | return stopEvent(response.choices[0].message.content); 56 | }); 57 | 58 | workflow.handle([stopEvent], (event) => { 59 | console.log("Response:", event.data); 60 | }); 61 | 62 | const { sendEvent } = workflow.createContext(); 63 | sendEvent(startEvent.with("Hello, Workflows!")); 64 | ``` 65 | 66 | ## Parallel processing with async handlers 67 | 68 | Tool calls are a common pattern in LLM applications, where the model generates a call to an external function or API. 69 | 70 | ### With Workflows 71 | 72 | Workflows provide [abort signals](#) and [parallel processing](#) out of the box. 73 | 74 | ```ts 75 | workflow.handle([toolCallEvent], ({ data: { id, name, args } }) => { 76 | const { signal, sendEvent } = getContext(); 77 | signal.onabort = () => 78 | sendEvent( 79 | toolCallResultEvent.with({ 80 | role: "tool", 81 | tool_call_id: id, 82 | content: "ERROR WHILE CALLING FUNCTION" + signal.reason.message, 83 | }), 84 | ); 85 | 86 | const result = callFunction(name, args); 87 | return toolCallResultEvent.with({ 88 | role: "tool", 89 | tool_call_id: id, 90 | content: result, 91 | }); 92 | }); 93 | ``` 94 | 95 | You can collect the results of the tool calls from the stream and send them back to the workflow. 96 | 97 | ```ts 98 | workflow.handle([startEvent], async (event) => { 99 | const { sendEvent, stream } = getContext(); 100 | // ... 101 | if (response.choices[0].message.tool_calls.length > 0) { 102 | response.choices[0].message.tool_calls.map((toolCall) => { 103 | const name = toolCall.function.name; 104 | const args = JSON.parse(toolCall.function.arguments); 105 | sendEvent( 106 | toolCallEvent.with({ 107 | id: toolCall.id, 108 | name, 109 | args, 110 | }), 111 | ); 112 | }); 113 | let counter = 0; 114 | const results = stream 115 | .until(() => counter++ === response.choices[0].message.tool_calls.length) 116 | .filter(toolCallResultEvent) 117 | .toArray(); 118 | return sendEvent( 119 | startEvent.with([...event.data, ...results.map((r) => r.data)]), 120 | ); 121 | } 122 | return stopEvent(response.choices[0].message.content); 123 | }); 124 | ``` 125 | 126 | ```ts 127 | const { sendEvent } = workflow.createContext(); 128 | sendEvent( 129 | startEvent.with([ 130 | { 131 | role: "user", 132 | content: "Hello, Workflows!", 133 | }, 134 | ]), 135 | ); 136 | ``` 137 | 138 | ## Ship to Production easily 139 | 140 | We provide tons of [middleware](#) and [integrations](#) to make it easy to ship your workflows to production. 141 | 142 | ### Hono /w Cloudflare Workers 143 | 144 | ```ts 145 | import { Hono } from "hono"; 146 | import { createHonoHandler } from "@llamaindex/workflow-core/interrupter/hono"; 147 | import { openaiChatWorkflow, startEvent, stopEvent } from "@/lib/workflow"; 148 | 149 | const app = new Hono(); 150 | 151 | app.post( 152 | "/workflow", 153 | createHonoHandler( 154 | openaiChatWorkflow, 155 | async (ctx) => startEvent.with(ctx.req.json()), 156 | stopEvent, 157 | ), 158 | ); 159 | 160 | export default app; 161 | ``` 162 | 163 | ### Next.js 164 | 165 | ```ts 166 | import { createNextHandler } from "@llamaindex/workflow-core/interrupter/next"; 167 | import { openaiChatWorkflow, startEvent, stopEvent } from "@/lib/workflows"; 168 | 169 | export const { GET } = createNextHandler( 170 | openaiChatWorkflow, 171 | async (req) => startEvent.with(req.body), 172 | stopEvent, 173 | ); 174 | ``` 175 | -------------------------------------------------------------------------------- /docs/workflows/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Workflow", 3 | "description": "Event-driven workflow engine", 4 | "root": true, 5 | "defaultOpen": false, 6 | "pages": [ 7 | "index", 8 | "basic-workflow", 9 | "streaming", 10 | "advanced-events", 11 | "llamaindex-integration" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /lint-staged.config.ts: -------------------------------------------------------------------------------- 1 | import type { Configuration } from "lint-staged"; 2 | 3 | export default { 4 | "*.{js,jsx,ts,tsx}": ["prettier --write"], 5 | "*.{json,md,yml}": ["prettier --write"], 6 | } satisfies Configuration; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "packageManager": "pnpm@10.12.4", 7 | "scripts": { 8 | "typecheck": "tsc -b --verbose", 9 | "lint": "prettier . --check", 10 | "lint:fix": "prettier . --write", 11 | "publish": "turbo build --filter=\"./packages/*\" && changeset publish", 12 | "prepare": "husky" 13 | }, 14 | "devDependencies": { 15 | "@changesets/cli": "^2.29.5", 16 | "@edge-runtime/vm": "^5.0.0", 17 | "happy-dom": "^18.0.1", 18 | "husky": "^9.1.7", 19 | "lint-staged": "^16.1.2", 20 | "prettier": "^3.6.1", 21 | "tsdown": "^0.12.9", 22 | "turbo": "^2.5.4", 23 | "vite-tsconfig-paths": "^5.1.4", 24 | "vitest": "^3.2.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @llamaindex/workflow-core 2 | 3 | ## 1.0.0 4 | 5 | ### Major Changes 6 | 7 | - 095fb04: chore: major release 8 | 9 | 🌊 is a simple, lightweight workflow engine, in TypeScript. 10 | 11 | ### First, define events 12 | 13 | ```ts 14 | import { workflowEvent } from "@llamaindex/workflow-core"; 15 | 16 | const startEvent = workflowEvent(); 17 | const stopEvent = workflowEvent<1 | -1>(); 18 | ``` 19 | 20 | ### Connect events with workflow 21 | 22 | ```ts 23 | import { createWorkflow } from "@llamaindex/workflow-core"; 24 | 25 | const convertEvent = workflowEvent(); 26 | 27 | const workflow = createWorkflow(); 28 | 29 | workflow.handle([startEvent], (start) => { 30 | return convertEvent.with(Number.parseInt(start.data, 10)); 31 | }); 32 | workflow.handle([convertEvent], (convert) => { 33 | return stopEvent.with(convert.data > 0 ? 1 : -1); 34 | }); 35 | ``` 36 | 37 | ### Trigger workflow 38 | 39 | ```ts 40 | import { pipeline } from "node:stream/promises"; 41 | 42 | const { stream, sendEvent } = workflow.createContext(); 43 | sendEvent(startEvent.with()); 44 | const allEvents = await stream.until(stopEvent).toArray(); 45 | ``` 46 | 47 | ## 0.4.6 48 | 49 | ### Patch Changes 50 | 51 | - 59b9199: chore: bump version 52 | 53 | ## 0.4.5 54 | 55 | ### Patch Changes 56 | 57 | - 1fb29de: chore: rename package 58 | 59 | ## 0.4.4 60 | 61 | ### Patch Changes 62 | 63 | - 24fe0b2: Add support for zod schemas for events 64 | 65 | ## 0.4.3 66 | 67 | ### Patch Changes 68 | 69 | - 9c17c2e: fix: build and export all middlewares to esm and cjs modules 70 | 71 | ## 0.4.2 72 | 73 | ### Patch Changes 74 | 75 | - 23ecfc7: feat: update http protocol 76 | - 4402a6d: fix: workflow stream extends standard readable stream 77 | - 9c65785: feat: add `withSnapshot` middleware API 78 | 79 | Add snapshot API, for human in the loop feature. The API is designed for cross JavaScript platform, including node.js, browser, and serverless platform such as cloudflare worker and edge runtime 80 | - `workflow.createContext(): Context` 81 | - `context.snapshot(): Promise<[requestEvent, snapshot]>` 82 | - `workflow.resume(data, snapshot)` 83 | 84 | ## 0.4.1 85 | 86 | ### Patch Changes 87 | 88 | - 1005e84: feat: add stream helper 89 | 90 | In this release, we built-in some stream helper (inspired from (TC39 Async Iterator Helpers)[https://github.com/tc39/proposal-async-iterator-helpers]) 91 | - move `@llamaindex/workflow-core/stream/until` into `stream.until` 92 | - move `@llamaindex/workflow-core/stream/filter` into `stream.filter` 93 | - move `@llamaindex/workflow-core/stream/consumer` into `stream.toArray()` 94 | - add `stream.take(limit)` 95 | - add `stream.toArray()` 96 | 97 | ```diff 98 | - import { collect } from "@llamaindex/workflow-core/stream/consumer"; 99 | - import { until } from "@llamaindex/workflow-core/stream/until"; 100 | - import { filter } from "@llamaindex/workflow-core/stream/filter"; 101 | 102 | - const results = await collect( 103 | - until( 104 | - filter( 105 | - stream, 106 | - (ev) => 107 | - processedValidEvent.include(ev) || processedInvalidEvent.include(ev), 108 | - ), 109 | - () => { 110 | - return results.length >= totalItems; 111 | - }, 112 | - ), 113 | - ); 114 | + const results = await stream 115 | + .filter( 116 | + (ev) => 117 | + processedValidEvent.include(ev) || processedInvalidEvent.include(ev), 118 | + ) 119 | + .take(totalItems) 120 | + .toArray(); 121 | ``` 122 | 123 | ## 0.4.0 124 | 125 | ### Minor Changes 126 | 127 | - 1fb2d98: feat: add `createStatefulMiddleware` API 128 | 129 | Remove `withState` API, because its createContext API type is confusing people, 130 | causing people cannot figure out what does state belong to (whether context or workflow instance) 131 | 132 | # Breaking Changes 133 | 134 | ## State Middleware API Changes (formerly Store Middleware) 135 | 136 | The state middleware has been significantly improved with a new API and renamed from "store" to "state". Here are the key changes: 137 | 138 | ### Before 139 | 140 | ```typescript 141 | import { withState } from "@llamaindex/workflow-core/middleware/state"; 142 | 143 | const workflow = withState( 144 | () => ({ 145 | count: 0, 146 | history: [], 147 | }), 148 | createWorkflow(), 149 | ); 150 | 151 | // Access state via getState() 152 | const state = workflow.getState(); 153 | ``` 154 | 155 | ### After 156 | 157 | ```typescript 158 | import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state"; 159 | 160 | const { withState, getContext } = createStatefulMiddleware(() => ({ 161 | count: 0, 162 | history: [], 163 | })); 164 | 165 | const workflow = withState(createWorkflow()); 166 | 167 | workflow.handle([], () => { 168 | const { state } = getContext(); 169 | }); 170 | 171 | // Access state via context.state 172 | const { state } = getContext(); 173 | ``` 174 | 175 | ### Migration Guide 176 | 177 | To migrate existing code: 178 | 1. Replace `withState` import with `createStatefulMiddleware` 179 | 2. Update state initialization to use the new API 180 | 3. Replace `workflow.getState()` calls with `getContext().state` 181 | 4. If using input parameters, update the state initialization accordingly 182 | 5. Update all variable names from `state` to `state` in your code 183 | 184 | The new API provides better type safety, more flexibility with input parameters, and a more consistent way to access the state through the workflow context. 185 | 186 | ## 0.3.10 187 | 188 | ### Patch Changes 189 | 190 | - f59679a: feat: add `event.uniqueId` 191 | - f3206a9: fix: use `subscribable` as the source of truth 192 | - 80066d0: feat: add `WorkflowStream.fromResponse/toResponse` API 193 | - 2a18aca: feat: `stream.on` API 194 | 195 | ```ts 196 | workflow.handle([startEvent], () => { 197 | const { sendEvent } = getContext(); 198 | sendEvent(messageEvent.with("Hello World")); 199 | }); 200 | 201 | const { stream, sendEvent } = workflow.createContext(); 202 | const unsubscribe = stream.on(messageEvent, (event) => { 203 | expect(event.data).toBe("Hello World"); 204 | }); 205 | sendEvent(startEvent.with()); 206 | ``` 207 | 208 | ## 0.3.9 209 | 210 | ### Patch Changes 211 | 212 | - 8f1738a: fix: browser dist 213 | 214 | ## 0.3.8 215 | 216 | ### Patch Changes 217 | 218 | - 89abee2: fix: module exports 219 | 220 | ## 0.3.7 221 | 222 | ### Patch Changes 223 | 224 | - e2f8e23: feat: add rxjs binding 225 | - 78b3141: feat: move third party to top-level 226 | 227 | ## 0.3.6 228 | 229 | ### Patch Changes 230 | 231 | - d8ca6ee: Added helpful utils for e2e runs of workflows 232 | 233 | ## 0.3.5 234 | 235 | ### Patch Changes 236 | 237 | - 6f6cb9d: feat(trace-events): add `getEventOrigins` 238 | -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 LlamaIndex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@llamaindex/workflow-core", 3 | "version": "1.0.0", 4 | "exports": { 5 | ".": "./src/core/index.ts", 6 | "./async-context": "./src/async-context/index.ts", 7 | "./middleware/state": "./src/middleware/state.ts", 8 | "./middleware/trace-events": "./src/middleware/trace-events.ts", 9 | "./middleware/validation": "./src/middleware/validation.ts", 10 | "./util/p-retry": "./src/util/p-retry.ts", 11 | "./util/zod": "./src/util/zod.ts", 12 | "./stream/consumer": "./src/stream/consumer.ts", 13 | "./stream/filter": "./src/stream/filter.ts", 14 | "./stream/find": "./src/stream/find.ts", 15 | "./stream/until": "./src/stream/until.ts" 16 | }, 17 | "publish": { 18 | "include": ["LICENSE", "README.md", "src/**/*.ts"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@llamaindex/workflow-core", 3 | "version": "1.0.0", 4 | "description": "event-based workflow engine", 5 | "type": "module", 6 | "main": "dist/index.cjs", 7 | "types": "dist/index.d.ts", 8 | "module": "dist/index.js", 9 | "browser": "dist/browser/index.js", 10 | "exports": { 11 | ".": { 12 | "import": { 13 | "types": "./dist/index.d.ts", 14 | "default": "./dist/index.js" 15 | }, 16 | "require": { 17 | "types": "./dist/index.d.cts", 18 | "default": "./dist/index.cjs" 19 | }, 20 | "default": { 21 | "types": "./dist/index.d.ts", 22 | "default": "./dist/index.js" 23 | } 24 | }, 25 | "./async-context": { 26 | "browser": { 27 | "types": "./async-context/index.d.ts", 28 | "default": "./async-context/index.browser.js" 29 | }, 30 | "import": { 31 | "types": "./async-context/index.d.ts", 32 | "default": "./async-context/index.js" 33 | }, 34 | "require": { 35 | "types": "./async-context/index.d.cts", 36 | "default": "./async-context/index.cjs" 37 | }, 38 | "default": { 39 | "types": "./async-context/index.d.ts", 40 | "default": "./async-context/index.js" 41 | } 42 | }, 43 | "./hono": { 44 | "types": "./dist/hono.d.ts", 45 | "default": "./dist/hono.js" 46 | }, 47 | "./mcp": { 48 | "types": "./dist/mcp.d.ts", 49 | "default": "./dist/mcp.js" 50 | }, 51 | "./next": { 52 | "types": "./dist/next.d.ts", 53 | "default": "./dist/next.js" 54 | }, 55 | "./observable": { 56 | "types": "./dist/observable.d.ts", 57 | "default": "./dist/observable.js" 58 | }, 59 | "./middleware/state": { 60 | "import": { 61 | "types": "./middleware/state.d.ts", 62 | "default": "./middleware/state.js" 63 | }, 64 | "require": { 65 | "types": "./middleware/state.d.cts", 66 | "default": "./middleware/state.cjs" 67 | }, 68 | "default": { 69 | "types": "./middleware/state.d.ts", 70 | "default": "./middleware/state.js" 71 | } 72 | }, 73 | "./middleware/trace-events": { 74 | "import": { 75 | "types": "./middleware/trace-events.d.ts", 76 | "default": "./middleware/trace-events.js" 77 | }, 78 | "require": { 79 | "types": "./middleware/trace-events.d.cts", 80 | "default": "./middleware/trace-events.cjs" 81 | }, 82 | "default": { 83 | "types": "./middleware/trace-events.d.ts", 84 | "default": "./middleware/trace-events.js" 85 | } 86 | }, 87 | "./middleware/validation": { 88 | "import": { 89 | "types": "./middleware/validation.d.ts", 90 | "default": "./middleware/validation.js" 91 | }, 92 | "require": { 93 | "types": "./middleware/validation.d.cts", 94 | "default": "./middleware/validation.cjs" 95 | }, 96 | "default": { 97 | "types": "./middleware/validation.d.ts", 98 | "default": "./middleware/validation.js" 99 | } 100 | }, 101 | "./middleware/snapshot": { 102 | "import": { 103 | "types": "./middleware/snapshot.d.ts", 104 | "default": "./middleware/snapshot.js" 105 | }, 106 | "require": { 107 | "types": "./middleware/snapshot.d.cts", 108 | "default": "./middleware/snapshot.cjs" 109 | }, 110 | "default": { 111 | "types": "./middleware/snapshot.d.ts", 112 | "default": "./middleware/snapshot.js" 113 | } 114 | }, 115 | "./util/p-retry": { 116 | "types": "./util/p-retry.d.ts", 117 | "default": "./util/p-retry.js" 118 | }, 119 | "./util/zod": { 120 | "types": "./util/zod.d.ts", 121 | "default": "./util/zod.js" 122 | }, 123 | "./stream/consumer": { 124 | "types": "./stream/consumer.d.ts", 125 | "default": "./stream/consumer.js" 126 | }, 127 | "./stream/filter": { 128 | "types": "./stream/filter.d.ts", 129 | "default": "./stream/filter.js" 130 | }, 131 | "./stream/find": { 132 | "types": "./stream/find.d.ts", 133 | "default": "./stream/find.js" 134 | }, 135 | "./stream/until": { 136 | "types": "./stream/until.d.ts", 137 | "default": "./stream/until.js" 138 | }, 139 | "./stream/run": { 140 | "types": "./stream/run.d.ts", 141 | "default": "./stream/run.js" 142 | } 143 | }, 144 | "files": [ 145 | "async-context", 146 | "dist", 147 | "interrupter", 148 | "util", 149 | "middleware", 150 | "stream" 151 | ], 152 | "scripts": { 153 | "build": "rimraf dist interrupter middleware stream util async-context && tsdown", 154 | "dev": "tsdown --watch", 155 | "test": "vitest run", 156 | "test:ui": "vitest --ui", 157 | "prepublishOnly": "cp ../../README.md ./README.md" 158 | }, 159 | "devDependencies": { 160 | "@modelcontextprotocol/sdk": "^1.13.1", 161 | "@types/node": "^24.0.4", 162 | "hono": "^4.8.3", 163 | "next": "^15.3.4", 164 | "p-retry": "^6.2.1", 165 | "rimraf": "^6.0.1", 166 | "rxjs": "^7.8.2", 167 | "stream-chain": "^3.4.0", 168 | "tsdown": "^0.12.9", 169 | "typescript": "^5.8.3", 170 | "zod": "^3.25.67" 171 | }, 172 | "peerDependencies": { 173 | "@modelcontextprotocol/sdk": "^1.7.0", 174 | "hono": "^4.7.4", 175 | "next": "^15.2.2", 176 | "p-retry": "^6.2.1", 177 | "rxjs": "^7.8.2", 178 | "zod": "^3.24.2" 179 | }, 180 | "license": "MIT", 181 | "peerDependenciesMeta": { 182 | "@modelcontextprotocol/sdk": { 183 | "optional": true 184 | }, 185 | "hono": { 186 | "optional": true 187 | }, 188 | "next": { 189 | "optional": true 190 | }, 191 | "p-retry": { 192 | "optional": true 193 | }, 194 | "rxjs": { 195 | "optional": true 196 | }, 197 | "zod": { 198 | "optional": true 199 | } 200 | }, 201 | "repository": { 202 | "type": "git", 203 | "url": "https://github.com/run-llama/workflows-ts.git" 204 | }, 205 | "publishConfig": { 206 | "access": "public" 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /packages/core/src/async-context/index.browser.ts: -------------------------------------------------------------------------------- 1 | export const createAsyncContext = () => { 2 | let currentStore: T | null = null; 3 | return { 4 | /** 5 | * You must call `getContext()` in the top level of the workflow, 6 | * otherwise we will lose the async context of the workflow. 7 | * 8 | * @example 9 | * ``` 10 | * workflow.handle([startEvent], async () => { 11 | * const { stream } = getContext(); // ✅ this is ok 12 | * await fetchData(); 13 | * }); 14 | * 15 | * workflow.handle([startEvent], async () => { 16 | * await fetchData(); 17 | * const { stream } = getContext(); // ❌ this is not ok 18 | * // we have no way 19 | * to know this code was originally part of the workflow 20 | * // w/o AsyncContext 21 | * }); 22 | * ``` 23 | */ 24 | getStore: () => { 25 | if (currentStore === null) { 26 | console.warn( 27 | "Woops! Looks like you are calling `getContext` after `await fn()`. Please move `getContext` to top level of handler.", 28 | ); 29 | } 30 | return currentStore; 31 | }, 32 | run(store: T, fn: () => R) { 33 | currentStore = store; 34 | try { 35 | return fn(); 36 | } finally { 37 | currentStore = null; 38 | } 39 | }, 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/core/src/async-context/index.ts: -------------------------------------------------------------------------------- 1 | import { AsyncLocalStorage } from "node:async_hooks"; 2 | 3 | export const createAsyncContext = () => { 4 | const als = new AsyncLocalStorage(); 5 | return { 6 | getStore: () => als.getStore(), 7 | run(store: T, fn: () => R) { 8 | return als.run(store, fn); 9 | }, 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/core/src/core/event.ts: -------------------------------------------------------------------------------- 1 | declare const opaqueSymbol: unique symbol; 2 | 3 | const eventMap = new WeakMap, WeakSet>(); 4 | const refMap = new WeakMap, WorkflowEvent>(); 5 | let i = 0; 6 | let j = 0; 7 | 8 | export type InferWorkflowEventData = 9 | T extends WorkflowEventData 10 | ? U 11 | : T extends WorkflowEvent 12 | ? U 13 | : never; 14 | 15 | export type WorkflowEventData = { 16 | get data(): Data; 17 | } & { readonly [opaqueSymbol]: DebugLabel }; 18 | 19 | export type WorkflowEvent = { 20 | /** 21 | * This is the label used for debugging purposes. 22 | */ 23 | debugLabel?: DebugLabel; 24 | /** 25 | * This is the unique identifier for the event, which is used for sharing cross the network boundaries. 26 | */ 27 | readonly uniqueId: string; 28 | with(data: Data): WorkflowEventData; 29 | include(event: unknown): event is WorkflowEventData; 30 | } & { readonly [opaqueSymbol]: DebugLabel }; 31 | 32 | export type WorkflowEventConfig = { 33 | debugLabel?: DebugLabel; 34 | uniqueId?: string; 35 | }; 36 | 37 | export const workflowEvent = ( 38 | config?: WorkflowEventConfig, 39 | ): WorkflowEvent => { 40 | const l1 = `${i++}`; 41 | const event = { 42 | debugLabel: config?.debugLabel ?? l1, 43 | include: ( 44 | instance: WorkflowEventData, 45 | ): instance is WorkflowEventData => s.has(instance), 46 | with: (data: Data) => { 47 | const l2 = `${j++}`; 48 | const ref = { 49 | [Symbol.toStringTag]: 50 | config?.debugLabel ?? `WorkflowEvent(${l1}.${l2})`, 51 | toString: () => 52 | config?.debugLabel ? config.debugLabel : `WorkflowEvent(${l1}.${l2})`, 53 | toJSON: () => { 54 | return { 55 | type: config?.debugLabel ? config.debugLabel : l1, 56 | data, 57 | }; 58 | }, 59 | get data() { 60 | return data; 61 | }, 62 | } as unknown as WorkflowEventData; 63 | s.add(ref); 64 | refMap.set(ref, event); 65 | return ref; 66 | }, 67 | } as unknown as WorkflowEvent; 68 | 69 | const s = new WeakSet(); 70 | eventMap.set(event, s); 71 | 72 | Object.defineProperty(event, Symbol.toStringTag, { 73 | get: () => event?.debugLabel ?? `WorkflowEvent<${l1}>`, 74 | }); 75 | 76 | Object.defineProperty(event, "displayName", { 77 | value: event?.debugLabel ?? `WorkflowEvent<${l1}>`, 78 | }); 79 | 80 | let uniqueId = config?.uniqueId; 81 | 82 | Object.defineProperty(event, "uniqueId", { 83 | get: () => { 84 | if (!uniqueId) { 85 | uniqueId = l1; 86 | } 87 | return uniqueId; 88 | }, 89 | set: () => { 90 | throw new Error("uniqueId is readonly"); 91 | }, 92 | }); 93 | 94 | event.toString = () => config?.debugLabel ?? `WorkflowEvent<${l1}>`; 95 | return event; 96 | }; 97 | 98 | // utils 99 | export const isWorkflowEvent = ( 100 | instance: unknown, 101 | ): instance is WorkflowEvent => 102 | typeof instance === "object" && instance !== null 103 | ? eventMap.has(instance as any) 104 | : false; 105 | export const isWorkflowEventData = ( 106 | instance: unknown, 107 | ): instance is WorkflowEventData => 108 | typeof instance === "object" && instance !== null 109 | ? refMap.has(instance as any) 110 | : false; 111 | export const eventSource = ( 112 | instance: unknown, 113 | ): WorkflowEvent | undefined => 114 | typeof instance === "object" && instance !== null 115 | ? refMap.get(instance as any) 116 | : undefined; 117 | -------------------------------------------------------------------------------- /packages/core/src/core/index.ts: -------------------------------------------------------------------------------- 1 | // workflow 2 | export { createWorkflow, type Workflow } from "./workflow"; 3 | // context 4 | export { getContext, type WorkflowContext, type Handler } from "./context"; 5 | // event system 6 | export { 7 | isWorkflowEvent, 8 | isWorkflowEventData, 9 | eventSource, 10 | workflowEvent, 11 | type WorkflowEvent, 12 | type WorkflowEventData, 13 | type WorkflowEventConfig, 14 | type InferWorkflowEventData, 15 | } from "./event"; 16 | // stream 17 | export { WorkflowStream } from "./stream"; 18 | -------------------------------------------------------------------------------- /packages/core/src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import type { WorkflowEvent, WorkflowEventData } from "./event"; 2 | 3 | export const isEventData = (data: unknown): data is WorkflowEventData => 4 | data != null && typeof data === "object" && "data" in data; 5 | 6 | export const isPromiseLike = (value: unknown): value is PromiseLike => 7 | value != null && typeof (value as PromiseLike).then === "function"; 8 | 9 | export function flattenEvents( 10 | acceptEventTypes: WorkflowEvent[], 11 | inputEventData: WorkflowEventData[], 12 | ): WorkflowEventData[] { 13 | const acceptance: WorkflowEventData[] = new Array( 14 | acceptEventTypes.length, 15 | ); 16 | for (const eventData of inputEventData) { 17 | for (let i = 0; i < acceptEventTypes.length; i++) { 18 | if (acceptance[i]) { 19 | continue; 20 | } 21 | if (acceptEventTypes[i]!.include(eventData)) { 22 | acceptance[i] = eventData; 23 | break; 24 | } 25 | } 26 | } 27 | return acceptance.filter(Boolean); 28 | } 29 | 30 | export type Subscribable = { 31 | subscribe: (callback: (...args: Args) => R) => () => void; 32 | publish: (...args: Args) => unknown[]; 33 | }; 34 | 35 | const __internal__subscribesSourcemap = new WeakMap< 36 | Subscribable, 37 | Set<(...args: any[]) => any> 38 | >(); 39 | 40 | /** 41 | * @internal 42 | */ 43 | export function getSubscribers( 44 | subscribable: Subscribable, 45 | ): Set<(...args: Args) => R> { 46 | return __internal__subscribesSourcemap.get(subscribable)!; 47 | } 48 | 49 | /** 50 | * @internal 51 | */ 52 | export function createSubscribable< 53 | FnOrArgs extends ((...args: any[]) => any) | any[], 54 | R = unknown, 55 | >(): FnOrArgs extends (...args: any[]) => any 56 | ? Subscribable, ReturnType> 57 | : FnOrArgs extends any[] 58 | ? Subscribable 59 | : never { 60 | const subscribers = new Set<(...args: any) => any>(); 61 | const obj = { 62 | subscribe: (callback: (...args: any) => any) => { 63 | subscribers.add(callback); 64 | return () => { 65 | subscribers.delete(callback); 66 | }; 67 | }, 68 | publish: (...args: any) => { 69 | const results: unknown[] = []; 70 | for (const callback of subscribers) { 71 | results.push(callback(...args)); 72 | } 73 | return results; 74 | }, 75 | }; 76 | __internal__subscribesSourcemap.set(obj, subscribers); 77 | return obj as any; 78 | } 79 | -------------------------------------------------------------------------------- /packages/core/src/core/workflow.ts: -------------------------------------------------------------------------------- 1 | import { type WorkflowEvent, type WorkflowEventData } from "./event"; 2 | import { createContext, type Handler, type WorkflowContext } from "./context"; 3 | 4 | export type Workflow = { 5 | handle< 6 | const AcceptEvents extends WorkflowEvent[], 7 | Result extends ReturnType["with"]> | void, 8 | >( 9 | accept: AcceptEvents, 10 | handler: Handler, 11 | ): void; 12 | createContext(): WorkflowContext; 13 | }; 14 | 15 | export const createWorkflow = (): Workflow => { 16 | const config = { 17 | steps: new Map< 18 | WorkflowEvent[], 19 | Set[], WorkflowEventData | void>> 20 | >(), 21 | }; 22 | 23 | return { 24 | handle: < 25 | const AcceptEvents extends WorkflowEvent[], 26 | Result extends ReturnType["with"]> | void, 27 | >( 28 | accept: AcceptEvents, 29 | handler: Handler, 30 | ): void => { 31 | if (config.steps.has(accept)) { 32 | const set = config.steps.get(accept) as Set< 33 | Handler 34 | >; 35 | set.add(handler); 36 | } else { 37 | const set = new Set>(); 38 | set.add(handler); 39 | config.steps.set( 40 | accept, 41 | set as Set< 42 | Handler[], WorkflowEventData | void> 43 | >, 44 | ); 45 | } 46 | }, 47 | createContext() { 48 | return createContext({ 49 | listeners: config.steps, 50 | }); 51 | }, 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /packages/core/src/hono.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Handler } from "hono"; 2 | import type { 3 | Workflow, 4 | WorkflowEvent, 5 | WorkflowEventData, 6 | } from "@llamaindex/workflow-core"; 7 | import { run } from "./stream/run"; 8 | 9 | export const createHonoHandler = ( 10 | workflow: Workflow, 11 | getStart: ( 12 | c: Context, 13 | ) => WorkflowEventData | Promise>, 14 | stopEvent: WorkflowEvent, 15 | wrapStopEvent?: (c: Context, stop: Stop) => Response, 16 | ): Handler => { 17 | if (!wrapStopEvent) { 18 | wrapStopEvent = (c, stop) => { 19 | return c.json(stop as any); 20 | }; 21 | } 22 | return async (c) => { 23 | const results = await run(workflow, await getStart(c)) 24 | .until(stopEvent) 25 | .toArray(); 26 | return wrapStopEvent(c, results.at(-1)!.data); 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/core/src/mcp.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncContext } from "@llamaindex/workflow-core/async-context"; 2 | import { z, type ZodRawShape, type ZodTypeAny } from "zod"; 3 | import type { Workflow, WorkflowEvent } from "@llamaindex/workflow-core"; 4 | import { runWorkflow } from "./stream/run"; 5 | import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; 6 | import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; 7 | 8 | const requestHandlerExtraAsyncLocalStorage = 9 | createAsyncContext>(); 10 | 11 | export const getReqHandlerExtra = () => { 12 | const extra = requestHandlerExtraAsyncLocalStorage.getStore(); 13 | if (!extra) { 14 | throw new Error("Request handler extra not set"); 15 | } 16 | return extra; 17 | }; 18 | 19 | export function mcpTool< 20 | Args extends ZodRawShape, 21 | Start extends z.objectOutputType, 22 | Stop extends CallToolResult, 23 | >( 24 | workflow: Workflow, 25 | start: WorkflowEvent, 26 | stop: WorkflowEvent, 27 | ): ( 28 | args: Start, 29 | extra: RequestHandlerExtra, 30 | ) => CallToolResult | Promise { 31 | return async (args, extra) => 32 | requestHandlerExtraAsyncLocalStorage.run(extra, async () => { 33 | const { data } = await runWorkflow(workflow, start.with(args), stop); 34 | return data; 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/middleware/snapshot/stable-hash.ts: -------------------------------------------------------------------------------- 1 | // Ref: https://github.com/shuding/stable-hash/blob/main/src/index.ts 2 | export function createStableHash() { 3 | // Use WeakMap to store the object-key mapping so the objects can still be 4 | // garbage collected. WeakMap uses a hashtable under the hood, so the lookup 5 | // complexity is almost O(1). 6 | const table = new WeakMap(); 7 | 8 | // A counter of the key. 9 | let counter = 0; 10 | 11 | // A stable hash implementation that supports: 12 | // - Fast and ensures unique hash properties 13 | // - Handles unserializable values 14 | // - Handles object key ordering 15 | // - Generates short results 16 | // 17 | // This is not a serialization function, and the result is not guaranteed to be 18 | // parsable. 19 | return function stableHash(arg: any): string { 20 | const type = typeof arg; 21 | const constructor = arg && arg.constructor; 22 | const isDate = constructor == Date; 23 | 24 | if (Object(arg) === arg && !isDate && constructor != RegExp) { 25 | // Object/function, not null/date/regexp. Use WeakMap to store the id first. 26 | // If it's already hashed, directly return the result. 27 | let result = table.get(arg); 28 | if (result) return result; 29 | // Store the hash first for circular reference detection before entering the 30 | // recursive `stableHash` calls. 31 | // For other objects like set and map, we use this id directly as the hash. 32 | result = ++counter + "~"; 33 | table.set(arg, result); 34 | let index: any; 35 | 36 | if (constructor == Array) { 37 | // Array. 38 | result = "@"; 39 | for (index = 0; index < arg.length; index++) { 40 | result += stableHash(arg[index]) + ","; 41 | } 42 | table.set(arg, result); 43 | } else if (constructor == Object) { 44 | // Object, sort keys. 45 | result = "#"; 46 | const keys = Object.keys(arg).sort(); 47 | while ((index = keys.pop() as string) !== undefined) { 48 | if (arg[index] !== undefined) { 49 | result += index + ":" + stableHash(arg[index]) + ","; 50 | } 51 | } 52 | table.set(arg, result); 53 | } 54 | return result; 55 | } 56 | if (isDate) return arg.toJSON(); 57 | if (type == "symbol") return arg.toString(); 58 | return type == "string" ? JSON.stringify(arg) : "" + arg; 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /packages/core/src/middleware/state.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Workflow, 3 | Workflow as WorkflowCore, 4 | WorkflowContext, 5 | } from "@llamaindex/workflow-core"; 6 | import { getContext } from "@llamaindex/workflow-core"; 7 | 8 | type WithState = Input extends void | undefined 9 | ? { 10 | ( 11 | workflow: Workflow, 12 | ): Omit & { 13 | createContext(): ReturnType & { 14 | get state(): State; 15 | }; 16 | }; 17 | } 18 | : { 19 | ( 20 | workflow: Workflow, 21 | ): Omit & { 22 | createContext(input: Input): ReturnType & { 23 | get state(): State; 24 | }; 25 | }; 26 | }; 27 | 28 | type CreateState = { 29 | getContext(): Context & { 30 | get state(): State; 31 | }; 32 | withState: WithState; 33 | }; 34 | 35 | export function createStatefulMiddleware< 36 | State, 37 | Input = void, 38 | Context extends WorkflowContext = WorkflowContext, 39 | >(init: (input: Input) => State): CreateState { 40 | return { 41 | getContext: getContext as never, 42 | withState: ((workflow: Workflow) => { 43 | return { 44 | ...workflow, 45 | createContext: (input: Input) => { 46 | const state = init(input); 47 | const context = workflow.createContext(); 48 | context.__internal__call_context.subscribe((_, next) => { 49 | // todo: make sure getContext is consistent with `workflow.createContext` 50 | const context = getContext(); 51 | if (!Reflect.has(context, "state")) { 52 | Object.defineProperty(context, "state", { 53 | get: () => state, 54 | }); 55 | } 56 | next(_); 57 | }); 58 | if (!Reflect.has(context, "state")) { 59 | Object.defineProperty(context, "state", { 60 | get: () => state, 61 | }); 62 | } 63 | return context as any; 64 | }, 65 | }; 66 | }) as unknown as WithState, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /packages/core/src/middleware/store.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Workflow, 3 | Workflow as WorkflowCore, 4 | WorkflowContext, 5 | } from "@llamaindex/workflow-core"; 6 | import { getContext } from "@llamaindex/workflow-core"; 7 | 8 | type WithState = Input extends void | undefined 9 | ? { 10 | ( 11 | workflow: Workflow, 12 | ): Omit & { 13 | createContext(): ReturnType & { 14 | get state(): State; 15 | }; 16 | }; 17 | } 18 | : { 19 | ( 20 | workflow: Workflow, 21 | ): Omit & { 22 | createContext(input: Input): ReturnType & { 23 | get state(): State; 24 | }; 25 | }; 26 | }; 27 | 28 | type CreateState = { 29 | getContext(): Context & { 30 | get state(): State; 31 | }; 32 | withState: WithState; 33 | }; 34 | 35 | export function createStateMiddleware< 36 | State, 37 | Input = void, 38 | Context extends WorkflowContext = WorkflowContext, 39 | >(init: (input: Input) => State): CreateState { 40 | return { 41 | getContext: getContext as never, 42 | withState: ((workflow: Workflow) => { 43 | return { 44 | ...workflow, 45 | createContext: (input: Input) => { 46 | const state = init(input); 47 | const context = workflow.createContext(); 48 | context.__internal__call_context.subscribe((_, next) => { 49 | // todo: make sure getContext is consistent with `workflow.createContext` 50 | const context = getContext(); 51 | if (!Reflect.has(context, "state")) { 52 | Object.defineProperty(context, "state", { 53 | get: () => state, 54 | }); 55 | } 56 | next(_); 57 | }); 58 | if (!Reflect.has(context, "state")) { 59 | Object.defineProperty(context, "state", { 60 | get: () => state, 61 | }); 62 | } 63 | return context as any; 64 | }, 65 | }; 66 | }) as unknown as WithState, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /packages/core/src/middleware/trace-events/create-handler-decorator.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Handler, 3 | WorkflowEvent, 4 | WorkflowEventData, 5 | } from "@llamaindex/workflow-core"; 6 | import type { HandlerContext } from "../../core/context"; 7 | 8 | const namespace = "decorator" as const; 9 | 10 | let counter = 0; 11 | 12 | export const decoratorRegistry = new Map< 13 | string, 14 | { 15 | handlers: WeakSet< 16 | Handler[], WorkflowEventData | void> 17 | >; 18 | debugLabel: string; 19 | getInitialValue: () => any; 20 | onBeforeHandler: ( 21 | handler: Handler[], WorkflowEventData | void>, 22 | handlerContext: Readonly, 23 | metadata: any, 24 | ) => Handler[], WorkflowEventData | void>; 25 | onAfterHandler: (metadata: any) => any; 26 | } 27 | >(); 28 | 29 | export function createHandlerDecorator(config: { 30 | debugLabel?: string; 31 | getInitialValue: () => Metadata; 32 | onBeforeHandler: ( 33 | handler: Handler[], WorkflowEventData | void>, 34 | handlerContext: HandlerContext, 35 | metadata: Metadata, 36 | ) => Handler[], WorkflowEventData | void>; 37 | onAfterHandler: (metadata: Metadata) => Metadata; 38 | }) { 39 | const uid = `${namespace}:${counter++}`; 40 | decoratorRegistry.set(uid, { 41 | handlers: new WeakSet(), 42 | debugLabel: config.debugLabel ?? uid, 43 | getInitialValue: config.getInitialValue, 44 | onAfterHandler: config.onAfterHandler, 45 | onBeforeHandler: config.onBeforeHandler, 46 | }); 47 | return function < 48 | const AcceptEvents extends WorkflowEvent[], 49 | Result extends ReturnType["with"]> | void, 50 | Fn extends Handler, 51 | >(handler: Fn) { 52 | decoratorRegistry 53 | .get(uid)! 54 | .handlers.add( 55 | handler as Handler[], WorkflowEventData | void>, 56 | ); 57 | return handler; 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /packages/core/src/middleware/trace-events/run-once.ts: -------------------------------------------------------------------------------- 1 | import { createHandlerDecorator } from "./create-handler-decorator"; 2 | 3 | const noop: (...args: any[]) => void = function noop() {}; 4 | 5 | export const runOnce = createHandlerDecorator({ 6 | debugLabel: "onceHook", 7 | getInitialValue: () => false, 8 | onBeforeHandler: (handler, _, tracked) => (tracked ? noop : handler), 9 | onAfterHandler: () => true, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/core/src/middleware/validation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getContext, 3 | type Handler, 4 | type Workflow, 5 | type WorkflowEvent, 6 | type WorkflowEventData, 7 | } from "@llamaindex/workflow-core"; 8 | 9 | export type ValidationHandler< 10 | Validation extends [ 11 | inputs: WorkflowEvent[], 12 | output: WorkflowEvent[], 13 | ][], 14 | AcceptEvents extends WorkflowEvent[], 15 | Result extends WorkflowEventData | void, 16 | > = ( 17 | sendEvent: ( 18 | ...inputs: Array< 19 | Validation[number] extends infer Tuple 20 | ? Tuple extends [AcceptEvents, infer Outputs] 21 | ? Outputs extends WorkflowEvent[] 22 | ? ReturnType 23 | : never 24 | : never 25 | : never 26 | > 27 | ) => void, 28 | ...events: { 29 | [K in keyof AcceptEvents]: ReturnType; 30 | } 31 | ) => Result | Promise; 32 | 33 | export type WithValidationWorkflow< 34 | Validation extends [ 35 | inputs: WorkflowEvent[], 36 | output: WorkflowEvent[], 37 | ][], 38 | > = { 39 | strictHandle< 40 | const AcceptEvents extends WorkflowEvent[], 41 | Result extends ReturnType["with"]> | void, 42 | >( 43 | accept: AcceptEvents, 44 | handler: ValidationHandler, 45 | ): void; 46 | }; 47 | 48 | export function withValidation< 49 | const Validation extends [ 50 | inputs: WorkflowEvent[], 51 | outputs: WorkflowEvent[], 52 | ][], 53 | WorkflowLike extends Workflow, 54 | >( 55 | workflow: WorkflowLike, 56 | validation: Validation, 57 | ): WithValidationWorkflow & WorkflowLike { 58 | const createSafeSendEvent = (...events: WorkflowEventData[]) => { 59 | const outputs = validation 60 | .filter(([inputs]) => 61 | inputs.every((input, idx) => input.include(events[idx])), 62 | ) 63 | .map(([_, outputs]) => outputs); 64 | const store = getContext(); 65 | const originalSendEvent = store.sendEvent; 66 | return (...inputs: WorkflowEventData[]) => { 67 | for (let i = 0; i < outputs.length; i++) { 68 | const output = outputs[i]!; 69 | if (output.length === inputs.length) { 70 | if (output.every((e, idx) => e.include(inputs[idx]))) { 71 | return originalSendEvent(...inputs); 72 | } 73 | } 74 | } 75 | console.warn( 76 | "Invalid input detected [%s]", 77 | inputs.map((i) => i.data).join(", "), 78 | ); 79 | return originalSendEvent(...inputs); 80 | }; 81 | }; 82 | return { 83 | ...workflow, 84 | strictHandle: (accept, handler) => { 85 | const wrappedHandler: Handler[], any> = ( 86 | ...events 87 | ) => { 88 | const context = getContext(); 89 | return handler( 90 | (context as any).safeSendEvent, 91 | // @ts-expect-error 92 | ...events, 93 | ); 94 | }; 95 | return workflow.handle(accept, wrappedHandler); 96 | }, 97 | createContext() { 98 | const context = workflow.createContext(); 99 | context.__internal__call_context.subscribe((context, next) => { 100 | (getContext() as any).safeSendEvent = createSafeSendEvent( 101 | ...context.inputs, 102 | ); 103 | next(context); 104 | }); 105 | return context; 106 | }, 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /packages/core/src/next.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import type { 3 | Workflow, 4 | WorkflowEventData, 5 | WorkflowEvent, 6 | } from "@llamaindex/workflow-core"; 7 | import { run } from "./stream/run"; 8 | 9 | type WorkflowAPI = { 10 | GET: (request: NextRequest) => Promise; 11 | }; 12 | 13 | export const createNextHandler = ( 14 | workflow: Workflow, 15 | getStart: ( 16 | request: NextRequest, 17 | ) => WorkflowEventData | Promise>, 18 | stop: WorkflowEvent, 19 | ): WorkflowAPI => { 20 | return { 21 | GET: async (request) => { 22 | const result = await run(workflow, await getStart(request)) 23 | .until(stop) 24 | .toArray(); 25 | return Response.json(result.at(-1)!.data); 26 | }, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/core/src/observable.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from "rxjs"; 2 | import { 3 | type WorkflowEventData, 4 | WorkflowStream, 5 | } from "@llamaindex/workflow-core"; 6 | 7 | export const toObservable = ( 8 | stream: ReadableStream> | WorkflowStream, 9 | ): Observable> => { 10 | return new Observable((subscriber) => { 11 | const reader = stream.getReader(); 12 | 13 | const read = async () => { 14 | try { 15 | const { done, value } = await reader.read(); 16 | if (done) { 17 | subscriber.complete(); 18 | } else { 19 | subscriber.next(value); 20 | read(); 21 | } 22 | } catch (error) { 23 | subscriber.error(error); 24 | } 25 | }; 26 | 27 | read().catch(subscriber.error); 28 | 29 | return () => { 30 | reader.cancel().catch(subscriber.error); 31 | }; 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/core/src/stream/consumer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type WorkflowEventData, 3 | WorkflowStream, 4 | } from "@llamaindex/workflow-core"; 5 | 6 | const noopStream = new WritableStream({ 7 | write: () => { 8 | // no-op 9 | }, 10 | }); 11 | 12 | /** 13 | * A no-op function that consumes a stream of events and does nothing with them. 14 | * 15 | * Do not collect the raw stream from `workflow.createContext()` 16 | * or `getContext()`, it's infinite and will never finish 17 | * 18 | * @deprecated uss `await stream.toArray()` instead 19 | */ 20 | export const nothing = async ( 21 | stream: ReadableStream | WorkflowStream, 22 | ): Promise => { 23 | await stream.pipeTo(noopStream); 24 | }; 25 | 26 | /** 27 | * Collects all events from a stream and returns them as an array. 28 | * 29 | * Do not collect the raw stream from `workflow.createContext()` 30 | * or getContext()`, it's infinite and will never finish. 31 | * 32 | * @deprecated uss `await stream.toArray()` instead 33 | */ 34 | export const collect = async >( 35 | stream: ReadableStream | WorkflowStream, 36 | ): Promise[]> => { 37 | const events: WorkflowEventData[] = []; 38 | await stream.pipeTo( 39 | new WritableStream({ 40 | write: (event: WorkflowEventData) => { 41 | events.push(event); 42 | }, 43 | }), 44 | ); 45 | return events; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/core/src/stream/filter.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | WorkflowEvent, 3 | WorkflowEventData, 4 | WorkflowStream, 5 | } from "@llamaindex/workflow-core"; 6 | 7 | /** 8 | * @deprecated use `stream.filter` instead. This will be removed in the next minor version. 9 | */ 10 | export function filter< 11 | Event extends WorkflowEventData, 12 | Final extends Event, 13 | >( 14 | stream: ReadableStream | WorkflowStream, 15 | cond: (event: Event) => event is Final, 16 | ): ReadableStream | WorkflowStream { 17 | return stream.pipeThrough( 18 | new TransformStream({ 19 | transform(event, controller) { 20 | if (cond(event)) { 21 | controller.enqueue(event); 22 | } 23 | }, 24 | }), 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/stream/find.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type WorkflowEvent, 3 | type WorkflowEventData, 4 | WorkflowStream, 5 | } from "@llamaindex/workflow-core"; 6 | 7 | /** 8 | * Consume a stream of events with a given event and time. 9 | */ 10 | export async function find( 11 | stream: ReadableStream> | WorkflowStream, 12 | event: WorkflowEvent, 13 | ): Promise> { 14 | const reader = stream.getReader(); 15 | let result = await reader.read(); 16 | while (!result.done) { 17 | const ev = result.value; 18 | if (event.include(ev)) { 19 | reader.releaseLock(); 20 | return ev; 21 | } 22 | result = await reader.read(); 23 | } 24 | throw new Error(`Event ${event.toString()} not found`); 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/stream/run.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Workflow, 3 | WorkflowEvent, 4 | WorkflowEventData, 5 | } from "@llamaindex/workflow-core"; 6 | 7 | /** 8 | * Runs a workflow with the provided events and returns the resulting stream. 9 | * 10 | * ```ts 11 | * const events = await run(workflow, startEvent.with("42")).toArray(); 12 | * ``` 13 | * 14 | */ 15 | export function run( 16 | workflow: Workflow, 17 | events: WorkflowEventData | WorkflowEventData[], 18 | ) { 19 | const { stream, sendEvent } = workflow.createContext(); 20 | sendEvent(...(Array.isArray(events) ? events : [events])); 21 | return stream; 22 | } 23 | 24 | /** 25 | * Runs a workflow with a specified input event and returns the first matching event of the specified output type. 26 | * 27 | * @deprecated Use `stream.until().toArray()` for a more idiomatic approach. 28 | * @example 29 | * ```ts 30 | * const result = await runWorkflow(workflow, startEvent.with("42"), stopEvent); 31 | * console.log(`Result: ${result.data === 1 ? 'positive' : 'negative'}`); 32 | * ``` 33 | */ 34 | export async function runWorkflow( 35 | workflow: Workflow, 36 | inputEvent: WorkflowEventData, 37 | outputEvent: WorkflowEvent, 38 | ): Promise> { 39 | const { stream, sendEvent } = workflow.createContext(); 40 | 41 | // Send the initial event 42 | sendEvent(inputEvent); 43 | 44 | // Create a stream until we get the output event 45 | const result = (await stream.until(outputEvent).toArray()).at(-1); 46 | if (!result) { 47 | throw new Error("No output event received"); 48 | } 49 | return result as WorkflowEventData; 50 | } 51 | 52 | /** 53 | * Runs a workflow with a specified input event and collects all events until a specified output event is encountered. 54 | * Returns an array containing all events including the final output event. 55 | * 56 | * @deprecated Use `stream.until().toArray()` for a more idiomatic approach. 57 | * @example 58 | * ```ts 59 | * const allEvents = await runAndCollect(workflow, startEvent.with("42"), stopEvent); 60 | * const finalEvent = allEvents[allEvents.length - 1]; 61 | * console.log(`Result: ${finalEvent.data === 1 ? 'positive' : 'negative'}`); 62 | * ``` 63 | */ 64 | export async function runAndCollect( 65 | workflow: Workflow, 66 | inputEvent: WorkflowEventData, 67 | outputEvent: WorkflowEvent, 68 | ): Promise[]> { 69 | const { stream, sendEvent } = workflow.createContext(); 70 | 71 | // Send the initial event 72 | sendEvent(inputEvent); 73 | 74 | // Collect all events until the output event 75 | return await stream.until(outputEvent).toArray(); 76 | } 77 | 78 | /** 79 | * Runs a workflow with a specified input event and returns an async iterable stream of all events 80 | * until a specified output event is encountered. 81 | * 82 | * This allows processing events one by one without collecting them all upfront. 83 | * 84 | * @deprecated Use `stream.until().toArray()` for a more idiomatic approach. 85 | * @example 86 | * ```ts 87 | * const eventStream = runStream(workflow, startEvent.with("42"), stopEvent); 88 | * for await (const event of eventStream) { 89 | * console.log(`Processing event: ${event}`); 90 | * // Do something with each event as it arrives 91 | * } 92 | * ``` 93 | */ 94 | export function runStream( 95 | workflow: Workflow, 96 | inputEvent: WorkflowEventData, 97 | outputEvent: WorkflowEvent, 98 | ): AsyncIterable> { 99 | const { stream, sendEvent } = workflow.createContext(); 100 | 101 | // Send the initial event 102 | sendEvent(inputEvent); 103 | 104 | // Return the stream that runs until the output event is encountered 105 | return stream.until(outputEvent).values(); 106 | } 107 | -------------------------------------------------------------------------------- /packages/core/src/stream/until.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type WorkflowEvent, 3 | type WorkflowEventData, 4 | WorkflowStream, 5 | } from "@llamaindex/workflow-core"; 6 | 7 | const isWorkflowEvent = (value: unknown): value is WorkflowEvent => 8 | value != null && 9 | typeof value === "object" && 10 | "with" in value && 11 | "include" in value; 12 | 13 | /** 14 | * @deprecated use `stream.until` instead. This will be removed in the next minor version. 15 | */ 16 | export function until( 17 | stream: WorkflowStream | ReadableStream>, 18 | cond: (event: WorkflowEventData) => boolean | Promise, 19 | ): WorkflowStream; 20 | export function until( 21 | stream: WorkflowStream | ReadableStream>, 22 | cond: WorkflowEvent, 23 | ): WorkflowStream; 24 | export function until( 25 | stream: WorkflowStream | ReadableStream>, 26 | cond: 27 | | ((event: WorkflowEventData) => boolean | Promise) 28 | | WorkflowEvent, 29 | ): WorkflowStream> { 30 | let reader: ReadableStreamDefaultReader> | null = null; 31 | return WorkflowStream.fromReadableStream( 32 | new ReadableStream>({ 33 | start: () => { 34 | reader = stream.getReader(); 35 | }, 36 | pull: async (controller) => { 37 | const { done, value } = await reader!.read(); 38 | if (value) { 39 | controller.enqueue(value); 40 | } 41 | if (done) { 42 | reader!.releaseLock(); 43 | reader = null; 44 | controller.close(); 45 | } else { 46 | if (isWorkflowEvent(cond) && cond.include(value)) { 47 | reader!.releaseLock(); 48 | controller.close(); 49 | } else if (typeof cond === "function" && (await cond(value))) { 50 | reader!.releaseLock(); 51 | controller.close(); 52 | } 53 | } 54 | }, 55 | }), 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/util/p-retry.ts: -------------------------------------------------------------------------------- 1 | import type { Handler, WorkflowEvent } from "@llamaindex/workflow-core"; 2 | import type { Options } from "p-retry"; 3 | import pRetry from "p-retry"; 4 | 5 | export function pRetryHandler< 6 | const AcceptEvents extends WorkflowEvent[], 7 | Result extends ReturnType["with"]> | void, 8 | >( 9 | handler: Handler, 10 | options: Options, 11 | ): Handler { 12 | return async ( 13 | ...args: Parameters> 14 | ): Promise => { 15 | const fn = () => handler(...args); 16 | return pRetry(fn, options); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/util/zod.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { 3 | workflowEvent, 4 | type WorkflowEvent, 5 | type WorkflowEventConfig, 6 | } from "@llamaindex/workflow-core"; 7 | 8 | export const zodEvent = ( 9 | schema: z.ZodType, 10 | config?: WorkflowEventConfig, 11 | ): WorkflowEvent & { readonly schema: z.ZodType } => { 12 | const event = workflowEvent(config); 13 | const originalWith = event.with; 14 | 15 | return Object.assign(event, { 16 | with: (data: T) => { 17 | schema.parse(data); 18 | return originalWith(data); 19 | }, 20 | get schema() { 21 | return schema; 22 | }, 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/core/tests/core/abort-signal.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from "vitest"; 2 | import { 3 | createWorkflow, 4 | getContext, 5 | workflowEvent, 6 | } from "@llamaindex/workflow-core"; 7 | 8 | describe("abort signal", () => { 9 | test("basic", () => { 10 | const startEvent = workflowEvent(); 11 | const workflow = createWorkflow(); 12 | 13 | const err = new Error("1"); 14 | workflow.handle([startEvent], () => { 15 | throw err; 16 | }); 17 | { 18 | const { sendEvent, signal } = workflow.createContext(); 19 | signal.onabort = vi.fn(() => { 20 | expect(signal.reason).toBe(err); 21 | }); 22 | sendEvent(startEvent.with()); 23 | expect(signal.onabort).toBeCalled(); 24 | } 25 | { 26 | const { sendEvent, signal } = workflow.createContext(); 27 | signal.onabort = vi.fn(() => { 28 | expect(signal.reason).toBe(err); 29 | }); 30 | sendEvent(startEvent.with()); 31 | expect(signal.onabort).toBeCalled(); 32 | } 33 | }); 34 | 35 | test("only inner signal called", () => { 36 | const startEvent = workflowEvent(); 37 | const workflow = createWorkflow(); 38 | 39 | const err = new Error("1"); 40 | let handlerSignal: AbortSignal; 41 | workflow.handle([startEvent], () => { 42 | const { signal } = getContext(); 43 | handlerSignal = signal; 44 | signal.onabort = vi.fn(() => { 45 | expect(signal.reason).toBe(err); 46 | }); 47 | throw err; 48 | }); 49 | 50 | const { sendEvent, signal } = workflow.createContext(); 51 | signal.onabort = vi.fn(() => { 52 | expect(signal.reason).toBe(err); 53 | }); 54 | sendEvent(startEvent.with()); 55 | expect(signal.onabort).not.toBeCalled(); 56 | expect(handlerSignal!.onabort).toBeCalled(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/core/tests/core/context-api.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from "vitest"; 2 | import { 3 | createWorkflow, 4 | eventSource, 5 | getContext, 6 | workflowEvent, 7 | type WorkflowEventData, 8 | } from "@llamaindex/workflow-core"; 9 | 10 | describe("workflow context api", () => { 11 | const startEvent = workflowEvent({ 12 | debugLabel: "startEvent", 13 | }); 14 | const stopEvent = workflowEvent({ 15 | debugLabel: "stopEvent", 16 | }); 17 | 18 | test("should work in loop", async () => { 19 | const startEvent = workflowEvent({ 20 | debugLabel: "startEvent", 21 | }); 22 | const stopEvent = workflowEvent<1 | -1>({ 23 | debugLabel: "stopEvent", 24 | }); 25 | const workflow = createWorkflow(); 26 | const parseEvent = workflowEvent({ 27 | debugLabel: "parseEvent", 28 | }); 29 | const parseResultEvent = workflowEvent({ 30 | debugLabel: "parseResult", 31 | }); 32 | workflow.handle([startEvent], async () => { 33 | const { sendEvent, stream } = getContext(); 34 | const ev = parseEvent.with(2); 35 | sendEvent(ev); 36 | await stream 37 | .until((e) => parseResultEvent.include(e) && e.data === 0) 38 | .toArray(); 39 | return stopEvent.with(1); 40 | }); 41 | workflow.handle([parseEvent], async ({ data }) => { 42 | if (data > 0) { 43 | const ev = parseEvent.with(data - 1); 44 | getContext().sendEvent(ev); 45 | } else { 46 | return parseResultEvent.with(0); 47 | } 48 | }); 49 | const { stream, sendEvent } = workflow.createContext(); 50 | sendEvent(startEvent.with("100")); 51 | const events: WorkflowEventData[] = await stream 52 | .until(stopEvent) 53 | .toArray(); 54 | expect(events.length).toBe(6); 55 | expect(events.at(-1)!.data).toBe(1); 56 | expect(events.map((e) => eventSource(e))).toEqual([ 57 | startEvent, 58 | parseEvent, 59 | parseEvent, 60 | parseEvent, 61 | parseResultEvent, 62 | stopEvent, 63 | ]); 64 | }); 65 | 66 | test("multiple parse", async () => { 67 | const startEvent = workflowEvent({ 68 | debugLabel: "startEvent", 69 | }); 70 | const stopEvent = workflowEvent<1 | -1>({ 71 | debugLabel: "stopEvent", 72 | }); 73 | const workflow = createWorkflow(); 74 | const parseEvent = workflowEvent({ 75 | debugLabel: "parseEvent", 76 | }); 77 | const parseResultEvent = workflowEvent({ 78 | debugLabel: "parseResult", 79 | }); 80 | workflow.handle([startEvent], async () => { 81 | const { sendEvent, stream } = getContext(); 82 | const ev = parseEvent.with(2); 83 | sendEvent(ev); 84 | await stream 85 | .until((e) => parseResultEvent.include(e) && e.data === 0) 86 | .toArray(); 87 | sendEvent(ev); 88 | await stream 89 | .until((e) => parseResultEvent.include(e) && e.data === 0) 90 | .toArray(); 91 | return stopEvent.with(1); 92 | }); 93 | workflow.handle([parseEvent], async ({ data }) => { 94 | const { sendEvent } = getContext(); 95 | if (data > 0) { 96 | const ev = parseEvent.with(data - 1); 97 | sendEvent(ev); 98 | } else { 99 | return parseResultEvent.with(0); 100 | } 101 | }); 102 | 103 | const { stream, sendEvent } = workflow.createContext(); 104 | sendEvent(startEvent.with("100")); 105 | const events: WorkflowEventData[] = await stream 106 | .until(stopEvent) 107 | .toArray(); 108 | expect(events.length).toBe(10); 109 | expect(events.at(-1)!.data).toBe(1); 110 | expect(events.map((e) => eventSource(e))).toEqual([ 111 | startEvent, 112 | parseEvent, 113 | parseEvent, 114 | parseEvent, 115 | parseResultEvent, 116 | parseEvent, 117 | parseEvent, 118 | parseEvent, 119 | parseResultEvent, 120 | stopEvent, 121 | ]); 122 | }); 123 | 124 | test("should exist in workflow", async () => { 125 | const workflow = createWorkflow(); 126 | const fn = vi.fn(() => { 127 | const context = getContext(); 128 | expect(context).toBeDefined(); 129 | expect(context.sendEvent).toBeTypeOf("function"); 130 | return stopEvent.with(); 131 | }); 132 | workflow.handle([startEvent], fn); 133 | const { stream, sendEvent } = workflow.createContext(); 134 | sendEvent(startEvent.with()); 135 | const events: WorkflowEventData[] = await stream 136 | .until(stopEvent) 137 | .toArray(); 138 | expect(fn).toBeCalledTimes(1); 139 | expect(events).toHaveLength(2); 140 | }); 141 | 142 | test("should work when request event single", async () => { 143 | const aEvent = workflowEvent({ 144 | debugLabel: "aEvent", 145 | }); 146 | const aResultEvent = workflowEvent({ 147 | debugLabel: "aResultEvent", 148 | }); 149 | const workflow = createWorkflow(); 150 | const fn = vi.fn(async () => { 151 | const context = getContext(); 152 | context.sendEvent(aEvent.with()); 153 | return stopEvent.with(); 154 | }); 155 | const fn2 = vi.fn(async () => { 156 | return aResultEvent.with(); 157 | }); 158 | workflow.handle([startEvent], fn); 159 | workflow.handle([aEvent], fn2); 160 | const { stream, sendEvent } = workflow.createContext(); 161 | sendEvent(startEvent.with()); 162 | const events: WorkflowEventData[] = await stream 163 | .until(stopEvent) 164 | .toArray(); 165 | expect(fn).toBeCalledTimes(1); 166 | expect(events.map((e) => eventSource(e))).toEqual([ 167 | startEvent, 168 | aEvent, 169 | aResultEvent, 170 | stopEvent, 171 | ]); 172 | expect(events).toHaveLength(4); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /packages/core/tests/core/event.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { workflowEvent } from "@llamaindex/workflow-core"; 3 | 4 | describe("event system api", () => { 5 | test("should set unique id as always", () => { 6 | const event = workflowEvent(); 7 | expect(typeof event.uniqueId).toBe("string"); 8 | }); 9 | 10 | test("can config unique id", () => { 11 | const event = workflowEvent({ uniqueId: "test" }); 12 | expect(event.uniqueId).toBe("test"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/core/tests/core/listener.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from "vitest"; 2 | import { 3 | createWorkflow, 4 | getContext, 5 | workflowEvent, 6 | } from "@llamaindex/workflow-core"; 7 | 8 | describe("workflow listener api", () => { 9 | test("can listen message event", () => { 10 | const workflow = createWorkflow(); 11 | const startEvent = workflowEvent(); 12 | const messageEvent = workflowEvent({}); 13 | workflow.handle([startEvent], () => { 14 | const { sendEvent } = getContext(); 15 | sendEvent(messageEvent.with("Hello World")); 16 | }); 17 | 18 | const { stream, sendEvent } = workflow.createContext(); 19 | const callback = vi.fn((event: ReturnType) => { 20 | expect(event.data).toBe("Hello World"); 21 | }); 22 | const unsubscribe = stream.on(messageEvent, callback); 23 | sendEvent(startEvent.with()); 24 | expect(callback).toHaveBeenCalledTimes(1); 25 | unsubscribe(); 26 | sendEvent(startEvent.with()); 27 | expect(callback).toHaveBeenCalledTimes(1); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/core/tests/core/run-helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from "vitest"; 2 | import { 3 | createWorkflow, 4 | eventSource, 5 | getContext, 6 | workflowEvent, 7 | } from "@llamaindex/workflow-core"; 8 | import type { WorkflowEventData } from "@llamaindex/workflow-core"; 9 | import { 10 | runWorkflow, 11 | runAndCollect, 12 | runStream, 13 | } from "@llamaindex/workflow-core/stream/run"; 14 | 15 | describe("workflow helper functions", () => { 16 | test("runWorkflow should execute workflow and return the final event", async () => { 17 | // Setup the workflow 18 | const startEvent = workflowEvent({ 19 | debugLabel: "start", 20 | }); 21 | const intermediateEvent = workflowEvent({ 22 | debugLabel: "intermediate", 23 | }); 24 | const stopEvent = workflowEvent<1 | -1>({ 25 | debugLabel: "stop", 26 | }); 27 | 28 | const workflow = createWorkflow(); 29 | workflow.handle([startEvent], (start) => { 30 | return intermediateEvent.with(Number.parseInt(start.data, 10)); 31 | }); 32 | workflow.handle([intermediateEvent], (convert) => { 33 | return stopEvent.with(convert.data > 0 ? 1 : -1); 34 | }); 35 | 36 | // Run workflow with positive number 37 | const positiveResult = await runWorkflow( 38 | workflow, 39 | startEvent.with("42"), 40 | stopEvent, 41 | ); 42 | expect(positiveResult.data).toBe(1); 43 | 44 | // Run workflow with negative number 45 | const negativeResult = await runWorkflow( 46 | workflow, 47 | startEvent.with("-10"), 48 | stopEvent, 49 | ); 50 | expect(negativeResult.data).toBe(-1); 51 | }); 52 | 53 | test("runAndCollect should return all events including the final event", async () => { 54 | // Setup the workflow 55 | const startEvent = workflowEvent({ 56 | debugLabel: "start", 57 | }); 58 | const messageEvent = workflowEvent({ 59 | debugLabel: "message", 60 | }); 61 | const stopEvent = workflowEvent({ 62 | debugLabel: "stop", 63 | }); 64 | 65 | const workflow = createWorkflow(); 66 | workflow.handle([startEvent], (start) => { 67 | const count = Number.parseInt(start.data, 10); 68 | for (let i = 0; i < count; i++) { 69 | return messageEvent.with(i); 70 | } 71 | return stopEvent.with("completed"); 72 | }); 73 | 74 | workflow.handle([messageEvent], () => { 75 | return stopEvent.with("processed"); 76 | }); 77 | 78 | // Run and collect all events 79 | const allEvents = await runAndCollect( 80 | workflow, 81 | startEvent.with("3"), 82 | stopEvent, 83 | ); 84 | 85 | // Verify events 86 | expect(allEvents).toHaveLength(3); // start -> message -> stop 87 | 88 | // Use eventSource instead of accessing source property directly 89 | expect(eventSource(allEvents[0])).toBe(startEvent); 90 | expect(eventSource(allEvents[1])).toBe(messageEvent); 91 | expect(eventSource(allEvents[2])).toBe(stopEvent); 92 | expect(allEvents[2]?.data).toBe("processed"); 93 | }); 94 | 95 | test("runStream should return an async iterable stream of events until the stop event is encountered", async () => { 96 | // Setup the workflow 97 | const startEvent = workflowEvent({ 98 | debugLabel: "start", 99 | }); 100 | const intermediateEvent = workflowEvent({ 101 | debugLabel: "intermediate", 102 | }); 103 | const stopEvent = workflowEvent({ 104 | debugLabel: "stop", 105 | }); 106 | 107 | const workflow = createWorkflow(); 108 | 109 | workflow.handle([startEvent], async (start) => { 110 | const { sendEvent } = getContext(); 111 | 112 | for (let i = 0; i < 10; i++) { 113 | sendEvent(intermediateEvent.with(i)); 114 | } 115 | 116 | return stopEvent.with("completed"); 117 | }); 118 | 119 | workflow.handle([intermediateEvent], async (intermediate) => { 120 | // fake some work 121 | await new Promise((resolve) => setTimeout(resolve, 1)); 122 | }); 123 | 124 | const stream = runStream(workflow, startEvent.with("run"), stopEvent); 125 | 126 | let collectedEvents: WorkflowEventData[] = []; 127 | for await (const event of stream) { 128 | collectedEvents.push(event); 129 | } 130 | 131 | expect(collectedEvents).toHaveLength(12); 132 | expect(collectedEvents.at(-1)?.data).toBe("completed"); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /packages/core/tests/core/shared/events.ts: -------------------------------------------------------------------------------- 1 | import { workflowEvent } from "@llamaindex/workflow-core"; 2 | 3 | export const messageEvent = workflowEvent({ 4 | debugLabel: "message", 5 | uniqueId: "message", 6 | }); 7 | 8 | export const haltEvent = workflowEvent({ 9 | debugLabel: "halt", 10 | uniqueId: "halt", 11 | }); 12 | -------------------------------------------------------------------------------- /packages/core/tests/core/stream.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WorkflowStream, 3 | createWorkflow, 4 | type WorkflowEventData, 5 | eventSource, 6 | } from "@llamaindex/workflow-core"; 7 | import { describe, expect, test } from "vitest"; 8 | import * as events from "./shared/events"; 9 | import { haltEvent } from "./shared/events"; 10 | 11 | describe("stream api", () => { 12 | test("should able to create stream", async () => { 13 | //#region server 14 | const workflow = createWorkflow(); 15 | const { sendEvent, stream } = workflow.createContext(); 16 | sendEvent(events.messageEvent.with()); 17 | const response = stream 18 | .pipeThrough( 19 | new TransformStream({ 20 | transform(event, controller) { 21 | controller.enqueue(event); 22 | controller.terminate(); 23 | }, 24 | }), 25 | ) 26 | .toResponse(); 27 | //#endregion 28 | 29 | //#region client 30 | const clientSideStream = WorkflowStream.fromResponse(response, { 31 | message: events.messageEvent, 32 | }); 33 | const list: WorkflowEventData[] = []; 34 | for await (const event of clientSideStream) { 35 | list.push(event); 36 | } 37 | expect(list).toHaveLength(1); 38 | expect(eventSource(list[0])).toBe(events.messageEvent); 39 | //#endregion 40 | }); 41 | 42 | test("stream.until", async () => { 43 | const workflow = createWorkflow(); 44 | const { sendEvent, stream } = workflow.createContext(); 45 | sendEvent(events.messageEvent.with()); 46 | sendEvent(events.messageEvent.with()); 47 | sendEvent(events.messageEvent.with()); 48 | sendEvent(events.haltEvent.with()); 49 | const list = await stream.until(events.haltEvent).toArray(); 50 | expect(list).toHaveLength(4); 51 | expect(list.map(eventSource)).toEqual([ 52 | events.messageEvent, 53 | events.messageEvent, 54 | events.messageEvent, 55 | events.haltEvent, 56 | ]); 57 | }); 58 | 59 | test("stream.filter", async () => { 60 | const workflow = createWorkflow(); 61 | const { sendEvent, stream } = workflow.createContext(); 62 | sendEvent(events.messageEvent.with()); 63 | sendEvent(events.messageEvent.with()); 64 | sendEvent(events.messageEvent.with()); 65 | sendEvent(events.haltEvent.with()); 66 | const list = await stream 67 | .until(events.haltEvent) 68 | .filter(events.messageEvent) 69 | .toArray(); 70 | expect(list).toHaveLength(3); 71 | expect(list.map(eventSource)).toEqual([ 72 | events.messageEvent, 73 | events.messageEvent, 74 | events.messageEvent, 75 | ]); 76 | }); 77 | 78 | test("stream.take", async () => { 79 | const workflow = createWorkflow(); 80 | const { sendEvent, stream } = workflow.createContext(); 81 | sendEvent(events.messageEvent.with()); 82 | sendEvent(events.messageEvent.with()); 83 | sendEvent(events.messageEvent.with()); 84 | sendEvent(events.haltEvent.with()); 85 | const list = await stream.until(events.haltEvent).take(2).toArray(); 86 | expect(list).toHaveLength(2); 87 | expect(list.map(eventSource)).toEqual([ 88 | events.messageEvent, 89 | events.messageEvent, 90 | ]); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /packages/core/tests/core/sub-workflow.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { 3 | createWorkflow, 4 | eventSource, 5 | getContext, 6 | workflowEvent, 7 | } from "@llamaindex/workflow-core"; 8 | import { run } from "@llamaindex/workflow-core/stream/run"; 9 | 10 | describe("sub workflow", () => { 11 | test("basic", async () => { 12 | const rootWorkflow = createWorkflow(); 13 | const startEvent = workflowEvent(); 14 | const stopEvent = workflowEvent(); 15 | const haltEvent = workflowEvent(); 16 | rootWorkflow.handle([startEvent], async () => { 17 | const { sendEvent } = getContext(); 18 | const subWorkflow = createWorkflow(); 19 | subWorkflow.handle([startEvent], () => { 20 | const { sendEvent } = getContext(); 21 | sendEvent(stopEvent.with()); 22 | }); 23 | await Promise.all([ 24 | run(subWorkflow, startEvent.with()).filter(stopEvent).take(1).toArray(), 25 | run(subWorkflow, startEvent.with()).filter(stopEvent).take(1).toArray(), 26 | run(subWorkflow, startEvent.with()).filter(stopEvent).take(1).toArray(), 27 | ]).then((evt) => sendEvent(...evt.flat())); 28 | sendEvent(haltEvent.with()); 29 | }); 30 | const { sendEvent, stream } = rootWorkflow.createContext(); 31 | sendEvent(startEvent.with()); 32 | 33 | const events = await stream.until(haltEvent).toArray(); 34 | expect(events.length).toBe(5); 35 | expect(events.map(eventSource)).toEqual([ 36 | startEvent, 37 | stopEvent, 38 | stopEvent, 39 | stopEvent, 40 | haltEvent, 41 | ]); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/core/tests/integration/stream-library.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { pipeline } from "node:stream/promises"; 3 | import { chain } from "stream-chain"; 4 | import { 5 | workflowEvent, 6 | createWorkflow, 7 | getContext, 8 | type WorkflowEventData, 9 | } from "@llamaindex/workflow-core"; 10 | 11 | describe("node:stream", () => { 12 | test("basic usage", async () => { 13 | const startEvent = workflowEvent({ 14 | debugLabel: "start", 15 | }); 16 | const stopEvent = workflowEvent({ 17 | debugLabel: "stop", 18 | }); 19 | const workflow = createWorkflow(); 20 | workflow.handle([startEvent], () => stopEvent.with()); 21 | const context = workflow.createContext(); 22 | const { stream, sendEvent } = context; 23 | sendEvent(startEvent.with()); 24 | const result = await pipeline(stream, async function (source) { 25 | for await (const event of source) { 26 | if (stopEvent.include(event)) { 27 | return "stop"; 28 | } 29 | } 30 | }); 31 | expect(result).toBe("stop"); 32 | }); 33 | }); 34 | 35 | describe("stream-chain", () => { 36 | test("basic usage", async () => { 37 | const startEvent = workflowEvent({ 38 | debugLabel: "start", 39 | }); 40 | const messageEvent = workflowEvent({ 41 | debugLabel: "message", 42 | }); 43 | const stopEvent = workflowEvent({ 44 | debugLabel: "stop", 45 | }); 46 | const workflow = createWorkflow(); 47 | workflow.handle([startEvent], () => { 48 | const { sendEvent } = getContext(); 49 | for (let i = 0; i < 10; i++) { 50 | sendEvent(messageEvent.with()); 51 | } 52 | return stopEvent.with(); 53 | }); 54 | const context = workflow.createContext(); 55 | const { stream, sendEvent } = context; 56 | sendEvent(startEvent.with()); 57 | const outputs: WorkflowEventData[] = []; 58 | const pipeline = chain([ 59 | stream.until(stopEvent), 60 | new TransformStream({ 61 | transform: (event: WorkflowEventData, controller) => { 62 | if (messageEvent.include(event)) { 63 | controller.enqueue(event); 64 | } 65 | }, 66 | }), 67 | new WritableStream({ 68 | write: (event) => { 69 | outputs.push(event); 70 | }, 71 | }), 72 | ]); 73 | pipeline.on("end", () => { 74 | expect(outputs.length).toBe(10); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /packages/core/tests/middleware/full-workflow.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect, expectTypeOf } from "vitest"; 2 | import { 3 | createWorkflow, 4 | eventSource, 5 | type WorkflowEvent, 6 | type WorkflowEventData, 7 | } from "@llamaindex/workflow-core"; 8 | import { createStateMiddleware } from "@llamaindex/workflow-core/middleware/store"; 9 | import { withTraceEvents } from "@llamaindex/workflow-core/middleware/trace-events"; 10 | import { withValidation } from "@llamaindex/workflow-core/middleware/validation"; 11 | import { zodEvent } from "@llamaindex/workflow-core/util/zod"; 12 | import { z } from "zod"; 13 | import { webcrypto } from "node:crypto"; 14 | 15 | describe("full workflow middleware", () => { 16 | const createFullWorkflow = < 17 | const Validation extends [ 18 | inputs: WorkflowEvent[], 19 | outputs: WorkflowEvent[], 20 | ][], 21 | T, 22 | Input, 23 | >( 24 | validation: Validation, 25 | createStore: (input: Input) => T, 26 | ) => { 27 | const { withState, getContext } = createStateMiddleware(createStore); 28 | return [ 29 | withState(withValidation(withTraceEvents(createWorkflow()), validation)), 30 | getContext, 31 | ] as const; 32 | }; 33 | test("type check", () => { 34 | const startEvent = zodEvent(z.string(), { 35 | debugLabel: "start", 36 | }); 37 | const messageEvent = zodEvent(z.string(), { 38 | debugLabel: "message", 39 | }); 40 | const stopEvent = zodEvent(z.string(), { 41 | debugLabel: "stop", 42 | }); 43 | const [workflow, getContext] = createFullWorkflow( 44 | [[[startEvent], [stopEvent]]], 45 | () => ({}), 46 | ); 47 | workflow.strictHandle([startEvent], (sendEvent, events) => { 48 | // @ts-expect-error 49 | sendEvent(messageEvent.with()); 50 | sendEvent(stopEvent.with("")); 51 | }); 52 | }); 53 | 54 | test("basic", async () => { 55 | const startEvent = zodEvent(z.string(), { 56 | debugLabel: "start", 57 | }); 58 | const stopEvent = zodEvent(z.string(), { 59 | debugLabel: "stop", 60 | }); 61 | const [workflow, getContext] = createFullWorkflow( 62 | [[[startEvent], [stopEvent]]], 63 | (id: string) => ({ 64 | id, 65 | }), 66 | ); 67 | workflow.strictHandle([startEvent], (sendEvent, start) => { 68 | expect(start.data).toBe("start"); 69 | sendEvent(stopEvent.with(getContext().state.id)); 70 | }); 71 | 72 | expectTypeOf(workflow.substream).not.toBeNever(); 73 | const id = webcrypto.randomUUID(); 74 | const { sendEvent, stream } = workflow.createContext(id); 75 | sendEvent(startEvent.with("start")); 76 | const events: WorkflowEventData[] = await stream 77 | .until(stopEvent) 78 | .toArray(); 79 | expect(events.length).toBe(2); 80 | expect(events.map(eventSource)).toEqual([startEvent, stopEvent]); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /packages/core/tests/middleware/with-state.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi } from "vitest"; 2 | import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; 3 | import { createStatefulMiddleware } from "@llamaindex/workflow-core/middleware/state"; 4 | 5 | describe("with state", () => { 6 | test("no input", () => { 7 | const ref = {}; 8 | const { withState } = createStatefulMiddleware(() => ref); 9 | const workflow = withState(createWorkflow()); 10 | const { state } = workflow.createContext(); 11 | expect(state).toBe(ref); 12 | }); 13 | 14 | test("with input", () => { 15 | const { withState } = createStatefulMiddleware((input: { id: string }) => ({ 16 | id: input.id, 17 | })); 18 | const workflow = withState(createWorkflow()); 19 | workflow.createContext({ 20 | id: "1", 21 | }); 22 | }); 23 | 24 | test("runtime call getState", async () => { 25 | const obj = {}; 26 | const startEvent = workflowEvent(); 27 | const { withState, getContext } = createStatefulMiddleware(() => obj); 28 | const workflow = withState(createWorkflow()); 29 | const fn = vi.fn(); 30 | workflow.handle([startEvent], () => { 31 | fn(); 32 | expect(getContext()).toBe(obj); 33 | }); 34 | const { sendEvent, state } = workflow.createContext(); 35 | expect(state).toBe(obj); 36 | sendEvent(startEvent.with()); 37 | expect(fn).toBeCalled(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/core/tests/middleware/with-validation.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, vi, afterEach } from "vitest"; 2 | import { createWorkflow, workflowEvent } from "@llamaindex/workflow-core"; 3 | import { withValidation } from "@llamaindex/workflow-core/middleware/validation"; 4 | import { find } from "@llamaindex/workflow-core/stream/find"; 5 | import { runWorkflow } from "@llamaindex/workflow-core/stream/run"; 6 | 7 | describe("with directed graph", () => { 8 | const consoleWarnMock = vi 9 | .spyOn(console, "warn") 10 | .mockImplementation(() => undefined); 11 | afterEach(() => { 12 | consoleWarnMock.mockReset(); 13 | }); 14 | 15 | test("basic", async () => { 16 | const startEvent = workflowEvent({ 17 | debugLabel: "start", 18 | }); 19 | const nonEvent = workflowEvent({ 20 | debugLabel: "non", 21 | }); 22 | const parseEvent = workflowEvent({ 23 | debugLabel: "parse", 24 | }); 25 | const stopEvent = workflowEvent({ 26 | debugLabel: "stop", 27 | }); 28 | const workflow = withValidation(createWorkflow(), [ 29 | [[startEvent], [stopEvent]], 30 | [[startEvent], [parseEvent, parseEvent]], 31 | ]); 32 | const fn = vi.fn(); 33 | 34 | workflow.strictHandle([startEvent], (sendEvent) => { 35 | fn(); 36 | sendEvent( 37 | // @ts-expect-error 38 | nonEvent.with(1), 39 | ); 40 | sendEvent(parseEvent.with("")); 41 | sendEvent(stopEvent.with(1)); 42 | }); 43 | 44 | const { sendEvent, stream } = workflow.createContext(); 45 | sendEvent(startEvent.with()); 46 | await find(stream, stopEvent); 47 | expect(fn).toBeCalled(); 48 | expect(consoleWarnMock).toBeCalledTimes(2); 49 | expect(consoleWarnMock).toHaveBeenNthCalledWith( 50 | 1, 51 | "Invalid input detected [%s]", 52 | "1", 53 | ); 54 | expect(consoleWarnMock).toHaveBeenNthCalledWith( 55 | 2, 56 | "Invalid input detected [%s]", 57 | "", 58 | ); 59 | }); 60 | 61 | test("promise", async () => { 62 | const startEvent = workflowEvent(); 63 | const stopEvent = workflowEvent(); 64 | const workflow = withValidation(createWorkflow(), [ 65 | [[startEvent], [stopEvent]], 66 | ]); 67 | 68 | workflow.strictHandle([startEvent], (sendEvent) => { 69 | sendEvent(stopEvent.with(1)); 70 | }); 71 | 72 | const result = await runWorkflow(workflow, startEvent.with(), stopEvent); 73 | expect(result.data).toBe(1); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /packages/core/tests/util/zod.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { z } from "zod"; 3 | import { zodEvent } from "../../src/util/zod"; 4 | 5 | describe("zodEvent", () => { 6 | test("can use zod schema to infer types and validate", () => { 7 | const UserSchema = z.object({ 8 | name: z.string(), 9 | age: z.number(), 10 | }); 11 | const event = zodEvent(UserSchema); 12 | 13 | const schema = event.schema; 14 | expect(schema).toBe(UserSchema); 15 | 16 | const validData = { name: "John", age: 30 }; 17 | const invalidData = { name: "John", age: "30" }; 18 | 19 | expect(() => event.with(validData)).not.toThrow(); 20 | expect(() => event.with(invalidData as any)).toThrow(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/core/tsconfig.browser.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.browser.json", 3 | "compilerOptions": { 4 | "composite": false 5 | }, 6 | "include": ["./src"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/tsconfig.browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/browser", 5 | "tsBuildInfoFile": "./lib/browser/.tsbuildinfo", 6 | "target": "es2015", 7 | "paths": { 8 | "@llamaindex/workflow-core": ["./src/core/index.ts"], 9 | "@llamaindex/workflow-core/stream/*": ["./src/stream/*.ts"], 10 | "@llamaindex/workflow-core/async-context": [ 11 | "./src/async-context/index.browser.ts" 12 | ], 13 | "@llamaindex/workflow-core/interrupter/*": ["./src/interrupter/*.ts"], 14 | "@llamaindex/workflow-core/middleware/*": ["./src/middleware/*.ts"], 15 | "@llamaindex/workflow-core/util/*": ["./src/util/*.ts"] 16 | } 17 | }, 18 | "include": ["./src"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/tsconfig.browser.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.browser.json", 3 | "compilerOptions": { 4 | "lib": ["esnext", "dom"], 5 | "outDir": "./lib/browser-test", 6 | "tsBuildInfoFile": "./lib/browser-test/.tsbuildinfo" 7 | }, 8 | "include": ["./src", "./tests", "./vitest.config.ts"], 9 | "references": [ 10 | { 11 | "path": "./tsconfig.browser.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "composite": false 5 | }, 6 | "include": ["./src"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "tsBuildInfoFile": "./lib/.tsbuildinfo", 6 | "paths": { 7 | "@llamaindex/workflow-core": ["./src/core/index.ts"], 8 | "@llamaindex/workflow-core/async-context": [ 9 | "./src/async-context/index.ts" 10 | ], 11 | "@llamaindex/workflow-core/interrupter/*": ["./src/interrupter/*.ts"], 12 | "@llamaindex/workflow-core/middleware/*": ["./src/middleware/*.ts"], 13 | "@llamaindex/workflow-core/util/*": ["./src/util/*.ts"], 14 | "@llamaindex/workflow-core/stream/*": ["./src/stream/*.ts"] 15 | } 16 | }, 17 | "include": ["./src"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["esnext"], 5 | "outDir": "./lib/test/browser", 6 | "tsBuildInfoFile": "./lib/test/.tsbuildinfo" 7 | }, 8 | "include": ["./src", "./tests", "./vitest.config.ts"], 9 | "references": [ 10 | { 11 | "path": "./tsconfig.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsdown"; 2 | 3 | export default defineConfig([ 4 | // Core APIs 5 | { 6 | entry: ["src/core/index.ts"], 7 | outDir: "dist", 8 | format: ["cjs", "esm"], 9 | external: ["@llamaindex/workflow-core/async-context"], 10 | tsconfig: "./tsconfig.build.json", 11 | dts: true, 12 | sourcemap: true, 13 | }, 14 | // Core APIs - Browser ESM 15 | { 16 | entry: ["src/core/index.ts"], 17 | outDir: "dist/browser", 18 | tsconfig: "./tsconfig.browser.build.json", 19 | platform: "browser", 20 | format: ["esm"], 21 | sourcemap: true, 22 | }, 23 | // Async Context APIs 24 | { 25 | entry: ["src/async-context/*.ts"], 26 | outDir: "async-context", 27 | format: ["cjs", "esm"], 28 | tsconfig: "./tsconfig.build.json", 29 | dts: true, 30 | sourcemap: true, 31 | }, 32 | // Interrupter APIs 33 | { 34 | entry: ["src/*.ts"], 35 | outDir: "dist", 36 | format: ["esm"], 37 | external: [ 38 | "next", 39 | "hono", 40 | "@llamaindex/workflow-core", 41 | "@llamaindex/workflow-core/async-context", 42 | ], 43 | tsconfig: "./tsconfig.build.json", 44 | dts: true, 45 | sourcemap: true, 46 | }, 47 | // Middleware APIs 48 | { 49 | entry: ["src/middleware/*.ts"], 50 | outDir: "middleware", 51 | external: [ 52 | "@llamaindex/workflow-core", 53 | "@llamaindex/workflow-core/async-context", 54 | ], 55 | format: ["esm", "cjs"], 56 | tsconfig: "./tsconfig.build.json", 57 | dts: true, 58 | sourcemap: true, 59 | }, 60 | // Utility APIs 61 | { 62 | entry: ["src/util/*.ts"], 63 | outDir: "util", 64 | format: ["esm"], 65 | external: [ 66 | "@llamaindex/workflow-core", 67 | "@llamaindex/workflow-core/async-context", 68 | ], 69 | tsconfig: "./tsconfig.build.json", 70 | dts: true, 71 | sourcemap: true, 72 | }, 73 | // Stream APIs 74 | { 75 | entry: ["src/stream/*.ts"], 76 | outDir: "stream", 77 | format: ["esm"], 78 | external: [ 79 | "@llamaindex/workflow-core", 80 | "@llamaindex/workflow-core/async-context", 81 | ], 82 | tsconfig: "./tsconfig.build.json", 83 | dts: true, 84 | sourcemap: true, 85 | }, 86 | ]); 87 | -------------------------------------------------------------------------------- /packages/core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import tsconfigPaths from "vite-tsconfig-paths"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | workspace: [ 7 | { 8 | plugins: [ 9 | tsconfigPaths({ 10 | projects: ["./tsconfig.browser.test.json"], 11 | }), 12 | ], 13 | test: { 14 | exclude: ["**/dist/**", "**/lib/**"], 15 | name: "DOM", 16 | environment: "happy-dom", 17 | }, 18 | }, 19 | { 20 | plugins: [ 21 | tsconfigPaths({ 22 | projects: ["./tsconfig.test.json"], 23 | }), 24 | ], 25 | test: { 26 | exclude: ["**/dist/**", "**/lib/**"], 27 | name: "Node.js", 28 | environment: "node", 29 | }, 30 | }, 31 | { 32 | plugins: [ 33 | tsconfigPaths({ 34 | projects: ["./tsconfig.test.json"], 35 | }), 36 | ], 37 | test: { 38 | exclude: ["**/dist/**", "**/lib/**"], 39 | name: "Edge Runtime", 40 | environment: "edge-runtime", 41 | }, 42 | }, 43 | ], 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /packages/http/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @llamaindex/workflow-http 2 | 3 | ## 1.0.0 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [095fb04] 8 | - @llamaindex/workflow-core@1.0.0 9 | 10 | ## 0.0.6 11 | 12 | ### Patch Changes 13 | 14 | - 59b9199: chore: bump version 15 | - Updated dependencies [59b9199] 16 | - @llamaindex/workflow-core@0.4.6 17 | 18 | ## 0.0.5 19 | 20 | ### Patch Changes 21 | 22 | - 1fb29de: chore: rename package 23 | - Updated dependencies [1fb29de] 24 | - @llamaindex/workflow-core@0.4.5 25 | 26 | ## 0.0.4 27 | 28 | ### Patch Changes 29 | 30 | - Updated dependencies [24fe0b2] 31 | - @llamaindex/workflow-core@0.4.4 32 | 33 | ## 0.0.3 34 | 35 | ### Patch Changes 36 | 37 | - Updated dependencies [9c17c2e] 38 | - @llamaindex/workflow-core@0.4.3 39 | 40 | ## 0.0.2 41 | 42 | ### Patch Changes 43 | 44 | - 23ecfc7: feat: update http protocol 45 | - Updated dependencies [23ecfc7] 46 | - Updated dependencies [4402a6d] 47 | - Updated dependencies [9c65785] 48 | - @llamaindex/workflow-core@0.4.2 49 | -------------------------------------------------------------------------------- /packages/http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@llamaindex/workflow-http", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "HTTP protocol for Llama Flow", 6 | "exports": { 7 | "./server": { 8 | "types": "./dist/server.d.ts", 9 | "require": "./dist/server.cjs", 10 | "default": "./dist/server.js" 11 | }, 12 | "./client": { 13 | "types": "./dist/client.d.ts", 14 | "require": "./dist/client.cjs", 15 | "default": "./dist/client.js" 16 | } 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/run-llama/workflows-ts.git" 21 | }, 22 | "scripts": { 23 | "build": "bunchee", 24 | "dev": "bunchee --watch" 25 | }, 26 | "devDependencies": { 27 | "@llamaindex/workflow-core": "workspace:*", 28 | "bunchee": "^6.5.4" 29 | }, 30 | "peerDependencies": { 31 | "@llamaindex/workflow-core": "workspace:*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/http/src/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type WorkflowEvent, 3 | WorkflowStream, 4 | type WorkflowEventData, 5 | } from "@llamaindex/workflow-core"; 6 | 7 | export const createClient = ( 8 | endpoint: string | URL, 9 | events: Record>, 10 | ) => { 11 | return { 12 | fetch: async ( 13 | data: Record, 14 | requestInit?: Omit, 15 | ): Promise>> => { 16 | const form = new FormData(); 17 | for (const key in data) { 18 | if (data[key] instanceof File) { 19 | form.append(key, data[key]); 20 | } else { 21 | form.append(key, JSON.stringify(data[key])); 22 | } 23 | } 24 | const response = await fetch(endpoint, { 25 | ...requestInit, 26 | method: "POST", 27 | headers: { 28 | ...requestInit?.headers, 29 | }, 30 | body: form, 31 | }); 32 | return WorkflowStream.fromResponse(response, events); 33 | }, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/http/src/server.ts: -------------------------------------------------------------------------------- 1 | import type { Workflow, WorkflowContext } from "@llamaindex/workflow-core"; 2 | 3 | export const createServer = ( 4 | workflow: Workflow, 5 | initRequest: ( 6 | json: any, 7 | sendEvent: WorkflowContext["sendEvent"], 8 | ) => void | Promise, 9 | handleStream: ( 10 | stream: WorkflowContext["stream"], 11 | ) => WorkflowContext["stream"], 12 | ) => { 13 | return async function fetch(request: Request): Promise { 14 | if (request.method !== "POST") { 15 | return new Response("Method Not Allowed", { 16 | status: 405, 17 | }); 18 | } 19 | const from = await request.formData(); 20 | const json: Record = {}; 21 | for (const [key, value] of from.entries()) { 22 | if (value instanceof File) { 23 | json[key] = value; 24 | } else { 25 | json[key] = JSON.parse(value as string); 26 | } 27 | } 28 | const { stream, sendEvent } = workflow.createContext(); 29 | await initRequest(json, sendEvent); 30 | return handleStream(stream).toResponse(); 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/http/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "tsBuildInfoFile": "./lib/.tsbuildinfo" 6 | }, 7 | "include": ["./src"], 8 | "references": [ 9 | { 10 | "path": "../core/tsconfig.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/http/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/test/browser", 5 | "tsBuildInfoFile": "./lib/test/.tsbuildinfo" 6 | }, 7 | "include": ["./tests", "./vitest.config.ts"], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - ./demo 3 | - ./demo/cloudflare 4 | - ./demo/browser 5 | - ./demo/waku 6 | - ./docs 7 | - ./packages/* 8 | - ./tests/* 9 | 10 | onlyBuiltDependencies: 11 | - "@tailwindcss/oxide" 12 | - esbuild 13 | - sharp 14 | - workerd 15 | -------------------------------------------------------------------------------- /tests/cjs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # workflows-ts-cjs-test 2 | 3 | ## 1.0.4 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [095fb04] 8 | - @llamaindex/workflow-core@1.0.0 9 | 10 | ## 1.0.3 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [59b9199] 15 | - @llamaindex/workflow-core@0.4.6 16 | 17 | ## 1.0.2 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [1fb29de] 22 | - @llamaindex/workflow-core@0.4.5 23 | 24 | ## 1.0.1 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [24fe0b2] 29 | - @llamaindex/workflow-core@0.4.4 30 | -------------------------------------------------------------------------------- /tests/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workflows-ts-cjs-test", 3 | "version": "1.0.4", 4 | "private": true, 5 | "description": "CommonJS test for workflows-ts functionality", 6 | "type": "commonjs", 7 | "scripts": { 8 | "test": "vitest run", 9 | "test:watch": "vitest", 10 | "typecheck": "tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "@llamaindex/workflow-core": "workspace:*" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^24.0.4", 17 | "typescript": "^5.8.3", 18 | "vitest": "^3.2.4" 19 | }, 20 | "engines": { 21 | "node": ">=18.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/cjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "CommonJS", 5 | "lib": ["ES2022"], 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "allowSyntheticDefaultImports": true, 13 | "declaration": false, 14 | "outDir": "./dist", 15 | "rootDir": "./", 16 | "types": ["vitest/globals"] 17 | }, 18 | "include": ["**/*.ts", "**/*.test.ts"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /tests/cjs/vitest.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("vitest/config"); 2 | 3 | module.exports = defineConfig({ 4 | test: { 5 | environment: "node", 6 | globals: true, // Enable globals for easier testing 7 | // Configure for CommonJS with TypeScript support 8 | pool: "forks", 9 | poolOptions: { 10 | forks: { 11 | singleFork: true, 12 | }, 13 | }, 14 | typecheck: { 15 | enabled: true, 16 | }, 17 | // Include TypeScript files 18 | include: ["**/*.test.ts", "**/*.test.js"], 19 | }, 20 | esbuild: { 21 | // Enable TypeScript compilation 22 | target: "node18", 23 | format: "cjs", // Ensure CJS output 24 | }, 25 | resolve: { 26 | conditions: ["require", "node"], 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /tests/cjs/workflow-cjs.test.ts: -------------------------------------------------------------------------------- 1 | // Pure CommonJS Vitest test file with TypeScript 2 | import type { WorkflowEvent, Workflow } from "@llamaindex/workflow-core"; 3 | 4 | // Type the require imports properly for TypeScript 5 | const workflows = require("@llamaindex/workflow-core") as { 6 | createWorkflow: () => Workflow; 7 | workflowEvent: () => WorkflowEvent; 8 | }; 9 | const stateLlama = require("@llamaindex/workflow-core/middleware/state") as { 10 | createStatefulMiddleware: (input: CallableFunction) => { 11 | withState: (workflow: Workflow) => Workflow; 12 | }; 13 | }; 14 | 15 | const { createStatefulMiddleware } = stateLlama; 16 | const { createWorkflow, workflowEvent } = workflows; 17 | 18 | interface JokeResult { 19 | joke: string; 20 | } 21 | 22 | describe("Llama Flow Pure CJS Tests", () => { 23 | describe("Basic CJS Import Tests", () => { 24 | test("should import core functions via require", () => { 25 | expect(createWorkflow).toBeDefined(); 26 | expect(workflowEvent).toBeDefined(); 27 | expect(typeof createWorkflow).toBe("function"); 28 | expect(typeof workflowEvent).toBe("function"); 29 | }); 30 | 31 | test("should create workflow events", () => { 32 | const event = workflowEvent(); 33 | expect(event).toBeDefined(); 34 | expect(typeof event.with).toBe("function"); 35 | expect(typeof event.include).toBe("function"); 36 | }); 37 | }); 38 | 39 | describe("Workflow Functionality", () => { 40 | let startEvent: WorkflowEvent; 41 | let resultEvent: WorkflowEvent; 42 | let jokeFlow: Workflow; 43 | let numIterations: number; 44 | 45 | beforeEach(() => { 46 | // Reset state before each test 47 | numIterations = 0; 48 | 49 | // Define our workflow events 50 | startEvent = workflowEvent(); // Input topic for joke 51 | resultEvent = workflowEvent(); // Final joke 52 | 53 | const { withState } = createStatefulMiddleware(() => ({})); 54 | 55 | // Create our workflow 56 | jokeFlow = withState(createWorkflow()); 57 | 58 | // Define handlers for each step 59 | jokeFlow.handle([startEvent], async (event: any) => { 60 | // Increment our manual state counter 61 | numIterations++; 62 | 63 | const joke = `Here is a wonderful joke about ${event.data} (iteration ${numIterations})`; 64 | 65 | return resultEvent.with({ joke: joke }); 66 | }); 67 | }); 68 | 69 | test("should create and execute a basic workflow", async () => { 70 | const { stream, sendEvent } = jokeFlow.createContext(); 71 | 72 | sendEvent(startEvent.with("pirates")); 73 | 74 | let result: JokeResult | undefined; 75 | for await (const event of stream) { 76 | if (resultEvent.include(event)) { 77 | result = event.data; 78 | break; 79 | } 80 | } 81 | 82 | expect(result).toBeDefined(); 83 | expect(result!.joke).toContain("pirates"); 84 | expect(result!.joke).toContain("iteration 1"); 85 | expect(numIterations).toBe(1); 86 | }); 87 | 88 | test("should handle multiple workflow executions", async () => { 89 | // First execution 90 | const { stream: stream1, sendEvent: sendEvent1 } = 91 | jokeFlow.createContext(); 92 | sendEvent1(startEvent.with("cats")); 93 | 94 | let result1: JokeResult | undefined; 95 | for await (const event of stream1) { 96 | if (resultEvent.include(event)) { 97 | result1 = event.data; 98 | break; 99 | } 100 | } 101 | 102 | // Second execution 103 | const { stream: stream2, sendEvent: sendEvent2 } = 104 | jokeFlow.createContext(); 105 | sendEvent2(startEvent.with("dogs")); 106 | 107 | let result2: JokeResult | undefined; 108 | for await (const event of stream2) { 109 | if (resultEvent.include(event)) { 110 | result2 = event.data; 111 | break; 112 | } 113 | } 114 | 115 | expect(result1!.joke).toContain("cats"); 116 | expect(result1!.joke).toContain("iteration 1"); 117 | expect(result2!.joke).toContain("dogs"); 118 | expect(result2!.joke).toContain("iteration 2"); 119 | expect(numIterations).toBe(2); 120 | }); 121 | 122 | test("should work with different data types", async () => { 123 | const numberEvent = workflowEvent(); 124 | const doubleEvent = workflowEvent(); 125 | 126 | const mathFlow = createWorkflow(); 127 | mathFlow.handle([numberEvent], (event) => { 128 | return doubleEvent.with(event.data * 2); 129 | }); 130 | 131 | const { stream, sendEvent } = mathFlow.createContext(); 132 | sendEvent(numberEvent.with(21)); 133 | 134 | let result: number | undefined; 135 | for await (const event of stream) { 136 | if (doubleEvent.include(event)) { 137 | result = event.data; 138 | break; 139 | } 140 | } 141 | 142 | expect(result).toBe(42); 143 | }); 144 | 145 | test("should not throw 'No current context found' errors", async () => { 146 | // This test specifically verifies the original error is avoided 147 | const { stream, sendEvent } = jokeFlow.createContext(); 148 | 149 | // This should not throw the context error 150 | await expect(async () => { 151 | sendEvent(startEvent.with("test")); 152 | 153 | for await (const event of stream) { 154 | if (resultEvent.include(event)) { 155 | break; 156 | } 157 | } 158 | }).not.toThrow(); 159 | }); 160 | 161 | test("should handle async workflows correctly", async () => { 162 | const asyncEvent = workflowEvent(); 163 | const asyncResultEvent = workflowEvent(); 164 | 165 | const asyncFlow = createWorkflow(); 166 | asyncFlow.handle([asyncEvent], async (event) => { 167 | // Simulate async work 168 | await new Promise((resolve) => setTimeout(resolve, 10)); 169 | return asyncResultEvent.with(`Async result: ${event.data}`); 170 | }); 171 | 172 | const { stream, sendEvent } = asyncFlow.createContext(); 173 | sendEvent(asyncEvent.with("async test")); 174 | 175 | let result: string | undefined; 176 | for await (const event of stream) { 177 | if (asyncResultEvent.include(event)) { 178 | result = event.data; 179 | break; 180 | } 181 | } 182 | 183 | expect(result).toBe("Async result: async test"); 184 | }); 185 | 186 | test("should demonstrate the original user code pattern", async () => { 187 | // This replicates the user's original pattern but working in CJS 188 | const { stream, sendEvent } = jokeFlow.createContext(); 189 | sendEvent(startEvent.with("pirates")); 190 | 191 | let result: JokeResult | undefined; 192 | 193 | for await (const event of stream) { 194 | // console.log(event.data); // optionally log the event data 195 | if (resultEvent.include(event)) { 196 | result = event.data; 197 | break; // Stop when we get the final result 198 | } 199 | } 200 | 201 | expect(result).toBeDefined(); 202 | expect(result!.joke).toContain("pirates"); 203 | 204 | // Verify this works without throwing context errors 205 | console.log("✅ Original user pattern works in CJS:", result); 206 | }); 207 | 208 | test("should have proper TypeScript typing", () => { 209 | // Test that TypeScript types work correctly 210 | const stringEvent = workflowEvent(); 211 | const numberEvent = workflowEvent(); 212 | const customEvent = workflowEvent<{ id: number; name: string }>(); 213 | 214 | // These should be properly typed 215 | const stringData = stringEvent.with("test"); 216 | const numberData = numberEvent.with(42); 217 | const customData = customEvent.with({ id: 1, name: "test" }); 218 | 219 | expect(stringData.data).toBe("test"); 220 | expect(numberData.data).toBe(42); 221 | expect(customData.data).toEqual({ id: 1, name: "test" }); 222 | 223 | // Type checking - these would fail at compile time if types are wrong 224 | expect(typeof stringData.data).toBe("string"); 225 | expect(typeof numberData.data).toBe("number"); 226 | expect(typeof customData.data).toBe("object"); 227 | }); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "composite": true, 5 | "target": "es2022", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "verbatimModuleSyntax": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noUncheckedIndexedAccess": true, 12 | "strictNullChecks": true, 13 | "exactOptionalPropertyTypes": true, 14 | "strict": true, 15 | "skipLibCheck": true, 16 | "stripInternal": true, 17 | "lib": ["ES2022", "DOM", "DOM.Iterable", "DOM.AsyncIterable"] 18 | }, 19 | "files": [], 20 | "include": [], 21 | "references": [ 22 | { 23 | "path": "./demo/tsconfig.json" 24 | }, 25 | { 26 | "path": "./tsconfig.lint-staged.json" 27 | }, 28 | { 29 | "path": "./packages/core/tsconfig.test.json" 30 | }, 31 | { 32 | "path": "./packages/core/tsconfig.json" 33 | }, 34 | { 35 | "path": "./packages/http/tsconfig.json" 36 | }, 37 | { 38 | "path": "./packages/http/tsconfig.test.json" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /tsconfig.lint-staged.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib/lint-staged", 5 | "tsBuildInfoFile": "./lib/lint-staged/.tsbuildinfo", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["./lint-staged.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "outputs": ["dist/**"], 6 | "dependsOn": ["^build"] 7 | }, 8 | "test": { 9 | "dependsOn": ["^build"] 10 | }, 11 | "dev": { 12 | "persistent": true, 13 | "cache": false 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | export default ["packages/*"]; 2 | --------------------------------------------------------------------------------