A turn, end to end
A turn is one top-level evaluation against a persistent Shell, and every
host starts it through one synchronous, runtime-agnostic door —
Shell::run_turn(TurnRequest) -> TurnReport in
core/src/driver.rs. The request’s Turn
carries a Program sum naming what runs: source text, or a registered hook
applied to first-order arguments — a Block/Lambda the host stored by name
in the session-lived hook table (Shell::register_hook), so the host conveys
data, never closures, across the dispatch boundary. No host reimplements
evaluation; each is a request supplier that hands the door a TurnRequest and
renders the flat TurnReport its own way
(unify-turn-evaluation,
run-turn-host-loop). Completion is the
door returning — never a channel disconnecting — so a detached spawned
worker (a server, a watch) holding a clone of the surface cannot keep a turn
from ending (run-turn-host-loop). The
reduction primitive behind the door is crate-private, so a host cannot start an
unframed evaluation against a stale frame
(run-turn-is-host-api).
The request carries the policy axes as types, never flags. A TurnRequest
is exactly the places hosts differ:
- IO — a
TurnIoregime sum over byte output.Inheritruns on the session’s ambient streams (cloned, then restored);Capturehas core mint fresh stdout/stderr buffers it returns inTurnReport::Ran’scaptured. - Stdin — a
TurnStdinchoice, orthogonal to the output regime:Inheritreads the session’s fd 0 (terminal, pipe, or file),Emptyinstalls an immediate-EOF source with no fall-through. - Terminal — a
RequestedTerminalAccess:Leasedmay foreground a terminal-bound child,Deniedmay not.Captureno longer implies terminal ownership — a pipedral -cisDeniedyetInherits its stdin (terminal-lease). - Capabilities — the
Capabilitiesceiling pushed for the turn’s dynamic extent (root()for the REPL, a grant profile for exarch). - Limits —
turn_limit(the foreground wall) anddetached_limit(the lifetime ceiling for workers the turn detaches at the durable root). - Surface — an optional turn-local
SurfaceSink(Arc<dyn EventSink>), installed only for this turn;Noneis the identity. - Lifecycle — optional pre/post-exec hooks (
Box::new(())for a host with none).
The spine the door orchestrates is one straight line, owning resources
(Sink, Source, TurnState, guards, buffers) while the request describes
policy. run_turn dispatches on the Program sum, and the arms differ only in
how the program resolves — the source arm compiles first, the hook arm looks
up the hook table; both then converge on run_built and the turn module’s
framed scaffold:
- Mint the turn’s foreground scope (a child of the shell’s durable root, so a
foreground timeout never reaches a detached worker —
cancellation) and arm its wall, if any, before
compiling, so
turn_limitbounds the whole turn — compile and typecheck included, not only evaluation.compile_turn’sprocess::cleartouches only the signal count, never the reaper, so the armed ceiling survives the compile. - The source arm’s
compile_turnrunscompile_and_typecheck(src, shell.session_schemes())seeded from the live session (the ladder; session-scheme-continuity). A parse or type failure returnsTurnReport::Static { diagnostics }at once — no turn state, no root context, no hooks; the host renders the diagnostics and treats it as status 1. The hook arm skips this: its program is an already-compiled value resolved by name in the hook table, and the hook’s registeredDefaultPolicy(capture, terminal authority, budget) folds into the turn’s conditions — the hook’s to decide, not the dispatching host’s. run_builtmaterialises the IO regime —Capturemints the buffers it reads back,Inheritleaves the ambient streams to flow — thenbuild_turnassembles theTurnState, the whole turn-local part of aShellin one field, seeded from the ambient session. The turn-localsurfaceanddetached_ceilingare seated on it here; the surface has no liveness role, so a clone of it can never define turn completion.run_framedinstalls that state through aTurnGuardand evaluates. The guard swaps the new frame ontoshell.turnand publishes the foreground and durable-root scopes into the signal-reachable slots. It is RAII: it restores the prior frame onDrop, so teardown survives a caught worker panic. The root context is installed, the pre-exec hook fires, andwith_capabilities(caps, body)runs the turn’s program under the request’s capability ceiling —eval_top_level(&comp, s)for the source arm, the in-framebuiltins::applyof the resolved hook for the hook arm (the machine; unify-turn-evaluation).eval_top_levelinstalls the post-runMobileon every outcome, so alet,cd, or env change persists to the next turn — the turn is a resume point regardless of completion, error, orexit(turn-ends-ready).run_framedcomputes the transport status, fires the post-exec hook, and the guard drops.- Back in
run_built, the wall is disarmed before the cause is read, so a reaper tripping in the gap between eval returning and classification cannot misread a turn that finished inside its budget as timed out.timed_outis thentrueonly for aDeadlinecause that genuinely elapsed; the captured bytes (if any) are drained, and everything flattens intoTurnReport::Ran { result, status, single_command, captured, timed_out }, carrying theSettled<Value>for the host to render.
The hosts differ only in the request they supply.
- The REPL’s
execute_input(ral/src/repl/exec.rs) suppliesscript_name: "<stdin>",Capabilities::root(), no limits,TurnIo::Inherit,RequestedTerminalAccess::Leased,TurnStdin::Inherit, no surface, and thepre-exec/chpwd/post-execplugin hooks; it callsrun_turnwith aProgram::Sourceturn synchronously on its prompt thread and renders withprint_result. Its plugin hooks and prompt body (ral/src/repl/plugin.rs,prompt.rs) dispatchProgram::Hookturns instead — hooks the REPL registered by name, run through the same frame. - exarch’s
run_shell(exarch/src/shell_eval.rs) suppliesscript_name: "<tool>", its session grant profile, a per-toolturn_limitand a 1 hdetached_limit,TurnIo::Capture,RequestedTerminalAccess::Denied,TurnStdin::Empty, and anAgentSinksurface that decodes eachValueonto its presentation bus; it callsrun_turnwith aProgram::Sourceturn and renders the cappedToolResult. The pushed grant frame is the sandbox — ral’s grant, not a source-levelgrant { … }the model could escape — which is why exarch needs no runtime of its own (exarch-architecture). - ral’s batch path (
ral/src/main.rs) suppliesTurnIo::Inherit, no surface, and a()lifecycle, withRequestedTerminalAccesskeyed to whether it owns the terminal — the third source-turn client ofrun_turn, closing the one entry unify-turn-evaluation flagged.
The human and the model are interchangeable suppliers of top-level turns over
one persistent Shell.
See also compilation-ladder, evaluator-machine, cancellation; run-turn-host-loop, run-turn-is-host-api, unify-turn-evaluation; maps repl, exarch.