diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..8f43f71
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,40 @@
+---
+name: Bug Report
+about: Report a bug to help us improve
+title: "[Bug] "
+labels: bug
+assignees: ''
+---
+
+## Describe the bug
+
+A clear and concise description of what the bug is.
+
+## To Reproduce
+
+Steps to reproduce the behavior:
+
+1. Configure agent with '...'
+2. Call `runTeam(...)` with '...'
+3. See error
+
+## Expected behavior
+
+A clear description of what you expected to happen.
+
+## Error output
+
+```
+Paste any error messages or logs here
+```
+
+## Environment
+
+- OS: [e.g. macOS 14, Ubuntu 22.04]
+- Node.js version: [e.g. 20.11]
+- Package version: [e.g. 0.1.0]
+- LLM provider: [e.g. Anthropic, OpenAI]
+
+## Additional context
+
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..c31759e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,23 @@
+---
+name: Feature Request
+about: Suggest an idea for this project
+title: "[Feature] "
+labels: enhancement
+assignees: ''
+---
+
+## Problem
+
+A clear description of the problem or limitation you're experiencing.
+
+## Proposed Solution
+
+Describe what you'd like to happen.
+
+## Alternatives Considered
+
+Any alternative solutions or features you've considered.
+
+## Additional context
+
+Add any other context, code examples, or screenshots about the feature request here.
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..739d91d
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,14 @@
+## What
+
+
+
+## Why
+
+
+
+## Checklist
+
+- [ ] `npm run lint` passes
+- [ ] `npm test` passes
+- [ ] Added/updated tests for changed behavior
+- [ ] No new runtime dependencies (or justified in the PR description)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..6f5b577
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,23 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [18, 20, 22]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: npm
+ - run: npm ci
+ - run: npm run lint
+ - run: npm test
diff --git a/.gitignore b/.gitignore
index 523e756..f321a49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
node_modules/
dist/
+coverage/
*.tgz
.DS_Store
promo-*.md
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..6cbeb45
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,80 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Commands
+
+```bash
+npm run build # Compile TypeScript (src/ → dist/)
+npm run dev # Watch mode compilation
+npm run lint # Type-check only (tsc --noEmit)
+npm test # Run all tests (vitest run)
+npm run test:watch # Vitest watch mode
+```
+
+Tests live in `tests/` (vitest). Examples in `examples/` are standalone scripts requiring API keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`).
+
+## Architecture
+
+ES module TypeScript framework for multi-agent orchestration. Three runtime dependencies: `@anthropic-ai/sdk`, `openai`, `zod`.
+
+### Core Execution Flow
+
+**`OpenMultiAgent`** (`src/orchestrator/orchestrator.ts`) is the top-level public API with three execution modes:
+
+1. **`runAgent(config, prompt)`** — single agent, one-shot
+2. **`runTeam(team, goal)`** — automatic orchestration: a temporary "coordinator" agent decomposes the goal into a task DAG via LLM call, then tasks execute in dependency order
+3. **`runTasks(team, tasks)`** — explicit task pipeline with user-defined dependencies
+
+### The Coordinator Pattern (runTeam)
+
+This is the framework's key feature. When `runTeam()` is called:
+1. A coordinator agent receives the goal + agent roster and produces a JSON task array (title, description, assignee, dependsOn)
+2. `TaskQueue` resolves dependencies topologically — independent tasks run in parallel, dependent tasks wait
+3. `Scheduler` auto-assigns any unassigned tasks (strategies: `dependency-first` default, `round-robin`, `least-busy`, `capability-match`)
+4. Each task result is written to `SharedMemory` so subsequent agents see prior results
+5. The coordinator synthesizes all task results into a final output
+
+### Layer Map
+
+| Layer | Files | Responsibility |
+|-------|-------|----------------|
+| Orchestrator | `orchestrator/orchestrator.ts`, `orchestrator/scheduler.ts` | Top-level API, task decomposition, coordinator pattern |
+| Team | `team/team.ts`, `team/messaging.ts` | Agent roster, MessageBus (point-to-point + broadcast), SharedMemory binding |
+| Agent | `agent/agent.ts`, `agent/runner.ts`, `agent/pool.ts`, `agent/structured-output.ts` | Agent lifecycle (idle→running→completed/error), conversation loop, concurrency pool with Semaphore, structured output validation |
+| Task | `task/queue.ts`, `task/task.ts` | Dependency-aware queue, auto-unblock on completion, cascade failure to dependents |
+| Tool | `tool/framework.ts`, `tool/executor.ts`, `tool/built-in/` | `defineTool()` with Zod schemas, ToolRegistry, parallel batch execution with concurrency semaphore |
+| LLM | `llm/adapter.ts`, `llm/anthropic.ts`, `llm/openai.ts` | `LLMAdapter` interface (`chat` + `stream`), factory `createAdapter()` |
+| Memory | `memory/shared.ts`, `memory/store.ts` | Namespaced key-value store (`agentName/key`), markdown summary injection into prompts |
+| Types | `types.ts` | All interfaces in one file to avoid circular deps |
+| Exports | `index.ts` | Public API surface |
+
+### Agent Conversation Loop (AgentRunner)
+
+`AgentRunner.run()`: send messages → extract tool-use blocks → execute tools in parallel batch → append results → loop until `end_turn` or `maxTurns` exhausted. Accumulates `TokenUsage` across all turns.
+
+### Concurrency Control
+
+Two independent semaphores: `AgentPool` (max concurrent agent runs, default 5) and `ToolExecutor` (max concurrent tool calls, default 4).
+
+### Structured Output
+
+Optional `outputSchema` (Zod) on `AgentConfig`. When set, the agent's final output is parsed as JSON and validated. On validation failure, one retry with error feedback is attempted. Validated data is available via `result.structured`. Logic lives in `agent/structured-output.ts`, wired into `Agent.executeRun()`.
+
+### Task Retry
+
+Optional `maxRetries`, `retryDelayMs`, `retryBackoff` on task config (used via `runTasks()`). `executeWithRetry()` in `orchestrator.ts` handles the retry loop with exponential backoff (capped at 30s). Token usage is accumulated across all attempts. Emits `task_retry` event via `onProgress`.
+
+### Error Handling
+
+- Tool errors → caught, returned as `ToolResult(isError: true)`, never thrown
+- Task failures → retry if `maxRetries > 0`, then cascade to all dependents; independent tasks continue
+- LLM API errors → propagate to caller
+
+### Built-in Tools
+
+`bash`, `file_read`, `file_write`, `file_edit`, `grep` — registered via `registerBuiltInTools(registry)`.
+
+### Adding an LLM Adapter
+
+Implement `LLMAdapter` interface with `chat(messages, options)` and `stream(messages, options)`, then register in `createAdapter()` factory in `src/llm/adapter.ts`.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..1036d4e
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,48 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a positive experience for everyone, regardless of background or
+identity.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment:
+
+- Using welcoming and inclusive language
+- Being respectful of differing viewpoints and experiences
+- Gracefully accepting constructive feedback
+- Focusing on what is best for the community
+- Showing empathy towards other community members
+
+Examples of unacceptable behavior:
+
+- Trolling, insulting or derogatory comments, and personal attacks
+- Public or private unwelcome conduct
+- Publishing others' private information without explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate or harmful.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+
+## Enforcement
+
+Instances of unacceptable behavior may be reported to the community leaders
+responsible for enforcement at **jack@yuanasi.com**. All complaints will be
+reviewed and investigated promptly and fairly.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..e17dd36
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,72 @@
+# Contributing
+
+Thanks for your interest in contributing to Open Multi-Agent! This guide covers the basics to get you started.
+
+## Setup
+
+```bash
+git clone https://github.com/JackChen-me/open-multi-agent.git
+cd open-multi-agent
+npm install
+```
+
+Requires Node.js >= 18.
+
+## Development Commands
+
+```bash
+npm run build # Compile TypeScript (src/ → dist/)
+npm run dev # Watch mode compilation
+npm run lint # Type-check (tsc --noEmit)
+npm test # Run all tests (vitest)
+npm run test:watch # Vitest watch mode
+```
+
+## Running Tests
+
+All tests live in `tests/`. They test core modules (TaskQueue, SharedMemory, ToolExecutor, Semaphore) without requiring API keys or network access.
+
+```bash
+npm test
+```
+
+Every PR must pass `npm run lint && npm test`. CI runs both automatically on Node 18, 20, and 22.
+
+## Making a Pull Request
+
+1. Fork the repo and create a branch from `main`
+2. Make your changes
+3. Add or update tests if you changed behavior
+4. Run `npm run lint && npm test` locally
+5. Open a PR against `main`
+
+### PR Checklist
+
+- [ ] `npm run lint` passes
+- [ ] `npm test` passes
+- [ ] New behavior has test coverage
+- [ ] Linked to a relevant issue (if one exists)
+
+## Code Style
+
+- TypeScript strict mode, ES modules (`.js` extensions in imports)
+- No additional linter/formatter configured — follow existing patterns
+- Keep dependencies minimal (currently 3 runtime deps: `@anthropic-ai/sdk`, `openai`, `zod`)
+
+## Architecture Overview
+
+See the [README](./README.md#architecture) for an architecture diagram. Key entry points:
+
+- **Orchestrator**: `src/orchestrator/orchestrator.ts` — top-level API
+- **Task system**: `src/task/queue.ts`, `src/task/task.ts` — dependency DAG
+- **Agent**: `src/agent/runner.ts` — conversation loop
+- **Tools**: `src/tool/framework.ts`, `src/tool/executor.ts` — tool registry and execution
+- **LLM adapters**: `src/llm/` — Anthropic, OpenAI, Copilot
+
+## Where to Contribute
+
+Check the [issues](https://github.com/JackChen-me/open-multi-agent/issues) page. Issues labeled `good first issue` are scoped and approachable. Issues labeled `help wanted` are larger but well-defined.
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the MIT License.
diff --git a/DECISIONS.md b/DECISIONS.md
new file mode 100644
index 0000000..a16151f
--- /dev/null
+++ b/DECISIONS.md
@@ -0,0 +1,43 @@
+# Architecture Decisions
+
+This document records deliberate "won't do" decisions for the project. These are features we evaluated and chose NOT to implement — not because they're bad ideas, but because they conflict with our positioning as the **simplest multi-agent framework**.
+
+If you're considering a PR in any of these areas, please open a discussion first.
+
+## Won't Do
+
+### 1. Agent Handoffs
+
+**What**: Agent A transfers an in-progress conversation to Agent B (like OpenAI Agents SDK `handoff()`).
+
+**Why not**: Handoffs are a different paradigm from our task-based model. Our tasks have clear boundaries — one agent, one task, one result. Handoffs blur those boundaries and add state-transfer complexity. Users who need handoffs likely need a different framework (OpenAI Agents SDK is purpose-built for this).
+
+### 2. State Persistence / Checkpointing
+
+**What**: Save workflow state to a database so long-running workflows can resume after crashes (like LangGraph checkpointing).
+
+**Why not**: Requires a storage backend (SQLite, Redis, Postgres), schema migrations, and serialization logic. This is enterprise infrastructure — it triples the complexity surface. Our target users run workflows that complete in seconds to minutes, not hours. If you need checkpointing, LangGraph is the right tool.
+
+**Related**: Closing #20 with this rationale.
+
+### 3. A2A Protocol (Agent-to-Agent)
+
+**What**: Google's open protocol for agents on different servers to discover and communicate with each other.
+
+**Why not**: Too early — the spec is still evolving and adoption is minimal. Our users run agents in a single process, not across distributed services. If A2A matures and there's real demand, we can revisit. Today it would add complexity for zero practical benefit.
+
+### 4. MCP Integration (Model Context Protocol)
+
+**What**: Anthropic's protocol for connecting LLMs to external tools and data sources.
+
+**Why not**: MCP is valuable but targets a different layer. Our `defineTool()` API already lets users wrap any external service as a tool in ~10 lines of code. Adding MCP would mean maintaining protocol compatibility, transport layers, and tool discovery — complexity that serves tool platform builders, not our target users who just want to run agent teams.
+
+### 5. Dashboard / Visualization
+
+**What**: Built-in web UI to visualize task DAGs, agent activity, and token usage.
+
+**Why not**: We expose data, we don't build UI. The `onProgress` callback and upcoming `onTrace` (#18) give users all the raw data. They can pipe it into Grafana, build a custom dashboard, or use console logs. Shipping a web UI means owning a frontend stack, which is outside our scope.
+
+---
+
+*Last updated: 2026-04-03*
diff --git a/README.md b/README.md
index 31d3509..d9b5d39 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
# Open Multi-Agent
-Build AI agent teams that work together. One agent plans, another implements, a third reviews — the framework handles task scheduling, dependencies, and communication automatically.
+TypeScript framework for multi-agent orchestration. One `runTeam()` call from goal to result — the framework decomposes it into tasks, resolves dependencies, and runs agents in parallel.
+
+3 runtime dependencies · 27 source files · Deploys anywhere Node.js runs · Mentioned in [Latent Space](https://www.latent.space/p/ainews-a-quiet-april-fools) AI News
[](https://github.com/JackChen-me/open-multi-agent/stargazers)
[](./LICENSE)
@@ -10,40 +12,26 @@ Build AI agent teams that work together. One agent plans, another implements, a
## Why Open Multi-Agent?
-- **Multi-Agent Teams** — Define agents with different roles, tools, and even different models. They collaborate through a message bus and shared memory.
-- **Task DAG Scheduling** — Tasks have dependencies. The framework resolves them topologically — dependent tasks wait, independent tasks run in parallel.
-- **Model Agnostic** — Claude and GPT in the same team. Swap models per agent. Bring your own adapter for any LLM.
-- **In-Process Execution** — No subprocess overhead. Everything runs in one Node.js process. Deploy to serverless, Docker, CI/CD.
+- **Goal In, Result Out** — `runTeam(team, "Build a REST API")`. A coordinator agent auto-decomposes the goal into a task DAG with dependencies and assignees, runs independent tasks in parallel, and synthesizes the final output. No manual task definitions or graph wiring required.
+- **TypeScript-Native** — Built for the Node.js ecosystem. `npm install`, import, run. No Python runtime, no subprocess bridge, no sidecar services. Embed in Express, Next.js, serverless functions, or CI/CD pipelines.
+- **Auditable and Lightweight** — 3 runtime dependencies (`@anthropic-ai/sdk`, `openai`, `zod`). 27 source files. The entire codebase is readable in an afternoon.
+- **Model Agnostic** — Claude, GPT, Gemma 4, and local models (Ollama, vLLM, LM Studio) in the same team. Swap models per agent via `baseURL`.
+- **Multi-Agent Collaboration** — Agents with different roles, tools, and models collaborate through a message bus and shared memory.
+- **Structured Output** — Add `outputSchema` (Zod) to any agent. Output is parsed as JSON, validated, and auto-retried once on failure. Access typed results via `result.structured`.
+- **Task Retry** — Set `maxRetries` on tasks for automatic retry with exponential backoff. Failed attempts accumulate token usage for accurate billing.
+- **Observability** — Optional `onTrace` callback emits structured spans for every LLM call, tool execution, task, and agent run — with timing, token usage, and a shared `runId` for correlation. Zero overhead when not subscribed, zero extra dependencies.
## Quick Start
+Requires Node.js >= 18.
+
```bash
npm install @jackchen_me/open-multi-agent
```
-Set `ANTHROPIC_API_KEY` (and optionally `OPENAI_API_KEY`) in your environment.
+Set `ANTHROPIC_API_KEY` (and optionally `OPENAI_API_KEY` or `GITHUB_TOKEN` for Copilot) in your environment. Local models via Ollama require no API key — see [example 06](examples/06-local-model.ts).
-```typescript
-import { OpenMultiAgent } from '@jackchen_me/open-multi-agent'
-
-const orchestrator = new OpenMultiAgent({ defaultModel: 'claude-sonnet-4-6' })
-
-// One agent, one task
-const result = await orchestrator.runAgent(
- {
- name: 'coder',
- model: 'claude-sonnet-4-6',
- tools: ['bash', 'file_write'],
- },
- 'Write a TypeScript function that reverses a string, save it to /tmp/reverse.ts, and run it.',
-)
-
-console.log(result.output)
-```
-
-## Multi-Agent Team
-
-This is where it gets interesting. Three agents, one goal:
+Three agents, one goal — the framework handles the rest:
```typescript
import { OpenMultiAgent } from '@jackchen_me/open-multi-agent'
@@ -88,132 +76,52 @@ console.log(`Success: ${result.success}`)
console.log(`Tokens: ${result.totalTokenUsage.output_tokens} output tokens`)
```
-## More Examples
+What happens under the hood:
-
-Task Pipeline — explicit control over task graph and assignments
-
-```typescript
-const result = await orchestrator.runTasks(team, [
- {
- title: 'Design the data model',
- description: 'Write a TypeScript interface spec to /tmp/spec.md',
- assignee: 'architect',
- },
- {
- title: 'Implement the module',
- description: 'Read /tmp/spec.md and implement the module in /tmp/src/',
- assignee: 'developer',
- dependsOn: ['Design the data model'], // blocked until design completes
- },
- {
- title: 'Write tests',
- description: 'Read the implementation and write Vitest tests.',
- assignee: 'developer',
- dependsOn: ['Implement the module'],
- },
- {
- title: 'Review code',
- description: 'Review /tmp/src/ and produce a structured code review.',
- assignee: 'reviewer',
- dependsOn: ['Implement the module'], // can run in parallel with tests
- },
-])
+```
+agent_start coordinator
+task_start architect
+task_complete architect
+task_start developer
+task_start developer // independent tasks run in parallel
+task_complete developer
+task_start reviewer // unblocked after implementation
+task_complete developer
+task_complete reviewer
+agent_complete coordinator // synthesizes final result
+Success: true
+Tokens: 12847 output tokens
```
-
+## Three Ways to Run
-
-Custom Tools — define tools with Zod schemas
+| Mode | Method | When to use |
+|------|--------|-------------|
+| Single agent | `runAgent()` | One agent, one prompt — simplest entry point |
+| Auto-orchestrated team | `runTeam()` | Give a goal, framework plans and executes |
+| Explicit pipeline | `runTasks()` | You define the task graph and assignments |
-```typescript
-import { z } from 'zod'
-import { defineTool, Agent, ToolRegistry, ToolExecutor, registerBuiltInTools } from '@jackchen_me/open-multi-agent'
+## Examples
-const searchTool = defineTool({
- name: 'web_search',
- description: 'Search the web and return the top results.',
- inputSchema: z.object({
- query: z.string().describe('The search query.'),
- maxResults: z.number().optional().describe('Number of results (default 5).'),
- }),
- execute: async ({ query, maxResults = 5 }) => {
- const results = await mySearchProvider(query, maxResults)
- return { data: JSON.stringify(results), isError: false }
- },
-})
+All examples are runnable scripts in [`examples/`](./examples/). Run any of them with `npx tsx`:
-const registry = new ToolRegistry()
-registerBuiltInTools(registry)
-registry.register(searchTool)
-
-const executor = new ToolExecutor(registry)
-const agent = new Agent(
- { name: 'researcher', model: 'claude-sonnet-4-6', tools: ['web_search'] },
- registry,
- executor,
-)
-
-const result = await agent.run('Find the three most recent TypeScript releases.')
+```bash
+npx tsx examples/01-single-agent.ts
```
-
-
-
-Multi-Model Teams — mix Claude and GPT in one workflow
-
-```typescript
-const claudeAgent: AgentConfig = {
- name: 'strategist',
- model: 'claude-opus-4-6',
- provider: 'anthropic',
- systemPrompt: 'You plan high-level approaches.',
- tools: ['file_write'],
-}
-
-const gptAgent: AgentConfig = {
- name: 'implementer',
- model: 'gpt-5.4',
- provider: 'openai',
- systemPrompt: 'You implement plans as working code.',
- tools: ['bash', 'file_read', 'file_write'],
-}
-
-const team = orchestrator.createTeam('mixed-team', {
- name: 'mixed-team',
- agents: [claudeAgent, gptAgent],
- sharedMemory: true,
-})
-
-const result = await orchestrator.runTeam(team, 'Build a CLI tool that converts JSON to CSV.')
-```
-
-
-
-
-Streaming Output
-
-```typescript
-import { Agent, ToolRegistry, ToolExecutor, registerBuiltInTools } from '@jackchen_me/open-multi-agent'
-
-const registry = new ToolRegistry()
-registerBuiltInTools(registry)
-const executor = new ToolExecutor(registry)
-
-const agent = new Agent(
- { name: 'writer', model: 'claude-sonnet-4-6', maxTurns: 3 },
- registry,
- executor,
-)
-
-for await (const event of agent.stream('Explain monads in two sentences.')) {
- if (event.type === 'text' && typeof event.data === 'string') {
- process.stdout.write(event.data)
- }
-}
-```
-
-
+| Example | What it shows |
+|---------|---------------|
+| [01 — Single Agent](examples/01-single-agent.ts) | `runAgent()` one-shot, `stream()` streaming, `prompt()` multi-turn |
+| [02 — Team Collaboration](examples/02-team-collaboration.ts) | `runTeam()` auto-orchestration with coordinator pattern |
+| [03 — Task Pipeline](examples/03-task-pipeline.ts) | `runTasks()` explicit dependency graph (design → implement → test + review) |
+| [04 — Multi-Model Team](examples/04-multi-model-team.ts) | `defineTool()` custom tools, mixed Anthropic + OpenAI providers, `AgentPool` |
+| [05 — Copilot](examples/05-copilot-test.ts) | GitHub Copilot as an LLM provider |
+| [06 — Local Model](examples/06-local-model.ts) | Ollama + Claude in one pipeline via `baseURL` (works with vLLM, LM Studio, etc.) |
+| [07 — Fan-Out / Aggregate](examples/07-fan-out-aggregate.ts) | `runParallel()` MapReduce — 3 analysts in parallel, then synthesize |
+| [08 — Gemma 4 Local](examples/08-gemma4-local.ts) | `runTasks()` + `runTeam()` with local Gemma 4 via Ollama — zero API cost |
+| [09 — Structured Output](examples/09-structured-output.ts) | `outputSchema` (Zod) on AgentConfig — validated JSON via `result.structured` |
+| [10 — Task Retry](examples/10-task-retry.ts) | `maxRetries` / `retryDelayMs` / `retryBackoff` with `task_retry` progress events |
+| [11 — Trace Observability](examples/11-trace-observability.ts) | `onTrace` callback — structured spans for LLM calls, tools, tasks, and agents |
## Architecture
@@ -246,6 +154,7 @@ for await (const event of agent.stream('Explain monads in two sentences.')) {
│ - prompt() │───►│ LLMAdapter │
│ - stream() │ │ - AnthropicAdapter │
└────────┬──────────┘ │ - OpenAIAdapter │
+ │ │ - CopilotAdapter │
│ └──────────────────────┘
┌────────▼──────────┐
│ AgentRunner │ ┌──────────────────────┐
@@ -265,17 +174,46 @@ for await (const event of agent.stream('Explain monads in two sentences.')) {
| `file_edit` | Edit a file by replacing an exact string match. |
| `grep` | Search file contents with regex. Uses ripgrep when available, falls back to Node.js. |
+## Supported Providers
+
+| Provider | Config | Env var | Status |
+|----------|--------|---------|--------|
+| Anthropic (Claude) | `provider: 'anthropic'` | `ANTHROPIC_API_KEY` | Verified |
+| OpenAI (GPT) | `provider: 'openai'` | `OPENAI_API_KEY` | Verified |
+| GitHub Copilot | `provider: 'copilot'` | `GITHUB_TOKEN` | Verified |
+| Ollama / vLLM / LM Studio | `provider: 'openai'` + `baseURL` | — | Verified |
+
+Verified local models with tool-calling: **Gemma 4** (see [example 08](examples/08-gemma4-local.ts)).
+
+Any OpenAI-compatible API should work via `provider: 'openai'` + `baseURL` (DeepSeek, Groq, Mistral, Qwen, MiniMax, etc.). These providers have not been fully verified yet — contributions welcome via [#25](https://github.com/JackChen-me/open-multi-agent/issues/25).
+
## Contributing
Issues, feature requests, and PRs are welcome. Some areas where contributions would be especially valuable:
-- **LLM Adapters** — Ollama, llama.cpp, vLLM, Gemini. The `LLMAdapter` interface requires just two methods: `chat()` and `stream()`.
+- **Provider integrations** — Verify and document OpenAI-compatible providers (DeepSeek, Groq, Qwen, MiniMax, etc.) via `baseURL`. See [#25](https://github.com/JackChen-me/open-multi-agent/issues/25). For providers that are NOT OpenAI-compatible (e.g. Gemini), a new `LLMAdapter` implementation is welcome — the interface requires just two methods: `chat()` and `stream()`.
- **Examples** — Real-world workflows and use cases.
- **Documentation** — Guides, tutorials, and API docs.
+## Author
+
+> JackChen — Ex PM (¥100M+ revenue), now indie builder. Follow on [X](https://x.com/JackChen_x) for AI Agent insights.
+
+## Contributors
+
+
+
+
+
## Star History
-[](https://star-history.com/#JackChen-me/open-multi-agent&Date)
+
+
+
+
+
+
+
## License
diff --git a/README_zh.md b/README_zh.md
index e9a3f00..a8b680c 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -1,6 +1,8 @@
# Open Multi-Agent
-构建能协同工作的 AI 智能体团队。一个智能体负责规划,一个负责实现,一个负责审查——框架自动处理任务调度、依赖关系和智能体间通信。
+TypeScript 多智能体编排框架。一次 `runTeam()` 调用从目标到结果——框架自动拆解任务、解析依赖、并行执行。
+
+3 个运行时依赖 · 27 个源文件 · Node.js 能跑的地方都能部署 · 被 [Latent Space](https://www.latent.space/p/ainews-a-quiet-april-fools) AI News 提及(AI 工程领域头部 Newsletter,17 万+订阅者)
[](https://github.com/JackChen-me/open-multi-agent/stargazers)
[](./LICENSE)
@@ -10,40 +12,26 @@
## 为什么选择 Open Multi-Agent?
-- **多智能体团队** — 定义不同角色、工具甚至不同模型的智能体。它们通过消息总线和共享内存协作。
-- **任务 DAG 调度** — 任务之间存在依赖关系。框架进行拓扑排序——有依赖的任务等待,无依赖的任务并行执行。
-- **模型无关** — Claude 和 GPT 可以在同一个团队中使用。每个智能体可以单独配置模型。你也可以为任何 LLM 编写自己的适配器。
-- **进程内执行** — 没有子进程开销。所有内容在一个 Node.js 进程中运行。可部署到 Serverless、Docker、CI/CD。
+- **目标进,结果出** — `runTeam(team, "构建一个 REST API")`。协调者智能体自动将目标拆解为带依赖关系的任务图,分配给对应智能体,独立任务并行执行,最终合成输出。无需手动定义任务或编排流程图。
+- **TypeScript 原生** — 为 Node.js 生态而生。`npm install` 即用,无需 Python 运行时、无子进程桥接、无额外基础设施。可嵌入 Express、Next.js、Serverless 函数或 CI/CD 流水线。
+- **可审计、极轻量** — 3 个运行时依赖(`@anthropic-ai/sdk`、`openai`、`zod`),27 个源文件。一个下午就能读完全部源码。
+- **模型无关** — Claude、GPT、Gemma 4 和本地模型(Ollama、vLLM、LM Studio)可以在同一个团队中使用。通过 `baseURL` 即可接入任何 OpenAI 兼容服务。
+- **多智能体协作** — 定义不同角色、工具和模型的智能体,通过消息总线和共享内存协作。
+- **结构化输出** — 为任意智能体添加 `outputSchema`(Zod),输出自动解析为 JSON 并校验,校验失败自动重试一次。通过 `result.structured` 获取类型化结果。
+- **任务重试** — 为任务设置 `maxRetries`,失败时自动指数退避重试。所有尝试的 token 用量累计,确保计费准确。
+- **可观测性** — 可选的 `onTrace` 回调为每次 LLM 调用、工具执行、任务和智能体运行发出结构化 span 事件——包含耗时、token 用量和共享的 `runId` 用于关联追踪。未订阅时零开销,零额外依赖。
## 快速开始
+需要 Node.js >= 18。
+
```bash
npm install @jackchen_me/open-multi-agent
```
-在环境变量中设置 `ANTHROPIC_API_KEY`(以及可选的 `OPENAI_API_KEY`)。
+在环境变量中设置 `ANTHROPIC_API_KEY`(以及可选的 `OPENAI_API_KEY` 或用于 Copilot 的 `GITHUB_TOKEN`)。通过 Ollama 使用本地模型无需 API key — 参见 [example 06](examples/06-local-model.ts)。
-```typescript
-import { OpenMultiAgent } from '@jackchen_me/open-multi-agent'
-
-const orchestrator = new OpenMultiAgent({ defaultModel: 'claude-sonnet-4-6' })
-
-// 一个智能体,一个任务
-const result = await orchestrator.runAgent(
- {
- name: 'coder',
- model: 'claude-sonnet-4-6',
- tools: ['bash', 'file_write'],
- },
- 'Write a TypeScript function that reverses a string, save it to /tmp/reverse.ts, and run it.',
-)
-
-console.log(result.output)
-```
-
-## 多智能体团队
-
-这才是有意思的地方。三个智能体,一个目标:
+三个智能体,一个目标——框架处理剩下的一切:
```typescript
import { OpenMultiAgent } from '@jackchen_me/open-multi-agent'
@@ -88,132 +76,52 @@ console.log(`成功: ${result.success}`)
console.log(`Token 用量: ${result.totalTokenUsage.output_tokens} output tokens`)
```
-## 更多示例
+执行过程:
-
-任务流水线 — 显式控制任务图和分配
-
-```typescript
-const result = await orchestrator.runTasks(team, [
- {
- title: 'Design the data model',
- description: 'Write a TypeScript interface spec to /tmp/spec.md',
- assignee: 'architect',
- },
- {
- title: 'Implement the module',
- description: 'Read /tmp/spec.md and implement the module in /tmp/src/',
- assignee: 'developer',
- dependsOn: ['Design the data model'], // 等待设计完成后才开始
- },
- {
- title: 'Write tests',
- description: 'Read the implementation and write Vitest tests.',
- assignee: 'developer',
- dependsOn: ['Implement the module'],
- },
- {
- title: 'Review code',
- description: 'Review /tmp/src/ and produce a structured code review.',
- assignee: 'reviewer',
- dependsOn: ['Implement the module'], // 可以和测试并行执行
- },
-])
+```
+agent_start coordinator
+task_start architect
+task_complete architect
+task_start developer
+task_start developer // 无依赖的任务并行执行
+task_complete developer
+task_start reviewer // 实现完成后自动解锁
+task_complete developer
+task_complete reviewer
+agent_complete coordinator // 综合所有结果
+Success: true
+Tokens: 12847 output tokens
```
-
+## 三种运行模式
-
-自定义工具 — 使用 Zod schema 定义工具
+| 模式 | 方法 | 适用场景 |
+|------|------|----------|
+| 单智能体 | `runAgent()` | 一个智能体,一个提示词——最简入口 |
+| 自动编排团队 | `runTeam()` | 给一个目标,框架自动规划和执行 |
+| 显式任务管线 | `runTasks()` | 你自己定义任务图和分配 |
-```typescript
-import { z } from 'zod'
-import { defineTool, Agent, ToolRegistry, ToolExecutor, registerBuiltInTools } from '@jackchen_me/open-multi-agent'
+## 示例
-const searchTool = defineTool({
- name: 'web_search',
- description: 'Search the web and return the top results.',
- inputSchema: z.object({
- query: z.string().describe('The search query.'),
- maxResults: z.number().optional().describe('Number of results (default 5).'),
- }),
- execute: async ({ query, maxResults = 5 }) => {
- const results = await mySearchProvider(query, maxResults)
- return { data: JSON.stringify(results), isError: false }
- },
-})
+所有示例都是可运行脚本,位于 [`examples/`](./examples/) 目录。使用 `npx tsx` 运行:
-const registry = new ToolRegistry()
-registerBuiltInTools(registry)
-registry.register(searchTool)
-
-const executor = new ToolExecutor(registry)
-const agent = new Agent(
- { name: 'researcher', model: 'claude-sonnet-4-6', tools: ['web_search'] },
- registry,
- executor,
-)
-
-const result = await agent.run('Find the three most recent TypeScript releases.')
+```bash
+npx tsx examples/01-single-agent.ts
```
-
-
-
-多模型团队 — 在一个工作流中混合使用 Claude 和 GPT
-
-```typescript
-const claudeAgent: AgentConfig = {
- name: 'strategist',
- model: 'claude-opus-4-6',
- provider: 'anthropic',
- systemPrompt: 'You plan high-level approaches.',
- tools: ['file_write'],
-}
-
-const gptAgent: AgentConfig = {
- name: 'implementer',
- model: 'gpt-5.4',
- provider: 'openai',
- systemPrompt: 'You implement plans as working code.',
- tools: ['bash', 'file_read', 'file_write'],
-}
-
-const team = orchestrator.createTeam('mixed-team', {
- name: 'mixed-team',
- agents: [claudeAgent, gptAgent],
- sharedMemory: true,
-})
-
-const result = await orchestrator.runTeam(team, 'Build a CLI tool that converts JSON to CSV.')
-```
-
-
-
-
-流式输出
-
-```typescript
-import { Agent, ToolRegistry, ToolExecutor, registerBuiltInTools } from '@jackchen_me/open-multi-agent'
-
-const registry = new ToolRegistry()
-registerBuiltInTools(registry)
-const executor = new ToolExecutor(registry)
-
-const agent = new Agent(
- { name: 'writer', model: 'claude-sonnet-4-6', maxTurns: 3 },
- registry,
- executor,
-)
-
-for await (const event of agent.stream('Explain monads in two sentences.')) {
- if (event.type === 'text' && typeof event.data === 'string') {
- process.stdout.write(event.data)
- }
-}
-```
-
-
+| 示例 | 展示内容 |
+|------|----------|
+| [01 — 单智能体](examples/01-single-agent.ts) | `runAgent()` 单次调用、`stream()` 流式输出、`prompt()` 多轮对话 |
+| [02 — 团队协作](examples/02-team-collaboration.ts) | `runTeam()` 自动编排 + 协调者模式 |
+| [03 — 任务流水线](examples/03-task-pipeline.ts) | `runTasks()` 显式依赖图(设计 → 实现 → 测试 + 评审) |
+| [04 — 多模型团队](examples/04-multi-model-team.ts) | `defineTool()` 自定义工具、Anthropic + OpenAI 混合、`AgentPool` |
+| [05 — Copilot](examples/05-copilot-test.ts) | GitHub Copilot 作为 LLM 提供者 |
+| [06 — 本地模型](examples/06-local-model.ts) | Ollama + Claude 混合流水线,通过 `baseURL` 接入(兼容 vLLM、LM Studio 等) |
+| [07 — 扇出聚合](examples/07-fan-out-aggregate.ts) | `runParallel()` MapReduce — 3 个分析师并行,然后综合 |
+| [08 — Gemma 4 本地](examples/08-gemma4-local.ts) | `runTasks()` + `runTeam()` 本地 Gemma 4 via Ollama — 零 API 费用 |
+| [09 — 结构化输出](examples/09-structured-output.ts) | `outputSchema`(Zod)— 校验 JSON 输出,通过 `result.structured` 获取 |
+| [10 — 任务重试](examples/10-task-retry.ts) | `maxRetries` / `retryDelayMs` / `retryBackoff` + `task_retry` 进度事件 |
+| [11 — 可观测性](examples/11-trace-observability.ts) | `onTrace` 回调 — LLM 调用、工具、任务、智能体的结构化 span 事件 |
## 架构
@@ -246,6 +154,7 @@ for await (const event of agent.stream('Explain monads in two sentences.')) {
│ - prompt() │───►│ LLMAdapter │
│ - stream() │ │ - AnthropicAdapter │
└────────┬──────────┘ │ - OpenAIAdapter │
+ │ │ - CopilotAdapter │
│ └──────────────────────┘
┌────────▼──────────┐
│ AgentRunner │ ┌──────────────────────┐
@@ -265,17 +174,46 @@ for await (const event of agent.stream('Explain monads in two sentences.')) {
| `file_edit` | 通过精确字符串匹配编辑文件。 |
| `grep` | 使用正则表达式搜索文件内容。优先使用 ripgrep,回退到 Node.js 实现。 |
+## 支持的 Provider
+
+| Provider | 配置 | 环境变量 | 状态 |
+|----------|------|----------|------|
+| Anthropic (Claude) | `provider: 'anthropic'` | `ANTHROPIC_API_KEY` | 已验证 |
+| OpenAI (GPT) | `provider: 'openai'` | `OPENAI_API_KEY` | 已验证 |
+| GitHub Copilot | `provider: 'copilot'` | `GITHUB_TOKEN` | 已验证 |
+| Ollama / vLLM / LM Studio | `provider: 'openai'` + `baseURL` | — | 已验证 |
+
+已验证支持 tool-calling 的本地模型:**Gemma 4**(见[示例 08](examples/08-gemma4-local.ts))。
+
+任何 OpenAI 兼容 API 均可通过 `provider: 'openai'` + `baseURL` 接入(DeepSeek、Groq、Mistral、Qwen、MiniMax 等)。这些 Provider 尚未完整验证——欢迎通过 [#25](https://github.com/JackChen-me/open-multi-agent/issues/25) 贡献验证。
+
## 参与贡献
欢迎提 Issue、功能需求和 PR。以下方向的贡献尤其有价值:
-- **LLM 适配器** — Ollama、llama.cpp、vLLM、Gemini。`LLMAdapter` 接口只需实现两个方法:`chat()` 和 `stream()`。
+- **Provider 集成** — 验证并文档化 OpenAI 兼容 Provider(DeepSeek、Groq、Qwen、MiniMax 等)通过 `baseURL` 接入。详见 [#25](https://github.com/JackChen-me/open-multi-agent/issues/25)。对于非 OpenAI 兼容的 Provider(如 Gemini),欢迎贡献新的 `LLMAdapter` 实现——接口只需两个方法:`chat()` 和 `stream()`。
- **示例** — 真实场景的工作流和用例。
- **文档** — 指南、教程和 API 文档。
+## 作者
+
+> JackChen — 前 WPS 产品经理,现独立创业者。关注小红书[「杰克西|硅基杠杆」](https://www.xiaohongshu.com/user/profile/5a1bdc1e4eacab4aa39ea6d6),持续获取我的 AI Agent 观点和思考。
+
+## 贡献者
+
+
+
+
+
## Star 趋势
-[](https://star-history.com/#JackChen-me/open-multi-agent&Date)
+
+
+
+
+
+
+
## 许可证
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..235d6d9
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,17 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+|---------|-----------|
+| latest | Yes |
+
+## Reporting a Vulnerability
+
+If you discover a security vulnerability, please report it responsibly via email:
+
+**jack@yuanasi.com**
+
+Please do **not** open a public GitHub issue for security vulnerabilities.
+
+We will acknowledge receipt within 48 hours and aim to provide a fix or mitigation plan within 7 days.
diff --git a/examples/05-copilot-test.ts b/examples/05-copilot-test.ts
new file mode 100644
index 0000000..d027aea
--- /dev/null
+++ b/examples/05-copilot-test.ts
@@ -0,0 +1,49 @@
+/**
+ * Quick smoke test for the Copilot adapter.
+ *
+ * Run:
+ * npx tsx examples/05-copilot-test.ts
+ *
+ * If GITHUB_COPILOT_TOKEN is not set, the adapter will start an interactive
+ * OAuth2 device flow — you'll be prompted to sign in via your browser.
+ */
+
+import { OpenMultiAgent } from '../src/index.js'
+import type { OrchestratorEvent } from '../src/types.js'
+
+const orchestrator = new OpenMultiAgent({
+ defaultModel: 'gpt-4o',
+ defaultProvider: 'copilot',
+ onProgress: (event: OrchestratorEvent) => {
+ if (event.type === 'agent_start') {
+ console.log(`[start] agent=${event.agent}`)
+ } else if (event.type === 'agent_complete') {
+ console.log(`[complete] agent=${event.agent}`)
+ }
+ },
+})
+
+console.log('Testing Copilot adapter with gpt-4o...\n')
+
+const result = await orchestrator.runAgent(
+ {
+ name: 'assistant',
+ model: 'gpt-4o',
+ provider: 'copilot',
+ systemPrompt: 'You are a helpful assistant. Keep answers brief.',
+ maxTurns: 1,
+ maxTokens: 256,
+ },
+ 'What is 2 + 2? Reply in one sentence.',
+)
+
+if (result.success) {
+ console.log('\nAgent output:')
+ console.log('─'.repeat(60))
+ console.log(result.output)
+ console.log('─'.repeat(60))
+ console.log(`\nTokens: input=${result.tokenUsage.input_tokens}, output=${result.tokenUsage.output_tokens}`)
+} else {
+ console.error('Agent failed:', result.output)
+ process.exit(1)
+}
diff --git a/examples/06-local-model.ts b/examples/06-local-model.ts
new file mode 100644
index 0000000..d7cf292
--- /dev/null
+++ b/examples/06-local-model.ts
@@ -0,0 +1,199 @@
+/**
+ * Example 06 — Local Model + Cloud Model Team (Ollama + Claude)
+ *
+ * Demonstrates mixing a local model served by Ollama with a cloud model
+ * (Claude) in the same task pipeline. The key technique is using
+ * `provider: 'openai'` with a custom `baseURL` pointing at Ollama's
+ * OpenAI-compatible endpoint.
+ *
+ * This pattern works with ANY OpenAI-compatible local server:
+ * - Ollama → http://localhost:11434/v1
+ * - vLLM → http://localhost:8000/v1
+ * - LM Studio → http://localhost:1234/v1
+ * - llama.cpp → http://localhost:8080/v1
+ * Just change the baseURL and model name below.
+ *
+ * Run:
+ * npx tsx examples/06-local-model.ts
+ *
+ * Prerequisites:
+ * 1. Ollama installed and running: https://ollama.com
+ * 2. Pull the model: ollama pull llama3.1
+ * 3. ANTHROPIC_API_KEY env var must be set.
+ */
+
+import { OpenMultiAgent } from '../src/index.js'
+import type { AgentConfig, OrchestratorEvent, Task } from '../src/types.js'
+
+// ---------------------------------------------------------------------------
+// Agents
+// ---------------------------------------------------------------------------
+
+/**
+ * Coder — uses Claude (Anthropic) for high-quality code generation.
+ */
+const coder: AgentConfig = {
+ name: 'coder',
+ model: 'claude-sonnet-4-6',
+ provider: 'anthropic',
+ systemPrompt: `You are a senior TypeScript developer. Write clean, well-typed,
+production-quality code. Use the tools to write files to /tmp/local-model-demo/.
+Always include brief JSDoc comments on exported functions.`,
+ tools: ['bash', 'file_write'],
+ maxTurns: 6,
+}
+
+/**
+ * Reviewer — uses a local Ollama model via the OpenAI-compatible API.
+ * The apiKey is required by the OpenAI SDK but Ollama ignores it,
+ * so we pass the placeholder string 'ollama'.
+ */
+const reviewer: AgentConfig = {
+ name: 'reviewer',
+ model: 'llama3.1',
+ provider: 'openai', // 'openai' here means "OpenAI-compatible protocol", not the OpenAI cloud
+ baseURL: 'http://localhost:11434/v1',
+ apiKey: 'ollama',
+ systemPrompt: `You are a code reviewer. You read source files and produce a structured review.
+Your review MUST include these sections:
+- Summary (2-3 sentences)
+- Strengths (bullet list)
+- Issues (bullet list — or "None found" if the code is clean)
+- Verdict: SHIP or NEEDS WORK
+
+Be specific and constructive. Reference line numbers or function names when possible.`,
+ tools: ['file_read'],
+ maxTurns: 4,
+}
+
+// ---------------------------------------------------------------------------
+// Progress handler
+// ---------------------------------------------------------------------------
+
+const taskTimes = new Map()
+
+function handleProgress(event: OrchestratorEvent): void {
+ const ts = new Date().toISOString().slice(11, 23)
+
+ switch (event.type) {
+ case 'task_start': {
+ taskTimes.set(event.task ?? '', Date.now())
+ const task = event.data as Task | undefined
+ console.log(`[${ts}] TASK READY "${task?.title ?? event.task}" → ${task?.assignee ?? '?'}`)
+ break
+ }
+ case 'task_complete': {
+ const elapsed = Date.now() - (taskTimes.get(event.task ?? '') ?? Date.now())
+ console.log(`[${ts}] TASK DONE task=${event.task} in ${elapsed}ms`)
+ break
+ }
+ case 'agent_start':
+ console.log(`[${ts}] AGENT START ${event.agent}`)
+ break
+ case 'agent_complete':
+ console.log(`[${ts}] AGENT DONE ${event.agent}`)
+ break
+ case 'error':
+ console.error(`[${ts}] ERROR ${event.agent ?? ''} task=${event.task ?? '?'}`)
+ break
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Orchestrator + Team
+// ---------------------------------------------------------------------------
+
+const orchestrator = new OpenMultiAgent({
+ defaultModel: 'claude-sonnet-4-6',
+ maxConcurrency: 2,
+ onProgress: handleProgress,
+})
+
+const team = orchestrator.createTeam('local-cloud-team', {
+ name: 'local-cloud-team',
+ agents: [coder, reviewer],
+ sharedMemory: true,
+})
+
+// ---------------------------------------------------------------------------
+// Task pipeline: code → review
+// ---------------------------------------------------------------------------
+
+const OUTPUT_DIR = '/tmp/local-model-demo'
+
+const tasks: Array<{
+ title: string
+ description: string
+ assignee?: string
+ dependsOn?: string[]
+}> = [
+ {
+ title: 'Write: retry utility',
+ description: `Write a small but complete TypeScript utility to ${OUTPUT_DIR}/retry.ts.
+
+The module should export:
+1. A \`RetryOptions\` interface with: maxRetries (number), delayMs (number),
+ backoffFactor (optional number, default 2), shouldRetry (optional predicate
+ taking the error and returning boolean).
+2. An async \`retry(fn: () => Promise, options: RetryOptions): Promise\`
+ function that retries \`fn\` with exponential backoff.
+3. A convenience \`withRetry\` wrapper that returns a new function with retry
+ behaviour baked in.
+
+Include JSDoc comments. No external dependencies — use only Node built-ins.
+After writing the file, also create a small test script at ${OUTPUT_DIR}/retry-test.ts
+that exercises the happy path and a failure case, then run it with \`npx tsx\`.`,
+ assignee: 'coder',
+ },
+ {
+ title: 'Review: retry utility',
+ description: `Read the files at ${OUTPUT_DIR}/retry.ts and ${OUTPUT_DIR}/retry-test.ts.
+
+Produce a structured code review covering:
+- Summary (2-3 sentences describing the module)
+- Strengths (bullet list)
+- Issues (bullet list — be specific about what and why)
+- Verdict: SHIP or NEEDS WORK`,
+ assignee: 'reviewer',
+ dependsOn: ['Write: retry utility'],
+ },
+]
+
+// ---------------------------------------------------------------------------
+// Run
+// ---------------------------------------------------------------------------
+
+console.log('Local + Cloud model team')
+console.log(` coder → Claude (${coder.model}) via Anthropic API`)
+console.log(` reviewer → Ollama (${reviewer.model}) at ${reviewer.baseURL}`)
+console.log()
+console.log('Pipeline: coder writes code → local model reviews it')
+console.log('='.repeat(60))
+
+const result = await orchestrator.runTasks(team, tasks)
+
+// ---------------------------------------------------------------------------
+// Summary
+// ---------------------------------------------------------------------------
+
+console.log('\n' + '='.repeat(60))
+console.log('Pipeline complete.\n')
+console.log(`Overall success: ${result.success}`)
+console.log(`Tokens — input: ${result.totalTokenUsage.input_tokens}, output: ${result.totalTokenUsage.output_tokens}`)
+
+console.log('\nPer-agent summary:')
+for (const [name, r] of result.agentResults) {
+ const icon = r.success ? 'OK ' : 'FAIL'
+ const provider = name === 'coder' ? 'anthropic' : 'ollama (local)'
+ const tools = r.toolCalls.map(c => c.toolName).join(', ')
+ console.log(` [${icon}] ${name.padEnd(10)} (${provider.padEnd(16)}) tools: ${tools || '(none)'}`)
+}
+
+// Print the reviewer's output
+const review = result.agentResults.get('reviewer')
+if (review?.success) {
+ console.log('\nCode review (from local model):')
+ console.log('─'.repeat(60))
+ console.log(review.output)
+ console.log('─'.repeat(60))
+}
diff --git a/examples/07-fan-out-aggregate.ts b/examples/07-fan-out-aggregate.ts
new file mode 100644
index 0000000..43b2c32
--- /dev/null
+++ b/examples/07-fan-out-aggregate.ts
@@ -0,0 +1,209 @@
+/**
+ * Example 07 — Fan-Out / Aggregate (MapReduce) Pattern
+ *
+ * Demonstrates:
+ * - Fan-out: send the same question to N "analyst" agents in parallel
+ * - Aggregate: a "synthesizer" agent reads all analyst outputs and produces
+ * a balanced final report
+ * - AgentPool with runParallel() for concurrent fan-out
+ * - No tools needed — pure LLM reasoning to keep the focus on the pattern
+ *
+ * Run:
+ * npx tsx examples/07-fan-out-aggregate.ts
+ *
+ * Prerequisites:
+ * ANTHROPIC_API_KEY env var must be set.
+ */
+
+import { Agent, AgentPool, ToolRegistry, ToolExecutor, registerBuiltInTools } from '../src/index.js'
+import type { AgentConfig, AgentRunResult } from '../src/types.js'
+
+// ---------------------------------------------------------------------------
+// Analysis topic
+// ---------------------------------------------------------------------------
+
+const TOPIC = `Should a solo developer build a SaaS product that uses AI agents
+for automated customer support? Consider the current state of AI technology,
+market demand, competition, costs, and the unique constraints of being a solo
+founder with limited time (~6 hours/day of productive work).`
+
+// ---------------------------------------------------------------------------
+// Analyst agent configs — three perspectives on the same question
+// ---------------------------------------------------------------------------
+
+const optimistConfig: AgentConfig = {
+ name: 'optimist',
+ model: 'claude-sonnet-4-6',
+ systemPrompt: `You are an optimistic technology analyst who focuses on
+opportunities, upside potential, and emerging trends. You see possibilities
+where others see obstacles. Back your optimism with concrete reasoning —
+cite market trends, cost curves, and real capabilities. Keep your analysis
+to 200-300 words.`,
+ maxTurns: 1,
+ temperature: 0.4,
+}
+
+const skepticConfig: AgentConfig = {
+ name: 'skeptic',
+ model: 'claude-sonnet-4-6',
+ systemPrompt: `You are a skeptical technology analyst who focuses on risks,
+challenges, failure modes, and hidden costs. You stress-test assumptions and
+ask "what could go wrong?" Back your skepticism with concrete reasoning —
+cite failure rates, technical limitations, and market realities. Keep your
+analysis to 200-300 words.`,
+ maxTurns: 1,
+ temperature: 0.4,
+}
+
+const pragmatistConfig: AgentConfig = {
+ name: 'pragmatist',
+ model: 'claude-sonnet-4-6',
+ systemPrompt: `You are a pragmatic technology analyst who focuses on practical
+feasibility, execution complexity, and resource requirements. You care about
+what works today, not what might work someday. You think in terms of MVPs,
+timelines, and concrete tradeoffs. Keep your analysis to 200-300 words.`,
+ maxTurns: 1,
+ temperature: 0.4,
+}
+
+const synthesizerConfig: AgentConfig = {
+ name: 'synthesizer',
+ model: 'claude-sonnet-4-6',
+ systemPrompt: `You are a senior strategy advisor who synthesizes multiple
+perspectives into a balanced, actionable recommendation. You do not simply
+summarise — you weigh the arguments, identify where they agree and disagree,
+and produce a clear verdict with next steps. Structure your output as:
+
+1. Key agreements across perspectives
+2. Key disagreements and how you weigh them
+3. Verdict (go / no-go / conditional go)
+4. Recommended next steps (3-5 bullet points)
+
+Keep the final report to 300-400 words.`,
+ maxTurns: 1,
+ temperature: 0.3,
+}
+
+// ---------------------------------------------------------------------------
+// Build agents — no tools needed for pure reasoning
+// ---------------------------------------------------------------------------
+
+function buildAgent(config: AgentConfig): Agent {
+ const registry = new ToolRegistry()
+ registerBuiltInTools(registry) // not needed here, but safe if tools are added later
+ const executor = new ToolExecutor(registry)
+ return new Agent(config, registry, executor)
+}
+
+const optimist = buildAgent(optimistConfig)
+const skeptic = buildAgent(skepticConfig)
+const pragmatist = buildAgent(pragmatistConfig)
+const synthesizer = buildAgent(synthesizerConfig)
+
+// ---------------------------------------------------------------------------
+// Set up the pool
+// ---------------------------------------------------------------------------
+
+const pool = new AgentPool(3) // 3 analysts can run simultaneously
+pool.add(optimist)
+pool.add(skeptic)
+pool.add(pragmatist)
+pool.add(synthesizer)
+
+console.log('Fan-Out / Aggregate (MapReduce) Pattern')
+console.log('='.repeat(60))
+console.log(`\nTopic: ${TOPIC.replace(/\n/g, ' ').trim()}\n`)
+
+// ---------------------------------------------------------------------------
+// Step 1: Fan-out — run all 3 analysts in parallel
+// ---------------------------------------------------------------------------
+
+console.log('[Step 1] Fan-out: 3 analysts running in parallel...\n')
+
+const analystResults: Map = await pool.runParallel([
+ { agent: 'optimist', prompt: TOPIC },
+ { agent: 'skeptic', prompt: TOPIC },
+ { agent: 'pragmatist', prompt: TOPIC },
+])
+
+// Print each analyst's output (truncated)
+const analysts = ['optimist', 'skeptic', 'pragmatist'] as const
+for (const name of analysts) {
+ const result = analystResults.get(name)!
+ const status = result.success ? 'OK' : 'FAILED'
+ console.log(` ${name} [${status}] — ${result.tokenUsage.output_tokens} output tokens`)
+ console.log(` ${result.output.slice(0, 150).replace(/\n/g, ' ')}...`)
+ console.log()
+}
+
+// Check all analysts succeeded
+for (const name of analysts) {
+ if (!analystResults.get(name)!.success) {
+ console.error(`Analyst '${name}' failed: ${analystResults.get(name)!.output}`)
+ process.exit(1)
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Step 2: Aggregate — synthesizer reads all 3 analyses
+// ---------------------------------------------------------------------------
+
+console.log('[Step 2] Aggregate: synthesizer producing final report...\n')
+
+const synthesizerPrompt = `Three analysts have independently evaluated the same question.
+Read their analyses below and produce your synthesis report.
+
+--- OPTIMIST ---
+${analystResults.get('optimist')!.output}
+
+--- SKEPTIC ---
+${analystResults.get('skeptic')!.output}
+
+--- PRAGMATIST ---
+${analystResults.get('pragmatist')!.output}
+
+Now synthesize these three perspectives into a balanced recommendation.`
+
+const synthResult = await pool.run('synthesizer', synthesizerPrompt)
+
+if (!synthResult.success) {
+ console.error('Synthesizer failed:', synthResult.output)
+ process.exit(1)
+}
+
+// ---------------------------------------------------------------------------
+// Final output
+// ---------------------------------------------------------------------------
+
+console.log('='.repeat(60))
+console.log('SYNTHESIZED REPORT')
+console.log('='.repeat(60))
+console.log()
+console.log(synthResult.output)
+console.log()
+console.log('-'.repeat(60))
+
+// ---------------------------------------------------------------------------
+// Token usage comparison
+// ---------------------------------------------------------------------------
+
+console.log('\nToken Usage Summary:')
+console.log('-'.repeat(60))
+
+let totalInput = 0
+let totalOutput = 0
+
+for (const name of analysts) {
+ const r = analystResults.get(name)!
+ totalInput += r.tokenUsage.input_tokens
+ totalOutput += r.tokenUsage.output_tokens
+ console.log(` ${name.padEnd(12)} — input: ${r.tokenUsage.input_tokens}, output: ${r.tokenUsage.output_tokens}`)
+}
+
+totalInput += synthResult.tokenUsage.input_tokens
+totalOutput += synthResult.tokenUsage.output_tokens
+console.log(` ${'synthesizer'.padEnd(12)} — input: ${synthResult.tokenUsage.input_tokens}, output: ${synthResult.tokenUsage.output_tokens}`)
+console.log('-'.repeat(60))
+console.log(` ${'TOTAL'.padEnd(12)} — input: ${totalInput}, output: ${totalOutput}`)
+
+console.log('\nDone.')
diff --git a/examples/08-gemma4-local.ts b/examples/08-gemma4-local.ts
new file mode 100644
index 0000000..0d31853
--- /dev/null
+++ b/examples/08-gemma4-local.ts
@@ -0,0 +1,192 @@
+/**
+ * Example 08 — Gemma 4 Local (100% Local, Zero API Cost)
+ *
+ * Demonstrates both execution modes with a fully local Gemma 4 model via
+ * Ollama. No cloud API keys needed — everything runs on your machine.
+ *
+ * Part 1 — runTasks(): explicit task pipeline (researcher → summarizer)
+ * Part 2 — runTeam(): auto-orchestration where Gemma 4 acts as coordinator,
+ * decomposes the goal into tasks, and synthesises the final result
+ *
+ * This is the hardest test for a local model — runTeam() requires it to
+ * produce valid JSON for task decomposition AND do tool-calling for execution.
+ * Gemma 4 e2b (5.1B params) handles both reliably.
+ *
+ * Run:
+ * no_proxy=localhost npx tsx examples/08-gemma4-local.ts
+ *
+ * Prerequisites:
+ * 1. Ollama >= 0.20.0 installed and running: https://ollama.com
+ * 2. Pull the model: ollama pull gemma4:e2b
+ * (or gemma4:e4b for better quality on machines with more RAM)
+ * 3. No API keys needed!
+ *
+ * Note: The no_proxy=localhost prefix is needed if you have an HTTP proxy
+ * configured, since the OpenAI SDK would otherwise route Ollama requests
+ * through the proxy.
+ */
+
+import { OpenMultiAgent } from '../src/index.js'
+import type { AgentConfig, OrchestratorEvent, Task } from '../src/types.js'
+
+// ---------------------------------------------------------------------------
+// Configuration — change this to match your Ollama setup
+// ---------------------------------------------------------------------------
+
+// See available tags at https://ollama.com/library/gemma4
+const OLLAMA_MODEL = 'gemma4:e2b' // or 'gemma4:e4b', 'gemma4:26b'
+const OLLAMA_BASE_URL = 'http://localhost:11434/v1'
+const OUTPUT_DIR = '/tmp/gemma4-demo'
+
+// ---------------------------------------------------------------------------
+// Agents
+// ---------------------------------------------------------------------------
+
+const researcher: AgentConfig = {
+ name: 'researcher',
+ model: OLLAMA_MODEL,
+ provider: 'openai',
+ baseURL: OLLAMA_BASE_URL,
+ apiKey: 'ollama', // placeholder — Ollama ignores this, but the OpenAI SDK requires a non-empty value
+ systemPrompt: `You are a system researcher. Use bash to run non-destructive,
+read-only commands (uname -a, sw_vers, df -h, uptime, etc.) and report results.
+Use file_write to save reports when asked.`,
+ tools: ['bash', 'file_write'],
+ maxTurns: 8,
+}
+
+const summarizer: AgentConfig = {
+ name: 'summarizer',
+ model: OLLAMA_MODEL,
+ provider: 'openai',
+ baseURL: OLLAMA_BASE_URL,
+ apiKey: 'ollama',
+ systemPrompt: `You are a technical writer. Read files and produce concise,
+structured Markdown summaries. Use file_write to save reports when asked.`,
+ tools: ['file_read', 'file_write'],
+ maxTurns: 4,
+}
+
+// ---------------------------------------------------------------------------
+// Progress handler
+// ---------------------------------------------------------------------------
+
+function handleProgress(event: OrchestratorEvent): void {
+ const ts = new Date().toISOString().slice(11, 23)
+ switch (event.type) {
+ case 'task_start': {
+ const task = event.data as Task | undefined
+ console.log(`[${ts}] TASK START "${task?.title ?? event.task}" → ${task?.assignee ?? '?'}`)
+ break
+ }
+ case 'task_complete':
+ console.log(`[${ts}] TASK DONE "${event.task}"`)
+ break
+ case 'agent_start':
+ console.log(`[${ts}] AGENT START ${event.agent}`)
+ break
+ case 'agent_complete':
+ console.log(`[${ts}] AGENT DONE ${event.agent}`)
+ break
+ case 'error':
+ console.error(`[${ts}] ERROR ${event.agent ?? ''} task=${event.task ?? '?'}`)
+ break
+ }
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Part 1: runTasks() — Explicit task pipeline
+// ═══════════════════════════════════════════════════════════════════════════
+
+console.log('Part 1: runTasks() — Explicit Pipeline')
+console.log('='.repeat(60))
+console.log(` model → ${OLLAMA_MODEL} via Ollama`)
+console.log(` pipeline → researcher gathers info → summarizer writes summary`)
+console.log()
+
+const orchestrator1 = new OpenMultiAgent({
+ defaultModel: OLLAMA_MODEL,
+ maxConcurrency: 1, // local model serves one request at a time
+ onProgress: handleProgress,
+})
+
+const team1 = orchestrator1.createTeam('explicit', {
+ name: 'explicit',
+ agents: [researcher, summarizer],
+ sharedMemory: true,
+})
+
+const tasks = [
+ {
+ title: 'Gather system information',
+ description: `Use bash to run system info commands (uname -a, sw_vers, sysctl, df -h, uptime).
+Then write a structured Markdown report to ${OUTPUT_DIR}/system-report.md with sections:
+OS, Hardware, Disk, and Uptime.`,
+ assignee: 'researcher',
+ },
+ {
+ title: 'Summarize the report',
+ description: `Read the file at ${OUTPUT_DIR}/system-report.md.
+Produce a concise one-paragraph executive summary of the system information.`,
+ assignee: 'summarizer',
+ dependsOn: ['Gather system information'],
+ },
+]
+
+const start1 = Date.now()
+const result1 = await orchestrator1.runTasks(team1, tasks)
+
+console.log(`\nSuccess: ${result1.success} Time: ${((Date.now() - start1) / 1000).toFixed(1)}s`)
+console.log(`Tokens — input: ${result1.totalTokenUsage.input_tokens}, output: ${result1.totalTokenUsage.output_tokens}`)
+
+const summary = result1.agentResults.get('summarizer')
+if (summary?.success) {
+ console.log('\nSummary (from local Gemma 4):')
+ console.log('-'.repeat(60))
+ console.log(summary.output)
+ console.log('-'.repeat(60))
+}
+
+// ═══════════════════════════════════════════════════════════════════════════
+// Part 2: runTeam() — Auto-orchestration (Gemma 4 as coordinator)
+// ═══════════════════════════════════════════════════════════════════════════
+
+console.log('\n\nPart 2: runTeam() — Auto-Orchestration')
+console.log('='.repeat(60))
+console.log(` coordinator → auto-created by runTeam(), also Gemma 4`)
+console.log(` goal → given in natural language, framework plans everything`)
+console.log()
+
+const orchestrator2 = new OpenMultiAgent({
+ defaultModel: OLLAMA_MODEL,
+ defaultProvider: 'openai',
+ defaultBaseURL: OLLAMA_BASE_URL,
+ defaultApiKey: 'ollama',
+ maxConcurrency: 1,
+ onProgress: handleProgress,
+})
+
+const team2 = orchestrator2.createTeam('auto', {
+ name: 'auto',
+ agents: [researcher, summarizer],
+ sharedMemory: true,
+})
+
+const goal = `Check this machine's Node.js version, npm version, and OS info,
+then write a short Markdown summary report to /tmp/gemma4-auto/report.md`
+
+const start2 = Date.now()
+const result2 = await orchestrator2.runTeam(team2, goal)
+
+console.log(`\nSuccess: ${result2.success} Time: ${((Date.now() - start2) / 1000).toFixed(1)}s`)
+console.log(`Tokens — input: ${result2.totalTokenUsage.input_tokens}, output: ${result2.totalTokenUsage.output_tokens}`)
+
+const coordResult = result2.agentResults.get('coordinator')
+if (coordResult?.success) {
+ console.log('\nFinal synthesis (from local Gemma 4 coordinator):')
+ console.log('-'.repeat(60))
+ console.log(coordResult.output)
+ console.log('-'.repeat(60))
+}
+
+console.log('\nAll processing done locally. $0 API cost.')
diff --git a/examples/09-structured-output.ts b/examples/09-structured-output.ts
new file mode 100644
index 0000000..2ffc29e
--- /dev/null
+++ b/examples/09-structured-output.ts
@@ -0,0 +1,73 @@
+/**
+ * Example 09 — Structured Output
+ *
+ * Demonstrates `outputSchema` on AgentConfig. The agent's response is
+ * automatically parsed as JSON and validated against a Zod schema.
+ * On validation failure, the framework retries once with error feedback.
+ *
+ * The validated result is available via `result.structured`.
+ *
+ * Run:
+ * npx tsx examples/09-structured-output.ts
+ *
+ * Prerequisites:
+ * ANTHROPIC_API_KEY env var must be set.
+ */
+
+import { z } from 'zod'
+import { OpenMultiAgent } from '../src/index.js'
+import type { AgentConfig } from '../src/types.js'
+
+// ---------------------------------------------------------------------------
+// Define a Zod schema for the expected output
+// ---------------------------------------------------------------------------
+
+const ReviewAnalysis = z.object({
+ summary: z.string().describe('One-sentence summary of the review'),
+ sentiment: z.enum(['positive', 'negative', 'neutral']),
+ confidence: z.number().min(0).max(1).describe('How confident the analysis is'),
+ keyTopics: z.array(z.string()).describe('Main topics mentioned in the review'),
+})
+
+type ReviewAnalysis = z.infer
+
+// ---------------------------------------------------------------------------
+// Agent with outputSchema
+// ---------------------------------------------------------------------------
+
+const analyst: AgentConfig = {
+ name: 'analyst',
+ model: 'claude-sonnet-4-6',
+ systemPrompt: 'You are a product review analyst. Analyze the given review and extract structured insights.',
+ outputSchema: ReviewAnalysis,
+}
+
+// ---------------------------------------------------------------------------
+// Run
+// ---------------------------------------------------------------------------
+
+const orchestrator = new OpenMultiAgent({ defaultModel: 'claude-sonnet-4-6' })
+
+const reviews = [
+ 'This keyboard is amazing! The mechanical switches feel incredible and the RGB lighting is stunning. Build quality is top-notch. Only downside is the price.',
+ 'Terrible experience. The product arrived broken, customer support was unhelpful, and the return process took 3 weeks.',
+ 'It works fine. Nothing special, nothing bad. Does what it says on the box.',
+]
+
+console.log('Analyzing product reviews with structured output...\n')
+
+for (const review of reviews) {
+ const result = await orchestrator.runAgent(analyst, `Analyze this review: "${review}"`)
+
+ if (result.structured) {
+ const data = result.structured as ReviewAnalysis
+ console.log(`Sentiment: ${data.sentiment} (confidence: ${data.confidence})`)
+ console.log(`Summary: ${data.summary}`)
+ console.log(`Topics: ${data.keyTopics.join(', ')}`)
+ } else {
+ console.log(`Validation failed. Raw output: ${result.output.slice(0, 100)}`)
+ }
+
+ console.log(`Tokens: ${result.tokenUsage.input_tokens} in / ${result.tokenUsage.output_tokens} out`)
+ console.log('---')
+}
diff --git a/examples/10-task-retry.ts b/examples/10-task-retry.ts
new file mode 100644
index 0000000..5f53e5e
--- /dev/null
+++ b/examples/10-task-retry.ts
@@ -0,0 +1,132 @@
+/**
+ * Example 10 — Task Retry with Exponential Backoff
+ *
+ * Demonstrates `maxRetries`, `retryDelayMs`, and `retryBackoff` on task config.
+ * When a task fails, the framework automatically retries with exponential
+ * backoff. The `onProgress` callback receives `task_retry` events so you can
+ * log retry attempts in real time.
+ *
+ * Scenario: a two-step pipeline where the first task (data fetch) is configured
+ * to retry on failure, and the second task (analysis) depends on it.
+ *
+ * Run:
+ * npx tsx examples/10-task-retry.ts
+ *
+ * Prerequisites:
+ * ANTHROPIC_API_KEY env var must be set.
+ */
+
+import { OpenMultiAgent } from '../src/index.js'
+import type { AgentConfig, OrchestratorEvent } from '../src/types.js'
+
+// ---------------------------------------------------------------------------
+// Agents
+// ---------------------------------------------------------------------------
+
+const fetcher: AgentConfig = {
+ name: 'fetcher',
+ model: 'claude-sonnet-4-6',
+ systemPrompt: `You are a data-fetching agent. When given a topic, produce a short
+JSON summary with 3-5 key facts. Output ONLY valid JSON, no markdown fences.
+Example: {"topic":"...", "facts":["fact1","fact2","fact3"]}`,
+ maxTurns: 2,
+}
+
+const analyst: AgentConfig = {
+ name: 'analyst',
+ model: 'claude-sonnet-4-6',
+ systemPrompt: `You are a data analyst. Read the fetched data from shared memory
+and produce a brief analysis (3-4 sentences) highlighting trends or insights.`,
+ maxTurns: 2,
+}
+
+// ---------------------------------------------------------------------------
+// Progress handler — watch for task_retry events
+// ---------------------------------------------------------------------------
+
+function handleProgress(event: OrchestratorEvent): void {
+ const ts = new Date().toISOString().slice(11, 23)
+
+ switch (event.type) {
+ case 'task_start':
+ console.log(`[${ts}] TASK START "${event.task}" (agent: ${event.agent})`)
+ break
+ case 'task_complete':
+ console.log(`[${ts}] TASK DONE "${event.task}"`)
+ break
+ case 'task_retry': {
+ const d = event.data as { attempt: number; maxAttempts: number; error: string; nextDelayMs: number }
+ console.log(`[${ts}] TASK RETRY "${event.task}" — attempt ${d.attempt}/${d.maxAttempts}, next in ${d.nextDelayMs}ms`)
+ console.log(` error: ${d.error.slice(0, 120)}`)
+ break
+ }
+ case 'error':
+ console.log(`[${ts}] ERROR "${event.task}" agent=${event.agent}`)
+ break
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Orchestrator + team
+// ---------------------------------------------------------------------------
+
+const orchestrator = new OpenMultiAgent({
+ defaultModel: 'claude-sonnet-4-6',
+ onProgress: handleProgress,
+})
+
+const team = orchestrator.createTeam('retry-demo', {
+ name: 'retry-demo',
+ agents: [fetcher, analyst],
+ sharedMemory: true,
+})
+
+// ---------------------------------------------------------------------------
+// Tasks — fetcher has retry config, analyst depends on it
+// ---------------------------------------------------------------------------
+
+const tasks = [
+ {
+ title: 'Fetch data',
+ description: 'Fetch key facts about the adoption of TypeScript in open-source projects as of 2024. Output a JSON object with a "topic" and "facts" array.',
+ assignee: 'fetcher',
+ // Retry config: up to 2 retries, 500ms base delay, 2x backoff (500ms, 1000ms)
+ maxRetries: 2,
+ retryDelayMs: 500,
+ retryBackoff: 2,
+ },
+ {
+ title: 'Analyze data',
+ description: 'Read the fetched data from shared memory and produce a 3-4 sentence analysis of TypeScript adoption trends.',
+ assignee: 'analyst',
+ dependsOn: ['Fetch data'],
+ // No retry — if analysis fails, just report the error
+ },
+]
+
+// ---------------------------------------------------------------------------
+// Run
+// ---------------------------------------------------------------------------
+
+console.log('Task Retry Example')
+console.log('='.repeat(60))
+console.log('Pipeline: fetch (with retry) → analyze')
+console.log(`Retry config: maxRetries=2, delay=500ms, backoff=2x`)
+console.log('='.repeat(60))
+console.log()
+
+const result = await orchestrator.runTasks(team, tasks)
+
+// ---------------------------------------------------------------------------
+// Summary
+// ---------------------------------------------------------------------------
+
+console.log('\n' + '='.repeat(60))
+console.log(`Overall success: ${result.success}`)
+console.log(`Tokens — input: ${result.totalTokenUsage.input_tokens}, output: ${result.totalTokenUsage.output_tokens}`)
+
+for (const [name, r] of result.agentResults) {
+ const icon = r.success ? 'OK ' : 'FAIL'
+ console.log(` [${icon}] ${name}`)
+ console.log(` ${r.output.slice(0, 200)}`)
+}
diff --git a/examples/11-trace-observability.ts b/examples/11-trace-observability.ts
new file mode 100644
index 0000000..20b463e
--- /dev/null
+++ b/examples/11-trace-observability.ts
@@ -0,0 +1,133 @@
+/**
+ * Example 11 — Trace Observability
+ *
+ * Demonstrates the `onTrace` callback for lightweight observability. Every LLM
+ * call, tool execution, task lifecycle, and agent run emits a structured trace
+ * event with timing data and token usage — giving you full visibility into
+ * what's happening inside a multi-agent run.
+ *
+ * Trace events share a `runId` for correlation, so you can reconstruct the
+ * full execution timeline. Pipe them into your own logging, OpenTelemetry, or
+ * dashboard.
+ *
+ * Run:
+ * npx tsx examples/11-trace-observability.ts
+ *
+ * Prerequisites:
+ * ANTHROPIC_API_KEY env var must be set.
+ */
+
+import { OpenMultiAgent } from '../src/index.js'
+import type { AgentConfig, TraceEvent } from '../src/types.js'
+
+// ---------------------------------------------------------------------------
+// Agents
+// ---------------------------------------------------------------------------
+
+const researcher: AgentConfig = {
+ name: 'researcher',
+ model: 'claude-sonnet-4-6',
+ systemPrompt: 'You are a research assistant. Provide concise, factual answers.',
+ maxTurns: 2,
+}
+
+const writer: AgentConfig = {
+ name: 'writer',
+ model: 'claude-sonnet-4-6',
+ systemPrompt: 'You are a technical writer. Summarize research into clear prose.',
+ maxTurns: 2,
+}
+
+// ---------------------------------------------------------------------------
+// Trace handler — log every span with timing
+// ---------------------------------------------------------------------------
+
+function handleTrace(event: TraceEvent): void {
+ const dur = `${event.durationMs}ms`.padStart(7)
+
+ switch (event.type) {
+ case 'llm_call':
+ console.log(
+ ` [LLM] ${dur} agent=${event.agent} model=${event.model} turn=${event.turn}` +
+ ` tokens=${event.tokens.input_tokens}in/${event.tokens.output_tokens}out`,
+ )
+ break
+ case 'tool_call':
+ console.log(
+ ` [TOOL] ${dur} agent=${event.agent} tool=${event.tool}` +
+ ` error=${event.isError}`,
+ )
+ break
+ case 'task':
+ console.log(
+ ` [TASK] ${dur} task="${event.taskTitle}" agent=${event.agent}` +
+ ` success=${event.success} retries=${event.retries}`,
+ )
+ break
+ case 'agent':
+ console.log(
+ ` [AGENT] ${dur} agent=${event.agent} turns=${event.turns}` +
+ ` tools=${event.toolCalls} tokens=${event.tokens.input_tokens}in/${event.tokens.output_tokens}out`,
+ )
+ break
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Orchestrator + team
+// ---------------------------------------------------------------------------
+
+const orchestrator = new OpenMultiAgent({
+ defaultModel: 'claude-sonnet-4-6',
+ onTrace: handleTrace,
+})
+
+const team = orchestrator.createTeam('trace-demo', {
+ name: 'trace-demo',
+ agents: [researcher, writer],
+ sharedMemory: true,
+})
+
+// ---------------------------------------------------------------------------
+// Tasks — researcher first, then writer summarizes
+// ---------------------------------------------------------------------------
+
+const tasks = [
+ {
+ title: 'Research topic',
+ description: 'List 5 key benefits of TypeScript for large codebases. Be concise.',
+ assignee: 'researcher',
+ },
+ {
+ title: 'Write summary',
+ description: 'Read the research from shared memory and write a 3-sentence summary.',
+ assignee: 'writer',
+ dependsOn: ['Research topic'],
+ },
+]
+
+// ---------------------------------------------------------------------------
+// Run
+// ---------------------------------------------------------------------------
+
+console.log('Trace Observability Example')
+console.log('='.repeat(60))
+console.log('Pipeline: research → write (with full trace output)')
+console.log('='.repeat(60))
+console.log()
+
+const result = await orchestrator.runTasks(team, tasks)
+
+// ---------------------------------------------------------------------------
+// Summary
+// ---------------------------------------------------------------------------
+
+console.log('\n' + '='.repeat(60))
+console.log(`Overall success: ${result.success}`)
+console.log(`Tokens — input: ${result.totalTokenUsage.input_tokens}, output: ${result.totalTokenUsage.output_tokens}`)
+
+for (const [name, r] of result.agentResults) {
+ const icon = r.success ? 'OK ' : 'FAIL'
+ console.log(` [${icon}] ${name}`)
+ console.log(` ${r.output.slice(0, 200)}`)
+}
diff --git a/package-lock.json b/package-lock.json
index b74dcd0..b48f976 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
},
"devDependencies": {
"@types/node": "^22.0.0",
+ "tsx": "^4.21.0",
"typescript": "^5.6.0",
"vitest": "^2.1.0"
},
@@ -321,6 +322,23 @@
"node": ">=12"
}
},
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
@@ -338,6 +356,23 @@
"node": ">=12"
}
},
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
@@ -355,6 +390,23 @@
"node": ">=12"
}
},
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
+ "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
@@ -1288,6 +1340,19 @@
"node": ">= 0.4"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.13.7",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
+ "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
@@ -1564,6 +1629,16 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/rollup": {
"version": "4.60.1",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.1.tgz",
@@ -1690,6 +1765,459 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
+ "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
+ "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
+ "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/android-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
+ "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
+ "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
+ "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
+ "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
+ "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
+ "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
+ "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
+ "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
+ "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
+ "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
+ "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
+ "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/linux-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
+ "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
+ "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
+ "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
+ "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/@esbuild/win32-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
+ "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tsx/node_modules/esbuild": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
+ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.7",
+ "@esbuild/android-arm": "0.27.7",
+ "@esbuild/android-arm64": "0.27.7",
+ "@esbuild/android-x64": "0.27.7",
+ "@esbuild/darwin-arm64": "0.27.7",
+ "@esbuild/darwin-x64": "0.27.7",
+ "@esbuild/freebsd-arm64": "0.27.7",
+ "@esbuild/freebsd-x64": "0.27.7",
+ "@esbuild/linux-arm": "0.27.7",
+ "@esbuild/linux-arm64": "0.27.7",
+ "@esbuild/linux-ia32": "0.27.7",
+ "@esbuild/linux-loong64": "0.27.7",
+ "@esbuild/linux-mips64el": "0.27.7",
+ "@esbuild/linux-ppc64": "0.27.7",
+ "@esbuild/linux-riscv64": "0.27.7",
+ "@esbuild/linux-s390x": "0.27.7",
+ "@esbuild/linux-x64": "0.27.7",
+ "@esbuild/netbsd-arm64": "0.27.7",
+ "@esbuild/netbsd-x64": "0.27.7",
+ "@esbuild/openbsd-arm64": "0.27.7",
+ "@esbuild/openbsd-x64": "0.27.7",
+ "@esbuild/openharmony-arm64": "0.27.7",
+ "@esbuild/sunos-x64": "0.27.7",
+ "@esbuild/win32-arm64": "0.27.7",
+ "@esbuild/win32-ia32": "0.27.7",
+ "@esbuild/win32-x64": "0.27.7"
+ }
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
diff --git a/package.json b/package.json
index c9910a8..d25f6b2 100644
--- a/package.json
+++ b/package.json
@@ -1 +1,50 @@
-{"name":"@jackchen_me/open-multi-agent","version":"0.1.0","description":"Production-grade multi-agent orchestration framework. Model-agnostic, supports team collaboration, task scheduling, and inter-agent communication.","type":"module","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"tsc","dev":"tsc --watch","test":"vitest run","test:watch":"vitest","lint":"tsc --noEmit","prepublishOnly":"npm run build"},"keywords":["ai","agent","multi-agent","orchestration","llm","claude","openai","ollama","mcp","tool-use","agent-framework"],"author":"","license":"MIT","engines":{"node":">=18.0.0"},"dependencies":{"@anthropic-ai/sdk":"^0.52.0","openai":"^4.73.0","zod":"^3.23.0"},"devDependencies":{"typescript":"^5.6.0","vitest":"^2.1.0","@types/node":"^22.0.0"}}
+{
+ "name":"@jackchen_me/open-multi-agent",
+ "version":"0.1.0",
+ "description":"Production-grade multi-agent orchestration framework. Model-agnostic, supports team collaboration, task scheduling, and inter-agent communication.",
+ "type":"module",
+ "main":"dist/index.js",
+ "types":"dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types":"./dist/index.d.ts",
+ "import":"./dist/index.js"
+ }
+ },
+ "scripts": {
+ "build":"tsc",
+ "dev":"tsc --watch",
+ "test":"vitest run",
+ "test:watch":"vitest",
+ "lint":"tsc --noEmit",
+ "prepublishOnly":"npm run build"
+ },
+ "keywords": [
+ "ai",
+ "agent",
+ "multi-agent",
+ "orchestration",
+ "llm",
+ "claude",
+ "openai",
+ "ollama",
+ "mcp",
+ "tool-use",
+ "agent-framework"
+ ],
+ "author":"",
+ "license":"MIT",
+ "engines":{
+ "node":">=18.0.0"
+ },
+ "dependencies": {
+ "@anthropic-ai/sdk":"^0.52.0",
+ "openai":"^4.73.0",
+ "zod":"^3.23.0"
+ },
+ "devDependencies": {
+ "typescript":"^5.6.0",
+ "vitest":"^2.1.0",
+ "@types/node":"^22.0.0"
+ }
+}
diff --git a/src/agent/agent.ts b/src/agent/agent.ts
index 1dc530d..58a1df3 100644
--- a/src/agent/agent.ts
+++ b/src/agent/agent.ts
@@ -32,10 +32,16 @@ import type {
TokenUsage,
ToolUseContext,
} from '../types.js'
+import { emitTrace, generateRunId } from '../utils/trace.js'
import type { ToolDefinition as FrameworkToolDefinition, ToolRegistry } from '../tool/framework.js'
import type { ToolExecutor } from '../tool/executor.js'
import { createAdapter } from '../llm/adapter.js'
-import { AgentRunner, type RunnerOptions, type RunOptions } from './runner.js'
+import { AgentRunner, type RunnerOptions, type RunOptions, type RunResult } from './runner.js'
+import {
+ buildStructuredOutputInstruction,
+ extractJSON,
+ validateOutput,
+} from './structured-output.js'
// ---------------------------------------------------------------------------
// Internal helpers
@@ -109,11 +115,20 @@ export class Agent {
}
const provider = this.config.provider ?? 'anthropic'
- const adapter = await createAdapter(provider)
+ const adapter = await createAdapter(provider, this.config.apiKey, this.config.baseURL)
+
+ // Append structured-output instructions when an outputSchema is configured.
+ let effectiveSystemPrompt = this.config.systemPrompt
+ if (this.config.outputSchema) {
+ const instruction = buildStructuredOutputInstruction(this.config.outputSchema)
+ effectiveSystemPrompt = effectiveSystemPrompt
+ ? effectiveSystemPrompt + '\n' + instruction
+ : instruction
+ }
const runnerOptions: RunnerOptions = {
model: this.config.model,
- systemPrompt: this.config.systemPrompt,
+ systemPrompt: effectiveSystemPrompt,
maxTurns: this.config.maxTurns,
maxTokens: this.config.maxTokens,
temperature: this.config.temperature,
@@ -144,12 +159,12 @@ export class Agent {
*
* Use this for one-shot queries where past context is irrelevant.
*/
- async run(prompt: string): Promise {
+ async run(prompt: string, runOptions?: Partial): Promise {
const messages: LLMMessage[] = [
{ role: 'user', content: [{ type: 'text', text: prompt }] },
]
- return this.executeRun(messages)
+ return this.executeRun(messages, runOptions)
}
/**
@@ -160,6 +175,7 @@ export class Agent {
*
* Use this for multi-turn interactions.
*/
+ // TODO(#18): accept optional RunOptions to forward trace context
async prompt(message: string): Promise {
const userMessage: LLMMessage = {
role: 'user',
@@ -183,6 +199,7 @@ export class Agent {
*
* Like {@link run}, this does not use or update the persistent history.
*/
+ // TODO(#18): accept optional RunOptions to forward trace context
async *stream(prompt: string): AsyncGenerator {
const messages: LLMMessage[] = [
{ role: 'user', content: [{ type: 'text', text: prompt }] },
@@ -252,33 +269,165 @@ export class Agent {
* Shared execution path used by both `run` and `prompt`.
* Handles state transitions and error wrapping.
*/
- private async executeRun(messages: LLMMessage[]): Promise {
+ private async executeRun(
+ messages: LLMMessage[],
+ callerOptions?: Partial,
+ ): Promise {
this.transitionTo('running')
+ const agentStartMs = Date.now()
+
try {
const runner = await this.getRunner()
+ const internalOnMessage = (msg: LLMMessage) => {
+ this.state.messages.push(msg)
+ callerOptions?.onMessage?.(msg)
+ }
+ // Auto-generate runId when onTrace is provided but runId is missing
+ const needsRunId = callerOptions?.onTrace && !callerOptions.runId
const runOptions: RunOptions = {
- onMessage: msg => {
- this.state.messages.push(msg)
- },
+ ...callerOptions,
+ onMessage: internalOnMessage,
+ ...(needsRunId ? { runId: generateRunId() } : undefined),
}
const result = await runner.run(messages, runOptions)
-
this.state.tokenUsage = addUsage(this.state.tokenUsage, result.tokenUsage)
- this.transitionTo('completed')
- return this.toAgentRunResult(result, true)
+ // --- Structured output validation ---
+ if (this.config.outputSchema) {
+ const validated = await this.validateStructuredOutput(
+ messages,
+ result,
+ runner,
+ runOptions,
+ )
+ this.emitAgentTrace(callerOptions, agentStartMs, validated)
+ return validated
+ }
+
+ this.transitionTo('completed')
+ const agentResult = this.toAgentRunResult(result, true)
+ this.emitAgentTrace(callerOptions, agentStartMs, agentResult)
+ return agentResult
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err))
this.transitionToError(error)
- return {
+ const errorResult: AgentRunResult = {
success: false,
output: error.message,
messages: [],
tokenUsage: ZERO_USAGE,
toolCalls: [],
+ structured: undefined,
+ }
+ this.emitAgentTrace(callerOptions, agentStartMs, errorResult)
+ return errorResult
+ }
+ }
+
+ /** Emit an `agent` trace event if `onTrace` is provided. */
+ private emitAgentTrace(
+ options: Partial | undefined,
+ startMs: number,
+ result: AgentRunResult,
+ ): void {
+ if (!options?.onTrace) return
+ const endMs = Date.now()
+ emitTrace(options.onTrace, {
+ type: 'agent',
+ runId: options.runId ?? '',
+ taskId: options.taskId,
+ agent: options.traceAgent ?? this.name,
+ turns: result.messages.filter(m => m.role === 'assistant').length,
+ tokens: result.tokenUsage,
+ toolCalls: result.toolCalls.length,
+ startMs,
+ endMs,
+ durationMs: endMs - startMs,
+ })
+ }
+
+ /**
+ * Validate agent output against the configured `outputSchema`.
+ * On first validation failure, retry once with error feedback.
+ */
+ private async validateStructuredOutput(
+ originalMessages: LLMMessage[],
+ result: RunResult,
+ runner: AgentRunner,
+ runOptions: RunOptions,
+ ): Promise {
+ const schema = this.config.outputSchema!
+
+ // First attempt
+ let firstAttemptError: unknown
+ try {
+ const parsed = extractJSON(result.output)
+ const validated = validateOutput(schema, parsed)
+ this.transitionTo('completed')
+ return this.toAgentRunResult(result, true, validated)
+ } catch (e) {
+ firstAttemptError = e
+ }
+
+ // Retry: send full context + error feedback
+ const errorMsg = firstAttemptError instanceof Error
+ ? firstAttemptError.message
+ : String(firstAttemptError)
+
+ const errorFeedbackMessage: LLMMessage = {
+ role: 'user' as const,
+ content: [{
+ type: 'text' as const,
+ text: [
+ 'Your previous response did not produce valid JSON matching the required schema.',
+ '',
+ `Error: ${errorMsg}`,
+ '',
+ 'Please try again. Respond with ONLY valid JSON, no other text.',
+ ].join('\n'),
+ }],
+ }
+
+ const retryMessages: LLMMessage[] = [
+ ...originalMessages,
+ ...result.messages,
+ errorFeedbackMessage,
+ ]
+
+ const retryResult = await runner.run(retryMessages, runOptions)
+ this.state.tokenUsage = addUsage(this.state.tokenUsage, retryResult.tokenUsage)
+
+ const mergedTokenUsage = addUsage(result.tokenUsage, retryResult.tokenUsage)
+ // Include the error feedback turn to maintain alternating user/assistant roles,
+ // which is required by Anthropic's API for subsequent prompt() calls.
+ const mergedMessages = [...result.messages, errorFeedbackMessage, ...retryResult.messages]
+ const mergedToolCalls = [...result.toolCalls, ...retryResult.toolCalls]
+
+ try {
+ const parsed = extractJSON(retryResult.output)
+ const validated = validateOutput(schema, parsed)
+ this.transitionTo('completed')
+ return {
+ success: true,
+ output: retryResult.output,
+ messages: mergedMessages,
+ tokenUsage: mergedTokenUsage,
+ toolCalls: mergedToolCalls,
+ structured: validated,
+ }
+ } catch {
+ // Retry also failed
+ this.transitionTo('completed')
+ return {
+ success: false,
+ output: retryResult.output,
+ messages: mergedMessages,
+ tokenUsage: mergedTokenUsage,
+ toolCalls: mergedToolCalls,
+ structured: undefined,
}
}
}
@@ -331,8 +480,9 @@ export class Agent {
// -------------------------------------------------------------------------
private toAgentRunResult(
- result: import('./runner.js').RunResult,
+ result: RunResult,
success: boolean,
+ structured?: unknown,
): AgentRunResult {
return {
success,
@@ -340,6 +490,7 @@ export class Agent {
messages: result.messages,
tokenUsage: result.tokenUsage,
toolCalls: result.toolCalls,
+ structured,
}
}
diff --git a/src/agent/pool.ts b/src/agent/pool.ts
index 915f361..aba0eb8 100644
--- a/src/agent/pool.ts
+++ b/src/agent/pool.ts
@@ -21,6 +21,7 @@
*/
import type { AgentRunResult } from '../types.js'
+import type { RunOptions } from './runner.js'
import type { Agent } from './agent.js'
import { Semaphore } from '../utils/semaphore.js'
@@ -123,12 +124,16 @@ export class AgentPool {
*
* @throws {Error} If the agent name is not found.
*/
- async run(agentName: string, prompt: string): Promise {
+ async run(
+ agentName: string,
+ prompt: string,
+ runOptions?: Partial,
+ ): Promise {
const agent = this.requireAgent(agentName)
await this.semaphore.acquire()
try {
- return await agent.run(prompt)
+ return await agent.run(prompt, runOptions)
} finally {
this.semaphore.release()
}
@@ -144,6 +149,7 @@ export class AgentPool {
*
* @param tasks - Array of `{ agent, prompt }` descriptors.
*/
+ // TODO(#18): accept RunOptions per task to forward trace context
async runParallel(
tasks: ReadonlyArray<{ readonly agent: string; readonly prompt: string }>,
): Promise