Skip to content

Locate every Engine error at its prompt line and clean up runvil-lua - #194

Merged
vinniefalco merged 36 commits into
cppalliance:masterfrom
vinniefalco:master
Nov 4, 2026
Merged

vinniefalco merged 36 commits into
cppalliance:masterfrom
vinniefalco:master

Conversation

@vinniefalco

@vinniefalco vinniefalco commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Every error the Engine produces now carries the prompt file and line that caused it, wherever one exists. The location shows up in three places: the error's at field, the failed run's message, and the error text that Lua sees. Internal bugs keep pointing at their Rust file and line, and the public API does not change (public-api.txt is untouched). Most of the work routes lines the Engine already had but dropped, plus one new way to read the author's line from the Lua stack.

On top of that, the branch cleans up runvil-lua: stale docs, copied helpers, misplaced items, and a few types that allowed states their docs forbid. Two of those changes alter behavior, listed below; the rest are docs, moves, renames, and narrowed visibility.

The branch holds 36 commits. The first two, which remove SetRule, SetEvidence, SetTool, and DepthVia, came before this work. The next six locate errors, and the last 28 are the cleanup.

Locating errors

  • One writer and one formatter (b682c1b8e): place and coordinates in runvil_types::error. place keeps a location an error already has, fills in an empty file, and never gives an Internal error a prompt location. coordinates is the one formatter of file:line.
  • Failures that reach Lua (126a8eaa7): every shim yield records the author's call line, and a Rust callback reads the author's line from the running coroutine. A failure crossing into Lua is placed on that line, a failure caught by pcall points at the failing call rather than the pcall, and the error table's message reads {file}:{line}: {prose}. The location never goes into the prose, so it cannot double.
  • The Engine's own failures (c5c876da4): tasks_live at the first leaked task's spawn line, a jump to a missing heading at the jump line, the H1 gate at its cause's line, store and malformed-yield failures at the call line, var and argv read-back failures at their block, and a failed substitution at its placeholder's line, library prose included. A located run failure's message now ends with (at {file}:{line}).
  • Parse and run-start failures (f2c5fc50e): the parse errors that already knew their line now point at it, frontmatter key lines come from a plain line scan, and a bad uses: entry, an unsupported runvil: version, and the start check's requirement notice point at their key.
  • Invariants (fc9438875): the types, Lua, and Engine crates state the location rules in their ## Invariants blocks.
  • One depth wording (65c49c3cd): the depth-cap error is now located, so its message no longer names the construct. It reads recursion exceeded cap of {cap}, and SpawnVia, the fanout mark, and the cap_name argument are gone.

Cleaning up runvil-lua

  • Docs that no longer matched the code (12 commits, such as 9a4a15238, 3499d9fde, 67d1f0811, d9a0130c3, 6afba7431, 5a1f1f9d2, 37019f690): dozens of docs and comments that described code since changed, restated a rule that lives elsewhere, or named a model handle loop or infer method that handles no longer have. The crate doc now lists all four items the facade re-exports, Request::Call::input runs under the caller's args, and VfsOp::Delete removes an empty directory too. The type-name docs say which refusal uses which rule; no message changes.
  • One copy of each shared helper (bf5099943, cad74b11c, 83a4fb299): failure and with_fact are free functions in runvil_types::error, so the Lua, Engine, and prompt crates stop copying them; internal and store_failure are shared from runvil-lua. The universal error fields read from one table in both directions, and tool and Pack objects share one registry lookup and one frozen guard.
  • Placement (d4cf401b2, 4951eda8d, 05ebcad20, fb949072f, 83bbb8e55): the chunk-line helpers move to a new chunk_line module, DiscoveryTool to bindings, AdhocTools to a crate-root adhoc module, and two constants and scalar_return out of vm. Six two-module cycles are gone. The alias module is deleted, since its one user discarded its error.
  • Types (269b9e797, 7ca443a6e, fb949072f): ModelReport gives way to ModelCall, which held the same two fields; a task's section and blocked live inside TaskPhase::Running, so an ended task cannot hold a live position; and AdhocTools::binding splits into by_id and by_wire_name, so callers stop re-checking for a /.
  • Names (539342bb0, 121a7b342, 99473df51): the shim chunk is no longer called a prelude, two private fields that collided with nearby names are renamed, the offers handle runtime becomes offers, and the production VM install helpers take the plain names.
  • Visibility: exhaustion_detail, Offer, OfferTarget, UseOptions, MAX_RECORD_META_BYTES, and object_id narrow to what their users need; NonEmpty::new becomes test-only, and store_error_value_fields moves behind test-support.
  • AGENTS.md (9b5baa6ab, 6afba7431): the error rule says a crate-private Err marker turned into Error by its caller is not another error type, and the Static Set definition no longer names a model handle loop.

