Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8d07b70
Add the model_only_call flag to ToolInfo
vinniefalco Oct 31, 2026
4f547a6
Add the closed set of five model roles
vinniefalco Oct 31, 2026
1264c6d
Fill all five model roles from the caller's role map
vinniefalco Oct 31, 2026
36ebaa6
Share one immutable model set and start on the default role
vinniefalco Oct 31, 2026
354eb6a
Add models.current and let models.use take a handle
vinniefalco Oct 31, 2026
64bffeb
Limit models: front matter to min_context and thinking
vinniefalco Oct 31, 2026
c6fb8a1
Let each chain own its model and pass it to children
vinniefalco Oct 31, 2026
7075dfc
Add the model-first form of tasks.fanout
vinniefalco Oct 31, 2026
d4a6e3f
Close plan: built-in-model-roles
vinniefalco Nov 1, 2026
6cfd4fc
Make model handles pure inspectable values
vinniefalco Nov 1, 2026
f3563cd
Move the run's start checks into the scheduler's first step
vinniefalco Nov 1, 2026
793ea31
Check run requirements in the start gate
vinniefalco Nov 1, 2026
fabc8a7
Use one RunOutcome type in the Engine and the Harness
vinniefalco Nov 1, 2026
cc14e34
Record every Harness run with one outcome
vinniefalco Nov 1, 2026
affdaaf
Withdraw every outstanding effect when the run is decided
vinniefalco Nov 1, 2026
d75a0e8
Delete facade surface that no production code uses
vinniefalco Nov 1, 2026
303da28
Replace the id and catalog errors with one `IdError`
vinniefalco Nov 1, 2026
70a4ef1
Reduce `RunErrorKind` from 12 kinds to 10
vinniefalco Nov 1, 2026
7d84ced
Split `RunState` into `SharedState` and `ChainState`
vinniefalco Nov 1, 2026
60bccf1
Rename `RunContext` to `RunConfig`
vinniefalco Nov 1, 2026
3527ad1
Drop `cancel` from the `run` module's method list
vinniefalco Nov 1, 2026
cf572c0
Tidy up vibe/ directory
vinniefalco Nov 1, 2026
4cec10f
Loosen the source layout rule in AGENTS.md
vinniefalco Nov 1, 2026
07c1cbe
Add ranged file reads to the VFS access handle
vinniefalco Nov 1, 2026
bea6670
Add shared std file-tool code and a real Read
vinniefalco Nov 1, 2026
316a76f
Add the std glob matcher and a real ListDir
vinniefalco Nov 1, 2026
f747290
Add a real Grep and a real-directory test
vinniefalco Nov 1, 2026
94e6a17
Close plan: std-read-listdir-grep
vinniefalco Nov 1, 2026
331a293
Show mount parents in the VFS and add a demo mount
vinniefalco Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Seven terms have exactly one meaning each, everywhere in this repository: code c
- No Engine test sleeps, reads the wall clock, waits with a timeout, or draws entropy from the OS.
- Pseudo-random test input comes only from a fixed seed that the failure prints.
- Temporary directory names come from the test's name, the process id, and a counter.
- Source directories are flat by default. A subdirectory of source files must contain at least three files, and its count includes the files in its nested subdirectories; one or two files belong beside the parent module as `foo-bar.rs` (parent stem, dash, kebab label), wired with an explicit path attribute so the module name stays clean: `#[path = "foo-bar.rs"] mod bar;`. The two forms are convertible in both directions: when a `foo-*.rs` sibling group grows to three files, rehydrate it into a `foo/` subdirectory in standard module layout (`foo/bar.rs` beside `foo.rs`) and drop the path attributes; when a subdirectory shrinks below three files, flatten it back to kebab siblings. Both conversions apply at each level of nesting. Apply whichever conversion applies when you touch files in a group on the wrong side of the line. Top-level `tests/` and `benches/` trees are exempt; they follow Cargo target conventions.
- A module with more than one file keeps its files in a directory named after it, in the standard module layout (`foo.rs` beside `foo/bar.rs`). A directory may hold any number of files, one included, and a module's unit tests are one of them (`foo/tests.rs`). Never move files between a flat sibling form and a directory because of how many files a group holds, and never change a group's layout only to bring it in line with this rule. Existing `foo-bar.rs` siblings wired with `#[path = "foo-bar.rs"] mod bar;` stay as they are, and new code adds no path attribute for layout. Top-level `tests/` and `benches/` trees follow Cargo target conventions.

## SPA and CSS Rules

Expand Down
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.

