use bevy_ecs::prelude::*;
use rig_core::observe::{Action, Emitter, HostAction, Observation, Reason, Stage, Subject};
use serde::{Deserialize, Serialize};
use crate::{
agent::{Failed, Failure, Run, Settled},
bus::{Subjects, Witnessing},
};
pub const AGENT_EMITTER: &str = "rig-ecs/agent";
pub fn agent_emitter() -> Emitter {
Emitter::versioned(AGENT_EMITTER, env!("CARGO_PKG_VERSION"))
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProviderRetry {
pub attempt: usize,
pub budget: usize,
pub reason: Reason,
}
impl HostAction for ProviderRetry {
const KIND: &'static str = "rig-ecs/agent/provider_retry";
}
pub(crate) fn observe_provider_retry(
witness: &Witnessing,
subject: Subject,
attempt: usize,
budget: usize,
report: &rig_core::error::ErrorReport,
) {
let fact = ProviderRetry {
attempt,
budget,
reason: Reason::with_detail(report.kind.code(), report.message.clone()),
};
if let Ok(action) = fact.action() {
witness.observe(Observation::new(
subject,
Stage::Runtime,
agent_emitter(),
action,
));
}
}
pub fn install(world: &mut World) {
world.add_observer(observe_settled);
world.add_observer(observe_failed);
world.add_observer(observe_run_despawned);
}
fn observe_settled(
added: On<bevy_ecs::lifecycle::Add, Settled>,
subjects: Subjects,
witness: Option<Res<Witnessing>>,
) {
let Some(witness) = witness else {
return;
};
let observation = Observation::new(
subjects.of_scope(added.event().entity),
Stage::Runtime,
agent_emitter(),
Action::Ended {
ending: Reason::code("settled"),
},
);
witness.observe(observation);
}
pub fn ending_of(failure: &Failure) -> Reason {
match failure {
Failure::Content(error) => Reason::with_detail("invalid_content", error.to_string()),
Failure::MaxTurns { limit } => Reason::with_detail(
"max_turns",
format!("the model-call budget of {limit} ran out"),
),
Failure::UnknownToolCall { name } => Reason::with_detail("unknown_tool_call", name.clone()),
Failure::Provider(report) => Reason::with_detail(
"provider",
format!("{}: {}", report.kind.code(), report.message),
),
Failure::Cancelled(report) => Reason::with_detail("cancelled", report.message.clone()),
Failure::Unsupported(what) => Reason::with_detail("unsupported", what.clone()),
Failure::OutputToolCollision { name } => {
Reason::with_detail("output_tool_collision", name.clone())
}
Failure::Tool(report) => Reason::with_detail(
"tool",
format!("{}: {}", report.kind.code(), report.message),
),
Failure::Memory(report) => Reason::with_detail(
"memory",
format!("{}: {}", report.kind.code(), report.message),
),
}
}
fn observe_failed(
added: On<bevy_ecs::lifecycle::Add, Failed>,
failures: Query<&Failed>,
subjects: Subjects,
witness: Option<Res<Witnessing>>,
) {
let Some(witness) = witness else {
return;
};
let entity = added.event().entity;
let Ok(Failed(failure)) = failures.get(entity) else {
return;
};
let observation = Observation::new(
subjects.of_scope(entity),
Stage::Runtime,
agent_emitter(),
Action::Ended {
ending: ending_of(failure),
},
);
witness.observe(observation);
}
fn observe_run_despawned(
removed: On<bevy_ecs::lifecycle::Despawn, Run>,
runs: Query<(Has<Settled>, Has<Failed>)>,
subjects: Subjects,
witness: Option<Res<Witnessing>>,
) {
let entity = removed.event().entity;
let Some(witness) = witness else { return };
let Ok((settled, failed)) = runs.get(entity) else {
return;
};
if settled || failed {
return;
}
let observation = Observation::new(
subjects.of_scope(entity),
Stage::Runtime,
agent_emitter(),
Action::Ended {
ending: Reason::code("despawned_without_ending"),
},
);
witness.observe(observation);
}