Continues from design.md: RLM feature map, CodeAct
loop, example session/graph node, Rhai embedding plan, Python
compatibility backend, safety, events, diagnostics, testkit, and
implementation milestones.
The goal is to port the useful rlm behavior into TinyAgents without porting
Python's unsafe local execution model.
rlm feature |
TinyAgents REPL equivalent |
|---|---|
Python context variable |
Rhai context variable |
| Python persistent locals | ReplSession::variables |
fenced repl blocks |
fenced ragsh blocks |
llm_query |
model_query |
llm_query_batched |
model_query_batched |
rlm_query |
agent_query or repl_query |
rlm_query_batched |
agent_query_batched or repl_query_batched |
| custom Python tools | registered Rust tool capabilities |
| generated Python programs | .ragsh cells plus generated .rag graph blueprints |
SHOW_VARS() |
show_vars() |
answer["ready"] = True |
answer(...) |
| max iterations | ReplPolicy::max_iterations for CodeAct loops |
| max depth | graph/harness recursion policy |
| max budget | harness cost policy |
| token compaction | harness summarization feature |
| JSONL trajectory logger | typed event stream plus store backend |
| Docker/cloud REPL isolation | future PythonSandboxRepl backend |
A model-driven REPL agent has this lifecycle:
- Create
ReplSession. - Load
context,state,messages,history, andrunvariables. - Build a model request explaining the available REPL functions.
- Invoke the model through the harness.
- Extract fenced
ragshblocks from the assistant message. - Execute each block in the REPL session.
- Capture stdout, changed variables, call records, events, and errors.
- Append a compact execution result as the next user message.
- Repeat until
answer(...)is called or limits are reached. - Persist events, usage, cost, and final answer.
This loop is a harness feature. When used inside a graph node, the graph still owns node routing, checkpointing, interrupts, recursion depth, and failure policy.
If the model writes .rag source, the loop should treat it as a graph proposal.
The REPL may validate, diff, compile, and run that proposal only through the
expressive-language compiler and the graph registry policy. This is how an
agent can define its own graph without acquiring arbitrary topology mutation or
host-code execution privileges.
let lines = context.split("\n");
let candidates = [];
for line in lines {
if line.contains("SECRET_NUMBER=") {
candidates.push(line);
}
}
emit("candidates_found", #{ count: candidates.len() });
let result = model_query(#{
model: "default",
prompt: "Return only the digits from this candidate line:\n" + candidates[0]
});
answer(result);
graph support_repl {
start investigate
node investigate {
kind repl_agent
model "default"
script "support-investigation.ragsh"
tools ["lookup_user", "create_ticket"]
routes {
final -> END
needs_review -> review
}
}
node review {
kind interrupt
prompt "Approve escalation?"
routes {
approved -> END
rejected -> investigate
}
}
}
The repl_agent node is a harness-backed node template. It may execute a fixed
script, a model-driven CodeAct loop, or a combination where a fixed prologue
sets up variables before the model starts writing cells.
The Rhai runtime should be isolated behind an interface so future Python or WASM backends can reuse the same TinyAgents semantics.
#[async_trait]
pub trait ReplBackend<State, Ctx = ()>: Send {
async fn execute_cell(
&mut self,
session: &mut ReplSession<State, Ctx>,
source: SourceCell,
) -> Result<ReplResult>;
}
pub struct RhaiReplBackend {
engine: rhai::Engine,
ast_cache: AstCache,
}Rhai-specific requirements:
- configure
Engine::set_max_operations - disable or avoid unneeded packages
- register only TinyAgents capability functions
- expose data through
Dynamic, maps, and arrays with explicit conversion - compile and cache ASTs for repeated scripts
- keep each session's
Scopeseparate - restore reserved names after each cell
- truncate stdout and returned values according to policy
- convert Rhai errors into structured diagnostics with spans
Async adapter requirement:
Rhai host functions are easiest to expose as synchronous functions. TinyAgents model, tool, and graph calls are async. The backend should not hide blocking in unbounded threads. Use one of these designs:
- command recording: Rhai functions create
ReplCommandvalues, then the Rust async runtime executes those commands after the cell - blocking bridge: host functions call into a bounded runtime handle with strict timeouts
- staged syntax:
let x = model_query(...)is transformed before evaluation into host-executed calls
Recommendation for v1: use a blocking bridge only in examples and tests, but design the public API around command recording. Command recording is easier to make deterministic and safer under async graph execution.
Python should be a compatibility backend, not the default embedded runtime.
pub struct PythonSandboxReplBackend {
sandbox: SandboxClient,
}Potential use cases:
- training model behavior that already expects Python
- local research workflows
- compatibility with RLM-style prompts
- data-heavy scripts where Python libraries are explicitly useful
Requirements:
- must run out of process
- must have no direct host filesystem access by default
- must communicate through a framed JSON protocol
- must expose the same TinyAgents capability functions
- must enforce the same
ReplPolicy - must emit the same
ReplEventstream
This lets TinyAgents support Python-like RLM ergonomics without making Python a trusted in-process extension language.
Safety rules:
- no arbitrary filesystem access in the default Rhai backend
- no environment variable interpolation from scripts
- no direct network access
- no process spawning
- no unregistered native functions
- bounded script size
- bounded operation count
- bounded output size
- bounded model/tool/graph calls
- bounded recursion depth
- bounded concurrency
- typed conversion at every capability boundary
- redaction before event and store writes
The REPL is an orchestration surface, not a privilege escalation surface.
The REPL event stream should compose with graph and harness events.
pub enum ReplEvent {
SessionStarted { session_id: SessionId, run_id: RunId },
CellStarted { cell_id: CellId, source_name: String },
CellStdout { cell_id: CellId, chunk: String },
CellCompleted { cell_id: CellId, elapsed: Duration },
CellFailed { cell_id: CellId, diagnostic: Diagnostic },
VariableChanged { cell_id: CellId, name: String },
CapabilityCallStarted { cell_id: CellId, call_id: CallId, name: String },
CapabilityCallCompleted { cell_id: CellId, call_id: CallId },
GraphBlueprintDefined { cell_id: CellId, graph_name: String },
GraphBlueprintValidated { cell_id: CellId, graph_name: String },
GraphBlueprintCompiled { cell_id: CellId, graph_name: String },
GraphBlueprintRegistered { cell_id: CellId, graph_name: String },
FinalAnswer { cell_id: CellId, content: String },
SessionCompleted { session_id: SessionId },
SessionFailed { session_id: SessionId, error: String },
}When the REPL calls a model, tool, agent, or graph, the child harness/graph events should preserve:
- root run id
- parent run id
- cell id
- node id when used inside a graph
- recursion depth
- capability name
Diagnostics should preserve source spans from scripts and model-generated cells.
Required errors:
- invalid script syntax
- unknown capability
- unknown model
- unknown tool
- unknown graph
- invalid graph source
- graph compilation failed
- generated graph review required
- graph registration denied
- invalid arguments
- unsupported value type
- operation limit exceeded
- timeout exceeded
- output limit exceeded
- call limit exceeded
- recursion limit exceeded
- unsafe backend requested
- reserved name overwrite
Example:
error[E-ragsh-unknown-tool]: tool `lookup_usr` is not registered
--> support.ragsh:8:18
|
8 | let user = tool_call(#{ tool: "lookup_usr", arguments: #{ id: id } });
| ^^^^^^^^^^^^^^^^^^^^^^^^^ unknown tool
|
help: did you mean `lookup_user`?
repl::testkit should include:
- fake model capability
- fake tool capability
- fake graph capability
- deterministic event recorder
- script execution helper
- CodeAct loop helper with scripted model responses
- operation-limit assertion
- output-limit assertion
- recursive-call assertion
- batched-call ordering assertion
- golden trajectory fixtures
- add this module doc
- add
replpackage shape to the spec - define
ReplSession,ReplPolicy,ReplResult, andReplEvent - no Rhai dependency yet
- add optional
repl-rhaifeature - embed Rhai behind
ReplBackend - support persistent variables
- support
show_vars,emit, andanswer - enforce operation and output limits
- add
model_query - add
model_query_batched - add fake-model tests
- forward harness events through REPL events
- add
tool_call - add
agent_query - validate schemas and limits
- record usage and cost rollups
- add
graph_run - add
graph_define,graph_validate,graph_compile,graph_diff, andgraph_register - support graph-node
kind repl_agent - preserve node id, parent run id, and depth in child events
- require generated-graph review gates when policy enables them
- parse fenced
ragshblocks from assistant messages - execute cells iteratively
- append compact execution feedback to model history
- stop on
answer(...) - add trajectory logging and tests
- add optional out-of-process backend
- expose the same capability protocol
- run RLM-compatible Python scripts under explicit policy
- keep it disabled by default