8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,10 @@ This is [`prompts/greet.md`](prompts/greet.md), one of the sample prompts in thi
name: greet
description: Greet the named input using a Lua-computed value
runvil: 0
models:
writer: {}
---

# Greet

```lua
models.default("writer")
```

Computes a greeting from the input in Lua, substitutes it into the prose, and
has the model echo it.

Expand All @@ -52,7 +46,7 @@ return models.infer(prose)
```
````

The frontmatter declares one model role, `writer`, and the Lua under the title makes it the default. Given the input `Ada`, the first block in `## Main` builds `Hello, Ada!`, the placeholder puts it into the prose, and `models.infer(prose)` sends that prose to the model. The model's reply is the run's result. The paragraph under the title never reaches the model, because no Lua reads it, so it works as a comment.
The prompt declares no models, so `models.infer(prose)` runs on the Host's default model. Given the input `Ada`, the first block in `## Main` builds `Hello, Ada!`, the placeholder puts it into the prose, and `models.infer(prose)` sends that prose to the model. The model's reply is the run's result. A prompt can pick another of the five roles (`default`, `strong`, `small`, `fast`, `creative`) with `models.use(role)`, and the Host decides which model fills each role. The paragraph under the title never reaches the model, because no Lua reads it, so it works as a comment.

## How it fits together

Expand Down
3 changes: 1 addition & 2 deletions crates/harness-gateway-client/src/failure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,13 @@ mod tests {
}

#[test]
fn a_decoded_parts_refusal_is_a_retryable_malformed_reply_with_its_specific() {
fn a_decoded_parts_refusal_is_a_malformed_reply_with_its_specific() {
use runvil::model::ToolCall;
let refusal = ToolCall::from_parts(" ", "fetch", serde_json::json!({}))
.expect_err("a blank id is refused");
assert_eq!(refusal.kind(), CompletionErrorKind::InvalidParts);
let reply = decoded(refusal);
assert_eq!(reply.kind(), CompletionErrorKind::MalformedResponse);
assert!(reply.is_retryable());
assert_eq!(
reply.to_string(),
"the model backend sent a reply that could not be understood: tool call id was blank"
Expand Down
4 changes: 0 additions & 4 deletions crates/harness-gateway-client/src/transport/tests/limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ async fn complete_on_a_disabled_client_is_an_unavailable_error() {
err.to_string(),
"model access is turned off or not configured"
);
assert!(!err.is_retryable());
}

#[tokio::test]
Expand Down Expand Up @@ -138,7 +137,6 @@ async fn a_request_past_the_timeout_is_a_timeout_failure() {
CompletionErrorKind::Timeout,
"the timeout must be recognizable: {err:?}"
);
assert!(err.is_retryable());
}

/// Reads one request through its body, so answering and closing never
Expand Down Expand Up @@ -270,7 +268,6 @@ async fn a_stream_that_stalls_after_the_headers_is_a_timeout_failure() {
CompletionErrorKind::Timeout,
"the timeout must be recognizable: {err:?}"
);
assert!(err.is_retryable());
}

#[tokio::test]
Expand Down Expand Up @@ -337,7 +334,6 @@ async fn a_refused_connection_is_a_transport_failure() {
err.to_string(),
"the connection to the model backend failed"
);
assert!(err.is_retryable());
assert!(std::error::Error::source(&err).is_some());
}

Expand Down
13 changes: 9 additions & 4 deletions crates/harness-gateway-client/src/wire/classify-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,15 @@ fn each_http_kind_carries_its_fixed_phrase_and_the_status_suffix() {
}

#[test]
fn a_429_is_retryable_now() {
assert!(classify_http_failure(429, "slow down").is_retryable());
assert!(!classify_http_failure(429, "insufficient_quota").is_retryable());
fn a_429_is_rate_limited_unless_its_body_names_a_spent_quota() {
assert_eq!(
classify_http_failure(429, "slow down").kind(),
CompletionErrorKind::RateLimited
);
assert_eq!(
classify_http_failure(429, "insufficient_quota").kind(),
CompletionErrorKind::QuotaExhausted
);
}

#[test]
Expand Down Expand Up @@ -406,5 +412,4 @@ fn an_in_stream_envelope_that_matches_no_rule_stays_a_transport_failure() {
"the connection to the model backend failed"
);
assert_eq!(error.detail(), Some("upstream\\ndied"));
assert!(error.is_retryable());
}
26 changes: 15 additions & 11 deletions crates/harness-gateway-client/src/wire/request-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
//! constructor, so [`tool_round`] takes both from a facade `Run` that went
//! through one tool round.

use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::num::NonZeroU32;
use std::sync::Arc;

