Skip to content
Closed
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
6 changes: 3 additions & 3 deletions crates/runvil-internal/engine/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,19 +207,19 @@ pub(crate) enum Error {
#[error("tool-call loop did not converge")]
ToolLoopExhausted,

/// A chain ended while author-origin tasks it owned were still live.
/// A chain ended while tasks it owned were still live.
///
/// A spawned task ends with its owner, so a task the author neither
/// waited on nor cancelled is the author's bug: the chain's outcome
/// becomes this error (the run's for the root walk, the call's answer
/// for a `call` chain) and the leaked tasks are abandoned. The message
/// names the ids in spawn order; the Lua table lists them as `tasks`.
#[error(
"chain ended with author tasks still live: {}; wait on or cancel every task a chain spawns before it ends",
"chain ended with tasks still live: {}; wait on or cancel every task a chain spawns before it ends",
join_task_ids(.tasks)
)]
TasksLive {
/// The live author tasks, in spawn order.
/// The live tasks, in spawn order.
tasks: Vec<TaskId>,
},

Expand Down
10 changes: 3 additions & 7 deletions crates/runvil-internal/engine/src/execute/context-bound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use runvil_parser::ModelKeyword;
use runvil_types::packs::PackId;
use runvil_types::tools::ToolInfo;

use crate::execute::scheduler::TaskBuiltin;
use crate::lua::{ToolBinding, ToolSet};
use crate::model::{ModelBinding, ModelInvocation, ModelSet};
use crate::parser::Prompt;
Expand Down Expand Up @@ -58,13 +57,10 @@ fn offered_bindings(ctx: &RunContext) -> Vec<ToolBinding> {
offered
}

/// Why `tool` can't be offered under `name`: a task built-in has the
/// name, no model round could advertise it, or an earlier offered tool has
/// it. `None` when it can be offered.
/// Why `tool` can't be offered under `name`: no model round could
/// advertise it, or an earlier offered tool has it. `None` when it can be
/// offered.
fn offer_refusal(name: &str, tool: &ToolInfo, taken: &BTreeSet<String>) -> Option<String> {
if TaskBuiltin::from_name(name).is_some() {
return Some("a task built-in has the same name".to_owned());
}
if let Err(error) = detail::tool_schema_new(
name,
tool.description.as_str(),
Expand Down
5 changes: 2 additions & 3 deletions crates/runvil-internal/engine/src/execute/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
//! unchanged, so existing `crate::execute::protocol::*` paths keep working.

pub(crate) use runvil_lua::{
Answer, Blocked, ChatOutcome, ChatResult, LocalToolOutcome, MessageContent, MessageRecord,
RecordRole, Request, SpawnVia, TaskDelivery, TaskPhase, TaskStatus, ToolCallOutcome,
ToolCallRecord, VfsOp, VfsOutcome, YieldParse,
Answer, Blocked, ChatOutcome, ChatResult, LocalToolOutcome, Request, SpawnVia, TaskDelivery,
TaskPhase, TaskStatus, ToolCallOutcome, ToolCallRecord, VfsOp, VfsOutcome, YieldParse,
};
41 changes: 2 additions & 39 deletions crates/runvil-internal/engine/src/execute/run/tests-drops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
//! `Cancelled` when nothing catches it. A `Chat` drop is covered for both
//! kinds of round, a nested `models.infer` round and a `models.loop` round.
//! A `Timer` drop is covered for the timed `tasks.join_any` and
//! `tasks.join` waits and the model's `await_tasks`, and under a run cancel,
//! where the drop wakes no one.
//! `tasks.join` waits, and under a run cancel, where the drop wakes no one.

use std::sync::Arc;

Expand All @@ -20,7 +19,7 @@ use crate::execute::RunResult;
use crate::execute::run::{
AnswerRecord, Effect, EffectAnswer, EffectId, EffectRecord, Round, Run, Step,
};
use crate::model::{Completion, CompletionResult, ToolCall};
use crate::model::{Completion, CompletionResult};
use crate::parser::Prompt;
use crate::test_support::scenario::{chat_answer, only_effect, test_model_with};
use crate::test_support::scripted_chat::text;
Expand Down Expand Up @@ -336,39 +335,3 @@ fn under_a_run_cancel_a_dropped_timer_ends_the_run_cancelled_without_resuming_th
"the wait's pcall never resumed, so the run did not go on: {result:?}"
);
}

