1use crate::agent::checkpoint::{
15 ToolTurnCommit, ToolTurnCommitted, ToolTurnHolds, TurnAssistant, TurnResults,
16};
17use crate::agent::content::{
18 binary::BinaryAssets,
19 parts::{
20 ContentError, ContentGraph, ToolResultLimit, ToolResultStatus, replace_deferred,
21 spawn_deferred, spawn_deferred_with, write_message,
22 },
23};
24use bevy_reflect::Reflect;
25
26use crate::agent::content::parts::{AssistantHead, EditTarget, RequestPartEdit};
27use bevy_ecs::{
28 prelude::*,
29 query::{QueryData, QueryFilter},
30};
31use rig_core::{
32 completion::message::{
33 AssistantContent, AssistantMessage, ToolChoice, ToolResultContent, UserContent,
34 },
35 effect::{EffectKind, FamilyDescriptor, Outcome},
36 error::ErrorKind,
37 structured_output::{
38 missing_required_fields, output_tool_callable, output_tool_name, reprompt_missing_fields,
39 reprompt_text_answer, text_satisfies_schema,
40 },
41 transcript::{assistant_text_from_choice, invalid_call_feedback, is_empty_assistant_turn},
42};
43
44use crate::{
45 agent::{
46 AcceptUnknownFinishReasons, AdditionalParams, Advert, Attachment, Batch, Cancelled,
47 Context, Conversation, Cursor, DEFAULT_PROVIDER_RETRIES, DocumentId, DocumentProps,
48 DocumentText, Failed, Failure, Grant, InvalidCall, InvalidCalls, InvalidRetries, MaxTokens,
49 MaxTurns, MemoryAppendScheduled, MessageParts, Options, Output, OutputKind, OutputRetries,
50 OutputToolConfig, OutputToolName, Outputs, Preamble, Prompt, ProviderRetried,
51 ProviderRetries, ProviderRetrying, Remembered, Remembering, Remembers, Reprompt,
52 RequestPatch, Resolution, Retrievable, Retrieval, RetrievalKind, Retrieves, Retrieving,
53 Retry, Run, RunCounter, RunOf, RunPhase, RunResult, RunSeq, Settled, StreamRequested,
54 Temperature, ToolAccess, ToolCallSlot, ToolChoiceSpec, ToolContextSpec, ToolPolicy, Turn,
55 Unhandled, Usage, UsesModel, Utterance,
56 },
57 bus::{
58 Bound, BusSet, EffectOutcome, Issued, PendingEffect, RigSchedule, ServedBy,
59 Streamed as BusStreamed, ToolInputs,
60 },
61 policy::{self, RequestGraph},
62};
63
64pub mod backoff;
65pub mod diagnostics;
66mod stream_invalid;
67pub mod witness;
68pub use stream_invalid::discover_streamed_invalid_calls;
69
70#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub enum RigSet {
73 Advance,
75 Select,
77 Assemble,
79 Patch,
81 Release,
83 Fold,
85 Judge,
87 Materialise,
89 Checkpoint,
91 Settle,
93}
94
95#[derive(QueryFilter)]
98pub struct Wanting {
99 phased: With<RunPhase>,
100 ready: With<crate::agent::Ready>,
101 live: Without<Failed>,
102}
103#[derive(QueryFilter)]
105pub struct Unopened {
106 run: With<Run>,
107 ready: With<crate::agent::Ready>,
108 not_failed: Without<Failed>,
109 not_settled: Without<Settled>,
110 phaseless: Without<RunPhase>,
111}
112#[derive(QueryFilter)]
114pub struct Unselected {
115 run: With<Run>,
116 unselected: Without<UsesModel>,
117}
118#[derive(QueryFilter)]
120pub struct LiveRun {
121 run: With<Run>,
122 live: Without<Failed>,
123}
124#[derive(QueryData)]
127pub struct EffectView {
128 pub turn_of: &'static ChildOf,
130 pub streamed: Option<&'static BusStreamed>,
132 pub outcome: Option<&'static EffectOutcome>,
134}
135#[derive(QueryData)]
138pub struct LandedEffect {
139 pub turn_of: &'static ChildOf,
141 pub outcome: &'static EffectOutcome,
143 pub streamed: Option<&'static BusStreamed>,
145}
146#[derive(QueryFilter)]
148pub struct Unresolved {
149 invalid: With<InvalidCall>,
150 unresolved: Without<Resolution>,
151}
152#[derive(QueryFilter)]
154pub struct Unread {
155 turn: With<Turn>,
156 unread: Without<Materialised>,
157}
158#[derive(QueryData)]
160pub struct AssemblingRun {
161 pub run_of: &'static RunOf,
163 pub seq: &'static RunSeq,
165 pub stream: &'static StreamRequested,
167 pub model: Option<&'static UsesModel>,
169 pub minted: &'static OutputToolName,
171}
172#[derive(bevy_ecs::system::SystemParam)]
174pub struct Settings<'w, 's> {
175 pub preambles: Query<'w, 's, &'static Preamble>,
177 pub temperatures: Query<'w, 's, &'static Temperature>,
179 pub max_tokens: Query<'w, 's, &'static MaxTokens>,
181 pub unknown_finishes: Query<'w, 's, &'static AcceptUnknownFinishReasons>,
183 pub params: Query<'w, 's, &'static AdditionalParams>,
185 pub options: Query<'w, 's, &'static Options>,
187 pub provider_options: Query<'w, 's, &'static crate::agent::ProviderOptions>,
189 pub choices: Query<'w, 's, &'static ToolChoiceSpec>,
191 pub outputs: Query<'w, 's, &'static Output>,
193 pub output_tools: Query<'w, 's, &'static OutputToolConfig>,
195 pub tool_access: Query<'w, 's, &'static ToolAccess>,
197 pub tool_result_limits: Query<'w, 's, &'static ToolResultLimit>,
199}
200#[derive(QueryData)]
203pub struct FreshTurn {
204 pub entity: Entity,
206 pub turn_of: &'static ChildOf,
208 pub patch: Option<&'static RequestPatch>,
210 pub retrieving: Has<Retrieving>,
212}
213#[derive(QueryFilter)]
215pub struct RetrievingTurn {
216 fresh: With<Fresh>,
217 retrieving: With<Retrieving>,
218}
219#[derive(QueryFilter)]
221pub struct NeedsMemoryAppend {
222 settled: With<Settled>,
223 remembering: With<Remembering>,
224 unscheduled: Without<MemoryAppendScheduled>,
225}
226#[derive(QueryFilter)]
228pub struct NotRetrieval {
229 effect: With<PendingEffect>,
230 completion: Without<Retrieval>,
231}
232#[derive(QueryData)]
234pub struct RunState {
235 pub phase: Option<&'static RunPhase>,
237 pub failed: Has<Failed>,
239}
240#[derive(QueryData)]
243pub struct TurnState {
244 pub run_of: &'static ChildOf,
246 pub materialised: Has<Materialised>,
248 pub batched: Has<Batch>,
250}
251
252#[derive(Component, Debug, Clone, Copy, Default, Reflect)]
254#[reflect(Component)]
255pub struct Fresh;
256
257#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
259#[reflect(Component)]
260pub struct Folded(pub OutputKind);
261
262#[derive(Component, Debug, Clone, Copy, Default, Reflect)]
264#[reflect(Component)]
265pub struct Materialised;
266
267#[derive(Debug, Clone)]
271pub struct GrantedTool {
272 pub name: String,
274 pub key: rig_core::effect::HandlerKey,
276 pub handler: Option<Entity>,
278}
279
280#[derive(Component, Debug, Clone)]
284pub struct TurnRead {
285 pub head: AssistantHead,
287 pub content: Vec<AssistantContent>,
289 pub granted: Vec<GrantedTool>,
291 pub assistant: Option<Entity>,
293}
294
295impl TurnRead {
296 fn calls(&self) -> impl Iterator<Item = &rig_core::completion::message::ToolCall> {
298 self.content.iter().filter_map(|part| match part {
299 AssistantContent::ToolCall(call) => Some(call),
300 _ => None,
301 })
302 }
303}
304
305#[derive(Debug, Default, Clone, Copy)]
308pub struct AgentPlugin;
309
310impl bevy_app::Plugin for AgentPlugin {
311 fn build(&self, app: &mut bevy_app::App) {
312 Self::install(app.world_mut());
313 if app
314 .world()
315 .contains_resource::<bevy_diagnostic::DiagnosticsStore>()
316 {
317 diagnostics::register(app);
318 app.add_systems(RigSchedule, diagnostics::measure.in_set(RigSet::Settle));
319 }
320 }
321}
322
323impl AgentPlugin {
324 pub fn install(world: &mut World) {
327 assert!(
328 world.contains_resource::<crate::bus::Policy>(),
329 "AgentPlugin needs BusPlugin first: it runs in the bus's RigSchedule"
330 );
331 world.init_resource::<BinaryAssets>();
332 world.init_resource::<RunCounter>();
333 world.add_observer(effect_cancelled);
334 world.add_observer(run_cancelled);
335 world.add_observer(batch_marker_follows_the_hold);
336 witness::install(world);
337 let mut schedules = world.resource_mut::<Schedules>();
338 let Some(schedule) = schedules.get_mut(RigSchedule) else {
339 return;
340 };
341 schedule.configure_sets(
342 (
343 RigSet::Advance,
344 RigSet::Select,
345 RigSet::Assemble,
346 RigSet::Patch,
347 RigSet::Release,
348 )
349 .chain()
350 .before(BusSet::Gate),
351 );
352 schedule.configure_sets(
353 (
354 RigSet::Fold,
355 RigSet::Judge,
356 RigSet::Materialise,
357 RigSet::Checkpoint,
358 RigSet::Settle,
359 )
360 .chain()
361 .after(BusSet::Judge),
362 );
363 schedule.add_systems((
364 (open_runs, advance).chain().in_set(RigSet::Advance),
365 attach_retrieved
366 .after(RigSet::Advance)
367 .before(RigSet::Select),
368 select.in_set(RigSet::Select),
369 fold_turn.in_set(RigSet::Assemble),
370 backoff::hold_retries
371 .after(RigSet::Patch)
372 .before(RigSet::Release),
373 release_batch.in_set(RigSet::Release),
374 (fold, discover_streamed_invalid_calls)
375 .chain()
376 .in_set(RigSet::Fold),
377 (
378 land_memory,
379 resolve_invalid_defaults,
380 land_batch,
381 record_usage,
382 judge_invalid_calls,
383 read_turn,
384 materialise_assistant,
385 materialise_batch,
386 materialise_reprompt,
387 materialise_answer,
388 )
389 .chain()
390 .in_set(RigSet::Materialise),
391 append_memory.in_set(RigSet::Settle),
392 ));
393 }
394}
395
396trait PhaseCommands {
398 fn phase(&mut self, next: RunPhase) -> &mut Self;
399 fn end(&mut self, ending: impl Bundle) -> &mut Self;
400}
401
402impl PhaseCommands for bevy_ecs::system::EntityCommands<'_> {
403 fn phase(&mut self, next: RunPhase) -> &mut Self {
404 self.insert(next)
405 }
406 fn end(&mut self, ending: impl Bundle) -> &mut Self {
407 self.remove::<RunPhase>().insert(ending)
408 }
409}
410
411fn fail_content(commands: &mut Commands, run: Entity, error: ContentError) {
413 commands
414 .entity(run)
415 .remove::<(RunPhase, Settled)>()
416 .insert(Failed(Failure::Content(error)));
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub enum RunBusy {
422 NotARun,
424 Unsettled,
426 InFlight,
429}
430
431#[derive(EntityEvent, Debug, Clone, Copy, PartialEq, Eq)]
435pub struct RunDespawnRefused {
436 pub entity: Entity,
438 pub reason: RunBusy,
440}
441
442pub trait RunCommands {
447 type Despawned;
450
451 fn spawn_run(
459 &mut self,
460 agent: Entity,
461 history: &[MessageParts],
462 prompt: impl Into<Prompt>,
463 streamed: bool,
464 max_turns: Option<usize>,
465 ) -> Entity;
466
467 fn cancel_run(&mut self, run: Entity, reason: impl Into<String>);
473
474 fn despawn_run(&mut self, run: Entity) -> Self::Despawned;
479}
480
481impl RunCommands for World {
482 type Despawned = Result<(), RunBusy>;
483
484 fn spawn_run(
485 &mut self,
486 agent: Entity,
487 history: &[MessageParts],
488 prompt: impl Into<Prompt>,
489 streamed: bool,
490 max_turns: Option<usize>,
491 ) -> Entity {
492 let run = self.spawn_empty().id();
493 spawn_run_at(
494 self,
495 run,
496 agent,
497 history,
498 prompt.into(),
499 streamed,
500 max_turns,
501 );
502 run
503 }
504
505 fn cancel_run(&mut self, run: Entity, reason: impl Into<String>) {
506 cancel_run_in(self, run, reason.into());
507 }
508
509 fn despawn_run(&mut self, run: Entity) -> Result<(), RunBusy> {
510 despawn_run_in(self, run)
511 }
512}
513
514impl RunCommands for Commands<'_, '_> {
515 type Despawned = ();
516
517 fn spawn_run(
518 &mut self,
519 agent: Entity,
520 history: &[MessageParts],
521 prompt: impl Into<Prompt>,
522 streamed: bool,
523 max_turns: Option<usize>,
524 ) -> Entity {
525 let run = self.spawn_empty().id();
526 let history = history.to_vec();
527 let prompt = prompt.into();
528 self.queue(move |world: &mut World| {
529 spawn_run_at(world, run, agent, &history, prompt, streamed, max_turns);
530 });
531 run
532 }
533
534 fn cancel_run(&mut self, run: Entity, reason: impl Into<String>) {
535 let reason = reason.into();
536 self.queue(move |world: &mut World| cancel_run_in(world, run, reason));
537 }
538
539 fn despawn_run(&mut self, run: Entity) {
540 self.queue(move |world: &mut World| {
541 if let Err(reason) = despawn_run_in(world, run) {
542 world.trigger(RunDespawnRefused {
543 entity: run,
544 reason,
545 });
546 }
547 });
548 }
549}
550
551fn spawn_run_at(
554 world: &mut World,
555 run: Entity,
556 agent: Entity,
557 history: &[MessageParts],
558 prompt: Prompt,
559 streamed: bool,
560 max_turns: Option<usize>,
561) {
562 if world.get_entity(run).is_err() {
563 return;
564 }
565 let Ok(mut entity) = world.get_entity_mut(run) else {
566 return;
567 };
568 entity.insert((Run, RunOf(agent), StreamRequested(streamed), prompt));
569 if let Some(limit) = max_turns {
570 let Ok(mut entity) = world.get_entity_mut(run) else {
571 return;
572 };
573 entity.insert(MaxTurns(limit));
574 }
575 for parts in history.iter().cloned() {
576 if world.get_entity(run).is_err() {
577 return;
578 }
579 if let Err(error) = spawn_utterance(world, run, parts) {
580 if let Ok(mut entity) = world.get_entity_mut(run) {
581 entity
582 .remove::<Prompt>()
583 .insert(Failed(Failure::Content(error)));
584 }
585 return;
586 }
587 }
588 let Ok(mut entity) = world.get_entity_mut(run) else {
589 return;
590 };
591 entity.insert(crate::agent::Ready);
592 open_run(world, run);
593}
594
595fn cancel_run_in(world: &mut World, run: Entity, reason: String) {
596 if let Ok(mut entity) = world.get_entity_mut(run)
597 && entity.contains::<Run>()
598 {
599 entity.insert(Cancelled(reason));
600 }
601}
602
603fn despawn_run_in(world: &mut World, run: Entity) -> Result<(), RunBusy> {
604 if world.get::<Run>(run).is_none() {
605 return Err(RunBusy::NotARun);
606 }
607 if world.get::<Settled>(run).is_none() && world.get::<Failed>(run).is_none() {
608 return Err(RunBusy::Unsettled);
609 }
610 let mut stack = vec![run];
611 while let Some(entity) = stack.pop() {
612 if world.get::<crate::bus::InFlight>(entity).is_some()
613 || (world.get::<PendingEffect>(entity).is_some()
614 && world.get::<EffectOutcome>(entity).is_none())
615 {
616 return Err(RunBusy::InFlight);
617 }
618 if let Some(children) = world.get::<Children>(entity) {
619 stack.extend(children.iter());
620 }
621 }
622 world.entity_mut(run).despawn();
623 Ok(())
624}
625
626pub fn open_runs(mut commands: Commands, runs: Query<Entity, Unopened>) {
630 for run in &runs {
633 commands.queue(move |world: &mut World| open_run(world, run));
634 }
635}
636
637fn open_run(world: &mut World, run: Entity) {
640 let Some(entity) = world.get_entity(run).ok() else {
641 return;
642 };
643 if !entity.contains::<Run>()
644 || !entity.contains::<crate::agent::Ready>()
645 || entity.contains::<Failed>()
646 || entity.contains::<Settled>()
647 || entity.contains::<RunPhase>()
648 {
649 return;
650 }
651 let agent = entity.get::<RunOf>().map(|run_of| run_of.0);
652 let had_history = entity.get::<Children>().is_some_and(|children| {
653 children
654 .iter()
655 .any(|child| world.get::<Utterance>(child).is_some())
656 });
657 if let Some(Prompt(content)) = world.entity_mut(run).take::<Prompt>()
658 && let Err(error) =
659 MessageParts::user(content).and_then(|prompt| spawn_utterance(world, run, prompt))
660 {
661 world
662 .entity_mut(run)
663 .insert(Failed(Failure::Content(error)));
664 return;
665 }
666 let memory = (!had_history)
667 .then(|| {
668 let agent = agent?;
669 let handler = world.get::<Remembers>(agent).map(|remembers| remembers.0)?;
670 let conversation = world
671 .get::<Conversation>(agent)
672 .map(|conversation| conversation.0.clone())?;
673 let key = world.get::<Bound>(handler).map(|bound| bound.key.clone())?;
674 Some((key, conversation))
675 })
676 .flatten();
677 match memory {
678 Some((key, conversation)) => {
679 world.entity_mut(run).insert((
680 RunPhase::LoadingMemory,
681 Remembering,
682 Conversation(conversation.clone()),
683 ));
684 world.spawn((
685 PendingEffect::new(
686 key,
687 EffectKind::Memory {
688 op: rig_core::effect::MemoryOp::Load {
689 conversation: rig_core::id::ConversationId::from(conversation.as_str()),
690 },
691 },
692 ),
693 ChildOf(run),
694 ));
695 }
696 None => {
697 world.entity_mut(run).insert(RunPhase::Assembling);
698 }
699 }
700}
701
702pub fn spawn_utterance(
705 world: &mut World,
706 run: Entity,
707 parts: MessageParts,
708) -> Result<Entity, ContentError> {
709 let entity = world.spawn((Utterance, ChildOf(run))).id();
710 if let Err(error) = write_message(world, entity, parts) {
711 world.despawn(entity);
712 return Err(error);
713 }
714 Ok(entity)
715}
716
717fn setting<'a, C: Component>(run: Entity, agent: Entity, query: &'a Query<&C>) -> Option<&'a C> {
719 query.get(run).ok().or_else(|| query.get(agent).ok())
720}
721
722fn links_in_order<'a, L: Component, F: bevy_ecs::query::QueryFilter>(
724 owner: Entity,
725 children: &Query<&Children>,
726 links: &'a Query<&L, F>,
727) -> Vec<&'a L> {
728 children
729 .get(owner)
730 .map(|children| {
731 children
732 .iter()
733 .filter_map(|child| links.get(child).ok())
734 .collect()
735 })
736 .unwrap_or_default()
737}
738
739pub fn advance(
744 mut commands: Commands,
745 runs: Query<(Entity, &RunOf, &Cursor, &RunSeq, &RunPhase), Wanting>,
746 fresh: Query<&ChildOf, With<Fresh>>,
747 children: Query<&Children>,
748 grants: Query<&Grant, Without<Retrievable>>,
749 contexts: Query<&Context>,
750 retrievals: Query<(), With<Retrieves>>,
751 max_turns: Query<&MaxTurns>,
752 retrying: Query<(), With<ProviderRetrying>>,
753 provider_retried: Query<&ProviderRetried>,
754 holds: Query<&ToolTurnHolds>,
755 commits: Query<(&ChildOf, &ToolTurnCommit)>,
756) {
757 let mut runs: Vec<_> = runs
758 .iter()
759 .filter(|(_, _, _, _, phase)| **phase == RunPhase::Assembling)
760 .collect();
761 runs.sort_by_key(|(_, _, _, seq, _)| **seq);
762 for (run, RunOf(agent), cursor, _, _) in runs {
763 if fresh.iter().any(|child_of| child_of.parent() == run) {
764 continue;
765 }
766 let retrying = retrying.get(run).is_ok();
769 let limit = setting(run, *agent, &max_turns).map_or(1, |limit| limit.0);
770 if !retrying && cursor.turn >= limit {
771 commands
772 .entity(run)
773 .end(Failed(Failure::MaxTurns { limit }));
774 continue;
775 }
776 if holds.get(run).is_ok_and(|holds| {
777 commits
778 .iter()
779 .any(|(parent, commit)| parent.parent() == run && holds.blocks(commit.turn))
780 }) {
781 continue;
782 }
783 let turn = commands.spawn((Turn, Fresh, ChildOf(run))).id();
784 let retrieves = children
785 .get(*agent)
786 .map(|children| children.iter().any(|child| retrievals.get(child).is_ok()))
787 .unwrap_or(false);
788 if retrieves {
789 commands.entity(turn).insert(Retrieving);
790 } else {
791 for Grant(tool) in links_in_order(*agent, &children, &grants) {
792 commands.spawn((Advert(*tool), ChildOf(turn)));
793 }
794 for Context(document) in links_in_order(*agent, &children, &contexts) {
795 commands.spawn((Attachment(*document), ChildOf(turn)));
796 }
797 }
798 if retrying {
799 commands.entity(run).remove::<ProviderRetrying>();
800 commands.entity(turn).insert(backoff::RetryAttempt(
801 provider_retried.get(run).map_or(1, |n| n.0),
802 ));
803 } else {
804 commands.entity(run).insert(Cursor {
805 turn: cursor.turn + 1,
806 });
807 }
808 }
809}
810
811pub fn attach_retrieved(
817 mut commands: Commands,
818 turns: Query<(Entity, &ChildOf), RetrievingTurn>,
819 runs: Query<&RunOf>,
820 children: Query<&Children>,
821 retrievals: Query<(&PendingEffect, &Retrieval, Option<&EffectOutcome>)>,
822 grants: Query<(&Grant, Has<Retrievable>)>,
823 contexts: Query<&Context>,
824 bound: Query<&Bound>,
825 indexes: Query<&Retrieves, With<Retrieval>>,
826 documents: Query<(Entity, &DocumentId)>,
827) {
828 for (turn, turn_of) in &turns {
829 let run = turn_of.parent();
830 let Ok(RunOf(agent)) = runs.get(run) else {
831 continue;
832 };
833 let effects: Vec<(&Retrieval, Option<&EffectOutcome>)> = children
834 .get(turn)
835 .map(|children| {
836 children
837 .iter()
838 .filter_map(|child| retrievals.get(child).ok())
839 .map(|(_, retrieval, outcome)| (retrieval, outcome))
840 .collect()
841 })
842 .unwrap_or_default();
843 let available_index = children.get(*agent).is_ok_and(|children| {
846 children.iter().any(|child| {
847 indexes
848 .get(child)
849 .is_ok_and(|Retrieves(index)| bound.get(*index).is_ok())
850 })
851 });
852 if (effects.is_empty() && available_index)
853 || effects.iter().any(|(_, outcome)| outcome.is_none())
854 {
855 continue;
856 }
857 let mut retrieved_tools: Vec<String> = Vec::new();
858 let mut retrieved_documents: Vec<(String, String)> = Vec::new();
859 for (retrieval, outcome) in &effects {
860 let Some(EffectOutcome(Ok(Outcome::Documents(results)))) = outcome else {
861 continue;
862 };
863 match (retrieval.what, results) {
864 (RetrievalKind::Tools, rig_core::effect::RetrievedDocuments::Ids(ids)) => {
865 retrieved_tools.extend(ids.iter().map(|(_, id)| id.clone()));
866 }
867 (
868 RetrievalKind::Documents,
869 rig_core::effect::RetrievedDocuments::Scored(scored),
870 ) => {
871 retrieved_documents.extend(scored.iter().map(|(_, id, value)| {
872 (
873 id.clone(),
874 serde_json::to_string_pretty(value)
875 .unwrap_or_else(|_| value.to_string()),
876 )
877 }));
878 }
879 (RetrievalKind::Tools, rig_core::effect::RetrievedDocuments::Scored(_))
880 | (RetrievalKind::Documents, rig_core::effect::RetrievedDocuments::Ids(_)) => {}
881 }
882 }
883 let links: Vec<(&Grant, bool)> = children
884 .get(*agent)
885 .map(|children| {
886 children
887 .iter()
888 .filter_map(|child| grants.get(child).ok())
889 .collect()
890 })
891 .unwrap_or_default();
892 let tool_named = |name: &str| -> Option<Entity> {
893 links.iter().find_map(|(Grant(tool), _)| {
894 bound
895 .get(*tool)
896 .is_ok_and(|bound| policy::tool_name(&bound.descriptor) == Some(name))
897 .then_some(*tool)
898 })
899 };
900 for name in &retrieved_tools {
901 if let Some(tool) = tool_named(name) {
902 commands.spawn((Advert(tool), ChildOf(turn)));
903 }
904 }
905 for (Grant(tool), retrievable) in &links {
906 if !retrievable {
907 commands.spawn((Advert(*tool), ChildOf(turn)));
908 }
909 }
910 for Context(document) in links_in_order(*agent, &children, &contexts) {
911 commands.spawn((Attachment(*document), ChildOf(turn)));
912 }
913 for (id, text) in retrieved_documents {
914 let document = documents
915 .iter()
916 .find(|(_, existing)| existing.0 == id)
917 .map(|(entity, _)| entity)
918 .unwrap_or_else(|| commands.spawn((DocumentId(id), DocumentText(text))).id());
919 commands.spawn((Attachment(document), ChildOf(turn)));
920 }
921 commands.entity(turn).remove::<Retrieving>();
922 }
923}
924
925pub fn land_memory(
929 mut commands: Commands,
930 mut assets: ResMut<BinaryAssets>,
931 runs: Query<(Entity, &RunPhase), Without<Failed>>,
932 children: Query<&Children>,
933 loads: Query<(&PendingEffect, &EffectOutcome)>,
934) {
935 for (run, _) in runs
936 .iter()
937 .filter(|(_, phase)| **phase == RunPhase::LoadingMemory)
938 {
939 let Some(outcome) = children.get(run).ok().and_then(|children| {
940 children.iter().find_map(|child| {
941 loads
942 .get(child)
943 .ok()
944 .and_then(|(effect, outcome)| match &effect.kind {
945 EffectKind::Memory {
946 op: rig_core::effect::MemoryOp::Load { .. },
947 } => Some(&outcome.0),
948 EffectKind::Memory { .. }
949 | EffectKind::Completion { .. }
950 | EffectKind::ToolCall { .. }
951 | EffectKind::Embed { .. }
952 | EffectKind::Rerank { .. }
953 | EffectKind::Retrieve { .. }
954 | EffectKind::Custom { .. } => None,
955 })
956 })
957 }) else {
958 continue;
959 };
960 match outcome {
961 Ok(Outcome::Memory(rig_core::effect::MemoryOutcome::Loaded { messages })) => {
962 if let Err(error) = land_loaded(&mut commands, &mut assets, run, messages) {
963 fail_content(&mut commands, run, error);
964 }
965 }
966 Ok(other) => {
967 commands.entity(run).end(Failed(Failure::Memory(
968 rig_core::error::ErrorReport::new(
969 ErrorKind::Internal,
970 format!(
971 "the memory handler answered a load with a {} outcome",
972 other.family()
973 ),
974 ),
975 )));
976 }
977 Err(report) => {
978 commands
979 .entity(run)
980 .end(Failed(Failure::Memory(report.clone())));
981 }
982 }
983 }
984}
985
986fn land_loaded(
989 commands: &mut Commands,
990 assets: &mut BinaryAssets,
991 run: Entity,
992 messages: &[rig_core::completion::Message],
993) -> Result<(), ContentError> {
994 let mut loaded = Vec::with_capacity(messages.len());
995 for message in messages {
996 if let Some(parts) = MessageParts::from_message(message) {
997 let utterance = spawn_deferred(commands, assets, run, parts)?;
998 commands.entity(utterance).insert(Remembered);
999 loaded.push(utterance);
1000 }
1001 }
1002 if !loaded.is_empty() {
1003 commands.entity(run).insert_children(0, &loaded);
1004 }
1005 commands.entity(run).phase(RunPhase::Assembling);
1006 Ok(())
1007}
1008
1009pub fn append_memory(
1013 mut commands: Commands,
1014 settled: Query<(Entity, &RunOf, &Conversation), NeedsMemoryAppend>,
1015 memories: Query<&Remembers>,
1016 bound: Query<&Bound>,
1017 children: Query<&Children>,
1018 utterances: Query<(Entity, Has<Remembered>), With<Utterance>>,
1019 content: ContentGraph,
1020) {
1021 for (run, RunOf(agent), Conversation(conversation)) in &settled {
1022 let Some(key) = memories
1023 .get(*agent)
1024 .ok()
1025 .and_then(|Remembers(memory)| bound.get(*memory).ok())
1026 .map(|bound| bound.key.clone())
1027 else {
1028 continue;
1029 };
1030 let said: Result<Vec<MessageParts>, ContentError> = children
1031 .get(run)
1032 .map(|children| {
1033 children
1034 .iter()
1035 .filter_map(|child| utterances.get(child).ok())
1036 .filter(|(_, remembered)| !*remembered)
1037 .map(|(entity, _)| content.message(entity))
1038 .collect()
1039 })
1040 .unwrap_or_else(|_| Ok(Vec::new()));
1041 let said = match said {
1042 Ok(said) => said,
1043 Err(error) => {
1044 fail_content(&mut commands, run, error);
1045 continue;
1046 }
1047 };
1048 commands.spawn((
1049 PendingEffect::new(
1050 key,
1051 EffectKind::Memory {
1052 op: rig_core::effect::MemoryOp::Append {
1053 conversation: rig_core::id::ConversationId::from(conversation.as_str()),
1054 messages: said.into_iter().map(|parts| parts.to_message()).collect(),
1055 },
1056 },
1057 ),
1058 ChildOf(run),
1059 ));
1060 commands.entity(run).insert(MemoryAppendScheduled);
1061 }
1062}
1063
1064pub fn select(
1067 mut commands: Commands,
1068 runs: Query<(Entity, &RunOf), Unselected>,
1069 models: Query<&UsesModel>,
1070) {
1071 for (run, RunOf(agent)) in &runs {
1072 if let Ok(UsesModel(model)) = models.get(*agent) {
1073 commands.entity(run).insert(UsesModel(*model));
1074 }
1075 }
1076}
1077
1078#[derive(QueryData)]
1081pub struct PartEdit {
1082 pub link: Entity,
1084 pub target: Option<&'static EditTarget>,
1086 pub edit: &'static RequestPartEdit,
1088}
1089
1090struct PartEdits {
1093 edits: std::collections::BTreeMap<Entity, RequestPartEdit>,
1094 consumed: Vec<Entity>,
1095 edited: std::collections::HashSet<Entity>,
1096}
1097
1098fn part_edits_of(
1103 turn: Entity,
1104 run: Entity,
1105 patch: Option<&RequestPatch>,
1106 children: &Query<&Children>,
1107 part_edits: &Query<PartEdit>,
1108 content: &ContentGraph,
1109) -> Result<PartEdits, ContentError> {
1110 let mut gathered = PartEdits {
1111 edits: std::collections::BTreeMap::new(),
1112 consumed: Vec::new(),
1113 edited: std::collections::HashSet::new(),
1114 };
1115 let links = children
1116 .get(turn)
1117 .into_iter()
1118 .flat_map(|children| children.iter())
1119 .filter_map(|link| part_edits.get(link).ok());
1120 for PartEditItem { link, target, edit } in links {
1121 let target = target.ok_or(ContentError::Missing)?;
1122 let utterance = content.target_utterance(target.0)?;
1123 gathered.edited.insert(utterance);
1124 if !children
1125 .get(run)
1126 .is_ok_and(|owned| owned.contains(&utterance))
1127 || patch.is_some_and(|patch| patch.history.is_some())
1128 {
1129 return Err(ContentError::Shape);
1130 }
1131 gathered.edits.insert(target.0, edit.clone());
1132 gathered.consumed.push(link);
1133 }
1134 Ok(gathered)
1135}
1136
1137fn render_history(
1141 turn: Entity,
1142 run: Entity,
1143 patch: Option<&RequestPatch>,
1144 children: &Query<&Children>,
1145 utterances: &Query<Entity, With<Utterance>>,
1146 part_edits: &Query<PartEdit>,
1147 content: &ContentGraph,
1148) -> Result<(Vec<MessageParts>, Vec<Entity>), ContentError> {
1149 let edits = part_edits_of(turn, run, patch, children, part_edits, content)?;
1150 let history = children
1151 .get(run)
1152 .into_iter()
1153 .flat_map(|children| children.iter())
1154 .filter_map(|child| utterances.get(child).ok())
1155 .map(|entity| {
1156 if edits.edited.contains(&entity) {
1157 content.message_with(entity, &edits.edits)
1158 } else {
1159 content.message(entity)
1160 }
1161 })
1162 .collect::<Result<_, _>>()?;
1163 Ok((history, edits.consumed))
1164}
1165
1166fn spawn_retrievals<'a>(
1170 commands: &mut Commands,
1171 turn: Entity,
1172 history: &[MessageParts],
1173 indexes: impl Iterator<Item = (&'a Retrieves, &'a Retrieval)>,
1174 bound: &Query<&Bound>,
1175) {
1176 let query = policy::retrieval_query(history);
1177 for (Retrieves(index), retrieval) in indexes {
1178 let Ok(index) = bound.get(*index) else {
1179 continue;
1180 };
1181 let request = rig_core::vector_store::request::VectorSearchRequest::builder()
1182 .query(query.clone())
1183 .samples(retrieval.samples)
1184 .build()
1185 .map_filter(rig_core::vector_store::request::Filter::interpret);
1186 let query = match retrieval.what {
1187 RetrievalKind::Documents => rig_core::effect::RetrieveQuery::TopN { req: request },
1188 RetrievalKind::Tools => rig_core::effect::RetrieveQuery::TopNIds { req: request },
1189 };
1190 commands.spawn((
1191 PendingEffect::new(index.key.clone(), EffectKind::Retrieve { query }),
1192 *retrieval,
1193 ChildOf(turn),
1194 ));
1195 }
1196}
1197
1198fn allowed_tools<'a>(
1204 commands: &mut Commands,
1205 turn: Entity,
1206 patch: Option<&RequestPatch>,
1207 children: &Query<&Children>,
1208 adverts: &Query<&Advert>,
1209 bound: &'a Query<&Bound>,
1210) -> Vec<(Entity, &'a Bound)> {
1211 children
1212 .get(turn)
1213 .into_iter()
1214 .flat_map(|children| children.iter())
1215 .filter_map(|link| {
1216 let Advert(tool) = adverts.get(link).ok()?;
1217 Some((link, *tool, bound.get(*tool).ok()?))
1218 })
1219 .filter_map(|(link, tool, bound)| {
1220 let allowed = match (
1221 patch.and_then(|p| p.active_tools.as_ref()),
1222 policy::tool_name(&bound.descriptor),
1223 ) {
1224 (Some(allowed), Some(name)) => allowed.iter().any(|allowed| allowed == name),
1225 (Some(_), None) | (None, _) => true,
1226 };
1227 if allowed {
1228 Some((tool, bound))
1229 } else {
1230 commands.entity(link).despawn();
1231 None
1232 }
1233 })
1234 .collect()
1235}
1236
1237fn completion_model<'a>(
1242 model: Option<&UsesModel>,
1243 bound: &'a Query<&Bound>,
1244) -> Result<(Entity, &'a Bound, bool), Failure> {
1245 let unavailable = |message: String| {
1246 Failure::Provider(rig_core::error::ErrorReport::new(
1247 rig_core::error::ErrorKind::HandlerUnavailable,
1248 message,
1249 ))
1250 };
1251 let Some((model, model_bound)) =
1252 model.and_then(|UsesModel(model)| bound.get(*model).ok().map(|bound| (*model, bound)))
1253 else {
1254 return Err(unavailable(
1255 "the run has no bound completion model; its selected model or its agent's binding was removed".to_owned(),
1256 ));
1257 };
1258 match &model_bound.descriptor.family {
1259 FamilyDescriptor::Completion { capabilities, .. } => Ok((
1260 model,
1261 model_bound,
1262 capabilities.composes_native_output_with_tools,
1263 )),
1264 FamilyDescriptor::Tool { .. }
1265 | FamilyDescriptor::Embed { .. }
1266 | FamilyDescriptor::Rerank { .. }
1267 | FamilyDescriptor::Memory { .. }
1268 | FamilyDescriptor::Retrieve { .. }
1269 | FamilyDescriptor::Custom { .. } => Err(unavailable(format!(
1270 "selected model `{}` does not serve completions",
1271 model_bound.key
1272 ))),
1273 }
1274}
1275
1276fn resolve_output_tool(
1283 minted: &OutputToolName,
1284 reserved_name: Option<&str>,
1285 output: &Output,
1286 tool_choice: Option<&ToolChoice>,
1287 tools: &[(Entity, &Bound)],
1288 access: &ToolAccess,
1289 composes: bool,
1290) -> Result<(OutputKind, String), String> {
1291 let granted_names: Vec<&str> = tools
1292 .iter()
1293 .filter_map(|(_, bound)| policy::tool_name(&bound.descriptor))
1294 .collect();
1295 let occupied_names: Vec<&str> = granted_names
1296 .iter()
1297 .copied()
1298 .chain(
1299 access
1300 .executable
1301 .iter()
1302 .flat_map(|executable| executable.keys().map(String::as_str)),
1303 )
1304 .collect();
1305 let output_tool = minted
1306 .0
1307 .clone()
1308 .or_else(|| reserved_name.map(str::to_owned))
1309 .unwrap_or_else(|| output_tool_name(|name| occupied_names.contains(&name)));
1310 let callable = output_tool_callable(tool_choice, &output_tool);
1311 let resolved = if minted.0.is_some() || (reserved_name.is_some() && output.schema.is_some()) {
1312 OutputKind::Tool
1313 } else {
1314 policy::resolve_output(
1315 output.mode,
1316 output.schema.is_some(),
1317 granted_names.len(),
1318 callable,
1319 composes,
1320 )
1321 };
1322 if resolved == OutputKind::Tool && occupied_names.contains(&output_tool.as_str()) {
1323 return Err(output_tool);
1326 }
1327 Ok((resolved, output_tool))
1328}
1329
1330fn attached_documents(
1333 turn: Entity,
1334 patch: Option<&RequestPatch>,
1335 children: &Query<&Children>,
1336 attachments: &Query<&Attachment>,
1337 documents: &Query<(&DocumentId, &DocumentText, Option<&DocumentProps>)>,
1338) -> Vec<rig_core::completion::Document> {
1339 links_in_order(turn, children, attachments)
1340 .into_iter()
1341 .filter_map(|Attachment(document)| documents.get(*document).ok())
1342 .map(|(id, text, props)| rig_core::completion::Document {
1343 id: id.0.clone(),
1344 text: text.0.clone(),
1345 additional_props: props.map(|props| props.0.clone()).unwrap_or_default(),
1346 })
1347 .chain(
1348 patch
1349 .into_iter()
1350 .flat_map(|patch| patch.extra_context.iter().cloned()),
1351 )
1352 .collect()
1353}
1354
1355fn tool_access_for(
1359 run: Entity,
1360 agent: Entity,
1361 tools: &[(Entity, &Bound)],
1362 tool_access: &Query<&ToolAccess>,
1363) -> ToolAccess {
1364 let mut access = setting(run, agent, tool_access)
1365 .cloned()
1366 .unwrap_or_default();
1367 let executable = access.executable.get_or_insert_with(|| {
1368 tools
1369 .iter()
1370 .filter_map(|(_, bound)| {
1371 policy::tool_name(&bound.descriptor)
1372 .map(|name| (name.to_owned(), bound.key.clone()))
1373 })
1374 .collect()
1375 });
1376 if access.allowed.is_none() {
1377 access.allowed = Some(executable.keys().cloned().collect());
1378 }
1379 access
1380}
1381
1382struct Resolved {
1385 preamble: Option<String>,
1386 temperature: Option<f64>,
1387 max_tokens: Option<u64>,
1388 accept_unknown_finish_reasons: bool,
1389 additional_params: Option<serde_json::Value>,
1390 options: rig_core::completion::GenerationOptions,
1391 provider_options: rig_core::completion::ProviderOptions,
1392 tool_choice: Option<ToolChoice>,
1393 output: Output,
1394 output_tool_config: Option<OutputToolConfig>,
1395}
1396
1397impl Settings<'_, '_> {
1398 fn resolve(&self, run: Entity, agent: Entity, patch: Option<&RequestPatch>) -> Resolved {
1402 let additional_params = match (
1403 setting(run, agent, &self.params).and_then(|p| p.0.clone()),
1404 patch.and_then(|p| p.additional_params.clone()),
1405 ) {
1406 (Some(base), Some(patched)) if base.is_object() && patched.is_object() => {
1407 Some(rig_core::json_utils::merge(base, patched))
1408 }
1409 (base, patched) => patched.or(base),
1410 };
1411 Resolved {
1412 preamble: patch
1413 .and_then(|p| p.preamble.clone())
1414 .or_else(|| setting(run, agent, &self.preambles).and_then(|p| p.0.clone())),
1415 temperature: patch
1416 .and_then(|p| p.temperature)
1417 .or_else(|| setting(run, agent, &self.temperatures).and_then(|t| t.0)),
1418 max_tokens: patch
1419 .and_then(|p| p.max_tokens)
1420 .or_else(|| setting(run, agent, &self.max_tokens).and_then(|m| m.0)),
1421 accept_unknown_finish_reasons: setting(run, agent, &self.unknown_finishes)
1422 .copied()
1423 .unwrap_or_default()
1424 .0,
1425 additional_params,
1426 options: {
1427 let base = self
1428 .options
1429 .get(agent)
1430 .map(|options| options.0.clone())
1431 .unwrap_or_default();
1432 match self.options.get(run) {
1433 Ok(over) if run != agent => base.overlay(&over.0),
1434 _ => base,
1435 }
1436 },
1437 provider_options: {
1438 let base = self
1439 .provider_options
1440 .get(agent)
1441 .map(|options| options.0.clone())
1442 .unwrap_or_default();
1443 match self.provider_options.get(run) {
1446 Ok(over) => base.overlay(&over.0),
1447 Err(_) => base,
1448 }
1449 },
1450 tool_choice: patch
1451 .and_then(|p| p.tool_choice.clone())
1452 .or_else(|| setting(run, agent, &self.choices).and_then(|c| c.0.clone())),
1453 output: setting(run, agent, &self.outputs)
1454 .cloned()
1455 .unwrap_or_default(),
1456 output_tool_config: setting(run, agent, &self.output_tools).cloned(),
1457 }
1458 }
1459}
1460
1461pub fn fold_turn(
1468 mut commands: Commands,
1469 fresh: Query<FreshTurn, With<Fresh>>,
1470 runs: Query<AssemblingRun, LiveRun>,
1471 children: Query<&Children>,
1472 utterances: Query<Entity, With<Utterance>>,
1473 content: ContentGraph,
1474 part_edits: Query<PartEdit>,
1475 retrievals: Query<(&Retrieves, &Retrieval)>,
1476 retrieving: Query<(), With<Retrieval>>,
1477 adverts: Query<&Advert>,
1478 attachments: Query<&Attachment>,
1479 documents: Query<(&DocumentId, &DocumentText, Option<&DocumentProps>)>,
1480 bound: Query<&Bound>,
1481 settings: Settings,
1482) {
1483 let mut turns: Vec<_> = fresh
1484 .iter()
1485 .filter_map(|turn| {
1486 let run = turn.turn_of.parent();
1487 runs.get(run).ok().map(|view| (view.seq.0, turn, run))
1488 })
1489 .collect();
1490 turns.sort_by_key(|(seq, _, _)| *seq);
1491 let mut folded = Vec::new();
1492 for (_, turn, run) in turns {
1493 let Ok(view) = runs.get(run) else {
1494 continue;
1495 };
1496 let agent = view.run_of.0;
1497 let (model, model_bound, composes) = match completion_model(view.model, &bound) {
1498 Ok(model) => model,
1499 Err(failure) => {
1500 commands.entity(run).end(Failed(failure));
1501 commands.entity(turn.entity).remove::<Fresh>();
1502 continue;
1503 }
1504 };
1505 let patch = turn.patch;
1506 let rendered = render_history(
1507 turn.entity,
1508 run,
1509 patch,
1510 &children,
1511 &utterances,
1512 &part_edits,
1513 &content,
1514 );
1515 let (mut history, consumed_edits) = match rendered {
1516 Ok(rendered) => rendered,
1517 Err(error) => {
1518 fail_content(&mut commands, run, error);
1519 continue;
1520 }
1521 };
1522 if turn.retrieving {
1523 let spawned = children
1524 .get(turn.entity)
1525 .is_ok_and(|children| children.iter().any(|child| retrieving.get(child).is_ok()));
1526 if !spawned {
1527 let indexes = children
1528 .get(agent)
1529 .into_iter()
1530 .flat_map(|children| children.iter())
1531 .filter_map(|child| retrievals.get(child).ok());
1532 spawn_retrievals(&mut commands, turn.entity, &history, indexes, &bound);
1533 }
1534 continue;
1535 }
1536 if let Some(limit) = setting(run, agent, &settings.tool_result_limits) {
1538 for parts in &mut history {
1539 if policy::tool_results_exceed(parts, limit) {
1540 policy::limit_tool_results(parts, limit);
1541 }
1542 }
1543 }
1544 let tools = allowed_tools(
1545 &mut commands,
1546 turn.entity,
1547 patch,
1548 &children,
1549 &adverts,
1550 &bound,
1551 );
1552 let attached = attached_documents(turn.entity, patch, &children, &attachments, &documents);
1553 if let Some(patched) = patch.and_then(|p| p.history.as_ref()) {
1555 let prompt = history.pop();
1556 history = patched.iter().cloned().chain(prompt).collect();
1557 }
1558 let resolved = settings.resolve(run, agent, patch);
1559 let reserved_name = resolved
1560 .output_tool_config
1561 .as_ref()
1562 .and_then(|config| config.name.as_deref());
1563 let access = tool_access_for(run, agent, &tools, &settings.tool_access);
1564 let (mode, output_tool) = match resolve_output_tool(
1565 view.minted,
1566 reserved_name,
1567 &resolved.output,
1568 resolved.tool_choice.as_ref(),
1569 &tools,
1570 &access,
1571 composes,
1572 ) {
1573 Ok(mode) => mode,
1574 Err(name) => {
1575 commands.entity(turn.entity).remove::<Fresh>();
1576 commands
1577 .entity(run)
1578 .end(Failed(Failure::OutputToolCollision { name }));
1579 continue;
1580 }
1581 };
1582 if mode == OutputKind::Tool && view.minted.0.is_none() {
1583 commands
1584 .entity(run)
1585 .insert(OutputToolName(Some(output_tool.clone())));
1586 }
1587 for link in consumed_edits {
1588 commands.entity(link).despawn();
1589 }
1590 commands.entity(turn.entity).insert(access);
1591 let graph = RequestGraph {
1592 preamble: resolved.preamble.as_deref(),
1593 utterances: history.iter().collect(),
1594 documents: attached,
1595 tools: tools.iter().map(|(_, bound)| &bound.descriptor).collect(),
1596 temperature: resolved.temperature,
1597 max_tokens: resolved.max_tokens,
1598 accept_unknown_finish_reasons: resolved.accept_unknown_finish_reasons,
1599 additional_params: resolved.additional_params.as_ref(),
1600 options: &resolved.options,
1601 provider_options: &resolved.provider_options,
1602 tool_choice: resolved.tool_choice.as_ref(),
1603 output: mode,
1604 schema: resolved.output.schema.as_ref(),
1605 output_tool: (mode == OutputKind::Tool).then_some(output_tool.as_str()),
1606 output_tool_config: resolved.output_tool_config.as_ref(),
1607 };
1608 match policy::fold_request(&graph) {
1609 Ok(request) => {
1610 let kind = EffectKind::Completion {
1611 request,
1612 stream: view.stream.0,
1613 };
1614 folded.push((turn.entity, run, model, model_bound.key.clone(), kind, mode));
1615 }
1616 Err(error) => fail_content(&mut commands, run, error),
1617 }
1618 }
1619 for (turn, run, model, key, kind, mode) in folded {
1621 commands.spawn((
1622 PendingEffect::new(key, kind),
1623 ServedBy(model),
1624 ChildOf(turn),
1625 ));
1626 commands
1627 .entity(turn)
1628 .remove::<(Fresh, RequestPatch)>()
1629 .insert((Folded(mode), Outputs::default()));
1630 commands.entity(run).phase(RunPhase::AwaitingModel);
1631 }
1632}
1633
1634pub fn fold(effects: Query<EffectView, NotRetrieval>, mut turns: Query<&mut Outputs, With<Turn>>) {
1637 for EffectViewItem {
1638 turn_of,
1639 streamed,
1640 outcome,
1641 } in &effects
1642 {
1643 let Ok(mut outputs) = turns.get_mut(turn_of.parent()) else {
1644 continue;
1645 };
1646 if outputs.done {
1647 continue;
1648 }
1649 match outcome {
1650 Some(EffectOutcome(Ok(Outcome::Completion(response)))) => {
1651 outputs.content = response.choice.clone();
1652 outputs.head = AssistantHead::of(&response.head());
1653 outputs.done = true;
1654 }
1655 Some(EffectOutcome(Ok(_))) | Some(EffectOutcome(Err(_))) => {
1656 outputs.done = true;
1657 }
1658 None => {
1659 if let Some(streamed) = streamed
1660 && !streamed.text.is_empty()
1661 {
1662 let current = assistant_text_from_choice(&outputs.content);
1663 if current != streamed.text {
1664 outputs.content = vec![AssistantContent::text(&streamed.text)];
1665 }
1666 }
1667 }
1668 }
1669 }
1670}
1671
1672pub fn resolve_invalid_defaults(
1676 mut commands: Commands,
1677 calls: Query<(Entity, &ChildOf), Unresolved>,
1678 turns: Query<&ChildOf, With<Turn>>,
1679 runs: Query<&RunOf>,
1680 policies: Query<&InvalidCalls>,
1681) {
1682 for (call, turn_of) in &calls {
1683 let Ok(run_of) = turns.get(turn_of.parent()) else {
1684 continue;
1685 };
1686 let run = run_of.parent();
1687 let Ok(RunOf(agent)) = runs.get(run) else {
1688 continue;
1689 };
1690 let unhandled = setting(run, *agent, &policies).map_or(Unhandled::Fail, |p| p.unhandled);
1691 commands.entity(call).insert(match unhandled {
1692 Unhandled::Fail => Resolution::Fail,
1693 Unhandled::Ignore => Resolution::Ignore,
1694 });
1695 }
1696}
1697
1698#[derive(QueryData)]
1702pub struct ToolChild {
1703 pub entity: Entity,
1705 pub slot: &'static ToolCallSlot,
1707 pub issued: Has<Issued>,
1709 pub outcome: Option<&'static EffectOutcome>,
1711 pub batch_held: Has<BatchHeld>,
1713}
1714
1715#[derive(Component, Debug, Default, Clone, Copy, Reflect)]
1722#[reflect(Component)]
1723pub struct BatchHeld;
1724
1725pub fn batch_marker_follows_the_hold(
1729 released: On<Remove, crate::bus::Held>,
1730 mut commands: Commands,
1731) {
1732 let entity = released.event().entity;
1733 commands.queue(move |world: &mut World| {
1734 let Ok(mut effect) = world.get_entity_mut(entity) else {
1735 return;
1736 };
1737 effect.remove::<BatchHeld>();
1738 crate::bus::hold::forget_owner(world, entity, "rig-ecs/batch");
1739 });
1740}
1741
1742fn batch_children<'a>(
1744 turn: Entity,
1745 children: &Query<&Children>,
1746 tools: &'a Query<ToolChild>,
1747) -> Vec<ToolChildItem<'a, 'a>> {
1748 let mut found: Vec<_> = children
1749 .get(turn)
1750 .into_iter()
1751 .flat_map(|children| children.iter())
1752 .filter_map(|child| tools.get(child).ok())
1753 .collect();
1754 found.sort_by_key(|child| child.slot.index);
1755 found
1756}
1757
1758pub fn release_batch(
1762 mut commands: Commands,
1763 turns: Query<(Entity, &ChildOf), With<Batch>>,
1764 runs: Query<&RunOf>,
1765 policies: Query<&ToolPolicy>,
1766 children: Query<&Children>,
1767 tools: Query<ToolChild>,
1768) {
1769 for (turn, turn_of) in &turns {
1770 let run = turn_of.parent();
1771 let Ok(RunOf(agent)) = runs.get(run) else {
1772 continue;
1773 };
1774 let concurrency = setting(run, *agent, &policies)
1775 .map_or(1, |policy| policy.concurrency)
1776 .max(1);
1777 let batch = batch_children(turn, &children, &tools);
1778 if batch.iter().any(|child| {
1779 child
1780 .outcome
1781 .is_some_and(|o| policy::tool_failure(&o.0).is_some())
1782 }) {
1783 continue;
1784 }
1785 let active = batch
1787 .iter()
1788 .filter(|child| !child.batch_held && child.outcome.is_none())
1789 .count();
1790 let mut free = concurrency.saturating_sub(active);
1791 for child in &batch {
1792 if free == 0 {
1793 break;
1794 }
1795 if child.batch_held && !child.issued {
1796 let entity = child.entity;
1797 commands.queue(move |world: &mut World| {
1798 if let Ok(mut effect) = world.get_entity_mut(entity) {
1799 effect.remove::<BatchHeld>();
1800 }
1801 let _ = crate::bus::release_hold(world, entity, "rig-ecs/batch");
1804 });
1805 free -= 1;
1806 }
1807 }
1808 }
1809}
1810
1811pub fn land_batch(
1816 mut commands: Commands,
1817 mut assets: ResMut<BinaryAssets>,
1818 turns: Query<(Entity, &ChildOf, &Batch, &Outputs)>,
1819 runs: Query<(&OutputToolName, &RunSeq, &Cursor, &RunPhase)>,
1820 children: Query<&Children>,
1821 tools: Query<ToolChild>,
1822) {
1823 let mut turns: Vec<_> = turns.iter().collect();
1824 turns.sort_by_key(|(_, turn_of, _, _)| {
1825 runs.get(turn_of.parent()).map(|(_, seq, _, _)| *seq).ok()
1826 });
1827 for (turn, turn_of, batch, outs) in turns {
1828 let run = turn_of.parent();
1829 let Ok((minted, _, cursor, &RunPhase::ResolvingTools)) = runs.get(run) else {
1830 continue;
1831 };
1832 let calls = batch_children(turn, &children, &tools);
1833 let failure = calls
1834 .iter()
1835 .find_map(|child| child.outcome.and_then(|o| policy::tool_failure(&o.0)));
1836 let started_landed = calls
1837 .iter()
1838 .all(|child| !child.issued || child.outcome.is_some());
1839 if let Some(failure) = failure {
1840 if !started_landed {
1841 continue;
1842 }
1843 commands.entity(turn).remove::<Batch>();
1846 commands.entity(run).end(Failed(failure));
1847 for child in &calls {
1848 if !child.issued && child.outcome.is_none() {
1849 commands.entity(child.entity).despawn();
1850 }
1851 }
1852 continue;
1853 }
1854 if calls.len() < batch.calls || calls.iter().any(|child| child.outcome.is_none()) {
1855 continue;
1856 }
1857 let mut parts = Vec::with_capacity(calls.len());
1858 let mut statuses = Vec::with_capacity(calls.len());
1859 let mut failed = None;
1860 for child in &calls {
1861 let Some(EffectOutcome(outcome)) = child.outcome else {
1862 continue;
1863 };
1864 let slot = child.slot;
1865 match policy::tool_result_part(slot.id.clone(), slot.name.clone(), outcome) {
1866 Ok((part, status)) => {
1867 parts.push(part);
1868 statuses.push(status);
1869 }
1870 Err(failure) => {
1871 failed = Some(failure);
1872 break;
1873 }
1874 }
1875 }
1876 commands.entity(turn).remove::<Batch>();
1877 if let Some(failure) = failed {
1878 commands.entity(run).end(Failed(failure));
1879 continue;
1880 }
1881 let results_entity = match MessageParts::user(parts).and_then(|results| {
1882 spawn_deferred_with(&mut commands, &mut assets, run, results, statuses)
1883 }) {
1884 Ok(results) => results,
1885 Err(error) => {
1886 fail_content(&mut commands, run, error);
1887 continue;
1888 }
1889 };
1890 commands.entity(turn).insert((
1891 ToolTurnCommit { turn: cursor.turn },
1892 TurnResults(results_entity),
1893 ));
1894 let output_call = minted.0.as_deref().and_then(|name| {
1895 outs.content.iter().find_map(|part| match part {
1896 AssistantContent::ToolCall(call) if call.function.name == name => {
1897 Some(call.function.arguments_value().to_string())
1898 }
1899 _ => None,
1900 })
1901 });
1902 match output_call {
1903 Some(arguments) => {
1904 commands.entity(run).end((RunResult(arguments), Settled));
1905 }
1906 None => {
1907 commands.entity(run).phase(RunPhase::Assembling);
1908 }
1909 }
1910 commands.queue(move |world: &mut World| {
1911 if world.get::<Run>(run).is_some()
1914 && world.get::<ToolTurnCommit>(turn).is_some()
1915 && world
1916 .get::<ChildOf>(turn)
1917 .is_some_and(|parent| parent.parent() == run)
1918 {
1919 world.trigger(ToolTurnCommitted { run, turn });
1920 }
1921 });
1922 }
1923}
1924
1925enum InvalidVerdict {
1930 Fail(InvalidCall),
1931 Retry(InvalidCall, String),
1932 Skip(InvalidCall, String),
1933 Edit,
1934}
1935
1936fn invalid_verdict(pending: &[(Entity, InvalidCall, Resolution)]) -> InvalidVerdict {
1937 if let Some((_, call, _)) = pending
1938 .iter()
1939 .find(|(_, _, resolution)| matches!(resolution, Resolution::Fail))
1940 {
1941 return InvalidVerdict::Fail(call.clone());
1942 }
1943 if let Some((_, call, Resolution::Retry { feedback })) = pending
1944 .iter()
1945 .find(|(_, _, resolution)| matches!(resolution, Resolution::Retry { .. }))
1946 {
1947 return InvalidVerdict::Retry(call.clone(), feedback.clone());
1948 }
1949 if let Some((_, call, Resolution::Skip { reason })) = pending
1950 .iter()
1951 .find(|(_, _, resolution)| matches!(resolution, Resolution::Skip { .. }))
1952 {
1953 return InvalidVerdict::Skip(call.clone(), reason.clone());
1954 }
1955 InvalidVerdict::Edit
1956}
1957
1958fn landed_effect<'a>(
1960 turn: Entity,
1961 effects: &'a Query<LandedEffect, NotRetrieval>,
1962) -> Option<LandedEffectItem<'a, 'a>> {
1963 effects
1964 .iter()
1965 .find(|effect| effect.turn_of.parent() == turn)
1966}
1967
1968fn granted_tools(
1972 turn: Entity,
1973 children: &Query<&Children>,
1974 adverts: &Query<&Advert>,
1975 bound: &Query<&Bound>,
1976 access: Option<&ToolAccess>,
1977) -> Vec<GrantedTool> {
1978 if let Some(executable) = access.and_then(|access| access.executable.as_ref()) {
1979 return executable
1980 .iter()
1981 .map(|(name, key)| GrantedTool {
1982 name: name.clone(),
1983 key: key.clone(),
1984 handler: None,
1985 })
1986 .collect();
1987 }
1988 links_in_order(turn, children, adverts)
1989 .into_iter()
1990 .filter_map(|Advert(tool)| bound.get(*tool).ok().map(|bound| (*tool, bound)))
1991 .filter_map(|(tool, bound)| {
1992 policy::tool_name(&bound.descriptor).map(|name| GrantedTool {
1993 name: name.to_owned(),
1994 key: bound.key.clone(),
1995 handler: Some(tool),
1996 })
1997 })
1998 .collect()
1999}
2000
2001pub fn record_usage(
2004 mut commands: Commands,
2005 mut turns: Query<(Entity, &ChildOf, &mut Outputs), Unread>,
2006 effects: Query<LandedEffect, NotRetrieval>,
2007 runs: Query<(&Usage, &RunPhase)>,
2008) {
2009 for (turn, turn_of, mut outs) in &mut turns {
2010 if outs.usage_recorded {
2011 continue;
2012 }
2013 let run = turn_of.parent();
2014 let Ok((usage, &RunPhase::AwaitingModel)) = runs.get(run) else {
2015 continue;
2016 };
2017 let Some(EffectOutcome(Ok(Outcome::Completion(response)))) =
2018 landed_effect(turn, &effects).map(|effect| effect.outcome)
2019 else {
2020 continue;
2021 };
2022 commands.entity(run).insert(Usage(usage.0 + response.usage));
2023 outs.usage_recorded = true;
2024 }
2025}
2026
2027fn pending_invalid_calls(
2029 turn: Entity,
2030 invalid_calls: &Query<(Entity, &ChildOf, &InvalidCall, &Resolution)>,
2031) -> Vec<(Entity, InvalidCall, Resolution)> {
2032 invalid_calls
2033 .iter()
2034 .filter(|(_, child_of, _, _)| child_of.parent() == turn)
2035 .map(|(entity, _, call, resolution)| (entity, call.clone(), resolution.clone()))
2036 .collect()
2037}
2038
2039fn edited_content(
2042 content: &[AssistantContent],
2043 pending: &[(Entity, InvalidCall, Resolution)],
2044) -> Vec<AssistantContent> {
2045 let mut content = content.to_vec();
2046 for (_, call, resolution) in pending {
2047 match resolution {
2048 Resolution::Repair { to } => {
2049 for part in &mut content {
2050 if let AssistantContent::ToolCall(tool_call) = part
2051 && tool_call.id == call.id
2052 && let Ok(to) = rig_core::message::ToolName::new(to.clone())
2053 {
2054 tool_call.function.name = to;
2055 }
2056 }
2057 }
2058 Resolution::Ignore => {
2059 content.retain(|part| match part {
2060 AssistantContent::ToolCall(tool_call) => tool_call.id != call.id,
2061 _ => true,
2062 });
2063 }
2064 Resolution::Fail | Resolution::Retry { .. } | Resolution::Skip { .. } => {}
2065 }
2066 }
2067 content
2068}
2069
2070fn fail_unknown_call(
2073 commands: &mut Commands,
2074 assets: &mut BinaryAssets,
2075 run: Entity,
2076 turn: Entity,
2077 call: InvalidCall,
2078) -> Result<(), ContentError> {
2079 commands.entity(turn).insert(Materialised);
2080 let prefix = AssistantMessage::aborted(
2083 call.origin.clone(),
2084 call.prefix.clone(),
2085 format!("the run failed on the unknown tool call `{}`", call.name),
2086 );
2087 if let Ok(assistant) = MessageParts::assistant(prefix) {
2088 spawn_deferred(commands, assets, run, assistant)?;
2089 }
2090 commands
2091 .entity(run)
2092 .end(Failed(Failure::UnknownToolCall { name: call.name }));
2093 Ok(())
2094}
2095
2096fn abandon_turn(
2102 commands: &mut Commands,
2103 assets: &mut BinaryAssets,
2104 run: Entity,
2105 turn: Entity,
2106 outs: &Outputs,
2107 call: &InvalidCall,
2108 feedback: &str,
2109 retries: Option<InvalidRetries>,
2110) -> Result<(), ContentError> {
2111 commands.entity(turn).insert(Materialised);
2112 let content = if call.prefix.is_empty() {
2115 outs.content.clone()
2116 } else {
2117 call.prefix.clone()
2118 };
2119 let diagnostic_id = &call.id;
2120 let message = if call.prefix.is_empty() {
2123 outs.head.message(content.clone())
2124 } else {
2125 AssistantMessage::rolled_back(call.origin.clone(), content.clone())
2126 };
2127 let assistant = MessageParts::assistant(message)?;
2128 spawn_deferred(commands, assets, run, assistant)?;
2129 let results = MessageParts::user(invalid_call_feedback(&content, diagnostic_id, feedback))
2130 .map_err(|_| ContentError::Shape)?;
2131 let skipped = match &results {
2132 MessageParts::User { content } => vec![ToolResultStatus::Skipped; content.len()],
2133 MessageParts::Assistant(_) => Vec::new(),
2134 };
2135 spawn_deferred_with(commands, assets, run, results, skipped)?;
2136 let mut run_commands = commands.entity(run);
2137 run_commands.insert(RunPhase::Assembling);
2138 if let Some(retries) = retries {
2139 run_commands.insert(retries);
2140 }
2141 Ok(())
2142}
2143
2144pub fn judge_invalid_calls(
2150 mut commands: Commands,
2151 mut assets: ResMut<BinaryAssets>,
2152 mut turns: Query<(Entity, &ChildOf, &mut Outputs), Unread>,
2153 effects: Query<LandedEffect, NotRetrieval>,
2154 runs: Query<(&RunOf, &RunSeq, &InvalidRetries, &OutputToolName, &RunPhase)>,
2155 invalid_calls: Query<(Entity, &ChildOf, &InvalidCall, &Resolution)>,
2156 choices: Query<&ToolChoiceSpec>,
2157 policies: Query<&InvalidCalls>,
2158) {
2159 let mut turns: Vec<_> = turns
2160 .iter_mut()
2161 .filter_map(|(turn, turn_of, outs)| {
2162 let run = turn_of.parent();
2163 runs.get(run)
2164 .ok()
2165 .map(|(_, seq, ..)| (*seq, turn, run, outs))
2166 })
2167 .collect();
2168 turns.sort_by_key(|(seq, ..)| *seq);
2169 for (_, turn, run, mut outs) in turns {
2170 let Ok((RunOf(agent), _, invalid_retries, _, &RunPhase::AwaitingModel)) = runs.get(run)
2171 else {
2172 continue;
2173 };
2174 let agent = *agent;
2175 let stream = landed_effect(turn, &effects).and_then(|effect| effect.streamed);
2176 let pending = pending_invalid_calls(turn, &invalid_calls);
2177 if pending.is_empty() {
2178 continue;
2179 }
2180 let budget = setting(run, agent, &policies).map_or(0, |p| p.retries);
2181 let tool_choice = setting(run, agent, &choices).and_then(|c| c.0.as_ref());
2182 let verdict = match invalid_verdict(&pending) {
2183 InvalidVerdict::Retry(call, _) if invalid_retries.0 >= budget => {
2184 InvalidVerdict::Fail(call)
2185 }
2186 InvalidVerdict::Skip(call, _) if matches!(tool_choice, Some(ToolChoice::None)) => {
2187 InvalidVerdict::Fail(call)
2188 }
2189 verdict => verdict,
2190 };
2191 if !outs.done && !matches!(verdict, InvalidVerdict::Fail(_)) {
2194 continue;
2195 }
2196 if matches!(verdict, InvalidVerdict::Edit)
2200 && stream.is_some_and(|stream| {
2201 outs.stream_validated < stream_invalid::validation_len(stream)
2202 })
2203 {
2204 continue;
2205 }
2206 for (entity, _, _) in &pending {
2207 commands.entity(*entity).despawn();
2208 }
2209 let judged = match verdict {
2210 InvalidVerdict::Fail(call) => {
2211 fail_unknown_call(&mut commands, &mut assets, run, turn, call)
2212 }
2213 InvalidVerdict::Retry(call, feedback) | InvalidVerdict::Skip(call, feedback) => {
2214 let retries = matches!(invalid_verdict(&pending), InvalidVerdict::Retry(..))
2215 .then(|| InvalidRetries(invalid_retries.0 + 1));
2216 abandon_turn(
2217 &mut commands,
2218 &mut assets,
2219 run,
2220 turn,
2221 &outs,
2222 &call,
2223 &feedback,
2224 retries,
2225 )
2226 }
2227 InvalidVerdict::Edit => {
2228 outs.content = edited_content(&outs.content, &pending);
2229 Ok(())
2230 }
2231 };
2232 if let Err(error) = judged {
2233 fail_content(&mut commands, run, error);
2234 }
2235 }
2236}
2237
2238fn provider_failed(
2241 commands: &mut Commands,
2242 run: Entity,
2243 report: &rig_core::error::ErrorReport,
2244 budget: usize,
2245 retried: usize,
2246 witness: Option<(&crate::bus::Witnessing, &crate::bus::Subjects)>,
2247) {
2248 if report.kind != ErrorKind::Cancelled && report.retryable && retried < budget {
2249 let attempt = retried + 1;
2250 commands.entity(run).insert((
2251 ProviderRetried(attempt),
2252 ProviderRetrying,
2253 RunPhase::Assembling,
2254 ));
2255 if let Some((witness, subjects)) = witness {
2256 witness::observe_provider_retry(
2257 witness,
2258 subjects.of_scope(run),
2259 attempt,
2260 budget,
2261 report,
2262 );
2263 }
2264 return;
2265 }
2266 let failure = if report.kind == ErrorKind::Cancelled {
2267 Failure::Cancelled(report.clone())
2268 } else {
2269 Failure::Provider(report.clone())
2270 };
2271 commands.entity(run).end(Failed(failure));
2272}
2273
2274pub fn read_turn(
2281 mut commands: Commands,
2282 turns: Query<(Entity, &ChildOf, &Outputs), Unread>,
2283 effects: Query<LandedEffect, NotRetrieval>,
2284 runs: Query<(
2285 &RunOf,
2286 &RunSeq,
2287 &ProviderRetried,
2288 &OutputToolName,
2289 &RunPhase,
2290 )>,
2291 pending: Query<&ChildOf, With<InvalidCall>>,
2292 children: Query<&Children>,
2293 adverts: Query<&Advert>,
2294 bound: Query<&Bound>,
2295 access: Query<&ToolAccess>,
2296 provider_retries: Query<&ProviderRetries>,
2297 subjects: crate::bus::Subjects,
2298 witness: Option<Res<crate::bus::Witnessing>>,
2299 mut assets: ResMut<BinaryAssets>,
2300) {
2301 let mut turns: Vec<_> = turns
2302 .iter()
2303 .filter_map(|(turn, turn_of, outs)| {
2304 let run = turn_of.parent();
2305 runs.get(run)
2306 .ok()
2307 .map(|(_, seq, ..)| (*seq, turn, run, outs))
2308 })
2309 .collect();
2310 turns.sort_by_key(|(seq, ..)| *seq);
2311 for (_, turn, run, outs) in turns {
2312 let Ok((RunOf(agent), _, provider_retried, minted, &RunPhase::AwaitingModel)) =
2313 runs.get(run)
2314 else {
2315 continue;
2316 };
2317 if !outs.done || pending.iter().any(|child_of| child_of.parent() == turn) {
2318 continue;
2319 }
2320 let Some(effect) = landed_effect(turn, &effects) else {
2321 continue;
2322 };
2323 commands.entity(turn).insert(Materialised);
2324 let response = match &effect.outcome.0 {
2325 Ok(Outcome::Completion(response)) => response,
2326 Ok(other) => {
2327 commands
2328 .entity(run)
2329 .end(Failed(Failure::Unsupported(format!(
2330 "a {} answer to a completion",
2331 other.family()
2332 ))));
2333 continue;
2334 }
2335 Err(report) => {
2336 let budget = setting(run, *agent, &provider_retries)
2337 .map_or(DEFAULT_PROVIDER_RETRIES, |retries| retries.0);
2338 let witness = witness.as_deref().map(|witness| (witness, &subjects));
2339 provider_failed(
2340 &mut commands,
2341 run,
2342 report,
2343 budget,
2344 provider_retried.0,
2345 witness,
2346 );
2347 continue;
2348 }
2349 };
2350 if let Some(message) = rig_core::completion::message::turn_failure(
2351 &outs.content,
2352 Some(&response.stop()),
2353 response.finish_reason().as_ref(),
2354 ) {
2355 let calls = outs
2358 .content
2359 .iter()
2360 .any(|block| matches!(block, AssistantContent::ToolCall(_)));
2361 if calls {
2362 let head = AssistantHead::of(&response.head());
2363 let kept = MessageParts::assistant(head.message(outs.content.clone()))
2364 .and_then(|turn| spawn_deferred(&mut commands, &mut assets, run, turn));
2365 if let Err(error) = kept {
2366 fail_content(&mut commands, run, error);
2367 continue;
2368 }
2369 }
2370 let report = rig_core::error::ErrorReport::from(
2371 &rig_core::error::ProviderError::Response(message),
2372 );
2373 commands.entity(run).end(Failed(Failure::Provider(report)));
2374 continue;
2375 }
2376 let access = access.get(turn).ok();
2377 let granted = granted_tools(turn, &children, &adverts, &bound, access);
2378 let allowed = access.and_then(|access| access.allowed.as_ref());
2379 let read = TurnRead {
2380 head: AssistantHead::of(&response.head()),
2381 content: outs.content.clone(),
2382 granted,
2383 assistant: None,
2384 };
2385 let invalid: Vec<_> = read
2386 .calls()
2387 .filter(|call| {
2388 let name = call.function.name.as_str();
2389 (!read.granted.iter().any(|tool| tool.name == name)
2390 || allowed.is_some_and(|allowed| !allowed.contains(name)))
2391 && minted.0.as_deref() != Some(name)
2392 })
2393 .collect();
2394 if !invalid.is_empty() {
2395 commands.entity(turn).remove::<Materialised>();
2396 for call in invalid {
2397 commands.spawn((
2398 InvalidCall {
2399 id: call.id.clone(),
2400 name: call.function.name.to_string(),
2401 arguments: call.function.arguments_value(),
2402 prefix: Vec::new(),
2403 stream_offset: None,
2404 origin: None,
2405 },
2406 ChildOf(turn),
2407 ));
2408 }
2409 continue;
2410 }
2411 commands.entity(turn).insert(read);
2412 }
2413}
2414
2415fn say_assistant(
2419 commands: &mut Commands,
2420 assets: &mut BinaryAssets,
2421 run: Entity,
2422 turn: Entity,
2423 read: &mut TurnRead,
2424 retry: Option<&Retry>,
2425) -> Result<(), ContentError> {
2426 let feedback_utterance = |commands: &mut Commands, assets: &mut BinaryAssets, feedback| {
2427 let user = MessageParts::User {
2428 content: vec![UserContent::text(feedback)],
2429 };
2430 spawn_deferred(commands, assets, run, user).map(drop)
2431 };
2432 if is_empty_assistant_turn(&read.content) {
2433 commands.entity(turn).remove::<(Retry, TurnRead)>();
2434 match retry {
2435 Some(Retry { feedback }) => {
2436 if let Some(feedback) = feedback {
2437 feedback_utterance(commands, assets, feedback)?;
2438 }
2439 commands.entity(run).phase(RunPhase::Assembling);
2440 }
2441 None => {
2442 commands
2443 .entity(run)
2444 .end((RunResult(String::new()), Settled));
2445 }
2446 }
2447 return Ok(());
2448 }
2449 let assistant = MessageParts::assistant(read.head.message(read.content.clone()))?;
2450 if let Some(Retry { feedback }) = retry {
2451 commands.entity(turn).remove::<(Retry, TurnRead)>();
2452 if read.calls().next().is_some() {
2453 commands.entity(run).end(Failed(Failure::Unsupported(
2454 "a retry of a tool-bearing turn: steer the tool calls instead".to_owned(),
2455 )));
2456 return Ok(());
2457 }
2458 if let Some(feedback) = feedback {
2459 spawn_deferred(commands, assets, run, assistant)?;
2460 feedback_utterance(commands, assets, feedback)?;
2461 }
2462 commands.entity(run).phase(RunPhase::Assembling);
2463 return Ok(());
2464 }
2465 read.assistant = Some(spawn_deferred(commands, assets, run, assistant)?);
2466 Ok(())
2467}
2468
2469pub fn materialise_assistant(
2472 mut commands: Commands,
2473 mut assets: ResMut<BinaryAssets>,
2474 mut turns: Query<(Entity, &ChildOf, &mut TurnRead, Option<&Retry>)>,
2475 runs: Query<&RunSeq>,
2476) {
2477 let mut turns: Vec<_> = turns
2478 .iter_mut()
2479 .filter_map(|(turn, turn_of, read, retry)| {
2480 let run = turn_of.parent();
2481 runs.get(run).ok().map(|seq| (*seq, turn, run, read, retry))
2482 })
2483 .collect();
2484 turns.sort_by_key(|(seq, ..)| *seq);
2485 for (_, turn, run, mut read, retry) in turns {
2486 if let Err(error) = say_assistant(&mut commands, &mut assets, run, turn, &mut read, retry) {
2487 fail_content(&mut commands, run, error);
2488 commands.entity(turn).remove::<TurnRead>();
2489 }
2490 }
2491}
2492
2493pub fn materialise_batch(
2497 mut commands: Commands,
2498 turns: Query<(Entity, &ChildOf, &TurnRead)>,
2499 runs: Query<(&RunOf, &RunSeq)>,
2500 tool_policies: Query<&ToolPolicy>,
2501 contexts: Query<&ToolContextSpec>,
2502) {
2503 let mut turns: Vec<_> = turns
2504 .iter()
2505 .filter_map(|(turn, turn_of, read)| {
2506 let run = turn_of.parent();
2507 runs.get(run)
2508 .ok()
2509 .map(|(RunOf(agent), seq)| (*seq, turn, run, *agent, read))
2510 })
2511 .collect();
2512 turns.sort_by_key(|(seq, ..)| *seq);
2513 for (_, turn, run, agent, read) in turns {
2514 let batch: Vec<_> = read
2515 .calls()
2516 .filter_map(|call| {
2517 read.granted
2518 .iter()
2519 .find(|tool| tool.name == call.function.name)
2520 .map(|tool| (call, tool))
2521 })
2522 .collect();
2523 let (Some(assistant), false) = (read.assistant, batch.is_empty()) else {
2524 continue;
2525 };
2526 let concurrency = setting(run, agent, &tool_policies)
2527 .map_or(1, |policy| policy.concurrency)
2528 .max(1);
2529 let inputs = setting(run, agent, &contexts)
2530 .map(|spec| spec.0.for_dispatch())
2531 .unwrap_or_default();
2532 let count = batch.len();
2533 for (index, (call, tool)) in batch.into_iter().enumerate() {
2534 if let Some(raw) = &call.function.invalid_arguments {
2537 commands.spawn((
2538 ToolCallSlot {
2539 index,
2540 id: call.id.clone(),
2541 name: call.function.name.clone(),
2542 },
2543 EffectOutcome(Ok(rig_core::effect::Outcome::ToolResult {
2544 result: rig_core::tool::ToolResult::skipped(
2545 rig_core::transcript::invalid_arguments_feedback(
2546 call.function.name.as_str(),
2547 raw,
2548 ),
2549 ),
2550 })),
2551 ChildOf(turn),
2552 ));
2553 continue;
2554 }
2555 let mut effect = commands.spawn((
2556 PendingEffect::new(
2557 tool.key.clone(),
2558 EffectKind::ToolCall {
2559 name: call.function.name.to_string(),
2560 args: call.function.arguments_value().to_string(),
2561 },
2562 ),
2563 ToolInputs(inputs.clone()),
2564 ToolCallSlot {
2565 index,
2566 id: call.id.clone(),
2567 name: call.function.name.clone(),
2568 },
2569 ChildOf(turn),
2570 ));
2571 if let Some(handler) = tool.handler {
2572 effect.insert(ServedBy(handler));
2573 }
2574 if index >= concurrency {
2575 effect.insert(BatchHeld);
2576 let entity = effect.id();
2577 commands.queue(move |world: &mut World| {
2578 let _ = crate::bus::acquire_hold(
2582 world,
2583 entity,
2584 rig_core::observe::Emitter::versioned(
2585 "rig-ecs/batch",
2586 env!("CARGO_PKG_VERSION"),
2587 ),
2588 );
2589 });
2590 }
2591 }
2592 commands
2593 .entity(turn)
2594 .remove::<TurnRead>()
2595 .insert((Batch { calls: count }, TurnAssistant(assistant)));
2596 commands.entity(run).phase(RunPhase::ResolvingTools);
2597 }
2598}
2599
2600fn reprompt_for(
2608 read: &TurnRead,
2609 name: &str,
2610 schema: Option<&serde_json::Value>,
2611) -> Option<(MessageParts, Vec<ToolResultStatus>)> {
2612 match read.calls().find(|call| call.function.name == name) {
2613 Some(call) => {
2614 let feedback = if let Some(raw) = &call.function.invalid_arguments {
2615 rig_core::transcript::invalid_arguments_feedback(name, raw)
2616 } else {
2617 let missing = schema
2618 .map(|schema| missing_required_fields(schema, &call.function.arguments_value()))
2619 .unwrap_or_default();
2620 if missing.is_empty() {
2621 return None;
2622 }
2623 reprompt_missing_fields(name, &missing)
2624 };
2625 let reprompt = MessageParts::User {
2626 content: vec![UserContent::ToolResult(
2627 call.error_result(vec![ToolResultContent::text(feedback)]),
2628 )],
2629 };
2630 Some((reprompt, vec![ToolResultStatus::Skipped]))
2631 }
2632 None => {
2633 if text_satisfies_schema(schema, &assistant_text_from_choice(&read.content)) {
2634 return None;
2635 }
2636 let reprompt = MessageParts::User {
2637 content: vec![UserContent::text(reprompt_text_answer(name))],
2638 };
2639 Some((reprompt, Vec::new()))
2640 }
2641 }
2642}
2643
2644pub fn materialise_reprompt(
2648 mut commands: Commands,
2649 mut assets: ResMut<BinaryAssets>,
2650 turns: Query<(Entity, &ChildOf, &TurnRead, &Folded)>,
2651 runs: Query<(&RunOf, &RunSeq, &Cursor, &OutputRetries, &OutputToolName)>,
2652 outputs: Query<&Output>,
2653 max_turns: Query<&MaxTurns>,
2654) {
2655 let mut turns: Vec<_> = turns
2656 .iter()
2657 .filter(|(_, _, _, Folded(mode))| *mode == OutputKind::Tool)
2658 .filter_map(|(turn, turn_of, read, _)| {
2659 let run = turn_of.parent();
2660 runs.get(run)
2661 .ok()
2662 .map(|(_, seq, ..)| (*seq, turn, run, read))
2663 })
2664 .collect();
2665 turns.sort_by_key(|(seq, ..)| *seq);
2666 for (_, turn, run, read) in turns {
2667 let Ok((RunOf(agent), _, cursor, retries, OutputToolName(Some(name)))) = runs.get(run)
2668 else {
2669 continue;
2670 };
2671 let agent = *agent;
2672 let limit = setting(run, agent, &max_turns).map_or(1, |limit| limit.0);
2673 if retries.0 >= 1 || cursor.turn >= limit {
2674 continue;
2675 }
2676 let schema = setting(run, agent, &outputs).and_then(|output| output.schema.as_ref());
2677 let Some((reprompt, statuses)) = reprompt_for(read, name, schema) else {
2678 continue;
2679 };
2680 commands
2681 .entity(turn)
2682 .remove::<TurnRead>()
2683 .insert(Reprompt(reprompt.to_message()));
2684 match spawn_deferred_with(&mut commands, &mut assets, run, reprompt, statuses) {
2685 Ok(_) => {
2686 commands
2687 .entity(run)
2688 .end((OutputRetries(retries.0 + 1), RunPhase::Assembling));
2689 }
2690 Err(error) => fail_content(&mut commands, run, error),
2691 }
2692 }
2693}
2694
2695pub fn materialise_answer(
2701 mut commands: Commands,
2702 mut assets: ResMut<BinaryAssets>,
2703 turns: Query<(Entity, &ChildOf, &TurnRead, &Folded)>,
2704 runs: Query<(&RunSeq, &OutputToolName)>,
2705) {
2706 let mut turns: Vec<_> = turns
2707 .iter()
2708 .filter_map(|(turn, turn_of, read, folded)| {
2709 let run = turn_of.parent();
2710 runs.get(run)
2711 .ok()
2712 .map(|(seq, minted)| (*seq, turn, run, read, folded, minted))
2713 })
2714 .collect();
2715 turns.sort_by_key(|(seq, ..)| *seq);
2716 for (_, turn, run, read, Folded(mode), minted) in turns {
2717 commands.entity(turn).remove::<TurnRead>();
2718 let output_call = match (mode, minted.0.as_deref(), read.assistant) {
2719 (OutputKind::Tool, Some(name), Some(assistant)) => read
2720 .calls()
2721 .find(|call| call.function.name == name)
2722 .map(|call| (assistant, call)),
2723 _ => None,
2724 };
2725 let Some((assistant, call)) = output_call else {
2726 commands.entity(run).end((
2727 RunResult(assistant_text_from_choice(&read.content)),
2728 Settled,
2729 ));
2730 continue;
2731 };
2732 if let Some(raw) = &call.function.invalid_arguments {
2733 let report = rig_core::error::ErrorReport::from(
2734 &rig_core::error::ProviderError::Response(format!(
2735 "the output tool `{}` was called with arguments that are not a JSON object: {raw}",
2736 call.function.name
2737 )),
2738 );
2739 commands.entity(run).end(Failed(Failure::Provider(report)));
2740 continue;
2741 }
2742 let output = call.function.arguments_value().to_string();
2743 let mut final_content: Vec<_> = read
2744 .content
2745 .iter()
2746 .filter(|part| !matches!(part, AssistantContent::ToolCall(_)))
2747 .cloned()
2748 .collect();
2749 final_content.push(AssistantContent::text(output.clone()));
2750 let restated = MessageParts::assistant(read.head.message(final_content));
2751 match restated
2752 .and_then(|restated| replace_deferred(&mut commands, &mut assets, assistant, restated))
2753 {
2754 Ok(()) => {
2755 commands.entity(run).end((RunResult(output), Settled));
2756 }
2757 Err(error) => fail_content(&mut commands, run, error),
2758 }
2759 }
2760}
2761
2762pub fn effect_cancelled(
2765 removed: On<bevy_ecs::lifecycle::Remove, PendingEffect>,
2766 effects: Query<(&ChildOf, Has<ToolCallSlot>), With<PendingEffect>>,
2767 turns: Query<TurnState, With<Turn>>,
2768 runs: Query<RunState, With<Run>>,
2769 mut commands: Commands,
2770) {
2771 let effect = removed.event().entity;
2772 let Ok((turn_of, is_tool_call)) = effects.get(effect) else {
2773 return;
2774 };
2775 let turn = turn_of.parent();
2776 let Ok(TurnStateItem {
2777 run_of,
2778 materialised,
2779 batched,
2780 }) = turns.get(turn)
2781 else {
2782 return;
2783 };
2784 let run = run_of.parent();
2785 let Ok(RunStateItem { phase, failed }) = runs.get(run) else {
2786 return;
2787 };
2788 if failed {
2791 return;
2792 }
2793 let cancelled = Failed(Failure::Cancelled(rig_core::serve::cancelled()));
2794 if is_tool_call && batched && phase == Some(&RunPhase::ResolvingTools) {
2795 commands.entity(turn).remove::<Batch>();
2796 commands.entity(run).end(cancelled);
2797 } else if !is_tool_call && !materialised && phase == Some(&RunPhase::AwaitingModel) {
2798 commands.entity(turn).insert(Materialised);
2799 commands.entity(run).end(cancelled);
2800 }
2801}
2802
2803pub fn run_cancelled(
2807 added: On<Add, Cancelled>,
2808 reasons: Query<(&Cancelled, Has<Settled>, Has<Failed>)>,
2809 children: Query<&Children>,
2810 turns: Query<(), With<Turn>>,
2811 effects: Query<Has<Issued>, With<PendingEffect>>,
2812 mut commands: Commands,
2813) {
2814 let run = added.event().entity;
2815 let Ok((Cancelled(reason), settled, failed)) = reasons.get(run) else {
2816 return;
2817 };
2818 if settled || failed {
2821 return;
2822 }
2823 let mut pending: Vec<Entity> = Vec::new();
2824 let mut unread: Vec<Entity> = Vec::new();
2825 for child in children
2826 .get(run)
2827 .map(|c| c.iter().collect::<Vec<_>>())
2828 .unwrap_or_default()
2829 {
2830 if let Ok(issued) = effects.get(child) {
2831 if !issued {
2832 pending.push(child);
2833 }
2834 continue;
2835 }
2836 if turns.get(child).is_ok() {
2837 unread.push(child);
2838 for effect in children
2839 .get(child)
2840 .map(|c| c.iter().collect::<Vec<_>>())
2841 .unwrap_or_default()
2842 {
2843 if let Ok(false) = effects.get(effect) {
2844 pending.push(effect);
2845 }
2846 }
2847 }
2848 }
2849 commands
2852 .entity(run)
2853 .remove::<RunPhase>()
2854 .insert(Failed(Failure::Cancelled(
2855 rig_core::error::ErrorReport::new(ErrorKind::Cancelled, reason.clone()),
2856 )));
2857 for effect in pending {
2858 commands.entity(effect).despawn();
2859 }
2860 for turn in unread {
2861 commands
2862 .entity(turn)
2863 .insert(Materialised)
2864 .remove::<(Batch, Fresh, RequestPatch)>();
2865 }
2866}