Conduct your AI orchestra
Warning: This project is in an experimental stage.
A TypeScript CLI tool that coordinates multiple AI agents (Claude, Codex, Gemini) using LangGraph state machines. It automatically breaks down complex tasks into step-by-step workflows, delegating subtasks to specialized AI agents based on their strengths.
Baton — the conductor's instrument for directing an orchestra.
This name was selected through a collaborative brainstorming process where three AI agents (Claude, Codex, and Gemini) evaluated candidate names together.
- Perfect metaphor — The baton is the conductor's instrument for directing an orchestra. This project conducts multiple AI "performers" (Claude, Codex, Gemini) through a coordinated workflow. Unlike "Maestro" which describes the person, "Baton" represents the tool.
- Distinctive — Not overused in the tech/orchestration space, unlike Maestro (AWS) or Relay (Meta GraphQL).
- Practical — Short (5 letters), easy to spell and pronounce globally, works as npm package, CLI command, and GitHub repo name.
- Memorable — Developers can immediately visualize a conductor's baton directing performers.
- Features
- Prerequisites
- Quick Start
- Usage
- Configuration
- Architecture
- Agent Selection Guide
- Troubleshooting
- Debugging
- Limitations
- API Reference
- Development
- Contributing
- License
- Dynamic Task Planning — A Manager agent (currently Claude) analyzes prompts and auto-generates state machine workflows
- Multi-Agent Orchestration — Coordinates
claude,codex, andgeminiCLI tools as worker nodes - State Machine Execution — Powered by LangGraph for robust, stateful workflows with parallel/conditional execution
- Per-Node Agent Instances — Each node gets its own dedicated agent instance that maintains session context within a single run
- Conditional Branching — Edges support JavaScript conditions for dynamic routing
- Two-Layer Retry Logic — Graph-level retries via
maxRetries(default: 0) plus internal agent-level retries (1 retry) for transient failures - Run State Logging — Each execution is saved to
.baton/<runId>/state.json
Required: Node.js 18+ (uses ESM modules and modern APIs)
This tool coordinates existing CLI agents—it does not include or replace them.
| Tool | Best For | Required | Installation | Verification |
|---|---|---|---|---|
| Claude CLI | Planning, reasoning, synthesis | Always | Anthropic Claude Code | claude --version |
| Codex CLI | Code generation, editing | If in config | See CLI Compatibility below | codex --version |
| Gemini CLI | General knowledge, verification | If in config | See CLI Compatibility below | gemini --version |
Important Notes:
- Claude CLI is always required as it powers the Manager agent
- Codex and Gemini are only required if listed in your
baton.config.yamlagentsarray - Each CLI tool must be authenticated with valid API credentials
- When no config file exists, the default includes all three agents — create a Claude-only config for first-time setup (see Quick Start)
CLI Compatibility: This project expects specific CLI interfaces. The
codexandgeminicommands are not bundled—you must provide CLI tools that implement these interfaces:
CLI Expected Command Format Response Format codexcodex exec --skip-git-repo-check /statusMust output session id: <uuid>codexcodex exec --skip-git-repo-check --json resume <session-id> "<prompt>"JSONL with item.completedevents containingagent_messageitemsgeminigemini -r <session-id> -o=json "<prompt>"JSON with session_idandresponsefieldsOptions for getting these CLIs:
- Use official CLI tools from OpenAI/Google if available in your organization
- Implement wrapper scripts that translate to your preferred AI CLI tools
- Modify
src/agents/codex.tsandsrc/agents/gemini.tsto match your CLI interfacesIf you only have Claude CLI, configure
agents: [claude]in your config to use Claude for all tasks.
Before starting: Ensure the prerequisites are installed and authenticated. Running without them will cause "command not found" errors.
# Clone and install
git clone https://github.com/coderhyme/baton.git
cd baton
npm install
# Create a minimal config (Claude-only to start)
# This is required to avoid errors from missing codex/gemini CLIs
cat > baton.config.yaml << 'EOF'
verbose: false
agents:
- claude
EOF
# Verify setup
claude --version
# Run your first task
npm run start -- "Write a Python function to calculate fibonacci numbers"The Manager will analyze your request, create an execution plan, and delegate to the appropriate agents based on the task and your configuration.
Execute a task directly from the command line:
npm run start -- "Write a Python function that fetches weather data from an API and saves it to CSV"Or read the prompt from a file:
npm run start -- -f prompt.txtThe Manager determines which agents to use based on your config and the task requirements.
See real-time output from the underlying agents:
npm run start:verbose -- "Refactor src/index.ts to improve error handling"CLI Arguments:
| Argument | Description |
|---|---|
-v, --verbose |
Show real-time agent output |
-f, --file <path> |
Read prompt from file |
[prompt] |
The task description for the Manager to plan and execute |
Note: You cannot use both -f and a prompt argument at the same time.
Available npm Scripts:
| Script | Description |
|---|---|
npm run start -- "<prompt>" |
Execute a task with default settings |
npm run start:verbose -- "<prompt>" |
Execute with verbose output |
npm run dev |
Development mode with file watching |
Example Session (Illustrative):
The following shows the general structure of output you'll see. Actual schema content and timing will vary based on your prompt:
Orchestrating prompt: "Create a Python script that scrapes Hacker News headlines"
Verbose mode: enabled
Initializing Manager agent...
Generating state machine schema...
Generated schema: {
"plan": "Multi-step web scraping implementation",
"nodes": [...],
"edges": [...],
...
}
Creating and initializing agent pool...
Run state saved to: .baton/<runId>
Building and executing graph...
============================================================
[NODE START] plan-impl (Plan Implementation)
Agent: claude
Attempt: 1/1
============================================================
------------------------------------------------------------
[NODE END] plan-impl - SUCCESS (2341ms)
------------------------------------------------------------
Output:
I recommend using requests library with BeautifulSoup...
------------------------------------------------------------
=== Final Answer ===
[Final node output appears here]
Error Handling: If a node fails after exhausting retries, you'll see:
------------------------------------------------------------
[NODE END] write-code - FAILED (15023ms)
------------------------------------------------------------
Error: API timeout after 30s
------------------------------------------------------------
Execution error: Node write-code failed after 2 attempts: API timeout after 30s
The error is captured in ctx.error for conditional routing to error-handling nodes.
Configuration is managed via baton.config.yaml in the project root.
Complete example (copy this to get started):
# baton.config.yaml
# Enable verbose output (shows real-time agent responses)
verbose: false
# Available agents for orchestration
# The Manager will only use agents listed here
# This also constrains the agentType values in generated schemas
agents:
- claude # Always required (powers the Manager)
- codex # Optional: for code generation tasks
- gemini # Optional: for general knowledge tasksMinimal example (Claude only):
verbose: false
agents:
- claudeConfiguration Precedence:
verbose: CLI flag (-v) overrides config file, which overrides default (false)agents: Config file only (no CLI flag available)
┌─────────────────────────────────────────────────────────────────┐
│ User Prompt │
└─────────────────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Manager Agent (Claude) │
│ • Analyzes the request │
│ • Generates StateMachineSchema (JSON) │
│ • Determines which agents handle which subtasks │
└─────────────────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Graph Builder │
│ • Converts schema to LangGraph │
│ • Sets up conditional routing │
│ • Creates node executors with retry logic │
└─────────────────────────────┬───────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Graph Executor │
│ • Invokes LangGraph with initial state │
│ • Multiple outgoing edges trigger parallel node execution │
│ • Returns final output (last entry in nodeOutputs) │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Claude Agent │ │ Codex Agent │ │ Gemini Agent │
│ (Reasoning) │ │ (Coding) │ │ (General) │
└───────────────┘ └───────────────┘ └───────────────┘
| Package | Purpose |
|---|---|
@langchain/langgraph |
State machine graph construction & execution |
@langchain/core |
LangChain foundation types |
zod |
Schema validation for state machine definitions |
zod-to-json-schema |
Converts Zod schemas to JSON Schema for Claude |
yaml |
Config file parsing |
node-pty |
Pseudo-terminal for Codex agent (required for session management) |
Tips:
- For code-heavy tasks, ensure
codexis in your agents config - For pure reasoning/planning tasks, Claude-only config works well
- The Manager's agent selection is non-deterministic—the same prompt may produce different execution plans on different runs
If you encounter errors related to node-pty, fix permissions on the prebuilt binaries:
chmod +x node_modules/**/node-pty/**/prebuilds/darwin-*/spawn-helperIf you see Error: spawn <agent> ENOENT, the CLI tool is not in your PATH:
# Verify Claude (always required)
claude --version
# Verify Codex/Gemini only if in your agents config
codex --version # Only if using codex
gemini --version # Only if using gemini
# Check PATH
which claudeTip: If you only have Claude CLI, use a Claude-only config:
agents:
- claudeIf you see "API key not found" or authentication errors:
- Claude: Run
claudeand follow the authentication prompts - Codex: Set up authentication per your Codex CLI documentation
- Gemini: Run
geminiand authenticate with your Google account
| Error Pattern | Cause | Solution |
|---|---|---|
Failed to extract session ID from codex /status output |
Codex /status command didn't return expected format |
Ensure Codex CLI outputs session id: <uuid> on status check |
No agent_message found in codex output |
Codex JSONL response missing agent_message items |
Verify Codex returns item.completed events with agent_message type |
Failed to parse Gemini JSON output: ... |
Gemini CLI returned invalid JSON | Verify Gemini CLI -o=json flag produces valid JSON with session_id and response fields |
Gemini CLI exited with code X |
Gemini CLI crashed or returned non-zero | Check Gemini authentication and CLI installation |
Claude CLI exited with code X |
Claude CLI crashed or auth issue | Check Claude authentication and API access |
Failed to spawn <agent> CLI: ... |
CLI binary not found or not executable | Verify CLI is installed and in PATH (which <agent>) |
If a node fails during execution, the error follows this format:
Node <nodeId> failed after <attempts> attempts: <error message>
When returned as final output, errors are prefixed:
Error during execution: Node <nodeId> failed after <attempts> attempts: <error message>
The error is stored in ctx.error and can be used for conditional routing to error-handling nodes.
Each execution saves state to .baton/<runId>/state.json before graph execution begins. This file captures the initial state:
{
"runId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2024-01-15T10:30:00.000Z",
"prompt": "Original user prompt",
"schema": {
"plan": "Generated execution plan",
"diagram": "ASCII diagram of workflow",
"nodes": [...],
"edges": [...],
"startNodeId": "plan-impl",
"endNodeId": "verify"
},
"agents": [
{ "nodeId": "plan-impl", "type": "claude", "sessionId": "session-abc123" },
{ "nodeId": "write-code", "type": "codex", "sessionId": "session-def456" },
{ "nodeId": "verify", "type": "gemini", "sessionId": "session-ghi789" }
]
}Note: This is a pre-execution snapshot. Node outputs (nodeOutputs) and execution metadata are not persisted to this file—they exist only in memory during execution.
Useful for:
- Understanding what schema the Manager generated
- Reviewing the execution plan before it ran
- Debugging agent initialization issues
- Correlating session IDs with agent logs
Use -v or npm run start:verbose to see real-time output from each agent as nodes execute:
============================================================
[NODE START] write-code (Write Script)
Agent: codex
Attempt: 1/1
============================================================
[... agent output streams here ...]
------------------------------------------------------------
[NODE END] write-code - SUCCESS (4123ms)
------------------------------------------------------------
This is invaluable for understanding agent behavior and debugging prompt issues.
Current limitations to be aware of:
- Non-deterministic planning — The same prompt may produce different execution plans on different runs due to LLM variability
- One-shot execution — Plans cannot be interactively refined after generation; the full workflow executes automatically
- No manual schema authoring — The Manager generates all schemas; custom schema input is not supported
- Agent availability — All agents in your config must be installed and authenticated before execution
- Parallel execution order — When nodes run in parallel, completion order (and thus final output selection) is non-deterministic
- No persistent sessions — Agent sessions only persist within a single run; subsequent runs start fresh
- Pre-execution state only — The
state.jsonfile captures initial state; node outputs are not persisted to disk - No streaming output — Graph execution uses
graph.invoke()which waits for completion rather than streaming results
Note: This reference is for understanding the generated schema structure. Manual schema authoring is not currently supported—the Manager generates all schemas automatically.
The Manager generates a workflow matching this structure (validated by Zod):
{
plan: string, // Execution plan description
diagram: string, // ASCII/text diagram of flow
nodes: [{
id: string, // Unique node identifier
name: string, // Human-readable name
description: string, // What this node does
agentType: "claude" | "codex" | "gemini", // Constrained by config
basePrompt: string, // The prompt for execution
maxRetries?: number, // Graph-level retry attempts (default: 0, meaning 1 total attempt)
onSuccess?: string, // JS expression for metadata on success
onError?: string // JS expression for metadata on error
}],
edges: [{
from: string,
to: string,
condition?: {
code: string, // JS expression returning boolean
description?: string
}
}],
startNodeId: string,
endNodeId: string,
initialMetadata?: Record<string, unknown>
}Note: The agentType field is constrained at runtime to only allow agents listed in your config.
There are two layers of retry logic:
-
Graph-level retries (
maxRetriesper node): Controlled by the schema. Default is 0, meaning 1 attempt. If a node fails, the graph can retry that node up tomaxRetriesadditional times. -
Agent-level retries (internal to
BaseCLIAgent): Each agent automatically retries once on transient failures (e.g., network issues). This is hardcoded to 1 retry (2 total attempts) and happens transparently within a single graph-level attempt.
Example: With maxRetries: 1 (2 graph attempts) and internal agent retry (2 attempts each), a node could attempt execution up to 4 times total before failing.
Each node receives its basePrompt plus a "Context from previous steps" section containing the nodeOutputs from all completed nodes as JSON. This allows downstream nodes to build upon prior results.
Example injected context:
Context from previous steps:
{
"plan-impl": "I recommend using requests library with BeautifulSoup...",
"write-code": "import requests\nfrom bs4 import BeautifulSoup..."
}
Note: The first node to execute receives {} as its context since no prior outputs exist.
Edge conditions are JavaScript expressions evaluated at runtime after the source node completes. The expression must return a boolean and has access to a ctx object:
// ctx provides:
{
nodeOutputs: Record<string, string>, // Outputs from completed nodes
metadata: Record<string, unknown>, // Accumulated metadata
error: string | null, // Current error state
currentNode: string | null // Current node ID (null at start)
}Example conditions:
// Route based on output content
ctx.nodeOutputs['write-code'] !== ''
// Route based on error state (success path)
ctx.error === null
// Route on error (error-handling path)
ctx.error !== null
// Route based on metadata
ctx.metadata.testsPass === trueNote: The code field must be an expression, not a statement block (it's wrapped internally as return (<code>)).
The onSuccess and onError hooks allow nodes to update shared metadata. These are JavaScript expressions with access to:
| Variable | Description | Available In |
|---|---|---|
$output |
The node's output string | onSuccess only |
$error |
Error message | onError only |
$elapsedMs |
Execution time in milliseconds | Both |
$retryCount |
Number of graph-level retry attempts used (0-indexed) | Both |
$metadata |
Current metadata object | Both |
$nodeId |
The node's ID | Both |
The hook must return an object to merge into metadata. Non-object returns are ignored.
// Simple: track the last completed node
({ lastNode: $nodeId })
// Track timing
({ [`${$nodeId}Duration`]: $elapsedMs })
// Track retry count
({ [`${$nodeId}Retries`]: $retryCount })
// Advanced: append to an array (requires initialMetadata setup)
({ completedNodes: [...($metadata.completedNodes || []), $nodeId] })Tip: Initialize arrays in initialMetadata before appending to them.
The final output is determined by Object.values(result.nodeOutputs).pop() — the last entry added to the nodeOutputs object by insertion order.
- In sequential execution, this is the last node to complete (typically
endNodeId). - In parallel execution, insertion order depends on completion timing, which may be non-deterministic.
If the final state contains an error after all retries are exhausted, the output is prefixed with Error during execution:.
Tip: For deterministic results, design your schema with a single final synthesis node that combines outputs from all prior nodes.
# Development mode with watch
npm run dev
# Type checking
npm run typecheck
# Linting
npm run lint
npm run lint:fix
# Build to dist/
npm run build
# Run compiled version (without tsx)
node dist/index.js "your prompt here"- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
ISC