Program into something that executes: the host supplies bindings through a RuntimeEnv, Interpreter::load checks everything up front, and Interpreter::run evaluates the program on an input map. This page lists the full surface, plus include_program! for compiling a .dsrs file into a Rust binary.
RuntimeEnv
What the host supplies at load: live models, host tool and hole bindings, the sandbox, and the capability grants. Secrets never travel in the artifact; model clients are bound here from host-held keys and env vars.
Interpreter::load
Loading front-loads every check; nothing is lazy and nothing waits for call time. The checks run in this order:
Program::validate(the structural graph rules).program.capsmust be a subset ofenv.grants(no ambient authority).- Every model must be bindable: pre-bound by name, or client-constructible from its config.
- Every
ToolKind::Hosttool name must be bound. - Every sandboxed tool and hole is registered through the full sandbox lifecycle (parse, compile, register). A hole that does not compile fails the load, not the call.
LoadError variants
Interpreter::run
Evaluates the program on an input map, reading parameters through an optional overlay (never mutating the program) and metering spend against a budget.
inputis a JSON object of the program’s input fields. It is checked against the program’s external signature: a missing required field or a type mismatch isRunError::Input.overlayisOption<Arc<Overlay>>. When present, itsbasemust equal the program’s hash or the run fails withRunError::Overlaybefore anything executes.budgetcaps spend for this run.
Interpreter::run_collecting
run_collecting(input, overlay, budget) is run with per-leaf metadata: it returns a RunOutput — the same output map plus one LeafOutcome per successful Predict-leaf evaluation, in execution order (ForkJoin branches append in declared branch order). This is the seam Predict<S> uses to reassemble CallMetadata when it executes through the interpreter.
Each LeafOutcome carries: name (the program-unique leaf name, the trace span component), raw_response, field_meta (per-field jsonish coercion flags and #[check] results, keyed by canonical field name), usage, model_config_hash, span_id (when a capture scope was active), and — for AgentLoop leaves — tool_calls and tool_executions. Scope rules: Predict and AgentLoop leaves report, Hole leaves do not; only successful evaluations report (a failed Retry attempt leaves no outcome, the succeeding one reports); an agent whose final output came from stop-tool args has empty field_meta; a replay-served leaf reports the recorded raw text and usage with empty field_meta.
Interpreter::run_conversation
run_conversation(chat, input, overlay, budget) is the conversation-in/conversation-out entry: one turn with the program’s single leaf over a caller-owned Chat, returning (RunOutput, Chat) — the turn’s output and metadata plus the extended conversation. It exists only for single-leaf programs (one predict or agent node, what Predict<S> compiles to); a multi-node graph is refused with RunError::Input.
chat/input combinations: an empty chat with Some(input) renders the opening turn (system + demos + the formatted input, identical to conversation_opening); a non-empty chat with None is sent as-is; a non-empty chat with Some(input) appends the formatted input as the next user turn. A turn is not a run: each call records one trace span (seq increments per turn) and meters against its own budget. On an agent leaf the turn runs the full tool loop, dispatching tool calls through their bound executors. Replay works turn by turn — a span keys on the full chat sent, so a recorded conversation serves each turn with tool effects baked in.
conversation_opening(&input, overlay) renders the opening Chat without calling anything: the same overlay-resolved system + demos (+ agent playbook) + input rendering a run would send. Use it to inspect or edit the first turn before run_conversation. Predict::build_chat/call_and_parse are thin wrappers over these two entries.
Caller-managed tool loops
run_conversation_caller_managed(chat, input, overlay, budget) is the same turn in suspending mode: when the model requests tool calls on an agent leaf, the loop suspends instead of dispatching and returns ConversationTurn::Suspended(ToolSuspension). Execute the calls yourself and feed the results back:
ToolSuspension::calls() is the pending calls in request order; ToolSuspension::chat() is the conversation so far, including the assistant tool-call turn. resume_conversation records one ToolRun event per result (metering the time the suspension was outstanding), pushes one batched tool-result user turn, and continues the loop under the same meters and turn cursor — trace spans, budget metering, and stop-tool semantics are identical to dispatching mode. A stop-tool call completes the turn instead of suspending; a replay scope never suspends (served turns carry every tool effect); Code Mode does not apply, since the caller executes the tools. Dropping a suspension without resuming closes its span as Cancelled. Feed a failed tool’s error text as its result to keep the conversational-repair behavior of dispatching mode.
Budget
Run-level spend limits; None means unlimited. Budget::default() and Budget::unlimited() are the same: no limits.
An
AgentLoop’s per-node budget chains a child meter under the run meter, so node spend also counts against the run.
RunError variants
RunError::retryable() is true only for Lm, Parse, Tool, and Hole; those are the errors Retry and Refine may intercept. Budget and CapabilityDenied are never retried.
Ambient overlays
These two functions let#[module] executable functions pick up a candidate without threading it through every call.
with_ambient_overlay(overlay, fut)runs a future with anArc<Overlay>as the ambient candidate for every#[module]function called on that task. Scoping is task-local: spawned subtasks do not inherit it, and nesting replaces the outer scope. The overlay’sbaseis checked byInterpreter::runagainst each module’s program, not here, so one scope can span calls into several modules and only the matching one accepts it.current_overlay()returns the ambient overlay (Option<Arc<Overlay>>) if awith_ambient_overlayscope is active on this task.#[module]-generated functions read it immediately beforeInterpreter::run.
default_lm
The globally configured LM, used when a module does not name a model. default_lm() returns Option<Arc<LM>>: the model set through configure(...) in the global settings, or None when nothing was configured. Generated #[module] environments use it to bind the default model ref at load.
Embedding programs with include_program!
include_program! compiles a .dsrs file into your binary:
qa.dsrs becomes mod qa). The path is resolved relative to your crate’s Cargo.toml directory. The generated module contains:
qa::SOURCE: the embedded text.qa::program(): the parsed, validated program (panics on a bad file).qa::try_program(): the same, but returns aResult.- A generated test, so
cargo testfails if the file ever becomes invalid.
dsrs-syntax — the shared .dsrs lexer and structural grammar both the macro and the full parser read from — so a malformed file breaks your build. Semantics (types, dataflow, capability rules) are checked by the full parser at first use of program(), and forced at CI time by the generated test — the sqlx-offline analogue. This is the shipping path for programs with host tools or host holes, which dsrs serve cannot bind: embed the file, bind your implementations with bind_host_tool and bind_host_hole, and serve from your own binary.
See also
- Program and nodes: the
Programvalue the runtime loads, and the Overlay API - The .dsrs file: the text format
include_program!embeds - CLI:
dsrs serve, the hosted version of this load-and-run path - Code Mode: the
run_jssurfacewith_code_modeenables - Capabilities: grants, caps ceilings, and
CapabilityDenied