Behavior changes worth a look

  • A located Internal or Substitution failure keeps its own code instead of becoming LuaRuntime, so a failed substitution in the H1 ends the run as Substitution (run kind Lua), not RequirementsUnmet. Every other typed failure maps as before.
  • Caught tool, store, model, and context failures now show {file}:{line}: in their Lua text. A model still reads a tool's failure as its prose alone.
  • Each prompt block compiles behind local __runvil_site <close> = nil; on its first line, so a main-chunk return f() keeps the author's frame. Line numbers do not move, and source() returns the block as written.
  • A frontmatter failure's run message no longer shows a refused Pack's id as the prompt's name.
  • Frontmatter key lines live on Prompt, not Frontmatter, so Frontmatter equality still compares content only.
  • Workshop leaves the line {line}: prefix off an Internal parse failure, whose line is in Rust.
  • A record of any message role but assistant that holds tool_calls is now refused by the append or replace call that adds it, with the same messages[{index}] sets tool_calls but is not an assistant message text, instead of failing at the next models.loop (4b577ce97).
  • A poisoned model selection, jump slot, or offers lock inside a Lua callback now raises kind internal instead of lua, so an author's pcall cannot treat an Engine fault as an authoring error (56b97661c).

Known limits

  • A tail call inside a function the author defined still drops that function's frame, so its failure points at the line that called the function.
  • A requirement notice runs over several lines, and its (at ...) suffix comes after the last one, even though it names the first gap's key.
  • A parse failure the Harness finds before a run starts still shows no location in the run's message.
  • No test covers the H1 gate's fallback to its block's start line, a failed install_lazy_prose, or the new Internal exit in prepare_spawn.
  • runvil-lua still has two module cycles: packs with tools, and test_support with vm.

Verification

Each of the first eight commits passed cargo fmt --check, both clippy runs with warnings denied, the workspace and facade docs gates, cargo test -p build-xtask, the docs-claims scan, both nextest runs, and cargo xtask api --check.

Each of the 28 cleanup commits compiles on its own (cargo check of the types, Lua, Engine, and prompt crates with all targets and features). The final tree passed cargo fmt --check, workspace clippy with warnings denied, workspace nextest (4176 passed), the workshop clippy and nextest runs (326 passed), the workspace, facade, and private-items Engine docs gates, cargo xtask api --check with public-api.txt unchanged, cargo test -p build-xtask, the docs-claims scan, and cargo deny. New tests pin the add-time tool_calls refusal, the internal kind of each poisoned lock, the store chunk's name, and every code of the universal error field table; the first two failed before their fixes.

The language guide updates are separate commits in runvil-docs: 5895fba for error locations and f891fca for the cleanup.

Each tool-set failure now builds its message at the check that refuses the sets, and `error::tool_sets` takes that message as a `String`. This removes `SetRule`, `SetEvidence`, `SetTool`, and `error/sets.rs`, and adds `tool_in_set` for the messages that name a tool and its set. The message text does not change.

- `tool_sets` and `tool_in_set` are in `error/tools.rs`, beside `bind_schema`.
- The failures no longer set the `FactKey::Rule` fact.
- `check_render` compares the Pack lists as names, not as `PackId` values.
- The scope tests now assert the literal message and `ErrorCode::ToolSetsInvalid` for each case. They no longer build the expected error through `tool_sets`.
`call_depth_exceeded` and `CallDepth::deeper` now take the cap's name as a `&str` instead of a `DepthVia`. The call dispatch passes `"call"`, and the spawn maps `SpawnVia::Fanout` to `"tasks.fanout"` and `SpawnVia::Spawn` to `"call"`. The message text does not change.