#[test]
fn a_dropped_await_tasks_timer_resumes_the_models_tool_call_with_the_cancelled_error() {
let mut run = bound_sections(
"## Only\n\n```lua\nmodels.use('writer')\ntools.allow_tasks()\n\
local msgs = messages.new()\nmsgs:user('wait')\n\
local ok, err = pcall(models.loop, msgs)\n\
return tostring(ok) .. ':' .. err.kind\n```\n",
);
let (round, effect) = only_effect(run.step());
assert!(
matches!(effect, Effect::Chat { .. }),
"the loop's round: {effect:?}"
);
let call = ToolCall::from_parts("call-1", "await_tasks", json!({ "timeout": 30 }))
.expect("a whole call");
let completion = Completion::from_result(CompletionResult::ToolCalls(vec![call]), "m")
.expect("a tool-call result is accepted");
run.resume(round, EffectAnswer::Chat(Ok(Box::new(completion))));
let (timer, effect) = only_effect(run.step());
assert!(
matches!(effect, Effect::Timer { .. }),
"await_tasks parks on its timeout alone: {effect:?}"
);
run.resume(timer, EffectAnswer::Dropped);
let Step::Done { result, .. } = run.step() else {
panic!("the loop's caught drop ends the run");
};
let RunResult::Ok(out) = result else {
panic!("a caught drop leaves the run to succeed: {result:?}");
};
assert_eq!(
out, "false:cancelled",
"the loop raised the cancelled error"
);
}
51 changes: 20 additions & 31 deletions crates/runvil-internal/engine/src/execute/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,47 +29,37 @@
//! path ([`SlicePath`](super::walk_target::SlicePath)) rather than by
//! borrow, and the scheduler owns itself outright.
//!
//! This file holds the scheduler core: the chain arena, the
//! ready queue, the admission queues (spawned tasks wait there until
//! the run's concurrency limits admit them), the pending table, the task
//! arena, and
//! This file holds the scheduler core: the chain arena, the ready queue,
//! the admission queues (spawned tasks wait there until the run's
//! concurrency limits admit them), the pending table, the task arena, and
//! the one `issue` path every leaf arm hands its effect through. The
//! submodules hold the rest: `pending` the pending table's entry (the
//! `Continuation` an answer is applied by), `drive` the run-level step
//! (the ready-queue drain, the terminal rules, and the teardown), `apply`
//! the answer application, `chain` the chain lifecycle (arena insertion
//! and the two chain-end paths), `chain_record` the chain record the
//! arena holds, `step` one chain's step to its next
//! suspension point, `walk` the section walk rules, `h1` the live H1 pass
//! and its hand-off to the walk, `dispatch` the request arms, `chat` the
//! one-round `chat` arm and its answer application, `tool_call` the
//! script and model-issued `tool_call` arm (the two arms the
//! section-visible `models.loop` shim drives), `builtins` the model's
//! task built-ins (`task`, `task_cancel`, `task_status`) answered over
//! the arena and advertised once a section runs `tools.allow_tasks`,
//! `await_tasks` the fourth built-in, the model's wait over its live
//! tasks, `notices` the model-task notices
//! (queued at a model task's end, drained into the owner's next round or
//! its `await_tasks` answer), `tasks` the task arena and the `spawn` arm,
//! `admission` the admission limits, `task_end` the chain-end rules for
//! tasks, `waits` the `join_any` wait and the
//! `ready`, `status`, `pending`, `note`, `concurrency`, and `cancel` arms over the arena,
//! `timer` the wait shims' internal timeout as an effect-backed slot, and
//! `test_hooks` (test builds only) the seams the suites inspect the arena
//! through. A fanout is Lua over those arms (the `fanout` shim spawns one
//! task per member, all up front, and waits on the live set).
//! `Continuation` an answer is applied by), `drive` the run-level step (the
//! ready-queue drain, the terminal rules, and the teardown), `apply` the
//! answer application, `chain` the chain lifecycle (arena insertion and the
//! two chain-end paths), `chain_record` the chain record the arena holds,
//! `step` one chain's step to its next suspension point, `walk` the section
//! walk rules, `h1` the live H1 pass and its hand-off to the walk,
//! `dispatch` the request arms, `chat` the one-round `chat` arm and its
//! answer application, `tool_call` the script and model-issued `tool_call`
//! arm (the two arms the section-visible `models.loop` shim drives),
//! `tasks` the task arena and the `spawn` arm, `admission` the admission
//! limits, `task_end` the chain-end rules for tasks, `waits` the `join_any`
//! wait and the `ready`, `status`, `pending`, `note`, `concurrency`, and
//! `cancel` arms over the arena, `timer` the wait shims' internal timeout
//! as an effect-backed slot, and `test_hooks` (test builds only) the seams
//! the suites inspect the arena through. A fanout is Lua over those arms
//! (the `fanout` shim spawns one task per member, all up front, and waits
//! on the live set).

