Skip to content
MainActorDevPublic

About

Butler — reusable agent runtime (provider, loop, tools, layered memory, sources, MCP)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Butler

Butler

The reusable agent runtime. Your tools, your policy — his service.

tests node provider surfaces

Architecture · Flow · What it does · Quickstart · Invariants · Docs


Butler is a reusable agent runtime: an OpenAI-compatible provider, a bounded tool-calling loop, permission-gated tools, layered memory with provenance, multi-source ingestion, and conversation-API + MCP transports — all in one importable package. A consumer (the generic demo/ today, Lionidas tomorrow) brings its own tools, sources, and policy; Butler brings the loop. He is the butler, not the household.

Live-proven: a 10-hop authoring session against glm-5.3-flash searched ingested tickets, drafted a spec, was corrected by the validator twice (external signal — never model self-critique), reached VALID, and cited the exact document sha256. Verified against the database, not the answer text (docs/drills.md).

Architecture & flow

┌─────────────────────────────────────────────────────────────────────────┐
│ CONSUMER APP (demo/ · later Lionidas)                                   │
│ registers tools + validator-as-ground-truth · picks policy (DRAFT)      │
│ wires sources · owns the sqlite file · provides auth + WebSocket bus    │
└───────────────▲─────────────────────────────────────────┬───────────────┘
                │ tools/policy/storage/events injected in │ imports butler
