Skip to content

Architecture

Every arrow is a process or crate boundary that can be tested on its own:

Telegram / Web / CLI / APIs
|
ka agent runtime
|
memory + task scheduler + plugin host
|
stable Ka interface (KaPort)
|
Ka engine

In the MVP, concretely:

Telegram Bot API ──long poll──┐ local admin CLI ──unix socket──┐
▼ ▼
┌──────── ka-agent daemon ──────────────────────┐
│ channels: telegram adapter, admin socket │
│ runtime: session manager, durable queue, │
│ approvals, worker supervisor, budgets, audit │
│ state + search (SQLite) · KaPort │
└───────────────────┬───────────────────────────┘
▼ supervised worker process
ka engine (one per session)
Crate Role
ka-agent-core Domain types and state machines: ids, sessions/turns, approvals, tasks, queue, outbox, budgets, audit, strict ka-agent.toml config. No I/O.
ka-agent-store SQLite persistence: schema, migrations, audit chain.
ka-agent-port The KaPort trait and the crate-backed worker boundary. The only crate besides the binary that may depend on ka-*.
ka-agent-runtime Session manager, durable queue, single-flight submission, approvals with deny-on-expiry, budgets, worker supervisor with backoff and a crash-loop breaker.
ka-agent-channels The Telegram Bot API channel and the secret store (env + 0600-file tiers).
apps/ka-agent The binary: run, status, chat, doctor, and the hidden __worker.

A CI gate (scripts/check-ka-deps.sh) keeps every ka-* dependency inside ka-agent-port, so swapping the boundary implementation stays a one-crate change.

ka agent talks to engines only through KaPort:

  • Forwarded commands: prompt, answer, abort, set_mode, set_model, set_name, compact. Nothing else.
  • Never reachable from a channel: shell, set_feature, set_sandbox, save_api_key, save_settings. The port API has no operation that maps to them; the invariant is enforced by construction and by contract tests.
  • No “always” from remote channels. Ka’s “always allow” is a coarse per-tool-name write into the project .ka/ka.toml. Durable grants live in ka agent’s own rules layer instead.

The worker applies a security overlay over any engine configuration, and the overlay always wins:

mode = "guarded" # sessions stay guarded
[tools.bash]
background_after_ms = 0 # bash jobs never detach (kill reaches them)
[features]
disable = ["tool:remember"] # memory goes through ka agent
# [guards] spend_usd comes from the daemon budgets

Workers run isolated from your interactive Ka state: their own KA_DATA_DIR, XDG_STATE_HOME, XDG_CONFIG_HOME and HOME, so the operator’s ka.toml, strands, skills and trust store are never shared.

  • Workers are separate processes, one per active session, spawned and stopped by the runtime.
  • Crashes respawn with exponential backoff (1s to 60s, configurable) and a crash-loop breaker: too many crashes inside the window degrade the session, and resuming then needs an explicit /resume or /new.
  • Stopping escalates: abort, wait the grace period (default 10s), then SIGTERM to the worker’s process group (so tool children die too), then SIGKILL.
  • An engine channel that closes without a terminal idle is treated as a crash: the turn is marked interrupted, you are notified, and nothing is auto-replayed.

Full contracts: docs/contract/ka-port.md and docs/contract/worker-protocol.md in the repository.