dogfood runtime #
This checkout now has one normal execution path: the Rust runtime/store owns state and mechanisms, a persistent Python workbench supplies policy and self-hosted code work, and the browser uses only the v2 lane protocol. The legacy agent/memory/tool loop is not started. Configured Discord ingress is normalized into the same durable runtime admission path. The transport owns no inbox database or model loop, and outbound sends remain explicit host effects.
source checkout #
The commands below are argv commands and work from Nushell. They do not depend on Bash.
bun run --cwd klbr-web build
cargo run -p klbr-daemon --bin klbr-daemon -- init
cargo run -p klbr-daemon --bin klbr-daemon -- doctor
cargo run -p klbr-daemon --bin klbr-daemon -- serve
Open http://127.0.0.1:8765/. serve is also the default subcommand. The service publishes
the editable behavior/ tree as an immutable content-addressed release before opening a
session. It refuses a non-loopback bind. Browser WebSockets also reject non-loopback origins;
native local clients do not need an Origin header.
Runtime paths are explicit:
| variable | default |
|---|---|
KLBR_WORKSPACE |
current directory |
KLBR_DATA_DIR |
the platform data directory plus klbr |
KLBR_PYTHON |
python3 |
KLBR_WORKER_RUNNER |
<workspace>/python/klbr-runtime/src/klbr_runtime/__main__.py |
KLBR_BEHAVIOR_DIR |
<workspace>/behavior |
KLBR_SKILLS_DIR |
<workspace>/skills |
KLBR_WEB_DIR |
<workspace>/klbr-web/dist |
KLBR_BIND_ADDR |
127.0.0.1:8765 |
KLBR_REMOTE_PLACEMENT |
unset; local workbench |
The state directory contains runtime.sqlite, releases/, drafts/, artifacts/, and
skills/.
The old agent.db is neither opened nor seeded by this path.
Nix later #
Dogfooding is source-first for now. The repository's existing NCI/crate2nix-oriented flake is left intact; a relocatable package and service are deferred until the runtime path is settled.
working on this checkout from a cell #
print(await search("TODO", "klbr-runtime/src", max_results=20))
note = await open_file("docs/note.md")
await note.replace("old", "new") # one occurrence, or an error
check = await run(("cargo", "check", "-p", "klbr-runtime"), timeout=120)
print(check.exit_code, check.stderr.decode(errors="replace"))
open_file, ls, search, delete and run are bound in every cell and act
on the session's workspace, resolved once per kernel generation
(klbr.workspace.PRELUDE_NAMES lists them; ws = await workspace.current() is the object form).
ls lists one level and ls(path, recursive=True, match="*.rs") walks with a name glob, and search reports
truncated rather than letting a capped result read as a complete one.
Paths are root-bound and symlink escapes are rejected. workspace.run is a typed host RPC,
not a shell call inside Python. The Rust host owns the process group, output cap, timeout, and
cancellation cleanup, so kernel replacement cannot strand a build process.
skills #
Instruction bodies live in skills/<id>/SKILL.md. The first non-empty line is # <id> and the
directory name must equal it; that title is the only schema, so the id is a handle and the body
is the trail to the state the id names. Nothing is stored beside the body, because a description
would become a second, staler definition of it.
import klbr.skills as skills
dir(skills) # the ids this kernel generation can import
help(skills.workspace) # one body
skills.refused # ids found and not offered, with the reason
skills.revision # content identity of the whole offered set
skills.roots # where those ids were read from
Builtin ids are read from KLBR_SKILLS_DIR (default <workspace>/skills), so they ship with the
checkout. Managed ids are read from <data>/skills, which init and serve create; a skill the
agent writes there survives a checkout move. Both are one namespace, and an id defined in both is
refused instead of silently winning - klbr doctor prints the ids and the refusals, and the
daemon logs the index at startup.
The host validates the roots and hands the bodies to the worker as a spawn payload, so a worker
never reads a skills directory itself: one reader, one validator, and a set that cannot change
under a running turn. The kernel generation's kernel.reset record carries the revision it
loaded, so the durable log says which instruction set a turn could have read rather than what the
directories happen to hold now.
The model sees the ids in its contract message and imports a body by id. A maintenance turn
additionally loads maintenance by default. Because the set is fixed at spawn, a skill written
during a turn is offered on the next generation rather than mid-turn.
operator path #
These commands use the same v2 socket as the UI and do not need a successful model or policy callback:
klbr status
klbr status default
klbr reset default
klbr compact default
klbr drain default
klbr stop default
klbr activate default 3 /absolute/path/to/releases/<sha256>
reset states explicitly that Python heap state was not restored. drain waits for accepted
host work before stopping. Behavior activation is an epoch compare-and-swap against a verified
content-addressed release.
model seam #
klbr-core still reads the existing KDL model configuration. Defaults target OpenAI-compatible
local endpoints at http://localhost:8001/v1, :8002/v1, and :8003/v1. doctor validates
the configuration and worker import path; a normal send is the live model/provider check.
Provider keys belong in the process environment or the configured KDL file, not source control.
Prompt caching is a route property, declared per route and never guessed:
llm {
url "https://api.example/v1"
model "m"
cache_profile "openai_prompt_cache_key" // or openai_prompt_cache_retention | openai_prompt_cache_options
cache_affinity "klbr:work" // stable key: the same one for related requests, never per request
cache_retention "24h" // only on a profile that accepts it: in_memory | 24h, or 30m on an options route
}
cache_profile is opt-in because /chat/completions is shared by many incompatible
OpenAI-style routes, and an unknown route must not receive a field it may reject. A profile
declares which cache fields that route accepts; cache_retention values are checked against it
at parse time, so a mismatch is a configuration error here rather than a parameter error from
the route after the request was paid for. Route extra_options cannot smuggle a cache field:
cache_prompt, cache_control, prompt_cache_key, prompt_cache_retention and
prompt_cache_options are removed from route options and only re-added by the declared profile.
Cache reads and writes are reported separately and are never inferred: a provider that reports
nothing leaves the measurement absent, which is a different statement from a provider that
reported zero. OpenAI's prompt_tokens_details.cached_tokens and cache_write_tokens, and
DeepSeek's prompt_cache_hit_tokens / prompt_cache_miss_tokens, are all read; a route with an
automatic cache (DeepSeek) needs no profile at all.
Rebuild the source UI after frontend changes with bun run --cwd klbr-web build.