Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/build-xtask/src/api/items-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use super::*;
#[test]
fn every_approved_crate_passes_closure_and_any_other_is_refused() {
let surface = Surface::default();
for krate in ["serde", "serde_core", "serde_json", "serde_yaml_ng"] {
for krate in ["serde", "serde_core", "serde_json"] {
let path = [krate.to_owned(), "Value".to_owned()];
assert_eq!(surface.refusal(krate, &path), None, "{krate}");
}
Expand All @@ -29,7 +29,7 @@ fn a_facade_s_own_allowed_crate_passes_closure_and_its_requirement_names_the_cra
let path = ["extra_sys".to_owned(), "Entry".to_owned()];
assert_eq!(surface.refusal("extra_sys", &path), None);
assert!(
surface.required().ends_with("serde_yaml_ng, extra_sys)"),
surface.required().ends_with("serde_json, extra_sys)"),
"{}",
surface.required()
);
Expand Down
9 changes: 4 additions & 5 deletions crates/build-xtask/src/api/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ use super::load::Loaded;
const STD_CRATES: [&str; 3] = ["std", "core", "alloc"];

/// Third-party crates the surface may mention: serde's traits are how
/// dependents serialize the surface's types, and `serde_json` and
/// `serde_yaml_ng` supply the values some of those types carry. `serde`
/// defines its traits in `serde_core` and re-exports them, and rustdoc
/// names the defining crate.
const ALLOWLIST: [&str; 4] = ["serde", "serde_core", "serde_json", "serde_yaml_ng"];
/// dependents serialize the surface's types, and `serde_json` supplies
/// the values some of those types carry. `serde` defines its traits in
/// `serde_core` and re-exports them, and rustdoc names the defining crate.
const ALLOWLIST: [&str; 3] = ["serde", "serde_core", "serde_json"];

/// What a closure mention must be, naming every crate it may come from:
/// the shared ones, then the crates in `allowed`, which one facade may
Expand Down
2 changes: 1 addition & 1 deletion crates/harness-gateway-client/src/wire/request-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ fn tool_round(offered: &[Offered]) -> Round {
let ctx = ctx
.with_tools(catalog)
.with_models(BTreeMap::from([(ModelRole::Default, model)]));
let mut run = Run::new(Arc::new(prompt), "", ctx);
let mut run = Run::new(Arc::new(prompt), "{}", ctx);
let mut rounds = Vec::new();
let result = loop {
let effects = match run.step() {
Expand Down
2 changes: 1 addition & 1 deletion crates/harness-internal/runner/src/effect_loop-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fn lost_run() -> Run {
);
let params = RunParams::new(SEED, Timestamp::UNIX_EPOCH)
.with_tools(ToolCatalog::new([wait]).expect("the one-tool catalog is valid"));
let mut run = Run::new(Arc::new(prompt.expect("the prompt parses")), "", params);
let mut run = Run::new(Arc::new(prompt.expect("the prompt parses")), "{}", params);
let Step::Pending { effects, .. } = run.step() else {
panic!("the run parks on its tool call");
};
Expand Down
76 changes: 8 additions & 68 deletions crates/harness-internal/runner/src/files.rs
Original file line number Diff line number Diff line change
@@ -1,78 +1,18 @@
//! The Harness's half of a prompt's declared store files: the launch's input
//! text staged at the frontmatter's `input:` path before a run, and the
//! `output:` path read after it.
//! The Harness's half of a prompt's declared output file: the `output:`
//! path read after a run.
//!
//! Both go through the handle's store view ([`VfsRef::acquire_store`]),
//! so the store's strict path rules apply to the declared paths, which
//! the parser takes as written, and the handle's policy and op sink see
//! each operation like any other. Both are synchronous, like the VFS, and
//! the Harness runs them inline in the run's future.
//! The read goes through the handle's store view
//! ([`VfsRef::acquire_store`]), so the store's strict path rules apply to
//! the declared path, which the parser takes as written, and the handle's
//! policy and op sink see the read like any other operation. It is
//! synchronous, like the VFS, and the Harness runs it inline in the run's
//! future.

use runvil_engine::vfs::{
Origin, VfsError, VfsOp, VfsOutcome, VfsRef, make_store_error, perform_vfs_op,
};
use runvil_engine::{Error, ErrorCode, ErrorFact};

/// Puts the prompt's declared input in place in `vfs`'s store: writes
/// `text` at `declared`, overwriting any file there, or, when the launch
/// supplied no text, checks that the store already holds the file.
///
/// # Errors
/// Returns `RequirementsUnmet` with the `Reason` fact `input_undeclared`
/// for text the prompt declares no file for, `RequirementsUnmet` with the
/// `Reason` fact `input_missing` for a declared file that is neither
/// supplied nor present, and the store failure's code when the store
/// refuses the write or the check. The last two carry the declared path
/// as the `Path` fact. The prose is the code's default.
pub fn stage_input(
vfs: &VfsRef,
declared: Option<&str>,
text: Option<String>,
) -> Result<(), Error> {
let path = match (declared, &text) {
(None, None) => return Ok(()),
(None, Some(_)) => {
return Err(make_reasoned_error(
ErrorCode::RequirementsUnmet,
"input_undeclared",
"the launch supplied input text, but the prompt declares no `input:` file",
));
}
(Some(path), _) => path,
};
let store = |source: VfsError| {
let mut error = make_store_error(&source);
error.message = format!("the input file `{path}` could not be staged: {source}");
error.facts.insert(ErrorFact::Path, path.into());
error
};
let view = vfs
.acquire_store(Origin::new(format!("input: {path}")))
.map_err(store)?;
let path = path.to_owned();
match text {
Some(contents) => perform_vfs_op(&view, VfsOp::Write { path, contents })
.map(drop)
.map_err(store),
None => match perform_vfs_op(&view, VfsOp::Exists { path: path.clone() }) {
Ok(VfsOutcome::Bool(true)) => Ok(()),
Ok(_) => {
let mut error = make_reasoned_error(
ErrorCode::RequirementsUnmet,
"input_missing",
format!(
"the prompt declares the input file `{path}`, but the launch supplied no \
input text and the store has no such file"
),
);
error.facts.insert(ErrorFact::Path, path.into());
Err(error)
}
Err(source) => Err(store(source)),
},
}
}

/// Reads the prompt's declared output file at `path` from `vfs`'s store.
///
/// # Errors
Expand Down
38 changes: 12 additions & 26 deletions crates/harness-internal/runner/src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,11 @@ impl Harness {
/// is `RecorderFailed`.
///
/// It begins the run's record, resolves the run's model through the
/// setup's broker, puts the prompt's declared input in place, makes
/// the run's Packs and waits for them to be ready, drives the run, and
/// reads the declared output file if the run completed. The run's
/// Pack instances drop before it returns. The returned future is
/// `Send`. Its only runtime needs are those of the Host's performers
/// and Packs.
/// setup's broker, makes the run's Packs and waits for them to be
/// ready, drives the run, and reads the declared output file if the
/// run completed. The run's Pack instances drop before it returns.
/// The returned future is `Send`. Its only runtime needs are those of
/// the Host's performers and Packs.
///
/// Every run gets a record and ends with its result, which the report
/// carries beside the run's id: the final text, or the error that
Expand All @@ -157,13 +156,12 @@ impl Harness {
/// calling `run`, or while the broker lists its models, ends the run
/// canceled. A model list the broker fails to fetch ends it with the
/// broker's failure, and a list that omits the selected model with a
/// `RequirementsUnmet` failure. A declared input file the Harness
/// fails to put in place ends it with the input's failure, and an
/// environment that falls short of the prompt's requirements, which
/// the run's first step finds, with a `RequirementsUnmet` failure. An
/// environment that falls short after a cancel is raised, as when the
/// cancel cuts short the wait for a Pack that is still starting, ends
/// the run canceled instead. A run that is pending with nothing to
/// `RequirementsUnmet` failure. An environment that falls short of
/// the prompt's requirements, which the run's first step finds, ends
/// it with a `RequirementsUnmet` failure too. An environment that
/// falls short after a cancel is raised, as when the cancel cuts
/// short the wait for a Pack that is still starting, ends the run
/// canceled instead. A run that is pending with nothing to
/// await, which means the Harness lost track of an effect, ends with
/// an `Internal` error. A run whose main chain waits on its inbox with
/// nothing else to run is not stalled: it waits for a
Expand Down Expand Up @@ -222,19 +220,7 @@ impl Harness {
.frontmatter()
.output()
.map(|decl| decl.path().to_owned());
let prepared = prepare(&host, prompt, &setup, run_id, params, model, cancel).await;
let (run, packs) = match prepared {
Ok(prepared) => prepared,
Err(refusal) if is_refusal(&refusal) => return Err(recorded(refusal)),
Err(error) => {
return Ok(RunReport {
run_id,
result: Err(error),
output: Err(make_not_completed_error()),
unread: control.send_queue().end(),
});
}
};
let (run, packs) = prepare(&host, prompt, &setup, params, model, cancel).await;

let (result, unread) = drive(
run,
Expand Down
27 changes: 13 additions & 14 deletions crates/harness-internal/runner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@
//! run gets on top of the Host. A [`Harness`] over the Host, the parsed
//! prompt, and the setup begins the run at the setup's recorder with the
//! inputs the Engine refuses to draw itself, resolves the run's model
//! through the setup's broker, prepares an Engine `Run` (putting the
//! declared input file in place, making the run's instance of each of its
//! Packs and taking the run's snapshot of them), steps it, performs each
//! chat, render, tool-call, and timer effect through its performer and
//! answers each Vfs effect inline, feeds the answers back, delivers each
//! message the launcher sends through the run's [`RunControl`] to the
//! main chain, hands every event, effect, and answer to the run's
//! recorder, ends the run there with its result, the final text or the
//! error that ended it, and reads the declared output file, all inside the
//! one future [`Harness::run`] returns. A cancel is the error whose code
//! is `canceled`. A recorder
//! through the setup's broker, prepares an Engine `Run` (making the run's
//! instance of each of its Packs and taking the run's snapshot of them),
//! steps it, performs each chat, render, tool-call, and timer effect
//! through its performer and answers each Vfs effect inline, feeds the
//! answers back, delivers each message the launcher sends through the
//! run's [`RunControl`] to the main chain, hands every event, effect, and
//! answer to the run's recorder, ends the run there with its result, the
//! final text or the error that ended it, and reads the declared output
//! file, all inside the one future [`Harness::run`] returns. A cancel is
//! the error whose code is `canceled`. A recorder
//! that refuses a write is the only way a run ends without a recorded
//! result, and that refusal, a `RecorderFailed` error, is the only error
//! [`Harness::run`] returns. A prompt that fails to parse is its caller's
Expand All @@ -31,9 +30,9 @@
//! Host's own runtime. The crate's async needs come from `futures-util`,
//! and `tokio` and `tokio-util` appear only under `[dev-dependencies]`
//! (enforced by the Harness tokio ban in `cargo test -p build-xtask`).
//! - The Harness reads each [`RunSetup`] field once per run: the input is
//! staged in, the run works in, and the declared output is read from
//! the one filesystem the setup holds.
//! - The Harness reads each [`RunSetup`] field once per run: the run works
//! in, and the declared output is read from, the one filesystem the
//! setup holds.
//! - The recorder is written in loop order: a step's events before the
//! step's effects are issued, each effect before its performer starts,
//! each answer before the run resumes with it, and each sent message's
Expand Down
2 changes: 1 addition & 1 deletion crates/harness-internal/runner/src/packs-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ fn unavailable_for(reason: &str) -> RosterEntry {
fn refusal(run: &RunPacks, prompt: &Prompt) -> String {
let ctx = RunParams::new(RunSeed::from_bytes([0; 32]), Timestamp::UNIX_EPOCH)
.with_packs(run.roster());
match Run::new(Arc::new(prompt.clone()), "", ctx).step() {
match Run::new(Arc::new(prompt.clone()), "{}", ctx).step() {
Step::Done {
result: Err(error), ..
} => error.located_message(),
Expand Down
65 changes: 17 additions & 48 deletions crates/harness-internal/runner/src/prepare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,32 @@
//! recorder with the start time the caller read, so the record holds both
//! verbatim. `Harness::run_to_end` begins the run before anything else, so
//! every run has a record. Then [`prepare`] takes the steps that build the
//! run: put the prompt's declared `input:` file in place in the store
//! (`files::stage_input`); hand the run's whole filesystem, real
//! directories and the declared store, to the params as given; make the
//! run's instance of each of its Packs, and take the run's snapshot of
//! them once each is ready, or the run's cancel fires, which yields the
//! catalog, the roster, the preludes, and the tool performer; set the
//! first three, the setup's limits, and the run's model on the params;
//! and build the `Run` beside that snapshot.
//! run: hand the run's whole filesystem, real directories and the
//! declared store, to the params as given; make the run's instance of
//! each of its Packs, and take the run's snapshot of them once each is
//! ready, or the run's cancel fires, which yields the catalog, the
//! roster, the preludes, and the tool performer; set the first three, the
//! setup's limits, and the run's model on the params; and build the `Run`
//! beside that snapshot.
//!
//! An input file that cannot be put in place is a run that ended before
//! it began: the recorder ends it with that failure as its result, the
//! same error `prepare` returns, so the record answers "why did this run
//! fail" for a run the loop never saw. An environment that cannot satisfy
//! the prompt fails no preparation: the run is built, and its first step
//! ends it with the Engine's model-readable notice, or canceled when the
//! run's cancel fired first, as when a cancel cuts the wait short while a
//! Preparation never fails. An environment that cannot satisfy the prompt
//! is prepared all the same: the run is built, and its first step ends it
//! with the Engine's model-readable notice, or canceled when the run's
//! cancel fired first, as when a cancel cuts the wait short while a
//! needed Pack is still starting. The effect loop records that end like
//! any other.
//!
//! The input staging is store work and runs inline, like every other VFS
//! operation of the run.

use std::path::PathBuf;
use std::sync::Arc;

use runvil_engine::Error;
use runvil_engine::cancel::CancelHandle;
use runvil_engine::model::{ModelInfo, ModelRole};
use runvil_engine::prompt::FileDecl;
use runvil_engine::{Prompt, Run, RunParams, RunSeed, Timestamp};

use crate::files::stage_input;
use crate::host::Host;
use crate::packs::RunPacks;
use crate::recorder::{RunId, RunMeta, of_run, refused};
use crate::recorder::{RunId, RunMeta};
use crate::setup::RunSetup;

/// Begins the run at `setup.recorder`: draws its seed from the OS random
Expand All @@ -64,10 +55,9 @@ pub(crate) async fn begin(
Ok((run_id, RunParams::new(seed, started_at)))
}

/// Stages `prompt`'s input in `setup.vfs`, makes the run's Packs from
/// `host.packs` and snapshots them once each is ready or `cancel` fires,
/// and builds the `Run` over `params` under `setup.limits`. Ends the run
/// at the recorder with an input failure as its result and returns it.
/// Makes the run's Packs from `host.packs` and snapshots them once each
/// is ready or `cancel` fires, and builds the `Run` over `params` under
/// `setup.limits`.
///
/// The run's `ui()` snapshot is `setup`'s selection and first workspace
/// root, and `model` fills all five roles, so the run's first step checks
Expand All @@ -78,35 +68,14 @@ pub(crate) async fn begin(
/// stands. A prompt the environment cannot satisfy is prepared all the
/// same: the run's first step ends it with the Engine's notice, or as
/// canceled when the run's cancel fired first.
///
/// # Errors
/// Returns the input failure from `stage_input` when the declared input
/// file cannot be put in place. The failure keeps its own code and gains
/// the run's `Run` fact, and the run is ended with it as its result.
/// Returns `RecorderFailed` with the `Run` fact when the recorder refuses
/// that end, and the input failure never has that code.
pub(crate) async fn prepare(
host: &Host,
prompt: Prompt,
setup: &RunSetup,
run_id: RunId,
params: RunParams,
model: Option<ModelInfo>,
cancel: &CancelHandle,
) -> Result<(Run, RunPacks), Error> {
// The declared input is in place before anything else sees the
// store: the run's first section may read it.
let declared = prompt.frontmatter().input().map(FileDecl::path);
if let Err(source) = stage_input(&setup.vfs, declared, setup.input_text.clone()) {
let error = of_run(run_id, source);
setup
.recorder
.end_run(run_id, Err(error.clone()))
.await
.map_err(|source| refused(run_id, source))?;
return Err(error);
}

) -> (Run, RunPacks) {
// The run's whole filesystem, the real directories and the declared
// store, goes to the params as given. The run makes its instance of
// each of its Packs, and the snapshot of them is taken once each is
Expand All @@ -132,7 +101,7 @@ pub(crate) async fn prepare(
);

let run = Run::new(Arc::new(prompt), &setup.args, params);
Ok((run, packs))
(run, packs)
}

/// The `ui()` snapshot as a JSON object: `selected_model`, and the first
Expand Down
Loading
Loading