use std::fmt;
use std::sync::Arc;
use async_trait::async_trait;
use crate::capability::{CapabilityEffectError, SupervisedTaskSpawner};
use crate::hooks::{
HookEngine, HookEvent, HookExecutor, HookOutcome, HookResult, HookTaskDispatcher,
HookTaskFuture,
};
struct CapabilityHookTaskDispatcher {
tasks: SupervisedTaskSpawner,
}
impl HookTaskDispatcher for CapabilityHookTaskDispatcher {
fn dispatch(&self, name: &'static str, task: HookTaskFuture) -> Result<(), String> {
self.tasks
.spawn_task(name, async move {
task.await;
Ok::<(), CapabilityEffectError>(())
})
.map(|_| ())
.map_err(|error| error.to_string())
}
}
pub(super) struct RunHookExecutor {
external: Option<Arc<dyn HookExecutor>>,
snapshot: Arc<HookEngine>,
tasks: SupervisedTaskSpawner,
}
impl RunHookExecutor {
pub(super) fn new(
external: Option<Arc<dyn HookExecutor>>,
snapshot: Arc<HookEngine>,
tasks: SupervisedTaskSpawner,
) -> Result<Arc<Self>, &'static str> {
let dispatcher: Arc<dyn HookTaskDispatcher> = Arc::new(CapabilityHookTaskDispatcher {
tasks: tasks.clone(),
});
snapshot
.attach_task_dispatcher(dispatcher)
.map_err(|_| "Run Hook task dispatcher was already attached")?;
Ok(Arc::new(Self {
external,
snapshot,
tasks,
}))
}
async fn fire_composed(&self, event: &HookEvent, inline_snapshot: bool) -> HookOutcome {
let external_modified = match &self.external {
Some(external) => match external.fire_outcome(event).await {
HookOutcome::Continue(modified) => modified,
HookOutcome::Skip => None,
terminal => return terminal,
},
None => None,
};
let snapshot_outcome = if inline_snapshot {
self.snapshot.fire_outcome_inline_observers(event).await
} else {
self.snapshot.fire_outcome(event).await
};
match snapshot_outcome {
HookOutcome::Continue(modified) => {
HookOutcome::Continue(modified.or(external_modified))
}
HookOutcome::Skip => HookOutcome::Continue(external_modified),
terminal => terminal,
}
}
}
impl fmt::Debug for RunHookExecutor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("RunHookExecutor")
.field("has_external", &self.external.is_some())
.field("snapshot", &self.snapshot)
.finish_non_exhaustive()
}
}
#[async_trait]
impl HookExecutor for RunHookExecutor {
async fn fire(&self, event: &HookEvent) -> HookResult {
self.fire_composed(event, false).await.into()
}
async fn fire_outcome(&self, event: &HookEvent) -> HookOutcome {
self.fire_composed(event, false).await
}
fn dispatch_observational(self: Arc<Self>, event: HookEvent) {
let tasks = self.tasks.clone();
let event_type = event.event_type();
if let Err(error) = tasks.spawn_task("hook.observation", async move {
let _ = self.fire_composed(&event, true).await;
Ok(())
}) {
tracing::warn!(
event_type = %event_type,
failure = %error,
"Run-scoped observational Hook could not be supervised"
);
}
}
async fn record_agent_event(
&self,
event: &crate::agent::AgentEvent,
run_id: &str,
session_id: &str,
) {
if let Some(external) = &self.external {
external.record_agent_event(event, run_id, session_id).await;
}
}
async fn record_run_cancelled(&self, run_id: &str, session_id: &str, reason: Option<&str>) {
if let Some(external) = &self.external {
external
.record_run_cancelled(run_id, session_id, reason)
.await;
}
}
}