mod admission;
mod apply;
mod await_tasks;
mod builtins;
mod chain;
mod chain_record;
mod chat;
mod dispatch;
mod drive;
mod h1;
mod notices;
mod pending;
mod step;
mod task_end;
Expand All @@ -95,7 +85,6 @@ use crate::{Error, Result};
use super::context::RunState;
use super::protocol::Answer;
use super::run::{Effect, EffectAnswer, EffectId, Round};
pub(in crate::execute) use builtins::TaskBuiltin;
use chain_record::{Chain, ChatAnchor};
use pending::{CallSite, Continuation, Pending, ToolCallContinuation, VfsContinuation};
use tasks::TaskSlot;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ impl Scheduler {
return Err(Error::internal("a task chain has a slot"));
};
let target = slot.target.clone();
let origin = slot.origin;
let emitter = Arc::clone(self.chains[spawner.index()].ctx.emitter());
// The start event fires at admission, on the spawner's sequence:
// the task first runs now, and the payload's seeds are enough to
Expand All @@ -210,7 +209,6 @@ impl Scheduler {
provenance,
task,
target,
origin,
input: record.input,
item,
index,
Expand Down
12 changes: 3 additions & 9 deletions crates/runvil-internal/engine/src/execute/scheduler/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,7 @@ impl Scheduler {
/// up the timeout and the run goes on, so the chain parked on the
/// timer leaves its wait with the cancelled error, as a dropped `Chat`
/// or `ToolCall` does: a timed `tasks.join` or `tasks.join_any` raises
/// it, and the model's `await_tasks` resumes its tool call with it. A
/// timer nothing waits on is dropped silently. Under a run cancel the
/// it. A timer nothing waits on is dropped silently. Under a run cancel the
/// drop wakes no one, since the cancel tears the waiter down with
/// every other chain.
fn drop_timer(&mut self, effect: EffectId) {
Expand All @@ -284,12 +283,7 @@ impl Scheduler {
{
return;
}
let chain = &mut self.chains[owner.index()];
chain.waiting_on.clear();
let answer = match chain.awaiting.take() {
Some(_) => Answer::ToolCallResult(Err(Error::Interrupted)),
None => Answer::JoinAny(Err(Error::Interrupted)),
};
self.wake_from_wait(owner, answer);
self.chains[owner.index()].waiting_on.clear();
self.wake_from_wait(owner, Answer::JoinAny(Err(Error::Interrupted)));
}
}
Loading
Loading