- The failure no longer sets the `FactKey::Reason` fact. It keeps the `FactKey::Limit` fact.
- The error test asserts that the `FactKey::Reason` fact is absent.
Error locations need one writer and one formatter, so this commit adds `place` and `coordinates` to `runvil_types::error`. `place` writes the prompt location of an `Error`, and `coordinates` formats a `Location` as `{file}:{line}`. `with_prompt_context` and the `ErrorCode::Internal` arm of `location` now call `coordinates`, and six new tests check both functions.

- `place` keeps the location an error already has and only fills in an empty file, so the first placement wins and the innermost line stays. It never changes an `ErrorCode::Internal` error, so that error keeps its Rust location.
- Both callers of `coordinates` keep their output text. `with_prompt_context` copies `file` into `at.file` before it writes `error.prose`, so `coordinates` gets the same file that the old code took from `file`.
- Only the new tests call `place`. Its doc comment calls it the only writer of a prompt location, but `with_prompt_context` still writes `at.file` on every error with a location, `ErrorCode::Internal` included.
- No new test checks the text that `with_prompt_context` or `location` builds.
A failure that crosses into a section's Lua is now located on the author's line that caused it, when that line is known. The VM records the author's line at each shim yield and reads the innermost author frame from the running stack, and `place` writes the location. The error text that Lua sees then starts with the prompt file and line, and an internal failure keeps its Rust location.

- The call line is per-VM state, a field of the `PromptFile` app data beside the file. `start_block_coro` clears it, and the Lua wrapper that `recording_yield` builds over the coroutine `yield` sets it at each shim yield, `None` included.
- A crossing failure takes the line of the innermost author frame, or else the call line, and `classified` tries the `mlua` traceback first. `place` keeps a location that a failure already has, so the first line found wins.
- Each prompt block now compiles with `local __runvil_site <close> = nil;` in front of its first line, so a main-chunk `return f()` is not a tail call. The author's frame stays on the stack, no line moves, and `source()` still returns the source as given.
- `error_table` now puts `coordinates` in front of the prose in the table's `message` when the failure is located and not `Internal`, and author prose becomes `{what}. {default}`. An uncaught Lua failure's `prose` thus names no file or line, and its line is only in `at`.
- `authored` now locates a crossing failure of any code but `Internal`, so a caught tool, model, store, or context failure also shows its file and line. Their prose does not change.
- `map_runtime_error_among` now returns a located `Internal` or `Substitution` failure as raised, and turns every other typed failure into `LuaRuntime` except `ContextExhausted` and `LuaQuota`. `ContextExhausted` now goes through the mapping too, so both keep their own failure and take the mapped line when they have no location.
- `kept_failure` and `answered_block_failure` now put the typed failure on the mapped line through `placed_like`, where they used to drop that line. An `Internal` mapping gives no line.
- `LuaProgram::compile` and `classified` now build `Internal` with `error::internal` instead of `Error::new`, so it has a Rust location. `with_prompt_context` now writes `file` only into a location with no file, and `AppError::prompt_parse` adds no `line N: ` prefix for `Internal`.
- Most test edits add the expected line, through the new `placed` helper and a new line argument on the `engine` helpers `refused` and `with_author_prose`. `spawn_shares_calls_target_resolution_and_raises_at_the_call_site` now puts its two calls on one line, so `tostring` still gives the same text for both.
- No production code in this change calls the new public `call_line` method of `SectionVm`. Only a test reads it.
- No test in this change covers the `Substitution` pass-through in `map_runtime_error_among`. The new mapping tests cover `Internal` and `ContextExhausted`.
Many Engine failures did not record the prompt line that caused them. Each such site now calls `place` with the prompt file and the line of the call, the block, or the `{{ }}` placeholder that caused the failure. `failure_message` now ends every failure that has a location with `(at {file}:{line})`, and keeps the `(prompt {name}, line {line}, column {column})` form only for the parser's own codes.

