{"id":25059502,"url":"https://github.com/langtail/ai-orchestra","last_synced_at":"2026-01-08T18:04:31.828Z","repository":{"id":275495755,"uuid":"926248520","full_name":"langtail/ai-orchestra","owner":"langtail","description":"Simple orchestration for AI Agents built around Vercel's streamText. Lightweight alternative to LangGraph for agent handoffs and state transitions.","archived":false,"fork":false,"pushed_at":"2025-02-12T11:53:19.000Z","size":80,"stargazers_count":65,"open_issues_count":0,"forks_count":5,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-08T13:51:12.721Z","etag":null,"topics":["agents","ai","ai-sdk","anthropic","langgraph-js","llm","nextjs","openai","orche","state-machine","streaming","swarm","typescript","vercel-ai-sdk"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/langtail.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-02-02T22:11:26.000Z","updated_at":"2025-04-05T13:35:18.000Z","dependencies_parsed_at":"2025-03-31T10:43:43.777Z","dependency_job_id":null,"html_url":"https://github.com/langtail/ai-orchestra","commit_stats":null,"previous_names":["petrbrzek/ai-orchestra","langtail/ai-orchestra"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/langtail%2Fai-orchestra","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/langtail%2Fai-orchestra/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/langtail%2Fai-orchestra/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/langtail%2Fai-orchestra/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/langtail","download_url":"https://codeload.github.com/langtail/ai-orchestra/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252888971,"owners_count":21820103,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agents","ai","ai-sdk","anthropic","langgraph-js","llm","nextjs","openai","orche","state-machine","streaming","swarm","typescript","vercel-ai-sdk"],"created_at":"2025-02-06T15:06:50.597Z","updated_at":"2026-01-08T18:04:31.727Z","avatar_url":"https://github.com/langtail.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AI Orchestra 🎭\n\n![AI Orchestra](https://replicate.delivery/xezq/i34RlRAenhShMiJRCC3qt2eq2k43vIZmfcrToCu8KtGvV1WoA/tmp9q_4jc7w.jpg)\n\nSimple orchestration for AI Agents built around Vercel's `streamText`. AI Orchestra provides a lightweight way to handle agent handoffs and state transitions, similar to OpenAI's Swarm but with more developer control and less magic. Think of it as a simpler alternative to LangGraph, focused on streaming and agent coordination.\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n## Features\n\n- 🎯 **Simple Agent Handoffs** - Coordinate multiple AI agents with clear state transitions\n- 🌊 **Built for Streaming** - Native support for Vercel's `streamText` and AI SDK\n- 🔄 **Developer Control** - Full control over agent behavior and state transitions\n- 📝 **TypeScript First** - Built with TypeScript for excellent type safety\n- 🎭 **Swarm-like Patterns** - Similar patterns to OpenAI's Swarm, but more flexible\n- 🚀 **Minimal Dependencies** - Core dependencies are Vercel's `ai` package and `zod` for type safety\n\n## Installation\n\n```bash\n# Using npm\nnpm install ai-orchestra\n\n# Using yarn\nyarn add ai-orchestra\n\n# Using pnpm\npnpm add ai-orchestra\n\n# Using bun\nbun add ai-orchestra\n```\n\n## Example\n\nHere's how to use AI Orchestra with the Vercel AI SDK and tool handling:\n\n```typescript\nimport { anthropic } from '@ai-sdk/anthropic'\nimport { CoreMessage, streamText } from 'ai'\nimport { z } from 'zod'\nimport {\n  createOrchestra,\n  createToolResponse,\n  processStream,\n} from 'ai-orchestra'\n\n// Define your context with message history\ninterface MyContext {\n  messages: CoreMessage[]\n}\n\nconst orchestra = createOrchestra\u003cMyContext\u003e()({\n  // Intent classification state\n  intent: async (context, dispatch) =\u003e {\n    const chunks = streamText({\n      model: anthropic('claude-3-5-haiku-20241022'),\n      system:\n        'You are a helpful assistant that processes intents. If you need to handoff to a different agent, use the handoff tool.',\n      messages: context.messages,\n      maxSteps: 10,\n      tools: {\n        // Tool for telling jokes\n        joke: {\n          description: 'Tells a joke',\n          parameters: z.object({\n            topic: z.string().describe('The topic of the joke'),\n          }),\n          execute: async ({ topic }) =\u003e {\n            return `Why did the ${topic} cross the road?`\n          },\n        },\n        // Tool for handing off to another agent\n        handoffToPlanningAgent: {\n          description: 'Hand off to the planning agent',\n          parameters: z.object({\n            name: z.string().describe('The name of the planning agent'),\n          }),\n        },\n      },\n    })\n\n    // Process the stream and handle tool calls\n    const { finishReason, toolCalls, messages } = await processStream(\n      chunks,\n      dispatch\n    )\n\n    if (finishReason === 'tool-calls') {\n      for (const toolCall of toolCalls) {\n        if (toolCall.toolName === 'handoffToPlanningAgent') {\n          return {\n            nextState: 'plan',\n            context: {\n              messages: [\n                ...messages,\n                createToolResponse(toolCall, 'Handing off to intent agent'),\n              ],\n            },\n          }\n        }\n      }\n    }\n\n    return {\n      nextState: 'intent',\n      context: {\n        messages: [...context.messages, ...messages],\n      },\n    }\n  },\n\n  // Planning state\n  plan: async (context, dispatch) =\u003e {\n    const chunks = streamText({\n      model: anthropic('claude-3-5-haiku-20241022'),\n      system: 'You are a helpful assistant that plans events',\n      messages: context.messages,\n      tools: {\n        // Anthropic needs tools defined for any conversation with previous tool usage\n        // https://github.com/BerriAI/litellm/issues/5388\n        dummyTool: {\n          description: 'A dummy tool',\n          parameters: z.object({}),\n        },\n      },\n    })\n\n    const { messages } = await processStream(chunks, dispatch)\n\n    return {\n      context: {\n        messages: [...context.messages, ...messages],\n      },\n    }\n  },\n})\n\n// Run the orchestra\nconst run = await orchestra.createRun({\n  agent: 'intent',\n  context: {\n    messages: [\n      {\n        role: 'user',\n        content: 'Help me plan a party',\n      },\n    ],\n  },\n})\n\n// Listen to all events\nfor await (const event of run.events) {\n  console.log('Event:', event)\n}\n\n// Get the final state\nconst finalState = run.history[run.history.length - 1]\n```\n\n## Run Completion Callback\n\nAI Orchestra provides an `onFinish` callback that is called when a run completes. This is useful for performing actions like saving the final state to a database or triggering follow-up processes.\n\n```typescript\nconst run = await orchestra.createRun({\n  agent: 'intent',\n  context: {\n    messages: [\n      {\n        role: 'user',\n        content: 'Help me plan a party',\n      },\n    ],\n  },\n  onFinish: async (finalState) =\u003e {\n    // finalState contains:\n    // - agent: The name of the last agent that ran\n    // - context: The final context state\n    // - timestamp: When the run completed\n\n    await saveToDatabase({\n      lastAgent: finalState.agent,\n      finalContext: finalState.context,\n      completedAt: finalState.timestamp,\n    })\n  },\n})\n```\n\nThe `onFinish` callback receives a `finalState` object containing:\n\n- `agent`: The name of the last agent that executed\n- `context`: The final context state after all transitions\n- `timestamp`: The timestamp when the run completed\n\nThis callback is executed after all state transitions are complete and before the run's events are closed. It's a great place to:\n\n- Save results to a database\n- Trigger follow-up processes\n- Log completion metrics\n- Send notifications\n\nThe callback is optional and non-blocking, meaning the stream will continue to work while the callback executes.\n\n## Streaming Custom Data\n\nAI Orchestra supports streaming custom data alongside the model's response, which can be used with Vercel's `useChat` hook. This is useful for sending additional information like status updates, message IDs, or content references.\n\nHere's how you can dispatch custom data in your state handlers:\n\n```typescript\nimport { anthropic } from '@ai-sdk/anthropic'\nimport { CoreMessage, streamText } from 'ai'\nimport { z } from 'zod'\nimport { createOrchestra, processStream } from 'ai-orchestra'\n\nconst orchestra = createOrchestra\u003cMyContext\u003e()({\n  intent: async (context, dispatch) =\u003e {\n    // Dispatch custom data that will be available in useChat hook\n    await dispatch('ai-sdk-stream-chunk', {\n      type: 'data',\n      value: { status: 'initialized' },\n    })\n\n    const chunks = streamText({\n      model: anthropic('claude-3-5-haiku-20241022'),\n      system: 'You are a helpful assistant',\n      messages: context.messages,\n      maxSteps: 10,\n    })\n\n    // Process the stream and handle tool calls\n    const { messages } = await processStream(chunks, async (chunk) =\u003e {\n      // You can dispatch data during stream processing\n      await dispatch('ai-sdk-stream-chunk', {\n        type: 'data',\n        value: { progress: 'processing chunk' },\n      })\n    })\n\n    // Dispatch completion data\n    await dispatch('ai-sdk-stream-chunk', {\n      type: 'data',\n      value: { status: 'completed' },\n    })\n\n    return {\n      context: {\n        messages: [...context.messages, ...messages],\n      },\n    }\n  },\n})\n```\n\nOn the client side, you can access this data using the `useChat` hook:\n\n```typescript\nimport { useChat } from 'ai/react'\n\nexport default function Chat() {\n  const { messages, data, setData } = useChat()\n\n  return (\n    \u003cdiv\u003e\n      {/* Display stream status */}\n      {data \u0026\u0026 \u003cdiv\u003eStatus: {data[data.length - 1]?.status}\u003c/div\u003e}\n\n      {/* Display messages */}\n      {messages.map((m) =\u003e (\n        \u003cdiv key={m.id}\u003e{m.content}\u003c/div\u003e\n      ))}\n\n      {/* Chat input */}\n      \u003cform onSubmit={handleSubmit}\u003e\n        \u003cinput\n          value={input}\n          onChange={handleInputChange}\n          placeholder=\"Say something...\"\n        /\u003e\n      \u003c/form\u003e\n    \u003c/div\u003e\n  )\n}\n```\n\nThe streamed data will be automatically available in the `data` property of the `useChat` hook, and you can use `setData` to manage this data manually if needed.\n\n## Using with Next.js\n\nAI Orchestra provides a helper function `orchestraToAIStream` to easily integrate with Next.js API routes. Here's how to use it:\n\n```typescript\n// app/api/chat/route.ts\nimport { streamText } from 'ai'\nimport { z } from 'zod'\nimport {\n  createOrchestra,\n  orchestraToAIStream,\n  processStream,\n  createToolResponse,\n} from 'ai-orchestra'\nimport { openai } from '@ai-sdk/openai'\n\nexport const runtime = 'edge'\n\nexport async function POST(req: Request) {\n  const { messages } = await req.json()\n\n  const orchestra = createOrchestra\u003c{ messages: any[] }\u003e()({\n    chat: async (context, dispatch) =\u003e {\n      // You can dispatch status updates\n      await dispatch('ai-sdk-stream-chunk', {\n        type: 'data',\n        value: { status: 'started' },\n      })\n\n      const chunks = streamText({\n        model: openai('gpt-4o'),\n        messages: context.messages,\n        tools: {\n          handoffToJokeAgent: {\n            description: 'Handoff to joke agent',\n            parameters: z.object({\n              query: z.string().describe('Joke query'),\n            }),\n          },\n        },\n      })\n\n      const {\n        toolCalls,\n        finishReason,\n        messages: responseMessages,\n      } = await processStream(chunks, dispatch)\n\n      if (finishReason === 'tool-calls') {\n        for (const toolCall of toolCalls) {\n          if (toolCall.toolName === 'handoffToJokeAgent') {\n            return {\n              nextState: 'joke',\n              context: {\n                messages: [...responseMessages, createToolResponse(toolCall)],\n              },\n            }\n          }\n        }\n      }\n\n      return {\n        context: {\n          messages: [...context.messages, ...responseMessages],\n        },\n      }\n    },\n    joke: async (context, dispatch) =\u003e {\n      const chunks = streamText({\n        model: openai('gpt-4'),\n        system: 'You are a joke teller',\n        messages: context.messages,\n      })\n\n      const { messages: responseMessages } = await processStream(\n        chunks,\n        dispatch\n      )\n\n      return {\n        context: { messages: [...context.messages, ...responseMessages] },\n      }\n    },\n  })\n\n  // Create a run instance\n  const run = await orchestra.createRun({\n    agent: 'chat',\n    context: { messages },\n  })\n\n  // Convert orchestra events to AI SDK stream\n  const aiStream = await orchestraToAIStream(run)\n\n  // Return the stream with proper headers\n  return new Response(aiStream, {\n    headers: {\n      'Content-Type': 'text/event-stream',\n      'Cache-Control': 'no-cache',\n      Connection: 'keep-alive',\n    },\n  })\n}\n```\n\n## Core Functions\n\n### `createOrchestra\u003cTContext\u003e()`\n\nCreates an orchestra instance that manages state transitions and context for AI agents.\n\n```typescript\nconst orchestra = createOrchestra\u003c{ messages: any[] }\u003e()({\n  chat: async (context, dispatch) =\u003e {\n    // Your chat agent implementation\n    return {\n      nextState: 'nextAgent', // Optional, omit to end\n      context: {\n        /* updated context */\n      },\n    }\n  },\n  // More agents...\n})\n```\n\nThe function accepts a generic type parameter for your context and returns a factory that takes a record of handlers. Each handler is an async function that receives:\n\n- `context`: The current state context\n- `dispatch`: Function to emit events during processing\n\n### `processStream(stream, dispatch)`\n\nProcesses a stream from `streamText` and handles tool calls, messages, and events.\n\n```typescript\nconst { finishReason, toolCalls, messages } = await processStream(\n  chunks,\n  dispatch\n)\n```\n\nReturns:\n\n- `finishReason`: The reason the stream finished (e.g., 'tool-calls', 'stop')\n- `toolCalls`: Array of tool calls made during the stream\n- `messages`: Array of messages generated during the stream\n\n### `createToolResponse(toolCall, result?)`\n\nCreates a properly formatted tool response message for the AI SDK.\n\n```typescript\nconst response = createToolResponse(toolCall, 'Optional result message')\n```\n\nReturns a `CoreMessage` with:\n\n- `role`: 'tool'\n- `content`: Array containing the tool result with toolCallId, toolName, and result\n\n### `orchestraToAIStream(run)`\n\nConverts an orchestra run into a ReadableStream compatible with the AI SDK.\n\n```typescript\nconst run = await orchestra.createRun({\n  agent: 'chat',\n  context: { messages },\n})\n\nconst aiStream = await orchestraToAIStream(run)\n```\n\nHandles:\n\n- Converting orchestra events to AI SDK stream format\n- Formatting different types of chunks (text, tool calls, data, etc.)\n- Proper encoding and streaming of events\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## License\n\nMIT © [Petr Brzek](https://github.com/petrbrzek)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flangtail%2Fai-orchestra","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flangtail%2Fai-orchestra","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flangtail%2Fai-orchestra/lists"}