Skip to main content

rig_ecs/agent/
mod.rs

1//! Components and relationships describing agents, conversations, runs, and turns.
2//!
3//! [`crate::policy::fold_request`] derives requests from this graph during
4//! [`crate::systems::RigSet::Assemble`]. Hosts steer runs by writing policy
5//! components at the corresponding schedule boundaries.
6//!
7//! ```
8//! use rig_ecs::agent::{Owner, Preamble};
9//! let mut world = bevy_ecs::world::World::new();
10//! world.spawn((Owner("assistant".into()), Preamble(Some("Be concise.".into()))));
11//! ```
12
13pub mod checkpoint;
14pub mod content;
15pub mod reflect;
16
17use bevy_ecs::prelude::*;
18use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
19use rig_core::{
20    completion::{
21        Usage as WireUsage,
22        message::{
23            AssistantContent, AssistantMessage, CallId, Message, ToolChoice, ToolName, UserContent,
24        },
25    },
26    error::ErrorReport,
27    tool::ToolContext,
28};
29use serde::{Deserialize, Serialize};
30
31/// The agent's name: the owner of every key it mints (`<owner>/model:..`),
32/// the scope of every run's records.
33#[derive(Component, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Reflect)]
34#[reflect(Component)]
35pub struct Owner(pub String);
36
37/// The system prompt: `None` is "no system message" (a run without a
38/// preamble), `Some("")` an empty one that is still sent.
39#[derive(Component, Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Reflect)]
40#[reflect(Component)]
41pub struct Preamble(pub Option<String>);
42
43/// Sampling temperature, if the request names one.
44#[derive(Component, Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, Reflect)]
45#[reflect(Component)]
46pub struct Temperature(pub Option<f64>);
47
48/// The answer's token budget, if the request names one.
49#[derive(
50    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
51)]
52#[reflect(Component)]
53pub struct MaxTokens(pub Option<u64>);
54
55/// Whether a finish reason outside the normalized vocabulary
56/// ([`FinishReason::Other`](rig_core::completion::FinishReason::Other)) ends
57/// a turn as a normal stop, on the agent or the run (the run's wins). Absent
58/// or `false`, such a turn fails the run. `Other` also holds genuine failures,
59/// such as a malformed tool call, so with this set their tool calls run.
60/// Filtered content still fails the turn.
61#[derive(
62    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
63)]
64#[reflect(Component)]
65pub struct AcceptUnknownFinishReasons(pub bool);
66
67/// Provider-specific parameters the request carries verbatim.
68#[derive(Component, Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Reflect)]
69#[reflect(Component)]
70pub struct AdditionalParams(
71    #[reflect(remote = crate::agent::reflect::OptionalJsonReflect)] pub Option<serde_json::Value>,
72);
73
74/// Portable generation options the request carries. A run's overlay the
75/// agent's field by field
76/// ([`GenerationOptions::overlay`](rig_core::completion::GenerationOptions::overlay)):
77/// each field the run sets wins, every other keeps the agent's.
78#[derive(Component, Debug, Clone, Default, PartialEq, Serialize, Deserialize, Reflect)]
79#[reflect(opaque, Component, Debug, PartialEq, Serialize, Deserialize)]
80pub struct Options(pub rig_core::completion::GenerationOptions);
81
82/// Typed per-provider options the request carries. A run's overlay the
83/// agent's per provider
84/// ([`ProviderOptions::overlay`](rig_core::completion::ProviderOptions::overlay)):
85/// each provider entry the run holds replaces the agent's, every other
86/// entry keeps the agent's.
87#[derive(Component, Debug, Clone, Default, PartialEq, Serialize, Deserialize, Reflect)]
88#[reflect(opaque, Component, Debug, PartialEq, Serialize, Deserialize)]
89pub struct ProviderOptions(pub rig_core::completion::ProviderOptions);
90
91impl ProviderOptions {
92    /// The component with `options` stored as the entry of their provider
93    /// ([`ExtensionOptions::Ext`](rig_core::completion::ExtensionOptions::Ext)),
94    /// replacing that provider's entry
95    /// ([`ProviderOptions::set`](rig_core::completion::ProviderOptions::set)).
96    /// Options that do not serialize fail the encode of each request that
97    /// carries them.
98    ///
99    /// ```
100    /// use rig_core::providers::openrouter::extension::{OpenRouterExt, OpenRouterOptions};
101    ///
102    /// let component = rig_ecs::agent::ProviderOptions::default()
103    ///     .set(OpenRouterOptions::new().session_id("s-1"));
104    /// assert!(component.0.contains::<OpenRouterExt>());
105    /// ```
106    pub fn set<O: rig_core::completion::ExtensionOptions>(self, options: O) -> Self {
107        Self(self.0.set(options))
108    }
109}
110
111/// The program's tool choice: what the request's `tool_choice` starts
112/// from before the output mode has its say.
113#[derive(Component, Debug, Clone, Default, PartialEq, Serialize, Deserialize, Reflect)]
114#[reflect(Component)]
115pub struct ToolChoiceSpec(
116    #[reflect(remote = crate::agent::reflect::ToolChoiceReflect)] pub Option<ToolChoice>,
117);
118
119/// How the run's answer is asked for and read.
120#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect)]
121#[serde(rename_all = "snake_case")]
122pub enum OutputKind {
123    /// Resolve at request time: `Tool` when there is a schema and at least
124    /// one tool and the tool choice permits it, else `Native`.
125    #[default]
126    Auto,
127    /// The provider's native structured output (`output_schema`), or plain
128    /// text when there is no schema.
129    Native,
130    /// A synthetic output tool the model must call with the answer.
131    Tool,
132    /// The schema in the preamble; the answer is the text, unvalidated.
133    Prompted,
134}
135
136/// The output mode and its schema, if any.
137#[derive(Component, Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Reflect)]
138#[reflect(Component)]
139pub struct Output {
140    /// The mode.
141    pub mode: OutputKind,
142    /// The JSON schema of the answer, raw.
143    #[reflect(remote = crate::agent::reflect::OptionalJsonReflect)]
144    pub schema: Option<serde_json::Value>,
145}
146
147/// Output-tool configuration, resolved from the run before the agent.
148///
149/// An explicit name reserves Tool mode when a schema is present, just like
150/// a name already committed on the run. A conflicting real tool fails before
151/// provider dispatch. Without a name, the normal collision-safe name is used.
152#[derive(Component, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Reflect)]
153#[reflect(Component)]
154pub struct OutputToolConfig {
155    /// Reserved name, or the automatically chosen name when absent.
156    pub name: Option<String>,
157    /// Tool description, or the standard final-answer description when absent.
158    pub description: Option<String>,
159    /// Append the standard output-tool instructions to the preamble.
160    pub augment_preamble: bool,
161}
162
163impl Default for OutputToolConfig {
164    fn default() -> Self {
165        Self {
166            name: None,
167            description: None,
168            augment_preamble: true,
169        }
170    }
171}
172
173/// The model-call budget of a run.
174#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
175#[reflect(Component)]
176pub struct MaxTurns(pub usize);
177
178/// Retry budget for completion failures marked retryable by their error report.
179/// The run's value overrides the agent's; absent values use [`DEFAULT_PROVIDER_RETRIES`].
180/// Retries preserve history, do not rerun tools, create no failed assistant
181/// utterance, and do not consume [`MaxTurns`]. Host gate systems control backoff.
182#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
183#[reflect(Component)]
184pub struct ProviderRetries(pub usize);
185
186/// The provider-retry budget of a run that declares none.
187pub const DEFAULT_PROVIDER_RETRIES: usize = 3;
188
189/// How long a provider retry waits before its completion is re-issued:
190/// `base × 2^(attempt-1)`, at most `max`, on the world's clock (bevy_time's
191/// `Time`: a host that pauses `Time<Virtual>` holds every backoff). On the
192/// agent or the run; without one, a retry is re-issued at once.
193#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
194#[reflect(Component)]
195pub struct Backoff {
196    /// The first retry's delay.
197    pub base: std::time::Duration,
198    /// The longest delay any retry waits.
199    pub max: std::time::Duration,
200}
201
202/// What to do with a tool call the program does not advertise when no
203/// system resolved it.
204#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect)]
205#[serde(rename_all = "snake_case")]
206pub enum Unhandled {
207    /// The run fails at the record.
208    #[default]
209    Fail,
210    /// The call is dropped; the run goes on.
211    Ignore,
212}
213
214/// The invalid-call policy: how many retries before `unhandled` applies.
215#[derive(
216    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
217)]
218#[reflect(Component)]
219pub struct InvalidCalls {
220    /// Retries the program allows an invalid call.
221    pub retries: usize,
222    /// What happens when they are spent.
223    pub unhandled: Unhandled,
224}
225
226/// The application's versioned declaration of its policy systems, ordering,
227/// and configuration not represented by the library's policy components.
228/// Set on the agent or override on the run before stamping its identity.
229/// Change this value when that policy changes. It is a declaration, not an
230/// automatic fingerprint of executable code or ambient credentials.
231#[derive(Component, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Reflect)]
232#[reflect(Component)]
233pub struct PolicyVersion(pub String);
234
235/// How many of a turn's tool calls may be in flight at once: 1 (the
236/// default) runs them one after another in call order; N keeps N going.
237/// The run's, else the agent's.
238#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
239#[reflect(Component)]
240pub struct ToolPolicy {
241    /// Calls in flight at once; 0 reads as 1.
242    pub concurrency: usize,
243}
244
245impl Default for ToolPolicy {
246    fn default() -> Self {
247        Self { concurrency: 1 }
248    }
249}
250
251/// Inbound context whose `for_dispatch` snapshot becomes each call's tool inputs,
252/// separate from the effect payload. The run's value overrides the agent's;
253/// absent values use an empty context.
254#[derive(Component, Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Reflect)]
255#[reflect(Component)]
256pub struct ToolContextSpec(
257    #[reflect(remote = crate::agent::reflect::ToolContextReflect)] pub ToolContext,
258);
259
260/// The default `max_turns` the agent was built with, part of its identity
261/// (a run-level override is not).
262#[derive(
263    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
264)]
265#[reflect(Component)]
266pub struct DefaultMaxTurns(pub Option<usize>);
267
268/// The agent uses this model: a relationship to the model's handler
269/// entity (the bus module's `Bound`). A run may carry its own to override.
270#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
271#[relationship(relationship_target = ModelOf)]
272#[reflect(Component)]
273pub struct UsesModel(pub Entity);
274
275/// The agents and runs using this model.
276#[derive(Component, Debug, Default, Reflect)]
277#[relationship_target(relationship = UsesModel)]
278#[reflect(Component)]
279pub struct ModelOf(Vec<Entity>);
280
281/// The agent remembers: a relationship to the memory handler entity. A run
282/// spawned with no history loads the conversation before its first turn
283/// and appends what it said when it settles (CONTRACT §11).
284#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
285#[relationship(relationship_target = RememberedBy)]
286#[reflect(Component)]
287pub struct Remembers(pub Entity);
288
289/// The agents remembering through this handler.
290#[derive(Component, Debug, Default, Reflect)]
291#[relationship_target(relationship = Remembers)]
292#[reflect(Component)]
293pub struct RememberedBy(Vec<Entity>);
294
295/// The conversation a run loads and appends under (the run's, else the
296/// agent's).
297#[derive(Component, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Reflect)]
298#[reflect(Component)]
299pub struct Conversation(pub String);
300
301/// An utterance that came from memory: not appended again.
302#[derive(
303    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
304)]
305#[reflect(Component)]
306pub struct Remembered;
307
308/// The run loaded its conversation and will append to it when it settles.
309#[derive(
310    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
311)]
312#[reflect(Component)]
313pub struct Remembering;
314
315/// Finalization has created this run's memory append intent. Persisted so
316/// rehydrating `Settled` cannot schedule a second operation. The child
317/// effect owns the request, saved dispatch id, and eventual outcome; this
318/// marker does not claim that the external write succeeded exactly once.
319#[derive(
320    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
321)]
322#[reflect(Component)]
323pub struct MemoryAppendScheduled;
324
325/// A retrieval the agent makes before every turn: a link entity, `ChildOf`
326/// the agent, naming the index handler entity, with [`Retrieval`] saying
327/// what for (CONTRACT §12).
328#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
329#[relationship(relationship_target = RetrievedBy)]
330#[reflect(Component)]
331pub struct Retrieves(pub Entity);
332
333/// The retrieval links naming this index.
334#[derive(Component, Debug, Default, Reflect)]
335#[relationship_target(relationship = Retrieves)]
336#[reflect(Component)]
337pub struct RetrievedBy(Vec<Entity>);
338
339/// What a [`Retrieves`] link retrieves.
340#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
341#[reflect(Component)]
342pub struct Retrieval {
343    /// How many results are asked for.
344    pub samples: u64,
345    /// Documents to attach, or tools to advertise.
346    pub what: RetrievalKind,
347}
348
349/// The two retrievals.
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
351#[serde(rename_all = "snake_case")]
352pub enum RetrievalKind {
353    /// Scored documents, attached to the turn after its static ones.
354    Documents,
355    /// Tool ids, advertised first among the turn's tools.
356    Tools,
357}
358
359/// A grant advertised only when a tool retrieval names it.
360#[derive(
361    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
362)]
363#[reflect(Component)]
364pub struct Retrievable;
365
366/// A fresh turn whose retrievals are out: folded once they land.
367#[derive(
368    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
369)]
370#[reflect(Component)]
371pub struct Retrieving;
372
373/// A route: a link entity, `ChildOf` the agent, naming another model the
374/// agent may be steered to (a system inserting [`UsesModel`] on the run);
375/// the required row names it.
376#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
377#[relationship(relationship_target = RoutedTo)]
378#[reflect(Component)]
379pub struct Route(pub Entity);
380
381/// The routes naming this model.
382#[derive(Component, Debug, Default, Reflect)]
383#[relationship_target(relationship = Route)]
384#[reflect(Component)]
385pub struct RoutedTo(Vec<Entity>);
386
387/// A grant: a link entity, `ChildOf` the agent, naming one tool the agent
388/// advertises. Advertisement order is the agent's sibling (`Children`) order.
389#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
390#[relationship(relationship_target = Grants)]
391#[reflect(Component)]
392pub struct Grant(pub Entity);
393
394/// Tool execution and permission policy, independent of request advertisements.
395///
396/// Resolved from the run, then its agent. Assembly stores
397/// the effective snapshot on the turn before issuing its completion. Later
398/// run changes therefore affect future turns only. On a turn this component
399/// is the runtime's retained snapshot, not an input override. The synthetic output tool
400/// remains governed by the output configuration rather than these ordinary
401/// tool permissions.
402#[derive(Component, Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Reflect)]
403#[reflect(opaque, Component, Debug, PartialEq, Serialize, Deserialize)]
404pub struct ToolAccess {
405    /// Executable names and their registered handler keys. `None` uses the
406    /// advertised handlers; an explicit map may include unadvertised tools.
407    pub executable: Option<std::collections::BTreeMap<String, rig_core::effect::HandlerKey>>,
408    /// Names accepted by invalid-call policy. `None` uses executable names;
409    /// `Some(empty)` denies all ordinary tools without changing the request.
410    /// Kept independently for diagnostics even when a name is not executable.
411    pub allowed: Option<std::collections::BTreeSet<String>>,
412}
413
414/// The grants naming this tool.
415#[derive(Component, Debug, Default, Reflect)]
416#[relationship_target(relationship = Grant)]
417#[reflect(Component)]
418pub struct Grants(Vec<Entity>);
419
420/// A context link: a link entity, `ChildOf` the agent, naming one document
421/// every turn carries as static context, in sibling (`Children`) order.
422#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
423#[relationship(relationship_target = ContextOf)]
424#[reflect(Component)]
425pub struct Context(pub Entity);
426
427/// The context links naming this document.
428#[derive(Component, Debug, Default, Reflect)]
429#[relationship_target(relationship = Context)]
430#[reflect(Component)]
431pub struct ContextOf(Vec<Entity>);
432
433/// The document's stable id.
434#[derive(Component, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Reflect)]
435#[reflect(Component)]
436pub struct DocumentId(pub String);
437
438/// The document's text.
439#[derive(Component, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Reflect)]
440#[reflect(Component)]
441pub struct DocumentText(pub String);
442
443/// The document's string metadata, rendered before its text.
444#[derive(Component, Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Reflect)]
445#[reflect(Component)]
446pub struct DocumentProps(pub std::collections::HashMap<String, String>);
447
448/// An attachment: a link entity, `ChildOf` a turn, naming one document the
449/// turn's request carries, in sibling (`Children`) order.
450#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
451#[relationship(relationship_target = AttachedTo)]
452#[reflect(Component)]
453pub struct Attachment(pub Entity);
454
455/// The attachments naming this document.
456#[derive(Component, Debug, Default, Reflect)]
457#[relationship_target(relationship = Attachment)]
458#[reflect(Component)]
459pub struct AttachedTo(Vec<Entity>);
460
461/// An utterance: one message of the conversation, `ChildOf` its run, in
462/// sibling (`Children`) order. Each content child carries one discriminated
463/// [`content::parts::ContentPart`] component; tool results own further children.
464#[derive(
465    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
466)]
467#[reflect(Component)]
468pub struct Utterance;
469
470/// Who spoke.
471#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
472#[serde(rename_all = "snake_case")]
473#[reflect(Component)]
474pub enum Role {
475    /// The user, or a tool result the user side reports.
476    User,
477    /// The model.
478    Assistant,
479}
480
481/// The transport content of one message, by role. Not a graph component:
482/// [`content::parts::write_message`] stores it as ordered content entities.
483#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Reflect)]
484#[serde(tag = "role", rename_all = "snake_case")]
485#[reflect(opaque, Debug, PartialEq, Serialize, Deserialize)]
486pub enum MessageParts {
487    /// A user message's parts.
488    User {
489        /// The parts.
490        content: Vec<UserContent>,
491    },
492    /// An assistant message: its parts, origin, stop and provider message.
493    Assistant(AssistantMessage),
494}
495
496impl MessageParts {
497    /// A user message of `content`, or [`ContentError::Shape`] when it has
498    /// no parts.
499    ///
500    /// [`ContentError::Shape`]: content::parts::ContentError::Shape
501    pub fn user(content: Vec<UserContent>) -> Result<Self, content::parts::ContentError> {
502        if content.is_empty() {
503            return Err(content::parts::ContentError::Shape);
504        }
505        Ok(Self::User { content })
506    }
507
508    /// The assistant message `message`, or [`ContentError::Shape`] when it
509    /// has no parts.
510    ///
511    /// [`ContentError::Shape`]: content::parts::ContentError::Shape
512    pub fn assistant(message: AssistantMessage) -> Result<Self, content::parts::ContentError> {
513        if message.content.is_empty() {
514            return Err(content::parts::ContentError::Shape);
515        }
516        Ok(Self::Assistant(message))
517    }
518
519    /// The message, verbatim.
520    pub fn to_message(&self) -> Message {
521        match self {
522            Self::User { content } => Message::User {
523                content: content.clone(),
524            },
525            Self::Assistant(message) => Message::Assistant(message.clone()),
526        }
527    }
528
529    /// From a message; a system message is not an utterance (it is the
530    /// preamble) and is refused.
531    pub fn from_message(message: &Message) -> Option<Self> {
532        match message {
533            Message::System { .. } => None,
534            Message::User { content } => Some(Self::User {
535                content: content.clone(),
536            }),
537            Message::Assistant(message) => Some(Self::Assistant(message.clone())),
538        }
539    }
540
541    /// The role.
542    pub fn role(&self) -> Role {
543        match self {
544            Self::User { .. } => Role::User,
545            Self::Assistant { .. } => Role::Assistant,
546        }
547    }
548}
549
550/// Ordered user content opening a run. Hosts must attach it before [`Ready`].
551/// Opening the run consumes this component and appends its utterance after history;
552/// a checkpoint saved before opening retains the prompt.
553#[derive(Component, Debug, Clone, PartialEq, Serialize, Deserialize, Reflect)]
554#[reflect(Component, opaque, Debug, PartialEq, Serialize, Deserialize)]
555pub struct Prompt(pub Vec<UserContent>);
556
557/// Marks a run's graph as ready to start. Hosts constructing a run manually must
558/// insert this after history and [`Prompt`]. Runs without it are not assembled.
559/// The marker remains throughout the run and is saved in checkpoints.
560#[derive(
561    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
562)]
563#[reflect(Component)]
564pub struct Ready;
565
566impl From<&str> for Prompt {
567    fn from(text: &str) -> Self {
568        Self(vec![UserContent::text(text)])
569    }
570}
571
572impl From<String> for Prompt {
573    fn from(text: String) -> Self {
574        Self(vec![UserContent::text(text)])
575    }
576}
577
578impl From<UserContent> for Prompt {
579    fn from(part: UserContent) -> Self {
580        Self(vec![part])
581    }
582}
583
584impl From<Vec<UserContent>> for Prompt {
585    fn from(parts: Vec<UserContent>) -> Self {
586        Self(parts)
587    }
588}
589
590/// One prompt processed to an answer or failure, with default cursor, retry
591/// counters, output-tool name, usage, and streaming mode.
592/// On addition, a missing [`RunSeq`] is assigned from [`RunCounter`] when present,
593/// along with an owner-qualified [`crate::bus::Scope`].
594#[derive(
595    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
596)]
597#[require(
598    Cursor,
599    OutputRetries,
600    InvalidRetries,
601    ProviderRetried,
602    OutputToolName,
603    Usage,
604    StreamRequested
605)]
606#[component(on_add = stamp_run)]
607#[reflect(Component)]
608pub struct Run;
609
610fn stamp_run(
611    mut world: bevy_ecs::world::DeferredWorld<'_>,
612    context: bevy_ecs::lifecycle::HookContext,
613) {
614    let entity = context.entity;
615    // A world without the counter (a checkpoint's scratch world) stamps
616    // nothing: the loaded run carries its own `RunSeq`.
617    if world.get::<RunSeq>(entity).is_none()
618        && let Some(mut counter) = world.get_resource_mut::<RunCounter>()
619    {
620        let seq = counter.0;
621        counter.0 += 1;
622        let owner = world
623            .get::<RunOf>(entity)
624            .and_then(|run_of| world.get::<Owner>(run_of.0))
625            .map(|owner| owner.0.clone())
626            .unwrap_or_default();
627        world
628            .commands()
629            .entity(entity)
630            .insert((RunSeq(seq), crate::bus::Scope(format!("{owner}/run#{seq}"))));
631    }
632}
633
634/// The run's agent.
635#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
636#[relationship(relationship_target = Runs)]
637#[reflect(Component)]
638pub struct RunOf(pub Entity);
639
640/// The agent's runs.
641#[derive(Component, Debug, Default, Reflect)]
642#[relationship_target(relationship = RunOf)]
643#[reflect(Component)]
644pub struct Runs(Vec<Entity>);
645
646impl Runs {
647    /// The run entities.
648    pub fn runs(&self) -> &[Entity] {
649        &self.0
650    }
651}
652
653/// The run's place among the world's runs: the order `Assemble` visits
654/// them, so effects are minted in a stable order. Stamped at spawn from
655/// [`RunCounter`].
656#[derive(
657    Component,
658    Debug,
659    Clone,
660    Copy,
661    Default,
662    PartialEq,
663    Eq,
664    PartialOrd,
665    Ord,
666    Serialize,
667    Deserialize,
668    Reflect,
669)]
670#[reflect(Component)]
671pub struct RunSeq(pub u64);
672
673/// The world's one run counter.
674#[derive(Resource, Debug, Default, Reflect)]
675#[reflect(Resource)]
676pub struct RunCounter(pub(crate) u64);
677
678/// Whether the model is asked for a stream.
679#[derive(
680    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
681)]
682#[reflect(Component)]
683pub struct StreamRequested(pub bool);
684
685/// Where the run is: the turn it is on.
686#[derive(
687    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
688)]
689#[reflect(Component)]
690pub struct Cursor {
691    /// Turns begun so far (the next turn's index).
692    pub turn: usize,
693}
694
695/// Where a run is between its opening and its ending, one component,
696/// replaced by insert (immutable: `On<Insert, RunPhase>` sees every
697/// change). A `Ready` run without one has not opened; a run with an ending
698/// ([`Settled`], [`Failed`]) has none.
699#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
700#[component(immutable)]
701#[serde(rename_all = "snake_case")]
702#[reflect(Component)]
703pub enum RunPhase {
704    /// The run's memory load is out; its first turn waits for it.
705    LoadingMemory,
706    /// The run wants a turn: `Advance` spawns one and `Assemble` folds it.
707    Assembling,
708    /// The run's current turn has an effect in flight.
709    AwaitingModel,
710    /// The run's current turn has its tool batch out: one effect per call,
711    /// `ChildOf` the turn; the run goes on when every one has landed.
712    ResolvingTools,
713}
714
715/// A turn whose batch is out: how many calls it holds. Removed when the
716/// batch lands and the results are history.
717#[derive(
718    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
719)]
720#[reflect(Component)]
721pub struct Batch {
722    /// Calls in the batch.
723    pub calls: usize,
724}
725
726/// Which of the turn's calls a tool effect entity is: what the result is
727/// shaped with. On the effect entity, beside the bus module's components.
728#[derive(Component, Debug, Clone, PartialEq, Serialize, Deserialize, Reflect)]
729#[reflect(Component)]
730pub struct ToolCallSlot {
731    /// The call's index among the turn's calls, the result's order.
732    pub index: usize,
733    /// The call's id, as the model gave it.
734    #[reflect(remote = crate::agent::reflect::ToolCallIdReflect)]
735    pub id: CallId,
736    /// The tool's name, as dispatched (a repaired call carries its repair).
737    #[reflect(remote = crate::agent::reflect::ToolNameReflect)]
738    pub name: ToolName,
739}
740
741/// The run ended with an answer.
742#[derive(
743    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
744)]
745#[reflect(Component)]
746pub struct Settled;
747
748/// The run ended without one.
749#[derive(Component, Debug, Clone, PartialEq, Serialize, Deserialize, Reflect)]
750#[reflect(Component)]
751pub struct Failed(pub Failure);
752
753/// Why a run failed.
754#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Reflect)]
755#[serde(tag = "failure", rename_all = "snake_case")]
756#[reflect(opaque, Debug, PartialEq, Serialize, Deserialize)]
757pub enum Failure {
758    /// The run's content graph or binary source is invalid.
759    Content(content::parts::ContentError),
760    /// The model-call budget ran out.
761    MaxTurns {
762        /// The budget.
763        limit: usize,
764    },
765    /// The model called a tool the program does not advertise, and nothing
766    /// resolved it.
767    UnknownToolCall {
768        /// The tool's name.
769        name: String,
770    },
771    /// The completion failed.
772    Provider(ErrorReport),
773    /// The run was cancelled: the effect in flight was despawned or its
774    /// stream dropped.
775    Cancelled(ErrorReport),
776    /// The run needs what a later stage brings (a tool dispatch, a
777    /// resolution kind); named, never silent.
778    Unsupported(String),
779    /// A granted tool conflicts with the reserved or already minted output name.
780    OutputToolCollision {
781        /// The name.
782        name: String,
783    },
784    /// A tool dispatch failed at the bus or replay boundary. The run fails with
785    /// this report instead of sending a tool-execution error to the model.
786    Tool(ErrorReport),
787    /// The conversation could not be loaded: the run fails at the memory
788    /// record, before any completion.
789    Memory(ErrorReport),
790}
791
792/// The run's answer.
793#[derive(Component, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Reflect)]
794#[reflect(Component)]
795pub struct RunResult(pub String);
796
797/// The run's token usage, summed over its completions.
798#[derive(Component, Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, Reflect)]
799#[reflect(Component)]
800pub struct Usage(#[reflect(remote = crate::agent::reflect::UsageReflect)] pub WireUsage);
801
802/// Output-tool reprompts spent.
803#[derive(
804    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
805)]
806#[reflect(Component)]
807pub struct OutputRetries(pub usize);
808
809/// Invalid-call retries spent.
810#[derive(
811    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
812)]
813#[reflect(Component)]
814pub struct InvalidRetries(pub usize);
815
816/// Provider retries spent ([`ProviderRetries`]).
817#[derive(
818    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
819)]
820#[reflect(Component)]
821pub struct ProviderRetried(pub usize);
822
823/// The run's next turn re-issues the completion its last turn lost to a
824/// retryable provider failure: `Advance` spawns it without counting it
825/// against `MaxTurns`, then removes this.
826#[derive(
827    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
828)]
829#[reflect(Component)]
830pub struct ProviderRetrying;
831
832/// The name the run's output tool was minted under, once a turn minted it.
833#[derive(Component, Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Reflect)]
834#[reflect(Component)]
835pub struct OutputToolName(pub Option<String>);
836
837/// A turn: one model call of a run, `ChildOf` the run, in sibling (`Children`) order.
838#[derive(
839    Component, Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Reflect,
840)]
841#[reflect(Component)]
842pub struct Turn;
843
844/// An advert: a link entity, `ChildOf` a turn, naming one tool the turn's
845/// request advertised, in sibling (`Children`) order.
846#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
847#[relationship(relationship_target = AdvertisedOn)]
848#[reflect(Component)]
849pub struct Advert(pub Entity);
850
851/// The adverts naming this tool.
852#[derive(Component, Debug, Default, Reflect)]
853#[relationship_target(relationship = Advert)]
854#[reflect(Component)]
855pub struct AdvertisedOn(Vec<Entity>);
856
857/// The turn's folded assistant content, as it lands (per tick for a
858/// stream).
859#[derive(Component, Debug, Clone, Default, PartialEq, Serialize, Deserialize, Reflect)]
860#[reflect(Component)]
861pub struct Outputs {
862    /// The parts so far.
863    #[reflect(remote = crate::agent::reflect::AssistantContentsReflect)]
864    pub content: Vec<AssistantContent>,
865    /// The answer's origin, stop and provider message.
866    pub head: content::parts::AssistantHead,
867    /// Whether the answer is complete.
868    pub done: bool,
869    /// Whether this turn's completed provider usage has entered the run total.
870    /// Invalid-call decisions may precede completion and survive its arrival.
871    pub usage_recorded: bool,
872    /// Number of delivered stream events already checked for invalid names.
873    pub stream_validated: usize,
874}
875
876/// A stop, written by any system at any moment: the run ends
877/// `Failed(Cancelled)` with this reason, its effects never issued are
878/// despawned (no record), the ones in flight left to their handler
879/// (CONTRACT §9.1). Serde: a checkpoint saved between the write and the
880/// read restores the decision.
881#[derive(Component, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Reflect)]
882#[reflect(Component)]
883pub struct Cancelled(pub String);
884
885/// A retry of a complete, tool-free turn, written on the turn before
886/// `Materialise` reads it (CONTRACT §9.4): with feedback, the turn and the
887/// feedback become history and another turn begins; without, nothing
888/// becomes history and another turn begins.
889#[derive(Component, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Reflect)]
890#[reflect(Component)]
891pub struct Retry {
892    /// What the model is told, if anything.
893    pub feedback: Option<String>,
894}
895
896/// A per-turn patch of the request, written on the fresh turn before
897/// `Assemble` folds it in (CONTRACT §9.3): what a completion-call hook
898/// changed about one model call, as data. For entity-targeted edits, attach ordered
899/// [`content::parts::RequestPartEdit`] links to this same fresh turn. These cannot
900/// be combined with replacement `history`. Two systems patching one turn
901/// [`merge`](Self::merge) in schedule order.
902#[derive(Component, Debug, Clone, Default, PartialEq, Serialize, Deserialize, Reflect)]
903#[reflect(opaque, Component, Debug, PartialEq, Serialize, Deserialize)]
904pub struct RequestPatch {
905    /// The preamble the system message is built from, instead of the
906    /// agent's.
907    pub preamble: Option<String>,
908    /// The sampling temperature for the turn.
909    pub temperature: Option<f64>,
910    /// The token budget for the turn.
911    pub max_tokens: Option<u64>,
912    /// The tool choice for the turn.
913    pub tool_choice: Option<ToolChoice>,
914    /// The tools advertised, narrowed to these names.
915    pub active_tools: Option<Vec<String>>,
916    /// Provider parameters for the turn (an object merges over the
917    /// agent's, later keys winning).
918    pub additional_params: Option<serde_json::Value>,
919    /// Documents appended after the turn's attachments.
920    pub extra_context: Vec<rig_core::completion::Document>,
921    /// The utterances sent instead of the run's (the prompt stays).
922    pub history: Option<Vec<MessageParts>>,
923}
924
925impl RequestPatch {
926    /// `later` merged over this patch: `extra_context` appends, an object
927    /// `additional_params` shallow-merges with later keys winning,
928    /// `active_tools` intersect, every other field takes the later value
929    /// when set.
930    #[must_use = "the merged patch is the returned value"]
931    pub fn merge(mut self, later: Self) -> Self {
932        self.extra_context.extend(later.extra_context);
933        self.additional_params = match (self.additional_params.take(), later.additional_params) {
934            (Some(base), Some(patch)) if base.is_object() && patch.is_object() => {
935                Some(rig_core::json_utils::merge(base, patch))
936            }
937            (base, patch) => patch.or(base),
938        };
939        self.preamble = later.preamble.or(self.preamble);
940        self.temperature = later.temperature.or(self.temperature);
941        self.max_tokens = later.max_tokens.or(self.max_tokens);
942        self.tool_choice = later.tool_choice.or(self.tool_choice);
943        self.history = later.history.or(self.history);
944        self.active_tools = match (self.active_tools.take(), later.active_tools) {
945            (Some(earlier), Some(later)) => Some(
946                earlier
947                    .into_iter()
948                    .filter(|name| later.contains(name))
949                    .collect(),
950            ),
951            (earlier, later) => earlier.or(later),
952        };
953        self
954    }
955}
956
957/// A reprompt the next turn carries as its last user message: the output
958/// tool was not called, or was called without a required field.
959#[derive(Component, Debug, Clone, PartialEq, Serialize, Deserialize, Reflect)]
960#[reflect(Component)]
961pub struct Reprompt(#[reflect(remote = crate::agent::reflect::MessageReflect)] pub Message);
962
963/// A tool call the program does not advertise, `ChildOf` the turn that
964/// made it, awaiting a [`Resolution`].
965#[derive(Component, Debug, Clone, PartialEq, Serialize, Deserialize, Reflect)]
966#[reflect(Component)]
967pub struct InvalidCall {
968    /// The completion-local correlation identity of the rejected call.
969    #[reflect(remote = crate::agent::reflect::ToolCallIdReflect)]
970    pub id: CallId,
971    /// The tool's name.
972    pub name: String,
973    /// The arguments, verbatim.
974    #[reflect(remote = crate::agent::reflect::JsonReflect)]
975    pub arguments: serde_json::Value,
976    /// The actual delivered assistant prefix at early stream validation.
977    /// Empty for calls discovered from an already completed turn.
978    #[serde(default, skip_serializing_if = "Vec::is_empty")]
979    #[reflect(remote = crate::agent::reflect::AssistantContentsReflect)]
980    pub prefix: Vec<AssistantContent>,
981    /// Position of the early name event in the completion's delivered events.
982    /// This distinguishes reused block identifiers within one stream.
983    #[serde(default, skip_serializing_if = "Option::is_none")]
984    pub stream_offset: Option<usize>,
985    /// Who the stream the prefix came from is from. The prefix keeps no
986    /// provider item, so the rolled-back turn replays canonically. `None`
987    /// for calls discovered from an already completed turn.
988    #[serde(default, skip_serializing_if = "Option::is_none")]
989    #[reflect(remote = crate::agent::content::reflect::OriginReflect)]
990    pub origin: Option<rig_core::message::Origin>,
991}
992
993/// What to do with an invalid call. Written by a user system before
994/// `Materialise` consumes it, else by the default-policy system from the
995/// run's [`InvalidCalls`].
996#[derive(Component, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Reflect)]
997#[serde(tag = "resolution", rename_all = "snake_case")]
998#[reflect(Component)]
999pub enum Resolution {
1000    /// The run fails with `UnknownToolCall`.
1001    Fail,
1002    /// The call is dropped from the turn; what is left goes on.
1003    Ignore,
1004    /// Ask the model again: the turn and a tool result carrying `feedback`
1005    /// for the call (and the invalid-peer notice for every other call of
1006    /// the turn) become history, nothing is dispatched, and another turn
1007    /// begins while `InvalidCalls.retries` remain; otherwise the run fails
1008    /// `UnknownToolCall`.
1009    Retry {
1010        /// What the model is told.
1011        feedback: String,
1012    },
1013    /// Rename the call to a granted tool and dispatch it as such.
1014    Repair {
1015        /// The tool's name.
1016        to: String,
1017    },
1018    /// Answer the call with `reason` as its result, dispatch nothing of
1019    /// the turn (every other call gets the invalid-peer notice), and go
1020    /// on; refused under `tool_choice: none`.
1021    Skip {
1022        /// What the model is told.
1023        reason: String,
1024    },
1025}
1026
1027// Every state component is serde and entity-free: relationships are the
1028// only holders of an `Entity`, and a checkpoint remaps them.
1029const _: () = {
1030    const fn assert_serde<T: Serialize + serde::de::DeserializeOwned>() {}
1031    assert_serde::<Owner>();
1032    assert_serde::<Preamble>();
1033    assert_serde::<Output>();
1034    assert_serde::<Failed>();
1035    assert_serde::<Outputs>();
1036    assert_serde::<InvalidCall>();
1037    assert_serde::<Resolution>();
1038    assert_serde::<ToolPolicy>();
1039    assert_serde::<ToolContextSpec>();
1040    assert_serde::<Batch>();
1041    assert_serde::<ToolCallSlot>();
1042    assert_serde::<Cancelled>();
1043    assert_serde::<Retry>();
1044    assert_serde::<RequestPatch>();
1045    assert_serde::<Conversation>();
1046    assert_serde::<Retrieval>();
1047};
1048
1049/// Clone a run and its linked graph on the same agent with a new run sequence
1050/// and scope, omitting effect identity and answer components.
1051/// Call only between turns, with no turn awaiting an effect; the clone cannot
1052/// resume excluded dispatch state. Panics if [`RunCounter`] or `run` is absent.
1053pub fn fork(world: &mut World, run: Entity) -> Entity {
1054    let seq = {
1055        let mut counter = world.resource_mut::<RunCounter>();
1056        let seq = counter.0;
1057        counter.0 += 1;
1058        seq
1059    };
1060    let owner = world
1061        .get::<RunOf>(run)
1062        .and_then(|run_of| world.get::<Owner>(run_of.0))
1063        .map(|owner| owner.0.clone())
1064        .unwrap_or_default();
1065    // Cross-sibling relationship hooks cannot run while linked cloning has
1066    // only reserved (not yet spawned) their target entities. Restore these
1067    // edges after the clone queue has completed, using its actual entity map.
1068    use bevy_ecs::entity::{EntityCloner, EntityHashMap};
1069    use checkpoint::{AssistantForTurns, ResultsForTurns, TurnAssistant, TurnResults};
1070    let links: Vec<_> = world
1071        .query::<(
1072            Entity,
1073            &ChildOf,
1074            Option<&TurnAssistant>,
1075            Option<&TurnResults>,
1076        )>()
1077        .iter(world)
1078        .filter(|(_, parent, _, _)| parent.parent() == run)
1079        .map(|(turn, _, assistant, results)| (turn, assistant.copied(), results.copied()))
1080        .collect();
1081    let clone = world.spawn_empty().id();
1082    let mut mapped = EntityHashMap::default();
1083    mapped.insert(run, clone);
1084    let mut cloner = EntityCloner::build_opt_out(world);
1085    cloner.linked_cloning(true).deny::<(
1086        crate::bus::PendingEffect,
1087        crate::bus::Seq,
1088        crate::bus::Issued,
1089        crate::bus::Reserved,
1090        crate::bus::EffectOutcome,
1091        crate::bus::ToolInputs,
1092        crate::bus::ToolOutputs,
1093        ToolCallSlot,
1094        TurnAssistant,
1095        AssistantForTurns,
1096        TurnResults,
1097        ResultsForTurns,
1098    )>();
1099    cloner.finish().clone_entity_mapped(world, run, &mut mapped);
1100    world.flush();
1101    for (turn, assistant, results) in links {
1102        if let Some(turn) = mapped.get(&turn).copied() {
1103            if let Some(TurnAssistant(target)) = assistant {
1104                let target = mapped.get(&target).copied().unwrap_or(target);
1105                world.entity_mut(turn).insert(TurnAssistant(target));
1106            }
1107            if let Some(TurnResults(target)) = results {
1108                let target = mapped.get(&target).copied().unwrap_or(target);
1109                world.entity_mut(turn).insert(TurnResults(target));
1110            }
1111        }
1112    }
1113    world
1114        .entity_mut(clone)
1115        .insert((RunSeq(seq), crate::bus::Scope(format!("{owner}/run#{seq}"))));
1116    clone
1117}
1118
1119#[cfg(test)]
1120mod tests;