Knowledge-centric Runtime Architecture¶
Current technical design for the knowledge-centric benchmark runtime. This is the canonical design reference; operational runbooks and wrapper details live in the README, campaign checklist, and experiment script README. For the research narrative behind this design — the method story and results — see the paper page.
1. System Map¶
The maintained runtime is the shared container path (--runtime container).
OpenAI models run through the same container runtime with
MODEL_PROVIDER=openai in the provider profile (the former --runtime openai
compatibility alias was removed).
Component map:
%%{init: {"flowchart": {"useMaxWidth": false}}}%%
flowchart LR
subgraph Control["Python control plane"]
CLI["ksi.cli<br/>uv run python -m ksi.cli"]
Loader["tasks.loaders<br/>arc / swebench_pro / polyglot / terminal_bench_2"]
Engine["orchestrator.engine<br/>generation loop"]
Eval["eval.*<br/>arc_session / swebench_pro / polyglot_harness / terminal_bench_2"]
HostLLM["runtime.llm<br/>reflection + distill calls"]
end
subgraph Host["Host runtime launcher"]
CHost["runtime.container_host<br/>KsiContainerExecutor"]
Main["runtime_runner/src/main.ts<br/>payload + stdout envelope"]
Runner["runtime_runner/src/container_runner.ts<br/>docker run + mounts"]
end
subgraph Worker["Worker container"]
Docker[("Docker image<br/>ksi-agent:bench")]
Agent["agent-runner/src/index.ts<br/>provider adapters"]
MCP["memory/mcp_server.py<br/>memory / forum / ARC toolsets"]
end
subgraph Providers["Provider APIs"]
Claude["Claude Code SDK<br/>+ direct Anthropic adapters"]
OpenAI["OpenAI Agents SDK"]
end
subgraph Stores["SQLite stores"]
KStore[("<stem>_knowledge.sqlite<br/>KnowledgeStore")]
RStore[("<stem>_runtime.sqlite<br/>MemoryStore audit sidecar")]
FBus[("ForumBus JSONL<br/>discussion write buffer")]
end
CLI --> Loader --> Engine
Engine --> CHost --> Main --> Runner --> Docker --> Agent
Agent <--> MCP
Agent --> Claude
Agent --> OpenAI
MCP --> FBus --> Engine
MCP <--> KStore
Engine --> Eval
Engine <--> KStore
Engine --> RStore
Engine --> HostLLM
HostLLM --> Claude
HostLLM --> OpenAI
This architecture is intentionally knowledge-centric: the durable asset is the shared knowledge DB, while worker containers are transient and generate evidence, critique one another, and leave reusable guidance behind. The control plane still tracks per-agent workstream labels and can optionally reuse agent-scoped sessions.
2. Attempt Runtime Lifecycle¶
One task attempt moves through these components, in three stages:
Dispatch — the host builds the payload and launches the container:
%%{init: {"sequence": {"useMaxWidth": false}}}%%
sequenceDiagram
participant Eng as engine.py
participant Host as container_host.py
participant Main as main.ts
participant CR as container_runner.ts
participant Dock as Docker container
Eng->>Host: run_task(task payload, seed package, DB paths)
Host->>Host: build workspace seed, arc_tools, knowledge/runtime payload
Host->>Main: JSON payload on stdin
Main->>CR: runContainerAgent()
CR->>Dock: docker run ksi-agent:bench with mounted DB/snapshot files
In-container execution — the agent runs, backed by MCP tools and the provider SDK:
%%{init: {"sequence": {"useMaxWidth": false}}}%%
sequenceDiagram
participant Dock as Docker container
participant AR as agent-runner
participant MCP as mcp_server.py
participant SDK as provider adapter/API
Dock->>AR: node dist/index.js
AR->>MCP: start stdio MCP server for selected toolset
AR->>SDK: query loop or direct provider adapter
SDK-->>AR: text, tool calls, usage, final status
Result path and scoring — continuing from the agent-runner's final status above, the envelope travels back up the host stack to be scored and persisted:
%%{init: {"sequence": {"useMaxWidth": false}}}%%
sequenceDiagram
participant AR as agent-runner
participant Main as main.ts
participant Host as container_host.py
participant Eng as engine.py
participant Eval as evaluator
participant DB as SQLite stores
AR-->>Main: JSON status envelope
Main->>Main: preserve streamed envelope, collect native session memory
Main-->>Host: stdout JSON envelope
Host->>Host: parse_runner_stdout(), normalize silent/error states
Host-->>Eng: RuntimeResult or runtime exception with runtime_meta
Eng->>Eval: score model output/tool trace
Eng->>DB: record attempt, best state, transcripts, token phases
payload.knowledge.db_path is resolved to an absolute host path before it
reaches the TypeScript runner. The container runner does not mount the parent
directory; it bind-mounts the knowledge DB file (+ -wal/-shm sidecars if
present), the optional runtime-audit DB (+ sidecars), and the memory snapshot
file individually under /app/memory-db — plus a forum_bus/
subdirectory mount, gated to forum-phase task sources only. The MCP server
selects the SQLite filename from the payload/environment and uses that file
as the authoritative knowledge substrate.
ARC runs natively for every provider — it neither mounts an ARC snapshot nor
registers an ARC MCP server. The agent reads payload.json from its workspace
and writes attempt files with native file tools; the host synthesizes the
arc_submit_trial trace after the container exits.
3. Runtime Status And Normalization¶
The in-container runner emits a JSON envelope. When the envelope is parseable, the envelope status and host normalization are more informative than the Docker exit code.
%%{init: {"flowchart": {"useMaxWidth": false}}}%%
flowchart TB
Runner["agent-runner envelope<br/>success / error / recovered_from_session"]
Host{"host parse + normalize"}
Activity{"success has output,<br/>tokens, or tool calls?"}
OK["success<br/>real attempt, evaluator decides score"]
REC["recovered_from_session<br/>session log supplied recovery evidence"]
ERR["error<br/>adapter/runtime failure"]
Silent["silent_failure<br/>host reclassified empty success-like envelope"]
Eval["engine evaluation / persistence"]
Fail["engine records runtime failure<br/>with runtime_meta"]
Runner --> Host
Host -->|status=success| Activity
Activity -->|yes| OK --> Eval
Activity -->|no| Silent --> Fail
Host -->|status=recovered_from_session| REC --> Eval
Host -->|status=error| ERR --> Fail
Status meanings:
| Status | Emitted by | Scoring behavior |
|---|---|---|
success |
agent-runner, then accepted by host normalization | Real attempt. It may be text-heavy or tool-heavy; the evaluator decides whether the output is scorable. |
recovered_from_session |
agent-runner recovery path after an empty SDK iterator with usable session-log evidence | Real attempt. Recovered output is scored when it contains a benchmark answer; recovery diagnostics are preserved either way. |
error |
agent-runner/adapter envelope or host runtime failure | Not scored as a normal attempt. The engine persists error metadata. |
silent_failure |
Python host normalization of a success-like envelope with no output, no tools, and no tokens | Not scored. Raised as a runtime failure so it does not become a false zero-score success. |
Token usage is normalized into token_phases when the runtime DB is enabled.
The tokens_source metadata records whether counts came from a final result
event, a per-turn sum, session recovery, or were unavailable.
4. Generation Knowledge Loop¶
The generation loop has more control flow than a simple five-step chain:
Generation control flow:
%%{init: {"flowchart": {"useMaxWidth": false}}}%%
flowchart TB
Start["Start generation N"]
Claim["Claim / assign tasks<br/>deterministic population strategy"]
Enrich["Enrich seed packages<br/>prior attempts + distilled bundles"]
Exec["Execute task containers"]
Persist["Evaluate + persist attempts<br/>KnowledgeStore authoritative state"]
PTGate{"per-task forum enabled?"}
PTForum["Per-task forum containers<br/>ForumBus -> per_task_forum posts"]
CTGate{"cross-task forum enabled?"}
CTForum["Cross-task forum containers<br/>__cross_task__ room"]
DistillGate{"distill_enabled?"}
Distill["Per-task distill in parallel<br/>+ per-target cross-task bundles<br/>(one broadcast bundle under target-conditioning=false)"]
MoreGate{"another generation configured?"}
MemoryGate{"memory enabled?"}
Seed["Seed next population<br/>write seed_snapshots"]
NoSeed["Skip seed phase<br/>continue with current population"]
End["Stop run"]
Start --> Claim --> Enrich --> Exec --> Persist --> PTGate
PTGate -->|no| CTGate
PTGate -->|yes| PTForum --> CTGate
CTGate -->|yes| CTForum --> DistillGate
CTGate -->|no| DistillGate
DistillGate -->|yes| Distill --> MoreGate
DistillGate -->|no| MoreGate
MoreGate -->|no| End
MoreGate -->|yes| MemoryGate
MemoryGate -->|yes| Seed --> Start
MemoryGate -->|no| NoSeed --> Start
Controls:
--per-task-forum-rounds 0skips the per-task forum phase.--cross-task-forum-rounds 0skips the cross-task forum phase.--distill-enabled falseskips distillation.--no-memorydisables agent knowledge tools, discussion phases, distillation, and seeding. The knowledge DB remains enabled for authoritative attempt/resume state. ARC tasks use native tools regardless.--forum-early-exit truelets forum phases finish before their timeout when all expected participants callforum_signal_done.
Conceptually, phases 2 through 5 primarily capture transferable knowledge in a shared substrate. They coexist with the current workstream-driven orchestration and optional agent-scoped session reuse rather than replacing those mechanisms entirely.
5. Database Ownership¶
Two SQLite files are used per experiment stem:
| File | Owner | Responsibility |
|---|---|---|
<stem>_knowledge.sqlite |
src/ksi/memory/knowledge_store.py (KnowledgeStore) |
Authoritative substrate: run identity, tasks, attempts, best task state, retrieval corpus, discussion posts, distillations, seed snapshots, FTS5, optional sqlite-vec index |
<stem>_runtime.sqlite |
src/ksi/memory/store.py (MemoryStore) |
Optional audit sidecar: raw transcripts, artifacts, timing, token accounting, debug/runtime metadata, legacy memory-document views |
--knowledge-db-path sets the authoritative knowledge DB. --runtime-db-path
sets the optional runtime audit DB. If omitted, the runtime DB is derived as a
<stem>_runtime.sqlite sidecar from the knowledge DB path; --no-runtime-db
disables the sidecar. --memory-db-path has been removed.
%%{init: {"flowchart": {"useMaxWidth": false}}}%%
flowchart LR
Engine["orchestrator.engine"]
MCP["mcp_server.py"]
Persist["SqlitePersistence<br/>+ token accumulator"]
subgraph KDB["Knowledge DB"]
KRun["runs / generations / agents"]
KTask["tasks / attempts / task_state"]
KKnowledge["knowledge / knowledge_fts"]
KVec["knowledge_vec / vector_status"]
KForum["discussion_done"]
KSeed["seed_snapshots"]
end
subgraph RDB["Runtime audit DB"]
RRun["runs / generations / agents"]
RTask["tasks / assignments / attempts"]
RArtifacts["attempt_artifacts / memory_docs"]
RForum["forum_events"]
RArc["arc_task_refs"]
RTokens["token_phases"]
end
Engine --> KDB
MCP --> KDB
Persist --> RDB
Engine --> RTokens
knowledge.entry_type values are attempt, insight, post, and
distillation. Current knowledge.source_phase values include execution,
per_task_forum, cross_task_forum, per_task_distill, and
cross_task_distill; legacy values such as discussion, condensation, and
seeding are retained for old DBs/tests.
Seed packages are stored in seed_snapshots.payload_json in the knowledge DB.
The vector_status table in the knowledge DB records embedder/vector-index
availability and embedding coverage per phase; it is the diagnostic for the
"Semantic vector search not active" troubleshooting row and is surfaced by
uv run ksi-doctor --knowledge-db <path>.
6. Knowledge Access And MCP Toolsets¶
Containers receive tools from src/ksi/memory/mcp_server.py based on the
MCP_TOOLSET selected by the TypeScript runner.
| Toolset / phase | Tools exposed | Notes |
|---|---|---|
| Normal task execution with memory enabled | none from the memory toolset | Task knowledge is pre-injected into MEMORY.md; task-time query() is intentionally not mounted. ARC tasks are separate. |
| Forum phases | query, forum_read, knowledge, forum_post, forum_signal_done |
Writes go through ForumBus; the orchestrator drains events into KnowledgeStore. |
| ARC tasks | native file tools (Bash/Read/Edit/...) | The agent reads payload.json and overwrites attempt_1.txt/attempt_2.txt (per-test attempt_<k>_<t>.txt files for multi-test tasks) with ASCII grids. No ARC-specific MCP tools exist. |
Compatibility all |
forum/knowledge tools | Used only where the runner asks for the broad toolset. |
query and forum_read remain compatibility/read tools for forum phases.
knowledge returns the unified task knowledge page. forum_post requires prior
retrieval according to the forum protocol guard, and forum_signal_done
supports early-exit tracking.
Retrieval defaults to lexical FTS5. Semantic vector search (sqlite-vec index +
embedding model) is opt-in via --require-vector, which also requires the
memory extra:
uv sync --extra memory
scripts/setup_all.sh installs this extra by default (uv sync --extra memory
--extra swebench-pro), so a machine set up through it already has
the vector dependencies present; they simply stay unused until a run passes
--require-vector. Only a minimal uv sync (without the memory extra) needs the
command above before enabling vector search.
Without --require-vector the engine writes no embeddings, so the whole
system — including the in-container forum query tool, whose read-only
knowledge DB then has no vector index — uses FTS5. With --require-vector set,
vector search is enabled and the run fails fast if sqlite-vec or the embedding
model cannot initialize (rather than silently degrading). This retrieval path
reaches the agent-facing surface: the MCP query tool's related field is
populated from KnowledgeStore.fts_search (lexical FTS5) whenever semantic
search is unavailable or a query embedding fails at runtime. Each query
response reports the mode actually used via a retrieval_mode field
(semantic / fts / none).
7. Discussion And Distillation Shapes¶
Per-task forum rows use:
entry_type='post'source_phase='per_task_forum'task_id=<task id>- optional
reply_tofor threaded replies
Cross-task forum rows use:
entry_type='post'source_phase='cross_task_forum'task_id='__cross_task__'
Distillation writes:
- per-task bundles with
source_phase='per_task_distill' - cross-task bundles with
source_phase='cross_task_distill'. Under the default (--cross-task-distill-target-conditioning true), one per-task-conditioned bundle is written per downstream seed target, each keyed by that targettask_id(no__cross_task__row). With default--drop-solved, targets are unsolved training tasks plus hold-out probes; with--no-drop-solved, retained solved tasks are included. With--cross-task-distill-target-conditioning false, a single legacy broadcast bundle is written undertask_id='__cross_task__'.
Knowledge-transfer sweeps export and inject only the cross-task bundle via
--seed-bundle-path, which takes an exported JSON bundle from a donor
knowledge database.
8. Benchmark Routing¶
| Task source | Typical evaluator | Main inputs |
|---|---|---|
arc |
arc_session |
ARC source directory plus task map |
swebench_pro |
swebench_pro |
SWE-bench Pro JSONL/CSV/parquet plus task map |
polyglot |
polyglot_harness |
Prepared Polyglot JSON |
terminal_bench_2 |
terminal_bench_2 |
Terminal-Bench 2 task-map JSON (delegates runtime) |
ARC uses native tools: the agent reads payload.json and overwrites
attempt_1.txt/attempt_2.txt (per-test attempt_<k>_<t>.txt files for
multi-test tasks) with ASCII grids, which the evaluator scores.
SWE-bench Pro and Polyglot use Docker-based evaluators after model output is
captured.
9. Provider Runtime¶
Provider configuration is loaded from configs/ksi/*.env files and
normalized by src/ksi/providers.py.
- Claude task execution defaults to the Claude Code SDK path in
agent-runner/src/index.ts, which also serves ARC natively. Scheduled forum tasks can use a direct Anthropic adapter unless configured back to the Claude Code path. - OpenAI task execution uses
@openai/agentsinagent-runner/src/openai.ts. - Host-side reflection, lesson extraction, and distillation use
src/ksi/runtime/llm.py, which wraps the Python Anthropic SDK or OpenAI Responses API according to the provider profile. - Token usage is normalized into
token_phaseswhen the runtime DB is enabled. - Native session memory capture is bounded by the
--native-memory-*limits.
The benchmark image is ksi-agent:bench. Rebuild it after material changes
under runtime_runner/agent-runner/src/ when you want the image to carry a
warm TypeScript build.
10. Egress Isolation¶
Container egress is isolated by default. Agent containers run on a
Docker --internal network (no direct internet route) and reach provider APIs
only through an allowlisting CONNECT proxy sidecar that runs on an external
bridge network.
Topology (per campaign, managed by runtime_runner/src/container_args.ts):
agent container ──[internal net, no gateway]──> proxy sidecar ──[external net]──> provider APIs
ksi-egress-int-<run-id>—--internalbridge (no default gateway; agent containers cannot initiate direct outbound connections).ksi-egress-ext-<run-id>— normal bridge; only the proxy sidecar is attached.- Proxy sidecar (
egress_proxy_main.js, compiled to/tmp/dist/in the bench image) listens on port 8080 and accepts only CONNECT tunnels to allowlisted hostnames on ports 443/80. Plain HTTP is rejected (405). - Agent containers are started with
HTTPS_PROXY/HTTP_PROXYpointing at the proxy; in-process undici clients pick this up viaruntime_runner/agent-runner/src/index.ts(ProxyAgentdispatcher). - Agent containers are started with
--dns 0.0.0.0: Docker's embedded resolver (127.0.0.11) still answers service discovery (the proxy container name) locally, but every external name lookup is forwarded to a non-routable blackhole and SERVFAILs. This closes the DNS-tunnel exfil channel that--internalalone leaves open. The agent never needs external DNS — the proxy resolves provider hostnames upstream on the agent's behalf.
Allowlist derivation (runtime_runner/src/egress_allowlist.ts):
The allowlist is derived from the provider profile at launch time:
MODEL_PROVIDER |
Default hosts |
|---|---|
anthropic (default) |
api.anthropic.com + ANTHROPIC_BASE_URL host (if set) |
openai |
api.openai.com + OPENAI_BASE_URL host (if set) |
Operator-supplied extras are appended via KSI_EGRESS_ALLOW=host1,host2
(comma-separated) — intended for Bedrock, Vertex, or other provider endpoints:
KSI_EGRESS_ALLOW=bedrock-runtime.us-east-1.amazonaws.com ...
These variables are read from the runner subprocess environment
deriveEgressAllowlist reads process.env of the TypeScript runner. The
Python CLI builds that environment from the provider profile plus a fixed
system-key list (_build_runner_env in
src/ksi/runtime/container_host.py), and neither KSI_EGRESS_ALLOW nor the
*_BASE_URL variables are on either list — so a value exported in your
shell does not currently reach the allowlist through a ksi /
scripts/run_ksi.sh run.
Escape hatch: set KSI_EGRESS=open to disable isolation and restore
the legacy direct-bridge behavior (no internal network, no proxy). Use only for
debugging — not for production campaigns.
Lifecycle: Python stamps a parent-process KSI_RUN_ID into every runner
subprocess by default, so one campaign process shares a single egress
proxy/network pair instead of creating a pair per task. The TypeScript runner
uses filesystem leases under ${TMPDIR:-/tmp}/ksi-egress-leases/ to keep the
shared infrastructure alive while sibling runner processes are active; the last
live lease tears the resources down on exit/SIGTERM. Stale resources from a
crashed session can be removed with:
docker rm -f ksi-egress-proxy-<run-id>
docker network rm ksi-egress-int-<run-id> ksi-egress-ext-<run-id>
Smoke gate: before flipping default-on in a new environment, run the
live smoke (bash benchmarks/egress_smoke.sh) to confirm (1) a
blocked host (exercism.io) is unreachable from the agent network, (2)
api.anthropic.com returns a 3-digit HTTP status through the proxy tunnel,
and (3) an external DNS lookup from the agent network SERVFAILs while the
proxy container name still resolves.
Known residuals / caveats:
- DNS-tunnel exfil is closed. Agent containers run with
--dns 0.0.0.0, so the embedded resolver can answer Docker service discovery locally but forwards every external lookup to a non-routable blackhole (SERVFAIL). A determined agent can no longer tunnel low-bandwidth data through DNS queries to an attacker-controlled nameserver. This is independent of whether a given Docker build runs its DNS forwarder host- or container-side. - Stale-network reaping is conservative. Setup scans
ksi-egress-*networks and removes only old, unattached networks whose sibling proxy is gone. Recent networks are left alone so a concurrently starting sibling is not reaped mid-setup. KSI_RUN_IDmust be unique per concurrent campaign. Run-id defaults to the parent Python process (ksi-<pid>); if you setKSI_RUN_IDexplicitly, two concurrent campaigns sharing the same value intentionally share one egress proxy/network lease and must have the same provider allowlist needs.
11. Operational References¶
- CLI flags:
uv run python -m ksi.cli --help - Run presets: benchmarks/README.md
- Startup performance notes: docs/runtime-startup-performance.md
- Egress allowlist and web-tool interaction: benchmarks/docs/web_tools_policy.md