- `lua_line` sets one rule for a failed `var` or `argv` read-back: the start line of the last Lua block at or before the block index. A fall-through gets the last Lua block, and a jump or a return gets its own block, because neither moves the index.
- The `jump` closure records `author_line` beside the heading, and the line goes through `take_jump`, `LuaBlockResult::Jump`, and `Settled::Jump`. Only `apply_jump` and `end_h1_at_jump` put that line on the failure when the heading does not resolve, because `call` and `prepare_spawn` also use `resolve_chain_target`.
- `TaskEntry` has a new `spawn_line`, which `prepare_spawn` reads from the `call_line()` of the chain that spawns the task. `settle_owned_tasks` puts `tasks_live` on the line of the first leaked task, and timers record `None`.
- The parser records the start line of each trimmed prose block in the new `line` field of `Block::Prose`, through the new `file_line_of`. `pending_prose` keeps that line beside the text, and library prose gets it through `LibraryBlock`, `begin_load`, and `swap_prose(template, line)`.
- `substitute` puts each failure on the prose line plus the newlines before the placeholder, because only `substitute` has both the text and the byte offset. `Sources` has the new fields `file` and `line` for this.
- `Running` and `Entered` move to the new `chain_record/entered.rs` to keep `chain_record.rs` under 500 lines. The moved code differs only in the type and doc of `pending_prose`, and in `pub(in crate::execute::scheduler)`, which keeps the reach of the old `pub(super)`.
- The two `jump` tests in `lua/src/tests/require.rs` move to the new `require/jump.rs` for the same limit, and they now also check the `jump` line.
- The test hook `dispatch_store_without_a_store_for_test` gives the chain at arena index 0 an access with no store and dispatches its store request again, because the start probe of the run prevents a store-view failure.
- In `settle_block`, the `RequirementsUnmet` refusal of the H1 gate takes the file and start line of its cause, or the first line of the block when the cause has no location. A failed `{{ }}` substitution in H1 keeps its `Substitution` code and its template line.
- `InvalidValue` and every other code of kind `Parse` outside the four parser codes keep that kind, but now end with `(at {file}:{line})`, or with nothing when they have no location. A run-time `InvalidValue` holds the bad value in its `Name` fact, which the parse form showed as a prompt name.
- A `LuaCompile` message already names its line, so its run message now names the line again, as in `(line 7)` and `(at prompt:7)`.
- `prepare_spawn` reads the line through `chain.frame()?`, so a spawn from a chain with no frame now fails with an `Internal` error. The store paths use `chain.frame().ok()` and give no line in that case.
- New tests pin each placement above except the block-line fallback of the H1 gate, a failed `install_lazy_prose`, the read-backs in `end_h1` other than `argv` at a fall-through, the other codes of kind `Parse`, and the new `Internal` exit in `prepare_spawn`.
A failed parse or a refused run start now tells the author which prompt line caused it, when that line is known. The parser now puts each failure on the line it knows, and `internal` records the Rust file and line of its call through `#[track_caller]`. With the new frontmatter key lines, the Engine puts `UnsupportedVersion` on the `runvil:` line and the `RequirementsUnmet` refusal on a `uses:` or `models:` line.

- `split_frontmatter` finds key lines with a text scan, not from the YAML parse, so `key_line` is meaningful only for a key that decoded to a value. A key gets a line only when exactly one line declares it at column 0, so a repeated key or a flow mapping gives no line, and a failure on that key has no location.
- `key_lines` is a field of `SplitSource` and `Prompt`, not of `Frontmatter`, so two `Frontmatter` values with the same content still compare equal. `Prompt` equality now also compares `key_lines`, and the Engine reads them through `key_line`, a new `pub fn` in `detail.rs`.
- `ChainState` builds the refusal once, with its location: `SharedState` holds it as `Option<Error>` in place of the notice text, and `check_start` returns a clone.
- `refusal` in `requirements.rs` puts the failure on the key of the first gap the prompt caused: `uses:` for a missing or unavailable Pack, else `models:` for a context or thinking shortfall. A reserved Pack id and a role map with no `default` entry are the caller's gaps, so a refusal made only of those has no location.
- `internal` now takes `&'static str` in place of `&str` and passes the message to `runvil_lua::internal_at` with no prefix. Its test pins one `internal invariant violated:` prefix and reads the expected site from `std::panic::Location::caller()`, so path separators do not matter.
- `bullet_items` now takes a closure that builds the failure from the first empty marker, so `sections.rs`, which holds the prompt body, builds and places it. There `empty_item` finds the marker's byte offset from its address relative to `shared`, and gives no location when the marker is not a slice of `shared`.
- A file with no opening frontmatter delimiter, or with frontmatter that never closes, fails on line 1, as an unclosed `lua` fence fails on its opening line. More than one H1 fails on the line of the second, and an empty list item fails on its marker, past the indent.
- A `FrontmatterInvalid` run message now ends with its line and column alone, such as `(line 4, column 1)`. For that code, `location` drops the `Name` fact, which, when present, is a refused Pack's id, so the message never names the Pack as the prompt.
- `check_start` now passes `UnsupportedVersion` through `with_prompt_context` as well, so the failure gets the prompt's `Name` fact and the prose that `with_prompt_context` writes. A prompt with no `runvil:` key still fails with `StructureInvalid` and no location.
- The `RequirementsUnmet` run message puts its `(at {file}:{line})` suffix after the whole notice, on the last gap's line, even when the suffix gives the line of the first gap's key.
- Three version tests that compared the whole error now compare the outcome or single fields, so none of them pins the prose that `UnsupportedVersion` now gets.
The types, Lua, and Engine crates now record the error location rules as invariants, and the change corrects stale comments to agree with the code. The three Lua shim chunks now say that their `yield` wraps `coroutine.yield`, and `program.rs` now says that its leading tag names the block's display location. A new test in `exit_rules.rs` compares the version gate's whole refusal error, including the prose that `unsupported_major_is_refused` does not check.

