use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::ids::{SessionId, ThreadId};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpEnvelope {
pub op_id: String,
pub thread_id: ThreadId,
pub session_id: SessionId,
pub op: Op,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Op {
SendMessage {
content: String,
#[serde(default = "default_mode")]
mode: String,
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
model_provider: Option<String>,
#[serde(default)]
allowed_tools: Option<Vec<String>>,
#[serde(default)]
dynamic_tools: Vec<Value>,
#[serde(default = "default_provenance")]
provenance: String,
},
Steer {
content: String,
},
ContinueGoal,
RunShellCommand {
command: String,
},
SetGoalStatus {
status: String,
#[serde(default)]
clear: bool,
},
Cancel,
Shutdown,
PreviewOutboundRequest {
#[serde(default)]
json: bool,
#[serde(default)]
base_prompt_only: bool,
},
}
fn default_mode() -> String {
"agent".to_string()
}
fn default_provenance() -> String {
"external_user".to_string()
}
impl Op {
#[must_use]
pub fn is_send_message(&self) -> bool {
matches!(self, Self::SendMessage { .. })
}
#[must_use]
pub fn kind_str(&self) -> &'static str {
match self {
Self::SendMessage { .. } => "send_message",
Self::Steer { .. } => "steer",
Self::ContinueGoal => "continue_goal",
Self::RunShellCommand { .. } => "run_shell_command",
Self::SetGoalStatus { .. } => "set_goal_status",
Self::Cancel => "cancel",
Self::Shutdown => "shutdown",
Self::PreviewOutboundRequest { .. } => "preview_outbound_request",
}
}
}
#[must_use]
pub fn headless_send_message_op(thread_id: ThreadId, content: impl Into<String>) -> OpEnvelope {
OpEnvelope {
op_id: format!("op-{}", uuid::Uuid::new_v4()),
thread_id: thread_id.clone(),
session_id: SessionId::new(),
op: Op::SendMessage {
content: content.into(),
mode: default_mode(),
model: None,
model_provider: None,
allowed_tools: None,
dynamic_tools: Vec::new(),
provenance: default_provenance(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn op_envelope_roundtrip() {
let env = headless_send_message_op(ThreadId::new(), "hello");
let json = serde_json::to_string(&env).unwrap();
let back: OpEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(back.thread_id, env.thread_id);
assert!(back.op.is_send_message());
}
#[test]
fn steer_roundtrip() {
let op = Op::Steer {
content: "more".into(),
};
let json = serde_json::to_string(&op).unwrap();
let back: Op = serde_json::from_str(&json).unwrap();
assert_eq!(back.kind_str(), "steer");
}
}