├── .env.example ├── original-port ├── .env.example ├── src │ ├── index.ts │ ├── utils │ │ ├── logger.ts │ │ ├── mergeDeep.ts │ │ ├── agentFuncDescToJson.ts │ │ ├── validateAgentFuncArgs.ts │ │ ├── mergeChunkAndToolCalls.ts │ │ └── logger.test.ts │ ├── types.ts │ ├── agent.ts │ ├── lib │ │ └── chatCompletion.ts │ └── swarm.ts ├── examples │ ├── custom │ │ ├── agents │ │ │ ├── email │ │ │ │ ├── agent.ts │ │ │ │ └── tools │ │ │ │ │ └── sendEmail.ts │ │ │ └── weather │ │ │ │ ├── agent.ts │ │ │ │ └── tools │ │ │ │ └── getWeather.ts │ │ └── main.ts │ ├── weather │ │ └── main.ts │ └── basic │ │ └── main.ts ├── package.json ├── ARCHITECTURE.md ├── README.md └── pnpm-lock.yaml ├── assets ├── swarm_diagram.png └── all_agents_chat_example.jpg ├── .eslintrc ├── .prettierrc ├── src ├── index.ts ├── types.ts ├── utils.ts ├── agent.ts └── swarm.ts ├── examples ├── local │ ├── index.ts │ └── localAgent.ts ├── pokemon │ ├── index.ts │ └── pokemonAgent.ts ├── filesystem │ ├── index.ts │ ├── filesystemAgent.ts │ └── filesystemTools.ts ├── webscraper │ ├── index.ts │ └── webScraperAgent.ts ├── triage-weather-email │ ├── triageAgent.ts │ ├── index.ts │ ├── emailAgent.ts │ └── weatherAgent.ts ├── all │ └── index.ts ├── run.ts └── readme-usage │ └── index.ts ├── tsup.config.ts ├── .changeset ├── config.json └── README.md ├── vitest.config.ts ├── tsconfig.json ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── LICENSE ├── package.json ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=your_api_key_here 2 | -------------------------------------------------------------------------------- /original-port/.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=your_api_key_here 2 | -------------------------------------------------------------------------------- /assets/swarm_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshmu/ts-swarm/HEAD/assets/swarm_diagram.png -------------------------------------------------------------------------------- /assets/all_agents_chat_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshmu/ts-swarm/HEAD/assets/all_agents_chat_example.jpg -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "ecmaVersion": 2020 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 80, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { Message } from './types'; 2 | export { runDemoLoop } from '../examples/run'; 3 | export { createAgent, type Agent } from './agent'; 4 | -------------------------------------------------------------------------------- /original-port/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Swarm } from './swarm'; 2 | export { 3 | Agent, 4 | type AgentFunction, 5 | type AgentFunctionDescriptor, 6 | createAgentFunction, 7 | } from './agent'; 8 | -------------------------------------------------------------------------------- /examples/local/index.ts: -------------------------------------------------------------------------------- 1 | import { runDemoLoop } from '../run'; 2 | import { localAgent } from './localAgent'; 3 | 4 | runDemoLoop({ 5 | initialAgentMessage: 'Would you like to know how many days are in a month?', 6 | initialAgent: localAgent, 7 | }); 8 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs', 'esm'], 6 | dts: true, 7 | sourcemap: true, 8 | clean: true, 9 | minify: true, 10 | splitting: false, 11 | }); 12 | -------------------------------------------------------------------------------- /examples/pokemon/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { pokemonAgent } from './pokemonAgent'; 3 | import { runDemoLoop } from '../run'; 4 | 5 | runDemoLoop({ 6 | initialAgentMessage: 'What Pokémon would you like to know about?', 7 | initialAgent: pokemonAgent, 8 | }); 9 | -------------------------------------------------------------------------------- /original-port/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Logs messages with a timestamp. 3 | */ 4 | export function logger(debug: boolean, ...args: any[]): void { 5 | if (!debug) return; 6 | const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; 7 | console.log(timestamp, ...args); 8 | } 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": true, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /original-port/examples/custom/agents/email/agent.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from '../../../../src/index'; 2 | import { sendEmail } from './tools/sendEmail'; 3 | 4 | export const emailAgent = new Agent({ 5 | name: 'Email Agent', 6 | instructions: 7 | 'You are an email assistant. Help users send emails when requested.', 8 | functions: [sendEmail], 9 | }); 10 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['**/*.test.ts'], 6 | exclude: ['**/*.spec.ts', '**/node_modules/**'], 7 | coverage: { 8 | provider: 'v8', 9 | include: ['src/**/*.ts'], 10 | exclude: ['src/**/*.spec.ts'], 11 | }, 12 | globals: true, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /examples/filesystem/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { filesystemAgent } from './filesystemAgent'; 3 | import { runDemoLoop } from '../run'; 4 | 5 | runDemoLoop({ 6 | initialAgentMessage: 7 | 'Hello! I can help you manage files and folders in the scratchpad workspace. I can create, read, update, delete, rename, and move both files and folders. What would you like to do?', 8 | initialAgent: filesystemAgent, 9 | }); 10 | -------------------------------------------------------------------------------- /original-port/examples/custom/agents/weather/agent.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from '../../../../src/index'; 2 | import { getWeather } from './tools/getWeather'; 3 | 4 | export const weatherAgent = new Agent({ 5 | name: 'Weather Agent', 6 | instructions: 7 | 'You are a weather assistant. Get weather information for the specified location, then transfer back to the Triage Agent with the result.', 8 | functions: [getWeather], 9 | }); 10 | -------------------------------------------------------------------------------- /examples/webscraper/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { webScraperAgent } from './webScraperAgent'; 3 | import { runDemoLoop } from '../run'; 4 | 5 | runDemoLoop({ 6 | initialAgentMessage: `Hello! I can help you analyze web content. I can: 7 | 1. Read URLs from your clipboard 8 | 2. Fetch and extract text from web pages 9 | 3. Answer questions about the content 10 | 4. Perform web searches 11 | Would you like me to check what URL is in your clipboard?`, 12 | initialAgent: webScraperAgent, 13 | }); 14 | -------------------------------------------------------------------------------- /examples/triage-weather-email/triageAgent.ts: -------------------------------------------------------------------------------- 1 | import { openai } from '@ai-sdk/openai'; 2 | import { createAgent } from '../../src/index'; 3 | 4 | export const triageAgent = createAgent({ 5 | id: 'Triage_Agent', 6 | model: openai('gpt-4o-2024-08-06', { structuredOutputs: true }), 7 | system: ` 8 | You are to answer the user's questions. 9 | If you are unable to answer the question, you should transfer responsibility to another agent to retrieve additional information to inform you answer. 10 | `, 11 | }); 12 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "./dist", 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "declaration": true, 11 | "sourceMap": true, 12 | "moduleResolution": "node", 13 | "lib": ["ES2020"], 14 | "noEmit": true 15 | }, 16 | "include": ["./src/**/*"], 17 | "exclude": ["node_modules", "dist"], 18 | 19 | /* AND if you're building for a library: */ 20 | "declaration": true, 21 | /* AND if you're building for a library in a monorepo: */ 22 | "declarationMap": true 23 | } 24 | -------------------------------------------------------------------------------- /original-port/examples/custom/agents/weather/tools/getWeather.ts: -------------------------------------------------------------------------------- 1 | import { type AgentFunction } from '../../../../../src/index'; 2 | 3 | export const getWeather: AgentFunction = { 4 | name: 'getWeather', 5 | func: ({ location }: { location: string }): string => { 6 | console.log('API Call: getWeather'); 7 | return `The weather in ${location} is sunny with a high of 32°C.`; 8 | }, 9 | descriptor: { 10 | name: 'getWeather', 11 | description: 'Get the weather for a specific location', 12 | parameters: { 13 | location: { 14 | type: 'string', 15 | required: true, 16 | description: 'The location to get weather for', 17 | }, 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /examples/triage-weather-email/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { runDemoLoop } from '../run'; 3 | import { weatherAgent } from './weatherAgent'; 4 | import { emailAgent } from './emailAgent'; 5 | import { triageAgent } from './triageAgent'; 6 | 7 | // give the triage agent the ability to transfer to the weather and email agents 8 | triageAgent.tools.push(weatherAgent, emailAgent); 9 | 10 | // give the weather and email agents the ability to transfer back to the triage agent 11 | weatherAgent.tools.push(triageAgent); 12 | emailAgent.tools.push(triageAgent); 13 | 14 | runDemoLoop({ 15 | initialAgentMessage: 16 | 'Hey, would you like to know about the weather or send an email?', 17 | initialAgent: triageAgent, 18 | }); 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | ci: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v4 22 | with: 23 | version: 9 24 | 25 | - name: Setup Node.js 20 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | 30 | - name: Install dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | - name: Run CI 34 | run: pnpm run ci 35 | env: 36 | CI: true 37 | -------------------------------------------------------------------------------- /original-port/src/utils/mergeDeep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Recursively merges fields from the source object into the target object. 3 | * Handles arrays by replacing them instead of merging. 4 | */ 5 | export function mergeDeep(target: any, source: any): any { 6 | if (Array.isArray(source)) { 7 | return [...source]; 8 | } 9 | if (typeof source !== 'object' || source === null) { 10 | return source; 11 | } 12 | return Object.keys(source).reduce( 13 | (acc, key) => { 14 | const sourceValue = source[key]; 15 | const targetValue = acc[key]; 16 | 17 | if (typeof sourceValue === 'string') { 18 | acc[key] = 19 | (typeof targetValue === 'string' ? targetValue : '') + sourceValue; 20 | } else { 21 | acc[key] = mergeDeep(targetValue, sourceValue); 22 | } 23 | 24 | return acc; 25 | }, 26 | { ...target }, 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /original-port/examples/custom/agents/email/tools/sendEmail.ts: -------------------------------------------------------------------------------- 1 | import { createAgentFunction } from '../../../../../src/index'; 2 | 3 | export const sendEmail = createAgentFunction({ 4 | name: 'sendEmail', 5 | func: ({ 6 | to, 7 | subject, 8 | body, 9 | }: { 10 | to: string; 11 | subject: string; 12 | body: string; 13 | }): string => { 14 | return `Sending email to ${to} with subject: ${subject}`; 15 | }, 16 | descriptor: { 17 | name: 'sendEmail', 18 | description: 'Send an email', 19 | parameters: { 20 | to: { 21 | type: 'string', 22 | required: true, 23 | description: 'Email recipient', 24 | }, 25 | subject: { 26 | type: 'string', 27 | required: true, 28 | description: 'Email subject', 29 | }, 30 | body: { 31 | type: 'string', 32 | required: true, 33 | description: 'Email body', 34 | }, 35 | }, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /original-port/src/utils/agentFuncDescToJson.ts: -------------------------------------------------------------------------------- 1 | import type { ChatCompletionTool } from 'openai/resources'; 2 | import { AgentFunctionDescriptor } from '../agent'; 3 | 4 | /** 5 | * Converts an agent function descriptor to JSON. 6 | */ 7 | export function agentFuncDescToJSON( 8 | descriptor: AgentFunctionDescriptor, 9 | ): ChatCompletionTool { 10 | return { 11 | type: 'function', 12 | function: { 13 | name: descriptor.name, 14 | description: descriptor.description, 15 | parameters: { 16 | type: 'object', 17 | properties: Object.entries(descriptor.parameters).reduce( 18 | (acc, [key, { type, description }]) => ({ 19 | ...acc, 20 | [key]: { type, description }, 21 | }), 22 | {}, 23 | ), 24 | required: Object.entries(descriptor.parameters) 25 | .filter(([, { required }]) => required) 26 | .map(([key]) => key), 27 | }, 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /examples/all/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { triageAgent } from '../triage-weather-email/triageAgent'; 3 | import { weatherAgent } from '../triage-weather-email/weatherAgent'; 4 | import { emailAgent } from '../triage-weather-email/emailAgent'; 5 | import { pokemonAgent } from '../pokemon/pokemonAgent'; 6 | import { filesystemAgent } from '../filesystem/filesystemAgent'; 7 | import { webScraperAgent } from '../webscraper/webScraperAgent'; 8 | import { localAgent } from '../local/localAgent'; 9 | import { runDemoLoop } from '../run'; 10 | 11 | const allAgents = [ 12 | triageAgent, 13 | weatherAgent, 14 | emailAgent, 15 | pokemonAgent, 16 | filesystemAgent, 17 | webScraperAgent, 18 | localAgent, 19 | ]; 20 | 21 | // Let all agents transfer to each other 22 | allAgents.forEach((agent) => { 23 | const otherAgents = allAgents.filter((a) => a.id !== agent.id); 24 | agent.tools.push(...otherAgents); 25 | }); 26 | 27 | runDemoLoop({ 28 | initialAgent: triageAgent, 29 | }); 30 | -------------------------------------------------------------------------------- /original-port/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-swarm__original-port", 3 | "version": "1.0.0", 4 | "description": "A minimal TypeScript Agentic library (original port) inspired by the OpenAI Swarm API.", 5 | "keywords": [ 6 | "swarm", 7 | "ai", 8 | "agentic", 9 | "typescript", 10 | "openai", 11 | "agent", 12 | "agentic-workflow" 13 | ], 14 | "type": "module", 15 | "main": "dist/index.js", 16 | "directories": { 17 | "example": "examples" 18 | }, 19 | "scripts": { 20 | "example:basic": "npx tsx examples/basic/main.ts", 21 | "example:weather": "npx tsx examples/weather/main.ts", 22 | "example:custom": "npx tsx examples/custom/main.ts" 23 | }, 24 | "dependencies": { 25 | "openai": "^4.57.0" 26 | }, 27 | "homepage": "https://github.com/joshmu/ts-swarm", 28 | "bugs": { 29 | "url": "https://github.com/joshmu/ts-swarm/issues" 30 | }, 31 | "author": "Josh Mu (https://joshmu.dev)", 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/joshmu/ts-swarm.git" 35 | }, 36 | "license": "MIT" 37 | } 38 | -------------------------------------------------------------------------------- /original-port/src/utils/validateAgentFuncArgs.ts: -------------------------------------------------------------------------------- 1 | import { AgentFunctionDescriptor } from '../agent'; 2 | 3 | /** 4 | * Validates the arguments against the function descriptor. 5 | * Throws an error if validation fails. 6 | */ 7 | export function validateAgentFuncArgs( 8 | args: any, 9 | descriptor: AgentFunctionDescriptor, 10 | ): Record { 11 | const validateParam = (key: string, param: any) => { 12 | if (param.required && !(key in args)) { 13 | throw new Error(`Missing required parameter: ${key}`); 14 | } 15 | if (key in args) { 16 | const expectedType = param.type.toLowerCase(); 17 | const actualType = typeof args[key]; 18 | if (actualType !== expectedType) { 19 | throw new Error( 20 | `Invalid type for parameter '${key}': expected '${expectedType}', got '${actualType}'`, 21 | ); 22 | } 23 | return { [key]: args[key] }; 24 | } 25 | return {}; 26 | }; 27 | 28 | return Object.entries(descriptor.parameters).reduce( 29 | (acc, [key, param]) => ({ ...acc, ...validateParam(key, param) }), 30 | {}, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Josh Mu 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 | -------------------------------------------------------------------------------- /examples/filesystem/filesystemAgent.ts: -------------------------------------------------------------------------------- 1 | import { openai } from '@ai-sdk/openai'; 2 | import { createAgent } from '../../src/index'; 3 | import { fileSystemTools } from './filesystemTools'; 4 | 5 | export const filesystemAgent = createAgent({ 6 | id: 'Filesystem_Agent', 7 | model: openai('gpt-4o-2024-08-06', { structuredOutputs: true }), 8 | system: ` 9 | You are a filesystem management agent that can perform operations on files and folders within the scratchpad workspace. 10 | 11 | Before performing any operation that modifies the filesystem (create, update, delete, rename, move), you MUST: 12 | 1. Clearly explain what you're about to do 13 | 2. Ask for explicit confirmation from the user 14 | 3. Only proceed if the user confirms with a clear "yes" or similar affirmative 15 | 16 | For read and list operations, you can proceed without confirmation. 17 | 18 | Always provide clear feedback about the operations performed and their results. 19 | If an error occurs, explain it in user-friendly terms. 20 | 21 | All paths must remain within the scratchpad directory for security. 22 | `, 23 | tools: fileSystemTools, 24 | }); 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4 20 | with: 21 | version: 9 22 | 23 | - name: Setup Node.js 20 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | 28 | - name: Install Dependencies 29 | run: pnpm install --frozen-lockfile 30 | 31 | - name: Create Release Pull Request or Publish to npm 32 | id: changesets 33 | uses: changesets/action@v1 34 | with: 35 | publish: pnpm ci:release 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 39 | 40 | - name: Success Notification 41 | if: steps.changesets.outputs.published == 'true' 42 | # You can do something when a publish happens. 43 | run: echo "Published new version of ${GITHUB_REPOSITORY} to NPM" 44 | -------------------------------------------------------------------------------- /examples/triage-weather-email/emailAgent.ts: -------------------------------------------------------------------------------- 1 | import { tool } from 'ai'; 2 | import { z } from 'zod'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { createAgent } from '../../src/index'; 5 | 6 | export const emailAgent = createAgent({ 7 | id: 'Email_Agent', 8 | model: openai('gpt-4o-2024-08-06', { structuredOutputs: true }), 9 | system: ` 10 | You are an email agent. You need to send an email. 11 | Once you have enough information you should request for the user to confirm the email details. 12 | You should attempt to resolve the user's request based on the tools you have available. 13 | After which, if you are still unable to fulfil the user's request you should transfer responsibility to another agent. 14 | `, 15 | tools: [ 16 | { 17 | id: 'email', 18 | description: 'A tool for sending an email.', 19 | parameters: z.object({ 20 | to: z.string().describe('The email address of the recipient'), 21 | subject: z.string().describe('The subject of the email'), 22 | body: z.string().describe('The body of the email'), 23 | }), 24 | execute: async ({ to, subject, body }) => { 25 | return `Email sent to ${to} with subject "${subject}" and body "${body}".`; 26 | }, 27 | }, 28 | ], 29 | }); 30 | -------------------------------------------------------------------------------- /original-port/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from './agent'; 2 | 3 | /** 4 | * Represents the response from the Swarm. 5 | */ 6 | export interface Response { 7 | messages: Array; 8 | agent?: Agent; 9 | context_variables: Record; 10 | } 11 | 12 | export const createSwarmResult = ( 13 | params: Partial = {}, 14 | ): Response => ({ 15 | messages: params.messages ?? [], 16 | agent: params.agent, 17 | context_variables: params.context_variables ?? {}, 18 | }); 19 | 20 | /** 21 | * Represents the result of a function executed by an agent. 22 | */ 23 | export interface Result { 24 | value: string; 25 | agent?: Agent; 26 | context_variables: Record; 27 | } 28 | 29 | export const createAgentFunctionResult = ( 30 | params: Partial = {}, 31 | ): Result => ({ 32 | value: params.value ?? '', 33 | agent: params.agent, 34 | context_variables: params.context_variables ?? {}, 35 | }); 36 | 37 | /** 38 | * Represents a function callable by the agent. 39 | */ 40 | export interface ToolFunction { 41 | arguments: string; 42 | name: string; 43 | } 44 | 45 | export const createToolFunction = ( 46 | params: Partial = {}, 47 | ): ToolFunction => ({ 48 | arguments: params.arguments ?? '', 49 | name: params.name ?? '', 50 | }); 51 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { CoreTool, generateText, CoreMessage } from 'ai'; 2 | import { type Agent } from './agent'; 3 | 4 | type GenerateText = typeof generateText; 5 | export type GenerateTextParams = Parameters[0]; 6 | type CustomCoreTool = CoreTool & { id: string }; 7 | export type SwarmTool = Agent | CustomCoreTool; 8 | export type SwarmMessageMeta = { 9 | swarmMeta?: { 10 | agentId: string; 11 | }; 12 | }; 13 | export type SwarmResult = { 14 | messages: Message[]; 15 | /** 16 | * The current active agent 17 | */ 18 | activeAgent: Agent; 19 | contextVariables: Record; 20 | }; 21 | export type RunSwarmOptions = { 22 | activeAgent: Agent; 23 | /** 24 | * Messages could be CoreMessage[] with additional swarm meta fields 25 | */ 26 | messages: Message[]; 27 | contextVariables?: Record; 28 | modelOverride?: string; 29 | debug?: boolean; 30 | maxTurns?: number; 31 | /** 32 | * Callback when new messages are received 33 | */ 34 | onMessages?: (messages: Message[]) => void; 35 | }; 36 | export type ReturnGenerateText = Awaited>; 37 | export type Tools = ReturnGenerateText['toolCalls']; 38 | export type ToolResults = ReturnGenerateText['toolResults']; 39 | export type Message = CoreMessage & SwarmMessageMeta; 40 | -------------------------------------------------------------------------------- /original-port/src/utils/mergeChunkAndToolCalls.ts: -------------------------------------------------------------------------------- 1 | import { mergeDeep } from './mergeDeep'; 2 | 3 | /** 4 | * Merges a delta chunk into the final response. 5 | * Support for tool calls 6 | */ 7 | export function mergeChunkAndToolCalls( 8 | finalResponse: Record, 9 | delta: Record, 10 | ): Record { 11 | // Destructure the delta object, separating tool_calls for special handling 12 | const { role, tool_calls, ...rest } = delta; 13 | 14 | // Handle merging of tool_calls 15 | const mergedToolCalls = tool_calls 16 | ? tool_calls.reduce((acc: any[], deltaToolCall: any) => { 17 | // Extract properties from the current tool call 18 | const { index, id, type, function: func } = deltaToolCall; 19 | // Initialize the accumulator at the current index if it doesn't exist 20 | if (!acc[index]) acc[index] = {}; 21 | // Merge properties into the accumulator 22 | if (id) acc[index].id = id; 23 | if (type) acc[index].type = type; 24 | if (func) { 25 | // Deep merge the function property 26 | acc[index].function = mergeDeep(acc[index].function || {}, func); 27 | } 28 | return acc; 29 | }, finalResponse.tool_calls || []) 30 | : finalResponse.tool_calls; 31 | 32 | return { 33 | ...(mergedToolCalls && { tool_calls: mergedToolCalls }), 34 | ...mergeDeep(finalResponse, rest), 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /original-port/src/agent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChatCompletionToolChoiceOption, 3 | ChatCompletionCreateParams, 4 | } from 'openai/resources'; 5 | 6 | export class Agent { 7 | name: string; 8 | model: ChatCompletionCreateParams['model']; 9 | instructions: string | ((contextVariables: Record) => string); 10 | functions: AgentFunction[]; 11 | tool_choice?: ChatCompletionToolChoiceOption; 12 | parallel_tool_calls: ChatCompletionCreateParams['parallel_tool_calls']; 13 | 14 | constructor(params: Partial = {}) { 15 | this.name = params.name ?? 'Agent'; 16 | this.model = params.model ?? 'gpt-4o-2024-08-06'; 17 | this.instructions = params.instructions ?? 'You are a helpful agent.'; 18 | this.functions = params.functions ?? []; 19 | this.tool_choice = params.tool_choice; 20 | this.parallel_tool_calls = params.parallel_tool_calls ?? true; 21 | } 22 | } 23 | 24 | export interface AgentFunctionDescriptor { 25 | name: string; 26 | description: string; 27 | parameters: Record< 28 | string, 29 | { type: string; required: boolean; description: string } 30 | >; 31 | } 32 | 33 | export function createAgentFunction(arg: AgentFunction): AgentFunction { 34 | return arg; 35 | } 36 | 37 | export interface AgentFunction { 38 | name: string; 39 | /** 40 | * The function that the agent will execute. 41 | */ 42 | func: (args: any) => string | Agent | Record; 43 | descriptor: AgentFunctionDescriptor; 44 | } 45 | -------------------------------------------------------------------------------- /examples/local/localAgent.ts: -------------------------------------------------------------------------------- 1 | import { createAgent } from '../../src/index'; 2 | import { ollama } from 'ollama-ai-provider'; 3 | import { z } from 'zod'; 4 | 5 | type OllamaModel = Parameters[0]; 6 | // Must ensure we use a model that supports tool calling 7 | const LOCAL_MODEL: OllamaModel = 'llama3.1'; 8 | 9 | export const localAgent = createAgent({ 10 | id: 'Local-Agent', 11 | model: ollama(LOCAL_MODEL), 12 | system: ` 13 | You are a helpful assistant that can answer questions and help with tasks. 14 | Always respond by mentioning you are running on a local machine with model "${LOCAL_MODEL}". 15 | 16 | The tools you have access to are: 17 | - get-days-in-month: Get the number of days in a month 18 | 19 | NEVER make up your own parameter values as tool function arguments, you must retrieve this from the user! 20 | NEVER use tool functions if not asked, instead revert to normal chat! 21 | `, 22 | tools: [ 23 | // tool to return number of days in a month 24 | { 25 | id: 'get-days-in-month', 26 | description: 'Get the number of days in a month', 27 | parameters: z.object({ 28 | month: z 29 | .number() 30 | .describe('The month number to get the number of days for'), 31 | year: z.number().describe('The year to get the number of days for'), 32 | }), 33 | execute: async ({ month, year }) => { 34 | return new Date(year, month, 0).getDate(); 35 | }, 36 | }, 37 | ], 38 | }); 39 | -------------------------------------------------------------------------------- /original-port/src/utils/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | describe, 3 | it, 4 | expect, 5 | vi, 6 | beforeEach, 7 | afterEach, 8 | MockInstance, 9 | } from 'vitest'; 10 | import { logger } from './logger'; 11 | 12 | describe('logger', () => { 13 | let consoleLogSpy: MockInstance; 14 | const originalConsoleLog = console.log; 15 | 16 | beforeEach(() => { 17 | // Mock console.log 18 | consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); 19 | // Mock Date to return a fixed timestamp 20 | vi.useFakeTimers(); 21 | vi.setSystemTime(new Date('2023-04-01T12:34:56.789Z')); 22 | }); 23 | 24 | afterEach(() => { 25 | // Restore the original console.log and system time 26 | console.log = originalConsoleLog; 27 | vi.useRealTimers(); 28 | vi.restoreAllMocks(); 29 | }); 30 | 31 | it('should log messages with timestamp when debug is true', () => { 32 | logger(true, 'Test message'); 33 | expect(consoleLogSpy).toHaveBeenCalledWith('12:34:56', 'Test message'); 34 | }); 35 | 36 | it('should not log messages when debug is false', () => { 37 | logger(false, 'Test message'); 38 | expect(consoleLogSpy).not.toHaveBeenCalled(); 39 | }); 40 | 41 | it('should handle multiple arguments', () => { 42 | logger(true, 'Message 1', 'Message 2', 123); 43 | expect(consoleLogSpy).toHaveBeenCalledWith( 44 | '12:34:56', 45 | 'Message 1', 46 | 'Message 2', 47 | 123, 48 | ); 49 | }); 50 | 51 | it('should handle objects and arrays', () => { 52 | const testObj = { key: 'value' }; 53 | const testArray = [1, 2, 3]; 54 | logger(true, 'Object:', testObj, 'Array:', testArray); 55 | expect(consoleLogSpy).toHaveBeenCalledWith( 56 | '12:34:56', 57 | 'Object:', 58 | testObj, 59 | 'Array:', 60 | testArray, 61 | ); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /original-port/examples/weather/main.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { Swarm, Agent, createAgentFunction } from '../../src/index'; 3 | 4 | const getWeather = createAgentFunction({ 5 | name: 'getWeather', 6 | func: ({ location }: { location: string }): string => { 7 | // mock API call... 8 | return `The weather in ${location} is sunny with a high of 32°C.`; 9 | }, 10 | descriptor: { 11 | name: 'getWeather', 12 | description: 'Get the weather for a specific location', 13 | parameters: { 14 | location: { 15 | type: 'string', 16 | required: true, 17 | description: 'The location to get weather for', 18 | }, 19 | }, 20 | }, 21 | }); 22 | 23 | const weatherAgent = new Agent({ 24 | name: 'Weather Agent', 25 | instructions: 'You are a weather assistant.', 26 | functions: [getWeather], 27 | }); 28 | 29 | const transferToWeatherAgent = createAgentFunction({ 30 | name: 'transferToWeatherAgent', 31 | func: () => weatherAgent, 32 | descriptor: { 33 | name: 'transferToWeatherAgent', 34 | description: 'Transfer the conversation to the Weather Agent', 35 | parameters: {}, 36 | }, 37 | }); 38 | 39 | const triageAgent = new Agent({ 40 | name: 'Triage Agent', 41 | instructions: 42 | "You are a helpful triage agent. Determine which agent is best suited to handle the user's request, and transfer the conversation to that agent.", 43 | functions: [transferToWeatherAgent], 44 | tool_choice: 'auto', 45 | parallel_tool_calls: false, 46 | }); 47 | 48 | const swarm = new Swarm({ apiKey: process.env.OPENAI_API_KEY }); 49 | 50 | // Run the swarm 51 | const result = await swarm.run({ 52 | agent: triageAgent, 53 | messages: [{ role: 'user', content: "What's the weather like in New York?" }], 54 | }); 55 | 56 | const lastMessage = result.messages.at(-1); 57 | console.log(lastMessage.content); 58 | // result: The weather in New York is sunny with a high of 32°C. 59 | -------------------------------------------------------------------------------- /examples/run.ts: -------------------------------------------------------------------------------- 1 | import readline from 'readline'; 2 | import { colors, prettyLogMsgs } from '../src/utils'; 3 | import { Agent, Message } from '../src/index'; 4 | 5 | /** 6 | * Create a readline interface so we can capture user input in the terminal 7 | */ 8 | const rl = readline.createInterface({ 9 | input: process.stdin, 10 | output: process.stdout, 11 | }); 12 | 13 | async function promptUser(): Promise { 14 | return new Promise((resolve) => { 15 | rl.question(`${colors.green}👤 User:${colors.reset} `, (input) => { 16 | resolve(input); 17 | }); 18 | }); 19 | } 20 | 21 | export async function runDemoLoop({ 22 | initialAgentMessage = 'Hey, how can I help you today?', 23 | initialAgent, 24 | }: { 25 | initialAgentMessage?: string; 26 | initialAgent: Agent; 27 | }) { 28 | prettyLogMsgs([ 29 | { 30 | role: 'assistant', 31 | content: initialAgentMessage, 32 | swarmMeta: { agentId: initialAgent.id }, 33 | }, 34 | ]); 35 | 36 | let messages: Message[] = []; 37 | 38 | let activeAgent = initialAgent; 39 | while (true) { 40 | const userInput = await promptUser(); 41 | 42 | // option for user to exit the conversation 43 | if (userInput.toLowerCase() === 'exit') { 44 | rl.close(); 45 | break; 46 | } 47 | 48 | // add the user message to the messages array 49 | messages.push({ 50 | role: 'user', 51 | content: userInput, 52 | }); 53 | 54 | // run the agent with swarm orchestration 55 | const result = await activeAgent.run({ 56 | messages, 57 | // using a callback to log the messages so we can get logs within each loop 58 | onMessages: prettyLogMsgs, 59 | }); 60 | 61 | // Or we could just log all the new messages after running the swarm loop 62 | // prettyLogMsgs(result.messages); 63 | 64 | // update the state 65 | activeAgent = result.activeAgent; 66 | messages = [...messages, ...result.messages]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /original-port/ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | Swarm focuses on making agent coordination and execution lightweight, highly controllable, and easily testable. 4 | 5 | ![Swarm Diagram](../assets/swarm_diagram.png) 6 | 7 | ## Core Components 8 | 9 | 1. **Swarm**: The main orchestrator that manages agents and their interactions. 10 | 2. **Agent**: Represents an AI agent with specific capabilities and instructions. 11 | 3. **AgentFunction**: A function that an agent can execute. 12 | 13 | ## High-Level Overview 14 | 15 | TS-SWARM follows a modular architecture that allows for easy extension and customization. The project is structured as follows: 16 | 17 | ```mermaid 18 | graph TD 19 | A((Swarm)) --> B[Agent] 20 | A --> C[EventEmitter] 21 | B --> D[AgentFunction] 22 | B --> E[Instructions] 23 | A --> F[OpenAI API] 24 | A --> G[Context Management] 25 | A --> H[Tool Calls] 26 | I[Utils] --> A 27 | I --> B 28 | J[Types] --> A 29 | J --> B 30 | ``` 31 | 32 | ## Example Sequence Flow 33 | 34 | Here's a simplified sequence diagram showing how the components interact in a typical scenario: 35 | 36 | ```mermaid 37 | sequenceDiagram 38 | participant User 39 | participant Swarm 40 | participant Agent 41 | participant AgentFunction 42 | participant OpenAI API 43 | User->>Swarm: Run with initial message 44 | Swarm->>Agent: Process message 45 | Agent->>OpenAI API: Generate response 46 | OpenAI API-->>Agent: Response with tool calls 47 | Agent->>AgentFunction: Execute tool call 48 | AgentFunction-->>Agent: Tool call result 49 | Agent->>Swarm: Updated response and context 50 | Swarm->>User: Final result 51 | ``` 52 | 53 | 1. The user initiates a request to the Swarm with an initial message. 54 | 2. The Swarm passes the message to the appropriate Agent. 55 | 3. The Agent processes the message and uses the OpenAI API to generate a response. 56 | 4. If the response includes tool calls, the Agent executes the corresponding agent function. 57 | 5. The Agent updates its response and context based on the tool call results. 58 | 6. The Swarm returns the final result to the user. 59 | 60 | This process can repeat multiple times, with the Swarm managing context and potentially switching between different specialized Agents as needed. 61 | -------------------------------------------------------------------------------- /examples/triage-weather-email/weatherAgent.ts: -------------------------------------------------------------------------------- 1 | import { tool } from 'ai'; 2 | import { z } from 'zod'; 3 | import { openai } from '@ai-sdk/openai'; 4 | import { createAgent } from '../../src/index'; 5 | 6 | // Helper function to fetch weather data 7 | async function fetchWeather(location: string) { 8 | try { 9 | // Format parameters: 10 | // %c - Weather condition symbol 11 | // %C - Weather condition text 12 | // %t - Temperature (Celsius) 13 | // %h - Humidity 14 | // %w - Wind 15 | // %p - Precipitation (mm) 16 | // %m - Moonphase 🌑🌒🌓🌔🌕🌖🌗🌘 17 | const response = await fetch( 18 | `https://wttr.in/${encodeURIComponent(location)}?format=%c+%C+%t(%f)+%h+%w+%p+%m` 19 | ); 20 | 21 | if (!response.ok) { 22 | throw new Error('Failed to fetch weather data'); 23 | } 24 | 25 | const data = await response.text(); 26 | return data; 27 | } catch (error) { 28 | console.error('Error fetching weather:', error); 29 | return 'Unable to fetch weather data at this time.'; 30 | } 31 | } 32 | 33 | export const weatherAgent = createAgent({ 34 | id: 'Weather_Agent', 35 | model: openai('gpt-4o-2024-08-06', { structuredOutputs: true }), 36 | system: ` 37 | You are a weather agent that provides accurate weather information using real-time data. 38 | You should interpret the weather symbols and data provided by the weather tool and present it in a clear, 39 | natural way to the user. 40 | 41 | The weather data includes: 42 | - Weather condition (symbol and text) 43 | - Temperature 44 | - Humidity 45 | - Wind conditions 46 | - Precipitation 47 | - Moon phase 48 | 49 | Present this information in a friendly, conversational manner. 50 | If you cannot fulfill the user's request, transfer responsibility to another agent. 51 | `, 52 | tools: [ 53 | { 54 | id: 'weather', 55 | description: 'Get real-time weather information for a specific location', 56 | parameters: z.object({ 57 | location: z.string().describe('The location to get weather for'), 58 | }), 59 | execute: async ({ location }) => { 60 | const weatherData = await fetchWeather(location.toLowerCase()); 61 | return weatherData; 62 | }, 63 | }, 64 | ], 65 | }); 66 | -------------------------------------------------------------------------------- /examples/readme-usage/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the usage example for the README.md file. 3 | */ 4 | 5 | import 'dotenv/config'; 6 | import { createAgent } from '../../src'; 7 | import { openai } from '@ai-sdk/openai'; // Ensure OPENAI_API_KEY environment variable is set 8 | import { z } from 'zod'; 9 | 10 | // Create the Weather Agent 11 | const weatherAgent = createAgent({ 12 | id: 'Weather_Agent', 13 | model: openai('gpt-4o-2024-08-06', { structuredOutputs: true }), 14 | system: ` 15 | You are a weather assistant. 16 | Your role is to: 17 | - Provide weather information for requested locations 18 | - Use the weather tool to fetch weather data`, 19 | tools: [ 20 | { 21 | id: 'weather', 22 | description: 'Get the weather for a specific location', 23 | parameters: z.object({ 24 | location: z.string().describe('The location to get weather for'), 25 | }), 26 | execute: async ({ location }) => { 27 | // Mock weather API call 28 | return `The weather in ${location} is sunny with a high of 67°F.`; 29 | }, 30 | }, 31 | ], 32 | }); 33 | 34 | // Create the Triage Agent 35 | const triageAgent = createAgent({ 36 | id: 'Triage_Agent', 37 | model: openai('gpt-4o-2024-08-06', { structuredOutputs: true }), 38 | system: ` 39 | You are a helpful triage agent. 40 | Your role is to: 41 | - Answer the user's questions by transferring to the appropriate agent`, 42 | tools: [ 43 | // Add ability to transfer to the weather agent 44 | weatherAgent, 45 | ], 46 | }); 47 | 48 | async function demo() { 49 | /** 50 | * Run the triage agent with swarm orchestration 51 | * Enabling tool calling and agent handoffs 52 | */ 53 | const result = await triageAgent.run({ 54 | // Example conversation passed in 55 | messages: [ 56 | { role: 'user', content: "What's the weather like in New York?" }, 57 | ], 58 | }); 59 | 60 | /** 61 | * We could wrap this logic in a loop to continue the conversation by 62 | * utilizing `result.activeAgent` which represents the last active agent during the run 63 | * For this example `result.activeAgent` would now be the weather agent 64 | * Refer to the `run.ts` example for an example of this 65 | */ 66 | 67 | // Log the last message (or the entire conversation if you prefer) 68 | const lastMessage = result.messages.at(-1); 69 | console.log( 70 | `${lastMessage?.swarmMeta?.agentId || 'User'}: ${lastMessage?.content}`, 71 | ); 72 | 73 | // done 74 | process.exit(0); 75 | } 76 | 77 | demo(); 78 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { TextPart, ToolCallPart, ToolResultPart } from 'ai'; 2 | import { RunSwarmOptions, Message } from './types'; 3 | 4 | /** 5 | * Colors for logging 6 | */ 7 | export const colors = { 8 | reset: '\x1b[0m', 9 | blue: '\x1b[34m', // for agents 10 | green: '\x1b[32m', // for user 11 | yellow: '\x1b[33m', // for tool calls 12 | } as const; 13 | 14 | /** 15 | * Pretty log message list 16 | * based on the message response object 17 | */ 18 | export function prettyLogMsgs(messages: Message[]) { 19 | messages.forEach((message) => { 20 | if (Array.isArray(message.content)) { 21 | message.content.forEach((content: Message['content'][number]) => { 22 | checkMessage(message, content); 23 | }); 24 | } else { 25 | checkMessage(message, message.content); 26 | } 27 | }); 28 | } 29 | 30 | function checkMessage(message: Message, content: Message['content'][number]) { 31 | userMessage(message); 32 | assistantMessage(message, content as TextPart | ToolCallPart); 33 | toolMessage(message, content as ToolResultPart); 34 | } 35 | function userMessage(message: Message) { 36 | if (message.role === 'user') { 37 | console.log(`${colors.green}👤 User:${colors.reset} ${message.content}`); 38 | } 39 | } 40 | function assistantMessage(message: Message, content: TextPart | ToolCallPart) { 41 | if (message.role === 'assistant') { 42 | if (typeof content === 'string' && content) { 43 | console.log( 44 | `${colors.blue}🤖 ${message.swarmMeta?.agentId}:${colors.reset} ${content}`, 45 | ); 46 | } else if (content.type === 'text' && content.text) { 47 | console.log( 48 | `${colors.blue}🤖 ${message.swarmMeta?.agentId}:${colors.reset} ${content.text}`, 49 | ); 50 | } else if (content.type === 'tool-call') { 51 | console.log( 52 | `${colors.blue}🤖 ${message.swarmMeta?.agentId} ${colors.yellow} (TOOL CALL - ${content.toolName}):${colors.reset} ${JSON.stringify(content.args)}`, 53 | ); 54 | } 55 | } 56 | } 57 | function toolMessage(message: Message, content: ToolResultPart) { 58 | if (message.role === 'tool') { 59 | if (content.type === 'tool-result') { 60 | console.log( 61 | `${colors.blue}🤖 ${message.swarmMeta?.agentId} ${colors.yellow} (TOOL RESULT - ${content.toolName}):${colors.reset} ${JSON.stringify(content.result)}`, 62 | ); 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * Debug log 69 | */ 70 | export function debugLog( 71 | debug: RunSwarmOptions['debug'], 72 | args: Parameters[0], 73 | ) { 74 | if (debug) console.dir(args, { depth: Infinity }); 75 | } 76 | -------------------------------------------------------------------------------- /original-port/src/lib/chatCompletion.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI } from 'openai'; 2 | import { Agent } from '../agent'; 3 | import { logger } from '../utils/logger'; 4 | import type { ChatCompletion, ChatCompletionChunk } from 'openai/resources'; 5 | import type { Stream } from 'openai/streaming'; 6 | import { agentFuncDescToJSON } from '../utils/agentFuncDescToJson'; 7 | 8 | /** 9 | * @todo: this should eventually be an adapter for other ai services... 10 | */ 11 | 12 | const CTX_VARS_NAME = 'context_variables'; 13 | 14 | export function getChatCompletion( 15 | client: OpenAI, 16 | agent: Agent, 17 | history: Array, 18 | context_variables: Record, 19 | model_override?: string, 20 | stream?: false, 21 | debug?: boolean, 22 | ): Promise; 23 | 24 | export function getChatCompletion( 25 | client: OpenAI, 26 | agent: Agent, 27 | history: Array, 28 | context_variables: Record, 29 | model_override?: string, 30 | stream?: true, 31 | debug?: boolean, 32 | ): Promise>; 33 | 34 | export function getChatCompletion( 35 | client: OpenAI, 36 | agent: Agent, 37 | history: Array, 38 | context_variables: Record, 39 | model_override = '', 40 | stream = false, 41 | debug = false, 42 | ): Promise> { 43 | const ctxVars = structuredClone(context_variables); 44 | const instructions = 45 | typeof agent.instructions === 'function' 46 | ? agent.instructions(ctxVars) 47 | : agent.instructions; 48 | const messages = [{ role: 'system', content: instructions }, ...history]; 49 | logger(debug, 'Getting chat completion for...', messages); 50 | 51 | const tools = agent.functions.map((func) => 52 | agentFuncDescToJSON(func.descriptor), 53 | ); 54 | // Hide context_variables from model 55 | tools.forEach((tool) => { 56 | delete (tool.function.parameters as any).properties?.[CTX_VARS_NAME]; 57 | const requiredIndex = (tool.function.parameters as any).required.indexOf( 58 | CTX_VARS_NAME, 59 | ); 60 | if (requiredIndex !== -1) { 61 | (tool.function.parameters as any).required.splice(requiredIndex, 1); 62 | } 63 | }); 64 | 65 | const createParams: OpenAI.Chat.Completions.ChatCompletionCreateParams = { 66 | model: model_override || agent.model, 67 | messages, 68 | tools: tools.length > 0 ? tools : undefined, 69 | tool_choice: agent.tool_choice, 70 | stream, 71 | }; 72 | 73 | if (tools.length > 0) { 74 | createParams.parallel_tool_calls = agent.parallel_tool_calls; 75 | } 76 | 77 | return client.chat.completions.create(createParams); 78 | } 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-swarm", 3 | "version": "1.2.7", 4 | "description": "TS-SWARM is a minimal TypeScript Agentic library mixing the simplicity of the OpenAI Swarm API with the flexibility of the Vercel AI SDK.", 5 | "keywords": [ 6 | "swarm", 7 | "ai", 8 | "agentic", 9 | "typescript", 10 | "openai", 11 | "agent", 12 | "agentic-workflow", 13 | "node", 14 | "vercel" 15 | ], 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "type": "module", 20 | "main": "dist/index.js", 21 | "files": [ 22 | "dist" 23 | ], 24 | "exports": { 25 | "./package.json": "./package.json", 26 | ".": { 27 | "import": "./dist/index.js", 28 | "require": "./dist/index.cjs" 29 | } 30 | }, 31 | "scripts": { 32 | "build": "tsup src/index.ts --format cjs,esm --dts", 33 | "ci": "pnpm build && pnpm check:format && pnpm check:types && pnpm lint && pnpm test", 34 | "lint": "tsc", 35 | "format": "prettier --write .", 36 | "check:format": "prettier --check ./src", 37 | "check:types": "attw --pack . --ignore-rules=cjs-resolves-to-esm", 38 | "test": "vitest run", 39 | "test:watch": "vitest", 40 | "test:coverage": "vitest run --coverage", 41 | "changeset": "changeset", 42 | "release": "changeset version && changeset publish", 43 | "ci:release": "pnpm build && pnpm release", 44 | "prepublishOnly": "pnpm run ci", 45 | "example:readme-usage": "npx tsx examples/readme-usage", 46 | "example:triage-weather-email": "npx tsx examples/triage-weather-email", 47 | "example:pokemon": "npx tsx examples/pokemon", 48 | "example:filesystem": "npx tsx examples/filesystem", 49 | "example:webscraper": "npx tsx examples/webscraper", 50 | "example:local": "npx tsx examples/local", 51 | "example:all": "npx tsx examples/all" 52 | }, 53 | "dependencies": { 54 | "ai": "^3.4.20", 55 | "zod": "^3.23.8" 56 | }, 57 | "devDependencies": { 58 | "@ai-sdk/openai": "^0.0.70", 59 | "@arethetypeswrong/cli": "^0.16.4", 60 | "@changesets/cli": "^2.27.9", 61 | "@types/node": "^22.7.9", 62 | "@vitest/coverage-v8": "^2.1.3", 63 | "clipboardy": "^3.0.0", 64 | "dotenv": "^16.4.5", 65 | "ollama-ai-provider": "^0.16.0", 66 | "prettier": "^3.3.3", 67 | "tsup": "^8.3.4", 68 | "typescript": "^5.6.3", 69 | "vitest": "^2.1.3" 70 | }, 71 | "engines": { 72 | "node": ">=18" 73 | }, 74 | "homepage": "https://github.com/joshmu/ts-swarm", 75 | "bugs": { 76 | "url": "https://github.com/joshmu/ts-swarm/issues" 77 | }, 78 | "author": "Josh Mu (https://joshmu.dev)", 79 | "repository": { 80 | "type": "git", 81 | "url": "git+https://github.com/joshmu/ts-swarm.git" 82 | }, 83 | "license": "MIT" 84 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .cursorrules 133 | scratchpad/ 134 | -------------------------------------------------------------------------------- /examples/pokemon/pokemonAgent.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { openai } from '@ai-sdk/openai'; 3 | import { createAgent, Agent } from '../../src/index'; 4 | 5 | const pokemonTools: Agent['tools'] = [ 6 | { 7 | id: 'pokemon', 8 | description: 'A tool for providing basic Pokémon details.', 9 | parameters: z.object({ 10 | name: z.string().describe('The name of the Pokémon'), 11 | }), 12 | execute: async ({ name }) => { 13 | const response = await fetch( 14 | `https://pokeapi.co/api/v2/pokemon/${name?.toLowerCase()}`, 15 | ); 16 | if (!response.ok) { 17 | return `Could not find details for Pokémon: ${name}.`; 18 | } 19 | const data: any = await response.json(); 20 | return `Pokémon ${data.name} has a height of ${data.height} and a weight of ${data.weight}.`; 21 | }, 22 | }, 23 | { 24 | id: 'abilities', 25 | description: 'A tool for providing Pokémon abilities.', 26 | parameters: z.object({ 27 | name: z.string().describe('The name of the Pokémon'), 28 | }), 29 | execute: async ({ name }) => { 30 | const response = await fetch( 31 | `https://pokeapi.co/api/v2/pokemon/${name?.toLowerCase()}`, 32 | ); 33 | if (!response.ok) { 34 | return `Could not find abilities for Pokémon: ${name}.`; 35 | } 36 | const data: any = await response.json(); 37 | const abilities = data.abilities 38 | .map((ability: any) => ability.ability.name) 39 | .join(', '); 40 | return `Pokémon ${data.name} has the following abilities: ${abilities}.`; 41 | }, 42 | }, 43 | { 44 | id: 'types', 45 | description: 'A tool for providing Pokémon types.', 46 | parameters: z.object({ 47 | name: z.string().describe('The name of the Pokémon'), 48 | }), 49 | execute: async ({ name }) => { 50 | const response = await fetch( 51 | `https://pokeapi.co/api/v2/pokemon/${name?.toLowerCase()}`, 52 | ); 53 | if (!response.ok) { 54 | return `Could not find types for Pokémon: ${name}.`; 55 | } 56 | const data: any = await response.json(); 57 | const types = data.types.map((type: any) => type.type.name).join(', '); 58 | return `Pokémon ${data.name} is of type(s): ${types}.`; 59 | }, 60 | }, 61 | ]; 62 | 63 | export const pokemonAgent = createAgent({ 64 | id: 'Pokemon_Agent', 65 | model: openai('gpt-4o-2024-08-06', { structuredOutputs: true }), 66 | system: ` 67 | You are a Pokémon agent. You need to provide details about a Pokémon. 68 | You can use the Pokémon tools to answer the question. 69 | You should attempt to resolve the user's request based on the tools you have available. 70 | If the customer is unsure, you could provide them a list of Pokémon to choose from. 71 | `, 72 | tools: pokemonTools, 73 | }); 74 | -------------------------------------------------------------------------------- /original-port/examples/basic/main.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { Swarm, Agent, createAgentFunction } from '../../src/index'; 3 | 4 | const getWeather = createAgentFunction({ 5 | name: 'getWeather', 6 | func: ({ location }: { location: string }): string => { 7 | // This is a mock function, in a real scenario, you'd call an API 8 | return `The weather in ${location} is sunny with a high of 32°C.`; 9 | }, 10 | descriptor: { 11 | name: 'getWeather', 12 | description: 'Get the weather for a specific location', 13 | parameters: { 14 | location: { 15 | type: 'string', 16 | required: true, 17 | description: 'The location to get weather for', 18 | }, 19 | }, 20 | }, 21 | }); 22 | 23 | const sendEmail = createAgentFunction({ 24 | name: 'sendEmail', 25 | func: ({ 26 | to, 27 | subject, 28 | body, 29 | }: { 30 | to: string; 31 | subject: string; 32 | body: string; 33 | }): string => { 34 | // This is a mock function, in a real scenario, you'd use an email service 35 | console.log(`Sending email to ${to} with subject: ${subject}`); 36 | return 'Email sent successfully!'; 37 | }, 38 | descriptor: { 39 | name: 'sendEmail', 40 | description: 'Send an email', 41 | parameters: { 42 | to: { 43 | type: 'string', 44 | required: true, 45 | description: 'Email recipient', 46 | }, 47 | subject: { 48 | type: 'string', 49 | required: true, 50 | description: 'Email subject', 51 | }, 52 | body: { 53 | type: 'string', 54 | required: true, 55 | description: 'Email body', 56 | }, 57 | }, 58 | }, 59 | }); 60 | 61 | // Create agents 62 | const weatherAgent = new Agent({ 63 | name: 'Weather Agent', 64 | instructions: 65 | 'You are a helpful weather agent. Provide weather information when asked.', 66 | functions: [getWeather], 67 | }); 68 | 69 | const emailAgent = new Agent({ 70 | name: 'Email Agent', 71 | instructions: 72 | 'You are an email assistant. Help users send emails when requested.', 73 | functions: [sendEmail], 74 | }); 75 | 76 | // Create swarm 77 | const swarm = new Swarm({ apiKey: process.env.OPENAI_API_KEY }); 78 | 79 | // Example usage 80 | async function runExample() { 81 | const weatherResult = await swarm.run({ 82 | agent: weatherAgent, 83 | messages: [ 84 | { role: 'user', content: "What's the weather like in New York?" }, 85 | ], 86 | }); 87 | console.log( 88 | 'Weather:', 89 | weatherResult.messages[weatherResult.messages.length - 1].content, 90 | ); 91 | 92 | const emailResult = await swarm.run({ 93 | agent: emailAgent, 94 | messages: [ 95 | { 96 | role: 'user', 97 | content: 'Send an email to john@example.com about the weather.', 98 | }, 99 | ], 100 | }); 101 | console.log( 102 | 'Email:', 103 | emailResult.messages[emailResult.messages.length - 1].content, 104 | ); 105 | } 106 | 107 | runExample(); 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.2.7 4 | 5 | ### Patch Changes 6 | 7 | - d2232d6: deprecation notice 8 | 9 | ## 1.2.6 10 | 11 | ### Patch Changes 12 | 13 | - 5c44689: update agent object to class instance 14 | 15 | ## 1.2.5 16 | 17 | ### Patch Changes 18 | 19 | - 1d51882: runDemoLoop rename 20 | 21 | ## 1.2.4 22 | 23 | ### Patch Changes 24 | 25 | - 39b5074: readme npm publish issue fix 26 | 27 | ## 1.2.3 28 | 29 | ### Patch Changes 30 | 31 | - c1abb80: docs readme update 32 | 33 | ## 1.2.2 34 | 35 | ### Patch Changes 36 | 37 | - a828f75: update docs, add runDemoLoop to allow consumers a way to more easily test agents in the terminal 38 | 39 | ## 1.2.1 40 | 41 | ### Patch Changes 42 | 43 | - a60a0e4: improve logging, clean up utils, provide message callabck 44 | - 2f0324c: readme doc usage update 45 | 46 | ## 1.2.0 47 | 48 | ### Minor Changes 49 | 50 | - 75715ca: simplify the public api further by removing the runSwarm util and providing this to agent.run 51 | 52 | ## 1.1.13 53 | 54 | ### Patch Changes 55 | 56 | - ff1effd: provide further documentation on the public api 57 | 58 | ## 1.1.12 59 | 60 | ### Patch Changes 61 | 62 | - 4362594: simplify requirement to declare agent transfers 63 | 64 | ## 1.1.11 65 | 66 | ### Patch Changes 67 | 68 | - 0876751: support local llm models 69 | 70 | ## 1.1.10 71 | 72 | ### Patch Changes 73 | 74 | - 76cf21c: update readme 75 | 76 | ## 1.1.9 77 | 78 | ### Patch Changes 79 | 80 | - 2eaf8a8: simplify the api footprint 81 | - c624e5e: update lock 82 | 83 | ## 1.1.8 84 | 85 | ### Patch Changes 86 | 87 | - 5c39d36: remove requirement for transfer to agent util 88 | 89 | ## 1.1.7 90 | 91 | ### Patch Changes 92 | 93 | - aa32395: doc update 94 | 95 | ## 1.1.6 96 | 97 | ### Patch Changes 98 | 99 | - 4361914: update docs 100 | 101 | ## 1.1.5 102 | 103 | ### Patch Changes 104 | 105 | - 1b5ec5c: improve weather example 106 | 107 | ## 1.1.4 108 | 109 | ### Patch Changes 110 | 111 | - 308248e: ensure the consumer will provide the necessary deps to use with ts-swarm 112 | 113 | ## 1.1.3 114 | 115 | ### Patch Changes 116 | 117 | - 43928a7: resolve deps 118 | 119 | ## 1.1.2 120 | 121 | ### Patch Changes 122 | 123 | - a1e56ad: docs: expose model in readme usage 124 | - 298cbc9: resolve lock file 125 | - 4acf529: readme doc update 126 | - 19577a7: build update to support correct deps 127 | 128 | ## 1.1.1 129 | 130 | ### Patch Changes 131 | 132 | - 9b20f03: update docs to mention the benefits of the vercel ai sdk using generateText 133 | 134 | ## 1.1.0 135 | 136 | ### Minor Changes 137 | 138 | - 2ecd71b: Updated the entire codebase to lean in to the vercel ai sdk and archived the port version, additionally added a variety of working examples 139 | 140 | ## 1.0.5 141 | 142 | ### Patch Changes 143 | 144 | - 4b3996c: acknowldge openai swarm :) 145 | 146 | ## 1.0.4 147 | 148 | ### Patch Changes 149 | 150 | - 3eb97e3: cosmetic doc changes 151 | 152 | ## 1.0.3 153 | 154 | ### Patch Changes 155 | 156 | - bc26070: add new automated release workflow 157 | 158 | ## 1.0.2 159 | 160 | ### Patch Changes 161 | 162 | - 063934d: reduce the public api footprint and refactor 163 | 164 | ## 1.0.1 165 | 166 | ### Patch Changes 167 | 168 | - c30e9c0: Initial release 169 | 170 | All notable changes to this project will be documented in this file. See [Commitizen](https://commitizen-tools.github.io/commitizen/) for commit guidelines. 171 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to TS-SWARM 2 | 3 | We deeply appreciate your interest in contributing to our repository! Whether you're reporting bugs, suggesting enhancements, improving docs, or submitting pull requests, your contributions help improve the project for everyone. 4 | 5 | ## Reporting Bugs 6 | 7 | If you've encountered a bug in the project, we encourage you to report it to us. Please follow these steps: 8 | 9 | 1. **Check the Issue Tracker**: Before submitting a new bug report, please check our issue tracker to see if the bug has already been reported. If it has, you can add to the existing report. 10 | 2. **Create a New Issue**: If the bug hasn't been reported, create a new issue. Provide a clear title and a detailed description of the bug. Include any relevant logs, error messages, and steps to reproduce the issue. 11 | 3. **Label Your Issue**: If possible, label your issue as a `bug` so it's easier for maintainers to identify. 12 | 13 | ## Suggesting Enhancements 14 | 15 | We're always looking for suggestions to make our project better. If you have an idea for an enhancement, please: 16 | 17 | 1. **Check the Issue Tracker**: Similar to bug reports, please check if someone else has already suggested the enhancement. If so, feel free to add your thoughts to the existing issue. 18 | 2. **Create a New Issue**: If your enhancement hasn't been suggested yet, create a new issue. Provide a detailed description of your suggested enhancement and how it would benefit the project. 19 | 20 | ## Code Contributions 21 | 22 | We welcome your contributions to our code and documentation. Here's how you can contribute: 23 | 24 | ### Setting Up the Repository Locally 25 | 26 | To set up the repository on your local machine, follow these steps: 27 | 28 | 1. **Fork the Repository**: Make a copy of the repository to your GitHub account. 29 | 2. **Clone the Repository**: Clone the repository to your local machine, e.g. using `git clone`. 30 | 3. **Install pnpm**: If you haven't already, install `pnpm`. You can do this by running `npm install -g pnpm` if you're using npm. Alternatively, if you're using Homebrew (Mac), you can run `brew install pnpm`. 31 | 4. **Install Dependencies**: Navigate to the project directory and run `pnpm install` to install all necessary dependencies. 32 | 33 | ### Submitting Pull Requests 34 | 35 | We greatly appreciate your pull requests. Here are the steps to submit them: 36 | 37 | 1. **Create a New Branch**: Initiate your changes in a fresh branch. It's recommended to name the branch in a manner that signifies the changes you're implementing. 38 | 2. **Commit Your Changes**: Ensure your commits are succinct and clear, detailing what modifications have been made and the reasons behind them. 39 | 3. **Push the Changes to Your GitHub Repository**: After committing your changes, push them to your GitHub repository. 40 | 4. **Open a Pull Request**: Propose your changes for review. Furnish a lucid title and description of your contributions. Make sure to link any relevant issues your PR resolves. 41 | 5. **Respond to Feedback**: Stay receptive to and address any feedback or alteration requests from the project maintainers. 42 | 43 | ### Fixing Prettier Issues 44 | 45 | > [!TIP] 46 | > Run `pnpm format` before opening a pull request. 47 | 48 | If you encounter any prettier issues, you can fix them by running `pnpm format`. This command will automatically fix any formatting issues in your code. 49 | 50 | ### Running the Examples 51 | 52 | 1. run `pnpm install` in the root 53 | 2. copy `.env.example` to `.env` and add your OpenAI API key 54 | 3. review the package.json for examples and run them: 55 | 1. `pnpm example:` 56 | 57 | Thank you for contributing to TS-SWARM! Your efforts make a significant difference. 58 | -------------------------------------------------------------------------------- /src/agent.ts: -------------------------------------------------------------------------------- 1 | import { CoreTool, generateText, tool } from 'ai'; 2 | import { z } from 'zod'; 3 | import { 4 | SwarmTool, 5 | GenerateTextParams, 6 | Message, 7 | RunSwarmOptions, 8 | } from './types'; 9 | import { runSwarm } from './swarm'; 10 | 11 | /** 12 | * Declarative helper to create an agent instance 13 | */ 14 | export function createAgent({ 15 | id, 16 | model, 17 | tools = [], 18 | ...config 19 | }: Omit, 'tools'> & { 20 | id: string; 21 | model: GenerateTextParams['model']; 22 | tools?: SwarmTool[]; 23 | }): Agent { 24 | return new Agent({ id, model, tools, ...config }); 25 | } 26 | 27 | /** 28 | * Agent class 29 | */ 30 | export class Agent { 31 | readonly id: string; 32 | model: GenerateTextParams['model']; 33 | tools: SwarmTool[]; 34 | baseConfig: Omit, 'tools'>; 35 | 36 | constructor({ 37 | id, 38 | model, 39 | tools = [], 40 | ...createConfig 41 | }: Omit, 'tools'> & { 42 | id: string; 43 | model: GenerateTextParams['model']; 44 | tools?: SwarmTool[]; 45 | }) { 46 | const AI_SDK_VALID_ID_REGEX = /^[a-zA-Z0-9_-]+$/; 47 | if (!RegExp(AI_SDK_VALID_ID_REGEX).exec(id)) { 48 | throw new Error( 49 | `Invalid agent ID: "${id}", must be in the format of [a-zA-Z0-9_-]`, 50 | ); 51 | } 52 | 53 | this.id = id; 54 | this.model = model; 55 | this.tools = tools; 56 | this.baseConfig = createConfig; 57 | } 58 | 59 | /** 60 | * Generate a raw vercel ai sdk generateText response 61 | */ 62 | generate(config: Partial) { 63 | return generateText({ 64 | model: this.model, 65 | tools: createToolMap(this.tools), 66 | ...this.baseConfig, 67 | ...config, 68 | /** 69 | * ! set the limit to 1 to allow the swarm to determine the orchestration 70 | * If we don't do this then internal orchestration will be triggered at the agent level which currently has undesired behavior 71 | */ 72 | maxSteps: 1, 73 | }); 74 | } 75 | 76 | /** 77 | * Run the agent and allow agent tool handoffs with swarm orchestration 78 | */ 79 | run( 80 | options: Partial> & { 81 | messages: Message[]; 82 | }, 83 | ) { 84 | return runSwarm({ 85 | activeAgent: this, 86 | ...options, 87 | }); 88 | } 89 | } 90 | 91 | /** 92 | * Create a tool map valid for the ai sdk 93 | */ 94 | function createToolMap(tools: SwarmTool[]) { 95 | return tools.reduce((acc, tool) => { 96 | return { 97 | ...acc, 98 | ...normalizeSwarmTool(tool), 99 | }; 100 | }, {}); 101 | } 102 | 103 | /** 104 | * Determine if the tool is an agent instance 105 | */ 106 | function isTransferToAgentTool(tool: SwarmTool): tool is Agent { 107 | return (tool as Record) instanceof Agent; 108 | } 109 | 110 | /** 111 | * Util to create the agent transfer tools 112 | * @see https://sdk.vercel.ai/docs/ai-sdk-core/agents#example-1 113 | */ 114 | function transferToAgent(agent: Agent): Record { 115 | return { 116 | [`transferTo${agent.id}`]: tool({ 117 | description: ` 118 | A tool to transfer responsibility to the ${agent.id} agent. 119 | `, 120 | parameters: z.object({}), 121 | execute: async () => () => agent, 122 | }), 123 | }; 124 | } 125 | 126 | /** 127 | * Normalize the swarm tool to a core tool 128 | */ 129 | function normalizeSwarmTool(swarmTool: SwarmTool): Record { 130 | if (isTransferToAgentTool(swarmTool)) { 131 | return transferToAgent(swarmTool); 132 | } 133 | 134 | // otherwise, it's a core tool 135 | const { id, ...toolConfig } = swarmTool; 136 | return { [id]: tool(toolConfig as Parameters[0]) } as Record< 137 | string, 138 | CoreTool 139 | >; 140 | } 141 | -------------------------------------------------------------------------------- /original-port/examples/custom/main.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { Swarm, Agent, createAgentFunction } from '../../src/index'; 3 | import { emailAgent } from './agents/email/agent'; 4 | import { weatherAgent } from './agents/weather/agent'; 5 | 6 | // Create agents 7 | const triageAgent = new Agent({ 8 | name: 'Triage Agent', 9 | instructions: 10 | "You are a helpful triage agent. Determine which agent is best suited to handle the user's request, and transfer the conversation to that agent. Only choose one agent at a time. After an agent completes its task, you will be called again to determine the next step.", 11 | functions: [], 12 | tool_choice: 'auto', 13 | parallel_tool_calls: false, 14 | }); 15 | 16 | // Define transfer functions 17 | const transferToWeather = createAgentFunction({ 18 | name: 'transferToWeather', 19 | func: () => weatherAgent, 20 | descriptor: { 21 | name: 'transferToWeather', 22 | description: 'Transfer the conversation to the Weather Agent', 23 | parameters: {}, 24 | }, 25 | }); 26 | 27 | const transferToEmail = createAgentFunction({ 28 | name: 'transferToEmail', 29 | func: () => emailAgent, 30 | descriptor: { 31 | name: 'transferToEmail', 32 | description: 'Transfer the conversation to the Email Agent', 33 | parameters: {}, 34 | }, 35 | }); 36 | 37 | const transferBackToTriage = createAgentFunction({ 38 | name: 'transferBackToTriage', 39 | func: () => triageAgent, 40 | descriptor: { 41 | name: 'transferBackToTriage', 42 | description: 'Transfer the conversation back to the Triage Agent', 43 | parameters: {}, 44 | }, 45 | }); 46 | 47 | // Assign transfer functions to agents 48 | triageAgent.functions = [transferToWeather, transferToEmail]; 49 | weatherAgent.functions.push(transferBackToTriage); 50 | emailAgent.functions.push(transferBackToTriage); 51 | 52 | // Create swarm 53 | const swarm = new Swarm({ apiKey: process.env.OPENAI_API_KEY }); 54 | 55 | // Add event listeners 56 | swarm.on('agentSwitch', (newAgent: Agent) => { 57 | console.log(`Switched to Agent: ${newAgent.name}`); 58 | }); 59 | 60 | swarm.on( 61 | 'toolCall', 62 | (toolCall: { name: string; args: any; result: string }) => { 63 | console.log( 64 | `Tool(${toolCall.name}): Args: ${JSON.stringify(toolCall.args)}`, 65 | ); 66 | console.log(`Tool(${toolCall.name}) Result: ${toolCall.result}`); 67 | }, 68 | ); 69 | 70 | // Example usage 71 | async function runExample() { 72 | const initialMessage = { 73 | role: 'user', 74 | content: ` 75 | I need to send an email to john@example.com. 76 | Please include the weather temperature in New York in the body of the email. 77 | The subject should be "Weather Update". 78 | `, 79 | }; 80 | 81 | let currentAgent = triageAgent; 82 | let messages = [initialMessage]; 83 | let context_variables = {}; 84 | 85 | while (true) { 86 | console.log(`\nCurrent Agent: ${currentAgent.name}`); 87 | console.log('Context Variables:', context_variables); 88 | 89 | const result = await swarm.run({ 90 | agent: currentAgent, 91 | messages, 92 | context_variables, 93 | }); 94 | 95 | console.log( 96 | `\nAgent(${currentAgent.name}) Response:`, 97 | result.messages[result.messages.length - 1].content, 98 | ); 99 | 100 | messages = [...messages, ...result.messages]; 101 | context_variables = { ...context_variables, ...result.context_variables }; 102 | 103 | if (result.agent && result.agent !== currentAgent) { 104 | currentAgent = result.agent; 105 | } else if (currentAgent !== triageAgent) { 106 | // If no agent switch occurred and we're not on the triage agent, 107 | // switch back to triage agent 108 | currentAgent = triageAgent; 109 | } else { 110 | // If we're on the triage agent and no switch occurred, we're done 111 | break; 112 | } 113 | } 114 | } 115 | 116 | runExample(); 117 | -------------------------------------------------------------------------------- /examples/webscraper/webScraperAgent.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { createAgent } from '../../src/index'; 3 | import clipboard from 'clipboardy'; 4 | import { openai } from '@ai-sdk/openai'; 5 | 6 | let lastScrapedContent: string | undefined; 7 | 8 | async function fetchWithTimeout(url: string, timeout = 5000) { 9 | const controller = new AbortController(); 10 | const timeoutId = setTimeout(() => controller.abort(), timeout); 11 | 12 | try { 13 | const response = await fetch(url, { 14 | signal: controller.signal, 15 | headers: { 16 | Accept: 'text/plain', 17 | }, 18 | }); 19 | clearTimeout(timeoutId); 20 | return response; 21 | } catch (error) { 22 | clearTimeout(timeoutId); 23 | throw error; 24 | } 25 | } 26 | 27 | export const webScraperAgent = createAgent({ 28 | id: 'WebScraper_Agent', 29 | model: openai('gpt-4o-mini'), 30 | system: ` 31 | You are a web scraping agent that can: 32 | 1. Read URLs from the clipboard 33 | 2. Fetch and extract text content from web pages 34 | 3. Analyze and summarize the content 35 | 4. Answer questions about the scraped content 36 | 5. Perform web searches and analyze results 37 | 38 | When processing URLs or searches: 39 | - Verify if the input looks valid 40 | - Handle errors gracefully and provide clear feedback 41 | - Summarize content in a clear, structured way 42 | - Be mindful of content length in responses 43 | 44 | You can use your tools to: 45 | - Read from clipboard 46 | - Fetch web content 47 | - Get current scraped content 48 | - Clear the current content 49 | - Perform web searches 50 | `, 51 | tools: [ 52 | { 53 | id: 'readClipboard', 54 | description: 'Reads content from the clipboard', 55 | parameters: z.object({}), 56 | execute: async () => { 57 | try { 58 | const content = await clipboard.read(); 59 | return content || 'Clipboard is empty'; 60 | } catch (error: any) { 61 | return `Error reading clipboard: ${error.message}`; 62 | } 63 | }, 64 | }, 65 | 66 | { 67 | id: 'fetchWebContent', 68 | description: 'Fetches and extracts text content from a URL', 69 | parameters: z.object({ 70 | url: z.string().describe('The URL to fetch content from'), 71 | }), 72 | execute: async ({ url }) => { 73 | try { 74 | const encodedUrl = encodeURIComponent(url); 75 | // @see https://jina.ai 76 | const jinaReaderUrl = `https://r.jina.ai/${encodedUrl}`; 77 | const response = await fetchWithTimeout(jinaReaderUrl, 20000); 78 | if (!response.ok) { 79 | return `Failed to fetch URL: ${response.statusText}`; 80 | } 81 | 82 | const textContent = await response.text(); 83 | 84 | // Store in context for later use 85 | lastScrapedContent = textContent; 86 | 87 | return `Successfully fetched and processed content. Preview: First 100 characters:\n${textContent.slice(0, 100)}...`; 88 | } catch (error: any) { 89 | return `Error fetching content: ${error.message}`; 90 | } 91 | }, 92 | }, 93 | 94 | { 95 | id: 'getScrapedContent', 96 | description: 'Gets the currently stored scraped content', 97 | parameters: z.object({}), 98 | execute: async () => { 99 | if (!lastScrapedContent) { 100 | return 'No content has been scraped yet'; 101 | } 102 | return lastScrapedContent; 103 | }, 104 | }, 105 | 106 | { 107 | id: 'clearScrapedContent', 108 | description: 'Clears the currently stored scraped content', 109 | parameters: z.object({}), 110 | execute: async () => { 111 | lastScrapedContent = undefined; 112 | return 'Scraped content has been cleared'; 113 | }, 114 | }, 115 | 116 | { 117 | id: 'performWebSearch', 118 | description: 'Performs a web search and return the results', 119 | parameters: z.object({ 120 | query: z.string().describe('The search query to look up'), 121 | }), 122 | execute: async ({ query }) => { 123 | try { 124 | // @see https://jina.ai 125 | const encodedQuery = encodeURIComponent(query); 126 | const jinaSearchUrl = `https://s.jina.ai/${encodedQuery}`; 127 | console.log('execute: > jinaSearchUrl:', jinaSearchUrl); 128 | 129 | const response = await fetchWithTimeout(jinaSearchUrl, 30000); 130 | if (!response.ok) { 131 | return `Failed to perform search: ${response.statusText}`; 132 | } 133 | 134 | const searchResults = await response.text(); 135 | 136 | // Store in context for later use 137 | lastScrapedContent = searchResults; 138 | 139 | return `Successfully performed search. Preview of results:\n${searchResults.slice(0, 150)}...`; 140 | } catch (error: any) { 141 | return `Error performing search: ${error.message}`; 142 | } 143 | }, 144 | }, 145 | ], 146 | }); 147 | -------------------------------------------------------------------------------- /original-port/README.md: -------------------------------------------------------------------------------- 1 | # TS-SWARM 🐝 2 | 3 | [![npm version](https://img.shields.io/npm/v/ts-swarm.svg)](https://www.npmjs.com/package/ts-swarm) 4 | [![OpenAI](https://img.shields.io/badge/OpenAI-API-green.svg)](https://openai.com/) 5 | [![TypeScript](https://img.shields.io/badge/TypeScript-5.6.3-blue.svg)](https://www.typescriptlang.org/) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | 8 | ## Overview 9 | 10 | TS-SWARM is a minimal TypeScript Agentic library inspired by the [OpenAI Swarm API](https://github.com/openai/swarm). It provides a flexible and extensible system for creating and managing AI agents that can collaborate, communicate, and solve complex tasks. 11 | 12 | > [!TIP] 13 | > Initially ported to Typescript from the [original Python codebase](https://github.com/openai/swarm), TS-SWARM diverges with a more functional typesafe approach and sprinkles in some additional features such as Event Emitter. Future plans are to add zod validation and a more generic adapter for the chat completions so that other LLMs can be leveraged. ⚡ 14 | 15 | ## Features 16 | 17 | - **Multi-Agent System**: Create and manage multiple AI agents with different roles and capabilities. 18 | - **Flexible Agent Configuration**: Easily define agent behaviors, instructions, and available functions. 19 | - **Task Delegation**: Agents can transfer tasks to other specialized agents. 20 | - **Streaming Responses**: Support for real-time streaming of agent responses. 21 | - **Context Management**: Maintain and update context variables across agent interactions. 22 | - **Event System**: Built-in event emitter for tracking agent switches and tool calls. 23 | - **TypeScript Support**: Fully typed for better development experience and code quality. 24 | 25 | ## Installation 26 | 27 | You will need Node.js 18+ and pnpm installed on your local development machine. 28 | 29 | ```bash 30 | pnpm add ts-swarm 31 | ``` 32 | 33 | ## Usage 34 | 35 | ```typescript 36 | import { Swarm, Agent, createAgentFunction } from 'ts-swarm'; 37 | 38 | const getWeather = createAgentFunction({ 39 | name: 'getWeather', 40 | func: ({ location }: { location: string }): string => { 41 | // mock API call... 42 | return `The weather in ${location} is sunny with a high of 67°F.`; 43 | }, 44 | descriptor: { 45 | name: 'getWeather', 46 | description: 'Get the weather for a specific location', 47 | parameters: { 48 | location: { 49 | type: 'string', 50 | required: true, 51 | description: 'The location to get weather for', 52 | }, 53 | }, 54 | }, 55 | }); 56 | 57 | const weatherAgent = new Agent({ 58 | name: 'Weather Agent', 59 | instructions: 'You are a weather assistant.', 60 | functions: [getWeather], 61 | }); 62 | 63 | const transferToWeatherAgent = createAgentFunction({ 64 | name: 'transferToWeatherAgent', 65 | func: () => weatherAgent, 66 | descriptor: { 67 | name: 'transferToWeatherAgent', 68 | description: 'Transfer the conversation to the Weather Agent', 69 | parameters: {}, 70 | }, 71 | }); 72 | 73 | const triageAgent = new Agent({ 74 | name: 'Triage Agent', 75 | instructions: 76 | "You are a helpful triage agent. Determine which agent is best suited to handle the user's request, and transfer the conversation to that agent.", 77 | functions: [transferToWeatherAgent], 78 | tool_choice: 'auto', 79 | parallel_tool_calls: false, 80 | }); 81 | 82 | const swarm = new Swarm({ apiKey: process.env.OPENAI_API_KEY }); 83 | 84 | // Run the swarm 85 | const result = await swarm.run({ 86 | agent: triageAgent, 87 | messages: [{ role: 'user', content: "What's the weather like in New York?" }], 88 | }); 89 | 90 | const lastMessage = result.messages.at(-1); 91 | console.log(lastMessage.content); 92 | // result: The weather in New York is sunny with a high of 67°F. 93 | ``` 94 | 95 | The diagram below demonstrates the usage above. A simple multi-agent system that allows for delegation of tasks to specialized agents. 96 | 97 | ![Swarm Diagram](../assets/swarm_diagram.png) 98 | 99 | To see more examples, check out the [examples](./src/examples) directory. 100 | 101 | Otherwise, for more examples please refer to the original openai repo: [swarm](https://github.com/openai/swarm) 102 | 103 | The primary goal of Swarm is to showcase the handoff & routines patterns explored in the [Orchestrating Agents: Handoffs & Routines cookbook](https://cookbook.openai.com/examples/orchestrating_agents) 104 | 105 | For more information on the architecture, see [ARCHITECTURE.md](./ARCHITECTURE.md). 106 | 107 | ## Contributing 108 | 109 | We welcome contributions to TS-SWARM! If you'd like to contribute, please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information. 110 | 111 | ## Troubleshooting 112 | 113 | If you encounter any issues while using TS-SWARM, try the following: 114 | 115 | 1. **Runtime Errors**: Enable debug mode by setting `debug: true` in the `swarm.run()` options to get more detailed logs. 116 | 117 | If you're still experiencing issues, please [open an issue](https://github.com/joshmu/ts-swarm/issues) on the GitHub repository with a detailed description of the problem and steps to reproduce it. 118 | 119 | ## Acknowledgements 120 | 121 | It goes without saying that this project would not have been possible without the original work done by the OpenAI team. :) Go give the [Swarm API](https://github.com/openai/swarm) a star! ⭐ 122 | 123 | ## License 124 | 125 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. 126 | -------------------------------------------------------------------------------- /src/swarm.ts: -------------------------------------------------------------------------------- 1 | import { CoreToolResult } from 'ai'; 2 | import { debugLog } from './utils'; 3 | import { 4 | RunSwarmOptions, 5 | SwarmResult, 6 | ToolResults, 7 | Tools, 8 | ReturnGenerateText, 9 | } from './types'; 10 | import { Agent } from './agent'; 11 | 12 | /** 13 | * Handle the list of tool call responses from the LLM 14 | */ 15 | function handleToolCalls( 16 | response: ReturnGenerateText['response'], 17 | toolResults: ToolResults = [], 18 | ): SwarmResult { 19 | const partialResponse: SwarmResult = createSwarmResponse(); 20 | 21 | if (toolResults.length) { 22 | toolResults.forEach((t: CoreToolResult) => { 23 | /** 24 | * Determine if we need to transfer to a new agent 25 | */ 26 | const transferToAgent = toolResults.find(isTransferAgentToolResult) as 27 | | (Omit, 'result'> & { 28 | result: () => Agent; 29 | }) 30 | | undefined; 31 | const newAgent = transferToAgent?.result?.(); 32 | if (newAgent) { 33 | partialResponse.activeAgent = newAgent; 34 | /** 35 | * Remove the Agent data object from the message history 36 | */ 37 | const replacementMsg = `transferring to: ${newAgent.id}`; 38 | t.result = replacementMsg; 39 | ( 40 | response.messages.find( 41 | (m: any) => m.content[0].toolCallId === t.toolCallId, 42 | )!.content[0] as any 43 | ).result = replacementMsg; 44 | } 45 | }); 46 | // add the response messages to the partial response 47 | partialResponse.messages.push(...response.messages); 48 | } 49 | 50 | return partialResponse; 51 | } 52 | 53 | /** 54 | * Check if the tool call is a transfer agent calls 55 | */ 56 | function isTransferAgentToolResult(tool: Tools[number]) { 57 | return tool.toolName.startsWith('transferTo'); 58 | } 59 | 60 | /** 61 | * Handle LLM call 62 | */ 63 | async function getChatCompletion(options: RunSwarmOptions) { 64 | const { activeAgent: agent, messages } = options; 65 | return await agent.generate({ messages }); 66 | } 67 | 68 | /** 69 | * Create swarm result 70 | */ 71 | function createSwarmResponse(params: Partial = {}): SwarmResult { 72 | return { 73 | messages: params.messages ?? [], 74 | /** 75 | * @todo: this type is not correct... 76 | */ 77 | activeAgent: params.activeAgent!, 78 | contextVariables: params.contextVariables ?? {}, 79 | }; 80 | } 81 | 82 | /** 83 | * Run the swarm by making a single LLM request which is NOT streamed 84 | */ 85 | export async function runSwarm(options: RunSwarmOptions) { 86 | const { 87 | activeAgent: agent, 88 | messages, 89 | contextVariables = {}, 90 | modelOverride, 91 | debug = false, 92 | maxTurns = 10, 93 | } = options; 94 | 95 | /** 96 | * Initialize 97 | */ 98 | let activeAgent: Agent | null = agent; 99 | let ctx_vars = structuredClone(contextVariables); 100 | const history = structuredClone(messages); 101 | const initialMessageLength = history.length; 102 | 103 | /** 104 | * Iterate 105 | */ 106 | while (history.length - initialMessageLength < maxTurns && activeAgent) { 107 | debugLog(debug, `Running ${activeAgent.id}`); 108 | debugLog(debug, history); 109 | 110 | /** 111 | * Make the LLM request 112 | */ 113 | const chatCompletionResponse = await getChatCompletion({ 114 | activeAgent, 115 | messages: history, 116 | contextVariables: ctx_vars, 117 | modelOverride, 118 | debug, 119 | }); 120 | debugLog(debug, chatCompletionResponse); 121 | const { toolCalls, toolResults, text, response } = chatCompletionResponse; 122 | 123 | /** 124 | * Handle the tool calls 125 | */ 126 | const partialResponse = handleToolCalls(response, toolResults); 127 | 128 | /** 129 | * Update the partial response 130 | */ 131 | if (text) { 132 | partialResponse.messages.push({ 133 | role: 'assistant', 134 | content: text, 135 | }); 136 | } 137 | 138 | /** 139 | * Update the messages with swarm meta information 140 | */ 141 | partialResponse.messages.forEach((m) => { 142 | m.swarmMeta = { 143 | agentId: activeAgent?.id!, 144 | }; 145 | }); 146 | 147 | /** 148 | * Option to log the messages via callback 149 | */ 150 | if (options.onMessages) options.onMessages(partialResponse.messages); 151 | 152 | /** 153 | * Update the history 154 | */ 155 | history.push(...partialResponse.messages); 156 | 157 | /** 158 | * If there are no tool calls, end the turn 159 | */ 160 | if (!toolCalls.length) { 161 | debugLog(debug, 'No toolCalls, ending run.'); 162 | break; 163 | } 164 | 165 | /** 166 | * Update context 167 | */ 168 | ctx_vars = { ...ctx_vars, ...partialResponse.contextVariables }; 169 | 170 | /** 171 | * Update active agent 172 | */ 173 | if (partialResponse.activeAgent) { 174 | activeAgent = partialResponse.activeAgent; 175 | } 176 | } 177 | 178 | /** 179 | * Return the result 180 | * However only the messages associated to this run 181 | * And not including the user query which we would already be aware of 182 | */ 183 | const newMessages = history.slice(initialMessageLength); 184 | return createSwarmResponse({ 185 | messages: newMessages, 186 | activeAgent: activeAgent!, 187 | contextVariables: ctx_vars, 188 | }); 189 | } 190 | -------------------------------------------------------------------------------- /original-port/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | .: 9 | dependencies: 10 | openai: 11 | specifier: ^4.57.0 12 | version: 4.68.4 13 | 14 | packages: 15 | '@types/node-fetch@2.6.11': 16 | resolution: 17 | { 18 | integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==, 19 | } 20 | 21 | '@types/node@18.19.61': 22 | resolution: 23 | { 24 | integrity: sha512-z8fH66NcVkDzBItOao+Nyh0fiy7CYdxIyxnNCcZ60aY0I+EA/y4TSi/S/W9i8DIQvwVo7a0pgzAxmDeNnqrpkw==, 25 | } 26 | 27 | abort-controller@3.0.0: 28 | resolution: 29 | { 30 | integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==, 31 | } 32 | engines: { node: '>=6.5' } 33 | 34 | agentkeepalive@4.5.0: 35 | resolution: 36 | { 37 | integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==, 38 | } 39 | engines: { node: '>= 8.0.0' } 40 | 41 | asynckit@0.4.0: 42 | resolution: 43 | { 44 | integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, 45 | } 46 | 47 | combined-stream@1.0.8: 48 | resolution: 49 | { 50 | integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, 51 | } 52 | engines: { node: '>= 0.8' } 53 | 54 | delayed-stream@1.0.0: 55 | resolution: 56 | { 57 | integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, 58 | } 59 | engines: { node: '>=0.4.0' } 60 | 61 | event-target-shim@5.0.1: 62 | resolution: 63 | { 64 | integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==, 65 | } 66 | engines: { node: '>=6' } 67 | 68 | form-data-encoder@1.7.2: 69 | resolution: 70 | { 71 | integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==, 72 | } 73 | 74 | form-data@4.0.1: 75 | resolution: 76 | { 77 | integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==, 78 | } 79 | engines: { node: '>= 6' } 80 | 81 | formdata-node@4.4.1: 82 | resolution: 83 | { 84 | integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==, 85 | } 86 | engines: { node: '>= 12.20' } 87 | 88 | humanize-ms@1.2.1: 89 | resolution: 90 | { 91 | integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==, 92 | } 93 | 94 | mime-db@1.52.0: 95 | resolution: 96 | { 97 | integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, 98 | } 99 | engines: { node: '>= 0.6' } 100 | 101 | mime-types@2.1.35: 102 | resolution: 103 | { 104 | integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, 105 | } 106 | engines: { node: '>= 0.6' } 107 | 108 | ms@2.1.3: 109 | resolution: 110 | { 111 | integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, 112 | } 113 | 114 | node-domexception@1.0.0: 115 | resolution: 116 | { 117 | integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==, 118 | } 119 | engines: { node: '>=10.5.0' } 120 | 121 | node-fetch@2.7.0: 122 | resolution: 123 | { 124 | integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==, 125 | } 126 | engines: { node: 4.x || >=6.0.0 } 127 | peerDependencies: 128 | encoding: ^0.1.0 129 | peerDependenciesMeta: 130 | encoding: 131 | optional: true 132 | 133 | openai@4.68.4: 134 | resolution: 135 | { 136 | integrity: sha512-LRinV8iU9VQplkr25oZlyrsYGPGasIwYN8KFMAAFTHHLHjHhejtJ5BALuLFrkGzY4wfbKhOhuT+7lcHZ+F3iEA==, 137 | } 138 | hasBin: true 139 | peerDependencies: 140 | zod: ^3.23.8 141 | peerDependenciesMeta: 142 | zod: 143 | optional: true 144 | 145 | tr46@0.0.3: 146 | resolution: 147 | { 148 | integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, 149 | } 150 | 151 | undici-types@5.26.5: 152 | resolution: 153 | { 154 | integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==, 155 | } 156 | 157 | web-streams-polyfill@4.0.0-beta.3: 158 | resolution: 159 | { 160 | integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==, 161 | } 162 | engines: { node: '>= 14' } 163 | 164 | webidl-conversions@3.0.1: 165 | resolution: 166 | { 167 | integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, 168 | } 169 | 170 | whatwg-url@5.0.0: 171 | resolution: 172 | { 173 | integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, 174 | } 175 | 176 | snapshots: 177 | '@types/node-fetch@2.6.11': 178 | dependencies: 179 | '@types/node': 18.19.61 180 | form-data: 4.0.1 181 | 182 | '@types/node@18.19.61': 183 | dependencies: 184 | undici-types: 5.26.5 185 | 186 | abort-controller@3.0.0: 187 | dependencies: 188 | event-target-shim: 5.0.1 189 | 190 | agentkeepalive@4.5.0: 191 | dependencies: 192 | humanize-ms: 1.2.1 193 | 194 | asynckit@0.4.0: {} 195 | 196 | combined-stream@1.0.8: 197 | dependencies: 198 | delayed-stream: 1.0.0 199 | 200 | delayed-stream@1.0.0: {} 201 | 202 | event-target-shim@5.0.1: {} 203 | 204 | form-data-encoder@1.7.2: {} 205 | 206 | form-data@4.0.1: 207 | dependencies: 208 | asynckit: 0.4.0 209 | combined-stream: 1.0.8 210 | mime-types: 2.1.35 211 | 212 | formdata-node@4.4.1: 213 | dependencies: 214 | node-domexception: 1.0.0 215 | web-streams-polyfill: 4.0.0-beta.3 216 | 217 | humanize-ms@1.2.1: 218 | dependencies: 219 | ms: 2.1.3 220 | 221 | mime-db@1.52.0: {} 222 | 223 | mime-types@2.1.35: 224 | dependencies: 225 | mime-db: 1.52.0 226 | 227 | ms@2.1.3: {} 228 | 229 | node-domexception@1.0.0: {} 230 | 231 | node-fetch@2.7.0: 232 | dependencies: 233 | whatwg-url: 5.0.0 234 | 235 | openai@4.68.4: 236 | dependencies: 237 | '@types/node': 18.19.61 238 | '@types/node-fetch': 2.6.11 239 | abort-controller: 3.0.0 240 | agentkeepalive: 4.5.0 241 | form-data-encoder: 1.7.2 242 | formdata-node: 4.4.1 243 | node-fetch: 2.7.0 244 | transitivePeerDependencies: 245 | - encoding 246 | 247 | tr46@0.0.3: {} 248 | 249 | undici-types@5.26.5: {} 250 | 251 | web-streams-polyfill@4.0.0-beta.3: {} 252 | 253 | webidl-conversions@3.0.1: {} 254 | 255 | whatwg-url@5.0.0: 256 | dependencies: 257 | tr46: 0.0.3 258 | webidl-conversions: 3.0.1 259 | -------------------------------------------------------------------------------- /examples/filesystem/filesystemTools.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { Agent } from '../../src/types'; 3 | import path from 'path'; 4 | import fs from 'fs/promises'; 5 | 6 | const SCRATCHPAD_DIR = path.join(process.cwd(), 'examples/scratchpad'); 7 | 8 | // Ensure scratchpad directory exists 9 | await fs.mkdir(SCRATCHPAD_DIR, { recursive: true }); 10 | 11 | export const fileSystemTools: Agent['tools'] = [ 12 | // File operations 13 | { 14 | id: 'createFile', 15 | description: 'Creates a new file in the scratchpad directory', 16 | parameters: z.object({ 17 | filename: z 18 | .string() 19 | .describe( 20 | 'The name of the file to create including the file extension otherwise use .txt', 21 | ), 22 | content: z.string().describe('The content of the file to create'), 23 | }), 24 | execute: async ({ filename, content }) => { 25 | const filepath = path.join(SCRATCHPAD_DIR, filename); 26 | try { 27 | await fs.writeFile(filepath, content, 'utf-8'); 28 | return `Successfully created file: ${filename}`; 29 | } catch (error: any) { 30 | return `Error creating file: ${error.message}`; 31 | } 32 | }, 33 | }, 34 | 35 | { 36 | id: 'readFile', 37 | description: 'Reads the content of a file from the scratchpad directory', 38 | parameters: z.object({ 39 | filename: z.string().describe('The name of the file to read'), 40 | }), 41 | execute: async ({ filename }) => { 42 | const filepath = path.join(SCRATCHPAD_DIR, filename); 43 | try { 44 | const content = await fs.readFile(filepath, 'utf-8'); 45 | return `Content of ${filename}:\n${content}`; 46 | } catch (error: any) { 47 | return `Error reading file: ${error.message}`; 48 | } 49 | }, 50 | }, 51 | 52 | { 53 | id: 'updateFile', 54 | description: 55 | 'Updates the content of an existing file in the scratchpad directory', 56 | parameters: z.object({ 57 | filename: z.string().describe('The name of the file to update'), 58 | content: z.string().describe('The content of the file to update'), 59 | }), 60 | execute: async ({ filename, content }) => { 61 | const filepath = path.join(SCRATCHPAD_DIR, filename); 62 | try { 63 | await fs.access(filepath); 64 | await fs.writeFile(filepath, content, 'utf-8'); 65 | return `Successfully updated file: ${filename}`; 66 | } catch (error: any) { 67 | return `Error updating file: ${error.message}`; 68 | } 69 | }, 70 | }, 71 | 72 | { 73 | id: 'deleteFile', 74 | description: 'Deletes a file from the scratchpad directory', 75 | parameters: z.object({ 76 | filename: z.string().describe('The name of the file to delete'), 77 | }), 78 | execute: async ({ filename }) => { 79 | const filepath = path.join(SCRATCHPAD_DIR, filename); 80 | try { 81 | await fs.unlink(filepath); 82 | return `Successfully deleted file: ${filename}`; 83 | } catch (error: any) { 84 | return `Error deleting file: ${error.message}`; 85 | } 86 | }, 87 | }, 88 | 89 | { 90 | id: 'appendToFile', 91 | description: 'Appends content to an existing file', 92 | parameters: z.object({ 93 | filename: z.string().describe('The name of the file to append to'), 94 | content: z.string().describe('The content to append to the file'), 95 | }), 96 | execute: async ({ filename, content }) => { 97 | const filepath = path.join(SCRATCHPAD_DIR, filename); 98 | try { 99 | await fs.appendFile(filepath, '\n' + content, 'utf-8'); 100 | return `Successfully appended content to ${filename}`; 101 | } catch (error: any) { 102 | return `Error appending to file: ${error.message}`; 103 | } 104 | }, 105 | }, 106 | 107 | { 108 | id: 'getFileInfo', 109 | description: 'Gets information about a file (size, creation date, etc)', 110 | parameters: z.object({ 111 | filename: z 112 | .string() 113 | .describe('The name of the file to get information about'), 114 | }), 115 | execute: async ({ filename }) => { 116 | const filepath = path.join(SCRATCHPAD_DIR, filename); 117 | try { 118 | const stats = await fs.stat(filepath); 119 | return `File information for ${filename}: 120 | Size: ${stats.size} bytes 121 | Created: ${stats.birthtime} 122 | Last modified: ${stats.mtime} 123 | Last accessed: ${stats.atime}`; 124 | } catch (error: any) { 125 | return `Error getting file info: ${error.message}`; 126 | } 127 | }, 128 | }, 129 | 130 | // Folder operations 131 | { 132 | id: 'createFolder', 133 | description: 'Creates a new folder in the scratchpad directory', 134 | parameters: z.object({ 135 | folderPath: z.string().describe('The name of the folder path to create'), 136 | }), 137 | execute: async ({ folderPath }) => { 138 | const fullPath = path.join(SCRATCHPAD_DIR, folderPath); 139 | try { 140 | await fs.mkdir(fullPath, { recursive: true }); 141 | return `Successfully created folder: ${folderPath}`; 142 | } catch (error: any) { 143 | return `Error creating folder: ${error.message}`; 144 | } 145 | }, 146 | }, 147 | 148 | { 149 | id: 'removeFolder', 150 | description: 151 | 'Removes a folder and all its contents from the scratchpad directory', 152 | parameters: z.object({ 153 | folderPath: z.string().describe('The name of the folder path to remove'), 154 | }), 155 | execute: async ({ folderPath }) => { 156 | const fullPath = path.join(SCRATCHPAD_DIR, folderPath); 157 | try { 158 | await fs.rm(fullPath, { recursive: true, force: true }); 159 | return `Successfully removed folder: ${folderPath}`; 160 | } catch (error: any) { 161 | return `Error removing folder: ${error.message}`; 162 | } 163 | }, 164 | }, 165 | 166 | { 167 | id: 'listFolder', 168 | description: 169 | 'Lists all files and folders in the specified directory. Use "" or "." for root directory.', 170 | parameters: z.object({ 171 | folderPath: z 172 | .string() 173 | .describe('The folder path to retrieve the list of files and folders'), 174 | }), 175 | execute: async ({ folderPath }) => { 176 | const fullPath = path.join(SCRATCHPAD_DIR, folderPath); 177 | try { 178 | const items = await fs.readdir(fullPath, { withFileTypes: true }); 179 | const files = items 180 | .filter((item) => item.isFile()) 181 | .map((item) => `📄 ${item.name}`); 182 | const folders = items 183 | .filter((item) => item.isDirectory()) 184 | .map((item) => `📁 ${item.name}`); 185 | 186 | if (items.length === 0) { 187 | return `Folder '${folderPath || 'root'}' is empty.`; 188 | } 189 | 190 | return `Contents of '${folderPath || 'root'}':\n\n${folders.join('\n')}\n${files.join('\n')}`; 191 | } catch (error: any) { 192 | return `Error listing folder contents: ${error.message}`; 193 | } 194 | }, 195 | }, 196 | 197 | { 198 | id: 'moveItem', 199 | description: 'Moves a file or folder to a new location within scratchpad', 200 | parameters: z.object({ 201 | sourcePath: z.string().describe('The path of the item to move'), 202 | destinationPath: z 203 | .string() 204 | .describe('The new path for the item to move to'), 205 | }), 206 | execute: async ({ sourcePath, destinationPath }) => { 207 | const fullSourcePath = path.join(SCRATCHPAD_DIR, sourcePath); 208 | const fullDestPath = path.join(SCRATCHPAD_DIR, destinationPath); 209 | try { 210 | await fs.rename(fullSourcePath, fullDestPath); 211 | return `Successfully moved ${sourcePath} to ${destinationPath}`; 212 | } catch (error: any) { 213 | return `Error moving item: ${error.message}`; 214 | } 215 | }, 216 | }, 217 | 218 | { 219 | id: 'getFolderInfo', 220 | description: 221 | 'Gets information about a folder (size, item count, etc). Use "" or "." for root directory.', 222 | parameters: z.object({ 223 | folderPath: z 224 | .string() 225 | .describe('The folder path to retrieve the information about'), 226 | }), 227 | execute: async ({ folderPath }) => { 228 | const fullPath = path.join(SCRATCHPAD_DIR, folderPath); 229 | try { 230 | const stats = await fs.stat(fullPath); 231 | const items = await fs.readdir(fullPath, { withFileTypes: true }); 232 | const fileCount = items.filter((item) => item.isFile()).length; 233 | const folderCount = items.filter((item) => item.isDirectory()).length; 234 | 235 | return `Folder information for '${folderPath || 'root'}': 236 | Total items: ${items.length} 237 | Files: ${fileCount} 238 | Folders: ${folderCount} 239 | Created: ${stats.birthtime} 240 | Last modified: ${stats.mtime} 241 | Last accessed: ${stats.atime}`; 242 | } catch (error: any) { 243 | return `Error getting folder info: ${error.message}`; 244 | } 245 | }, 246 | }, 247 | ]; 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TS-SWARM 🐝 2 | 3 | [![npm version](https://img.shields.io/npm/v/ts-swarm.svg)](https://www.npmjs.com/package/ts-swarm?link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fts-swarm) 4 | ![NPM Downloads](https://img.shields.io/npm/d18m/ts-swarm?link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fts-swarm) 5 | [![TypeScript](https://img.shields.io/badge/TypeScript-5.6.3-blue.svg)](https://www.typescriptlang.org/) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | 8 | > [!WARNING] 9 | > TS-SWARM has reached end-of-life and is no longer receiving updates or bug fixes. 10 | > Consider migrating to alternative solutions or forking the repository if you need continued support. 11 | 12 | ## Overview 13 | 14 | TS-SWARM is a minimal TypeScript Agentic library mixing the simplicity of [OpenAI Swarm API](https://github.com/openai/swarm) with the flexibility of the [Vercel AI SDK](https://github.com/vercel/ai). 15 | 16 | ## Features 17 | 18 | - **Minimal Interface**: `createAgent` & `.run()`, that's it! 19 | - **Multi-Agent System**: Create and manage multiple AI agents with different roles and capabilities. 20 | - **Flexible Agent Configuration**: Easily define agent behaviors, instructions, and available functions. 21 | - **Task Delegation**: Agents can transfer tasks to other specialized agents. 22 | - **Tools**: Agents can use tools to perform tasks. 23 | - **Zod Validation**: Tools can use zod validation to ensure the input is correct. 24 | - **Model Choice**: Easily switch between different LLMs by changing a single line of code. 25 | - **Local Agents**: Option to run locally with the [ollama-ai-provider](https://sdk.vercel.ai/providers/community-providers/ollama). 26 | 27 | ## Examples 28 | 29 | Some examples of agents and agentic patterns, additionally take a look at [`examples/run.ts`](./examples/run.ts) on how to have a conversation with agents. 30 | 31 | - [Local Agent with Ollama](./examples/local/localAgent.ts) 32 | - [Pokemon Agent](./examples/pokemon/pokemonAgent.ts) 33 | - [Web Scraper Agent](./examples/webscraper/webScraperAgent.ts) 34 | - [Filesystem Agent](./examples/filesystem/filesystemAgent.ts) 35 | - [Triage Weather Email Agents](./examples/triage-weather-email/index.ts) 36 | - [All Agents Connected](./examples/all/index.ts) 37 | 38 | > [!TIP] 39 | > Grab the repo and invoke the examples via scripts provided in the [package.json](./package.json) :) 40 | 41 | Demo using the [All Agents Connected](./examples/all/index.ts) example: 42 | ![All Agents Chat Example](./assets/all_agents_chat_example.jpg) 43 | 44 | ## Installation 45 | 46 | Use your preferred package manager: 47 | 48 | ```bash 49 | pnpm add ts-swarm ai zod 50 | ``` 51 | 52 | Depending on the LLM you want to use via the Vercel AI SDK, you will need to install the appropriate package. 53 | 54 | **Run via an LLM provider service** such as OpenAI: 55 | 56 | ```bash 57 | # OpenAI - Ensure OPENAI_API_KEY environment variable is set 58 | pnpm add @ai-sdk/openai 59 | ``` 60 | 61 | **Or run locally** with [ollama-ai-provider](https://sdk.vercel.ai/providers/community-providers/ollama): 62 | 63 | ```bash 64 | # Ollama - Ensure you leverage a model that supports tool calling 65 | pnpm add ollama-ai-provider 66 | ``` 67 | 68 | ## Usage 69 | 70 | TS-SWARM is kept minimal and simple. `createAgent` will create agents. Once you have your agent, `.run()` method will orchestrate the swarm conversation allowing for tool calling and agent handoffs. 71 | 72 | > [!TIP] 73 | > The `createAgent` util is a thin wrapper over [`generateText` from the Vercel AI SDK](https://sdk.vercel.ai/docs/reference/ai-sdk-core/generate-text). Thus you have access to **tools**, **zod validation**, and **model choice**. ⚡ 74 | 75 | ```typescript 76 | import { createAgent } from 'ts-swarm'; 77 | import { openai } from '@ai-sdk/openai'; // Ensure OPENAI_API_KEY environment variable is set 78 | import { z } from 'zod'; 79 | 80 | // Create the Weather Agent 81 | const weatherAgent = createAgent({ 82 | id: 'Weather_Agent', 83 | model: openai('gpt-4o-2024-08-06', { structuredOutputs: true }), 84 | system: ` 85 | You are a weather assistant. 86 | Your role is to: 87 | - Provide weather information for requested locations 88 | - Use the weather tool to fetch weather data`, 89 | tools: [ 90 | { 91 | id: 'weather', 92 | description: 'Get the weather for a specific location', 93 | parameters: z.object({ 94 | location: z.string().describe('The location to get weather for'), 95 | }), 96 | execute: async ({ location }) => { 97 | // Mock weather API call 98 | return `The weather in ${location} is sunny with a high of 67°F.`; 99 | }, 100 | }, 101 | ], 102 | }); 103 | 104 | // Create the Triage Agent 105 | const triageAgent = createAgent({ 106 | id: 'Triage_Agent', 107 | model: openai('gpt-4o-2024-08-06', { structuredOutputs: true }), 108 | system: ` 109 | You are a helpful triage agent. 110 | Your role is to: 111 | - Answer the user's questions by transferring to the appropriate agent`, 112 | tools: [ 113 | // Add ability to transfer to the weather agent 114 | weatherAgent, 115 | ], 116 | }); 117 | 118 | async function demo() { 119 | /** 120 | * Run the triage agent with swarm orchestration 121 | * Enabling tool calling and agent handoffs 122 | */ 123 | const result = await triageAgent.run({ 124 | // Example conversation passed in 125 | messages: [ 126 | { role: 'user', content: "What's the weather like in New York?" }, 127 | ], 128 | }); 129 | 130 | /** 131 | * We could wrap this logic in a loop to continue the conversation by 132 | * utilizing `result.activeAgent` which represents the last active agent during the run 133 | * For this example `result.activeAgent` would now be the weather agent 134 | * Refer to the `run.ts` example for an example of this 135 | */ 136 | 137 | // Log the last message (or the entire conversation if you prefer) 138 | const lastMessage = result.messages.at(-1); 139 | console.log( 140 | `${lastMessage?.swarmMeta?.agentId || 'User'}: ${lastMessage?.content}`, 141 | ); 142 | } 143 | 144 | demo(); 145 | // Query: What's the weather like in New York? 146 | // Triage_Agent: transferring... 147 | // Result: Weather_Agent: The weather in New York is sunny with a high of 67°F. 148 | ``` 149 | 150 | The following diagram demonstrates the usage above. A simple multi-agent system that allows for delegation of tasks to specialized agents. 151 | 152 | ![Swarm Diagram](./assets/swarm_diagram.png) 153 | 154 | To see more examples, check out the [examples](./examples) directory. 155 | 156 | Otherwise, for more examples please refer to the original openai repo: [swarm](https://github.com/openai/swarm) 157 | 158 | The primary goal of Swarm is to showcase the handoff & routines patterns explored in the [Orchestrating Agents: Handoffs & Routines cookbook](https://cookbook.openai.com/examples/orchestrating_agents) 159 | 160 | ## createAgent() 161 | 162 | `createAgent` defines your agent. It's a thin wrapper over [`generateText` from the Vercel AI SDK](https://sdk.vercel.ai/docs/reference/ai-sdk-core/generate-text). 163 | 164 | ### Arguments 165 | 166 | | Argument | Type | Description | Default | 167 | | -------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------- | 168 | | id | `string` | Unique identifier for the agent (must match `/^[a-zA-Z0-9_-]+$/`) | (required) | 169 | | model | `LanguageModelV1` | Refer to [Providers and Models from the Vercel AI SDK](https://sdk.vercel.ai/docs/foundations/providers-and-models) | (required) | 170 | | tools | `SwarmTool[]` | Array of core tools or agents to transfer to | `[]` | 171 | | ...rest | `Partial` | Refer to [`generateText` from the Vercel AI SDK](https://sdk.vercel.ai/docs/reference/ai-sdk-core/generate-text) | `{}` | 172 | 173 | ### Returns 174 | 175 | Returns an `Agent` object containing: 176 | 177 | - `id`: Agent's unique identifier 178 | - `generate`: Function to generate a response 179 | - `run`: **Function to run the agent with swarm orchestration allowing for tool calls and agent transfers** 180 | - `tools`: Array of available tools 181 | 182 | ## agent.run() 183 | 184 | The `.run()` method handles the LLM request loop through an agent-based system, managing tool calls and agent handoffs. 185 | 186 | ### Arguments 187 | 188 | | Argument | Type | Description | Default | 189 | | ----------- | ------------------------------- | ----------------------------------------------------------- | ------------- | 190 | | messages | `Message[]` | Array of llm message objects with additional swarm metadata | (required) | 191 | | activeAgent | `Agent` | option to override the current active agent to be called | current agent | 192 | | onMessages | `(messages: Message[]) => void` | Callback when new messages are received | `undefined` | 193 | | debug | `boolean` | Enables debug logging when true | `false` | 194 | | maxTurns | `number` | Maximum number of conversational turns allowed | `10` | 195 | 196 | ### Returns 197 | 198 | Returns a `SwarmResult` object containing: 199 | 200 | - `messages`: Array of llm messages from the conversation 201 | - `activeAgent`: Current active agent on completion of the run (useful for continuing the conversation) 202 | 203 | ## runDemoLoop() 204 | 205 | The `runDemoLoop` function is a convenience function to allow you to test your agents in the terminal. It's a simple while loop that continues to run until the user decides to 'exit'. It is what is used in most of the examples and the source code can be viewed in [`examples/run.ts`](./examples/run.ts). 206 | 207 | ### Arguments 208 | 209 | | Argument | Type | Description | 210 | | ------------------- | -------- | --------------------------------------------------------------------------- | 211 | | initialAgentMessage | `string` | The initial message from the agent to the user to commence the conversation | 212 | | initialAgent | `Agent` | The initial agent to start the conversation with | 213 | 214 | ## Roadmap 215 | 216 | - [ ] Support streaming 217 | - [ ] Additional context passing 218 | - [ ] Provide agentic design pattern examples 219 | - [ ] More test coverage 220 | - [ ] Bash the bugs 221 | - [ ] Continue to find as much simplicity while maintaining flexibility :) 222 | 223 | ## Contributing 224 | 225 | We welcome contributions to TS-SWARM! If you'd like to contribute, please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more information. 226 | 227 | ## Troubleshooting 228 | 229 | If you're experiencing issues, please [open an issue](https://github.com/joshmu/ts-swarm/issues) on the GitHub repository with a detailed description of the problem and steps to reproduce it. 230 | 231 | ## Acknowledgements 232 | 233 | It goes without saying that this project would not have been possible without the original work done by the OpenAI & Vercel teams. :) Go give the [Swarm API](https://github.com/openai/swarm) & [Vercel AI SDK](https://github.com/vercel/ai) a star! ⭐ 234 | 235 | ## License 236 | 237 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) for details. 238 | -------------------------------------------------------------------------------- /original-port/src/swarm.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI } from 'openai'; 2 | import { mergeChunkAndToolCalls } from './utils/mergeChunkAndToolCalls'; 3 | import { logger } from './utils/logger'; 4 | import { validateAgentFuncArgs } from './utils/validateAgentFuncArgs'; 5 | import { 6 | Response, 7 | Result, 8 | createSwarmResult, 9 | createAgentFunctionResult, 10 | createToolFunction, 11 | } from './types'; 12 | import { Agent, AgentFunction } from './agent'; 13 | import { 14 | ChatCompletion, 15 | ChatCompletionMessageToolCall, 16 | } from 'openai/resources'; 17 | import { EventEmitter } from 'events'; 18 | import { getChatCompletion } from './lib/chatCompletion'; 19 | 20 | const CTX_VARS_NAME = 'context_variables'; 21 | 22 | type SwarmRunOptions = { 23 | agent: Agent; 24 | messages: Array; 25 | context_variables?: Record; 26 | model_override?: string; 27 | stream?: TStream; 28 | debug?: boolean; 29 | max_turns?: number; 30 | execute_tools?: boolean; 31 | }; 32 | 33 | export class Swarm extends EventEmitter { 34 | private readonly client: OpenAI; 35 | 36 | constructor({ apiKey }: { apiKey?: string } = {}) { 37 | super(); 38 | if (apiKey) { 39 | this.client = new OpenAI({ apiKey }); 40 | } else { 41 | // Default configuration 42 | this.client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); 43 | } 44 | } 45 | 46 | /** 47 | * Normalize the result of the agent function 48 | * Takes in to account the possibility of the result being an agent 49 | */ 50 | private handleFunctionResult(result: any, debug: boolean): Result { 51 | if (result && typeof result === 'object' && 'value' in result) { 52 | return result; 53 | } else if (result instanceof Agent) { 54 | return createAgentFunctionResult({ 55 | value: JSON.stringify({ assistant: result.name }), 56 | agent: result, 57 | }); 58 | } else { 59 | try { 60 | return createAgentFunctionResult({ value: String(result) }); 61 | } catch (e: any) { 62 | const errorMessage = `Failed to cast response to string: ${result}. Make sure agent functions return a string or Result object. Error: ${e.message}`; 63 | logger(debug, errorMessage); 64 | throw new TypeError(errorMessage); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Handle the list of tool call responses from the LLM 71 | */ 72 | private handleToolCalls( 73 | tool_calls: ChatCompletionMessageToolCall[], 74 | functions: AgentFunction[], 75 | context_variables: Record, 76 | debug: boolean, 77 | ): Response { 78 | /** 79 | * Create the function tool registry 80 | */ 81 | const function_map: Record = {}; 82 | functions.forEach((func) => { 83 | function_map[func.name] = func; 84 | }); 85 | 86 | /** 87 | * Initialize the swarm response output 88 | */ 89 | const partialResponse = createSwarmResult({ 90 | messages: [], 91 | agent: undefined, 92 | context_variables: {}, 93 | }); 94 | 95 | /** 96 | * Iterate over the tools which have been called 97 | */ 98 | tool_calls.forEach((tool_call) => { 99 | const name = tool_call.function.name; 100 | /** 101 | * Check if the tool is registered 102 | * And if not, log an error and add it to the response 103 | */ 104 | if (!(name in function_map)) { 105 | logger(debug, `Tool ${name} not found in function map.`); 106 | partialResponse.messages.push({ 107 | role: 'tool', 108 | tool_call_id: tool_call.id, 109 | tool_name: name, 110 | content: `Error: Tool ${name} not found.`, 111 | }); 112 | return; 113 | } 114 | 115 | /** 116 | * Retrive the function tool arguments 117 | */ 118 | const args = JSON.parse(tool_call.function.arguments); 119 | logger( 120 | debug, 121 | `Processing tool call: ${name} with arguments`, 122 | JSON.stringify(args), 123 | ); 124 | 125 | /** 126 | * Provide the context variables to the function if required 127 | */ 128 | const func = function_map[name]; 129 | const hasFunc = func.func.length; 130 | const hasContextVars = (func as Record).hasOwnProperty( 131 | CTX_VARS_NAME, 132 | ); 133 | if (hasFunc && hasContextVars) args[CTX_VARS_NAME] = context_variables; 134 | 135 | /** 136 | * Validate the arguments 137 | * By ensuring they match the function descriptor 138 | */ 139 | let validatedArgs: any; 140 | try { 141 | validatedArgs = validateAgentFuncArgs(args, func.descriptor); 142 | } catch (e: any) { 143 | logger( 144 | debug, 145 | `Argument validation failed for function ${name}: ${e.message}`, 146 | ); 147 | partialResponse.messages.push({ 148 | role: 'tool', 149 | tool_call_id: tool_call.id, 150 | tool_name: name, 151 | content: `Error: ${e.message}`, 152 | }); 153 | return; 154 | } 155 | 156 | logger( 157 | debug, 158 | `Processing tool call: ${name} with arguments`, 159 | JSON.stringify(validatedArgs), 160 | ); 161 | 162 | /** 163 | * Invoke the function with the validated arguments 164 | */ 165 | const raw_result = func.func(validatedArgs); 166 | logger(debug, 'Raw result:', raw_result); 167 | 168 | /** 169 | * Normalize the result of the agent function 170 | * Takes in to account the possibility of the result being an agent 171 | */ 172 | const result: Result = this.handleFunctionResult(raw_result, debug); 173 | /** 174 | * Add the result to the response 175 | */ 176 | partialResponse.messages.push({ 177 | role: 'tool', 178 | tool_call_id: tool_call.id, 179 | tool_name: name, 180 | content: result.value, 181 | }); 182 | /** 183 | * Update the context variables 184 | */ 185 | partialResponse.context_variables = { 186 | ...partialResponse.context_variables, 187 | ...result.context_variables, 188 | }; 189 | /** 190 | * If the result is an agent, switch to it 191 | */ 192 | if (result.agent) { 193 | partialResponse.agent = result.agent; 194 | this.emit('agentSwitch', result.agent); 195 | } 196 | this.emit('toolCall', { 197 | name, 198 | args: validatedArgs, 199 | result: result.value, 200 | }); 201 | }); 202 | 203 | return partialResponse; 204 | } 205 | 206 | /** 207 | * Run the swarm by making a single LLM request which is streamed 208 | */ 209 | async *runAndStream(options: SwarmRunOptions): AsyncIterable { 210 | const { 211 | agent, 212 | messages, 213 | context_variables = {}, 214 | model_override, 215 | debug = false, 216 | max_turns = Infinity, 217 | execute_tools = true, 218 | } = options; 219 | 220 | let active_agent = agent; 221 | let ctx_vars = structuredClone(context_variables); 222 | const history = structuredClone(messages); 223 | const init_len = history.length; 224 | 225 | while (history.length - init_len < max_turns && active_agent) { 226 | const message: any = { 227 | content: '', 228 | sender: agent.name, 229 | role: 'assistant', 230 | function_call: null, 231 | tool_calls: {}, 232 | }; 233 | 234 | // Update the getChatCompletion call 235 | const completion = await getChatCompletion( 236 | this.client, 237 | active_agent, 238 | history, 239 | ctx_vars, 240 | model_override, 241 | true, 242 | debug, 243 | ); 244 | 245 | yield { delim: 'start' }; 246 | for await (const chunk of completion) { 247 | logger(debug, 'Received chunk:', JSON.stringify(chunk)); 248 | const delta: OpenAI.Chat.Completions.ChatCompletionChunk.Choice.Delta & { 249 | sender?: string; 250 | } = chunk.choices[0].delta; 251 | if (chunk.choices[0].delta.role === 'assistant') { 252 | delta.sender = active_agent.name; 253 | } 254 | yield delta; 255 | delete delta.role; 256 | delete delta.sender; 257 | mergeChunkAndToolCalls(message, delta); 258 | } 259 | yield { delim: 'end' }; 260 | 261 | message.tool_calls = Object.values(message.tool_calls); 262 | if (message.tool_calls.length === 0) { 263 | message.tool_calls = null; 264 | } 265 | logger(debug, 'Received completion:', message); 266 | history.push(message); 267 | 268 | if (!message.tool_calls || !execute_tools) { 269 | logger(debug, 'Ending turn.'); 270 | break; 271 | } 272 | 273 | // Convert tool_calls to objects 274 | const tool_calls: ChatCompletionMessageToolCall[] = 275 | message.tool_calls.map((tc: any) => { 276 | const func = createToolFunction({ 277 | arguments: tc.function.arguments, 278 | name: tc.function.name, 279 | }); 280 | return { 281 | id: tc.id, 282 | function: func, 283 | type: tc.type, 284 | }; 285 | }); 286 | 287 | // Handle function calls, updating context_variables and switching agents 288 | const partial_response = this.handleToolCalls( 289 | tool_calls, 290 | active_agent.functions, 291 | ctx_vars, 292 | debug, 293 | ); 294 | history.push(...partial_response.messages); 295 | ctx_vars = { ...ctx_vars, ...partial_response.context_variables }; 296 | if (partial_response.agent) { 297 | active_agent = partial_response.agent; 298 | } 299 | } 300 | 301 | yield { 302 | response: createSwarmResult({ 303 | messages: history.slice(init_len), 304 | agent: active_agent, 305 | context_variables: ctx_vars, 306 | }), 307 | }; 308 | } 309 | 310 | /** 311 | * Run the swarm by making a single LLM request which is NOT streamed 312 | */ 313 | async run( 314 | options: SwarmRunOptions, 315 | ): Promise : Response> { 316 | const { 317 | agent, 318 | messages, 319 | context_variables = {}, 320 | model_override, 321 | stream = false, 322 | debug = false, 323 | max_turns = Infinity, 324 | execute_tools = true, 325 | } = options; 326 | 327 | /** 328 | * Handle a streamed response by delegating to the runAndStream method 329 | */ 330 | if (stream) { 331 | return this.runAndStream({ 332 | agent, 333 | messages, 334 | context_variables, 335 | model_override, 336 | debug, 337 | max_turns, 338 | execute_tools, 339 | }) as TStream extends true ? AsyncIterable : never; 340 | } 341 | 342 | /** 343 | * Initialize 344 | */ 345 | let active_agent = agent; 346 | let ctx_vars = structuredClone(context_variables); 347 | const history = structuredClone(messages); 348 | const initialMessageLength = history.length; 349 | 350 | /** 351 | * Iterate 352 | */ 353 | while (history.length - initialMessageLength < max_turns && active_agent) { 354 | /** 355 | * Make the LLM request 356 | */ 357 | const completion: ChatCompletion = await getChatCompletion( 358 | this.client, 359 | active_agent, 360 | history, 361 | ctx_vars, 362 | model_override, 363 | false, 364 | debug, 365 | ); 366 | 367 | /** 368 | * Update the history 369 | */ 370 | const messageData = completion.choices[0].message; 371 | logger(debug, 'Received completion:', messageData); 372 | const message: any = { ...messageData, sender: active_agent.name }; 373 | history.push(message); 374 | 375 | /** 376 | * If there are no tool calls, end the turn 377 | */ 378 | if (!message.tool_calls || !execute_tools) { 379 | logger(debug, 'Ending turn.'); 380 | break; 381 | } 382 | 383 | /** 384 | * Handle function calls, updating context_variables and switching agents 385 | */ 386 | const partial_response = this.handleToolCalls( 387 | message.tool_calls, 388 | active_agent.functions, 389 | ctx_vars, 390 | debug, 391 | ); 392 | /** 393 | * Add to history 394 | */ 395 | history.push(...partial_response.messages); 396 | /** 397 | * Update context 398 | */ 399 | ctx_vars = { ...ctx_vars, ...partial_response.context_variables }; 400 | /** 401 | * If the result is an agent, switch to it 402 | */ 403 | if (partial_response.agent) { 404 | active_agent = partial_response.agent; 405 | } 406 | } 407 | /** 408 | * Return the result 409 | * However only the messages associated to this run 410 | */ 411 | const newMessages = history.slice(initialMessageLength); 412 | return createSwarmResult({ 413 | messages: newMessages, 414 | agent: active_agent, 415 | context_variables: ctx_vars, 416 | }) as TStream extends true ? never : Response; 417 | } 418 | } 419 | --------------------------------------------------------------------------------