The turn entry is the host API; frame and outcome mirrors collapse
Shell::run_turn(src, TurnRequest) -> TurnReport is the only host-facing
evaluation seam. TurnFrame, IoFrame, TurnOutcome, and public eval_turn
disappear as host-visible names; core materialises a request directly into the
existing TurnState/Io machinery and returns one report. The split that
remains is semantic: hosts describe policy (TurnRequest, TurnIo,
SurfaceSink, lifecycle hooks), while core owns resources (Sink, Source,
TurnState, guards, buffers, signal slots). A second vocabulary is allowed only
when it enforces a different invariant. A mirror type is deleted.
Context
run-turn-host-loop fixes the daemon-task hang by making turn completion explicit: the turn call returns, or the host’s worker sends a done value. That decision introduces the right host seam, but it also raises a naming question: should there be public request/report types and internal frame/outcome types with nearly the same shape?
The current source exposes the intermediate layer. ral_core publicly re-exports
IoFrame, TurnFrame, TurnOutcome, and eval_turn; the REPL and exarch import
those names and assemble frames themselves. That was the useful bridge for
unify-turn-evaluation, but it is not
the final seam. If it remains public after run_turn lands, the repository has
two host APIs: the clean one and the old one.
The cutover rule is simple: policy crosses the host boundary; materialisation stays in core.
Decision
1. The public turn API is one layer
Hosts may name these turn types:
impl Shell {
pub fn run_turn(&mut self, src: &str, req: TurnRequest<'_>) -> TurnReport;
}
pub struct TurnRequest<'a> {
pub script_name: &'a str,
pub caps: Capabilities,
pub turn_limit: Option<Duration>,
pub detached_limit: Option<Duration>,
pub io: TurnIo,
pub surface: Option<SurfaceSink>,
pub lifecycle: Box<dyn TurnLifecycle + 'a>,
}
pub enum TurnIo { Inherit, Capture }
pub enum TurnReport {
Static { diagnostics: StaticDiagnostics },
Ran {
result: Settled<Value>,
status: i32,
single_command: bool,
captured: Option<Captured>,
timed_out: bool,
},
}SurfaceSink, EventSink, TurnLifecycle, Captured, and
StaticDiagnostics are part of the same public seam because hosts supply or
render them. Sink, Source, Io, TurnState, signal slots, guard types,
arm_lifetime, and evaluation helpers are not.
2. TurnFrame and IoFrame collapse
TurnRequest replaces TurnFrame as the host policy object. Core consumes the
request and builds the installed turn directly:
TurnIo::Inheritclones the ambientshell.turn.iostreams.TurnIo::Capturemints private stdout/stderr buffers and remembers them in a privateCaptureBuffersholder for the finalTurnReport.surfaceis installed on the newTurnStatefor same-thread children; the detached path receives bounded deferred storage instead.- foreground scope, detached ceiling, source cursor, and lifecycle hooks are
installed by
run_turn, not by the host.
No public IoFrame remains. No long-lived internal IoFrame is needed either:
TurnIo is intent, Io is the materialised resource. A small private helper
struct is permitted only if it names resources (MaterializedIo,
CaptureBuffers, TurnInstall), not if it mirrors the host request.
3. TurnOutcome collapses into TurnReport
TurnOutcome exists today because eval_turn stops before capture and timeout
classification. After run_turn owns frame construction, capture buffers, and
limit arming, that distinction stops carrying its own invariant.
run_turn returns TurnReport directly:
- parse/type failure becomes
TurnReport::Static; - runtime completion becomes
TurnReport::Ran; status,single_command,captured, andtimed_outare computed once, in the same function that owns the relevant state.
Internal helpers may return ordinary Result/tuples while factoring the code,
but there is no named core TurnOutcome type. The exarch session::TurnOutcome
for provider-message outcomes is a different layer and remains outside this
cutover.
4. eval_turn stops being a host function
eval_turn is inlined into run_turn or made pub(crate) under a name that
does not read as a host API, such as eval_compiled_turn. Tests that need the
real host seam call run_turn; tests that need guard mechanics live in core and
use private helpers.
core/src/lib.rs no longer re-exports TurnFrame, IoFrame, TurnOutcome, or
eval_turn.
Implementation plan
- Add the public seam in
core/src/host.rs:TurnRequest,TurnIo,TurnReport,Captured,EventSink,SurfaceSink, andShell::run_turn. - Move compile/typecheck, frame install, lifecycle, eval, status classification,
timeout classification, and capture draining behind
run_turn. - Delete
IoFrameandTurnFrame; replace their construction sites with private materialisation helpers that returnIo,CaptureBuffers, and the installedTurnStateguard. - Delete core
TurnOutcome; returnTurnReportfromrun_turnand adapt tests to match it. - Stop exporting
eval_turn; make any remaining evaluator helperpub(crate). - Migrate
ral/src/repl/exec.rs,ral/src/main.rs, andexarch/src/shell_eval.rsto buildTurnRequestand renderTurnReport. - Add grep guards: hosts must not name
TurnFrame,IoFrame,eval_turn,arm_lifetime, or coreTurnOutcome;ral_coremust not nametokio,spawn_blocking,mpsc, orselect.
Consequences
- There is one host API, not a clean API beside a legacy frame API.
- Capture and timeout classification live beside the state that proves them.
shell_eval.rs,repl/exec.rs, andmain.rsbecome request builders and report renderers.TurnStateremains the only installed dynamic frame;Ioremains the only materialised byte-resource bundle.TurnIois small because it is intent.Iois rich because it owns resources.- The name
TurnOutcomeis freed fromral_core, reducing confusion with exarch’s provider-levelsession::TurnOutcome.
Alternatives considered
- Keep
TurnFrame/IoFrameas private internal mirrors. Rejected as the default: private is better than public, but a mirror still invites drift. Use private resource holders only when they name a real resource or guard. - Keep
TurnOutcomeinternal. Rejected unless a future helper proves it owns an invariant different fromTurnReport. Today it is just the report before the fieldsrun_turncan now fill. - Expose materialised IO to hosts. Rejected: it leaks
Sink/Sourceand reopens the old frame-construction API under another name.
See also run-turn-host-loop, after-turn-api-simplifications (the follow-on cleanup draft), unify-turn-evaluation, host-embedding-api, and no-core-repr-leak-into-exarch.