Skip to main content

rig_ecs/
stage.rs

1//! The stages every effect moves through within a frame. An effect is an
2//! entity whose components are its request and its outcome, such as an
3//! [`EffectRequest`] and, once answered, an [`Outcome`]; generic components
4//! say how far it got. Each run of the [`AgentPass`] schedule goes through
5//! the [`Stage`] sets in order, and the loop in `Update` ([`AgentLoop`])
6//! runs it again until nothing moved, so an effect spawned, answered or
7//! released goes as far as it can in that frame.
8//!
9//! A plugin steps in with an ordinary system in a stage that queries stage
10//! data, ordered by its phase ([`PrepareSystems`], [`ReviewSystems`],
11//! [`GateSystems`]) and against other plugins' public sets. Every effect is
12//! [`Admitted`] first, so every Prepare and Gate system sees it before it
13//! starts. A gate decides in the effect's [`Decision`] (the first decision
14//! wins), a reviewer edits the [`Outcome`] in place, and any entity
15//! [`HoldOf`] an effect keeps it where it is. A failing Prepare, Gate or
16//! Review system closes its stage for that pass. From a frame's second pass
17//! on, Bevy's clocks advance by zero, so timers count each frame once.
18//!
19//! ```
20//! use rig_ecs::prelude::*;
21//!
22//! // Refuse every effect of the kind `deploy` when it is about to start.
23//! fn no_deploys(mut effects: Query<(&EffectRequest, &mut Decision), (ReadyToStart, PluginEffects)>) {
24//!     for (request, mut decision) in &mut effects {
25//!         if request.kind.name() == "deploy" {
26//!             decision.decide(Decider::plugin("no-deploys"), Outcome::refused("no deploys today"));
27//!         }
28//!     }
29//! }
30//!
31//! let mut app = App::new();
32//! app.add_plugins(AgentPlugin)
33//!     .add_systems(AgentPass, no_deploys.in_set(Stage::Gate));
34//! ```
35
36use std::borrow::Cow;
37use std::cell::{Cell, RefCell};
38use std::collections::HashSet;
39use std::sync::Arc;
40use std::time::Duration;
41
42use bevy_app::prelude::*;
43use bevy_ecs::error::{BevyError, ErrorContext, ErrorHandler, FallbackErrorHandler};
44use bevy_ecs::lifecycle::HookContext;
45use bevy_ecs::prelude::*;
46use bevy_ecs::query::QueryData;
47use bevy_ecs::schedule::{
48    InternedSystemSet, LogLevel, ScheduleBuildSettings, ScheduleLabel, SingleThreadedExecutor,
49};
50use bevy_ecs::world::DeferredWorld;
51use bevy_log::tracing::Instrument;
52use bevy_log::{info_span, warn};
53use bevy_reflect::prelude::*;
54use bevy_tasks::{AsyncComputeTaskPool, TaskPool};
55use bevy_time::{Real, Time, Virtual};
56use rig_core::completion::CompletionResponse;
57use rig_core::effect::{
58    self, Decider, Disposition, EffectId, EffectKind, EffectRecord, HandlerKey, tool_key,
59};
60use rig_core::error::{ErrorKind, ErrorReport};
61use rig_core::serve::{DispatchContext, Recorder, Reply, cancelled};
62use rig_core::tool::{ToolExecutionError, ToolResult, ToolResultContext};
63use rig_core::{millis, now_ms};
64use web_time::SystemTime;
65
66use super::agent::{AgentId, CallOf, Conversation, Notice, Turn, TurnOutcome};
67use super::effects::{Effects, Handler, Ran};
68use super::inbox::{Incoming, accept, carry_on_accepted, message_kind, settle_unsent};
69use super::restore::{Interrupted, answer_interrupted, mark_started};
70use super::session_log::{Drain, SessionLog, save_now};
71use super::tasks::{Running, Wake, arrive};
72use super::tools::{
73    CallHandler, Footprint, ToolRequest, ToolStart, ToolStarts, complete_calls, compute_footprints,
74    redact_results, refuse_calls,
75};
76use super::turn::{
77    Adapters, DroppedOptions, ModelRequest, ModelStarts, SentTo, StreamDelta, TurnCall, back_off,
78    build_requests, carry_on_called, commit_replies, commit_turn_calls, end_turns, feed,
79    judge_failures, keeps_settled, model_pool, stop_calls, stream_partials, stream_reply,
80    wait_in_order,
81};
82
83/// One pass over the stages. Plugins add their systems to it in a
84/// [`Stage`]: `app.add_systems(AgentPass, gate.in_set(Stage::Gate))`.
85#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
86pub struct AgentPass;
87
88/// The system in `Update` that runs [`AgentPass`] until nothing moves. Work
89/// done after it in a frame, such as an effect spawned in `PostUpdate`, waits
90/// for the next frame: a system that spawns, answers or releases effects
91/// runs `.before(AgentLoop)`.
92#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
93pub struct AgentLoop;
94
95/// The stages of a pass, run in this order. The kernel's own work in each
96/// is in the [module](self) doc and the plugin guide.
97#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
98pub enum Stage {
99    /// Finished tasks become their effects' [`Outcome`]s, streamed text is
100    /// read ([`StreamDelta`]), and failed model calls get their
101    /// [`RetryVerdict`](super::turn::RetryVerdict).
102    Arrive,
103    /// Plugins read, edit in place or hold an [`Outcome`]
104    /// ([`ReadyToSettle`]), and a turn's proposed [`TurnOutcome`], in the
105    /// [`ReviewSystems`] phases. Before them the kernel redacts every
106    /// secret in a tool call's answer ([`rig_core::redact`]).
107    Review,
108    /// The kernel settles what is answered and unheld, writes its
109    /// [`EffectRecorded`], commits replies and their tool results, and ends the
110    /// turns nothing holds.
111    Commit,
112    /// The kernel admits what was spawned since the last pass, builds the
113    /// requests of [`TurnCall`]s and [`SideCall`](super::turn::SideCall)s,
114    /// and marks the calls that wait for an earlier one [`Waiting`].
115    Admit,
116    /// Plugins edit the requests admitted this pass (`Added<Admitted>`), in
117    /// the [`PrepareSystems`] phases.
118    Prepare,
119    /// Plugins hold ([`HoldOf`]) or decide ([`Decision`]) what is about to
120    /// start ([`ReadyToStart`]), once the kernel answered the tool calls it
121    /// refuses ([`ToolRequest`]).
122    Gate,
123    /// The kernel applies the decisions made since the last pass as
124    /// [`Outcome`]s, then starts what is admitted, unheld and unanswered: it
125    /// accepts messages ([`Incoming`]), and runs handlers, a model call on
126    /// its adapted, fitted request. What it does not run stays open,
127    /// [`Started`], until answered.
128    Start,
129}
130
131impl Stage {
132    /// Every stage, in pass order.
133    const ALL: [Self; 7] = [
134        Self::Arrive,
135        Self::Review,
136        Self::Commit,
137        Self::Admit,
138        Self::Prepare,
139        Self::Gate,
140        Self::Start,
141    ];
142}
143
144/// The phases of [`Stage::Prepare`], run in this order. A plugin puts a
145/// Prepare system in the phase of what it does, and so is ordered against
146/// every other plugin's, without naming them.
147#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
148pub enum PrepareSystems {
149    /// What came in is rewritten before anything is read from it: a
150    /// message's text, a tool call's arguments.
151    Input,
152    /// The parts that come from what a request names and from its agent go
153    /// in: the files a message names, a default system prompt, the
154    /// directory a tool call runs in.
155    Compose,
156    /// The composed request is edited as it will be sent: its prompt,
157    /// messages, tools and options, the model it goes to, its [`Adapters`],
158    /// a tool call's [`CallHandler`].
159    Edit,
160    /// The request as it will be sent ([`ModelStarts::sent`]) is measured
161    /// against the window of the model it goes to and fitted, such as by
162    /// compaction.
163    Fit,
164}
165
166/// The phases of [`Stage::Review`], run in this order.
167#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
168pub enum ReviewSystems {
169    /// An [`Outcome`], a failed model call's
170    /// [`RetryVerdict`](super::turn::RetryVerdict) or a turn's proposed
171    /// [`TurnOutcome`] is edited in place.
172    Edit,
173    /// What was reviewed is acted on: a failed call taken over by a
174    /// successor, a turn held or carried on, a result followed up.
175    Act,
176}
177
178/// The positions around [`Stage::Gate`], in this order: `FromRecord`,
179/// every gate (`in_set(Stage::Gate)`), `After`, then `Fallback`. A system
180/// that fails in `FromRecord` or `After` closes Gate for the pass, as a
181/// gate does.
182#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
183pub enum GateSystems {
184    /// Decides effects from a record of an earlier run of them, such as a
185    /// session replay, before every gate, so each gate sees the effect
186    /// decided. The record holds what every gate decided in that run; a
187    /// gate deciding again would judge today's settings and files instead.
188    FromRecord,
189    /// After every gate had its say: an answer that stands in for running
190    /// the effect (a cached result, a dry run, an editor's buffer), which
191    /// must not answer what a gate refused, and a coordinator of a group
192    /// that starts together or not at all. A decision made in Gate is
193    /// applied at Start, so here a decided effect still matches
194    /// [`ReadyToStart`]: read its [`Decision`]. A tool call the kernel refuses
195    /// is answered before every gate: read its [`Outcome`]. No gate sees an
196    /// effect held from its spawn, so a coordinator spawns its members
197    /// unheld and queries them without [`ReadyToStart`]. While any member is
198    /// held by another or [`Waiting`], it holds the free ones with holds of
199    /// its own; once none is, it refuses them all if one was refused or
200    /// answered with an error, else releases its holds.
201    After,
202    /// The kernel's own answers once every plugin had its say, such as an
203    /// [`Interrupted`] call's or an [`Unsent`](super::inbox::Unsent)
204    /// message's. A default plugin's fallback goes between
205    /// (`.after(GateSystems::After).before(GateSystems::Fallback)`), so it
206    /// sees what every plugin decided or held and the kernel has not
207    /// answered yet.
208    Fallback,
209}
210
211/// The kernel's systems that open a stage, before any of its own.
212#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
213struct Enter(Stage);
214
215/// Every effect. Spawning it takes the next effect id, and sets `parent`
216/// from the effect it is a `ChildOf`, when not given. Despawned before it
217/// settles, it is recorded as cancelled. Insert it once: inserting it again
218/// loses its id.
219#[derive(Component, Debug)]
220#[component(on_add = take_id, on_remove = record_unsettled)]
221#[require(Decision)]
222pub struct Effect {
223    id: EffectId,
224    /// The effect that asked for this one, in the record.
225    pub parent: Option<EffectId>,
226    /// What it is for, such as `"summary"`, in the record.
227    pub purpose: Option<Cow<'static, str>>,
228    /// How its first answer came.
229    answered: Option<Disposition>,
230    /// The first answer, which a reviewer may edit.
231    raw: Option<Result<effect::Answered, ErrorReport>>,
232    /// Who decided the answer, when the kernel applied a decision.
233    decided_by: Option<Decider>,
234}
235
236impl Default for Effect {
237    fn default() -> Self {
238        Self::with_parent_id(None)
239    }
240}
241
242impl Effect {
243    /// An effect asked for by `parent`, such as a successor keeping the
244    /// parent of the effect it supersedes.
245    pub fn with_parent_id(parent: Option<EffectId>) -> Self {
246        Self {
247            id: EffectId::from_raw(0),
248            parent,
249            purpose: None,
250            answered: None,
251            raw: None,
252            decided_by: None,
253        }
254    }
255
256    /// This effect, made for `purpose`.
257    #[must_use]
258    pub fn with_purpose(mut self, purpose: impl Into<Cow<'static, str>>) -> Self {
259        self.purpose = Some(purpose.into());
260        self
261    }
262
263    /// Its id; 0 until it is spawned.
264    pub fn id(&self) -> EffectId {
265        self.id
266    }
267
268    /// Whether its first answer came from its handler, which ran.
269    pub(crate) fn ran(&self) -> bool {
270        self.answered == Some(Disposition::Ran)
271    }
272}
273
274/// A plugin's rig-core effect: the kernel dispatches `kind` to `handler` at
275/// Start, recorded, and its answer becomes the [`Outcome`]. Without a
276/// handler it is open: Start marks it [`Started`], and it waits for any
277/// system to answer it.
278#[derive(Component, Clone)]
279#[require(Effect)]
280pub struct EffectRequest {
281    /// The effect.
282    pub kind: EffectKind,
283    /// What answers it, if the kernel runs it.
284    pub handler: Option<Handler>,
285}
286
287/// The agent a stage entity works for. Without one, the effect is the
288/// app's. Despawning the agent despawns its work.
289#[derive(Component, Reflect, Debug)]
290#[reflect(Component)]
291#[relationship(relationship_target = AgentWork)]
292pub struct ForAgent(pub Entity);
293
294/// The stage entities working for this agent.
295#[derive(Component, Reflect, Debug)]
296#[reflect(Component)]
297#[relationship_target(relationship = ForAgent, linked_spawn)]
298pub struct AgentWork(Vec<Entity>);
299
300/// Inserted by the kernel in [`Stage::Admit`] on every effect spawned since
301/// the last pass. Start starts only admitted effects.
302#[derive(Component, Reflect, Debug, Default)]
303#[component(storage = "SparseSet", on_add = moved::<Admitted>)]
304#[reflect(Component, Default)]
305pub struct Admitted;
306
307/// When the kernel started the effect.
308#[derive(Component, Clone, Copy, Debug)]
309#[component(on_add = moved::<Started>)]
310pub struct Started {
311    /// The wall-clock time.
312    pub at: SystemTime,
313}
314
315/// The effect's answer, applied by the kernel from its [`Decision`]. A
316/// reviewer edits it in place (`Query<&mut Outcome, ReadyToSettle>`), and the
317/// record keeps the first answer as its outcome and the edited one as the
318/// reviewed one. Applied to a running effect, it cancels the handler. A
319/// tool call's answer is its result: `Outcome::from(ToolResult)`.
320#[derive(Component, Clone, Debug)]
321#[component(on_insert = answered)]
322pub struct Outcome(pub Result<effect::Answered, ErrorReport>);
323
324impl Outcome {
325    /// A refusal for `why`, decided before the effect ran. A tool call's
326    /// refusal reaches the model as the tool's refusal.
327    pub fn refused(why: impl Into<String>) -> Self {
328        Self(Err(ErrorReport::new(ErrorKind::Denied, why).refused()))
329    }
330
331    /// The response a model call answered with, if it did.
332    pub fn completion(&self) -> Option<&CompletionResponse> {
333        match &self.0 {
334            Ok(effect::Answered::Completion(response)) => Some(response),
335            _ => None,
336        }
337    }
338
339    /// What a tool call answered this way returns to the model: its
340    /// result, or the failure as an error result.
341    pub fn tool_result(&self) -> ToolResult {
342        match &self.0 {
343            Ok(effect::Answered::ToolResult { result }) => result.clone(),
344            Ok(other) => ToolResult::failed(ToolExecutionError::other(format!(
345                "the tool answered with a {} outcome",
346                other.family()
347            ))),
348            Err(report) => ToolResult::failed(report.clone().into()),
349        }
350    }
351
352    /// An answer of a kind of its own, rig-core's custom outcome with
353    /// `payload`, such as a plugin's taking over a message.
354    pub fn custom(payload: impl Into<serde_json::Value>) -> Self {
355        Self(Ok(effect::Answered::Custom {
356            payload: payload.into(),
357        }))
358    }
359}
360
361impl From<ToolResult> for Outcome {
362    fn from(result: ToolResult) -> Self {
363        Self(Ok(effect::Answered::ToolResult { result }))
364    }
365}
366
367impl From<Result<effect::Answered, ErrorReport>> for Outcome {
368    fn from(outcome: Result<effect::Answered, ErrorReport>) -> Self {
369        Self(outcome)
370    }
371}
372
373/// Where an effect's answer is decided before the kernel applies it, on
374/// every effect: the first decision wins. Gates decide in Gate, and the
375/// owner of an open effect decides whenever its work is done, through
376/// `Query<&mut Decision>`, so Bevy sees two unordered deciders. The kernel
377/// applies the decision as the effect's [`Outcome`] when the pass reaches
378/// Start, unless the effect was answered meanwhile, and its record names
379/// who decided (`EffectRecord::decided_by`); it stays decided afterwards.
380/// An effect no one decided, such as a call every gate let run, records
381/// no decider. Until then the effect is still [`ReadyToStart`] for the gates
382/// after the one that decided: a gate that holds or asks checks
383/// [`is_decided`](Self::is_decided) first.
384#[derive(Component, Debug, Default)]
385pub struct Decision(Option<(Outcome, Decider)>);
386
387impl Decision {
388    /// Decides `outcome` for `by`, such as `Decider::plugin("protected-paths")`,
389    /// unless a decision was made; whether this one won.
390    pub fn decide(&mut self, by: Decider, outcome: impl Into<Outcome>) -> bool {
391        if self.0.is_some() {
392            return false;
393        }
394        self.0 = Some((outcome.into(), by));
395        true
396    }
397
398    /// The decision, if one was made.
399    pub fn get(&self) -> Option<&Outcome> {
400        self.0.as_ref().map(|(outcome, _)| outcome)
401    }
402
403    /// Who made the decision, if one was made.
404    pub fn by(&self) -> Option<&Decider> {
405        self.0.as_ref().map(|(_, by)| by)
406    }
407
408    /// Whether a decision was made.
409    pub fn is_decided(&self) -> bool {
410        self.0.is_some()
411    }
412}
413
414/// What the effect's answer is if it is stopped before it answers, such as
415/// a running command's output so far. Whoever runs the effect keeps it
416/// current; the kernel answers with it, in place of "interrupted" or
417/// "stopped", whenever it stops the effect: an
418/// [`StopTurn`](super::agent::StopTurn), its turn failing or stopping,
419/// its cancellation, and the app's exit, which logs it for the restart.
420#[derive(Component, Clone, Debug)]
421pub struct IfStopped(pub Outcome);
422
423/// What a handler published beside its answer, on its effect once
424/// answered, and recorded with it: a tool's facts, such as rig-core's
425/// `FilesChanged` and `ExitStatus`, or a fact a layer of a model call's
426/// handler published about the reply, such as an adapter's stream rewrite
427/// ([`PublishedContext::insert`](rig_core::tool::PublishedContext::insert)).
428/// Read a value with `published.0.get::<FilesChanged>()`.
429#[derive(Component, Clone, Debug)]
430pub struct Published(pub ToolResultContext);
431
432/// Keeps the effect it names where it is: not started, or not settled. Any
433/// entity can be a hold; despawn it, or remove this, to release it. Holds
434/// add up: releasing one never releases another's, and every release wakes
435/// the loop.
436#[derive(Component, Reflect, Debug)]
437#[component(on_remove = moved::<HoldOf>)]
438#[reflect(Component)]
439#[relationship(relationship_target = Holds)]
440pub struct HoldOf(pub Entity);
441
442/// The holds on an effect, removed with the last one. Despawning the
443/// effect despawns its holds.
444#[derive(Component, Reflect, Debug)]
445#[reflect(Component)]
446#[relationship_target(relationship = HoldOf, linked_spawn)]
447pub struct Holds(Vec<Entity>);
448
449/// This effect is replaced by the one it names, such as a call retried on
450/// another model, or each losing member of a race: it never starts, its
451/// task is cancelled if it runs, and its outcome is never applied. It
452/// settles at the next Commit, recorded as superseded. Any number of
453/// effects may name one successor: a retry is
454/// `commands.spawn(retry).add_related::<SupersededBy>(&[call])`, and each
455/// loser of a race gets `insert(SupersededBy(winner))`.
456#[derive(Component, Reflect, Debug)]
457#[component(on_add = moved::<SupersededBy>)]
458#[reflect(Component)]
459#[relationship(relationship_target = Supersedes)]
460pub struct SupersededBy(pub Entity);
461
462impl SupersededBy {
463    /// The successor.
464    pub fn successor(&self) -> Entity {
465        self.0
466    }
467}
468
469/// The effects this one replaces ([`SupersededBy`]).
470#[derive(Component, Reflect, Debug)]
471#[reflect(Component)]
472#[relationship_target(relationship = SupersededBy)]
473pub struct Supersedes(Vec<Entity>);
474
475/// The effect was committed. It stays until the next frame's loop starts,
476/// for `Added<Settled>` readers, then it is despawned.
477#[derive(Component, Reflect, Debug, Default)]
478#[component(storage = "SparseSet", on_add = moved::<Settled>)]
479#[reflect(Component, Default)]
480pub struct Settled;
481
482/// When an effect carrying `M` is superseded, moves `M`, and the hold the
483/// effect keeps ([`HoldOf`]), to the successor, so the plugin that owns the
484/// effect by `M` takes the successor's outcome. The successor's hold is
485/// inserted before the old one is removed, so the held effect is never
486/// unheld in between: `app.add_observer(follow_successor::<Summarizing>)`.
487pub fn follow_successor<M: Component>(
488    superseded: On<Add<SupersededBy>>,
489    owned: Query<(&SupersededBy, Option<&HoldOf>), With<M>>,
490    mut commands: Commands,
491) {
492    let old = superseded.entity;
493    let Ok((by, hold)) = owned.get(old) else {
494        return;
495    };
496    let new = by.successor();
497    let held = hold.map(|&HoldOf(held)| held);
498    commands.queue(move |world: &mut World| {
499        let Ok(mut to) = world.get_entity_mut(new) else {
500            return;
501        };
502        if let Some(held) = held {
503            to.insert(HoldOf(held));
504        }
505        let Ok(mut from) = world.get_entity_mut(old) else {
506            return;
507        };
508        let marker = from.take::<M>();
509        if held.is_some() {
510            from.remove::<HoldOf>();
511        }
512        if let (Some(marker), Ok(mut to)) = (marker, world.get_entity_mut(new)) {
513            to.insert(marker);
514        }
515    });
516}
517
518/// When an effect settles, despawns its `ChildOf` children carrying `M`
519/// that are still open, such as a request a gate holds when a stop ends
520/// the call that made it: each is recorded as cancelled, which ends
521/// whatever held it. `app.add_observer(despawn_open_children::<TaskCall>)`.
522pub fn despawn_open_children<M: Component>(
523    settled: On<Add<Settled>>,
524    children: Query<&Children>,
525    open: Query<(), (With<M>, Without<Settled>)>,
526    mut commands: Commands,
527) {
528    let children = children.get(settled.entity).into_iter().flatten();
529    for &child in children.filter(|&&child| open.contains(child)) {
530        commands.entity(child).despawn();
531    }
532}
533
534/// The record of every effect that settled or was cancelled, in settle
535/// order, and of the dispatches nested in their handlers. Only effects of a
536/// kind the kernel knows, such as an [`EffectRequest`], have a record.
537#[derive(Message, Clone, Debug)]
538pub struct EffectRecorded(pub EffectRecord);
539
540/// On a call of a model's reply that waits for an earlier call of that
541/// reply it must not run beside ([`Footprint`]) to settle. The kernel sets
542/// and clears it when [`Stage::Gate`] opens, from the calls as they stand
543/// then, so Gate sees each call when it is about to run.
544#[derive(Component, Reflect, Debug, Default)]
545#[component(storage = "SparseSet", on_remove = moved::<Waiting>)]
546#[reflect(Component, Default)]
547pub struct Waiting;
548
549/// Effects about to start: admitted, not waiting, not started, unanswered,
550/// unheld, not superseded and not a restored call the kernel answers
551/// ([`Interrupted`]). A gate's filter.
552pub type ReadyToStart = (
553    With<Admitted>,
554    Without<Waiting>,
555    Without<Started>,
556    Without<Outcome>,
557    Without<Holds>,
558    Without<Settled>,
559    Without<SupersededBy>,
560    Without<Interrupted>,
561);
562
563/// Answered effects nothing holds, not settled yet. A reviewer's filter;
564/// it matches on every pass until Commit settles the effect, so a reviewer
565/// edits once, keeping a marker on what it edited.
566pub type ReadyToSettle = (With<Outcome>, Without<Holds>, Without<Settled>);
567
568/// Tool calls ([`ToolRequest`]), so Bevy can see they are no other kind of
569/// effect: two queries of different kinds never conflict. Two queries in
570/// one system that write the same component must rule each other out like
571/// this, or the system panics when it is initialized.
572pub type ToolCalls = (
573    With<ToolRequest>,
574    Without<ModelRequest>,
575    Without<Incoming>,
576    Without<EffectRequest>,
577);
578
579/// Model calls whose request is built ([`ModelRequest`]).
580pub type ModelCalls = (
581    With<ModelRequest>,
582    Without<ToolRequest>,
583    Without<Incoming>,
584    Without<EffectRequest>,
585);
586
587/// Messages on their way to an agent ([`Incoming`]).
588pub type IncomingMessages = (
589    With<Incoming>,
590    Without<ToolRequest>,
591    Without<ModelRequest>,
592    Without<EffectRequest>,
593);
594
595/// Plugins' own effects ([`EffectRequest`]), such as a question. A system
596/// over one of them names its marker too, such as `With<Ask>`.
597pub type PluginEffects = (
598    With<EffectRequest>,
599    Without<ToolRequest>,
600    Without<ModelRequest>,
601    Without<Incoming>,
602);
603
604/// How many passes a frame runs at most.
605const MAX_PASSES: usize = 16;
606
607/// The loop's bookkeeping.
608#[derive(Resource, Default)]
609struct Passes {
610    /// Bumped by the hooks whenever a stage entity moves.
611    moved: u64,
612    /// `moved` when the loop last ended quiet.
613    quiet_at: u64,
614    /// What moved last, for the warning when the loop does not settle.
615    last: Option<(&'static str, Entity)>,
616    /// The failed systems already named in a notice.
617    noticed: HashSet<String>,
618}
619
620thread_local! {
621    /// The stage running on this thread, for the error handler.
622    static STAGE: Cell<Option<Stage>> = const { Cell::new(None) };
623    /// The systems that failed this pass.
624    static FAILED: RefCell<Vec<Failure>> = const { RefCell::new(Vec::new()) };
625    /// The app's error handler, which the kernel's forwards to.
626    static APP_HANDLER: Cell<Option<ErrorHandler>> = const { Cell::new(None) };
627}
628
629/// Adds the stages, the loop and their bookkeeping.
630pub(crate) fn build(app: &mut App) {
631    app.init_resource::<Passes>()
632        .add_message::<EffectRecorded>()
633        .add_message::<StreamDelta>()
634        .edit_schedule(AgentPass, |pass| {
635            pass.set_executor(SingleThreadedExecutor::new());
636            if cfg!(debug_assertions) {
637                // Unordered systems writing the same data in one stage are
638                // reported, as their order is meaningless: two deciders, two
639                // reviewers. Holds and spawns go through Commands, which
640                // Bevy does not see, and need no order.
641                pass.set_build_settings(ScheduleBuildSettings {
642                    ambiguity_detection: LogLevel::Warn,
643                    ..Default::default()
644                });
645            }
646        })
647        .configure_sets(
648            AgentPass,
649            (
650                Enter(Stage::Arrive),
651                Stage::Arrive,
652                Enter(Stage::Review),
653                Stage::Review,
654                Enter(Stage::Commit),
655                Stage::Commit,
656                Enter(Stage::Admit),
657                Stage::Admit,
658                Enter(Stage::Prepare),
659                Stage::Prepare,
660                Enter(Stage::Gate),
661                GateSystems::FromRecord,
662                Stage::Gate,
663                GateSystems::After,
664                GateSystems::Fallback,
665                Enter(Stage::Start),
666                Stage::Start,
667            )
668                .chain(),
669        )
670        .configure_sets(
671            AgentPass,
672            (
673                (
674                    PrepareSystems::Input,
675                    PrepareSystems::Compose,
676                    PrepareSystems::Edit,
677                    PrepareSystems::Fit,
678                )
679                    .chain()
680                    .in_set(Stage::Prepare),
681                (ReviewSystems::Edit, ReviewSystems::Act)
682                    .chain()
683                    .in_set(Stage::Review),
684            ),
685        )
686        .add_systems(
687            AgentPass,
688            // After every reviewer and before anything is committed: no
689            // key reaches the model, the conversation or a log, and no
690            // plugin has to order itself against it. A plugin that swaps
691            // secrets for placeholders in Review leaves nothing to redact.
692            redact_results.in_set(Enter(Stage::Commit)),
693        )
694        .configure_sets(
695            AgentPass,
696            (
697                Stage::Review,
698                Stage::Commit,
699                Stage::Admit,
700                Stage::Prepare,
701                GateSystems::FromRecord,
702                Stage::Gate,
703                GateSystems::After,
704                GateSystems::Fallback,
705                Stage::Start,
706            )
707                .distributive_run_if(moved_since_quiet),
708        )
709        .add_systems(
710            AgentPass,
711            (
712                (arrive, stream_partials, judge_failures)
713                    .chain()
714                    .in_set(Stage::Arrive),
715                (
716                    stop_calls,
717                    back_off.run_if(passed(Stage::Review)),
718                    settle,
719                    commit_replies,
720                    end_turns.run_if(passed(Stage::Review)),
721                    commit_turn_calls,
722                )
723                    .chain()
724                    .in_set(Stage::Commit),
725                (complete_calls, admit, carry_on_called, build_requests)
726                    .chain()
727                    .in_set(Stage::Admit),
728                (
729                    answer_unprepared,
730                    compute_footprints.run_if(moved_since_quiet),
731                    wait_in_order.run_if(moved_since_quiet),
732                )
733                    .chain()
734                    .in_set(Enter(Stage::Gate)),
735                refuse_calls
736                    .after(GateSystems::FromRecord)
737                    .before(Stage::Gate)
738                    .run_if(moved_since_quiet),
739                (answer_interrupted, settle_unsent)
740                    .chain()
741                    .in_set(GateSystems::Fallback),
742                apply_decisions.in_set(Enter(Stage::Start)),
743                (accept.run_if(passed(Stage::Gate)), carry_on_accepted, start)
744                    .chain()
745                    .in_set(Stage::Start),
746            ),
747        )
748        .add_systems(
749            Update,
750            run_passes
751                .in_set(AgentLoop)
752                .run_if(any_match_filter::<Or<(With<Effect>, With<Turn>)>>),
753        );
754    for stage in Stage::ALL {
755        app.add_systems(AgentPass, entering(stage).in_set(Enter(stage)));
756    }
757    // The conversations changed are saved between the stages, so a
758    // `ConversationChanged` reader in a later stage of the pass sees the change, and
759    // at the end of the pass; each save keeps followed components with
760    // their lines (`SaveConversations`).
761    let mut after: Option<InternedSystemSet> = None;
762    for stage in Stage::ALL {
763        let save = save_in_pass.before(Enter(stage));
764        match after {
765            Some(after) => app.add_systems(AgentPass, save.after(after)),
766            None => app.add_systems(AgentPass, save),
767        };
768        after = Some(match stage {
769            Stage::Gate => GateSystems::Fallback.intern(),
770            stage => stage.intern(),
771        });
772    }
773    app.add_systems(AgentPass, save_in_pass.after(Stage::Start));
774}
775
776/// Saves the agent conversations changed since it last ran, so a change
777/// made in a stage is announced to the later stages of the pass; one that
778/// saved any counts as a move, so the loop runs another pass, where the
779/// earlier stages see it too.
780fn save_in_pass(world: &mut World) {
781    if let Some(agent) = save_now(world)
782        && let Some(mut passes) = world.get_resource_mut::<Passes>()
783    {
784        saved_some(&mut passes, agent);
785    }
786}
787
788/// Counts the save of `agent`'s conversation as a move.
789fn saved_some(passes: &mut Passes, agent: Entity) {
790    passes.moved = passes.moved.wrapping_add(1);
791    passes.last = Some((std::any::type_name::<Conversation>(), agent));
792}
793
794/// The marker system that opens `stage`.
795fn entering(stage: Stage) -> impl Fn() + Clone + Send + Sync + 'static {
796    move || STAGE.set(Some(stage))
797}
798
799/// Whether anything moved since the loop last ended quiet: the stages after
800/// Arrive have nothing to do otherwise.
801fn moved_since_quiet(passes: Res<Passes>) -> bool {
802    passes.moved != passes.quiet_at
803}
804
805/// Despawns what settled last frame, but the calls of a reply still
806/// running, then runs passes until one moves nothing, with zero time steps
807/// after the first, and the kernel's error handler in place of the app's.
808fn run_passes(world: &mut World, settled: &mut QueryState<Entity, With<Settled>>) {
809    let done: Vec<Entity> = settled
810        .iter(world)
811        .filter(|&entity| !keeps_settled(world, entity))
812        .collect();
813    for entity in done {
814        // A settled effect may go with its parent.
815        world.try_despawn(entity).ok();
816    }
817    let mut clocks = None;
818    let app_handler = world.get_resource::<FallbackErrorHandler>().copied();
819    APP_HANDLER.set(Some(world.fallback_error_handler()));
820    world.insert_resource(FallbackErrorHandler(fail_closed));
821    let quiet = world.try_schedule_scope(AgentPass, |world, pass| {
822        (0..MAX_PASSES).any(|n| {
823            if n == 1 {
824                clocks = Some((
825                    zero_step::<()>(world),
826                    zero_step::<Real>(world),
827                    zero_step::<Virtual>(world),
828                ));
829            }
830            FAILED.with_borrow_mut(Vec::clear);
831            let before = moves(world);
832            pass.run(world);
833            moves(world) == before
834        })
835    });
836    STAGE.set(None);
837    FAILED.with_borrow_mut(Vec::clear);
838    APP_HANDLER.set(None);
839    match app_handler {
840        Some(handler) => world.insert_resource(handler),
841        None => {
842            world.remove_resource::<FallbackErrorHandler>();
843        }
844    }
845    if let Some((time, real, virtual_time)) = clocks {
846        put_back(world, time);
847        put_back(world, real);
848        put_back(world, virtual_time);
849    }
850    let Some(mut passes) = world.get_resource_mut::<Passes>() else {
851        return;
852    };
853    match quiet {
854        Ok(true) => passes.quiet_at = passes.moved,
855        Ok(false) => match passes.last {
856            Some((what, entity)) => warn!(
857                "the agent loop still moved after {MAX_PASSES} passes; the last change: {what} \
858                 on {entity}"
859            ),
860            None => warn!("the agent loop still moved after {MAX_PASSES} passes"),
861        },
862        Err(_) => {}
863    }
864}
865
866/// The counter of moves.
867fn moves(world: &World) -> u64 {
868    world
869        .get_resource::<Passes>()
870        .map_or(0, |passes| passes.moved)
871}
872
873/// Steps the clock `Time<T>` by zero (the same elapsed time, a zero
874/// delta), and returns it as it was, to put back after the passes. A change
875/// a stage system makes to a clock after the first pass, such as a pause,
876/// is lost then.
877fn zero_step<T: Default + Copy + Send + Sync + 'static>(world: &mut World) -> Option<Time<T>> {
878    let mut time = world.get_resource_mut::<Time<T>>()?;
879    let was = *time;
880    time.advance_by(Duration::ZERO);
881    Some(was)
882}
883
884/// Puts back a clock [`zero_step`] took.
885fn put_back<T: Default + Copy + Send + Sync + 'static>(world: &mut World, was: Option<Time<T>>) {
886    if let (Some(was), Some(mut time)) = (was, world.get_resource_mut::<Time<T>>()) {
887        *time = was;
888    }
889}
890
891/// The error handler during the passes: notes a system that failed in a
892/// stage, then hands the error to the app's handler.
893fn fail_closed(error: BevyError, context: ErrorContext) {
894    let in_system = matches!(
895        context,
896        ErrorContext::System { .. } | ErrorContext::RunCondition { .. }
897    );
898    if in_system && let Some(stage) = STAGE.get() {
899        let message = error.to_string();
900        let failure = Failure {
901            stage,
902            system: context.to_string(),
903            error: message.lines().next().unwrap_or_default().to_owned(),
904        };
905        FAILED.with_borrow_mut(|failed| failed.push(failure));
906    }
907    let handler = APP_HANDLER
908        .get()
909        .unwrap_or(FallbackErrorHandler::default().0);
910    handler(error, context);
911}
912
913/// A system that failed in a stage.
914#[derive(Clone)]
915pub(crate) struct Failure {
916    stage: Stage,
917    /// Which system, as Bevy names it: "System `name` failed".
918    system: String,
919    /// The first line of its error.
920    error: String,
921}
922
923/// The first system that failed in `stage` this pass.
924pub(crate) fn failed_in(stage: Stage) -> Option<Failure> {
925    FAILED.with_borrow(|failed| failed.iter().find(|failed| failed.stage == stage).cloned())
926}
927
928/// Whether no system failed in `stage` this pass: a run condition, read
929/// right before its system because the pass runs single-threaded.
930fn passed(stage: Stage) -> impl Fn() -> bool + Clone {
931    move || failed_in(stage).is_none()
932}
933
934/// The notice that a system failed and what that closed, once per system
935/// and stage.
936fn closed(passes: &mut Passes, failure: Failure, what: &str) -> Option<Notice> {
937    let Failure {
938        stage,
939        system,
940        error,
941    } = failure;
942    let text = format!("{system} in {stage:?} ({error}): {what}.");
943    passes
944        .noticed
945        .insert(format!("{stage:?} {system}"))
946        .then(|| Notice::error(None, text))
947}
948
949/// Counts a move of a stage entity, by the component `C`.
950pub(crate) fn moved<C: Component>(mut world: DeferredWorld, context: HookContext) {
951    if let Some(mut passes) = world.get_resource_mut::<Passes>() {
952        passes.moved = passes.moved.wrapping_add(1);
953        passes.last = Some((std::any::type_name::<C>(), context.entity));
954    }
955}
956
957/// Gives a new effect its id, and its parent from the effect it is a
958/// `ChildOf`.
959fn take_id(mut world: DeferredWorld, context: HookContext) {
960    let entity = context.entity;
961    let id = world.get_resource::<Effects>().map(Effects::next_id);
962    let parent = world
963        .get::<ChildOf>(entity)
964        .and_then(|of| world.get::<Effect>(of.parent()))
965        .map(Effect::id);
966    if let Some(mut effect) = world.get_mut::<Effect>(entity) {
967        if let Some(id) = id {
968            effect.id = id;
969        }
970        if effect.parent.is_none() {
971            effect.parent = parent;
972        }
973    }
974    moved::<Effect>(world, context);
975}
976
977/// Records an effect despawned before it settled as cancelled.
978fn record_unsettled(mut world: DeferredWorld, context: HookContext) {
979    if world.get::<Settled>(context.entity).is_some() {
980        return;
981    }
982    if let Some(record) = assemble(&world, context.entity, Some(Disposition::Cancelled)) {
983        world.write_message(EffectRecorded(record));
984    }
985}
986
987/// Notes how an effect's first answer came, and keeps it for the record.
988fn answered(mut world: DeferredWorld, context: HookContext) {
989    let entity = context.entity;
990    let started = world.get::<Started>(entity).is_some();
991    let running = world.get::<Running>(entity).is_some();
992    let first = world
993        .get::<Effect>(entity)
994        .filter(|effect| effect.answered.is_none())
995        .and_then(|_| world.get::<Outcome>(entity))
996        .map(|outcome| outcome.0.clone());
997    if let (Some(first), Some(mut effect)) = (first, world.get_mut::<Effect>(entity)) {
998        effect.answered = Some(match (started, running) {
999            (false, _) => Disposition::AnsweredBeforeStart,
1000            (true, true) => Disposition::AnsweredWhileRunning,
1001            (true, false) => Disposition::Ran,
1002        });
1003        effect.raw = Some(first);
1004    }
1005    moved::<Outcome>(world, context);
1006}
1007
1008/// Applies each new decision as its effect's [`Outcome`], in the order the
1009/// effects were spawned, unless it was answered meanwhile. The decision
1010/// stays, so it still reads as decided.
1011fn apply_decisions(
1012    mut decided: Query<
1013        (Entity, &mut Effect, &Decision),
1014        (Changed<Decision>, Without<Outcome>, Without<Settled>),
1015    >,
1016    mut commands: Commands,
1017) {
1018    let mut decided: Vec<_> = decided
1019        .iter_mut()
1020        .filter_map(|(entity, effect, decision)| Some((entity, effect, decision.0.as_ref()?)))
1021        .collect();
1022    decided.sort_unstable_by_key(|(_, effect, _)| effect.id());
1023    for (entity, mut effect, (outcome, by)) in decided {
1024        effect.decided_by = Some(by.clone());
1025        commands.entity(entity).try_insert(outcome.clone());
1026    }
1027}
1028
1029/// Admits every effect spawned since the last pass.
1030fn admit(
1031    new: Query<Entity, (With<Effect>, Without<Admitted>, Without<Settled>)>,
1032    mut commands: Commands,
1033) {
1034    for entity in &new {
1035        commands.entity(entity).try_insert(Admitted);
1036    }
1037}
1038
1039/// After a Prepare system failed, answers what was admitted this pass with
1040/// an error: its preparation is unknown, and edits do not run twice.
1041fn answer_unprepared(
1042    admitted: Query<Entity, Added<Admitted>>,
1043    mut passes: ResMut<Passes>,
1044    mut commands: Commands,
1045    mut notices: MessageWriter<Notice>,
1046) {
1047    let Some(failure) = failed_in(Stage::Prepare) else {
1048        return;
1049    };
1050    let why = format!("{} in Prepare: {}", failure.system, failure.error);
1051    for entity in &admitted {
1052        let failed = ErrorReport::new(ErrorKind::Internal, why.clone());
1053        commands
1054            .entity(entity)
1055            .try_insert_if_new(Outcome(Err(failed)));
1056    }
1057    let what = "what it admitted is answered with an error";
1058    notices.write_batch(closed(&mut passes, failure, what));
1059}
1060
1061/// The agents' conversations as Start marks calls on them, and saves them.
1062type Saving<'w, 's> = ParamSet<
1063    'w,
1064    's,
1065    (
1066        Drain<'static, 'static>,
1067        Query<'static, 'static, &'static mut Conversation>,
1068    ),
1069>;
1070
1071/// Starts every admitted, unheld, unanswered, undecided effect, unless a
1072/// Gate system failed this pass: an effect request with a handler is
1073/// dispatched on the kernel's recorded path, a model call and a tool call
1074/// as [`ModelStarts`] and [`ToolStarts`] say, and any other effect stays
1075/// open.
1076fn start(
1077    mut ready: Query<StartItem, ReadyToStart>,
1078    agents: Query<&AgentId>,
1079    turns: Query<&TurnOutcome>,
1080    (tools, models): (ToolStarts, ModelStarts),
1081    effects: Res<Effects>,
1082    wake: Res<Wake>,
1083    (log, mut saving): (Res<SessionLog>, Saving),
1084    mut passes: ResMut<Passes>,
1085    mut commands: Commands,
1086    mut notices: MessageWriter<Notice>,
1087) {
1088    if let Some(failure) = failed_in(Stage::Gate) {
1089        if !ready.is_empty() {
1090            let what = "nothing started this pass";
1091            notices.write_batch(closed(&mut passes, failure, what));
1092        }
1093        return;
1094    }
1095    let at = SystemTime::now();
1096    let stopping = |item: &StartItemReadOnlyItem| {
1097        let ending = item.of.and_then(|of| turns.get(of.0).ok());
1098        ending.and_then(TurnOutcome::stops).is_some()
1099    };
1100    // Before a call that may change something starts, the reply that asked
1101    // for it and the call's mark are written to the store (not yet synced
1102    // to disk). When they cannot be, no such call starts: a restart could
1103    // not tell that it may have run.
1104    let changing: Vec<_> = ready
1105        .iter()
1106        .filter(|item| !item.message && !item.decision.is_decided() && !stopping(item))
1107        .filter(|item| !item.footprint.is_some_and(|it| it.0.reads_only()))
1108        .filter_map(|item| Some((item.agent?.0, item.tool?.call.id.clone())))
1109        .collect();
1110    let mut unsaved = None;
1111    if !changing.is_empty() {
1112        let mut conversations = saving.p1();
1113        for (agent, call) in &changing {
1114            if let Ok(mut conversation) = conversations.get_mut(*agent) {
1115                mark_started(&mut conversation, call);
1116            }
1117        }
1118        if let Some(agent) = saving.p0().save() {
1119            saved_some(&mut passes, agent);
1120        }
1121        unsaved = log.flush().err().map(|failure| {
1122            let why = format!(
1123                "Not run: the session can no longer be saved ({failure}). Nothing changed."
1124            );
1125            ErrorReport::new(ErrorKind::Internal, why)
1126        });
1127    }
1128    // In the order they were spawned, such as a reply's calls in call order.
1129    let mut ready: Vec<_> = ready.iter_mut().collect();
1130    ready.sort_unstable_by_key(|item| item.effect.id());
1131    for item in ready {
1132        // Messages are accepted before. An effect decided since the
1133        // decisions were applied, such as by an observer of an answer
1134        // they applied, is answered by the next pass's.
1135        if item.message || item.decision.is_decided() {
1136            continue;
1137        }
1138        let ending = item.of.and_then(|of| turns.get(of.0).ok());
1139        if ending.and_then(TurnOutcome::stops).is_some() {
1140            continue;
1141        }
1142        let (entity, effect) = (item.entity, item.effect);
1143        let changes = item.tool.is_some() && !item.footprint.is_some_and(|it| it.0.reads_only());
1144        if let Some(unsaved) = unsaved.as_ref().filter(|_| changes) {
1145            commands
1146                .entity(entity)
1147                .try_insert_if_new(Outcome(Err(unsaved.clone())));
1148            continue;
1149        }
1150        let agent = item.agent.map(|agent| agent.0);
1151        let caller = agent.and_then(|agent| agents.get(agent).ok());
1152        let scope = caller.map(|id| id.0.as_str()).unwrap_or_default();
1153        let origin = DispatchContext {
1154            parent: effect.parent,
1155            scope: caller.map(|id| Arc::from(id.0.as_str())),
1156            purpose: effect.purpose.clone(),
1157        };
1158        let mut started = commands.entity(entity);
1159        if let Some(mut model) = item.model {
1160            let (connection, mut dropped) = match models.start(&mut model, item.adapters, agent) {
1161                Ok(started) => started,
1162                Err(report) => {
1163                    started.try_insert_if_new(Outcome(Err(report)));
1164                    continue;
1165                }
1166            };
1167            let kind = EffectKind::Completion {
1168                request: model.request.clone(),
1169                stream: true,
1170            };
1171            dropped.extend(connection.handler.0.check(&kind));
1172            let reference = connection.spec.reference();
1173            let span =
1174                info_span!("model_call", agent = scope, effect = %effect.id, model = reference);
1175            let (sender, feed) = feed();
1176            let wake_up = wake.clone();
1177            let consume = move |reply| stream_reply(reply, sender, wake_up);
1178            let handler = connection.handler.0.clone();
1179            let work = effects.run(effect.id, origin, handler, kind, consume);
1180            let running = Running::finishing(model_pool(), &wake, work.instrument(span), answer);
1181            started.try_insert((
1182                Started { at },
1183                running,
1184                feed,
1185                SentTo(connection),
1186                DroppedOptions(dropped),
1187            ));
1188            continue;
1189        }
1190        let (handler, kind) = match (item.request, item.tool) {
1191            (
1192                Some(EffectRequest {
1193                    kind,
1194                    handler: Some(handler),
1195                }),
1196                _,
1197            ) => (handler.clone(), kind.clone()),
1198            (_, Some(ToolRequest { call })) => match tools.start(call, item.handler, agent) {
1199                ToolStart::Run(handler, kind) => (handler, kind),
1200                ToolStart::Open(open, tool) => {
1201                    commands.queue(move |world: &mut World| {
1202                        if let Err(report) = open(world, tool, entity, Started { at })
1203                            && let Ok(mut call) = world.get_entity_mut(entity)
1204                        {
1205                            call.insert_if_new(Outcome(Err(report)));
1206                        }
1207                    });
1208                    continue;
1209                }
1210                ToolStart::Refused(report) => {
1211                    started.try_insert_if_new(Outcome(Err(report)));
1212                    continue;
1213                }
1214            },
1215            _ => {
1216                started.try_insert(Started { at });
1217                continue;
1218            }
1219        };
1220        let span = match &kind {
1221            EffectKind::ToolCall { name, .. } => {
1222                info_span!("tool_call", agent = scope, tool = name.as_str(), parent = ?effect.parent)
1223            }
1224            other => info_span!("effect", kind = other.name()),
1225        };
1226        let work = effects.run(effect.id, origin, handler.0, kind, Reply::into_outcome);
1227        let running = Running::finishing(effect_pool(), &wake, work.instrument(span), answer);
1228        started.try_insert((Started { at }, running));
1229    }
1230}
1231
1232/// Puts what a handler's run left on its effect, unless it was answered
1233/// meanwhile: what a tool published, then the outcome.
1234fn answer(mut entity: EntityWorldMut, (outcome, published): Ran) {
1235    if entity.contains::<Outcome>() {
1236        return;
1237    }
1238    if let Some(published) = published {
1239        entity.insert(Published(published));
1240    }
1241    entity.insert(Outcome(outcome));
1242}
1243
1244/// An effect as Start finds it.
1245#[derive(QueryData)]
1246#[query_data(mutable)]
1247struct StartItem {
1248    entity: Entity,
1249    effect: &'static Effect,
1250    decision: &'static Decision,
1251    request: Option<&'static EffectRequest>,
1252    tool: Option<&'static ToolRequest>,
1253    model: Option<&'static mut ModelRequest>,
1254    adapters: Option<&'static Adapters>,
1255    turn_call: Has<TurnCall>,
1256    message: Has<Incoming>,
1257    handler: Option<&'static CallHandler>,
1258    footprint: Option<&'static Footprint>,
1259    agent: Option<&'static ForAgent>,
1260    of: Option<&'static CallOf>,
1261}
1262
1263/// Plugin effects run on the async compute pool, as tool calls do.
1264fn effect_pool() -> &'static AsyncComputeTaskPool {
1265    AsyncComputeTaskPool::get_or_init(TaskPool::default)
1266}
1267
1268/// The effects Commit settles: answered and unheld, or superseded.
1269type Due = (
1270    With<Effect>,
1271    Without<Settled>,
1272    Or<((With<Outcome>, Without<Holds>), With<SupersededBy>)>,
1273);
1274
1275/// Settles the answered effects nothing holds, and the superseded ones, in
1276/// id order, unless a Review system failed this pass: cancels a handler
1277/// still running, and writes each one's record. Then writes the records of
1278/// dispatches nested in handlers.
1279fn settle(
1280    world: &mut World,
1281    due: &mut QueryState<(Entity, &Effect), Due>,
1282    live: &mut QueryState<&Effect, Without<Settled>>,
1283) {
1284    let mut settling: Vec<(EffectId, Entity)> = due
1285        .iter(world)
1286        .map(|(entity, effect)| (effect.id, entity))
1287        .collect();
1288    if let Some(failure) = failed_in(Stage::Review) {
1289        let what = "nothing settled this pass";
1290        let notice = world
1291            .get_resource_mut::<Passes>()
1292            .filter(|_| !settling.is_empty())
1293            .and_then(|mut passes| closed(&mut passes, failure, what));
1294        if let Some(notice) = notice {
1295            world.write_message(notice);
1296        }
1297        return;
1298    }
1299    settling.sort_unstable();
1300    for (_, entity) in settling {
1301        let superseded = world.get::<SupersededBy>(entity).is_some();
1302        settle_now(world, entity, superseded.then_some(Disposition::Superseded));
1303    }
1304    record_nested(world, live);
1305}
1306
1307/// Settles `entity` now, its record's disposition overridden by
1308/// `disposition`: a task still running is dropped, which cancels it, it is
1309/// [`Settled`], and its record is written.
1310fn settle_now(world: &mut World, entity: Entity, disposition: Option<Disposition>) {
1311    let record = assemble(world, entity, disposition);
1312    if let Ok(mut effect) = world.get_entity_mut(entity) {
1313        effect.remove::<Running>().insert(Settled);
1314    }
1315    if let Some(record) = record {
1316        world.write_message(EffectRecorded(record));
1317    }
1318}
1319
1320/// Despawns `holds`, the holds on an effect or a turn's outcome.
1321pub(crate) fn release(commands: &mut Commands, holds: Option<&Holds>) {
1322    for hold in holds.into_iter().flat_map(Holds::iter) {
1323        commands.entity(hold).try_despawn();
1324    }
1325}
1326
1327/// Settles `entity` at once, cancelled, if it is an unsettled effect, such
1328/// as a call of a turn being interrupted: whatever holds it is despawned,
1329/// it is answered with its [`IfStopped`], else `why`, unless it has an
1330/// answer, its task is cancelled, and its record is written.
1331pub(crate) fn cancel(world: &mut World, entity: Entity, why: &str) {
1332    let unsettled = world
1333        .get_entity(entity)
1334        .is_ok_and(|effect| effect.contains::<Effect>() && !effect.contains::<Settled>());
1335    if !unsettled {
1336        return;
1337    }
1338    if let Ok(mut effect) = world.get_entity_mut(entity) {
1339        effect.despawn_related::<Holds>();
1340    }
1341    let Ok(mut effect) = world.get_entity_mut(entity) else {
1342        return;
1343    };
1344    if !effect.contains::<Outcome>() {
1345        let stopped = match effect.get::<IfStopped>() {
1346            Some(IfStopped(outcome)) => outcome.clone(),
1347            None => Outcome(Err(ErrorReport::new(ErrorKind::Cancelled, why))),
1348        };
1349        effect.insert(stopped);
1350    }
1351    settle_now(world, entity, Some(Disposition::Cancelled));
1352}
1353
1354/// Writes the resolved records that belong to no unsettled effect: the
1355/// dispatches nested in handlers.
1356fn record_nested(world: &mut World, live: &mut QueryState<&Effect, Without<Settled>>) {
1357    let Some(buffer) = world
1358        .get_resource::<Effects>()
1359        .map(|effects| effects.buffer().clone())
1360    else {
1361        return;
1362    };
1363    if buffer.resolved() == 0 {
1364        return;
1365    }
1366    let live: HashSet<EffectId> = live.iter(world).map(Effect::id).collect();
1367    for record in buffer.take_resolved_where(|id| !live.contains(&id)) {
1368        world.write_message(EffectRecorded(record));
1369    }
1370}
1371
1372/// The record of `entity`: the handler's record, taken out of the kernel's
1373/// buffer, when there is one, else one made from the entity.
1374/// `disposition` overrides how its answer came.
1375fn assemble(
1376    world: &World,
1377    entity: Entity,
1378    disposition: Option<Disposition>,
1379) -> Option<EffectRecord> {
1380    let buffer = world.get_resource::<Effects>()?.buffer();
1381    let effect = world.get::<Effect>(entity)?;
1382    let current = world
1383        .get::<Outcome>(entity)
1384        .map(|outcome| outcome.0.clone());
1385    // Applied, an outcome's ticks are equal; a review moves only `changed`.
1386    let reviewed = world
1387        .get_entity(entity)
1388        .ok()
1389        .and_then(|entity| entity.get_change_ticks::<Outcome>())
1390        .is_some_and(|ticks| ticks.changed != ticks.added);
1391    let (outcome, reviewed) = match (reviewed, &effect.raw, current) {
1392        (true, Some(raw), current) => (Some(raw.clone()), current),
1393        (_, _, current) => (current, None),
1394    };
1395    let disposition = disposition
1396        .or(effect.answered.filter(|_| outcome.is_some()))
1397        .unwrap_or(Disposition::Cancelled);
1398    let outcome = outcome.unwrap_or_else(|| match disposition {
1399        Disposition::Superseded => Err(ErrorReport::new(
1400            ErrorKind::Cancelled,
1401            "superseded before it was answered",
1402        )),
1403        _ => Err(cancelled()),
1404    });
1405    let id = effect.id;
1406    let handled = buffer.take(id).or_else(|| {
1407        // Still in flight: its answer came from elsewhere, or none did.
1408        buffer.contains(id).then(|| {
1409            buffer.resolve(id, outcome.clone());
1410            buffer.take(id)
1411        })?
1412    });
1413    let mut record = match handled {
1414        Some(mut record) => {
1415            if disposition != Disposition::Ran {
1416                record.outcome = outcome;
1417            }
1418            record
1419        }
1420        None => {
1421            let (key, kind) = kind_of(world, entity)?;
1422            EffectRecord {
1423                published: world
1424                    .get::<Published>(entity)
1425                    .map(|published| published.0.clone()),
1426                id,
1427                key,
1428                kind,
1429                outcome,
1430                events: None,
1431                stream_origin: None,
1432                parent: effect.parent,
1433                scope: scope_of(world, entity),
1434                purpose: effect.purpose.clone(),
1435                turn: None,
1436                started_ms: world
1437                    .get::<Started>(entity)
1438                    .map(|started| millis(started.at)),
1439                finished_ms: Some(now_ms()),
1440                reviewed: None,
1441                disposition: Disposition::Ran,
1442                decided_by: None,
1443            }
1444        }
1445    };
1446    record.reviewed = reviewed;
1447    record.disposition = disposition;
1448    record.decided_by.clone_from(&effect.decided_by);
1449    record.turn = world
1450        .get::<CallOf>(entity)
1451        .and_then(|call| world.get::<Turn>(call.0))
1452        .map(|turn| turn.index);
1453    Some(record)
1454}
1455
1456/// The handler key and kind of an effect the kernel knows.
1457fn kind_of(world: &World, entity: Entity) -> Option<(HandlerKey, EffectKind)> {
1458    if let Some(ToolRequest { call }) = world.get::<ToolRequest>(entity) {
1459        let name = call.function.name.as_str();
1460        let key = world
1461            .get::<CallHandler>(entity)
1462            .map_or_else(|| tool_key(name), |handler| handler.0.0.key().clone());
1463        return Some((key, EffectKind::from(call)));
1464    }
1465    if let Some(model) = world.get::<ModelRequest>(entity) {
1466        let key = model.connection.as_ref().map_or_else(
1467            || HandlerKey::new("completion"),
1468            |connection| connection.handler.0.key().clone(),
1469        );
1470        let kind = EffectKind::Completion {
1471            request: model.request.clone(),
1472            stream: true,
1473        };
1474        return Some((key, kind));
1475    }
1476    if let Some(message) = world.get::<Incoming>(entity) {
1477        return Some(message_kind(message));
1478    }
1479    let request = world.get::<EffectRequest>(entity)?;
1480    let key = match &request.handler {
1481        Some(handler) => handler.0.key().clone(),
1482        None => match &request.kind {
1483            EffectKind::ToolCall { name, .. } => tool_key(name),
1484            other => HandlerKey::new(other.name()),
1485        },
1486    };
1487    Some((key, request.kind.clone()))
1488}
1489
1490/// The stable id of the agent the effect works for.
1491fn scope_of(world: &World, entity: Entity) -> Option<Arc<str>> {
1492    let agent = world.get::<ForAgent>(entity)?.0;
1493    world
1494        .get::<AgentId>(agent)
1495        .map(|id| Arc::from(id.0.as_str()))
1496}