Skip to main content

rig_ecs/
inbox.rs

1//! Messages to agents. A message, whoever sends it (the user typing, an
2//! agent reporting, a plugin), is an effect entity: an [`Incoming`]
3//! [`ForAgent`] the agent, spawned by [`Deliver`] or by hand, which plugins
4//! edit in Prepare and take over in Gate by answering it. Each carries its
5//! [`Author`], kept with the conversation and its log; text that is not
6//! the user's own goes to the model headed by a line naming its sender.
7//! Each message is its own entry: nothing merges into the message before.
8//!
9//! At Start the kernel accepts what is left, in the order it was sent: an
10//! idle agent takes a message into its conversation and starts a turn with
11//! it, unless it is a [`DeliveryMode::Inform`]; a busy agent keeps it until
12//! the turn's next call ([`DeliveryMode::Steer`]) or its end
13//! ([`DeliveryMode::Queue`]). A user's `/` line ([`Incoming::command`]) is
14//! text to the kernel: an app that has slash commands, such as
15//! rig-harness, takes it over in Gate and runs it.
16//! A message delivered into a turn gets [`DeliveredIn`] that turn. An
17//! agent's inbox is the messages it accepted that are not answered yet.
18//!
19//! What a turn ends without sending is [`Unsent`]: a plugin keeps it or
20//! takes it over, else the kernel hands what the user typed back to the
21//! views as [`Recalled`] and keeps what agents and plugins sent for the
22//! next turn. The inbox is saved with the conversation and restored after
23//! a restart. The plugin guide (rig-harness's `PLUGINS.md`, "What the user
24//! types") has the full rules.
25
26use std::collections::{HashMap, HashSet};
27use std::sync::Arc;
28
29use bevy_ecs::lifecycle::HookContext;
30use bevy_ecs::prelude::*;
31use bevy_ecs::system::SystemParam;
32use bevy_ecs::world::DeferredWorld;
33use bevy_reflect::prelude::*;
34use rig_core::catalog::ModelSpec;
35use rig_core::completion::Message;
36use rig_core::conversation::{Aside, AsideId, Author, AuthorKind, Entry, Halt};
37use rig_core::effect::{EffectKind, HandlerKey};
38use rig_core::error::{ErrorKind, ErrorReport};
39use rig_core::message::UserContent;
40use serde::{Deserialize, Serialize};
41use web_time::SystemTime;
42
43use super::agent::{
44    ActiveTurn, Agent, CallOf, Conversation, EffectParent, Notice, TurnOf, TurnOutcome,
45};
46use super::model::Connection;
47#[cfg(doc)]
48use super::stage::Stage;
49use super::stage::{
50    AgentWork, Decision, Effect, ForAgent, Holds, Outcome, ReadyToStart, Settled, Started,
51    SupersededBy, moved,
52};
53use super::tasks::Wake;
54use super::turn::{Exiting, TurnCall, spawn_turn_call, take_outcome};
55
56/// When a message goes to the agent's model, and whether it asks for an
57/// answer. An idle agent starts a turn with a [`Steer`](Self::Steer) or
58/// [`Queue`](Self::Queue) message either way.
59#[derive(Reflect, Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum DeliveryMode {
62    /// With the running turn's next model call.
63    #[default]
64    Steer,
65    /// Once the running turn would end, as its next step: everything
66    /// queued meanwhile goes in that one step.
67    Queue,
68    /// Needs no answer, such as a report that only points at another: it
69    /// goes with the agent's next model call, whenever one is made, and
70    /// never makes one. An idle agent keeps it in its conversation for
71    /// the next turn.
72    #[serde(rename = "note")]
73    Inform,
74}
75
76/// Content read by a message's sender, such as an image file the user
77/// named. An image goes only to a model that reads images; another model
78/// gets the rest of the message, and the user is told.
79#[derive(Reflect, Clone, Debug, Serialize, Deserialize)]
80#[reflect(opaque, Clone, Debug)]
81pub struct Attachment {
82    /// How the user knows it, such as its path.
83    pub label: String,
84    /// The content.
85    pub content: UserContent,
86}
87
88/// One piece of a message ([`Incoming::parts`]).
89#[derive(Reflect, Clone, Debug, Serialize, Deserialize)]
90#[reflect(Clone, Debug)]
91#[serde(rename_all = "snake_case")]
92pub enum MessagePart {
93    /// Text.
94    Text(String),
95    /// Content the sender read, such as an image.
96    Attachment(Attachment),
97}
98
99/// A message to the agent it is [`ForAgent`]: its `parts`, text and
100/// attachments in the order its author put them, by `author`, that go to
101/// the agent's model as `mode` says. An effect that moves through the
102/// stages (see the [module](self)): edit it in Prepare, take it over in
103/// Gate by deciding it ([`Decision`]) with an
104/// [`Outcome`]; once accepted, it is [`Started`] until the kernel answers
105/// it. A message with no text and no attachment is ignored.
106///
107/// ```
108/// use rig_ecs::prelude::*;
109/// use rig_ecs::prelude::message::{ImageMediaType, UserContent};
110///
111/// let mut message = Incoming::new("What is in this picture?", DeliveryMode::Steer);
112/// let image = UserContent::image_base64("iVBORw0KGgo=", Some(ImageMediaType::PNG), None);
113/// let picture = Attachment { label: "cat.png".into(), content: image };
114/// message.parts.push(MessagePart::Attachment(picture)); // after the text
115/// message.text_mut().push_str(" Be brief.");
116/// assert_eq!(message.text(), "What is in this picture? Be brief.");
117/// ```
118///
119/// Spawning `(Incoming, ForAgent(agent))`, with any components of the
120/// sender's own, sends it: it is recorded under the effect the agent's
121/// work is recorded under ([`EffectParent`]), unless it names a parent,
122/// and the loop is woken. [`Deliver`] does the same for a sender that
123/// needs nothing else. Its serde form is what its agent's conversation
124/// saves of it while it is in the inbox.
125#[derive(Component, Reflect, Clone, Debug, Serialize, Deserialize)]
126#[component(on_add = sent)]
127#[reflect(Component, Clone, Debug)]
128#[require(Effect, Name = Name::new("message"))]
129pub struct Incoming {
130    /// What it says, in its author's order.
131    pub parts: Vec<MessagePart>,
132    /// Who wrote it.
133    #[reflect(ignore)]
134    pub author: Author,
135    /// When it goes to a busy agent's model.
136    pub mode: DeliveryMode,
137    /// Whether the user's text goes to the model as it is even when it
138    /// starts with `/`, such as a refused command sent again.
139    pub literal: bool,
140}
141
142impl Incoming {
143    /// `text`, as `mode` says, as the user's own text unless
144    /// [`with_author`](Self::with_author) names who wrote it.
145    pub fn new(text: impl Into<String>, mode: DeliveryMode) -> Self {
146        Self {
147            parts: vec![MessagePart::Text(text.into())],
148            author: Author::user(),
149            mode,
150            literal: false,
151        }
152    }
153
154    /// Its first text part, empty when it has none.
155    pub fn text(&self) -> &str {
156        texts(&self.parts).next().unwrap_or_default()
157    }
158
159    /// Its first text part, added after the other parts when it has none.
160    pub fn text_mut(&mut self) -> &mut String {
161        let first = self
162            .parts
163            .iter()
164            .position(|part| matches!(part, MessagePart::Text(_)));
165        let (at, part) = match first {
166            Some(at) => (at, self.parts.remove(at)),
167            None => (self.parts.len(), MessagePart::Text(String::new())),
168        };
169        match self.parts.insert_mut(at, part) {
170            MessagePart::Text(text) => text,
171            // Never: the part put back is a text.
172            MessagePart::Attachment(attachment) => &mut attachment.label,
173        }
174    }
175
176    /// Whether it has no text and no attachment, which the kernel ignores.
177    fn is_empty(&self) -> bool {
178        self.parts.iter().all(|part| match part {
179            MessagePart::Text(text) => text.trim().is_empty(),
180            MessagePart::Attachment(_) => false,
181        })
182    }
183
184    /// This message, by `author`.
185    #[must_use]
186    pub fn with_author(self, author: Author) -> Self {
187        Self { author, ..self }
188    }
189
190    /// This message sent [`literal`](Self::literal)ly.
191    #[must_use]
192    pub fn literal(self) -> Self {
193        Self {
194            literal: true,
195            ..self
196        }
197    }
198
199    /// The slash command line, without its `/`, that an app with slash
200    /// commands runs instead of sending this message: the user's text that
201    /// starts with `/`, unless it is [`literal`](Self::literal).
202    pub fn command(&self) -> Option<&str> {
203        let user = self.author.kind == AuthorKind::User && !self.literal;
204        user.then(|| self.text().trim_start().strip_prefix('/'))
205            .flatten()
206    }
207}
208
209/// Send `message` to the agent `entity`: the kernel spawns it as an
210/// [`Incoming`] effect [`ForAgent`] that agent, and the stages take it
211/// from there (see the [module](self)). A plugin changes or takes over a
212/// message through its `Incoming`, not this.
213#[derive(EntityEvent, Reflect, Clone, Debug)]
214#[reflect(Event, Clone, Debug)]
215pub struct Deliver {
216    /// The agent.
217    pub entity: Entity,
218    /// The message.
219    pub message: Incoming,
220}
221
222impl Deliver {
223    /// `text` for `agent`, as `mode` says, as the user's own text unless
224    /// [`with_author`](Self::with_author) names who wrote it.
225    pub fn new(agent: Entity, text: impl Into<String>, mode: DeliveryMode) -> Self {
226        Self {
227            entity: agent,
228            message: Incoming::new(text, mode),
229        }
230    }
231
232    /// This message, by `author`.
233    #[must_use]
234    pub fn with_author(self, author: Author) -> Self {
235        Self {
236            message: self.message.with_author(author),
237            ..self
238        }
239    }
240
241    /// This message sent [`literal`](Incoming::literal)ly.
242    #[must_use]
243    pub fn literal(self) -> Self {
244        Self {
245            message: self.message.literal(),
246            ..self
247        }
248    }
249}
250
251/// Text the user typed that was not sent: the messages the agent's turn
252/// ended without sending, joined in the order they were typed, or a
253/// refused slash command, with `why`. A view puts it back in its input.
254#[derive(Message, Clone, Debug)]
255pub struct Recalled {
256    /// The agent.
257    pub agent: Entity,
258    /// The text.
259    pub text: String,
260    /// Why a command was refused.
261    pub why: Option<String>,
262}
263
264/// On a message ([`Incoming`]) the kernel delivered into a turn: the turn whose
265/// model call it went with, the turn it started or the running turn it steered
266/// or carried on. Inserted when the message goes into the conversation, with
267/// its answer. It is no hold: a turn's end does not wait for it. An
268/// `Inform` message to an idle agent, and what a turn that ended some other way
269/// puts in the conversation, go with no turn and have none.
270#[derive(Component, Reflect, Clone, Copy, Debug)]
271#[reflect(Component, Clone, Debug)]
272pub struct DeliveredIn(pub Entity);
273
274/// On a message its agent accepted and had not sent when the turn it
275/// waited for ended, however it ended, or that a restart brought back to
276/// an idle agent. In Gate a plugin keeps it in the inbox by removing this,
277/// so it goes with the agent's next model call as its mode says, or takes
278/// it over by deciding it. Otherwise, once every plugin had its say
279/// ([`GateSystems::Fallback`](super::stage::GateSystems::Fallback)), the
280/// kernel hands the user's back to the views as one [`Recalled`] and puts
281/// the others in the conversation for the next turn. Not on exit: the
282/// inbox stays saved for the restart.
283#[derive(Component, Clone, Debug)]
284#[component(on_insert = moved::<Unsent>)]
285pub struct Unsent {
286    /// How the turn ended; `None` after a restart.
287    pub ended: Option<TurnOutcome>,
288}
289
290/// The kind of the aside that keeps a message in its agent's inbox, its
291/// value the message, saved with the conversation until it is answered.
292const INBOX_MARK: &str = "rig.inbox";
293
294/// On a message in its agent's inbox: its `rig.inbox` aside.
295#[derive(Component, Clone, Copy, Debug)]
296pub(crate) struct InboxMark(AsideId);
297
298/// Marks `message` kept in the inbox of the agent whose `conversation` it
299/// is, after the last entry.
300fn mark(conversation: &mut Conversation, message: &Incoming) -> InboxMark {
301    let after = conversation.entries().next_back().map(|entry| entry.id);
302    let value = serde_json::to_value(message).unwrap_or_default();
303    let aside = Aside::new(INBOX_MARK, Author::rig(), value).after(after);
304    InboxMark(conversation.add_aside(aside))
305}
306
307/// The messages kept in the inbox of `conversation`, with their marks.
308pub(crate) fn kept(conversation: &Conversation) -> Vec<(InboxMark, Incoming)> {
309    let marks = conversation
310        .asides()
311        .filter(|(_, aside)| aside.kind == INBOX_MARK);
312    let read = |aside: &Aside| Incoming::deserialize(&aside.value).ok();
313    marks
314        .filter_map(|(id, aside)| Some((InboxMark(id), read(aside)?)))
315        .collect()
316}
317
318/// Takes its mark off a message in the inbox once it is answered.
319pub(crate) fn unmark(
320    answered: On<Insert<Outcome>>,
321    marked: Query<(&InboxMark, &ForAgent)>,
322    mut agents: Query<&mut Conversation>,
323) {
324    if let Ok((&InboxMark(id), &ForAgent(agent))) = marked.get(answered.entity)
325        && let Ok(mut conversation) = agents.get_mut(agent)
326    {
327        conversation.remove_aside(id);
328    }
329}
330
331/// Spawns a [`Deliver`]'s message for its agent.
332pub(crate) fn on_deliver(
333    deliver: On<Deliver>,
334    agents: Query<(), With<Agent>>,
335    mut commands: Commands,
336) {
337    if agents.contains(deliver.entity) {
338        commands.spawn((deliver.message.clone(), ForAgent(deliver.entity)));
339    }
340}
341
342/// Puts a new message under the effect its agent's work is recorded
343/// under, unless it names a parent, and wakes the loop: the frame that
344/// sent it may have run its stages already.
345fn sent(mut world: DeferredWorld, context: HookContext) {
346    let entity = context.entity;
347    let parent = world
348        .get::<ForAgent>(entity)
349        .and_then(|agent| world.get::<EffectParent>(agent.0))
350        .map(|parent| parent.0);
351    if let Some(parent) = parent
352        && let Some(mut effect) = world.get_mut::<Effect>(entity)
353        && effect.parent.is_none()
354    {
355        effect.parent = Some(parent);
356    }
357    if let Some(wake) = world.get_resource::<Wake>() {
358        wake.wake();
359    }
360}
361
362/// Accepts each message about to start, in the order they were sent, unless a
363/// Gate system failed this pass, in [`Stage::Start`]: gives an idle agent the
364/// message at once and starts a turn with it, with every message accepted with
365/// it, unless it is an `Inform` message, which is kept for the next turn; and
366/// keeps it, [`Started`], for a busy agent.
367pub(crate) fn accept(
368    ready: Query<(Entity, &Effect, &Incoming, Option<&ForAgent>), ReadyToStart>,
369    mut agents: Query<(&mut Conversation, Option<&Connection>, Has<ActiveTurn>), With<Agent>>,
370    mut commands: Commands,
371    mut notices: MessageWriter<Notice>,
372) {
373    let mut ready: Vec<_> = ready.iter().collect();
374    ready.sort_unstable_by_key(|(_, effect, ..)| effect.id());
375    let at = SystemTime::now();
376    // The agents whose turn starts with this pass's messages, and the turn.
377    let mut starting = HashMap::new();
378    for (entity, effect, message, agent) in ready {
379        let agent = agent.map(|agent| agent.0);
380        let Some((agent, (mut conversation, connection, busy))) =
381            agent.and_then(|agent| Some((agent, agents.get_mut(agent).ok()?)))
382        else {
383            let why = ErrorReport::new(ErrorKind::Internal, "a message needs an agent");
384            commands.entity(entity).try_insert_if_new(Outcome(Err(why)));
385            continue;
386        };
387        if message.is_empty() {
388            commands
389                .entity(entity)
390                .try_insert_if_new(Outcome::custom("empty"));
391            continue;
392        }
393        let joins = starting.get(&agent).copied();
394        if busy && joins.is_none() {
395            let mark = mark(&mut conversation, message);
396            commands.entity(entity).try_insert((Started { at }, mark));
397            continue;
398        }
399        let spec = connection.map(|connection| &*connection.spec);
400        // An `Inform` message to an idle agent waits for the next turn.
401        let turn = match joins {
402            None if message.mode == DeliveryMode::Inform => None,
403            None => Some(commands.spawn(TurnOf(agent)).id()),
404            joins => joins,
405        };
406        commands.entity(entity).try_insert(Started { at });
407        // After a failure that kept the user's message, the new text joins it.
408        let conversation = &mut *conversation;
409        let into = (turn, &mut commands);
410        deliver(
411            agent,
412            spec,
413            [(entity, message)],
414            conversation,
415            &mut notices,
416            into,
417        );
418        match (turn, joins) {
419            (None, _) => {
420                conversation.halt(Halt::Kept);
421            }
422            (Some(turn), None) => {
423                starting.insert(agent, turn);
424                // The turn's first call is the message's work.
425                commands.spawn((
426                    TurnCall,
427                    CallOf(turn),
428                    ForAgent(agent),
429                    Effect::with_parent_id(Some(effect.id())),
430                ));
431            }
432            (Some(_), Some(_)) => {}
433        }
434    }
435}
436
437/// Carries on each turn proposed [`TurnOutcome::Answered`] whose agent
438/// accepted a Steer or Queue message this pass, in [`Stage::Start`]: the
439/// outcome is taken off, what carries the turn on goes into the
440/// conversation, and the model is called again.
441pub(crate) fn carry_on_accepted(
442    accepted: Query<(&Incoming, &ForAgent), (Added<Started>, Without<Outcome>)>,
443    mut agents: Query<(&mut Conversation, Option<&Connection>, &ActiveTurn)>,
444    turns: Query<(&TurnOutcome, Option<&Holds>)>,
445    inboxes: Inboxes,
446    mut commands: Commands,
447    mut notices: MessageWriter<Notice>,
448) {
449    let mut carried = HashSet::new();
450    for (message, &ForAgent(agent)) in &accepted {
451        if message.mode == DeliveryMode::Inform || !carried.insert(agent) {
452            continue;
453        }
454        let Ok((mut conversation, connection, active)) = agents.get_mut(agent) else {
455            continue;
456        };
457        let turn = active.turn();
458        let Ok((TurnOutcome::Answered(_), holds)) = turns.get(turn) else {
459            continue;
460        };
461        let Some(next) = carry_on(inboxes.of(agent)) else {
462            continue;
463        };
464        let spec = connection.map(|connection| &*connection.spec);
465        let conversation = &mut *conversation;
466        let into = (Some(turn), &mut commands);
467        deliver(agent, spec, next, conversation, &mut notices, into);
468        take_outcome(&mut commands, turn, holds);
469        spawn_turn_call(&mut commands, turn, agent);
470    }
471}
472
473/// The messages agents accepted and were not given yet, as the kernel
474/// reads them. A message decided is out the moment it is, though its
475/// decision becomes its [`Outcome`] only at Start.
476#[derive(SystemParam)]
477pub(crate) struct Inboxes<'w, 's> {
478    work: Query<'w, 's, &'static AgentWork>,
479    accepted: Query<
480        'w,
481        's,
482        (
483            Entity,
484            &'static Effect,
485            &'static Incoming,
486            &'static Decision,
487        ),
488        (With<Started>, Without<Outcome>, Without<Settled>),
489    >,
490    /// Not a message that settled, or that another superseded: its
491    /// successor is on its way in its place.
492    on_the_way: Query<
493        'w,
494        's,
495        (&'static Incoming, &'static Decision),
496        (
497            Without<Started>,
498            Without<Outcome>,
499            Without<Settled>,
500            Without<SupersededBy>,
501        ),
502    >,
503}
504
505impl Inboxes<'_, '_> {
506    /// What `agent` accepted and was not given yet, oldest first.
507    pub(crate) fn of(&self, agent: Entity) -> Vec<(Entity, &Incoming)> {
508        let work = self.work.get(agent).into_iter().flat_map(AgentWork::iter);
509        let accepted = self.accepted.iter_many(work).flatten();
510        let mut waiting: Vec<_> = accepted
511            .filter(|(.., decision)| !decision.is_decided())
512            .collect();
513        waiting.sort_unstable_by_key(|(_, effect, ..)| effect.id());
514        waiting
515            .into_iter()
516            .map(|(entity, _, message, _)| (entity, message))
517            .collect()
518    }
519
520    /// Whether a Steer or Queue message to `agent` is on its way: spawned,
521    /// not accepted, not decided.
522    pub(crate) fn coming(&self, agent: Entity) -> bool {
523        let work = self.work.get(agent).into_iter().flat_map(AgentWork::iter);
524        self.on_the_way
525            .iter_many(work)
526            .flatten()
527            .any(|(message, decision)| {
528                message.mode != DeliveryMode::Inform && !decision.is_decided()
529            })
530    }
531}
532
533/// What of `waiting` carries a turn on that would end: the Inform messages
534/// with the steering messages, else with everything queued, in the order they came;
535/// none when neither waits.
536pub(crate) fn carry_on(waiting: Vec<(Entity, &Incoming)>) -> Option<Vec<(Entity, &Incoming)>> {
537    let steering = waiting
538        .iter()
539        .any(|(_, message)| message.mode == DeliveryMode::Steer);
540    let carries = match steering {
541        true => DeliveryMode::Steer,
542        false => DeliveryMode::Queue,
543    };
544    let next: Vec<_> = waiting
545        .into_iter()
546        .filter(|(_, message)| {
547            matches!(message.mode, DeliveryMode::Inform) || message.mode == carries
548        })
549        .collect();
550    next.iter()
551        .any(|(_, message)| message.mode == carries)
552        .then_some(next)
553}
554
555/// Marks what the agent of a turn that just ended accepted and did not
556/// send [`Unsent`], unless the app is [`Exiting`].
557pub(crate) fn unsent_on_turn_end(
558    end: On<Remove<TurnOf>>,
559    turns: Query<(&TurnOf, Option<&TurnOutcome>)>,
560    inboxes: Inboxes,
561    exiting: Option<Res<Exiting>>,
562    mut commands: Commands,
563) {
564    let Ok((&TurnOf(agent), ended)) = turns.get(end.entity) else {
565        return;
566    };
567    if exiting.is_some() {
568        return;
569    }
570    for (message, _) in inboxes.of(agent) {
571        let ended = ended.cloned();
572        commands.entity(message).try_insert(Unsent { ended });
573    }
574}
575
576/// Settles each [`Unsent`] message nothing kept, holds or decided, in
577/// [`GateSystems::Fallback`](super::stage::GateSystems::Fallback): an
578/// idle agent hands the user's back to the views, joined in one
579/// [`Recalled`] in the order they were sent, and puts the others in its
580/// conversation, left halted for the next turn; a busy agent keeps them
581/// for its running turn.
582pub(crate) fn settle_unsent(
583    unsent: Query<
584        (Entity, &Effect, &Incoming, &ForAgent, &Decision),
585        (With<Unsent>, Without<Outcome>, Without<Holds>),
586    >,
587    mut agents: Query<(&mut Conversation, Option<&Connection>, Has<ActiveTurn>)>,
588    mut commands: Commands,
589    mut recalled: MessageWriter<Recalled>,
590    mut notices: MessageWriter<Notice>,
591) {
592    let mut unsent: Vec<_> = unsent.iter().collect();
593    unsent.sort_unstable_by_key(|(_, effect, ..)| effect.id());
594    let mut typed: HashMap<Entity, Vec<&str>> = HashMap::new();
595    for (entity, _, message, &ForAgent(agent), decision) in unsent {
596        let Ok((mut conversation, connection, busy)) = agents.get_mut(agent) else {
597            continue;
598        };
599        if decision.is_decided() {
600            continue;
601        }
602        if busy {
603            commands.entity(entity).try_remove::<Unsent>();
604        } else if message.author.kind == AuthorKind::User {
605            commands
606                .entity(entity)
607                .try_insert_if_new(Outcome::custom("recalled"));
608            typed.entry(agent).or_default().push(message.text().trim());
609        } else {
610            let spec = connection.map(|connection| &*connection.spec);
611            let conversation = &mut *conversation;
612            let into = (None, &mut commands);
613            deliver(
614                agent,
615                spec,
616                [(entity, message)],
617                conversation,
618                &mut notices,
619                into,
620            );
621            conversation.halt(Halt::Kept);
622        }
623    }
624    for (agent, typed) in typed {
625        recalled.write(Recalled {
626            agent,
627            text: typed.join("\n\n"),
628            why: None,
629        });
630    }
631}
632
633/// The payload of a message's effect record (custom kind `message`): its
634/// joined text, author and mode, without its attachments.
635#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
636pub struct MessageRecord {
637    /// The text.
638    pub text: String,
639    /// Who wrote it.
640    pub author: Author,
641    /// When it went to a busy agent's model.
642    pub mode: DeliveryMode,
643}
644
645impl MessageRecord {
646    /// The kind of a message's record.
647    pub const KIND: &str = "message";
648
649    /// The message a record of `kind` is about, if it is a message's.
650    pub fn read(kind: &EffectKind) -> Option<Self> {
651        match kind {
652            EffectKind::Custom { kind, payload } if &**kind == Self::KIND => {
653                serde_json::from_value(payload.clone()).ok()
654            }
655            _ => None,
656        }
657    }
658}
659
660/// The record's handler key and kind of a message.
661pub(crate) fn message_kind(message: &Incoming) -> (HandlerKey, EffectKind) {
662    let record = MessageRecord {
663        text: texts(&message.parts).collect::<Vec<_>>().join("\n"),
664        author: message.author.clone(),
665        mode: message.mode,
666    };
667    let kind = EffectKind::Custom {
668        kind: Arc::from(MessageRecord::KIND),
669        payload: serde_json::to_value(record).unwrap_or_default(),
670    };
671    (HandlerKey::new(MessageRecord::KIND), kind)
672}
673
674/// Puts each of `messages`, accepted by `agent`, in its `conversation`
675/// as [`pending`] does, and answers it as delivered, [`DeliveredIn`] the
676/// turn `into` names, if any.
677pub(crate) fn deliver<'a>(
678    agent: Entity,
679    spec: Option<&ModelSpec>,
680    messages: impl IntoIterator<Item = (Entity, &'a Incoming)>,
681    conversation: &mut Conversation,
682    notices: &mut MessageWriter<Notice>,
683    (into, commands): (Option<Entity>, &mut Commands),
684) {
685    for (entity, message) in messages {
686        push_messages(agent, spec, [message], conversation, notices);
687        let mut delivered = commands.entity(entity);
688        delivered.try_insert_if_new(Outcome::custom("delivered"));
689        if let Some(turn) = into {
690            delivered.try_insert(DeliveredIn(turn));
691        }
692    }
693}
694
695/// The text parts of `parts`, in order.
696fn texts(parts: &[MessagePart]) -> impl Iterator<Item = &str> {
697    parts.iter().filter_map(|part| match part {
698        MessagePart::Text(text) => Some(text.as_str()),
699        MessagePart::Attachment(_) => None,
700    })
701}
702
703/// Pushes each of `messages` to `conversation`, that of `agent`, whose
704/// model is `spec`, as a user message of its own by its author: its parts
705/// in order, each attachment the model takes and each text trimmed, the
706/// first text headed by its author's line when it is not the user's own.
707/// The author is kept, and logged, unless it is the plain user, so a
708/// user's message with a title keeps it.
709pub(crate) fn push_messages<'a>(
710    agent: Entity,
711    spec: Option<&ModelSpec>,
712    messages: impl IntoIterator<Item = &'a Incoming>,
713    conversation: &mut Conversation,
714    notices: &mut MessageWriter<Notice>,
715) {
716    for message in messages {
717        let mut header = message.author.header();
718        let mut content = Vec::with_capacity(message.parts.len());
719        let mut refused = Vec::new();
720        for part in &message.parts {
721            let attachment = match part {
722                MessagePart::Text(text) => {
723                    let text = text.trim();
724                    let text = match header.take() {
725                        Some(header) if text.is_empty() => header,
726                        Some(header) => format!("{header}\n{text}"),
727                        None => text.to_owned(),
728                    };
729                    if !text.is_empty() {
730                        content.push(UserContent::text(text));
731                    }
732                    continue;
733                }
734                MessagePart::Attachment(attachment) => attachment,
735            };
736            let Attachment {
737                label,
738                content: item,
739            } = attachment;
740            let why = match (item, spec) {
741                (UserContent::Image(_), Some(spec)) if !spec.input.image => Some(format!(
742                    "{} does not read images, so {label} is sent as its name only.",
743                    spec.display_name
744                )),
745                (UserContent::Image(_), None) => Some(format!(
746                    "No model is connected, so {label} is sent as its name only."
747                )),
748                _ => None,
749            };
750            match why {
751                Some(note) => {
752                    notices.write(Notice::info(agent, note));
753                    refused.push(label.as_str());
754                }
755                None => content.push(item.clone()),
756            }
757        }
758        // A message of attachments alone keeps its header first, and the
759        // names of those the model cannot read when nothing else is left.
760        if content.is_empty() {
761            content.extend(refused.into_iter().map(UserContent::text));
762        }
763        if let Some(header) = header {
764            content.insert(0, UserContent::text(header));
765        }
766        let entry = Entry::new(Message::User { content });
767        let entry = match message.author == Author::user() {
768            true => entry,
769            false => entry.by(message.author.clone()),
770        };
771        conversation.push(entry);
772    }
773}
774
775#[cfg(test)]
776mod tests;