use std::sync::Arc;
use tokio::sync::broadcast;
use crate::types::AgentMessage;
use super::AgentHarness;
impl AgentHarness {
pub fn subscribe_harness(&self, listener: SessionListener) -> Box<dyn FnOnce() + Send> {
self.harness_listeners.lock().push(listener.clone());
let target = Arc::as_ptr(&listener) as *const () as usize;
let listeners = Arc::clone(&self.harness_listeners);
Box::new(move || {
let mut guard = listeners.lock();
if let Some(index) = guard
.iter()
.position(|item| (Arc::as_ptr(item) as *const () as usize) == target)
{
guard.remove(index);
}
})
}
pub fn subscribe_session_broadcast(&self) -> broadcast::Receiver<SessionEvent> {
self.session_broadcast_tx.subscribe()
}
pub(crate) fn emit_harness_event(&self, event: SessionEvent) {
let listeners = self.harness_listeners.lock().clone();
for listener in listeners {
let event = event.clone();
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || listener(event)));
}
let _ = self.session_broadcast_tx.send(event);
}
pub(super) async fn ensure_session_start_emitted(&self) {
let should_emit = {
let mut emitted = self.session_start_emitted.lock();
if *emitted {
false
} else {
*emitted = true;
true
}
};
if !should_emit {
return;
}
let messages_replayed = self.agent.state().messages.len();
self.runtime_extensions.ensure_session_start().await;
self.emit_harness_event(SessionEvent::Started { messages_replayed });
}
pub async fn start_runtime_extensions(&self) {
self.ensure_session_start_emitted().await;
}
pub async fn shutdown_runtime_extensions(&self) {
self.abort();
if tokio::time::timeout(SHUTDOWN_IDLE_TIMEOUT, self.agent.wait_until_idle())
.await
.is_err()
{
tracing::warn!("runtime-extension shutdown: run did not go idle in time; continuing");
}
self.runtime_extensions.shutdown().await;
}
}
const SHUTDOWN_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(45);
#[derive(Clone, Debug)]
pub enum SessionEvent {
Started { messages_replayed: usize },
Compaction {
from_hook: bool,
summary: String,
tokens_before: u64,
},
Branch {
from_entry_id: Option<String>,
to_entry_id: Option<String>,
summary_entry_id: Option<String>,
},
PersistenceError { context: String, message: String },
TurnDecision {
decision: &'static str,
continuation_count: u32,
reason: Option<String>,
next_prompt_preview: Option<String>,
},
SkillsReloaded { total: usize },
ExtensionCommandOutcome {
outcome: theway_contract::extension::ExtensionCommandOutcome,
},
}
pub type SessionListener = Arc<dyn Fn(SessionEvent) + Send + Sync>;
#[derive(Clone)]
pub struct OnTurnEndContext {
pub transcript: Vec<AgentMessage>,
pub continuation_count: u32,
pub last_user_prompt: Option<String>,
}
#[derive(Clone, Debug)]
pub enum TurnEndAction {
Noop,
Stop,
Pause { reason: String },
Continue { prompt: String },
}
impl TurnEndAction {
pub fn as_audit_str(&self) -> Option<&'static str> {
match self {
Self::Noop => None,
Self::Stop => Some("stop"),
Self::Pause { .. } => Some("pause"),
Self::Continue { .. } => Some("continue"),
}
}
}
#[derive(Clone, Debug)]
pub struct TurnEndDecision {
pub action: TurnEndAction,
pub payload: Option<serde_json::Value>,
}
impl From<TurnEndAction> for TurnEndDecision {
fn from(action: TurnEndAction) -> Self {
Self {
action,
payload: None,
}
}
}
pub type OnTurnEndHook = Arc<
dyn Fn(
OnTurnEndContext,
tokio_util::sync::CancellationToken,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = TurnEndDecision> + Send>>
+ Send
+ Sync,
>;
pub const DEFAULT_TURN_CONTINUATION_CAP: u32 = 25;