Architecture.
One small orchestrator at the center. Every front-end calls orchestrator.run(task); the hub recalls memory, runs a bounded ReAct loop, logs the trace, and halts cleanly. dokoro gives tachi-agent persistent memory. TachiBot gives it better judgment — TachiBot orchestrates models; tachi-agent orchestrates work.
Front-ends in, three seams out.
The ReAct loop lives in the client — never inside an MCP server — so there's no agent-inside-a-server recursion and no hidden control flow on the wire.
Swap any seam. Never touch the core.
The complete public API is three interfaces — see src/types.ts. Because the orchestrator depends only on these, every integration composes the same hub.
| Seam | Swap it to… | Default |
|---|---|---|
| Driver · the brain | Pick a registered heart via TACHI_DRIVER — ollama · hermes · openai · openrouter — or register your own via registerDriver (a cloud model, OpenClaw, a Kimi swarm). A queued task's driver field overrides per task. | local Qwen2.5 / Ollama (native /api/chat) |
| ToolHost · the tools | Add or remove MCP servers and tools — config, not code. An allow allowlist keeps dangerous tools out unless granted. | dokoro + tachibot merged over stdio, namespaced ${server}_${tool} |
| Memory · the context | Swap the persistent-context backend or disable it entirely. The orchestrator is stateless between runs. | dokoro session recall / log |
dokoro ↔ tachi-agent ↔ TachiBot.
Memory, runtime, and reasoning. Each is independently replaceable; none depends on the internals of another.
dokoro.recall before each run and dokoro.log after — bookending the loop with durable context. Opt in to in-loop recall and per-step note writes for long multi-step tasks.tachibot_jury, tachibot_council, tachibot_grok_search and other council tools over MCP — TachiBot handles provider routing and adjudication.“TachiBot orchestrates models; tachi-agent orchestrates work.”
Whatever you connect, the agent can call.
Tools come from whatever MCP servers are connected — all config, not code. Connect tachibot and dokoro and these surface automatically, namespaced and ready. The agent ships a small curated allowlist (TACHI_ALLOW); set it to tachibot_,dokoro_ to expose everything.
1# namespaced ${server}_${tool} — no registration in code2tachibot_jury3tachibot_council4tachibot_grok_search5tachibot_perplexity_ask6tachibot_nextThought7tachibot_execute_prompt_technique8tachibot_workflow9dokoro_session_recall10# …add a server in config and its tools join the loopRecall → reason & act → log. Always bounded.
maxIterations, a wall-clock timeoutMs, or a cooperative AbortSignal — Ctrl-C yields haltedBy: "aborted".&& elapsed < timeoutMs
&& !signal.aborted )
reason → maybe call tool → observe ↻
Bookend by default. In-loop when you need it.
By default memory is a bookend — recall once before the loop, log once after. Set memoryInLoop: true in OrchestratorOptions to go further.
Memory.note → dokoro shared_note_append — the append-only, agent-tagged blackboard.1const result = await createOrchestrator({2 driver,3 host,4 memory,5 options: {6 maxIterations: 20,7 timeoutMs: 120_000,8 memoryInLoop: true, // opt-in9 },10}).run("audit every ADR and flag gaps");Around the hub: a daemon that owns the unattended machinery.
The hub stays a per-run unit. The daemon wraps it — queue, worker, schedules, event log, notifiers. TACHI_DRIVER sets the default heart; each queued task can override it.
.tachi/queue.json) and a worker that drains it. Interrupted tasks re-queue on restart; failures retry with exponential backoff. POST /tasks enqueues from outside..tachi/schedules.json (daily at HH:MM or every N minutes), re-read live each tick; machine state kept in a separate -state.json. Due entries feed the same queue.TACHI_DRIVER picks the default brain — ollama · hermes · openai · openrouter — and a task's driver field overrides it per task. Explicit selection only: an unavailable heart fails the task loudly, never silently substitutes..tachi/runs/, and TACHI_NOTIFY pushes each task outcome to Telegram / Slack — the record and the ping for runs nobody watched.Orchestrator, one AbortController, and one dokoro session / workspace id per tenant. The orchestrator is stateless between runs, so N tenants = N independent run() calls with no shared state. Don't share memory across tenants — scope recall / log by tenant session id — and use per-tenant tool allowlists and per-tenant maxIterations / timeoutMs as rate and cost limits.