use serde_json::{json, Value};
pub(crate) const STEER_EVENTS: [&str; 8] = [
"SessionStart",
"SubagentStart",
"PreToolUse",
"PostToolUse",
"PostToolBatch",
"UserPromptSubmit",
"Stop",
"SubagentStop",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Mode {
Steer,
Queue,
}
impl Mode {
pub(crate) fn as_str(self) -> &'static str {
match self {
Mode::Steer => "steer",
Mode::Queue => "queue",
}
}
pub(crate) fn parse(s: &str) -> Option<Self> {
match s {
"steer" => Some(Mode::Steer),
"queue" => Some(Mode::Queue),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Relation {
Parent,
Child,
Sibling,
CrossSession,
CrossProject,
External,
Unknown,
}
impl Relation {
pub(crate) fn as_str(self) -> &'static str {
match self {
Relation::Parent => "parent",
Relation::Child => "child",
Relation::Sibling => "sibling",
Relation::CrossSession => "cross-session",
Relation::CrossProject => "cross-project",
Relation::External => "external",
Relation::Unknown => "unknown",
}
}
pub(crate) fn parse(s: &str) -> Option<Self> {
Some(match s {
"parent" => Relation::Parent,
"child" => Relation::Child,
"sibling" => Relation::Sibling,
"cross-session" => Relation::CrossSession,
"cross-project" => Relation::CrossProject,
"external" => Relation::External,
"unknown" => Relation::Unknown,
_ => return None,
})
}
pub(crate) fn needs_peer_caution(self) -> bool {
matches!(
self,
Relation::Sibling
| Relation::CrossSession
| Relation::CrossProject
| Relation::External
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SenderKind {
Lane,
External,
}
impl SenderKind {
pub(crate) fn as_str(self) -> &'static str {
match self {
SenderKind::Lane => "lane",
SenderKind::External => "external",
}
}
pub(crate) fn parse(s: &str) -> Option<Self> {
match s {
"lane" => Some(SenderKind::Lane),
"external" => Some(SenderKind::External),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct MessageFrom {
pub(crate) kind: SenderKind,
pub(crate) session: Option<String>,
pub(crate) lane: Option<String>,
pub(crate) label: Option<String>,
pub(crate) cwd: Option<String>,
}
impl MessageFrom {
pub(crate) fn envelope_token(&self) -> String {
match self.kind {
SenderKind::Lane => self
.lane
.clone()
.or_else(|| self.session.clone())
.unwrap_or_else(|| "unknown".to_string()),
SenderKind::External => {
let label = self.label.as_deref().unwrap_or("unknown");
format!("external:{}", sanitize_header_value(label))
}
}
}
pub(crate) fn session_prefix(&self) -> String {
self.session
.as_deref()
.and_then(|s| s.get(..8))
.unwrap_or("unknown")
.to_string()
}
pub(crate) fn to_json(&self) -> Value {
json!({
"kind": self.kind.as_str(),
"session": self.session,
"lane": self.lane,
"label": self.label,
"cwd": self.cwd,
})
}
pub(crate) fn from_json(v: &Value) -> Option<Self> {
Some(MessageFrom {
kind: SenderKind::parse(str_field(v, "kind")?.as_str())?,
session: str_field(v, "session"),
lane: str_field(v, "lane"),
label: str_field(v, "label"),
cwd: str_field(v, "cwd"),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TargetForm {
Transcript,
Routing,
}
impl TargetForm {
pub(crate) fn as_str(self) -> &'static str {
match self {
TargetForm::Transcript => "transcript",
TargetForm::Routing => "routing",
}
}
pub(crate) fn parse(s: &str) -> Option<Self> {
match s {
"transcript" => Some(TargetForm::Transcript),
"routing" => Some(TargetForm::Routing),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct MessageTo {
pub(crate) session: String,
pub(crate) lane: String,
pub(crate) form: TargetForm,
pub(crate) routing_id: Option<String>,
}
impl MessageTo {
pub(crate) fn to_json(&self) -> Value {
json!({
"session": self.session,
"lane": self.lane,
"form": self.form.as_str(),
"routing_id": self.routing_id,
})
}
pub(crate) fn from_json(v: &Value) -> Option<Self> {
Some(MessageTo {
session: str_field(v, "session")?,
lane: str_field(v, "lane")?,
form: TargetForm::parse(str_field(v, "form")?.as_str())?,
routing_id: str_field(v, "routing_id"),
})
}
}
#[derive(Debug, Clone)]
pub(crate) struct Message {
pub(crate) id: String,
pub(crate) ts_utc: String,
pub(crate) from: MessageFrom,
pub(crate) to: MessageTo,
pub(crate) mode: Mode,
pub(crate) ttl_secs: u64,
pub(crate) relation: Relation,
pub(crate) cross_project: bool,
pub(crate) body: String,
}
impl Message {
pub(crate) fn to_json(&self) -> Value {
json!({
"id": self.id,
"ts_utc": self.ts_utc,
"from": self.from.to_json(),
"to": self.to.to_json(),
"mode": self.mode.as_str(),
"ttl_secs": self.ttl_secs,
"relation": self.relation.as_str(),
"cross_project": self.cross_project,
"body": self.body,
})
}
pub(crate) fn from_json(v: &Value) -> Option<Self> {
Some(Message {
id: str_field(v, "id")?,
ts_utc: str_field(v, "ts_utc")?,
from: MessageFrom::from_json(v.get("from")?)?,
to: MessageTo::from_json(v.get("to")?)?,
mode: Mode::parse(str_field(v, "mode")?.as_str())?,
ttl_secs: v.get("ttl_secs").and_then(Value::as_u64).unwrap_or(0),
relation: Relation::parse(str_field(v, "relation")?.as_str())?,
cross_project: v
.get("cross_project")
.and_then(Value::as_bool)
.unwrap_or(false),
body: str_field(v, "body").unwrap_or_default(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Vehicle {
AdditionalContext,
Exit2,
}
impl Vehicle {
pub(crate) fn as_str(self) -> &'static str {
match self {
Vehicle::AdditionalContext => "additionalContext",
Vehicle::Exit2 => "exit2",
}
}
pub(crate) fn parse(s: &str) -> Option<Self> {
match s {
"additionalContext" => Some(Vehicle::AdditionalContext),
"exit2" => Some(Vehicle::Exit2),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Verdict {
Ok,
Full,
MayFail,
Unpredictable,
Refused,
}
impl Verdict {
pub(crate) fn as_str(self) -> &'static str {
match self {
Verdict::Ok => "OK",
Verdict::Full => "FULL",
Verdict::MayFail => "MAY-FAIL",
Verdict::Unpredictable => "UNPREDICTABLE",
Verdict::Refused => "REFUSED",
}
}
}
pub(crate) fn str_field(v: &Value, key: &str) -> Option<String> {
v.get(key)
.and_then(Value::as_str)
.map(std::string::ToString::to_string)
}
fn sanitize_header_value(s: &str) -> String {
let folded: String = s
.chars()
.map(|c| {
if c.is_whitespace() || c == ']' || c == '[' || (c as u32) < 0x20 {
'_'
} else {
c
}
})
.collect();
if folded.is_empty() {
"unknown".to_string()
} else {
folded
}
}
pub(crate) fn new_message_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0u64, |d| d.as_nanos() as u64);
let seed =
nanos ^ (u64::from(std::process::id()) << 33) ^ seq.wrapping_mul(0x9E37_79B9_7F4A_7C15);
format!("{:016x}", splitmix64(seed))
}
pub(crate) fn splitmix64(seed: u64) -> u64 {
let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
pub(crate) fn now_utc() -> String {
let ts = jiff::Timestamp::now();
ts.round(jiff::Unit::Millisecond).unwrap_or(ts).to_string()
}
pub(crate) fn expires_at(ts_utc: &str, ttl_secs: u64) -> Option<String> {
let ts: jiff::Timestamp = ts_utc.parse().ok()?;
let secs = i64::try_from(ttl_secs).ok()?;
let out = ts.checked_add(jiff::SignedDuration::from_secs(secs)).ok()?;
Some(
out.round(jiff::Unit::Millisecond)
.unwrap_or(out)
.to_string(),
)
}
pub(crate) fn is_expired(expires_utc: &str, now_utc: &str) -> bool {
let (Ok(exp), Ok(now)) = (
expires_utc.parse::<jiff::Timestamp>(),
now_utc.parse::<jiff::Timestamp>(),
) else {
return false;
};
exp <= now
}