use harn_session_store::ControlEvent;
use super::{append_event_to_state, SESSIONS};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ControlRecordOutcome {
Recorded,
UnknownSession,
Rejected,
}
impl ControlRecordOutcome {
pub fn is_recorded(self) -> bool {
matches!(self, Self::Recorded)
}
pub fn as_str(self) -> &'static str {
match self {
Self::Recorded => "recorded",
Self::UnknownSession => "unknown_session",
Self::Rejected => "rejected",
}
}
}
pub fn record_control_event(session_id: &str, control: &ControlEvent) -> ControlRecordOutcome {
let event = crate::stdlib::json_to_vm_value(&control.to_payload());
SESSIONS.with(|sessions| {
let mut sessions = sessions.borrow_mut();
let Some(state) = sessions.get_mut(session_id) else {
return ControlRecordOutcome::UnknownSession;
};
match append_event_to_state(state, event, "record_control_event") {
Ok(()) => ControlRecordOutcome::Recorded,
Err(error) => {
tracing::warn!(
session_id = %session_id,
action = control.action.as_str(),
%error,
"accepted control word could not be appended to the session event stream"
);
ControlRecordOutcome::Rejected
}
}
})
}
pub fn control_events(session_id: &str) -> Vec<ControlEvent> {
SESSIONS.with(|sessions| {
let sessions = sessions.borrow();
let Some(state) = sessions.get(session_id) else {
return Vec::new();
};
let Some(dict) = state.transcript.as_dict() else {
return Vec::new();
};
let Some(crate::value::VmValue::List(events)) = dict.get("events") else {
return Vec::new();
};
events
.iter()
.filter_map(|event| {
ControlEvent::from_payload(&crate::llm::helpers::vm_value_to_json(event))
})
.collect()
})
}
#[cfg(test)]
mod tests {
use harn_session_store::{ControlAction, ControlEvent, SessionEventKind};
use super::*;
async fn open_journaled_session(root: &std::path::Path, session_id: &str) {
let mut options = crate::value::DictMap::new();
crate::value::VmDictExt::put_str(&mut options, "root", root.to_string_lossy().as_ref());
let prepared = crate::agent_session_journal::prepare(
session_id,
&options,
"run-control".to_string(),
"turn-control".to_string(),
)
.await
.expect("prepare journal");
crate::agent_sessions::open_or_create(Some(session_id.to_string()));
crate::agent_sessions::install_journal(session_id, prepared.state)
.expect("install journal");
}
async fn stored_events(
root: &std::path::Path,
session_id: &str,
) -> Vec<harn_session_store::StoredEvent> {
let store = crate::stdlib::session_store::open_canonical_agent_session(
&crate::stdlib::session_store::SessionStoreDir::under_root(root),
session_id,
None,
harn_session_store::SessionType::User,
)
.await
.expect("open canonical session");
crate::stdlib::session_store::read_all_events(&store, session_id)
.await
.expect("read canonical events")
}
#[tokio::test(flavor = "current_thread")]
async fn an_accepted_steer_is_persisted_as_a_typed_control_event() {
crate::agent_sessions::reset_session_store();
let root = tempfile::tempdir().expect("temp root");
let session_id = "control-steer";
open_journaled_session(root.path(), session_id).await;
let outcome = record_control_event(
session_id,
&ControlEvent::injection(
"session/inject",
"ctl-steer-1",
"accepted",
"steer",
"finish_step",
"msg_inj_steer",
"if no typed code exists, say so and stop",
),
);
assert_eq!(outcome, ControlRecordOutcome::Recorded);
crate::agent_session_journal::flush(session_id)
.await
.expect("flush journal");
let events = stored_events(root.path(), session_id).await;
let control_rows: Vec<_> = events
.iter()
.filter(|event| event.kind == SessionEventKind::Control)
.collect();
assert_eq!(
control_rows.len(),
1,
"expected exactly one typed control row; stored kinds were {:?}",
events
.iter()
.map(|event| event.kind.discriminator())
.collect::<Vec<_>>()
);
let typed = ControlEvent::from_stored_event(control_rows[0])
.expect("control row must read back as a typed control event");
assert_eq!(typed.action, ControlAction::Steer);
assert_eq!(typed.requested_mode.as_deref(), Some("steer"));
assert_eq!(typed.delivery_mode.as_deref(), Some("finish_step"));
assert_eq!(typed.message_id.as_deref(), Some("msg_inj_steer"));
assert_eq!(
typed.text.as_deref(),
Some("if no typed code exists, say so and stop"),
"the steer text must survive into the store"
);
crate::agent_sessions::reset_session_store();
}
#[tokio::test(flavor = "current_thread")]
async fn an_accepted_stop_is_persisted_as_a_typed_control_event() {
crate::agent_sessions::reset_session_store();
let root = tempfile::tempdir().expect("temp root");
let session_id = "control-stop";
open_journaled_session(root.path(), session_id).await;
let outcome = record_control_event(
session_id,
&ControlEvent::stop("session/cancel", "ctl-stop-1", "cancelled"),
);
assert_eq!(outcome, ControlRecordOutcome::Recorded);
crate::agent_session_journal::flush(session_id)
.await
.expect("flush journal");
let events = stored_events(root.path(), session_id).await;
let typed: Vec<ControlEvent> = events
.iter()
.filter_map(ControlEvent::from_stored_event)
.collect();
assert_eq!(
typed.len(),
1,
"expected one typed stop row; stored rows were {:#?}",
events
.iter()
.map(|event| (
event.kind.discriminator().to_string(),
event.payload.clone()
))
.collect::<Vec<_>>()
);
assert_eq!(typed[0].action, ControlAction::Stop);
assert_eq!(typed[0].status, "cancelled");
assert!(
typed[0].text.is_none(),
"a stop carries no steer text: {typed:?}"
);
crate::agent_sessions::reset_session_store();
}
#[tokio::test(flavor = "current_thread")]
async fn a_control_free_session_persists_no_control_events() {
crate::agent_sessions::reset_session_store();
let root = tempfile::tempdir().expect("temp root");
let session_id = "control-free";
open_journaled_session(root.path(), session_id).await;
crate::agent_sessions::inject_message(
session_id,
crate::stdlib::json_to_vm_value(&serde_json::json!({
"role": "user",
"content": "if no typed code exists, say so and stop",
})),
)
.expect("inject an ordinary user message");
crate::agent_session_journal::flush(session_id)
.await
.expect("flush journal");
let events = stored_events(root.path(), session_id).await;
assert!(
!events.is_empty(),
"the store read returned nothing at all, so the zero below would be vacuous"
);
assert_eq!(
events
.iter()
.filter(|event| event.kind == SessionEventKind::Control)
.count(),
0,
"a session with no accepted control must persist no control rows; \
message prose that merely reads like a stop must not become one"
);
assert!(control_events(session_id).is_empty());
crate::agent_sessions::reset_session_store();
}
#[test]
fn a_control_for_an_unknown_session_reports_the_miss() {
crate::agent_sessions::reset_session_store();
let outcome = record_control_event(
"no-such-session",
&ControlEvent::stop("session/cancel", "ctl-miss", "cancelled"),
);
assert_eq!(outcome, ControlRecordOutcome::UnknownSession);
assert!(!outcome.is_recorded());
}
}