SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
27.7 KB · 221 lines markdown
Rendered Raw Blame History
1<!--2KHAELOR3File: docs/research/MINI_SWE_ANALYSIS.md4Author: Simon-Pierre Boucher5Contact: contact@spboucher.ai6-->78# mini-SWE-agent — Phase 0 Analysis910> **Guiding question: HOW LITTLE AGENT SCAFFOLDING IS ACTUALLY NECESSARY?**11>12> Reference: `references/mini-swe-agent/` (version 2.4.6, per `src/minisweagent/__init__.py`).13> Built by the Princeton/Stanford SWE-bench team. README claim: **>74% on SWE-bench Verified** with this scaffolding.1415## 0. Executive answer1617The *entire* competitive core of mini-swe-agent is roughly **585 lines of Python**:1819| Component | File | Lines |20|---|---|---|21| Agent loop + config | `src/minisweagent/agents/default.py` | 190 |22| Exceptions (control flow) | `src/minisweagent/exceptions.py` | 26 |23| Local environment | `src/minisweagent/environments/local.py` | 92 |24| LiteLLM model wrapper | `src/minisweagent/models/litellm_model.py` | 164 |25| Tool-call parsing/formatting | `src/minisweagent/models/utils/actions_toolcall.py` | 113 |2627Everything else in the package (~5,350 total lines) is *alternatives and periphery*: extra model backends (OpenRouter, Portkey, Requesty, Responses API), extra environments (Docker, Singularity, bubblewrap, SWE-ReX), run scripts, benchmark harnesses, and a trajectory inspector. The project's own `AGENTS.md` states its thesis explicitly: *"The idea of this project is to write the simplest, smallest, most readable agent"* and *"The project embraces polymorphism: every individual class should be simple, but we offer alternatives."*2829The lesson is not "agents need 200 lines." The lesson is **which 200 lines are the kernel, and that everything else can be a swappable component around it** — which is exactly KHAELOR's Absolute Rule #3.3031---3233## 1. The core agent class (`agents/default.py`, 190 lines)3435### 1.1 State3637`DefaultAgent.__init__` (lines 39–50) holds the *complete* agent state — eight fields:3839```python40self.config = config_class(**kwargs)   # pydantic AgentConfig41self.messages: list[dict] = []          # the ONLY conversation state42self.model = model                      # Model protocol43self.env = env                          # Environment protocol44self.extra_template_vars = {}45self.cost = 0.046self.n_calls = 047self.n_consecutive_format_errors = 048self._start_time = time.time()49```5051There is no session object, no event bus, no context engine, no tool registry, no planner. `self.messages` — a flat list of dicts — *is* the session, the history, the context, and the persistence format simultaneously.5253`AgentConfig` (lines 19–35) is equally minimal: `system_template`, `instance_template`, `step_limit` (default 0 = unlimited), `cost_limit` (default $3.00), `wall_time_limit_seconds`, `max_consecutive_format_errors` (default 3), `output_path`.5455### 1.2 The loop5657`run()` (lines 88–124) is the fundamental loop. Reconstructed:5859```python60def run(self, task, **kwargs) -> dict:61    self.messages = []62    self.add_messages(system_msg, instance_msg)   # Jinja2-rendered templates63    while True:64        try:65            self.step()                            # step() = execute_actions(self.query())66        except FormatError as e: ...               # append corrective message, count strikes67        except InterruptAgentFlow as e: ...        # append exception's messages68        except Exception as e: ...                 # record, re-raise69        finally:70            self.save(self.config.output_path)     # trajectory saved EVERY step71        if self.messages[-1].get("role") == "exit":72            break73    return self.messages[-1].get("extra", {})74```7576`step()` is one line (line 126–128): `return self.execute_actions(self.query())`. That is the whole "query model → execute actions → return observations → repeat" cycle:7778- `query()` (lines 130–152): checks step/cost/time limits (raising `LimitsExceeded`/`TimeExceeded`), calls `self.model.query(self.messages)` — the **entire** message list, every turn — accumulates cost from real API metadata, appends the assistant message.79- `execute_actions()` (lines 154–157): `outputs = [self.env.execute(action) for action in message["extra"]["actions"]]`, then appends observation messages formatted by the model class.8081### 1.3 Exceptions as control flow — the key trick8283`exceptions.py` (26 lines) defines a tiny hierarchy rooted at `InterruptAgentFlow`, whose constructor **carries messages**: `Submitted`, `LimitsExceeded`, `TimeExceeded(LimitsExceeded)`, `UserInterruption`, `FormatError`.8485Every non-linear event — task completion, budget exhaustion, malformed model output, Ctrl-C — is expressed as *"raise an exception that carries the messages to append."* The `run()` loop catches them, appends the messages, and loops. Termination is purely data-driven: the loop exits when the last message has `role == "exit"` (line 122). No state machine, no status enums, no `is_done` flags scattered across classes.8687Consequences:8889- **Completion detection lives in the environment, not the agent.** `LocalEnvironment._check_finished` (`environments/local.py:45–56`) raises `Submitted` when a command's first output line is exactly `COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT` with returncode 0. The agent has no completion logic at all; the prompt (`config/mini.yaml`) instructs the model to `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT` when done. This is a magic-string protocol — clever, fragile, and benchmark-oriented.90- **Format violations are self-healing.** `FormatError` (raised by parsers in `models/utils/actions_toolcall.py` / `actions_text.py`) carries a templated corrective user message (`format_error_template` in `mini.yaml`, which even distinguishes `finish_reason == "length"` truncation from genuine mistakes). `run()` appends it and re-queries. `max_consecutive_format_errors` (3) converts repeated failure into a `RepeatedFormatError` exit — a strike system in ~15 lines (`default.py:100–114`). Note the subtlety at line 102: the failed call's cost is still charged.91- **Limits are checked at the top of `query()`** (lines 132–147), not in a supervisor. A second global layer, `GlobalModelStats` (`models/__init__.py:13–42`, thread-safe, env-var-driven), guards multi-agent/batch runs.9293### 1.4 Persistence9495`serialize()`/`save()` (lines 159–190) dump `{info, messages, trajectory_format}` to JSON after **every** step (`finally` block). Crash-safe trajectories for free — but this is *recording*, not *resumption*: there is no code path that loads a trajectory back into a live agent. `run/utilities/inspector.py` (316 lines, Textual) is a read-only trajectory browser.9697### 1.5 The interactive layer is a subclass, not a framework9899`agents/interactive.py` (209 lines) shows how far the small kernel stretches: `InteractiveAgent(DefaultAgent)` adds three modes (`human` / `confirm` / `yolo`), a regex `whitelist_actions` list, Ctrl-C steering (`step()` override catches `KeyboardInterrupt` and raises `UserInterruption` carrying the user's typed comment), confirm-before-exit ("agent wants to finish — type a new task or Enter to quit", `_check_for_new_task_or_submit`), and slash commands (`/y`, `/c`, `/u`, `/m`, `/h`) — all by overriding `add_messages`, `query`, `step`, and `execute_actions`. The entire "permission system" is `_should_ask_confirmation` (lines 162–163): one mode check plus one regex whitelist scan.100101---102103## 2. The environment abstraction104105The `Environment` protocol (`src/minisweagent/__init__.py:61–70`) is three methods: `execute(action, cwd) -> dict`, `get_template_vars()`, `serialize()`. Protocol-based duck typing — no base classes, no inheritance requirements.106107### 2.1 LocalEnvironment (`environments/local.py`, 92 lines)108109- **Yes, `subprocess` with `shell=True`**`_run()` (lines 72–92) uses `subprocess.Popen(command, shell=True, text=True, ..., stdout=PIPE, stderr=STDOUT, start_new_session=os.name == "posix")`.110- **stderr merged into stdout** — one output stream, one observation field.111- **Process-group kill on timeout** (default 30s): `os.killpg(process.pid, signal.SIGKILL)` so children aren't orphaned, then `TimeoutExpired` is re-raised *with the partial output attached* — the model sees what happened before the kill.112- **Every command runs in a fresh subshell.** No persistent shell session — `cd` and env vars do not survive between actions. Rather than engineering a PTY session manager, the system prompt (`config/mini.yaml:39–40`) tells the model: *"Directory or environment variable changes are not persistent... you can prefix any action with `MY_ENV_VAR=... cd /path && ...`"*. A hard engineering problem converted into one sentence of prompt.113- **Observations are plain dicts**: `{"output": str, "returncode": int, "exception_info": str}`. Exceptions during execution (including timeouts) are folded into the same dict shape with `returncode: -1` (lines 31–41) — the model always receives a uniform observation.114115### 2.2 Observation formatting is a config template, not code116117The dict is rendered into the message by a Jinja2 `observation_template` owned by the *model* config (`litellm_model.py:40–44`, overridden in `config/mini.yaml:112–128`). The production template emits JSON-ish text and performs the **only "context management" in the whole system**: outputs over 10,000 chars are truncated to `output_head` (first 5,000) + `output_tail` (last 5,000) + `elided_chars` + a warning — in the template, not in Python.118119### 2.3 Other environments120121`DockerEnvironment` (`environments/docker.py`, 161 lines): starts a long-lived container running `sleep 2h` (`_start_container`), then each action is `docker exec -w cwd <container> bash -lc <command>`. Same observation shape, same `_check_finished` magic string, `cleanup()` via `__del__`. `environments/singularity.py`, `extra/bubblewrap.py`, `extra/swerex_docker.py`, `extra/swerex_modal.py`, `extra/contree.py` are drop-in alternatives. Because the interface is one `execute()` method, adding an environment costs ~100–150 lines each.122123---124125## 3. The model abstraction126127The `Model` protocol (`__init__.py:43–58`): `query(messages) -> dict`, `format_message(**kw)`, `format_observation_messages(message, outputs, template_vars)`, `get_template_vars()`, `serialize()`.128129Notably, in v2 the **model class owns action parsing and observation formatting** — the agent never touches wire formats. The agent reads `message["extra"]["actions"]` and hands raw output dicts back to the model for formatting. This is how one 190-line agent supports both native tool-calling and text-based protocols unchanged.130131### 3.1 LitellmModel (`models/litellm_model.py`, 164 lines)132133- `litellm.completion(model=..., messages=..., tools=[BASH_TOOL], ...)`**exactly one tool** is advertised: `BASH_TOOL` (`models/utils/actions_toolcall.py:11–27`), schema = a single required `command: string`. The whole tool surface area is one function with one parameter.134- `query()` (lines 81–106): retry loop (`models/utils/retry.py`, 25 lines, tenacity-style with `abort_exceptions` including `ContextWindowExceededError` and `AuthenticationError`), cost calculated from litellm's cost calculator with a **hard failure if cost cannot be computed** (`_calculate_cost`, lines 108–126 — "never fabricate/ignore cost" as a default), parsed actions plus the full raw response stashed under `message["extra"]`.135- `_prepare_messages_for_api()` strips the internal `extra` key, reorders Anthropic thinking blocks (`models/utils/anthropic_utils.py`), and applies **prompt cache control**: `get_model()` (`models/__init__.py:56–60`) auto-enables `set_cache_control: "default_end"` whenever the model name looks like Anthropic — cache breakpoints handled in `models/utils/cache_control.py` (67 lines).136- **No streaming anywhere.** Blocking `litellm.completion`; the interactive agent shows a Rich spinner ("Waiting for the LM to respond...", `interactive.py:73`).137138### 3.2 Templating: Jinja2 + StrictUndefined everywhere139140Every string the model sees is a Jinja2 template rendered with `StrictUndefined` (typo in a variable = crash, not silent empty string): `system_template`, `instance_template`, `observation_template`, `format_error_template`. Template variables come from `DefaultAgent.get_template_vars()` (`default.py:52–64`), which `recursive_merge`s the agent config, environment vars (`LocalEnvironment.get_template_vars` includes `platform.uname()` and `os.environ`), model config, and live counters (`n_model_calls`, `model_cost`, `elapsed_seconds`). This lets `mini.yaml` do things like emit macOS-specific `sed -i ''` guidance (`{%- if system == "Darwin" -%}`, lines 69–73).141142**The prompt is the product.** `config/mini.yaml` is 151 lines — comparable in size to the agent itself — encoding the workflow (reproduce → fix → verify → submit), the subshell semantics, file-creation via heredoc, editing via `sed`, viewing via `nl | sed -n`. Behavior tuning happens in YAML, never in the loop. Run scripts compose everything: `run/mini.py` merges config specs (`-c file.yaml -c agent.mode=yolo`) and does `get_model(...)`, `get_environment(...)`, `get_agent(...)`, `agent.run(task)` — class selection is string-based dynamic import (`_MODEL_CLASS_MAPPING`, `_AGENT_MAPPING`).143144### 3.3 Text-based fallback145146`models/litellm_textbased_model.py` (48 lines) is the v1/original-SWE-agent protocol: no `tools` parameter at all; actions are extracted from the assistant text with a regex (`` r"```mswea_bash_command\s*\n(.*?)\n```" ``), exactly one action per response enforced (`parse_regex_actions`, `models/utils/actions_text.py:15–40`), observations returned as `role: "user"` messages. The docstring in `actions_text.py` notes: *"As of mini-swe-agent v2.0, we strongly recommend to use toolcalls instead"* — an empirical admission that native tool-calling APIs beat markdown-block parsing, while proving the agent loop is indifferent to which protocol is used.147148---149150## 4. What mini-swe-agent deliberately OMITS — and the consequences151152| Omission | Detail | Consequence |153|---|---|---|154| **Rich tool set** | One tool: `bash`. No read/write/edit/grep/glob. Editing is `sed`/heredocs per the prompt. | Works for strong models on benchmarks; brutal for interactive use — a one-character `sed` mistake silently corrupts files, no diffs, no ambiguity detection, no atomic writes. This is the single largest gap for a daily driver. |155| **Context compaction** | None. `self.messages` grows monotonically; full history sent every call. `ContextWindowExceededError` is an *abort* exception (`litellm_model.py:54`) — hitting the window kills the run. | Fine for bounded benchmark episodes (~$3 cost cap doubles as an implicit context cap). Disqualifying for long interactive sessions. |156| **Session resume** | Trajectories saved every step, but write-only. No resume/branch/rewind; `inspector.py` is read-only. | Every invocation starts from scratch. Unacceptable for a daily tool. |157| **Streaming** | Blocking completion calls; spinner while waiting. | Terrible perceived latency for interactive use; irrelevant for benchmarks. |158| **Persistent shell / process manager** | Fresh subshell per command, 30s timeout, no background processes. Dev servers, watchers, REPLs are impossible. | Prompt-level workaround for cwd/env; no workaround exists for long-running processes. |159| **Permission engine** | `confirm`/`yolo`/`human` mode + regex whitelist (`interactive.py:162–163`). No capability model, no persistence of grants, no scoping. | Adequate for a single trusted user in a sandbox; not for a product where "always allow in this project" must persist. |160| **Repository intelligence** | Zero. The model runs `ls`, `grep`, `find` itself. | Elegant (no index to maintain, never stale) but wasteful: repeated discovery burns tokens and turns; nothing survives between sessions. |161| **Event bus** | None. The message list *is* the event log. UI = `add_messages` override printing via Rich. | Works only because there is one linear consumer. A real TUI (status line, collapsible tool calls, diff viewer, parallel processes) needs typed events. |162| **Parallel/linear history** | Strictly linear; one action stream; no subagents. | Simple to reason about and serialize; caps throughput and precludes delegation. |163164The trade summarized: **mini-swe-agent optimizes for benchmark performance per line of code and for research legibility** (trivially hackable, trivially reproducible, environment-swappable for SWE-bench containers). Nearly every omission that is free on a benchmark is a first-order product defect in a daily-driver terminal agent.165166## 5. Accidental vs. essential complexity — subsystem by subsystem167168For each subsystem larger agents (Claude Code, OpenCode, Hermes, OpenHands) carry, is mini-swe-agent's *absence* of it viable for KHAELOR?169170- **Tool registry — absence NOT viable, but the lesson holds.** mini proves the *count* of tools in big agents is largely accidental (one tool scores >74%), but bash-only is viable only because SWE-bench never shows a human the intermediate states. KHAELOR needs `read/write/edit/grep/glob` for observability (diffs, headers, permission classification), safety, and token efficiency — CLAUDE.md's ~7 primitives is the right calibration. Adopt mini's registry *shape*: a static, tiny declaration like `BASH_TOOL`, not a plugin framework.171- **Permission system — absence NOT viable.** But mini shows the *evaluator* can be small: mode + whitelist is ~10 lines. KHAELOR's capability model (`file.write.project`, etc.) is essential product complexity (persistence, scoping, elegant UI); a rules-engine DSL would be accidental complexity.172- **Context engine — absence NOT viable, and this is mini's clearest cliff.** Growing a flat list until the window explodes is only survivable under a $3 cap. However, mini shows the *first 80%* of context management is trivial: head/tail truncation in an observation template. KHAELOR's compaction/checkpointing is essential; what would be accidental is *retrieval-heavy context stuffing* — mini demonstrates models navigate repositories themselves very well when given a good shell.173- **TUI — absence NOT viable for KHAELOR by definition (Absolute Rule #2).** mini's Rich-print interface is honest about being a research harness. But its interaction *grammar* — Ctrl-C becomes a steering message, "agent wants to finish" becomes a prompt for the next task, mode switching mid-run — is excellent and cheap, and KHAELOR should preserve exactly those semantics under a real TUI.174- **Session persistence — absence (of resume) NOT viable.** But save-full-state-every-step-in-a-`finally` is the right *durability* discipline: KHAELOR's event journal should be crash-safe at every loop boundary, exactly like `default.py:120–121`.175- **Event bus — absence viable only without streaming.** The moment streaming exists (KHAELOR §7 makes it non-negotiable), messages-as-the-only-log stops working. Essential for KHAELOR; the mini lesson is to keep the *kernel's* view simple (kernel appends to history; the bus is a service that observes it).176- **Subagents / planners / multi-agent orchestration — absence VIABLE for V1.** mini plus a strong model plans in-context and scores >74% with no planner, no critic, no orchestration graph. Strong evidence this whole layer is accidental complexity at current model capability. Matches KHAELOR's "NOT V1" list.177- **Memory / skills — absence VIABLE for V1.** Same evidence, same conclusion.178- **Model abstraction — mini's is *larger* than KHAELOR needs.** Ten model classes + litellm exist because mini is a research harness for arbitrary models. Anthropic-only KHAELOR needs one `ModelClient` — but should copy three specifics: automatic prompt cache control (`cache_control.py`), cost from real API metadata with loud failure when unknown (`_calculate_cost`), and `finish_reason`-aware format-error messages.179180---181182## WHAT MINI-SWE-AGENT PROVES1831841. **A competitive agent loop is ~150 lines.** `DefaultAgent` — eight fields of state, `run()`/`step()`/`query()`/`execute_actions()` — scores >74% on SWE-bench Verified. Any kernel larger than a few hundred lines is carrying non-kernel work.1852. **The model is the intelligence; scaffolding is plumbing.** No planner, no self-reflection, no orchestration, no retrieval — one bash tool and a good prompt. Capability lives in the LLM; scaffolding's job is faithful transport of actions and observations.1863. **Three interfaces suffice to decouple everything**: `Model.query(messages)`, `Environment.execute(action)`, and the agent between them (the `Protocol`s in `__init__.py` are ~30 lines). Swapping local↔Docker↔Singularity or toolcall↔regex protocols requires zero kernel changes.1874. **Exceptions-carrying-messages is a remarkably clean control-flow pattern.** Completion, limits, format errors, and user interruption are all "raise with the messages to append"; the loop stays four branches and termination is data-driven (`role == "exit"`).1885. **Format errors are conversation, not crashes.** Feed the parse error back as a message with a strike counter; the model self-corrects. ~15 lines replace an entire "robust output parsing" subsystem.1896. **Budgets are trivial and non-negotiable**: step, cost, and wall-clock checks at the top of `query()`, cost from real API metadata, hard failure when cost is unknowable. Ten lines buy the most important safety property an autonomous agent has.1907. **Prompts and templates absorb enormous complexity.** Non-persistent subshells, output truncation, OS-specific editing advice, submission protocol — all handled in `mini.yaml` Jinja2, not code. Config-as-behavior keeps the loop frozen while behavior iterates.1918. **Durability by default is cheap**: serialize full state in a `finally` every iteration.1929. **What it proves negatively:** the exact omissions that keep it small — no streaming, no compaction, no resume, no rich tools, no processes — are precisely what separates a benchmark harness from a daily driver. Minimalism of the *kernel* generalizes; minimalism of the *product* does not.193194## WHAT KHAELOR'S KERNEL SHOULD PRESERVE FROM THIS MINIMALISM195196- **The four-verb loop, verbatim in spirit.** KHAELOR's `AgentKernel` should be recognizably `while active: query → parse → execute → observe`, readable in one screen. Everything in CLAUDE.md §4's conceptual loop already maps 1:1 onto `default.py`; keep it that way.197- **Tiny, enumerable kernel state.** mini needs eight fields; KHAELOR's kernel should need barely more (history handle, model, tool runtime, budget counters, run status). If a field isn't consulted by the loop itself, it belongs to a service.198- **Kernel talks to interfaces only**: `ModelClient`, `ToolRuntime`, `Workspace` — the TypeScript analogue of mini's three Protocols. The kernel must never know Anthropic wire formats, tool schemas, or filesystem details (mini's agent never parses tool calls — the model layer does).199- **Structured interrupts as control flow.** Port the `InterruptAgentFlow` pattern: completion, budget exhaustion, cancellation, and steering injection are typed signals carrying the events to record, handled in one place in the loop — not booleans threaded through call stacks. (In TS: typed control-flow results/exceptions caught at the loop boundary.)200- **Format-violation recovery as messages + strike limit.** Feed tool-call errors back to the model with `finish_reason` awareness; cap consecutive failures.201- **Budget checks at the top of every query**, with real usage metadata only (Absolute Rule #4) and a global as well as per-session layer.202- **Journal state at every loop boundary** (`finally`-style) so crashes never lose a session — mini's save discipline applied to KHAELOR's event log.203- **Behavior in templates/config, not in the loop.** System prompt, observation rendering, error phrasing, truncation thresholds → configurable data, so the kernel is frozen while behavior iterates.204- **No planner, no subagent framework, no memory system in V1.** mini is the existence proof that these are unnecessary for strong coding performance today; KHAELOR's "NOT V1" list is validated.205- **Trust the model with the repository.** Grep/glob/bash as honest primitives beat premature indexing; ship repository *intelligence* as a context-engine service later, never as kernel logic.206207## WHERE KHAELOR MUST DIVERGE (AND WHY)2082091. **Streaming is the substrate, not an add-on.** mini blocks on `litellm.completion` and shows a spinner — acceptable when nobody watches. KHAELOR's terminal-native identity (Absolute Rule #2, CLAUDE.md §7) requires `stream(request): AsyncIterable<ModelEvent>` from day one; the kernel consumes an event stream and the TUI renders deltas. This is the single biggest structural divergence.2102. **A typed event bus instead of "the message list is the log."** mini's single linear consumer (Rich prints in `add_messages`) cannot drive a status line, collapsible tool calls, diff viewers, cost display, and a journal simultaneously. KHAELOR: kernel emits typed events; history, TUI, and persistence are *subscribers* (services around the kernel — the mini philosophy, one level up).2113. **First-class file tools with diffs.** Bash-only editing via `sed`/heredocs is unobservable, unreviewable, and unsafe outside a disposable container. KHAELOR ships `read/write/edit/grep/glob` as structured tools so every mutation yields a diff, a permission classification, and a header check. Keep the *set* mini-small (≤8 primitives, one-parameter-simple schemas like `BASH_TOOL`).2124. **A real process manager.** Fresh-subshell-per-command with a 30s kill makes dev servers and watchers impossible. KHAELOR's `bash` vs `process` split (CLAUDE.md §10) is mandatory; adopt mini's process-group-kill hygiene (`local.py:_run`) inside it.2135. **Context engine with compaction.** mini treats `ContextWindowExceededError` as fatal and resends full history every call. KHAELOR sessions are long-lived; budget awareness, checkpoint summaries, and cache-friendly message layout are essential. Steal mini's cheap trick (template-level head/tail truncation of tool output) as the first line of defense.2146. **Persistent, resumable sessions.** Keep mini's every-step durability, add what it lacks: load, resume, replay from the event journal (`/sessions`, `/resume`).2157. **Explicit completion, not a magic string.** `COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT` sniffed from stdout (`local.py:45–56`) is spoofable by any command output and encodes completion in the prompt–environment contract. KHAELOR: completion is a structured signal validated against `CompletionEvidence` (CLAUDE.md §17) — the kernel verifies before it believes.2168. **A capability permission engine with persistence and elegant UI.** mini's confirm/yolo/whitelist is the right *size* of evaluator but the wrong *model* for a product: KHAELOR needs `file.write.project`-style capabilities, remembered grants, and the inline permission panel — while preserving mini's property that a rejection becomes a steering message to the model (`_ask_confirmation_or_interrupt``UserRejection`).2179. **Anthropic-native model layer, not a router.** Drop the ten-backend polymorphism (that's mini's research mission, not KHAELOR's); keep the three valuable behaviors buried in it: automatic prompt cache control, cost from real usage metadata with loud failure, and truncation-aware error recovery.21810. **Interruption and steering as first-class concurrency.** mini's Ctrl-C-to-comment is the right UX seed, but it works only because everything is synchronous. With streaming + background processes, KHAELOR needs cancellation that propagates through model stream → tools → processes without corrupting the session (CLAUDE.md §14), plus queued steering — a genuinely new design, not an extension of mini's.219220**Bottom line:** mini-swe-agent proves KHAELOR's kernel can — and therefore must — stay under a few hundred lines with three narrow interfaces and exception-style control flow. Everything KHAELOR adds beyond that (streaming, events, rich tools, processes, compaction, sessions, permissions, TUI) is justified product complexity — and every piece of it must live in services *around* that kernel, or we will have learned nothing from the smallest agent that works.221