- The types invariant names `coordinates` as the only code that formats a location as `file:line`. `map_chunk_line_to_absolute` in `program.rs` writes its own leading tag with `format!("{location}:{absolute}: {result}")`, where `location` is the block's display location.
- The Lua invariant on error tables puts `file:line` only in the table's `message`, never in the error's prose. The new test expects the version gate's refusal to start its prose with `prompt:4:`, because the refusal goes through the parser's prompt context.
- The `coro.rs` changes move line breaks only; no words change.
The depth-cap error gives the prompt line where the cap stops a `call`, `tasks.spawn`, or `tasks.fanout`, so its message does not have to include the cap name. The change removes the `cap_name` argument of `call_depth_exceeded` and `deeper`, and the `SpawnVia` enum that chose the name for a spawn. The fanout shim no longer sets the `fanout = true` mark, and `Request::Spawn` loses its `via` field.

- The message of every depth-cap refusal is now `recursion exceeded cap of {cap}`, without `call` or `tasks.fanout` before it. The error code is still `ErrorCode::CallDepthExceeded`, and the `FactKey::Limit` fact still gives the cap.
- `parse_spawn` no longer reads a `fanout` field. The change removes the `FieldFailure::Malformed` result that `shim_via` gave for a non-boolean value, and its test `spawn_reads_the_fanout_mark_and_rejects_a_non_boolean_one`.
- In `fanout_arms.rs`, the text alone no longer shows if the cap stopped a `tasks.fanout` or a `call`. Two tests now check `error.at` instead: `at_line(16)` for the `tasks.fanout` line, and `at_line(33)` for the `call` line in an arm.
The coroutine shim chunk shared the word prelude with a Pack's Lua prelude, so its docs now say shim chunk or shim install, and `install_shim_prelude` becomes `install_shims`. The embed, `@`-name, and compile-once rule is now stated once on `LuaProgram::compile_internal`, so each embedded chunk constant's doc says only what it holds, and the `take_failure`, `stash_shims`, and `install_store_shims` docs now match their code. A new test pins that `STORE_CHUNK_NAME` resolves to the file `STORE_SOURCE` embeds, as the other chunk names already are.
The `engine_globals` module doc read as the full list of Engine globals, so it now names the persistent `log`, `untrusted`, `store`, and `ui` globals it installs and says the others install from their own modules. The `install_store_table` doc now says what `exists` returns, and two `log` budget comments drop a tag that pointed at a review list no longer in the repository.
The `failure`, `with_fact`, `internal`, and store-message helpers were copied across the types, Lua, Engine, and prompt crates, so each now has one definition the others import. `failure` and `with_fact` become free functions in `runvil_types::error` beside `place`, so the facade's `Error` gains no inherent method, and `runvil_lua` now exports `internal` and `store_failure`. The Engine and prompt crates re-export `internal` from `runvil_lua`, and `#[track_caller]` keeps each call site the reported location.
Several error and error-value docs named only some of each kind's fields or sources, so each `ErrorKind` variant now names its fields, `Lua` lists all its sources, and the `error::universal`, `error_value`, and `authored_at` docs say what the code does. `store_error_value_fields`, which only tests call, now builds only under `test` or `test-support`, and its field-shape text moves onto `store_error` beside a note that both store paths build their failure there. The unused `ErrorField::as_integer` is removed.
`error_fields` and `field_facts` spelled the same code-to-field table once in each direction, so a code added to one match could drop its fact on read-back. A private `string_fact` now maps each code to its one string field and fact, and both directions read it, while `TasksLive` and the store kind keep their own arms. The round-trip test list now holds `UnboundTool`, `ModelOnlyTool`, `TaskConsumed`, and `TaskCancelled`, so it covers every code of each group.
`program` and `error` imported each other only through five chunk-line helpers that use nothing but `std`, so the helpers move into a new crate-private `chunk_line` module and the import cycle ends. `chunk_line` also holds `STRING_CHUNK_OPENING` and `PROMPT_CHUNK_PREFIX`, and `error::author` holds `TRACEBACK_HEADING`, so every writer and parser spells each marker through one constant. Each site keeps its own `find`, `rfind`, or `split_once`.
`ModelReport` held the same call id and turn as `ModelCall`, so `prepare_model_dispatch` now takes a `&ModelCall` and the engine builds one for each model-issued call. The dispatch docs and the crate `AGENTS.md` now describe the one executor that applies these bodies, not every executor.
The `Answer`, `Retained`, and `render_envelope` docs restated how a typed error survives the resume envelope, so they now point to `SectionVm::resume_block_coro_answer`, which also states the cancel and quota exception. The protocol docs no longer claim one `Answer` variant per request, and they name the three submodules that use `NonEmpty`.
The `FieldFailure` type and the parse module doc already state where a field failure surfaces, so the readers and per-op parsers no longer repeat it. The parse module doc now names the parsers it holds itself, and the `parse::tasks` doc no longer lists a `pending` parser it does not have.
`Request::Call::input` now says an absent override runs under the caller's own args, and `VfsOp::Delete` says it also removes an empty directory. `MAX_RECORD_META_BYTES` becomes private, and `NonEmpty::new` becomes a test-only constructor, since only this crate's tests call it.
`TaskStatus` held `section` and `blocked` beside `state`, so an ended task could still carry them. They now live on `TaskPhase::Running`, which loses `Copy`, and the engine sets them only for a live backing chain. The Lua status table keeps its shape.
The rule reads two fields of one record, so `parse_record` now refuses a non-empty `tool_calls` on every role but assistant, and the error lands on the call that adds the record. The late check in the projection and its test case go away, and the record and projection docs say where the rule now sits.
The `models` docs claimed the five roles always arrive filled, and the loop docs still described a Lua shim loop that now runs in Rust. Each doc now states what the code does, and the raw model id rule keeps one home on `RawModelIds`. `UseOptions` and its `apply` become private, since only `models.rs` uses them.
The shim comments said their type names match the protocol parse, and `models::lua_type_name` said it matches Lua's `type()`, yet neither holds for `json.null`. Each comment and doc now states the rule its namer applies, and the models loop namer becomes `engine_type_name`, after the shim's `engine_type` whose names it copies. No message or test assertion changes.
`Encoder::keys` held the path to the current value beside parameters named `keys` that hold a table's own keys, and `ListState::last` held the last send beside `last` range bounds. The fields become `path` and `last_send`, and the local in `Encoder::error` becomes `shown` so it does not shadow the new field. No behavior changes.
`bindings` imported `scope` only for `DiscoveryTool`, so the type and its reserved-Pack test now live in `bindings`, and the root re-export lists it there. `OfferTarget` and `Offer` become `pub(crate)` and leave the root re-export, since no other crate names them. The `scope`, `bindings`, `ToolBinding`, `from_info`, and `static_set` docs now state what each holds, the model-only flag included.
`walk_budget`, `prose`, and `protocol::parse::tools` each imported one item from `vm`, which imports each of them. `DEFAULT_LUA_MEMORY_BYTES` now lives in `walk_budget` beside `HeapCeiling`, `SYS_LIVE_POISONED` in `sys`, and `scalar_return` with `value_to_string` in `section` beside `LuaBlockResult`. The two VM default docs now say each holds only until `apply_lua_limits` sets the run limits, in place of a claim that they match `RunLimits`.
Model handles have no methods, so `loop_running` and the Static Set definition in `AGENTS.md` now name only a `models.loop` call. The `re_seal_sys`, `vm::state`, suspending-call, and injection docs now say when the reseal runs, which installs follow injection, and that `store` calls suspend too, and drop claims about store callbacks and the emitter. `prose::install` was `swap` with its result dropped, so `install_lazy_prose` calls `swap` directly.
The offer verbs are the only callers of `refuse_after_render` and `TOOL_LIST_FIXED`, so both move into `tools/offer.rs` as private items, and `object_id` loses its `pub(super)`. `tools.adhoc` checks its name with `is_alias` directly instead of through `Alias`, whose own error it discarded, so `alias.rs` goes away. The two tool object constructor docs now say only which kind of tool each one builds.
`AdhocTools` serves `tools` and `packs` as much as the VM, so it moves from `vm/adhoc.rs` to a crate-root `adhoc` module that also holds the adhoc call protocol doc, and `runvil_lua::AdhocTools` keeps its path. `AdhocTools::binding` picked the id or wire-name space by whether its argument held a `/`; `by_id` and `by_wire_name` replace it. Callers that hold a string id parse it with `ToolId::parse` first, and a string that does not parse holds no adhoc tool.
`tool_object` and `pack_object` read two registry tables with one body, and the tool and Pack userdata refused assignment with one closure that differed only in its label. `proxy` now holds `registry_object` and `add_frozen_guard`, and the two lookups stay as one-line wrappers whose names say which registry they read. The refusal texts are unchanged.
The section offers handle was still named `runtime`, and two production methods carried suffixes while test helpers held the plain names. The handle is now `offers`; `inject_values_with_var` and `install_scheduler_control_globals` become `inject_values` and `install_control_globals`, and the test helpers become `inject_test_values`, `install_test_control_globals`, and `tool_set_and_offers`. `SectionVm::rendered_tools` is removed, so callers read the frozen sets through `messages().rendered_tools()`.
The model selection, jump slot, and offers locks raised a bare string from Lua callbacks, so an author `pcall` saw kind `lua` for an Engine fault. Each callback now raises `error::internal` wrapped in `mlua::Error::external`, and each lock message is one constant, `SELECTION_POISONED`, `JUMP_SLOT_POISONED`, or `OFFERS_POISONED`, that the Rust-side reads share. New tests in `models-tests.rs` and `vm/tests.rs` poison each lock and assert that `pcall` reports kind `internal`.
The crate doc now says a failed store op surfaces as that store failure, names all four items the facade re-exports, and points to `install_preludes` for the prelude rules instead of restating them. The README now says that store calls a Pack prelude makes while it installs run directly against the run's store view. `exhaustion_detail` has only in-crate users, so it becomes `pub(crate)` and leaves the root re-export.
The `hardening`, `iteration`, `var`, and `walk_budget` docs now name every case their code handles: the `table.concat` wrapper, the unordered resume of `next`, the `tasks.spawn` capture of `var`, and the offer and options entries `HELD_KEY_LIMIT` bounds. The `argv` module doc and `guarded_var` now point to `Argv::Frozen` and the registry constants instead of restating them. `CancelHook::is_cancelled` now calls `cancelled`, so the cancel flag read has one body.
The error rule forbade every other error enum or struct, which read as covering crate-private markers that their callers turn into `Error`. The rule now says that such a marker, or a type that pairs an `Error` with a value the caller must still release, such as `InjectFailure`, is not another error type. No error type changes.
Model handles have no methods, so a prompt runs a handle through `models.infer` or `models.loop`. The `RunLimits` and `max_tool_iterations` docs now name only `models.loop`, and four test comments and one assertion message name `models.infer` on a handle.
@vinniefalco vinniefalco changed the title Locate every Engine error at its prompt line Locate every Engine error at its prompt line and clean up runvil-lua Nov 4, 2026
@vinniefalco
vinniefalco merged commit 37019f6 into cppalliance:master Nov 4, 2026
11 checks passed

This branch was previously deployed

1 inactive deployment
github-pages — 37019f69 Deployed Nov 5, 2026 by vinniefalco via deploy #163
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant