// the hub

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.

Three composable layers: Dokoro gives tachi-agent persistent memory. TachiBot gives it better judgment — TachiBot orchestrates models; tachi-agent orchestrates work.

┌─ the hub flow

Front-ends in, three seams out.

tachi-agent hub data flow Seven front-end chips (CLI, REPL, Telegram, Slack, Claude Code via MCP, Gateway, OpenClaw) feed one labeled edge, orchestrator.run(task), down into the central Orchestrator. The Orchestrator depicts a cycle — recall, a bounded ReAct loop, and log — guarded by a HALT bound on maxIterations and a wall-clock timeoutMs. Three out-edges branch from it to the three swappable seams: Driver (the brain, default Qwen2.5 on Ollama, swappable to Hermes, cloud, or OpenClaw), ToolHost (merged MCP tools namespaced dokoro_* and tachibot_*), and Memory (dokoro recall and log). // front-ends CLI REPL Telegram Slack Claude Code (MCP) Gateway OpenClaw orchestrator.run(task) Orchestrator — the hub stateless between runs recall dokoro.recall(session) ReAct loop reason → act → observe log dokoro.log(trace) ↻ iterate HALT guard maxIterations · timeoutMs · AbortSignal ▍ seam · the brain Driver default: Qwen2.5 / Ollama swap: Hermes · cloud OpenClaw · Kimi swarm registerDriver() ▍ seam · the tools ToolHost merged MCP tools, namespaced dokoro_* + tachibot_* config, not code · allowlist ▍ seam · the context Memory dokoro — session store recall / log swappable · optional opt-in: in-loop ↓ tool results → next reasoning step (bounded) progress streams to stderr · final answer prints to stdout
// the three seams, in depth

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.

SeamSwap it to…Default
Driver
the brain
Pick a registered heart via TACHI_DRIVERollama · 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. Implement one Driver interface. 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
┌─ the three layers

Dokoro ↔ tachi-agent ↔ TachiBot.

Three composable layers — memory, runtime, and reasoning. Each is independently replaceable; none depends on the internals of another.

memory

Dokoro

Persistent session memory. tachi-agent calls 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.

runtime

tachi-agent

The ReAct loop that orchestrates work: drives the bounded reason/act/observe cycle, owns the task queue and daemon, manages skills, and surfaces the REPL, CLI, Telegram, and Gateway front-ends.

reasoning

TachiBot

Multi-model council that orchestrates models. tachi-agent calls 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."

┌─ tools auto-appear

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:

auto-discovered tools
# namespaced ${server}_${tool} — no registration in code
tachibot_jury
tachibot_council
tachibot_grok_search
tachibot_perplexity_ask
tachibot_nextThought
tachibot_execute_prompt_technique
tachibot_workflow
dokoro_session_recall
# …add a server in config and its tools join the loop
// the ReAct loop

Recall → reason & act → log. Always bounded.

Every run is wrapped by memory and bounded on three axes:

  • Recall. The hub asks dokoro for relevant session context before the first reasoning step.
  • Reason & act. The driver reasons, optionally calls allowlisted tools; results feed the next step.
  • Log. The trace is written back to dokoro for the next run to recall.
  • Bounded. A run halts at maxIterations, a wall-clock timeoutMs, or a cooperative AbortSignal — Ctrl-C yields haltedBy: "aborted".
run lifecycle: recall, bounded reason-and-act loop, halt, log A vertical flow showing one bounded run. First, recall touches memory — dokoro.recall(session) returns prior context before the first step. Next, the central reason-and-act loop runs under a guard, while iter is below maxIterations and elapsed is below timeoutMs and the AbortSignal is not aborted; each iteration cycles reason, then maybe call a tool, then observe, with a loop-back arrow back to reason. The loop halts for one of four reasons — final-answer, maxIterations, timeout, or aborted — the last three being the three bounds that make every run bounded. Finally, log writes the trace back to memory via dokoro.log(session, trace). A dim wrapping bracket on the left ties log back up to recall, showing recall and log as the memory bookend around the loop. memory bookend · recall … log ▍ memory · before first step recall dokoro.recall(session) → prior context ▍ the heart · bounded reason & act loop while ( iter < maxIterations && elapsed < timeoutMs && !signal.aborted ) reason → maybe call tool → observe ↻ next iteration · until halt guard fails ↓ ▍ halt — exactly one reason halt final-answer maxIterations · timeout · aborted ↑ the three bounds ▍ memory · after halt log dokoro.log(session, trace) recall → loop → log · always bounded
// memory in the loop (opt-in)

Bookend by default. In-loop when you need it.

By default memory is a bookendrecall once before the loop, log once after. Set memoryInLoop: true in OrchestratorOptions to go further:

  • Per-iteration recall. The hub refreshes context each tool-calling step, keyed on the evolving conversation. A single in-place "live memory" block is rewritten in-place — no context bloat.
  • Per-step notes. Each iteration writes a working-memory note via Memory.note → dokoro shared_note_append — the append-only, agent-tagged blackboard.
  • Off by default. The extra per-step tool calls degrade small local models; zero behavior change for existing callers. Enable for long, multi-step tasks.
typescript — opt in
const result = await createOrchestrator({
  driver,
  host,
  memory,
  options: {
    maxIterations: 20,
    timeoutMs: 120_000,
    memoryInLoop: true,  // opt-in
  },
}).run("audit every ADR and flag gaps");
memory: bookend by default versus in-loop opt-in Two side-by-side panels contrast how memory wraps the ReAct loop. The left panel, bookend, is the default: recall runs once before the loop and log runs once after. The right panel, in-loop, is opt-in via memoryInLoop and off by default: in addition to the bookend, every loop iteration refreshes a single in-place live-memory block via per-iteration recall and appends a per-step note to dokoro shared_note_append, an append-only blackboard. These extra in-loop arrows are drawn in vermilion and gold to distinguish them from the default bookend flow. // bookend default memory wraps the loop recall dokoro.recall(session) · once [ ReAct loop ] reason → act → observe ↻ next iteration · until halt no memory mid-loop log dokoro.log(trace) · once two touches total — recall, then log // in-loop opt-in off by default recall dokoro.recall(session) · once [ ReAct loop ] reason → act → observe ↻ next iteration · until halt log dokoro.log(trace) · once live memory ↻ per-iteration recall rewritten in-place note ↳ each step shared_note_append

bookend (default): recall → loop → log · in-loop (opt-in): per-iteration recall refresh + per-step note → shared_note_append

┌─ the daemon — standalone foundation

Around the hub: a daemon that owns the unattended machinery.

The hub stays a per-run unit. The daemon wraps it for unattended operation — it owns the queue, the worker, the schedules, the event log, and the notifiers. Drivers are pluggable hearts behind the registry: TACHI_DRIVER sets the default, each queued task can override it.

durable work

Queue + worker

A persistent, crash-safe task queue (.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.

recurrence

Schedules

A hand-edited .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.

pluggable hearts

Driver registry

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.

observability

Event log + notifiers

Every run appends to a durable JSONL log in .tachi/runs/, and TACHI_NOTIFY pushes each task outcome to Telegram / Slack — the record and the ping for runs nobody watched.