use runvil::effect::{Effect, EffectAnswer, IssuedEffect};
use runvil::model::{Completion, CompletionResult, ModelId, ModelInfo, ThinkingMode, ToolCall};
use runvil::model::{
Completion, CompletionResult, ModelId, ModelInfo, ModelRole, ThinkingMode, ToolCall,
};
use runvil::timestamp::Timestamp;
use runvil::tools::{ToolCatalog, ToolId, ToolInfo, ToolOutput};
use runvil::{Prompt, Run, RunContext, RunResult, RunSeed, Step};
use runvil::{Prompt, Run, RunConfig, RunOutcome, RunSeed, Step};
use serde_json::json;

use super::*;
Expand Down Expand Up @@ -45,8 +48,8 @@ fn tool_round(offered: &[Offered]) -> Round {
}
let source = format!(
"---\nname: round\ndescription: One tool round.\nrunvil: 0\n\
models:\n writer: {{}}\n---\n\n# Round\n\n## Ask\n\n\
```lua\nmodels.use('writer')\n{offers}messages:user('echo once')\n\
---\n\n# Round\n\n## Ask\n\n\
```lua\n{offers}messages:user('echo once')\n\
models.loop()\nreturn messages[#messages].content\n```\n"
);
let first_wire_name = format!("example_test_{}", offered[0].0);
Expand All @@ -65,16 +68,17 @@ fn tool_round(offered: &[Offered]) -> Round {
NonZeroU32::new(8_192).expect("8192 is non-zero"),
ThinkingMode::Never,
);
let ctx = RunContext::new("round", RunSeed::from_bytes([7; 32]), Timestamp::UNIX_EPOCH);
let ctx = RunConfig::new("round", RunSeed::from_bytes([7; 32]), Timestamp::UNIX_EPOCH);
let catalog = ToolCatalog::new(tools).expect("the offered tools are distinct");
let (ctx, requirements) = ctx.with_tools(catalog).prepare(&prompt, Some(model));
assert!(requirements.refusal().is_none(), "the round prepares");
let ctx = ctx
.with_tools(catalog)
.with_models(BTreeMap::from([(ModelRole::Default, model)]));
let mut run = Run::new(Arc::new(prompt), "", ctx);
let mut rounds = Vec::new();
let result = loop {
let outcome = loop {
let effects = match run.step() {
Step::Pending { effects, .. } => effects,
Step::Done { result, .. } => break result,
Step::Done { outcome, .. } => break outcome,
};
for IssuedEffect { id, effect, .. } in effects {
let answer = match effect {
Expand Down Expand Up @@ -112,8 +116,8 @@ fn tool_round(offered: &[Offered]) -> Round {
}
};
assert!(
matches!(&result, RunResult::Completed(text) if text == "done"),
"the round runs to its terminal reply: {result:?}"
matches!(&outcome, RunOutcome::Completed { final_text } if final_text == "done"),
"the round runs to its terminal reply: {outcome:?}"
);
assert_eq!(rounds.len(), 2, "one tool round, then the terminal round");
rounds.pop().expect("the terminal round was recorded")
Expand Down
2 changes: 0 additions & 2 deletions crates/harness-gateway-client/src/wire/stream-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ fn finish_fails_a_tool_call_batch_sharing_an_id_as_a_retryable_malformed_reply()
.finish(Value::Null, None)
.expect_err("two calls sharing an id must fail");
assert_eq!(error.kind(), CompletionErrorKind::MalformedResponse);
assert!(error.is_retryable());
}

#[test]
Expand Down Expand Up @@ -385,7 +384,6 @@ fn error_envelope_naming_a_context_limit_is_a_context_overflow() {
.expect_err("an error envelope must fail the stream");
assert_eq!(error.kind(), CompletionErrorKind::ContextOverflow);
assert_eq!(error.overflow(), (Some(5000), Some(4096)));
assert!(!error.is_retryable());
}

#[test]
Expand Down
8 changes: 8 additions & 0 deletions crates/harness-internal/runner/src/effect_loop-flight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ impl Flights {
self.effects.len() + self.waiting.len()
}

/// Tears down every future the set holds, aborted ones not yet polled
/// included, and every waiting call, with no answer for any of them.
pub(super) fn clear(&mut self) {
self.futures.clear();
self.effects.clear();
self.waiting.clear();
}

/// Puts `answer`, the future of chat, render, or timer effect `id`, in
/// flight under `provenance`. It never waits and never counts toward
/// the cap.
Expand Down
35 changes: 0 additions & 35 deletions crates/harness-internal/runner/src/effect_loop-outcome.rs

This file was deleted.

Loading
Loading