Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/quiet-agents-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@tanstack/ai-orchestration': minor
---

Add the first Workflow-backed AI orchestration package. It provides typed agent
definitions, a `ctx.ai.agent()` Workflow middleware, and adapters that project
Workflow events into TanStack AI AG-UI streams without introducing another
workflow runtime or store.
60 changes: 60 additions & 0 deletions packages/ai-orchestration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# @tanstack/ai-orchestration

Durable TanStack AI agent calls powered by TanStack Workflow.

```ts
import { chat } from '@tanstack/ai'
import {
agentMiddleware,
defineAgent,
toAIStream,
} from '@tanstack/ai-orchestration'
import { openaiText } from '@tanstack/ai-openai'
import {
createWorkflow,
inMemoryRunStore,
runWorkflow,
} from '@tanstack/workflow-core'
import { z } from 'zod'

const writer = defineAgent({
name: 'writer',
input: z.object({ topic: z.string() }),
output: z.object({ article: z.string() }),
run: ({ input, abortController }) =>
chat({
adapter: openaiText('gpt-5.5'),
messages: [{ role: 'user', content: `Write about ${input.topic}` }],
outputSchema: z.object({ article: z.string() }),
stream: true,
abortController,
}),
})

const article = createWorkflow({
id: 'article',
input: z.object({ topic: z.string() }),
})
.middleware([agentMiddleware()])
.handler(async (ctx) =>
ctx.ai.agent('draft', writer, { topic: ctx.input.topic }),
)

const workflowEvents = runWorkflow({
workflow: article,
runStore: inMemoryRunStore(),
input: { topic: 'durable execution' },
})

for await (const chunk of toAIStream(workflowEvents)) {
// Send AG-UI chunks to the client or a durable delivery stream.
}
```

Workflow owns execution, replay, retries, approvals, signals, deadlines, stores,
and leases. This package only defines agent-step ergonomics and maps Workflow
events to TanStack AI's AG-UI stream.

AI tool approvals inside an agent call are intentionally unsupported for now.
Use `ctx.approve()` between agent calls so Workflow can persist and resume the
pause.
68 changes: 68 additions & 0 deletions packages/ai-orchestration/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"name": "@tanstack/ai-orchestration",
"version": "0.0.0",
"description": "Durable TanStack AI agent orchestration powered by TanStack Workflow.",
"author": "",
"license": "MIT",
"homepage": "https://tanstack.com/ai",
"repository": {
"type": "git",
"url": "git+https://github.com/TanStack/ai.git",
"directory": "packages/ai-orchestration"
},
"bugs": {
"url": "https://github.com/TanStack/ai/issues"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"type": "module",
"module": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts",
"exports": {
".": {
"types": "./dist/esm/index.d.ts",
"import": "./dist/esm/index.js"
}
},
"sideEffects": false,
"engines": {
"node": ">=18"
},
"files": [
"dist",
"src"
],
"scripts": {
"build": "vite build",
"clean": "premove ./build ./dist",
"lint:fix": "oxlint src --type-aware --fix",
"test:build": "publint --strict",
"test:coverage": "vitest run --coverage",
"test:oxlint": "oxlint src --type-aware",
"test:lib": "vitest run",
"test:lib:dev": "pnpm test:lib --watch",
"test:types": "tsc"
},
"keywords": [
"ai",
"tanstack",
"workflow",
"durable-execution",
"orchestration",
"agents"
],
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@tanstack/workflow-core": "^0.0.4"
},
"peerDependencies": {
"@tanstack/ai": "workspace:^"
},
"devDependencies": {
"@tanstack/ai": "workspace:*",
"@vitest/coverage-v8": "4.0.14",
"zod": "^4.2.0"
}
}
83 changes: 83 additions & 0 deletions packages/ai-orchestration/src/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { StandardSchemaV1 } from '@standard-schema/spec'
import type { StreamChunk } from '@tanstack/ai'
import type {
AgentDefinition,
AgentRunResult,
AgentStreamResult,
} from './types'

export interface DefineAgentConfig<TInput, TOutput, TName extends string> {
name: TName
description?: string
input?: StandardSchemaV1<unknown, TInput>
output?: StandardSchemaV1<unknown, TOutput>
run: (context: {
input: TInput
signal: AbortSignal
abortController: AbortController
}) => AgentRunResult<TOutput>
}

export function defineAgent<TInput, TOutput, const TName extends string>(
config: DefineAgentConfig<TInput, TOutput, TName> & {
output: StandardSchemaV1<unknown, TOutput>
},
): AgentDefinition<TInput, TOutput, TName>
export function defineAgent<
TInput = unknown,
const TName extends string = string,
>(
config: DefineAgentConfig<TInput, string, TName> & { output?: undefined },
): AgentDefinition<TInput, string, TName>
export function defineAgent<TInput, TOutput, const TName extends string>(
config: DefineAgentConfig<TInput, TOutput, TName>,
): AgentDefinition<TInput, TOutput, TName> {
return {
kind: 'agent',
name: config.name,
description: config.description,
inputSchema: config.input,
outputSchema: config.output,
run: config.run,
}
}

export function agentStream<TOutput>(
stream: AsyncIterable<StreamChunk>,
output: TOutput | Promise<TOutput>,
): AgentStreamResult<TOutput> {
return { kind: 'agent-stream', stream, output }
}

export class AgentValidationError extends Error {
override name = 'AgentValidationError'

constructor(
message: string,
readonly issues: ReadonlyArray<unknown>,
) {
super(message)
}
}

export class AgentApprovalUnsupportedError extends Error {
override name = 'AgentApprovalUnsupportedError'

constructor(agentName: string) {
super(
`Agent "${agentName}" paused for an AI tool approval inside a durable step. Use Workflow ctx.approve() between agent calls until approval resumption is defined.`,
)
}
}

export class AgentStreamError extends Error {
override name = 'AgentStreamError'

constructor(
agentName: string,
message: string,
readonly code?: string,
) {
super(`Agent "${agentName}" failed: ${message}`)
}
}
8 changes: 8 additions & 0 deletions packages/ai-orchestration/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const AI_AGENT_META_KEY = 'tanstack.ai.agent'
export const AI_STREAM_CHUNK_EVENT = 'tanstack-ai.stream-chunk'

export const WORKFLOW_APPROVAL_REQUESTED_EVENT = 'workflow.approval.requested'
export const WORKFLOW_APPROVAL_RESOLVED_EVENT = 'workflow.approval.resolved'
export const WORKFLOW_SIGNAL_AWAITED_EVENT = 'workflow.signal.awaited'
export const WORKFLOW_SIGNAL_RESOLVED_EVENT = 'workflow.signal.resolved'
export const WORKFLOW_STEP_FAILED_EVENT = 'workflow.step.failed'
Loading
Loading