use std::borrow::Cow;
use std::fmt;
use std::mem;
use std::pin::Pin;
use std::sync::Arc;
use std::time::{Duration, UNIX_EPOCH};
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::prelude::*;
use bevy_ecs::query::QueryData;
use bevy_ecs::system::SystemParam;
use bevy_ecs::world::DeferredWorld;
use bevy_log::{info, warn};
use bevy_reflect::prelude::*;
use bevy_tasks::futures::check_ready;
use bevy_tasks::{IoTaskPool, TaskPool};
use bevy_time::DelayedCommandsExt;
use crossbeam_channel::{Receiver, Sender};
use futures::StreamExt;
use rig_core::completion::message::turn_failure;
use rig_core::completion::{
self, CompletionRequest, CompletionResponse, GenerationOptions, Message, Section,
UnsupportedOption,
};
use rig_core::conversation::pairing::close_pending_with;
use rig_core::conversation::{Author, Halt, TurnEnd};
use rig_core::effect::{self, EffectId, EffectKind};
use rig_core::error::{ErrorKind, ErrorReport};
use rig_core::message::{AssistantContent, AssistantMessage, ToolCall, UserContent};
use rig_core::now_ms;
use rig_core::serve::{Reply, stream_truncated};
use rig_core::streaming::{Item, Relayed, StreamEvent};
use rig_core::tool::{ToolErrorKind, ToolExecutionError, ToolResult, feedback};
use rig_memory::Anchor;
use web_time::Instant;
use super::agent::{
ActiveTurn, Agent, AgentId, CacheKey, CallOf, Calls, Conversation, EffectParent, LastUsage,
Notice, Partial, Retry, Spawned, StopTurn, SystemPrompt, ToolAccess, Turn, TurnOf, TurnOutcome,
};
use super::inbox::{DeliveryMode, Inboxes, carry_on, deliver};
use super::model::{Connection, ReasoningSetting};
use super::prompt::{PromptSection, SectionOf};
use super::restore::{Restored, commit_results, mark_settled};
use super::retry::{RetryPolicy, Verdict};
use super::session_log::{ConversationChanged, SessionLog, save_conversation};
#[cfg(doc)]
use super::stage::Stage;
use super::stage::{
Admitted, Effect, ForAgent, HoldOf, Holds, IfStopped, Outcome, Settled, Started, SupersededBy,
Waiting, cancel, release,
};
use super::tasks::{Running, Wake};
use super::tools::{Footprint, ToolDef, ToolRequest};
pub(crate) const NO_MODEL: &str = "No model is connected; pick one first.";
pub const RETRY_POLICY: RetryPolicy = RetryPolicy::DEFAULT;
#[derive(Component, Reflect, Clone, Copy, Debug, Default)]
#[reflect(Component, Default)]
pub struct Recovery {
pub retries: u32,
}
#[derive(Component, Reflect, Clone, Debug)]
#[reflect(Component, from_reflect = false)]
pub struct Backoff {
pub attempt: u32,
#[reflect(ignore)]
pub until: Instant,
}
impl Backoff {
pub fn seconds_left(&self) -> u64 {
let left = self.until.saturating_duration_since(Instant::now());
left.as_secs() + u64::from(left.subsec_nanos() > 0)
}
}
#[derive(Component, Reflect, Clone)]
#[reflect(opaque, Component, Clone)]
#[require(Effect, Adapters)]
pub struct ModelRequest {
pub request: Arc<CompletionRequest>,
pub connection: Option<Connection>,
pub prompt: Option<completion::SystemPrompt>,
pub history: Option<rig_core::conversation::Conversation>,
}
impl ModelRequest {
pub fn new(request: impl Into<Arc<CompletionRequest>>) -> Self {
Self {
request: request.into(),
connection: None,
prompt: None,
history: None,
}
}
pub fn request_mut(&mut self) -> &mut CompletionRequest {
Arc::make_mut(&mut self.request)
}
#[must_use]
pub fn on(self, connection: Connection) -> Self {
Self {
connection: Some(connection),
..self
}
}
}
#[derive(Component, Clone)]
pub struct SentTo(pub Connection);
#[derive(Component, Clone, Debug, Default)]
pub struct DroppedOptions(pub Vec<UnsupportedOption>);
#[derive(Component, Clone, Default, Debug)]
pub struct Adapters(pub Vec<Adapter>);
impl Adapters {
pub fn apply(&self, connection: Connection) -> Connection {
let placed = |outermost| self.0.iter().filter(move |it| it.outermost == outermost);
placed(false)
.chain(placed(true))
.fold(connection, |connection, adapter| adapter.apply(connection))
}
}
#[derive(Clone)]
pub struct Adapter {
pub name: Cow<'static, str>,
outermost: bool,
apply: Arc<dyn Fn(Connection) -> Connection + Send + Sync>,
}
impl Adapter {
pub fn new(
name: impl Into<Cow<'static, str>>,
apply: impl Fn(Connection) -> Connection + Send + Sync + 'static,
) -> Self {
Self {
name: name.into(),
outermost: false,
apply: Arc::new(apply),
}
}
#[must_use]
pub fn outermost(self) -> Self {
Self {
outermost: true,
..self
}
}
pub fn apply(&self, connection: Connection) -> Connection {
(self.apply)(connection)
}
}
impl fmt::Debug for Adapter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Adapter").field(&self.name).finish()
}
}
#[derive(Component, Reflect, Clone, Copy, Debug, Default)]
#[reflect(Component, Default)]
#[require(Effect, Partial, Name = Name::new("model call"))]
pub struct TurnCall;
#[derive(Component, Reflect, Clone, Copy, Debug)]
#[reflect(Component)]
#[require(Effect)]
pub struct SideCall(pub Entity);
#[derive(Component, Clone, Debug)]
pub struct RetryVerdict(pub Verdict);
#[derive(Component)]
pub(crate) struct Feed(Receiver<Fed>);
pub(crate) struct Fed(web_time::SystemTime, Option<StreamEvent>);
#[derive(Message, Clone, Debug)]
pub struct StreamDelta {
pub effect: Entity,
pub agent: Option<Entity>,
pub event: StreamEvent,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct StreamTiming {
pub first: web_time::SystemTime,
pub last: web_time::SystemTime,
pub items: u64,
}
pub(crate) fn model_pool() -> &'static IoTaskPool {
IoTaskPool::get_or_init(TaskPool::default)
}
pub(crate) fn spawn_turn_call(commands: &mut Commands, turn: Entity, agent: Entity) {
commands.spawn((TurnCall, CallOf(turn), ForAgent(agent)));
}
pub(crate) fn on_retry(
retry: On<Retry>,
mut agents: Query<(&mut Conversation, Has<ActiveTurn>), With<Agent>>,
mut commands: Commands,
mut notices: MessageWriter<Notice>,
) {
let agent = retry.entity;
let Ok((mut conversation, busy)) = agents.get_mut(agent) else {
return;
};
if busy {
notices.write(Notice::turn_running(agent));
return;
}
if !conversation.resume() {
notices.write(Notice::info(
agent,
"Nothing to retry: the model answered the last message.",
));
return;
}
let turn = commands.spawn(TurnOf(agent)).id();
spawn_turn_call(&mut commands, turn, agent);
}
pub(crate) fn start_turn(mut world: DeferredWorld, context: HookContext) {
let Some(&TurnOf(agent)) = world.get::<TurnOf>(context.entity) else {
return;
};
world
.commands()
.entity(context.entity)
.try_insert(ForAgent(agent));
let restored = world.get::<Restored>(context.entity).is_some();
let Some(mut conversation) = world.get_mut::<Conversation>(agent) else {
return;
};
let last = conversation.turns().next_back().map(|(_, record)| record);
let record = match last.filter(|record| restored && record.end.is_none()) {
Some(record) => record,
None => conversation.open_turn(),
};
if let Some(mut turn) = world.get_mut::<Turn>(context.entity) {
turn.index = record.index;
turn.start = record.start;
}
}
fn turn_end(outcome: &TurnOutcome, conversation: &Conversation) -> TurnEnd {
match outcome {
TurnOutcome::Answered(_) => {
let last = conversation.entries().next_back();
let answer = last.filter(|entry| matches!(entry.message, Message::Assistant(_)));
TurnEnd::Answered {
answer: answer.map(|entry| entry.id),
}
}
TurnOutcome::Failed(why) => TurnEnd::Failed { why: why.clone() },
TurnOutcome::Stopped(why) => TurnEnd::Stopped { why: why.clone() },
}
}
#[derive(Component, Clone, Debug)]
pub(crate) struct Ending {
notice: String,
take_out: bool,
}
const STOPPED_BY_USER: &str = "the user stopped the turn";
pub(crate) fn on_stop_turn(
interrupt: On<StopTurn>,
spawned: Query<&Spawned>,
mut agents: Query<(&AgentId, &mut Conversation, &ActiveTurn)>,
mut turns: Query<(&mut Turn, Option<&Calls>)>,
replies: Query<ReplyCall>,
partials: Query<&Partial, (With<TurnCall>, Without<Settled>, Without<SupersededBy>)>,
log: Res<SessionLog>,
mut committed: MessageWriter<ConversationChanged>,
mut commands: Commands,
mut notices: MessageWriter<Notice>,
) {
let root = interrupt.entity;
let reason = interrupt.reason.clone();
let why = reason.clone().unwrap_or_else(|| STOPPED_BY_USER.to_owned());
let below = spawned.iter_descendants::<Spawned>(root);
let stopping: Vec<Entity> = std::iter::once(root).chain(below).collect();
let mut stopped = false;
for agent in stopping {
let Ok((id, mut conversation, active)) = agents.get_mut(agent) else {
continue;
};
let turn = active.turn();
let (reply, calls) = match turns.get_mut(turn) {
Ok((mut state, calls)) => (state.reply.take(), calls),
Err(_) => (None, None),
};
let streamed = partials.iter_many(calls.into_iter().flat_map(Calls::iter));
let (mut reasoning, mut text) = (String::new(), String::new());
for partial in streamed.flatten() {
reasoning.push_str(&partial.reasoning);
text.push_str(&partial.text);
}
let reasoning =
(!reasoning.trim().is_empty()).then(|| AssistantContent::reasoning(reasoning));
let text = (!text.trim().is_empty()).then(|| AssistantContent::text(text));
let content: Vec<_> = reasoning.into_iter().chain(text).collect();
if !content.is_empty() {
let aborted = AssistantMessage::aborted(None, content, format!("stopped: {why}"));
conversation.push(Message::Assistant(aborted));
}
let reply = reply
.zip(calls)
.map(|(reply, calls)| reply_calls(reply, calls, &replies));
let stopped_call =
|call: &ReplyCallItem| Some(not_finished(&why, call.started, call.footprint));
if let Some(results) = reply_results(reply.unwrap_or_default(), stopped_call) {
commit_results(&mut conversation, results);
}
conversation.halt(Halt::Stopped);
conversation.end_turn(TurnEnd::Stopped {
why: reason.clone(),
});
let conversation = conversation.bypass_change_detection();
committed.write_batch(save_conversation(&log, agent, id, conversation));
let calls: Vec<Entity> = calls.into_iter().flat_map(Calls::iter).collect();
let (why, reason) = (why.clone(), reason.clone());
commands.queue(move |world: &mut World| {
if let Ok(mut turn) = world.get_entity_mut(turn) {
turn.insert(TurnOutcome::Stopped(reason));
}
for call in calls {
let said = world.get_entity(call).ok().and_then(|call| {
let footprint = call.get::<Footprint>();
call.contains::<ToolRequest>()
.then(|| not_finished_text(&why, call.contains::<Started>(), footprint))
});
cancel(world, call, said.as_deref().unwrap_or(&why));
}
if let Ok(mut turn) = world.get_entity_mut(turn) {
turn.despawn_related::<Holds>();
}
if let Ok(mut turn) = world.get_entity_mut(turn) {
turn.remove::<(TurnOf, Ending)>().insert(Settled);
}
});
notices.write(Notice::info(agent, "Stopped."));
stopped = true;
}
if stopped {
let _ = log.flush();
}
}
fn not_finished_text(why: &str, started: bool, footprint: Option<&Footprint>) -> String {
let footprint = footprint.cloned().unwrap_or_default();
feedback::not_finished(why, started, &footprint.0)
}
pub(crate) fn not_finished(why: &str, started: bool, footprint: Option<&Footprint>) -> ToolResult {
let text = not_finished_text(why, started, footprint);
ToolResult::failed(ToolExecutionError::other(text))
}
fn reply_results(
calls: Vec<ReplyCallItem>,
missing: impl Fn(&ReplyCallItem) -> Option<ToolResult>,
) -> Option<Message> {
let results: Vec<_> = calls
.into_iter()
.filter_map(|call| {
let outcome = call.outcome.or(call.if_stopped.map(|stopped| &stopped.0));
let result = outcome
.map(Outcome::tool_result)
.or_else(|| missing(&call))?;
Some(call.request.call.answer(&result))
})
.collect();
(!results.is_empty()).then(|| Message::tool_results(results))
}
const EXIT_GRACE: Duration = Duration::from_secs(1);
type Cancelling = Vec<Pin<Box<dyn Future<Output = ()>>>>;
#[derive(Resource, Clone, Copy, Debug, Default)]
pub(crate) struct Exiting;
pub(crate) fn stop_turns_on_exit(world: &mut World) {
world.insert_resource(Exiting);
let mut cancelling = Cancelling::new();
take_running(world, &mut cancelling);
let deadline = Instant::now() + EXIT_GRACE;
loop {
cancelling.retain_mut(|cancel| check_ready(cancel).is_none());
if cancelling.is_empty() || cfg!(target_family = "wasm") || Instant::now() >= deadline {
break;
}
#[cfg(not(target_family = "wasm"))]
std::thread::sleep(Duration::from_millis(2));
}
if let Err(error) = world.run_system_cached(log_answered_results) {
warn!("could not log the results of stopped turns: {error}");
}
let turns: Vec<Entity> = world
.query_filtered::<Entity, With<TurnOf>>()
.iter(world)
.collect();
for turn in turns {
world.despawn(turn);
}
world.flush();
}
fn log_answered_results(
turns: Query<(&TurnOf, &Turn, &Calls)>,
replies: Query<ReplyCall>,
mut agents: Query<&mut Conversation>,
) {
for (&TurnOf(agent), state, calls) in &turns {
let Some(reply) = state.reply else {
continue;
};
let results = reply_results(reply_calls(reply, calls, &replies), |_| None);
if let Ok(mut conversation) = agents.get_mut(agent)
&& let Some(results) = results
{
commit_results(&mut conversation, results);
}
}
}
fn take_running(world: &mut World, cancelling: &mut Cancelling) {
let calls: Vec<Entity> = world
.query_filtered::<Entity, With<Running>>()
.iter(world)
.collect();
for call in calls {
if let Ok(mut call) = world.get_entity_mut(call)
&& let Some(running) = call.take::<Running>()
{
cancelling.push(Box::pin(running.cancel()));
}
}
}
#[derive(QueryData)]
#[query_data(mutable)]
pub(crate) struct Builder {
id: &'static AgentId,
cache: Option<&'static CacheKey>,
connection: Option<&'static Connection>,
effort: &'static ReasoningSetting,
prompt: &'static SystemPrompt,
access: &'static ToolAccess,
conversation: &'static mut Conversation,
parent: Option<&'static EffectParent>,
}
#[derive(QueryData)]
#[query_data(mutable)]
pub(crate) struct Unbuilt {
entity: Entity,
effect: &'static mut Effect,
from: Option<&'static SideCall>,
turn_call: Has<TurnCall>,
of: Option<&'static CallOf>,
agent: Option<&'static ForAgent>,
}
#[derive(SystemParam)]
pub(crate) struct Parts<'w, 's> {
tools: Query<'w, 's, &'static ToolDef>,
sections: Query<'w, 's, (&'static PromptSection, Option<&'static SectionOf>)>,
}
pub(crate) fn build_requests(
mut unbuilt: Query<
Unbuilt,
(
Or<(With<TurnCall>, With<SideCall>)>,
Added<Admitted>,
Without<ModelRequest>,
Without<Outcome>,
Without<Started>,
),
>,
turns: Query<&TurnOf>,
mut agents: Query<Builder>,
inboxes: Inboxes,
parts: Parts,
mut commands: Commands,
mut notices: MessageWriter<Notice>,
) {
for mut call in &mut unbuilt {
let turn = call.of.map(|of| of.0);
let agent = call
.from
.map(|from| from.0)
.or(call.agent.map(|agent| agent.0))
.or_else(|| turns.get(turn?).ok().map(|of| of.0));
let mut built = commands.entity(call.entity);
let Some((agent, mut builder)) =
agent.and_then(|agent| Some((agent, agents.get_mut(agent).ok()?)))
else {
let why = ErrorReport::new(ErrorKind::Internal, "a model call needs an agent");
built.try_insert_if_new(Outcome(Err(why)));
continue;
};
if call.agent.is_none() {
built.try_insert(ForAgent(agent));
}
let effect = &mut *call.effect;
if effect.parent.is_none() {
effect.parent = builder.parent.map(|parent| parent.0);
}
let spec = builder.connection.map(|connection| &*connection.spec);
if call.turn_call {
effect.purpose.get_or_insert("turn".into());
let waiting = inboxes.of(agent).into_iter();
let waiting = waiting.filter(|(_, message)| message.mode != DeliveryMode::Queue);
let conversation = &mut *builder.conversation;
let into = (turn, &mut commands);
deliver(agent, spec, waiting, conversation, &mut notices, into);
}
if call.turn_call && builder.conversation.messages().is_empty() {
let why = ErrorReport::new(ErrorKind::Internal, "The conversation is empty.");
commands
.entity(call.entity)
.try_insert_if_new(Outcome(Err(why)));
continue;
}
let history = builder.conversation.request_view();
let model = build((agent, &builder), spec, &parts, history);
commands.entity(call.entity).try_insert(model);
}
}
fn build(
(agent, builder): (Entity, &BuilderItem),
spec: Option<&rig_core::catalog::ModelSpec>,
parts: &Parts,
history: rig_core::conversation::Conversation,
) -> ModelRequest {
let mut offered: Vec<&ToolDef> = parts
.tools
.iter()
.filter(|def| builder.access.allows(def.0.name.as_str()))
.collect();
offered.sort_by(|a, b| a.0.name.as_str().cmp(b.0.name.as_str()));
let mut sections: Vec<&PromptSection> = parts
.sections
.iter()
.filter(|(_, of)| of.is_none_or(|of| of.0 == agent))
.map(|(section, _)| section)
.collect();
sections.sort_by(|a, b| (a.order, &a.tag).cmp(&(b.order, &b.tag)));
let sections = sections
.into_iter()
.map(|section| Section::new(§ion.tag, §ion.text));
let prompt = completion::SystemPrompt {
role: builder.prompt.0.clone(),
sections: sections.collect(),
};
let effort = builder.effort.0;
let options = spec.map_or_else(
|| {
let options = GenerationOptions::default();
match effort {
Some(reasoning) => options.reasoning(reasoning),
None => options,
}
},
|spec| spec.default_options(effort),
);
let request = CompletionRequest::from(Vec::new())
.tools(offered.iter().map(|def| def.0.clone()))
.options(options.cache_key(CacheKey::of(builder.cache, builder.id)));
ModelRequest {
request: request.into(),
connection: builder.connection.cloned(),
prompt: Some(prompt),
history: Some(history),
}
}
#[derive(SystemParam)]
pub struct ModelStarts<'w, 's> {
agents: Query<'w, 's, &'static Connection, With<Agent>>,
}
impl ModelStarts<'_, '_> {
pub fn sent(
&self,
model: &ModelRequest,
adapters: Option<&Adapters>,
agent: Option<Entity>,
) -> Result<(Connection, CompletionRequest), ErrorReport> {
let (connection, request, _) = self.sending(model, adapters, agent)?;
Ok((connection, request))
}
pub fn dropped(
&self,
model: &ModelRequest,
adapters: Option<&Adapters>,
agent: Option<Entity>,
) -> Result<Vec<UnsupportedOption>, ErrorReport> {
let (connection, request, mut dropped) = self.sending(model, adapters, agent)?;
let kind = EffectKind::Completion {
request: request.into(),
stream: true,
};
dropped.extend(connection.handler.0.check(&kind));
Ok(dropped)
}
pub fn prompt(
&self,
model: &ModelRequest,
adapters: Option<&Adapters>,
agent: Option<Entity>,
) -> Result<(Connection, Option<completion::SystemPrompt>), ErrorReport> {
let connection = self.connection(model.connection.as_ref(), adapters, agent)?;
Ok((connection, model.prompt.clone()))
}
fn sending(
&self,
model: &ModelRequest,
adapters: Option<&Adapters>,
agent: Option<Entity>,
) -> Result<(Connection, CompletionRequest, Vec<UnsupportedOption>), ErrorReport> {
let mut model = model.clone();
let (connection, dropped) = self.start(&mut model, adapters, agent)?;
Ok((connection, Arc::unwrap_or_clone(model.request), dropped))
}
pub(crate) fn start(
&self,
model: &mut ModelRequest,
adapters: Option<&Adapters>,
agent: Option<Entity>,
) -> Result<(Connection, Vec<UnsupportedOption>), ErrorReport> {
let request = Arc::make_mut(&mut model.request);
let to = model.connection.as_ref();
let started = self.fit(request, to, adapters, agent)?;
let history = match model.history.take() {
Some(history) => history.into_messages(),
None => mem::take(&mut request.chat_history),
};
let prompt = model.prompt.take();
let prompt = prompt.map(|prompt| Arc::new(Message::system(prompt.text())));
request.chat_history = prompt.into_iter().chain(history).collect();
Ok(started)
}
fn connection(
&self,
to: Option<&Connection>,
adapters: Option<&Adapters>,
agent: Option<Entity>,
) -> Result<Connection, ErrorReport> {
let own = agent.and_then(|agent| self.agents.get(agent).ok());
let Some(connection) = to.or(own).cloned() else {
return Err(ErrorReport::new(ErrorKind::HandlerUnavailable, NO_MODEL));
};
Ok(match adapters {
Some(adapters) => adapters.apply(connection),
None => connection,
})
}
fn fit(
&self,
request: &mut CompletionRequest,
to: Option<&Connection>,
adapters: Option<&Adapters>,
agent: Option<Entity>,
) -> Result<(Connection, Vec<UnsupportedOption>), ErrorReport> {
let connection = self.connection(to, adapters, agent)?;
let spec = &connection.spec;
if !spec.tools {
request.tools.clear();
}
if request.options.cache.is_none() {
request.options.cache = spec.default_options(request.options.reasoning).cache;
}
let dropped = spec.fit(request);
Ok((connection, dropped))
}
}
pub(crate) async fn stream_reply(
reply: Reply,
feed: Sender<Fed>,
wake: Wake,
) -> Result<effect::Answered, ErrorReport> {
let mut events = reply.into_stream();
while let Some(item) = events.next().await {
let event = match item {
Err(report) => return Err(report),
Ok(Relayed::Origin(_)) => continue,
Ok(Relayed::Done(response)) => {
feed.send(Fed(web_time::SystemTime::now(), None)).ok();
return Ok(effect::Answered::Completion(response));
}
Ok(Relayed::Item(Item::Event(event))) => Some(event),
Ok(Relayed::Item(_)) => None,
};
feed.send(Fed(web_time::SystemTime::now(), event)).ok();
wake.wake();
}
Err(stream_truncated())
}
pub(crate) fn feed() -> (Sender<Fed>, Feed) {
let (sender, receiver) = crossbeam_channel::unbounded();
(sender, Feed(receiver))
}
pub(crate) fn stream_partials(
mut calls: Query<(
Entity,
&Feed,
Option<&mut Partial>,
Option<&mut StreamTiming>,
Option<&ForAgent>,
)>,
mut deltas: MessageWriter<StreamDelta>,
mut commands: Commands,
) {
for (effect, feed, mut partial, mut streamed, agent) in &mut calls {
let mut first: Option<StreamTiming> = None;
for Fed(at, event) in feed.0.try_iter() {
match streamed.as_deref_mut().or(first.as_mut()) {
Some(stamp) => {
stamp.last = at;
stamp.items += 1;
}
None => {
first = Some(StreamTiming {
first: at,
last: at,
items: 1,
});
}
}
let Some(event) = event else {
continue;
};
if let Some(partial) = partial.as_mut() {
match &event {
StreamEvent::Text { text, .. } => partial.text.push_str(text),
StreamEvent::Reasoning { text, .. } => partial.reasoning.push_str(text),
_ => {}
}
}
let agent = agent.map(|agent| agent.0);
deltas.write(StreamDelta {
effect,
agent,
event,
});
}
if let Some(first) = first {
commands.entity(effect).try_insert(first);
}
}
}
pub(crate) fn judge_failures(
failed: Query<
(Entity, &Effect, &Outcome, Option<&CallOf>),
(With<ModelRequest>, Added<Outcome>, Without<Settled>),
>,
turns: Query<&Recovery>,
mut commands: Commands,
) {
for (call, effect, Outcome(outcome), of) in &failed {
let Err(report) = outcome else {
continue;
};
if !effect.ran() {
continue;
}
let retries = of
.and_then(|of| turns.get(of.0).ok())
.map_or(0, |recovery| recovery.retries);
let now = UNIX_EPOCH + Duration::from_millis(now_ms());
let verdict = RETRY_POLICY.verdict(report, retries, now);
commands.entity(call).try_insert(RetryVerdict(verdict));
}
}
pub(crate) fn back_off(
failed: Query<
(Entity, &Effect, &Outcome, &RetryVerdict, &CallOf),
(
With<TurnCall>,
Without<Holds>,
Without<Settled>,
Without<SupersededBy>,
),
>,
mut turns: Query<(&TurnOf, &mut Recovery, Option<&TurnOutcome>)>,
mut commands: Commands,
mut notices: MessageWriter<Notice>,
) {
for (call, effect, Outcome(outcome), RetryVerdict(verdict), &CallOf(turn)) in &failed {
let (Err(report), Verdict::Retry(delay)) = (outcome, verdict) else {
continue;
};
let Ok((&TurnOf(agent), mut recovery, ending)) = turns.get_mut(turn) else {
continue;
};
if ending.and_then(TurnOutcome::stops).is_some() {
continue;
}
recovery.retries += 1;
let backoff = Backoff {
attempt: recovery.retries,
until: Instant::now() + *delay,
};
notices.write(Notice::info(
agent,
format!(
"The model call failed: {report}. Retrying in {}s ({}/{}).",
backoff.seconds_left(),
backoff.attempt,
RETRY_POLICY.max_retries
),
));
let hold = commands
.spawn((Name::new("retry wait"), backoff, HoldOf(call)))
.id();
let parent = effect.parent;
commands
.delayed()
.duration(*delay)
.queue(move |world: &mut World| {
if world.get_entity(hold).is_err() {
return;
}
world.despawn(hold);
world
.spawn((
TurnCall,
CallOf(turn),
ForAgent(agent),
Effect::with_parent_id(parent),
))
.add_related::<SupersededBy>(&[call]);
});
}
}
pub(crate) fn stop_calls(
turns: Query<(&TurnOutcome, &Calls), With<TurnOf>>,
calls: Query<
(
Has<Outcome>,
Option<&Holds>,
Option<&IfStopped>,
(Has<ToolRequest>, Has<Started>, Option<&Footprint>),
),
(With<Admitted>, Without<Settled>),
>,
mut commands: Commands,
) {
for (ending, of) in &turns {
let Some(why) = ending.stops() else {
continue;
};
for (call, (answered, holds, if_stopped, (tool, started, footprint))) in of
.iter()
.filter_map(|call| Some((call, calls.get(call).ok()?)))
{
release(&mut commands, holds);
if !answered {
let said = match tool {
true => not_finished_text(&why, started, footprint),
false => why.clone(),
};
let stopped = match if_stopped {
Some(IfStopped(outcome)) => outcome.clone(),
None => Outcome(Err(ErrorReport::new(ErrorKind::Cancelled, said))),
};
commands.entity(call).try_insert_if_new(stopped);
}
}
}
}
pub(crate) fn commit_turn_calls(
calls: Query<
(
Entity,
&Effect,
Option<&Outcome>,
Option<&RetryVerdict>,
&CallOf,
Has<SupersededBy>,
),
(With<TurnCall>, Added<Settled>),
>,
mut turns: Query<(&TurnOf, &mut Recovery, &mut Turn, Option<&TurnOutcome>)>,
mut agents: Query<Answered>,
inboxes: Inboxes,
mut commands: Commands,
mut notices: MessageWriter<Notice>,
) {
for (call, effect, outcome, verdict, &CallOf(turn), superseded) in &calls {
commands.entity(call).try_remove::<(Partial, Feed)>();
let Some(Outcome(outcome)) = outcome.filter(|_| !superseded) else {
continue;
};
let Ok((&TurnOf(agent), mut recovery, mut state, ending)) = turns.get_mut(turn) else {
continue;
};
let Ok(mut answered) = agents.get_mut(agent) else {
continue;
};
let stopped = ending.and_then(TurnOutcome::stops);
let propose = |commands: &mut Commands, outcome: TurnOutcome, kernel: Option<Ending>| {
if ending.is_none() {
let mut turn = commands.entity(turn);
turn.try_insert(outcome);
if let Some(kernel) = kernel {
turn.try_insert(kernel);
}
}
};
let report = match outcome {
Ok(effect::Answered::Completion(response)) => {
recovery.retries = 0;
let inbox = (&inboxes, &mut commands);
let next =
commit_reply((agent, turn), response, &mut answered, inbox, &mut notices);
match (next, stopped) {
(Next::End(outcome, kernel), _) => propose(&mut commands, outcome, kernel),
(Next::Call, None) => spawn_turn_call(&mut commands, turn, agent),
(Next::Run(calls), None) => {
state.reply = Some(effect.id());
let cut_off = cut_off(response);
spawn_reply(&mut commands, (agent, turn), calls, effect.id(), &cut_off);
}
(Next::Run(calls), Some(why)) => {
let results = close_pending_with(&calls, &format!("not run: {why}"));
commit_results(&mut answered.conversation, results);
}
(Next::Call, Some(_)) => {}
}
continue;
}
_ if stopped.is_some() => continue,
Ok(other) => ErrorReport::new(
ErrorKind::Internal,
format!("the model answered with a {} outcome", other.family()),
),
Err(report) => report.clone(),
};
let kept = |notice: String| {
Some(Ending {
notice,
take_out: false,
})
};
let mut propose = |outcome, kernel| propose(&mut commands, outcome, kernel);
match verdict.map(|verdict| &verdict.0) {
None => propose(
TurnOutcome::Failed(report.message.clone()),
kept(report.message),
),
Some(Verdict::GaveUp(why)) => propose(
TurnOutcome::Failed(report.to_string()),
kept(format!(
"The model call failed: {report}. Not retrying: {why}. Your message is kept \
for a retry."
)),
),
Some(Verdict::Retry(_)) => propose(
TurnOutcome::Failed(report.to_string()),
kept(format!(
"The model call failed: {report}. Your message is kept for a retry."
)),
),
Some(verdict @ (Verdict::Overflow | Verdict::Final)) => {
let why = match verdict {
Verdict::Overflow => format!(
"The conversation does not fit the model's context window: {report}"
),
_ => format!("The model call failed: {report}"),
};
let ending = Ending {
notice: why.clone(),
take_out: true,
};
propose(TurnOutcome::Failed(why), Some(ending));
}
}
}
}
enum Next {
End(TurnOutcome, Option<Ending>),
Call,
Run(Vec<ToolCall>),
}
fn commit_reply(
(agent, turn): (Entity, Entity),
response: &CompletionResponse,
answered: &mut AnsweredItem,
(inboxes, commands): (&Inboxes, &mut Commands),
notices: &mut MessageWriter<Notice>,
) -> Next {
if let Some(message) = response.message() {
answered.conversation.push(message);
}
let through = answered.conversation.messages().len().checked_sub(1);
let through = through.and_then(|last| answered.conversation.id(last));
if let Some(through) = through.filter(|_| response.usage.context_tokens().is_some()) {
let usage = response.usage;
*answered.last = LastUsage(Some(Anchor { usage, through }));
}
let tool_calls: Vec<ToolCall> = response.tool_calls().cloned().collect();
let failure = turn_failure(
&response.choice,
Some(&response.stop()),
response.finish_reason().as_ref(),
);
if let Some(failure) = failure {
if !tool_calls.is_empty() {
let results = close_pending_with(&tool_calls, &format!("not run: {failure}"));
commit_results(&mut answered.conversation, results);
}
let ending = Ending {
notice: format!("The turn failed: {failure}."),
take_out: true,
};
return Next::End(TurnOutcome::Failed(failure.to_string()), Some(ending));
}
if !tool_calls.is_empty() {
return Next::Run(tool_calls);
}
if let Some(next) = carry_on(inboxes.of(agent)) {
let spec = answered.connection.map(|connection| &*connection.spec);
let conversation = &mut *answered.conversation;
let into = (Some(turn), commands);
deliver(agent, spec, next, conversation, notices, into);
return Next::Call;
}
match answered
.conversation
.messages()
.last()
.map(|message| &**message)
{
Some(answer @ Message::Assistant(_)) => {
Next::End(TurnOutcome::Answered(answer.clone()), None)
}
_ => {
let why = "the model ended the turn without a message".to_owned();
Next::End(TurnOutcome::Failed(why), None)
}
}
}
fn drop_unanswered(
agent: Entity,
conversation: &mut Conversation,
notices: &mut MessageWriter<Notice>,
) {
let unanswered = conversation.messages().last().is_some_and(|message| {
matches!(&**message, Message::User { content }
if !content.iter().any(|item| matches!(item, UserContent::ToolResult(_))))
});
if unanswered {
let last = conversation.messages().len().saturating_sub(1);
conversation.splice(last.., [], Author::rig());
notices.write(Notice::info(
agent,
"Your last message was taken out of the conversation; send it again.",
));
}
}
#[derive(QueryData)]
#[query_data(mutable)]
pub(crate) struct Answered {
conversation: &'static mut Conversation,
connection: Option<&'static Connection>,
last: &'static mut LastUsage,
}
pub(crate) fn spawn_reply(
commands: &mut Commands,
(agent, turn): (Entity, Entity),
calls: Vec<ToolCall>,
reply: EffectId,
cut_off: &[ToolCall],
) {
for call in calls {
let refused = cut_off.iter().find(|cut| cut.id == call.id).map(|cut| {
let name = cut.function.name.as_str();
let why = feedback::output_limit_feedback(name, &cut.function.raw_arguments());
Outcome(Err(ErrorReport::new(
ErrorKind::Tool(ToolErrorKind::InvalidArgs),
why,
)))
});
let mut call = commands.spawn((
ToolRequest { call },
Effect::with_parent_id(Some(reply)),
CallOf(turn),
ForAgent(agent),
));
if let Some(refused) = refused {
call.insert(refused);
}
}
}
fn cut_off(response: &CompletionResponse) -> Vec<ToolCall> {
let truncated = response
.finish_reason()
.is_some_and(|finish| finish.truncated_output());
let unfinished = response.unfinished_calls().filter(|_| truncated);
unfinished.cloned().collect()
}
#[derive(QueryData)]
pub(crate) struct ReplyCall {
effect: &'static Effect,
request: &'static ToolRequest,
outcome: Option<&'static Outcome>,
if_stopped: Option<&'static IfStopped>,
successor: Option<&'static SupersededBy>,
footprint: Option<&'static Footprint>,
settled: Has<Settled>,
started: Has<Started>,
waiting: Has<Waiting>,
entity: Entity,
}
fn reply_calls<'a>(
reply: EffectId,
calls: &Calls,
replies: &'a Query<ReplyCall>,
) -> Vec<ReplyCallItem<'a, 'a>> {
let mut asked: Vec<ReplyCallItem<'a, 'a>> = Vec::new();
for call in calls.iter() {
let Ok(mut call) = replies.get(call) else {
continue;
};
let first = |at: &ReplyCallItem| at.request.call.id == call.request.call.id;
if call.effect.parent != Some(reply) || asked.iter().any(first) {
continue;
}
for _ in 0..calls.len() {
match call
.successor
.and_then(|by| replies.get(by.successor()).ok())
{
Some(successor) => call = successor,
None => break,
}
}
asked.push(call);
}
asked
}
pub(crate) fn wait_in_order(
turns: Query<(&Turn, &Calls)>,
replies: Query<ReplyCall>,
mut commands: Commands,
) {
let anything = Footprint::default();
for (state, calls) in &turns {
let Some(reply) = state.reply else {
continue;
};
let mut earlier: Vec<&Footprint> = Vec::new();
for call in reply_calls(reply, calls, &replies) {
let footprint = call.footprint.unwrap_or(&anything);
let waits = earlier
.iter()
.any(|before| footprint.0.conflicts_with(&before.0));
let open = !call.started && call.outcome.is_none() && !call.settled;
if open && waits != call.waiting {
let mut entity = commands.entity(call.entity);
match waits {
true => entity.try_insert(Waiting),
false => entity.try_remove::<Waiting>(),
};
}
if !call.settled {
earlier.push(footprint);
}
}
}
}
pub(crate) fn commit_replies(
mut turns: Query<(Entity, &TurnOf, &mut Turn, &Calls, Option<&TurnOutcome>)>,
replies: Query<ReplyCall>,
mut agents: Query<&mut Conversation>,
mut commands: Commands,
) {
for (turn, &TurnOf(agent), mut state, calls, ending) in &mut turns {
let Some(reply) = state.reply else {
continue;
};
let calls = reply_calls(reply, calls, &replies);
let Ok(mut conversation) = agents.get_mut(agent) else {
continue;
};
if calls.iter().any(|call| !call.settled) {
for call in calls.iter().filter(|call| call.settled) {
if let Some(outcome) = call.outcome {
let result = outcome.tool_result();
mark_settled(&mut conversation, &call.request.call.id, &result);
}
}
continue;
}
state.reply = None;
let unanswered = ToolResult::failed(ToolExecutionError::other("the call had no result"));
if let Some(results) = reply_results(calls, |_| Some(unanswered.clone())) {
commit_results(&mut conversation, results);
}
if ending.and_then(TurnOutcome::stops).is_none() {
spawn_turn_call(&mut commands, turn, agent);
}
}
}
pub(crate) fn end_turns(
turns: Query<
(
Entity,
&TurnOf,
&TurnOutcome,
Option<&Ending>,
Option<&Calls>,
),
Without<Holds>,
>,
open: Query<(), (With<Effect>, Without<Settled>)>,
arrived: Query<(), (With<TurnCall>, Added<Settled>)>,
mut agents: Query<(&AgentId, &mut Conversation)>,
inboxes: Inboxes,
mut commands: Commands,
mut notices: MessageWriter<Notice>,
) {
for (turn, &TurnOf(agent), outcome, ending, calls) in &turns {
let Ok((id, mut conversation)) = agents.get_mut(agent) else {
continue;
};
if let TurnOutcome::Answered(_) = outcome
&& inboxes.coming(agent)
{
continue;
}
let waiting = |call| open.contains(call) || arrived.contains(call);
if calls.is_some_and(|calls| calls.iter().any(waiting)) {
continue;
}
if let Some(ending) = ending {
notices.write(Notice::error(agent, ending.notice.clone()));
if ending.take_out {
drop_unanswered(agent, &mut conversation, &mut notices);
}
}
if !matches!(outcome, TurnOutcome::Answered(_)) {
conversation.halt(Halt::Kept);
}
let end = turn_end(outcome, &conversation);
conversation.end_turn(end);
let how = match outcome {
TurnOutcome::Answered(_) => "answered",
TurnOutcome::Failed(_) => "failed",
TurnOutcome::Stopped(_) => "stopped",
};
info!(agent = %id.0, "turn ended: {how}");
commands
.entity(turn)
.try_remove::<(TurnOf, Ending)>()
.try_insert(Settled);
}
}
pub(crate) fn carry_on_called(
called: Query<&CallOf, (With<TurnCall>, Added<Admitted>)>,
ending: Query<Option<&Holds>, With<TurnOutcome>>,
mut commands: Commands,
) {
for &CallOf(turn) in &called {
if let Ok(holds) = ending.get(turn) {
take_outcome(&mut commands, turn, holds);
}
}
}
pub(crate) fn take_outcome(commands: &mut Commands, turn: Entity, holds: Option<&Holds>) {
commands.entity(turn).try_remove::<(TurnOutcome, Ending)>();
release(commands, holds);
}
pub(crate) fn keeps_settled(world: &World, entity: Entity) -> bool {
let reply = world
.get::<CallOf>(entity)
.and_then(|of| world.get::<Turn>(of.0))
.and_then(|turn| turn.reply);
let parent = world.get::<Effect>(entity).and_then(|effect| effect.parent);
reply.is_some() && parent == reply && world.get::<ToolRequest>(entity).is_some()
}