use bevy_ecs::prelude::*;
use rig_core::{
effect::EffectId,
error::{ErrorKind, ErrorReport},
serve::{Reply, stream_truncated},
streaming::{Item, Relayed, StreamEvent},
};
use std::task::Poll;
use super::{
effect::{
EffectOutcome, InFlight, Issued, Publishing, Serving, Streamed, Streaming, Tasks,
ToolOutputs, WorldOutcome,
},
plugin::Wake,
record::{DeliveryBatch, Observed, Recording},
witness::{SeenOutcome, Subjects, Witnessing, bus_emitter, fingerprint},
};
use rig_core::observe::{Action, Emitter, OutcomeSummary, Reason, Stage};
pub const TRUNCATION_TAIL: usize = 8;
pub const STREAM_WORK_PER_TICK: usize = 4096;
const STREAM_ITEMS_PER_EFFECT: usize = 64;
#[derive(Default)]
pub struct StreamCursor {
last: Option<super::Seq>,
}
#[derive(Component)]
pub struct CollectedOutcome;
pub fn collect_world(
mut commands: Commands,
ready: Query<(Entity, &WorldOutcome), (With<InFlight>, Without<EffectOutcome>)>,
) {
let mut ready: Vec<_> = ready.iter().collect();
ready.sort_by_key(|(_, outcome)| outcome.order());
for (entity, outcome) in ready {
commands
.entity(entity)
.insert(CollectedOutcome)
.insert(EffectOutcome(outcome.outcome.clone()))
.remove::<WorldOutcome>();
}
}
pub fn collect_tasks(
mut commands: Commands,
serving: Query<(Entity, Option<&Publishing>, &Serving), With<InFlight>>,
policy: Res<super::Policy>,
wake: Res<Wake>,
mut tasks: Tasks,
) {
for (entity, publishing, _) in &serving {
let Some(reply) = tasks.poll(entity) else {
continue;
};
let mut entity_commands = commands.entity(entity);
entity_commands.remove::<Serving>();
match reply {
Reply::Outcome(outcome) => {
if let Some(Publishing(published)) = publishing {
entity_commands.remove::<Publishing>();
if let Some(context) = published.take() {
entity_commands.insert(ToolOutputs(context));
}
}
entity_commands
.insert(CollectedOutcome)
.insert(EffectOutcome(outcome));
}
Reply::Stream(stream) => {
let streaming =
tasks.streaming(entity, stream, policy.0.stream_capacity, wake.clone());
entity_commands.insert(streaming);
}
}
}
}
pub fn collect_streams(
mut commands: Commands,
mut streaming: Query<StreamingView, With<InFlight>>,
mut tasks: Tasks,
recording: Option<Res<Recording>>,
witness: Option<Res<Witnessing>>,
subjects: Subjects,
batch: Res<DeliveryBatch>,
mut cursor: Local<StreamCursor>,
mut order: Local<Vec<(super::Seq, Entity)>>,
) {
let mut remaining = STREAM_WORK_PER_TICK;
order.clear();
order.extend(streaming.iter().map(|(entity, seq, ..)| (*seq, entity)));
order.sort_unstable_by_key(|(seq, _)| *seq);
let start = cursor
.last
.map_or(0, |last| order.partition_point(|(seq, _)| *seq <= last));
for &(seq, entity) in order.iter().cycle().skip(start).take(order.len()) {
if remaining == 0 {
break;
}
let Ok((_, _, Issued(id), mut streaming, mut streamed, publishing)) =
streaming.get_mut(entity)
else {
continue;
};
let mut delivered = 0;
let start = streamed
.as_ref()
.map_or(0, |state| state.events.len() + state.errors.len());
let mut items = Vec::new();
for _ in 0..STREAM_ITEMS_PER_EFFECT {
if remaining == 0 {
break;
}
remaining -= 1;
let polled = match streaming.events.try_recv() {
Ok(item) => Poll::Ready(Some(item)),
Err(futures::channel::mpsc::TryRecvError::Empty) => Poll::Pending,
Err(futures::channel::mpsc::TryRecvError::Closed) => Poll::Ready(None),
};
let outcome = match polled {
Poll::Pending => break,
Poll::Ready(Some(item)) => {
streaming.delivered += 1;
if let Some(streamed) = &mut streamed {
if streamed.outcome.is_none()
&& let Some(outcome) = streaming.fold.observe(&item)
{
if matches!(item, Ok(Relayed::Done(_))) {
streamed.bypass_change_detection().outcome = Some(outcome);
} else {
streamed.outcome = Some(outcome);
}
}
let position = streamed.events.len() + streamed.errors.len();
match item {
Ok(Relayed::Origin(origin)) => {
streamed.origin = Some(origin);
continue;
}
Ok(Relayed::Done(_)) => continue,
Ok(Relayed::Item(item)) => {
if let Item::Event(StreamEvent::Text { text, .. }) = &item {
streamed.text.push_str(text);
}
items.push(Ok(item.clone()));
if let Err(error) = streamed.events.push(item) {
streamed.errors.push((
position,
ErrorReport::new(ErrorKind::Response, error.to_string()),
));
}
}
Err(error) => {
items.push(Err(error.clone()));
streamed.errors.push((position, error));
}
}
delivered += 1;
continue;
}
let Some(outcome) = streaming.fold.observe(&item) else {
continue;
};
outcome
}
Poll::Ready(None) => match streamed
.as_ref()
.and_then(|streamed| streamed.outcome.clone())
{
Some(outcome) => outcome,
None => {
if let Some(witness) = &witness {
let (tail, errors) = streamed.as_ref().map_or_else(
|| (Vec::new(), Vec::new()),
|streamed| {
let skip =
streamed.events.len().saturating_sub(TRUNCATION_TAIL);
(
streamed
.events
.items()
.iter()
.skip(skip)
.map(|item| match item {
Item::Event(event) => event.name().to_owned(),
Item::Unknown(_) => "Unknown".to_owned(),
})
.collect(),
streamed
.errors
.iter()
.map(|(_, error)| Reason::from_report(error))
.collect(),
)
},
);
witness.emit(
subjects.of(entity),
Stage::Collect,
bus_emitter(),
Action::stream_truncated(
streamed.as_ref().map_or(streaming.delivered, |streamed| {
streamed.events.len() + streamed.errors.len()
}),
tail,
errors,
),
);
}
Err(stream_truncated())
}
},
};
tasks.forget_stream(entity);
if !items.is_empty() {
commands.trigger(super::StreamItemsDelivered {
effect: entity,
id: *id,
start,
items: std::mem::take(&mut items),
});
}
let published = publishing.map(|Publishing(published)| published.take());
commands.queue(move |world: &mut World| {
let Ok(mut effect) = world.get_entity_mut(entity) else {
return;
};
if let Some(context) = published {
effect.remove::<Publishing>();
if let Some(context) = context {
effect.insert(ToolOutputs(context));
}
}
effect
.remove::<Streaming>()
.insert(CollectedOutcome)
.insert(EffectOutcome(outcome));
});
break;
}
if !items.is_empty() {
commands.trigger(super::StreamItemsDelivered {
effect: entity,
id: *id,
start,
items,
});
}
if delivered != 0
&& let Some(recording) = &recording
{
recording.delivery(rig_core::effect::Delivery {
batch: batch.0,
id: *id,
kind: rig_core::effect::DeliveryKind::Stream { items: delivered },
});
}
if remaining == 0 {
cursor.last = Some(seq);
}
}
}
pub type StreamingView = (
Entity,
&'static super::Seq,
&'static Issued,
&'static mut Streaming,
Option<&'static mut Streamed>,
Option<&'static Publishing>,
);
pub type Landing = (Added<EffectOutcome>, With<InFlight>);
pub fn settle(
mut commands: Commands,
landed: Query<
(
Entity,
&Issued,
&EffectOutcome,
Option<&Observed>,
Option<&ToolOutputs>,
),
Landing,
>,
replaced: Query<&super::record::ReplacedBy>,
recording: Option<Res<Recording>>,
witness: Option<Res<Witnessing>>,
subjects: Subjects,
) {
for (entity, &Issued(id), outcome, observed, outputs) in &landed {
let recorded = observed
.and_then(|observed| observed.0.take_outcome())
.unwrap_or_else(|| outcome.0.clone());
let discarded = observed.is_some_and(|observed| observed.0.is_discarded());
let replaced_by = replaced
.get(entity)
.ok()
.and_then(super::record::ReplacedBy::layer);
if let (Some(recording), false) = (&recording, discarded) {
if observed.is_none()
&& let Some(outputs) = outputs
{
recording.tool_output(id, outputs.0.result_context());
}
recording.resolve(id, recorded.clone());
}
if let Some(witness) = &witness {
let subject = subjects.of(entity);
if discarded {
} else {
let served = OutcomeSummary::of(&recorded);
let seen = SeenOutcome::of(&outcome.0);
let consumed = seen.summary.clone();
let differs = seen.fingerprint != fingerprint(&recorded);
commands.entity(entity).insert(seen);
let observation = rig_core::observe::Observation::new(
subject.clone(),
Stage::Collect,
bus_emitter(),
Action::Landed {
outcome: served.clone(),
},
);
witness.observe(observation);
if differs {
witness.emit(
subject,
Stage::Handler,
replaced_by.map_or_else(Emitter::unknown, Emitter::named),
Action::Replaced {
recorded: served,
consumed,
},
);
}
}
}
commands.entity(entity).remove::<CollectedOutcome>();
commands
.entity(entity)
.remove::<(InFlight, Observed, super::record::ReplacedBy)>();
commands.trigger(Landed { entity, id });
}
}
#[derive(EntityEvent, Debug, Clone, Copy)]
#[entity_event(propagate, auto_propagate)]
pub struct Landed {
pub entity: Entity,
pub id: EffectId,
}
#[cfg(test)]
mod tests;