An intelligent, modular AI coding agent with a server/client architecture.
Butterfly Agent is a pnpm monorepo providing a production-ready agent backend: a typed HTTP + SSE server that owns all agent logic, session state, and event broadcasting. Clients are pure UI — you can build your own in any language.
┌──────────────┐ HTTP + SSE ┌──────────────────────────────┐
│ Your client │ ◄──────────────────────► │ Butterfly Server │
│ (CLI, TUI, │ /api/event (SSE) │ @butterfly/server │
│ IDE, web) │ /api/sessions (CRUD) │ ├─ ServerApp (shared core) │
│ │ /api/sessions/:id/ │ ├─ EventBus (typed pub/sub) │
│ │ prompt (agent run) │ ├─ SessionManager │
│ │ /api/providers (LLMs) │ ├─ RunStateManager │
│ │ /api/permission (HITL)│ ├─ AgentLoop (SCE+COE) │
│ │ │ ├─ Model Router (tiers) │
│ │ ─ or over stdio ─ │ ├─ Tool Registry (14 tools) │
│ │ ACP (JSON-RPC) │ └─ MCP / LSP integrations │
└──────────────┘ └──────────────────────────────┘
- Backend owns everything: agent loop, session state, LLM routing, tools, permissions, MCP/LSP. The server is the single source of truth — mirroring OpenCode's architecture.
- Clients are thin: subscribe to
/api/event(SSE) and call typed endpoints. - No client in this repo (yet): build your own with
@butterfly/client(TS), the OpenAPI spec at/openapi.json, the ACP stdio server, or plain HTTP.
# 1. Install
pnpm install
# 2. Configure (copy .env.example → .env and set LLM_API_KEY + model)
cp .env.example .env
# 3. Start the HTTP server
pnpm --filter @butterfly/server-app dev
# 🦋 Butterfly Server running at http://127.0.0.1:3000
# 4. Health check
curl http://127.0.0.1:3000/healthTry it:
# List available providers + models (from the models.dev catalog)
curl http://127.0.0.1:3000/api/providers
# Create a session and run the agent
curl -X POST http://127.0.0.1:3000/api/sessions -H 'Content-Type: application/json' \
-d '{"mode":"build"}'
curl -X POST http://127.0.0.1:3000/api/sessions/<id>/prompt?wait=true \
-H 'Content-Type: application/json' -d '{"prompt":"explain this repo"}'pnpm typecheck # tsc across all packages
pnpm lint # biome check
pnpm test # vitest (224+ tests)
pnpm build # compile all packages
pnpm format # biome formatThe default suite uses mock LLMs (deterministic, free). To exercise the real provider path (streaming, retries, tool-call parsing) against a live API:
BUTTERFLY_TEST_LIVE=1 LLM_API_KEY=sk-... npx vitest run tests/live-llm.test.tsSkipped automatically when BUTTERFLY_TEST_LIVE is unset or no key is present.
import { createButterflyClient } from "@butterfly/client"
const client = createButterflyClient({ baseUrl: "http://localhost:3000" })
// Browse providers/models
const { models, providers } = await client.providers()
const pick = models.find((m) => m.id === "anthropic/claude-sonnet-4-5")
// Sessions
const session = await client.sessions.create({ mode: "build", selectedModel: pick?.id })
// Run the agent and wait for it to finish
const run = await client.promptAndWait(session.id, "Refactor this into modules")
// Or run async + stream events
const { subscribeEvents, subscribeToSession } = client
const handle = subscribeToSession(session.id, {
onEvent: (e) => console.log(e.kind, e.data),
})
await client.prompt(session.id, "Show me the architecture")
handle.close() // unsubscribes
// Human-in-the-loop permission flow
const { pending } = await client.permissions.list(session.id)
for (const req of pending) {
await client.permissions.reply(req.requestId, "yes")
}- Endpoints: see API Reference or
GET /openapi.json(auto-generated OpenAPI 3.0 spec). - Events:
GET /api/event(global) andGET /api/sessions/:id/stream(per-session) are SSE streams. Each event carries anid— resume withLast-Event-IDafter a reconnect (the server replays the last 500 events).
Every event has the shape { id, kind, sessionId, timestamp, data }. Kinds:
| Kind | When | data highlights |
|---|---|---|
session.created/updated/deleted/archived/forked/imported |
session lifecycle | session |
run.started/completed/aborted/error/recovered |
run lifecycle | run metadata |
stream.text_delta |
live streaming — emitted per token chunk during generation | { text } |
stream.reasoning |
live reasoning tokens | { text } |
stream.usage |
per-step token/cost totals | { promptTokens, completionTokens, totalTokens, costUsd? } |
tool.start/result/error |
tool execution | tool name, input, output/error |
file.changed |
write/patch/delete applied | { filePath, change } |
message.added |
persisted — emitted once at run end (or on edit) with the full message | { message } |
message.updated |
PATCH /messages/:id edited a message |
{ messageId, message } |
message.removed |
POST /retry truncated the tail (rewritten messages) |
{ count } |
todo.updated |
the agent maintains a todo list via todowrite (live progress panel) |
{ todos } |
permission.requested/resolved |
HITL permission flow | request + reply |
mcp.* |
MCP server lifecycle | server name, status |
Streaming guidance for client builders:
- Live text comes from
stream.text_delta— accumulate deltas into the assistant message currently being generated. - Final truth is
message.added, which fires once per new message after a run completes (or when you edit via the API). Build your message list from these, not from deltas. - Todo panels subscribe to
todo.updated. EachTodoItemcarries a stableid(preserved across updates by content match), so you can reconcile which item changed instead of diffing by text. Events fire only when the todo list actually changes — no spam for tool-only or resumed sessions. - Edits and retries surface as
message.updated/message.removedso clients can rewrite or prune rendered messages instead of full refetches.
Any ACP-compatible client (IDE, CLI, TUI) can drive Butterfly directly:
pnpm --filter @butterfly/acp-app dev # run the ACP stdio server
# or install as a bin:
# butterfly-acpWorks with tools like the official acp CLI:
acp run "pnpm --filter @butterfly/acp-app dev" -- "explain this repo"Auth: set BUTTERFLY_API_KEY and ACP clients provide apiKey/token/bearerToken
in authenticate.
- Providers: OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, OpenRouter,
Mistral, Cohere, Perplexity, Cerebras, Fireworks, Together AI, Azure, Bedrock,
GitHub Copilot, Cloudflare, Baseten, DeepInfra, Vercel — plus any
OpenAI-compatible endpoint via
providersin.butterfly/config.json. - Model catalog: fetched from models.dev (cached 5 min with disk fallback), with per-model cost, context limits, capabilities, and status.
- Tiered routing: trivial → standard → complex → escalate, with automatic
escalation on tool failures. Pick a fixed model per session via
selectedModel, or "auto" for tiered routing. - Streaming: all adapters stream text, reasoning, and tool-call deltas end-to-end to the SSE bus.
COE's context budget defaults to a model-aware value: when butterfly.coe.maxContextTokens
is unset, the server looks up the selected model's context window in the models.dev
catalog and uses 70% of it (min 1000, fallback 8000). So a 128k model isn't crushed
into an 8k budget, and a small 7B model with an 8k window still fits. Explicit config
always wins.
Two client-facing conveniences are built into the backend, ready for any UI:
Define custom commands in .butterfly/config.json under the commands key.
Each maps a /name to a prompt template; {args} is replaced by the rest of
the user's input after the command name:
{
"commands": {
"fix": "Fix the following issue in the codebase: {args}",
"test": "Write tests for: {args}",
"explain": "Explain this code in detail: {args}",
"deploy": "Deploy the project now."
}
}- A prompt of
/fix the login bugis rewritten toFix the following issue in the codebase: the login bugbefore the loop runs. - Unknown commands pass through unchanged.
- Clients can discover available commands via
GET /api/sessions/commands.
Reference files in any prompt with @path/to/file.ts. The server extracts the
references, reads the files (workspace-bound, 1MB cap), and injects their
content into the first user message as a REFERENCED FILES: block — the model
sees the code without an explicit read call:
"Refactor @src/utils.ts and @src/index.ts to share a helper"
- Files are resolved against the server
cwd; absolute paths work too. - Missing or oversized files are skipped with an inline
[SKIPPED: …]notice. - Multi-turn: the
@pathtext stays in the query so the model can stillreadthe file itself if it wants.
Each session accumulates an estimated costUsd alongside token counts,
computed from the model's catalog pricing (per-1M-token input/output).
Configured provider model overrides take precedence over catalog prices (useful
for proxies/self-hosted gateways). Visible in session.usage.costUsd and on
/api/sessions lists. Unknown pricing leaves the field unset rather than
misleading the user.
- No silent deletion: sessions are never auto-deleted by default. Enable
cleanup explicitly via
butterfly.backgroundJobs.staleSessionAgeMs(e.g. 7 days =604800000). - Run recovery: an
activeRunmarker is persisted when a run starts. After a server restart,/api/sessions/:id/statusreportsinterruptedfor sessions whose run was cut off (with the stale run metadata) instead of lying that they're still running. - Export/import: sessions export to portable JSON (
/api/sessions/:id/export) and import into a fresh session (/api/sessions/import) — shareable across machines, never overwrites existing data.
Butterfly is built for parallel use:
- Multiple sessions at once: run any number of sessions concurrently; the
RunStateManagertracks each independently (/api/sessions/:id/status). - One run per session: a new prompt on a busy session aborts the in-flight
run first (with a concurrency-safe
expectedAbortguard). - Parallel-safe tool execution: pure reads (
read,grep,glob,list,search,web_fetch) run concurrently within a single agent step; writes, bash, and interactive tools run serially for determinism. - Streaming to many clients: the event bus supports many SSE subscribers per process (max 200 listeners) with per-session filtering.
// Example: two sessions in parallel
const [s1, s2] = await Promise.all([client.sessions.create(), client.sessions.create()])
const [r1, r2] = await Promise.all([
client.promptAndWait(s1.id, "Task A"),
client.promptAndWait(s2.id, "Task B"),
])- API key auth (optional):
BUTTERFLY_API_KEY— timing-safe compare, configurable header,/health+/openapi.jsonstay public. - Workspace confinement: file tools reject paths outside
cwd(symlink-aware); bashworkdirvalidated against workspace roots. - SSRF protection in
web_fetch(private/loopback/metadata IPs blocked). - Command safety: dangerous shell patterns and injection metacharacters blocked; output truncated with file offload.
- Persistence safety: atomic tmp+rename writes, per-file locking with stale-lock detection, session-id sanitization (path-traversal-proof).
- Rate limiting: per-IP/API-key buckets with SSE/health exempt paths.
See .env.example for the annotated list. Key ones:
| Variable | Default | Description |
|---|---|---|
LLM_API_KEY |
— | OpenAI-compatible API key (required) |
BUTTERFLY_MODEL |
anthropic/claude-sonnet-4-5 |
Default model (provider/model) |
BUTTERFLY_MODEL_{TRIVIAL,STANDARD,COMPLEX,ESCALATE} |
tier default | Per-tier overrides |
BUTTERFLY_API_KEY |
unset | Enable HTTP+ACP auth (Bearer) |
BUTTERFLY_CWD |
process.cwd() |
Working directory for the agent |
BUTTERFLY_SESSION_STORE |
fs |
fs or sqlite persistence |
BUTTERFLY_LSP |
1 |
LSP integration toggle |
PORT |
3000 |
HTTP port |
AGENT_LOG_LEVEL |
info |
debug, info, warn, error |
AGENT_MAX_STEPS |
20 |
Max agent loop iterations |
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health, uptime, active runs |
/api/event |
GET | Global SSE stream (all events) |
/api/sessions |
GET/POST | List (cursor-paginated) / create |
/api/sessions/:id |
GET/PATCH/DELETE | Get / update / delete |
/api/sessions/:id/prompt |
POST | Run agent (?wait=true to block) |
/api/sessions/:id/abort |
POST | Abort active run |
/api/sessions/:id/fork |
POST | Fork a session |
/api/sessions/:id/summarize |
POST | Generate summary |
/api/sessions/:id/messages |
GET | Messages (cursor-paginated) |
/api/sessions/:id/tool-calls |
GET | Tool call history |
/api/sessions/:id/file-changes |
GET | File change history |
/api/sessions/:id/status |
GET | Run status (running/idle/interrupted) |
/api/sessions/:id/stream |
GET | Per-session SSE stream |
/api/sessions/:id/export |
GET | Export session as portable JSON |
/api/sessions/:id/diff |
GET | Unified diff of file changes |
/api/sessions/:id/revert |
POST | Revert files to before-state (workspace-bound) |
/api/sessions/:id/restore |
POST | Restore working tree to a git snapshot |
/api/sessions/:id/messages/:messageId |
PATCH | Edit a message's content |
/api/sessions/:id/retry |
POST | Truncate to last user message + re-run |
/api/sessions/commands |
GET | List configured slash commands |
/api/sessions/search |
GET | Search sessions by title/content |
/api/sessions/import |
POST | Import a session from export JSON |
/api/search |
GET | Symbol-level code search (identifier index) |
/api/providers |
GET | Providers + full model catalog |
/api/models |
GET | Model catalog |
/api/models/:provider |
GET | Models for one provider |
/api/config |
GET | Redacted config |
/api/file |
GET | List directory (workspace-bound) |
/api/file/content |
GET | Read file |
/api/file/status |
GET | File metadata |
/api/find/file |
GET | Glob search |
/api/mcp |
GET | MCP server status |
/api/mcp/connect |
POST | Connect an MCP server |
/api/mcp/:name/connect · disconnect |
POST | Connect / disconnect |
/api/permission |
GET | Pending HITL requests |
/api/permission/:requestId/reply |
POST | Reply to a request |
/openapi.json |
GET | Auto-generated OpenAPI spec |
├── core/ # @butterfly/core — config, logging, dotenv, tracing
├── packages/
│ ├── session/ # @butterfly/session — types, FS store, SQLite store
│ ├── tools/ # @butterfly/tools — 14 tools, registry, MCP, plugins
│ ├── llm/ # @butterfly/llm — Vercel AI, Anthropic, Gemini adapters
│ ├── context/ # @butterfly/context — SCE + COE + tokenizer + LSP
│ ├── agent/ # @butterfly/agent — loop, prompt, router, subagent
│ ├── server/ # @butterfly/server — ServerApp, EventBus, HTTP routes
│ ├── acp/ # @butterfly/acp — Agent Client Protocol integration
│ └── client/ # @butterfly/client — typed HTTP+SSE client SDK
├── apps/
│ ├── server/ # HTTP server entry point
│ └── acp/ # ACP stdio server entry point
├── docs/ # SCE.md, COE.md
└── tests/ # Vitest suite (224+ tests incl. opt-in live LLM)
MIT