Skip to main content

rig_ecs/
agent.rs

1//! The agent entity: its components, the calls it owns, and the requests a
2//! view sends it.
3
4use std::ops::{Deref, DerefMut};
5
6use bevy_ecs::prelude::*;
7use bevy_reflect::prelude::*;
8use rig_core::completion::{Message, Usage};
9use rig_core::conversation::{self, MessageId, short_id};
10use rig_core::effect::EffectId;
11use rig_memory::Anchor;
12use serde::{Deserialize, Serialize};
13
14use super::model::ReasoningSetting;
15use super::session_log::{ReflectSaved, save_discarded};
16use super::stage::moved;
17use super::turn::{NO_MODEL, Recovery, start_turn};
18
19/// Marks an agent. Spawning it adds every per-agent component with its
20/// default, including a fresh [`AgentId`]. An agent has no
21/// [`ModelChoice`](super::model::ModelChoice) until one is picked.
22#[derive(Component, Reflect, Default)]
23#[reflect(Component)]
24#[require(
25    AgentId,
26    Conversation,
27    ReasoningSetting,
28    LastUsage,
29    SystemPrompt,
30    ToolAccess
31)]
32pub struct Agent;
33
34/// The agent's stable id, used in its log's name, effect scopes and logs.
35/// `Entity` ids are not stable across a restart; this one is. It never
36/// changes after spawn.
37#[derive(Component, Reflect, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
38#[component(immutable)]
39#[reflect(Component)]
40#[serde(transparent)]
41pub struct AgentId(pub String);
42
43impl Default for AgentId {
44    fn default() -> Self {
45        Self(uuid::Uuid::new_v4().to_string())
46    }
47}
48
49impl From<AgentId> for String {
50    fn from(id: AgentId) -> Self {
51        id.0
52    }
53}
54
55impl AgentId {
56    /// The first characters of the id, enough to tell agents apart.
57    pub fn short(&self) -> &str {
58        short_id(&self.0)
59    }
60
61    /// The agent among `agents` whose id this is, such as in a
62    /// `Query<(Entity, &AgentId)>`: ids name agents across a restart, where
63    /// entities do not, as in an [`Author`](conversation::Author)'s `from`.
64    pub fn find_in<'a>(
65        &self,
66        agents: impl IntoIterator<Item = (Entity, &'a Self)>,
67    ) -> Option<Entity> {
68        let mut agents = agents.into_iter();
69        agents.find_map(|(agent, id)| (id == self).then_some(agent))
70    }
71}
72
73/// The key the agent's model requests share a provider's prompt cache
74/// under (`prompt_cache_key`); empty for the agent's [`AgentId`]. An agent
75/// forked from another keeps the other's ([`ForkAgent`](super::fork::ForkAgent)),
76/// so the fork's requests read what the source's wrote. Saved with the
77/// session.
78#[derive(Component, Reflect, Clone, Debug, Default, PartialEq, Eq)]
79#[reflect(Component, Saved, Default, Clone, Debug)]
80pub struct CacheKey(pub String);
81
82impl CacheKey {
83    /// The key the requests of the agent `id`, with `key`, are sent under.
84    pub fn of<'a>(key: Option<&'a Self>, id: &'a AgentId) -> &'a str {
85        match key {
86            Some(Self(key)) if !key.is_empty() => key,
87            _ => &id.0,
88        }
89    }
90}
91
92/// The agent that spawned this one, such as the agent whose tool call
93/// started it. Despawning that agent despawns this one, and stopping its
94/// turn ([`StopTurn`]) stops this one's too: work meant to outlive its
95/// parent is spawned without it. The kernel gives it no other meaning: a
96/// restore links the agents again by their logs' headers, and views list
97/// agents by it.
98#[derive(Component, Reflect, Debug)]
99#[reflect(Component)]
100#[relationship(relationship_target = Spawned)]
101pub struct SpawnedBy(pub Entity);
102
103/// The agents this one spawned, in the order it spawned them.
104#[derive(Component, Reflect, Debug)]
105#[reflect(Component)]
106#[relationship_target(relationship = SpawnedBy, linked_spawn)]
107pub struct Spawned(Vec<Entity>);
108
109/// The effect the agent's model calls are recorded under, such as the
110/// tool call that asked it for the work it does now, so the effect log
111/// nests that work under the call. An agent without one records its model
112/// calls at the top level. Not saved: effect ids do not outlive a run.
113#[derive(Component, Reflect, Clone, Copy, Debug, Serialize, Deserialize)]
114#[reflect(opaque, Component, Clone, Debug, Serialize, Deserialize)]
115#[serde(transparent)]
116pub struct EffectParent(pub EffectId);
117
118/// The agent's conversation: rig-core's
119/// [`Conversation`](conversation::Conversation), which it derefs to, so a
120/// plugin reads and changes it with that type's methods. Requests send it as
121/// it is.
122///
123/// Each change waits in the conversation's outbox until the kernel saves it
124/// and announces it as a
125/// [`ConversationChanged`](super::session_log::ConversationChanged). A value
126/// inserted in place of the agent's own is saved as one reset, after the
127/// changes the old one had not saved. Saved through serde.
128///
129/// ```
130/// use rig_ecs::prelude::*;
131///
132/// // `/undo`: the user's last message and everything after it go.
133/// fn undo(mut agents: Query<&mut Conversation, With<Agent>>) {
134///     for mut conversation in &mut agents {
135///         let last = conversation.entries().rev().find(|entry| entry.is_users_own());
136///         if let Some(last) = last.map(|entry| entry.index) {
137///             conversation.splice(last.., [], Author::plugin("undo"));
138///         }
139///     }
140/// }
141/// # let _ = undo;
142/// ```
143#[derive(Component, Reflect, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
144#[component(on_discard = save_discarded)]
145#[reflect(opaque, Component, Default, Clone, Serialize, Deserialize)]
146#[serde(transparent)]
147pub struct Conversation(pub conversation::Conversation);
148
149impl Deref for Conversation {
150    type Target = conversation::Conversation;
151
152    fn deref(&self) -> &Self::Target {
153        &self.0
154    }
155}
156
157impl DerefMut for Conversation {
158    fn deref_mut(&mut self) -> &mut Self::Target {
159        &mut self.0
160    }
161}
162
163impl From<conversation::Conversation> for Conversation {
164    fn from(conversation: conversation::Conversation) -> Self {
165        Self(conversation)
166    }
167}
168
169/// The usage of the agent's last model reply that reported its tokens,
170/// with the last entry of the conversation it counts (rig-memory's
171/// [`Anchor`]), `None` before one did. Only the kernel writes it, from
172/// what the provider reported. Saved with the session.
173#[derive(Component, Reflect, Clone, Copy, Debug, Default, Serialize, Deserialize)]
174#[reflect(opaque, Component, Saved, Default, Clone, Serialize, Deserialize)]
175#[serde(transparent)]
176pub struct LastUsage(pub Option<Anchor>);
177
178impl LastUsage {
179    /// What the provider reported for the last reply.
180    pub fn usage(&self) -> Option<&Usage> {
181        self.0.as_ref().map(|anchor| &anchor.usage)
182    }
183
184    /// The tokens the last reply read and wrote, which the next request
185    /// sends again, when it reported them.
186    pub fn context(&self) -> Option<u64> {
187        self.usage()?.context_tokens()
188    }
189}
190
191/// The agent's own part of its system prompt: who it is and how it works.
192/// Each request's prompt adds the app's
193/// [`PromptSection`](super::prompt::PromptSection)s and the agent's own.
194/// Empty, the agent has
195/// none of its own, and the app's plugins may give it one in `Prepare`,
196/// as rig-basics gives rig's. Saved with the session, as `null` when empty,
197/// so a restored agent without its own gets what the build that restores
198/// it gives.
199#[derive(Component, Reflect, Clone, Default, Serialize, Deserialize)]
200#[reflect(opaque, Component, Saved, Default, Clone, Serialize, Deserialize)]
201#[serde(from = "Option<String>", into = "Option<String>")]
202pub struct SystemPrompt(pub String);
203
204impl From<SystemPrompt> for Option<String> {
205    fn from(prompt: SystemPrompt) -> Self {
206        (!prompt.0.is_empty()).then_some(prompt.0)
207    }
208}
209
210impl From<Option<String>> for SystemPrompt {
211    fn from(prompt: Option<String>) -> Self {
212        Self(prompt.unwrap_or_default())
213    }
214}
215
216/// Which registered tools the agent may call. Saved with the session.
217#[derive(Component, Reflect, Clone, Default, Serialize, Deserialize)]
218#[reflect(opaque, Component, Saved, Default, Clone, Serialize, Deserialize)]
219pub enum ToolAccess {
220    /// Every registered tool.
221    #[default]
222    All,
223    /// Only the tools named.
224    Only(Vec<String>),
225}
226
227impl ToolAccess {
228    /// Whether the tool `name` may be called.
229    pub fn allows(&self, name: &str) -> bool {
230        match self {
231            Self::All => true,
232            Self::Only(names) => names.iter().any(|allowed| allowed == name),
233        }
234    }
235}
236
237/// A running turn of the agent it names, from a user message to the reply
238/// that ends it; at most one per agent. It ends as its [`TurnOutcome`] says:
239/// Commit then removes this, so the agent is idle, and marks the turn
240/// [`Settled`](super::stage::Settled). The turn stays [`ForAgent`] its agent,
241/// and goes with it. Its retries are counted in a [`Recovery`]; its `Name` is
242/// `turn` unless spawned with another.
243///
244/// [`ForAgent`]: super::stage::ForAgent
245#[derive(Component, Reflect, Debug)]
246#[reflect(Component)]
247#[relationship(relationship_target = ActiveTurn)]
248#[require(Recovery, Turn, Name = Name::new("turn"))]
249pub struct TurnOf(pub Entity);
250
251/// Where a turn stands, on the turn. The kernel fills it in when the turn
252/// is spawned, from the turn's record in its agent's conversation
253/// ([`TurnRecord`](conversation::TurnRecord)), and keeps it.
254#[derive(Component, Reflect, Clone, Debug, Default)]
255#[component(on_add = start_turn)]
256#[reflect(Component, Default)]
257pub struct Turn {
258    /// How many turns its agent started before this one, over the whole
259    /// session.
260    pub index: u64,
261    /// Where the turn begins in its agent's [`Conversation`]: the id of
262    /// the user's message it answers, or, when the conversation does not
263    /// end in one, of the first message the turn adds. Its index now is
264    /// [`conversation::Conversation::start_of`] it, as splices move messages.
265    /// The entries it answers are
266    /// [`conversation::Conversation::asked`], the message that asked for it
267    /// [`conversation::Conversation::request`].
268    /// Not reflected: rig-core's types are not `Reflect`.
269    #[reflect(ignore)]
270    pub start: MessageId,
271    /// The model call whose reply's tool calls run now, their effects'
272    /// parent, until their results are committed. Other calls of the turn
273    /// keep it open but are never committed as tool results.
274    #[reflect(ignore)]
275    pub reply: Option<EffectId>,
276}
277
278/// The agent's running turn. An agent with it is busy.
279#[derive(Component, Reflect, Debug)]
280#[reflect(Component)]
281#[relationship_target(relationship = TurnOf, linked_spawn)]
282pub struct ActiveTurn(Entity);
283
284impl ActiveTurn {
285    /// The turn entity.
286    pub fn turn(&self) -> Entity {
287        self.0
288    }
289}
290
291/// A model or tool call of the turn it names. Despawning the turn despawns
292/// its calls, which cancels their tasks.
293#[derive(Component, Reflect, Debug)]
294#[reflect(Component)]
295#[relationship(relationship_target = Calls)]
296pub struct CallOf(pub Entity);
297
298/// The calls of a turn, in the order they were spawned: the tool calls of
299/// a reply in the reply's order.
300#[derive(Component, Reflect, Debug)]
301#[reflect(Component)]
302#[relationship_target(relationship = CallOf, linked_spawn)]
303pub struct Calls(Vec<Entity>);
304
305/// Text streamed so far by an in-flight model call, for views. A plugin
306/// that continues a reply in a successor call may start the successor's
307/// `Partial` with the text so far: views show one growing reply, and a
308/// stop saves what the user saw.
309#[derive(Component, Reflect, Default)]
310#[reflect(Component, Default)]
311pub struct Partial {
312    /// Answer text.
313    pub text: String,
314    /// Reasoning text.
315    pub reasoning: String,
316}
317
318/// Send the agent's conversation to its model again as it stands: after a
319/// turn that failed on a transient error and kept the user's message, or
320/// one that stopped before the model answered the last tool results.
321#[derive(EntityEvent, Reflect, Clone, Debug)]
322#[reflect(Event, Clone, Debug)]
323pub struct Retry {
324    /// The agent.
325    pub entity: Entity,
326}
327
328/// Stop the agent's running turn at once, and the running turns of every
329/// agent below it in [`Spawned`], even when the agent itself is idle:
330/// stopping follows ancestry, so work meant to outlive its parent is
331/// spawned without [`SpawnedBy`]. Each turn ends [`TurnOutcome::Stopped`]
332/// with `reason`, the text its reply streamed so far is kept, every call of
333/// it that has not settled is answered as stopped (rig-core's
334/// [`not_finished`](rig_core::tool::feedback::not_finished)), and whatever
335/// held them is despawned. It cannot be reviewed or held.
336#[derive(EntityEvent, Reflect, Clone, Debug)]
337#[reflect(Event, Clone, Debug)]
338pub struct StopTurn {
339    /// The agent.
340    pub entity: Entity,
341    /// Why, when it is not the user who stops it.
342    pub reason: Option<String>,
343}
344
345impl StopTurn {
346    /// The user stopping `agent`'s turn.
347    pub fn new(agent: Entity) -> Self {
348        Self {
349            entity: agent,
350            reason: None,
351        }
352    }
353}
354
355/// How a [`Notice`] is shown and logged.
356#[derive(Reflect, Clone, Copy, Debug, Default, PartialEq, Eq)]
357pub enum NoticeLevel {
358    /// Information.
359    #[default]
360    Info,
361    /// Something went wrong that did not stop anything, such as a refused
362    /// key with another one used instead: shown like an error, but never a
363    /// failure.
364    Warn,
365    /// Something failed or was refused.
366    Error,
367}
368
369/// A line for the user, shown by views. A system reads the notices sent
370/// before it in the frame and, from the frame before, those sent after it.
371/// So one sent in `Last`, such as the session log's write failure (in
372/// [`WriteLogs`](crate::WriteLogs)), is read a frame late and, on the exit
373/// frame, only by a system ordered after its sender.
374#[derive(Message, Clone, Debug)]
375pub struct Notice {
376    /// The agent it is about, or `None` when it is about the whole app.
377    pub agent: Option<Entity>,
378    /// The text.
379    pub text: String,
380    /// Whether it informs, warns or reports a failure.
381    pub level: NoticeLevel,
382}
383
384impl Notice {
385    fn new(agent: Option<Entity>, text: String, level: NoticeLevel) -> Self {
386        Self { agent, text, level }
387    }
388
389    /// Information about `agent`, or about the whole app with `None`.
390    pub fn info(agent: impl Into<Option<Entity>>, text: impl Into<String>) -> Self {
391        Self::new(agent.into(), text.into(), NoticeLevel::Info)
392    }
393
394    /// A warning about `agent`, or about the whole app with `None`:
395    /// something went wrong, but nothing failed.
396    pub fn warn(agent: impl Into<Option<Entity>>, text: impl Into<String>) -> Self {
397        Self::new(agent.into(), text.into(), NoticeLevel::Warn)
398    }
399
400    /// A failure or refusal concerning `agent`, or the whole app with
401    /// `None`.
402    pub fn error(agent: impl Into<Option<Entity>>, text: impl Into<String>) -> Self {
403        Self::new(agent.into(), text.into(), NoticeLevel::Error)
404    }
405
406    /// The refusal of what would change `agent`, or the whole app with
407    /// `None`, while a turn runs: the rest of the turn would go on with
408    /// something it did not start with.
409    pub fn turn_running(agent: impl Into<Option<Entity>>) -> Self {
410        Self::info(agent, "A turn is running; stop it first.")
411    }
412
413    /// The refusal of what needs a model when `agent` has none.
414    pub fn no_model(agent: Entity) -> Self {
415        Self::info(agent, NO_MODEL)
416    }
417}
418
419/// How a turn ends, on the turn.
420///
421/// - **Proposed:** the kernel proposes `Answered` or `Failed` in Commit.
422/// - **Reviewed:** in the next pass Review may edit it in place
423///   (`Query<&mut TurnOutcome, With<TurnOf>>`) or hold it
424///   ([`HoldOf`](super::stage::HoldOf) the turn). A `Queue` or `Steer`
425///   message the agent accepts takes an `Answered` proposal away, and its
426///   next [`TurnCall`](super::turn::TurnCall), once admitted, takes any;
427///   the holds on a proposal taken away are despawned with it.
428/// - **Stopped:** a plugin may insert `Failed` or `Stopped` at any time with
429///   a plain `insert`, so no pending proposal loses it. The turn's calls not
430///   started never start, the others are answered as stopped, and the turn
431///   ends once they settled.
432///
433/// A turn that ended is [`Settled`](super::stage::Settled) with it until the
434/// next frame: `On<Add<Settled>>` or `Added<Settled>` readers of
435/// `(&TurnOutcome, &ForAgent)` see each turn's last outcome once.
436#[derive(Component, Clone, Debug)]
437#[component(on_insert = moved::<TurnOutcome>, on_remove = moved::<TurnOutcome>)]
438pub enum TurnOutcome {
439    /// The model answered: its last message, which ended the turn.
440    Answered(Message),
441    /// The turn failed, for the reason given.
442    Failed(String),
443    /// The turn was stopped before an answer: by the user when no reason
444    /// is given, or on exit.
445    Stopped(Option<String>),
446}
447
448impl TurnOutcome {
449    /// Why the calls of a turn that ends this way did not finish, when it
450    /// stops them: `Failed` and `Stopped` do, `Answered` does not. A tool
451    /// call reads it through rig-core's
452    /// [`not_finished`](rig_core::tool::feedback::not_finished).
453    pub(crate) fn stops(&self) -> Option<String> {
454        match self {
455            Self::Answered(_) => None,
456            Self::Failed(why) => Some(format!("the turn failed: {why}")),
457            Self::Stopped(None) => Some("the user stopped the turn".to_owned()),
458            Self::Stopped(Some(why)) => Some(why.clone()),
459        }
460    }
461}