Three new docs for contributors: - engine-v2-architecture.md: Two-layer architecture (Rust kernel + Python orchestrator), five primitives, execution model with nested Monty VMs, bridge layer, memory/reflection, missions, capabilities - self-improvement.md: Three improvement levels (prompt/orchestrator/ config/code), autoresearch-inspired Mission loop, versioned orchestrator with auto-rollback, fix pattern database, safety model - development-history.md: Summary of 6 Claude Code sessions that built the system, key design decisions and debugging moments, architecture evolution from 900-line Rust loop to Python orchestrator Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
12 KiB
Engine v2 Architecture
This document describes the IronClaw Engine v2 architecture for new contributors. It covers the execution model, the Python orchestrator, the bridge layer, and how everything fits together.
Overview
IronClaw Engine v2 replaces ~10 fragmented abstractions (Session, Job, Routine, Channel, Tool, Skill, Hook, Observer, Extension, LoopDelegate) with a unified model built on 5 primitives. The engine lives in crates/ironclaw_engine/ as a standalone crate with no dependency on the main ironclaw crate.
The key architectural innovation: the execution loop is Python code running inside the Monty interpreter, not Rust. Rust provides the infrastructure (LLM calls, tool execution, safety, persistence). Python provides the orchestration (tool dispatch, output formatting, state management). This makes the glue layer self-modifiable at runtime by the self-improvement Mission.
Five Primitives
| Primitive | Purpose | Replaces |
|---|---|---|
| Thread | Unit of work with lifecycle, parent-child tree, capability leases | Session + Job + Routine + Sub-agent |
| Step | Unit of execution (one LLM call + its action executions) | Agentic loop iteration + tool calls |
| Capability | Unit of effect (actions + knowledge + policies) | Tool + Skill + Hook + Extension |
| MemoryDoc | Unit of durable knowledge (summaries, lessons, playbooks) | Workspace memory blobs |
| Project | Unit of context (scopes memory, threads, missions) | Flat workspace namespace |
Execution Model
The Two-Layer Architecture
Rust Layer (stable kernel — rarely changes)
├── LlmBackend trait → make LLM API calls
├── EffectExecutor trait → run tools with safety/policy/hooks
├── Store trait → persist threads, steps, events, docs
├── LeaseManager → grant/check/consume/revoke capability leases
├── PolicyEngine → deterministic allow/deny/require-approval
├── ThreadManager → spawn, stop, inject messages, join threads
├── Monty VM → embedded Python interpreter
└── Safety layer → sanitization, leak detection, policy enforcement
Python Layer (self-modifiable orchestrator — where bugs get fixed)
├── The step loop → call LLM → handle response → repeat
├── Tool dispatch → name resolution, alias mapping
├── Output formatting → truncation, context assembly
├── State management → persisted_state dict across code steps
├── FINAL() extraction → parse termination signals from text
├── Tool intent nudging → detect when LLM describes instead of acts
└── Doc injection → format memory docs for context
How It Works
-
Bootstrap (
ExecutionLoop::run()inloop_engine.rs, ~80 lines):- Transition thread to Running state
- Inject CodeAct system prompt (with runtime prompt overlay if available)
- Load versioned Python orchestrator from Store (or compiled-in default)
- Execute orchestrator via Monty VM
- Map return value to
ThreadOutcome - Persist final state
-
Orchestrator (
orchestrator/default.py, ~230 lines):- Calls host functions to interact with Rust infrastructure
- Runs the step loop: check signals → check budget → call LLM → handle response
- For text responses: extract FINAL(), check nudge, or complete
- For code responses: run user code in nested Monty VM, format output
- For action calls: execute each action, handle approval flow
- Returns outcome dict:
{outcome, response, error, ...}
-
Host functions (Rust, called via Monty's suspension mechanism):
__llm_complete__→ callLlmBackend::complete()__execute_code_step__→ run user CodeAct code in a nested Monty VM__execute_action__→ execute a tool with lease + policy + safety__check_signals__→ poll for stop/inject signals__emit_event__→ broadcast ThreadEvent + record in thread__add_message__→ append message to thread history__save_checkpoint__→ persist state to thread metadata__transition_to__→ validated thread state transition__retrieve_docs__→ query memory docs from Store__check_budget__→ remaining tokens/time/USD__get_actions__→ available tool definitions from leases
Nested Execution (CodeAct)
When the LLM responds with Python code, the orchestrator calls __execute_code_step__(code, state). This suspends the orchestrator VM and creates a second Monty VM for the user's code:
Orchestrator VM (Monty #1)
→ calls __execute_code_step__(code, state)
→ suspends
→ Rust creates Monty #2 (user code VM)
→ User code calls web_search() → suspends → Rust executes tool → resumes
→ User code calls FINAL("answer") → terminates
→ Rust collects results
→ Orchestrator VM resumes with results dict
→ Orchestrator formats output, decides next step
This is the same mechanism as rlm_query() (recursive sub-agent). Each VM owns its own heap — no shared state, no locks.
Thread State Machine
Created → Running → Waiting → Running (resume)
→ Suspended → Running (resume)
→ Completed → Reflecting → Done
→ Failed
Terminal states: Done, Failed. Validated by ThreadState::can_transition_to().
Bridge Layer (src/bridge/)
The bridge connects the engine to existing IronClaw infrastructure:
| Adapter | Wraps | Purpose |
|---|---|---|
LlmBridgeAdapter |
LlmProvider |
Converts ThreadMessage ↔ ChatMessage, depth-based model routing, code block detection |
EffectBridgeAdapter |
ToolRegistry + SafetyLayer |
Tool execution with all v1 security controls, name normalization (underscore ↔ hyphen), rate limiting |
HybridStore |
Workspace |
In-memory for ephemeral data, workspace files for MemoryDocs |
EngineRouter |
Agent |
Routes messages through engine when ENGINE_V2=true, manages SSE events |
Enabling Engine v2
Set ENGINE_V2=true environment variable. The router in src/bridge/router.rs intercepts messages and routes them through the engine instead of the v1 agent loop.
For trace debugging: ENGINE_V2_TRACE=1 writes full JSON traces to engine_trace_*.json.
Memory and Reflection
MemoryDoc Types
| Type | Purpose | Produced By |
|---|---|---|
Summary |
What a thread accomplished | Reflection (always) |
Lesson |
Durable learning from experience | Reflection (on errors) |
Playbook |
Reusable multi-step procedure | Reflection (on success with 2+ tools) |
Issue |
Detected problem for follow-up | Reflection (on failure) |
Spec |
Missing capability request | Reflection (on "not found" errors) |
Note |
Working memory / scratch | Self-improvement, orchestrator code |
Reflection Pipeline
After a thread completes with enable_reflection: true:
- Trace analysis (non-LLM, always runs) — detects 8 issue categories
- LLM reflection — spawns a Reflection-type CodeAct thread with read-only tools
- Doc production — creates Summary, Lesson, Issue, Spec, Playbook docs
- Persistence — saves docs to Store (HybridStore → workspace files)
- Event firing — if issues detected, fires OnSystemEvent missions (self-improvement)
Context Injection
On each LLM call, build_step_context() retrieves up to 5 relevant MemoryDocs from the project and appends them to the system prompt as "## Prior Knowledge". This gives the LLM access to lessons, playbooks, and known issues from prior threads.
Missions
Missions are long-running goals that spawn threads over time. They replace v1 Routines.
Mission
├── goal: "Increase test coverage to 80%"
├── cadence: Cron("0 9 * * *") | OnSystemEvent | Manual | Webhook
├── current_focus: "Write tests for auth module" (evolves)
├── approach_history: ["Analyzed codebase", "Added 15 tests for db"]
├── thread_history: [thread_1, thread_2, ...]
└── max_threads_per_day: 10
How Missions Fire
- Cron: Background ticker checks every 60s, fires missions with past
next_fire_at - OnSystemEvent: Event listener subscribes to ThreadManager events, fires matching missions when threads complete with issues
- Manual:
mission_fire(id)from CodeAct or API - Webhook: Bridge routes incoming webhooks to matching missions
Meta-Prompt Generation
When a mission fires, build_meta_prompt() assembles:
- Mission goal + success criteria
- Current focus (what to work on next)
- Approach history (what was tried and what happened)
- Project knowledge (relevant MemoryDocs)
- Trigger payload (event data, trace issues)
The thread runs with this context and returns: what it accomplished, what to focus on next, whether the goal is achieved. process_mission_outcome() extracts these and updates the mission.
Capability System
Leases
Threads don't have static permissions. They receive leases — scoped, time-limited, use-limited grants:
CapabilityLease {
thread_id,
capability_name,
granted_actions: ["web_search", "read_file", ...],
expires_at: Option<DateTime>,
max_uses: Option<u32>,
revoked: bool,
}
Policy Engine
The PolicyEngine evaluates actions against leases deterministically:
- Check global denied effects (e.g., deny all Financial)
- Check capability-level policies (per-action rules)
- Check action's
requires_approvalflag - Check effect types against lease grant
Decision priority: Deny > RequireApproval > Allow
Effect Types
Every action declares its side effects:
ReadLocal, ReadExternal, WriteLocal, WriteExternal,
CredentialedNetwork, Compute, Financial
Key Files
| File | Purpose |
|---|---|
crates/ironclaw_engine/orchestrator/default.py |
The Python execution loop (v0) |
crates/ironclaw_engine/src/executor/orchestrator.rs |
Host functions + versioning + loading |
crates/ironclaw_engine/src/executor/loop_engine.rs |
Bootstrap (loads + runs orchestrator) |
crates/ironclaw_engine/src/executor/scripting.rs |
Monty VM integration, user code execution |
crates/ironclaw_engine/src/runtime/manager.rs |
ThreadManager (spawn, stop, join, reflection) |
crates/ironclaw_engine/src/runtime/mission.rs |
MissionManager (lifecycle, firing, self-improvement) |
crates/ironclaw_engine/src/types/ |
All core data structures |
crates/ironclaw_engine/src/traits/ |
LlmBackend, Store, EffectExecutor |
src/bridge/router.rs |
Engine v2 entry point from main crate |
src/bridge/effect_adapter.rs |
Tool execution bridge with safety |
src/bridge/llm_adapter.rs |
LLM provider bridge |
src/bridge/store_adapter.rs |
HybridStore (in-memory + workspace) |
Testing
cargo check -p ironclaw_engine # compiles
cargo clippy -p ironclaw_engine --all-targets -- -D warnings # zero warnings
cargo test -p ironclaw_engine # 189 tests
cargo clippy --all --all-features # full crate
cargo test # full suite
Design Influences
- RLM paper (arXiv:2512.24601) — context as variable, FINAL() termination, recursive sub-calls
- karpathy/autoresearch — the self-improvement loop as a program.md, fixed-budget evaluation, git as state machine
- Official RLM impl (alexzhang13/rlm) — 30 max iterations, compaction at 85%, budget inheritance
- fast-rlm (avbiswas/fast-rlm) — Step 0 orientation, parallel sub-calls, dual model routing
See also: docs/plans/2026-03-20-engine-v2-architecture.md for the full 8-phase roadmap.