Skip to content

Architecture

ka is a workspace of small crates with a strictly one-way dependency direction: ka-harness → ka-term / ka-protocol / ka-dialect / ka-strand → ka-engine. The engine never imports a surface.

ka-harness ──► ka-term (ratatui TUI)
│ ► ka-protocol (Command / Event enums)
│ ► ka-dialect (catalog + 3 wires + discovery)
│ ► ka-strand (append-only sessions)
└────────────► ka-engine (engine: turn machine, Hands, gate, digests)
ka-index (SQLite FTS, optional) ka-sandbox (landlock/…)

The binary crate is published as ka-harness (renamed from ka-agent in v0.3.0; the old name is reserved for Ka Agent, a planned separate product: an optional persistent companion with long-term memory, messaging channels, and a web UI). Ka Agent will build on ka strictly from the outside, through the shipped surfaces: ka run --print ndjson, ka serve, ka acp, the strand format, or the binary as a subprocess. Resident-agent concerns (channels, schedulers, plugin runtimes, memory beyond strands) never enter these crates, and CI gates the binary’s dependency graph (cargo xtask deps) to keep it that way. Decision record: research/ka-harness/agent-boundary.md.

Two queues connect any surface to the engine, both plain serde enums, both NDJSON-shaped:

  • Command (surface → engine): Prompt, Steer (mid-turn steering; wire tag interject), Queue (held until the turn settles; wire tag defer), Abort, SetModel, SetEffort, SetMode, AlwaysAllow, Resume, Compact, Answer…
  • Event (engine → surface): TurnStarted (with a context meter), Delta (text, thought, or tool call), CallStarted / CallFinished, Ask, TurnFinished (stop kind, usage, cost, cache-hit), DigestStarted / DigestFinished, ModeChanged, Error…

Because the protocol is NDJSON-first, ka run --print ndjson is literally the engine’s Event side serialized. The TUI, the HTTP server, and the ACP adapter are all just different consumers of the same stream.

Every prompt runs through four states:

  1. Receiving: the prompt lands, guards and trust are checked.
  2. Speaking: the active Speaker streams the model’s response. Steers are checked between deltas; Esc aborts, keeping the partial transcript.
  3. Acting: tool calls whose clearance is granted run in parallel (FuturesUnordered), with results re-ordered to call order and per-tool timeouts. Exec-tier calls wait for your approval when in guarded mode. Filesystem work (read/grep/glob and the per-turn git snapshot) runs on blocking threads, and grep scans large candidate sets on parallel workers with byte-identical output, so a big repo walk never stalls streaming, redraws, or interrupts.
  4. Settling: post-turn maintenance. Ledger sync, digest threshold checks, queue drain, harness checks, strand flush.

Speaking and Acting alternate until the model stops calling tools or the step cap (max_steps, default 20) forces a text reply.

The engine crate knows nothing about terminals, TOML files, or any vendor’s HTTP quirks:

  • Speaker is the provider trait (“the model speaks through it”). The three implementations (Anthropic messages, OpenAI chat, OpenAI Responses) live in ka-dialect, vendored on reqwest + rustls with hand-rolled SSE. No SDK crates.
  • Hands is the tool registry trait (“the engine acts through them”). Every tool is a struct behind the same seam, which is where MCP servers, pathfinder, and delegate plug in.
  • A strand is the state: replaying its records to the tip reproduces everything: model, effort, mode, queued items. No other persistence exists in core.

Context pressure is handled by a ladder, cheapest first:

  1. Caps + spill: oversized tool output is parked on disk and referenced as spill://<id> (default 32 KB tail / 8 KB head) before the model ever sees it.
  2. Prune: blank tool results older than the protected window are elided with markers.
  3. Digest: a same-model summary that never cuts between a tool call and its result; triggers at a reserve threshold.
  4. Promotion: on overflow, ka can re-dispatch to a bigger-context sibling model ([context] promote, on by default).
  • Footprint contract in CI: binary size, cold-start, and idle RSS asserted against committed baselines.json.
  • Wire fixtures: recorded SSE transcripts per wire (including malformed chunks and partial-JSON repair cases) replayed in CI; never a live key.
  • Property tests: strand replay determinism, ledger refusal on stale writes, bash decomposition tables, hardstop matrix including wrapper obfuscation.

Read the original design document in the repository: research/ka-harness/architecture.md.