Session 9 changes driven by live trace analysis:
- CodeAct event pipeline: handle_execute_code_step now transfers
CodeExecutionResult events to thread.events and broadcasts via event_tx
(fixes false-positive no_tools_used trace warnings)
- Monty globals()/locals() builtins: returns dict of available action names
from capability leases, enabling "tool_name" in globals() probing
- PlatformInfo injection into system prompts (version, LLM backend, model,
database, channels, owner, repo URL)
- Mission goal prompts moved to prompts/*.md files (include_str! pattern)
- /expected command for triggering self-improvement from user feedback
- Session 9 development history
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Skill selection was in Rust (SkillSelector in loop_engine.rs) — now it's
in the Python orchestrator where the self-improvement mission can evolve it.
Rust provides data access via two new host functions:
- __list_skills__() — loads DocType::Skill MemoryDocs from Store
- __record_skill_usage__(doc_id, success) — confidence tracking
Python orchestrator handles everything else:
- score_skill() — keyword/tag/confidence scoring (~40 lines)
- select_skills() — budget-aware top-N selection (~15 lines)
- format_skills() — XML block injection into system prompt (~20 lines)
- Injection at step 0 with active_skill_ids stored in state
Removed from Rust:
- SkillSelector field + builder on ExecutionLoop and ThreadManager
- format_skills_section() from prompt.rs
- Rust-side skill injection block in loop_engine.rs
- SkillSelector wiring in bridge/router.rs
E2E test updated: skills stored in TestStore, Python orchestrator
finds them via __list_skills__() and injects based on goal keywords.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Three major changes to the v2 engine:
1. **Consolidated action execution** — `handle_execute_action` in Rust is now
the single source of truth for lease lookup, policy check, lease consumption,
action execution, event emission, and ActionResult message recording. The
Python orchestrator no longer duplicates event/message logic. This fixes the
empty call_id bug (OpenAI HTTP 400) and the missing tool_calls on assistant
messages (Codex "No tool call found" error).
2. **Removed reflection system** — Deleted the per-thread reflection pipeline
(pipeline.rs, executor.rs), ThreadState::Reflecting, ThreadType::Reflection,
enable_reflection config, and all 3 reflection event kinds. Learning is now
handled entirely by event-driven missions that fire selectively.
3. **Three learning missions** replace reflection:
- `self-improvement` — fires on trace issues (error diagnosis, prompt fixes)
- `playbook-extraction` — fires on successful 5+ step threads (reusable procedures)
- `conversation-insights` — fires every 5 threads per project (user preferences,
domain knowledge, workflow patterns)
Additional fixes:
- llm_query()/llm_query_batched() always include system message (Codex compat)
- handle_llm_complete adds assistant message with structured action_calls for
Tier 0 responses (prevents "No tool call found" errors)
- Gateway broadcasts without thread_id emit as Status events instead of being dropped
- Comprehensive tests for call_id propagation and trace analysis (17 new tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Wire engine v2 into the full submission pipeline and expose threads,
projects, and missions through the web gateway REST API.
Bridge routing — route ExecApproval, Interrupt, NewThread, and Clear
submissions to engine v2 when ENGINE_V2=true. Previously only UserInput
and ApprovalResponse were handled; all other control commands fell
through to disconnected v1 sessions.
Bridge query layer — add 11 read-only query functions and 6 DTO types
so gateway handlers can inspect engine state (threads, steps, events,
projects, missions) without direct access to the EngineState singleton.
Gateway endpoints — new /api/engine/* routes:
GET /threads, /threads/{id}, /threads/{id}/steps, /threads/{id}/events
GET /projects, /projects/{id}
GET /missions, /missions/{id}
POST /missions/{id}/fire, /missions/{id}/pause, /missions/{id}/resume
SSE events — add ThreadStateChanged, ChildThreadSpawned, and
MissionThreadSpawned AppEvent variants. Expand the bridge event mapper
to forward StateChanged and ChildSpawned engine events to the browser.
Engine crate — add ConversationManager::clear_conversation() for /new
and /clear commands.
Code quality — replace 10 .expect() calls with proper error returns,
remove dead AgentConfig.engine_v2 field, log silent init errors, fix
duplicate doc comment, improve fallthrough documentation.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add version lifecycle for the Python orchestrator:
- Failure tracking via MemoryDoc (orchestrator:failures)
- Auto-rollback: after 3 consecutive failures, skip the latest version
and fall back to previous (or compiled-in v0)
- Success resets the failure counter
- OrchestratorRollback event for observability
Update self-improvement Mission goal with Level 1.5 instructions for
orchestrator patches — the agent can now modify the execution loop
itself via memory_write with versioned orchestrator docs.
12 new tests: version selection (highest wins), rollback after failures,
rollback to default, failure counting/resetting, outcome parsing for
all 5 ThreadOutcome variants.
189 tests pass, zero clippy warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Increment step_count and track tokens in __emit_event__("step_completed")
so thread bookkeeping matches the old Rust loop behavior
- Remove double-counting of tokens in bootstrap (orchestrator handles it)
- Match nudge text to existing TOOL_INTENT_NUDGE constant
- Fix FINAL result propagation (use stored final_result, not VM return)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Replace the 900-line Rust execution loop with a ~80-line bootstrap
that loads and runs the versioned Python orchestrator via Monty VM.
The orchestrator Python code (orchestrator/default.py) is the v0
compiled-in version. Runtime versions can override it via MemoryDoc
storage (orchestrator:main with tag orchestrator_code).
Key fixes during switchover:
- Use ExtFunctionResult::NotFound for unknown functions so Monty
falls through to Python-defined functions (extract_final, etc.)
- Move helper function definitions above run_loop for Monty scoping
- Use FINAL result value (not VM return value) in Complete handler
- Rename 'final' variable to 'final_answer' to avoid Python keyword
Status: 171/177 tests pass. 6 remaining failures are step_count and
token tracking bookkeeping — the orchestrator manages these internally
but doesn't yet update the thread's counters via host functions.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Wire the self-improvement loop as a Mission with OnSystemEvent cadence,
inspired by karpathy/autoresearch's program.md approach. The mission
fires when threads complete with issues, receives trace data as trigger
payload, and uses tools directly to diagnose and fix problems.
Key changes:
Engine self-improvement (Phase A+B from design doc):
- Add fire_on_system_event() to MissionManager for OnSystemEvent cadence
- Add start_event_listener() that subscribes to thread events and fires
matching missions when non-Mission threads complete with trace issues
- Add ensure_self_improvement_mission() with autoresearch-style goal
prompt (concrete loop steps, not vague instructions)
- Add process_self_improvement_output() for structured JSON fallback
- Seed fix pattern database with 8 known patterns from debugging
- Runtime prompt overlay via MemoryDoc (build_codeact_system_prompt now
async + Store-aware, appends learned rules from prompt_overlay docs)
- Pass Store to ExecutionLoop for overlay loading
Bridge review fixes (P1/P2):
- Scope engine v2 SSE events to requesting user (broadcast_for_user)
- Per-user pending approvals via HashMap instead of global Option
- Reset tool-call limit counter before each thread execution
- Only persist auto-approval when user chose "always", not one-off "yes"
- Remove dead store/mission_manager fields from EngineState
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Adds a complete approval flow that mirrors v1 behavior, using the
existing v1 security controls (Tool::requires_approval, auto-approve
sets, StatusUpdate::ApprovalNeeded).
## How it works
### Step 1: Tool blocked at execution
When the LLM's code calls a tool (e.g., `shell("ls")`):
1. EffectBridgeAdapter.execute_action() looks up the Tool object
2. Calls tool.requires_approval(¶ms) — returns ApprovalRequirement
3. If Always → EngineError::LeaseDenied (always blocks)
4. If UnlessAutoApproved → checks auto_approved HashSet → if not in set,
returns EngineError::LeaseDenied
5. If Never → proceeds to execution
### Step 2: Engine returns NeedApproval
The LeaseDenied error propagates through:
- CodeAct path: becomes Python RuntimeError, code halts, thread returns
NeedApproval with action_name + parameters
- Structured path: same via ActionResult.is_error
### Step 3: Router stores pending approval
- PendingApproval { action_name, original_content } stored on EngineState
- StatusUpdate::ApprovalNeeded sent to channel (shows approval card in
CLI/web with tool name, parameters, yes/always/no buttons)
- Returns text: "Tool 'shell' requires approval. Reply yes/always/no."
### Step 4: User responds
handle_message() intercepts Submission::ApprovalResponse when ENGINE_V2:
- 'yes' → auto_approve_tool(name) on EffectBridgeAdapter, re-processes
original message (tool now passes the approval check on second run)
- 'always' → same + logs for session persistence
- 'no' → returns "Denied: tool was not executed."
### Key design choice
Instead of pausing/resuming mid-execution (which needs engine changes
to freeze/restore the Monty VM state), we auto-approve the tool and
re-run the full message. The EffectBridgeAdapter's auto_approved set
persists across runs, so the second execution passes immediately.
This trades one extra LLM call for zero engine modifications.
## Files changed
- src/bridge/router.rs: PendingApproval struct, handle_approval(),
NeedApproval → StatusUpdate::ApprovalNeeded conversion
- src/bridge/mod.rs: export handle_approval
- src/agent/agent_loop.rs: intercept ApprovalResponse for engine v2
- src/bridge/effect_adapter.rs: fmt fixes
151 tests passing, clippy + fmt clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Panic: 'byte index 80 is not a char boundary; it is inside ''' when
tool output contained multi-byte UTF-8 characters (smart quotes from
web search results).
Fixed 4 unsafe byte-index slices:
- thread.rs:281: message preview &content[..80] → chars().take(80)
- loop_engine.rs:556: tool output &str[..4000] → chars().take(4000)
- loop_engine.rs:579: output tail &str[len-8000..] → chars().skip()
- scripting.rs:82: stdout tail &str[len-N..] → chars().skip()
All now use .chars().take() or .chars().skip() which respect character
boundaries. Follows CLAUDE.md rule: "Never use byte-index slicing on
user-supplied or external strings."
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
When code fails with NameError/UnboundLocalError (model trying to
access variables from a previous step), the error output now includes:
[HINT] Variables don't persist between code blocks. Use the `state`
dict to access data from previous steps. Available keys: ["web_search",
"last_return"]
This teaches the model to use `state["web_search"]` instead of `result`
after a NameError, reducing wasted steps from 3-4 to 1.
Also integrates RetrievalEngine into context building and ThreadManager:
- build_step_context() now accepts optional RetrievalEngine to inject
relevant memory docs (Lessons, Specs, Playbooks) into LLM context
- RetrievalEngine uses keyword matching with doc-type priority scoring
- Memory docs from reflection (Phase 4) now feed back into future threads
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Monty creates a fresh runtime per code step, so variables are lost
between steps. This caused the model to re-paste tool results from
system messages, wasting tokens.
Fix: maintain a `persisted_state` JSON dict in the ExecutionLoop that
accumulates across steps:
- Tool results stored by tool name: state["web_search"] = {results...}
- Return values stored: state["last_return"], state["step_0_return"]
- Injected as a `state` Python variable in each new MontyRun
Now the model can do:
Step 1: results = web_search(query="...") # tool result saved in state
Step 2: data = state["web_search"] # access previous result
summary = llm_query("summarize", str(data))
FINAL(summary)
System prompt updated to document the `state` variable.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The LLM was ignoring tool results and answering from training data
because the compact output metadata didn't include what tools returned.
Tool results lived only as ActionResult messages (role: Tool) which
some providers flatten or the model ignores.
Now the code step output includes:
- stdout from Python print() statements
- [tool_name result] with the actual output (truncated to 4K per tool)
- [tool_name error] for failed tools
- [return] for the code's return value
- Total output truncated to 8K chars to prevent context bloat
This ensures the model sees web_search results, API responses, etc.
in the next iteration and can reason about them instead of hallucinating.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Engine v2 now shows live progress in the CLI (and any channel):
- "Thinking..." when a step starts
- Tool name + success/error when actions execute
- "Processing results..." when a step completes
Implementation:
- ThreadManager holds a broadcast::Sender<ThreadEvent> (capacity 256)
- ExecutionLoop.emit_event() writes to thread.events AND broadcasts
- ThreadManager.subscribe_events() returns a receiver
- Router uses tokio::select! to listen for events while waiting for
thread completion, forwarding them as StatusUpdate to the channel
This replaces the polling approach with zero-latency event streaming.
Agent.channels visibility widened to pub(crate) for bridge access.
102 tests passing, zero clippy warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Models sometimes write FINAL() outside code blocks — as plain text
after an explanation. The Hyperliquid case: model outputs a long
analysis then FINAL("""...""") at the end, not inside ```repl fences.
Fixes:
- extract_final_from_text(): regex-based FINAL detection in text
responses, matching the official RLM's find_final_answer() fallback
- Handles: double-quoted, single-quoted, triple-quoted, unquoted,
nested parens
- Checked in LlmResponse::Text handler BEFORE tool intent nudge
(FINAL takes priority)
9 new tests:
- codeact_final_in_text_response: FINAL("answer") in plain text
- codeact_final_triple_quoted_in_text: FINAL("""multi\nline""") in text
- final_double_quoted, final_single_quoted, final_triple_quoted,
final_unquoted, final_with_nested_parens, final_after_long_text,
no_final_returns_none
102 tests passing (93 + 9 new), zero clippy warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Comprehensive test coverage for the Monty Python execution path:
- codeact_simple_final: Python code calls FINAL('answer') → thread completes
- codeact_tool_call_then_final: code calls test_tool() → FunctionCall
suspends VM → MockEffects returns result → code resumes → FINAL()
- codeact_pure_python_computation: sum([1,2,3,4,5]) → FINAL('Sum is 15')
with no tool calls — pure Python in Monty
- codeact_multi_step: first step prints output (no FINAL), second step
sees output metadata and calls FINAL — tests iterative REPL flow
- codeact_error_recovery: first step has NameError → error flows to LLM
as stdout → second step recovers with FINAL — tests error transparency
- codeact_context_variables_available: code accesses `goal` and `context`
variables injected by the RLM context builder
- codeact_multiple_tool_calls_in_loop: for loop calls test_tool() 3 times
→ 3 FunctionCall suspensions → all results collected → FINAL
- codeact_llm_query_recursive: code calls llm_query('prompt') → VM
suspends → MockLlm provides sub-agent response → result returned as
Python string variable
93 tests passing (85 prior + 8 new), zero clippy warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The engine now operates in CodeAct/RLM mode:
System prompt (executor/prompt.rs):
- Instructs LLM to write Python in ```repl fenced blocks
- Documents available tools as callable Python functions
- Documents llm_query(), llm_query_batched(), FINAL()
- Documents context variables (context, goal, step_number, previous_results)
- Strategy guidance: examine context, break into steps, use tools, call FINAL()
Code block detection (bridge/llm_adapter.rs):
- extract_code_block() scans LLM text responses for ```repl or ```python blocks
- When detected, returns LlmResponse::Code instead of LlmResponse::Text
- The ExecutionLoop routes Code responses through Monty for execution
No structured tool definitions sent to LLM:
- Tools are described in the system prompt as Python functions
- The LLM call sends empty actions array, forcing text-mode responses
- This ensures the LLM writes code blocks (CodeAct) instead of
structured tool calls (which would bypass the REPL)
85 tests passing, zero clippy warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The ExecutionLoop was sending empty messages to the LLM because the
thread was spawned with the user's input as the goal but no messages.
Fixes:
- ThreadManager.spawn_thread() now adds the goal as an initial user
message before starting the execution loop
- ExecutionLoop.run() injects a default system prompt if none exists
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Cross-referenced our implementation against the official RLM (alexzhang13/rlm),
fast-rlm (avbiswas/fast-rlm), and Prime Intellect's verifiers implementation.
Key enhancements:
- FINAL(answer) / FINAL_VAR(name): explicit termination pattern matching
all three reference implementations. Code can signal completion at any
point, not just via return value.
- llm_query_batched(prompts): parallel recursive sub-calls via tokio::spawn,
matching fast-rlm's asyncio.gather pattern and Prime Intellect's llm_batch.
- Output truncation increased to 8000 chars (from 120), matching Prime
Intellect's 8192 default. Shows [TRUNCATED: last N chars] or [FULL OUTPUT].
- Step 0 orientation preamble: auto-injects context metadata (message count,
total chars, goal, last user message preview) before first code step,
matching fast-rlm's auto-print pattern.
- Error-to-LLM flow: Python parse errors, runtime errors, NameErrors,
OS errors, and async errors now flow back as stdout content instead of
terminating the step, enabling LLM self-correction on next iteration.
Only VM panics (catch_unwind) terminate as EngineError.
74 tests passing, zero clippy warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>