1use 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#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
86pub struct AgentPass;
87
88#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
93pub struct AgentLoop;
94
95#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
98pub enum Stage {
99 Arrive,
103 Review,
108 Commit,
112 Admit,
116 Prepare,
119 Gate,
123 Start,
129}
130
131impl Stage {
132 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#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
148pub enum PrepareSystems {
149 Input,
152 Compose,
156 Edit,
160 Fit,
164}
165
166#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
168pub enum ReviewSystems {
169 Edit,
173 Act,
176}
177
178#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
183pub enum GateSystems {
184 FromRecord,
189 After,
202 Fallback,
209}
210
211#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
213struct Enter(Stage);
214
215#[derive(Component, Debug)]
220#[component(on_add = take_id, on_remove = record_unsettled)]
221#[require(Decision)]
222pub struct Effect {
223 id: EffectId,
224 pub parent: Option<EffectId>,
226 pub purpose: Option<Cow<'static, str>>,
228 answered: Option<Disposition>,
230 raw: Option<Result<effect::Answered, ErrorReport>>,
232 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 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 #[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 pub fn id(&self) -> EffectId {
265 self.id
266 }
267
268 pub(crate) fn ran(&self) -> bool {
270 self.answered == Some(Disposition::Ran)
271 }
272}
273
274#[derive(Component, Clone)]
279#[require(Effect)]
280pub struct EffectRequest {
281 pub kind: EffectKind,
283 pub handler: Option<Handler>,
285}
286
287#[derive(Component, Reflect, Debug)]
290#[reflect(Component)]
291#[relationship(relationship_target = AgentWork)]
292pub struct ForAgent(pub Entity);
293
294#[derive(Component, Reflect, Debug)]
296#[reflect(Component)]
297#[relationship_target(relationship = ForAgent, linked_spawn)]
298pub struct AgentWork(Vec<Entity>);
299
300#[derive(Component, Reflect, Debug, Default)]
303#[component(storage = "SparseSet", on_add = moved::<Admitted>)]
304#[reflect(Component, Default)]
305pub struct Admitted;
306
307#[derive(Component, Clone, Copy, Debug)]
309#[component(on_add = moved::<Started>)]
310pub struct Started {
311 pub at: SystemTime,
313}
314
315#[derive(Component, Clone, Debug)]
321#[component(on_insert = answered)]
322pub struct Outcome(pub Result<effect::Answered, ErrorReport>);
323
324impl Outcome {
325 pub fn refused(why: impl Into<String>) -> Self {
328 Self(Err(ErrorReport::new(ErrorKind::Denied, why).refused()))
329 }
330
331 pub fn completion(&self) -> Option<&CompletionResponse> {
333 match &self.0 {
334 Ok(effect::Answered::Completion(response)) => Some(response),
335 _ => None,
336 }
337 }
338
339 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 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#[derive(Component, Debug, Default)]
385pub struct Decision(Option<(Outcome, Decider)>);
386
387impl Decision {
388 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 pub fn get(&self) -> Option<&Outcome> {
400 self.0.as_ref().map(|(outcome, _)| outcome)
401 }
402
403 pub fn by(&self) -> Option<&Decider> {
405 self.0.as_ref().map(|(_, by)| by)
406 }
407
408 pub fn is_decided(&self) -> bool {
410 self.0.is_some()
411 }
412}
413
414#[derive(Component, Clone, Debug)]
421pub struct IfStopped(pub Outcome);
422
423#[derive(Component, Clone, Debug)]
430pub struct Published(pub ToolResultContext);
431
432#[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#[derive(Component, Reflect, Debug)]
445#[reflect(Component)]
446#[relationship_target(relationship = HoldOf, linked_spawn)]
447pub struct Holds(Vec<Entity>);
448
449#[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 pub fn successor(&self) -> Entity {
465 self.0
466 }
467}
468
469#[derive(Component, Reflect, Debug)]
471#[reflect(Component)]
472#[relationship_target(relationship = SupersededBy)]
473pub struct Supersedes(Vec<Entity>);
474
475#[derive(Component, Reflect, Debug, Default)]
478#[component(storage = "SparseSet", on_add = moved::<Settled>)]
479#[reflect(Component, Default)]
480pub struct Settled;
481
482pub 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
518pub 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#[derive(Message, Clone, Debug)]
538pub struct EffectRecorded(pub EffectRecord);
539
540#[derive(Component, Reflect, Debug, Default)]
545#[component(storage = "SparseSet", on_remove = moved::<Waiting>)]
546#[reflect(Component, Default)]
547pub struct Waiting;
548
549pub 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
563pub type ReadyToSettle = (With<Outcome>, Without<Holds>, Without<Settled>);
567
568pub type ToolCalls = (
573 With<ToolRequest>,
574 Without<ModelRequest>,
575 Without<Incoming>,
576 Without<EffectRequest>,
577);
578
579pub type ModelCalls = (
581 With<ModelRequest>,
582 Without<ToolRequest>,
583 Without<Incoming>,
584 Without<EffectRequest>,
585);
586
587pub type IncomingMessages = (
589 With<Incoming>,
590 Without<ToolRequest>,
591 Without<ModelRequest>,
592 Without<EffectRequest>,
593);
594
595pub type PluginEffects = (
598 With<EffectRequest>,
599 Without<ToolRequest>,
600 Without<ModelRequest>,
601 Without<Incoming>,
602);
603
604const MAX_PASSES: usize = 16;
606
607#[derive(Resource, Default)]
609struct Passes {
610 moved: u64,
612 quiet_at: u64,
614 last: Option<(&'static str, Entity)>,
616 noticed: HashSet<String>,
618}
619
620thread_local! {
621 static STAGE: Cell<Option<Stage>> = const { Cell::new(None) };
623 static FAILED: RefCell<Vec<Failure>> = const { RefCell::new(Vec::new()) };
625 static APP_HANDLER: Cell<Option<ErrorHandler>> = const { Cell::new(None) };
627}
628
629pub(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 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 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 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
776fn 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
788fn 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
794fn entering(stage: Stage) -> impl Fn() + Clone + Send + Sync + 'static {
796 move || STAGE.set(Some(stage))
797}
798
799fn moved_since_quiet(passes: Res<Passes>) -> bool {
802 passes.moved != passes.quiet_at
803}
804
805fn 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 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
866fn moves(world: &World) -> u64 {
868 world
869 .get_resource::<Passes>()
870 .map_or(0, |passes| passes.moved)
871}
872
873fn 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
884fn 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
891fn 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#[derive(Clone)]
915pub(crate) struct Failure {
916 stage: Stage,
917 system: String,
919 error: String,
921}
922
923pub(crate) fn failed_in(stage: Stage) -> Option<Failure> {
925 FAILED.with_borrow(|failed| failed.iter().find(|failed| failed.stage == stage).cloned())
926}
927
928fn passed(stage: Stage) -> impl Fn() -> bool + Clone {
931 move || failed_in(stage).is_none()
932}
933
934fn 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
949pub(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
957fn 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
977fn 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
987fn 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
1008fn 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
1029fn 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
1039fn 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
1061type Saving<'w, 's> = ParamSet<
1063 'w,
1064 's,
1065 (
1066 Drain<'static, 'static>,
1067 Query<'static, 'static, &'static mut Conversation>,
1068 ),
1069>;
1070
1071fn 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 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 let mut ready: Vec<_> = ready.iter_mut().collect();
1130 ready.sort_unstable_by_key(|item| item.effect.id());
1131 for item in ready {
1132 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
1232fn 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#[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
1263fn effect_pool() -> &'static AsyncComputeTaskPool {
1265 AsyncComputeTaskPool::get_or_init(TaskPool::default)
1266}
1267
1268type Due = (
1270 With<Effect>,
1271 Without<Settled>,
1272 Or<((With<Outcome>, Without<Holds>), With<SupersededBy>)>,
1273);
1274
1275fn 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
1307fn 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
1320pub(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
1327pub(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
1354fn 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
1372fn 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 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 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
1456fn 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
1490fn 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}