┌───────────────┴─────────────────────────────────────────▼───────────────┐
│ TRANSPORTS          conversation API (/api/agent/*) · MCP (/mcp, SDK)   │
│                     turn events → consumer's WS bus (butler.*)          │
├─────────────────────────────────────────────────────────────────────────┤
│ EVAL HARNESS        retrieval top-k · budget caps · authoring judged    │
│                     by YOUR validator — canary proves it can fail       │
├─────────────────────────────────────────────────────────────────────────┤
│ LOOP                reason→act→observe · budgets (hops/tokens/WALL —    │
│                     the loop owns the clock) · ReAct observations ·     │
│                     partial transcript rides every error                │
├──────────────────┬──────────────────────────────────────────────────────┤
│ TOOLS + POLICY    registry: read < draft < act · typed named refusals   │
├──────────────────┼──────────────────────────────────────────────────────┤
│ PROVIDER          plain fetch, any OpenAI-compatible base · retries     │
│                   capped by remaining wall · deterministic mock         │
├──────────────────┴──────────────────────────────────────────────────────┤
│ MEMORY             sqlite (six butler_* tables, idempotent migrations)  │
│ RETRIEVAL          BM25 over chunks + notes (embeddings deferred)       │
│ SOURCES            file · jira · github · repo → sha256 documents,      │
│                    structural chunks, caps, binaries to artifacts dir   │
└─────────────────────────────────────────────────────────────────────────┘

Rendered diagram: docs/architecture.html.

The authoring flow

Live-proven against glm-5.3-flash (docs/drills.md):

1.  user message → conversation API (or MCP tools/call — same registry)
2.  session resumed from rows alone (butler_messages replay)
3.  LOOP assembles context → PROVIDER call (wall budget owns everything)
4.  model calls docs.search   → BM25 over ingested chunks
5.  model calls docs.get      → full ticket fixture by sha256
6.  model calls spec.draft    → VALIDATOR: INVALID   ← external signal,
7.  model repairs the draft   → VALIDATOR: INVALID     never self-critique
8.  model repairs again       → VALIDATOR: VALID ✓    (observed live, twice)
9.  act-class tools           → REFUSED by policy (DRAFT) — human publishes
10. final answer + cited sha256 → persisted · WS events · tokens accounted

Every message, tool call, and token lands in butler_messages — a session is replayable from rows alone.

What Butler does

You bring Butler brings
Provider config (any OpenAI-compatible base URL + key + model) Retries capped by YOUR wall budget, per-call timeouts, deterministic mock
Tools with permission classes (read/draft/act) The bounded ReAct loop, typed policy refusals, provider-visible filtering
A validator tool as ground truth The authoring eval that judges drafts by it — never model self-report
Sources (file/jira/github/repo) + an artifacts dir sha256 content-addressed ingestion, structural chunking, BM25 retrieval
A sqlite file and a Fastify app Idempotent migrations, sessions/notes/documents, conversation API + MCP

Quickstart

pnpm install
npx tsx demo/app.ts     # mock provider; set BUTLER_PROVIDER_BASE_URL for live
# POST /api/agent/sessions · WS /ws (butler.* events) · POST /mcp (MCP)
curl -X POST localhost:8790/api/agent/sessions -H 'content-type: application/json' -d '{}'
curl -X POST localhost:8790/api/agent/sessions/<id>/messages \
  -H 'content-type: application/json' -d '{"content":"draft a spec from the tickets"}'

Design invariants

  • Dependency direction is one way. Consumers import Butler; Butler never imports app code. The boundary test fails CI on leakage.
  • Autonomy is a policy, not a fork. Act-class tools are registered and REFUSED under DRAFT — the refusal is the product behavior.
  • Validators are the ground truth. The model never grades its own draft.
  • Provenance is mandatory. Notes without a source ref are rejected; supersession, never deletion.
  • The loop owns the wall clock. A hanging provider is aborted at the deadline — budgets hold even when the model misbehaves.
  • Every claim has a test that can fail. Evals ship with a canary that proves the harness detects regressions.

Status — v0.1, all stages shipped

  • Stage 0 — repo skeleton, boundary guard (fails on app-import leakage)
  • Stage 1 — provider + loop (mock-driven; hang → PROVIDER_TIMEOUT inside wall budget)
  • Stage 2 — tools + policy + sqlite memory (provenance mandatory, supersession)
  • Stage 3 — source ingestion + structural chunking + BM25 retrieval
  • Stage 4 — generic demo consumer (conversation API + WS + MCP)
  • Stage 5 — eval harness + canary + consumer docs

91 tests green (pnpm check = tsc + vitest + UI build, hermetic — mock provider, zero network in CI).

Roadmap complete (except Lionidas integration, by decision): demo tour + Vue UI ✅ · MCP stdio bin ✅ · orchestration L2 (supervisor, budget roll-up, named-skip) ✅ · embeddings + hybrid retrieval (BM25 default) ✅ · postgres driver (contract-parameterized) ✅ · symbol chunks (repo sources opt-in) ✅.

Try the UI: npx tsx demo/app.ts → open http://127.0.0.1:8790 — chat with the live agent, watch tool calls stream, browse sessions/documents/ notes. Scripted tour: npx tsx demo/tour.ts. Guided walkthrough: docs/use-case-qa-spec.md.

Mount in your MCP client (Claude Code / Codex / Hermes):

{ "mcpServers": { "butler": { "command": "npx",
  "args": ["tsx", "/path/to/butler/bin/butler-mcp.ts"] } } }

Serves the demo registry over stdio (fixtures corpus, draft policy — spec.publish filtered out). Same registry as the HTTP/MCP transport: one tool set, every surface.

Docs map

  • docs/agent-core.md — P1 agent core: profiles, workflows, artifacts, claude-cli executor
Doc What's in it
docs/architecture.html The layer/flow diagram, rendered
docs/api.md The package surface with runnable snippets
docs/consumers.md The adoption contract (what you bring / what you get)
docs/drills.md Live verification log (condition 3: glm-5.3-flash)
ROADMAP.md Deferred machinery (orchestration L2 seams + triggers, embeddings, postgres, MCP stdio)

Plan of record: plans/20261001T035356Z-agentic-infrastructure-core-plan.md (rev 3.1, in the Lionidas repo).

Development

pnpm install
pnpm check            # tsc --noEmit + vitest (mock provider — hermetic)

Node ≥ 24.12 (better-sqlite3 ABI). Live drills: set BUTLER_PROVIDER_BASE_URL, BUTLER_API_KEY, BUTLER_MODEL (works with any OpenAI-compatible endpoint; verified against z.ai's coding endpoint).

Architecture (R3 — durable chat core)

Turns are durable: every user message becomes a command; a PURE orchestrator decides events (no I/O); the EventSink commits events + projections + receipts + outbox in ONE sqlite transaction; the EffectWorker drains side effects and feeds results back as commands. Replay any session's event log to reproduce its state bit-for-bit.

The ACP v1 wire (createAcpAgent()) exposes initialize/session lifecycle over JSON-RPC — in-process for Lionidas, stdio for third-party clients. usage_update streams both meters; compaction is visible, originals preserved.

See src/orchestrate/turn-engine.ts, src/orchestrate/event-sink.ts, src/orchestrate/effect-worker.ts, src/acp/.

About

Butler — reusable agent runtime (provider, loop, tools, layered memory, sources, MCP)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages