pub const PROTOCOL_VERSION: u32 = 1;
pub mod daemon;
use car_ir::ActionProposal;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub parameters: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub returns: Option<Value>,
#[serde(default)]
pub idempotent: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_ttl_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rate_limit: Option<ToolRateLimit>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolRateLimit {
pub max_calls: u32,
pub interval_secs: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInitRequest {
pub client_id: String,
#[serde(default)]
pub tools: Vec<ToolDefinition>,
#[serde(default)]
pub policies: Vec<PolicyDefinition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyDefinition {
pub name: String,
pub rule: String, #[serde(default)]
pub target: String,
#[serde(default)]
pub key: String,
#[serde(default)]
pub value: Value,
#[serde(default)]
pub pattern: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProposalSubmitRequest {
pub proposal: ActionProposal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyRequest {
pub proposal: ActionProposal,
#[serde(default)]
pub initial_state: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolExecuteRequest {
pub action_id: String,
pub tool: String,
pub parameters: Value,
#[serde(default)]
pub timeout_ms: Option<u64>,
#[serde(default)]
pub attempt: u32,
#[serde(default)]
pub request_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCancelRequest {
pub request_id: String,
#[serde(default)]
pub action_id: String,
#[serde(default)]
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolExecuteResponse {
pub action_id: String,
#[serde(default)]
pub output: Option<Value>,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionEvent {
pub kind: String, #[serde(default)]
pub action_id: Option<String>,
#[serde(default)]
pub proposal_id: Option<String>,
#[serde(default)]
pub data: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum HostAgentStatus {
Idle,
Running,
WaitingForApproval,
Paused,
Completed,
Errored,
Stopped,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct HostAgentDisplay {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accent: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostAgent {
pub id: String,
pub name: String,
#[serde(default)]
pub kind: String,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub status: HostAgentStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_task: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(default)]
pub display: HostAgentDisplay,
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub metadata: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterHostAgentRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub name: String,
#[serde(default)]
pub kind: String,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(default)]
pub display: HostAgentDisplay,
#[serde(default)]
pub metadata: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetHostAgentStatusRequest {
pub agent_id: String,
pub status: HostAgentStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_task: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default)]
pub payload: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum HostApprovalStatus {
Pending,
Resolved,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostApprovalRequest {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
pub action: String,
pub details: Value,
#[serde(default)]
pub options: Vec<String>,
pub status: HostApprovalStatus,
pub created_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolved_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolution: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateHostApprovalRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
pub action: String,
#[serde(default)]
pub details: Value,
#[serde(default)]
pub options: Vec<String>,
#[serde(default)]
pub system_level: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolveHostApprovalRequest {
pub approval_id: String,
pub resolution: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostEvent {
pub id: String,
pub timestamp: DateTime<Utc>,
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
pub message: String,
#[serde(default)]
pub payload: Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HostManifestRole {
Owner,
Observer,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostIdentity {
pub version: String,
pub pid: u32,
pub manifest_path: Option<String>,
pub manifest_role: HostManifestRole,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parslee: Option<ParsleeIdentity>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParsleeIdentity {
pub account_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_organization: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub organization_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostSnapshot {
pub subscribed: bool,
#[serde(default)]
pub agents: Vec<HostAgent>,
#[serde(default)]
pub approvals: Vec<HostApprovalRequest>,
#[serde(default)]
pub events: Vec<HostEvent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identity: Option<HostIdentity>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RunTermination {
Outcome {
status: car_ir::OutcomeStatus,
outcome: car_ir::AgentOutcome,
},
Incomplete,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunStarted {
pub run_id: String,
pub agent_id: String,
pub intent: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome_description: Option<String>,
pub started_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunEnded {
pub run_id: String,
pub agent_id: String,
pub termination: RunTermination,
pub ended_at: DateTime<Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CliOutcome {
Exited { code: i64 },
Killed,
Timeout,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VerifierVerdict {
Pass,
Fail,
NotRun,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolicyRejection {
pub rule: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub param: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunTurn {
pub index: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool: Option<String>,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub parameters: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cli_outcome: Option<CliOutcome>,
pub verifier_verdict: VerifierVerdict,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy_rejected: Option<PolicyRejection>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "record", rename_all = "snake_case")]
pub enum RunRecord {
Started(RunStarted),
Ended(RunEnded),
Turn(RunTurn),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunStartRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_name: Option<String>,
pub intent: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome_description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunStartResponse {
pub run_id: String,
pub agent_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunCompleteRequest {
pub run_id: String,
pub outcome: car_ir::AgentOutcome,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunCompleteResponse {
pub run_id: String,
pub ok: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunRecordTurnsRequest {
pub run_id: String,
pub turns: Vec<RunTurn>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunRecordTurnsResponse {
pub run_id: String,
pub base_index: usize,
pub count: usize,
pub ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dropped: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunLiveStatus {
InProgress,
Completed,
Incomplete,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunSubscribeRequest {
pub run_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunSubscribeResponse {
pub run_id: String,
pub agent_id: String,
pub turns_so_far: Vec<RunRecord>,
pub cursor: usize,
pub status: RunLiveStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunUnsubscribeRequest {
pub run_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunUnsubscribeResponse {
pub run_id: String,
pub removed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunTraceEvent {
pub run_id: String,
pub agent_id: String,
pub record: RunRecord,
pub cursor: usize,
pub status: RunLiveStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunListRequest {
pub agent_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunGetTraceRequest {
pub run_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInitResponse {
pub session_id: String,
pub tools_registered: usize,
pub policies_registered: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyResponse {
pub valid: bool,
pub issues: Vec<VerifyIssueProto>,
pub simulated_state: HashMap<String, Value>,
#[serde(default)]
pub execution_levels: Vec<Vec<String>>,
#[serde(default)]
pub conflicts: Vec<(String, String, String)>,
#[serde(default)]
pub evidence: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyIssueProto {
pub action_id: String,
pub severity: String,
pub message: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_definition_roundtrip() {
let td = ToolDefinition {
name: "search".to_string(),
description: "Search the web".to_string(),
parameters: serde_json::json!({"type": "object", "properties": {"query": {"type": "string"}}}),
returns: None,
idempotent: false,
cache_ttl_secs: None,
rate_limit: None,
};
let json = serde_json::to_string(&td).unwrap();
let rt: ToolDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(rt.name, "search");
}
#[test]
fn tool_definition_back_compat_pre_v05_clients() {
let legacy = r#"{"name":"read","description":"","parameters":{}}"#;
let td: ToolDefinition = serde_json::from_str(legacy).unwrap();
assert_eq!(td.name, "read");
assert!(td.returns.is_none());
assert!(!td.idempotent);
assert!(td.cache_ttl_secs.is_none());
assert!(td.rate_limit.is_none());
}
#[test]
fn tool_execute_request_roundtrip() {
let req = ToolExecuteRequest {
action_id: "a1".to_string(),
tool: "search".to_string(),
parameters: serde_json::json!({"query": "rust"}),
timeout_ms: Some(5000),
attempt: 1,
request_id: "cb-7".to_string(),
};
let json = serde_json::to_string(&req).unwrap();
let rt: ToolExecuteRequest = serde_json::from_str(&json).unwrap();
assert_eq!(rt.tool, "search");
assert_eq!(rt.timeout_ms, Some(5000));
assert_eq!(rt.request_id, "cb-7");
}
#[test]
fn tool_execute_request_request_id_defaults_for_pre264_hosts() {
let legacy = r#"{"action_id":"a1","tool":"x","parameters":{},"attempt":1}"#;
let rt: ToolExecuteRequest = serde_json::from_str(legacy).unwrap();
assert_eq!(rt.request_id, "");
assert_eq!(rt.tool, "x");
}
#[test]
fn tool_cancel_request_roundtrip() {
let c = ToolCancelRequest {
request_id: "cb-3".to_string(),
action_id: "a2".to_string(),
reason: "tool 'drive_cli' callback timed out (185s)".to_string(),
};
let json = serde_json::to_string(&c).unwrap();
let rt: ToolCancelRequest = serde_json::from_str(&json).unwrap();
assert_eq!(rt.request_id, "cb-3");
assert_eq!(rt.action_id, "a2");
assert!(rt.reason.contains("timed out"));
let minimal: ToolCancelRequest =
serde_json::from_str(r#"{"request_id":"cb-9"}"#).unwrap();
assert_eq!(minimal.request_id, "cb-9");
assert_eq!(minimal.action_id, "");
assert_eq!(minimal.reason, "");
}
#[test]
fn tool_execute_response_success() {
let resp = ToolExecuteResponse {
action_id: "a1".to_string(),
output: Some(Value::from("results")),
error: None,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("results"));
}
#[test]
fn tool_execute_response_error() {
let resp = ToolExecuteResponse {
action_id: "a1".to_string(),
output: None,
error: Some("timeout".to_string()),
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("timeout"));
}
#[test]
fn session_init_request() {
let req = SessionInitRequest {
client_id: "client-1".to_string(),
tools: vec![ToolDefinition {
name: "read".to_string(),
description: "Read file".to_string(),
parameters: serde_json::json!({}),
returns: None,
idempotent: false,
cache_ttl_secs: None,
rate_limit: None,
}],
policies: vec![],
};
let json = serde_json::to_string(&req).unwrap();
let rt: SessionInitRequest = serde_json::from_str(&json).unwrap();
assert_eq!(rt.tools.len(), 1);
}
#[test]
fn verify_request() {
let req = VerifyRequest {
proposal: ActionProposal {
id: "p1".to_string(),
source: "test".to_string(),
actions: vec![],
timestamp: chrono::Utc::now(),
context: HashMap::new(),
},
initial_state: [("x".to_string(), Value::from(1))].into(),
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("p1"));
}
#[test]
fn run_start_request_resolves_optional_agent_id() {
let wire = r#"{"intent":"ship the feature","agent_name":"Bulldozer"}"#;
let req: RunStartRequest = serde_json::from_str(wire).unwrap();
assert_eq!(req.intent, "ship the feature");
assert_eq!(req.agent_id, None);
assert_eq!(req.agent_name.as_deref(), Some("Bulldozer"));
assert_eq!(req.outcome_description, None);
}
#[test]
fn run_record_started_ended_roundtrip() {
let started = RunRecord::Started(RunStarted {
run_id: "run-1".to_string(),
agent_id: "agent-1".to_string(),
intent: "do the thing".to_string(),
outcome_description: Some("the thing is done".to_string()),
started_at: chrono::Utc::now(),
});
let json = serde_json::to_string(&started).unwrap();
assert!(json.contains("\"record\":\"started\""));
let rt: RunRecord = serde_json::from_str(&json).unwrap();
match rt {
RunRecord::Started(s) => assert_eq!(s.run_id, "run-1"),
other => panic!("expected Started, got {other:?}"),
}
let ended = RunRecord::Ended(RunEnded {
run_id: "run-1".to_string(),
agent_id: "agent-1".to_string(),
termination: RunTermination::Outcome {
status: car_ir::OutcomeStatus::Success,
outcome: car_ir::AgentOutcome::success("done"),
},
ended_at: chrono::Utc::now(),
});
let json = serde_json::to_string(&ended).unwrap();
assert!(json.contains("\"record\":\"ended\""));
assert!(json.contains("\"kind\":\"outcome\""));
let rt: RunRecord = serde_json::from_str(&json).unwrap();
match rt {
RunRecord::Ended(e) => match e.termination {
RunTermination::Outcome { status, .. } => {
assert_eq!(status, car_ir::OutcomeStatus::Success)
}
other => panic!("expected Outcome, got {other:?}"),
},
other => panic!("expected Ended, got {other:?}"),
}
}
#[test]
fn run_termination_incomplete_serializes_distinctly() {
let term = RunTermination::Incomplete;
let json = serde_json::to_string(&term).unwrap();
assert_eq!(json, r#"{"kind":"incomplete"}"#);
}
#[test]
fn cli_outcome_tagged_variants_roundtrip() {
let exited = CliOutcome::Exited { code: 0 };
let json = serde_json::to_string(&exited).unwrap();
assert_eq!(json, r#"{"kind":"exited","code":0}"#);
assert_eq!(
serde_json::from_str::<CliOutcome>(&json).unwrap(),
exited
);
assert_eq!(
serde_json::to_string(&CliOutcome::Killed).unwrap(),
r#"{"kind":"killed"}"#
);
assert_eq!(
serde_json::to_string(&CliOutcome::Timeout).unwrap(),
r#"{"kind":"timeout"}"#
);
assert_eq!(
serde_json::from_str::<CliOutcome>(r#"{"kind":"timeout"}"#).unwrap(),
CliOutcome::Timeout
);
}
#[test]
fn verifier_verdict_serializes_snake_case() {
assert_eq!(
serde_json::to_string(&VerifierVerdict::Pass).unwrap(),
r#""pass""#
);
assert_eq!(
serde_json::to_string(&VerifierVerdict::Fail).unwrap(),
r#""fail""#
);
assert_eq!(
serde_json::to_string(&VerifierVerdict::NotRun).unwrap(),
r#""not_run""#
);
assert_eq!(
serde_json::from_str::<VerifierVerdict>(r#""not_run""#).unwrap(),
VerifierVerdict::NotRun
);
}
#[test]
fn policy_rejection_omits_none_param() {
let pr = PolicyRejection {
rule: "policy 'x': denied".to_string(),
param: None,
};
let json = serde_json::to_string(&pr).unwrap();
assert!(!json.contains("param"), "None param must be omitted: {json}");
let with_param = PolicyRejection {
rule: "policy 'x': param 'prompt' matches 'rm -rf'".to_string(),
param: Some("prompt".to_string()),
};
let json = serde_json::to_string(&with_param).unwrap();
assert!(json.contains("\"param\":\"prompt\""));
assert_eq!(
serde_json::from_str::<PolicyRejection>(&json).unwrap(),
with_param
);
}
#[test]
fn run_record_turn_variant_roundtrip() {
let turn = RunRecord::Turn(RunTurn {
index: 0,
prompt: Some("make the test pass".to_string()),
tool: Some("drive_cli".to_string()),
parameters: serde_json::json!({ "cli": "claude", "prompt": "make the test pass" }),
output: Some(serde_json::json!({ "exit_code": 0, "output_tail": "done" })),
cli_outcome: Some(CliOutcome::Exited { code: 0 }),
verifier_verdict: VerifierVerdict::NotRun,
policy_rejected: None,
});
let json = serde_json::to_string(&turn).unwrap();
assert!(
json.contains("\"record\":\"turn\""),
"turn must tag on `record`: {json}"
);
match serde_json::from_str::<RunRecord>(&json).unwrap() {
RunRecord::Turn(t) => {
assert_eq!(t.index, 0);
assert_eq!(t.tool.as_deref(), Some("drive_cli"));
assert_eq!(t.cli_outcome, Some(CliOutcome::Exited { code: 0 }));
assert_eq!(t.verifier_verdict, VerifierVerdict::NotRun);
}
other => panic!("expected Turn, got {other:?}"),
}
}
#[test]
fn run_turn_minimal_omits_optional_fields() {
let turn = RunTurn {
index: 3,
prompt: None,
tool: Some("search".to_string()),
parameters: serde_json::json!({ "query": "rust" }),
output: Some(Value::from("results")),
cli_outcome: None,
verifier_verdict: VerifierVerdict::NotRun,
policy_rejected: None,
};
let json = serde_json::to_string(&turn).unwrap();
assert!(!json.contains("prompt"));
assert!(!json.contains("cli_outcome"));
assert!(!json.contains("policy_rejected"));
assert!(json.contains("\"verifier_verdict\":\"not_run\""));
let rt: RunTurn = serde_json::from_str(&json).unwrap();
assert_eq!(rt, turn);
}
#[test]
fn run_live_status_roundtrip() {
assert_eq!(
serde_json::to_string(&RunLiveStatus::InProgress).unwrap(),
"\"in_progress\""
);
assert_eq!(
serde_json::from_str::<RunLiveStatus>("\"completed\"").unwrap(),
RunLiveStatus::Completed
);
assert_eq!(
serde_json::from_str::<RunLiveStatus>("\"incomplete\"").unwrap(),
RunLiveStatus::Incomplete
);
}
#[test]
fn run_trace_event_wraps_record_and_cursor() {
let ev = RunTraceEvent {
run_id: "run-1".to_string(),
agent_id: "agent-a".to_string(),
record: RunRecord::Turn(RunTurn {
index: 4,
prompt: Some("fix it".to_string()),
tool: Some("drive_cli".to_string()),
parameters: serde_json::json!({ "prompt": "fix it" }),
output: Some(serde_json::json!({ "exit_code": 0 })),
cli_outcome: Some(CliOutcome::Exited { code: 0 }),
verifier_verdict: VerifierVerdict::NotRun,
policy_rejected: None,
}),
cursor: 5,
status: RunLiveStatus::InProgress,
};
let json = serde_json::to_string(&ev).unwrap();
let back: RunTraceEvent = serde_json::from_str(&json).unwrap();
assert_eq!(back.run_id, "run-1");
assert_eq!(back.cursor, 5);
assert_eq!(back.status, RunLiveStatus::InProgress);
match back.record {
RunRecord::Turn(t) => assert_eq!(t.index, 4),
other => panic!("expected Turn, got {other:?}"),
}
}
#[test]
fn run_subscribe_response_turns_only_snapshot() {
let resp = RunSubscribeResponse {
run_id: "run-1".to_string(),
agent_id: "agent-a".to_string(),
turns_so_far: vec![RunRecord::Turn(RunTurn {
index: 0,
prompt: None,
tool: Some("drive_cli".to_string()),
parameters: Value::Null,
output: None,
cli_outcome: None,
verifier_verdict: VerifierVerdict::NotRun,
policy_rejected: None,
})],
cursor: 1,
status: RunLiveStatus::InProgress,
};
let json = serde_json::to_string(&resp).unwrap();
let back: RunSubscribeResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.cursor, 1);
assert_eq!(back.turns_so_far.len(), 1);
assert!(matches!(back.turns_so_far[0], RunRecord::Turn(_)));
}
#[test]
fn run_complete_request_accepts_harness_outcome_shape() {
let req_json = serde_json::json!({
"run_id": "r1",
"outcome": {
"status": "success",
"summary": "Created file",
"evidence": [],
"metrics": {
"turns": 3,
"tool_calls": 3,
"actions_succeeded": 3,
"actions_failed": 0
},
"tools_called": ["drive_cli", "check_outcome", "finish"]
}
});
let req: RunCompleteRequest =
serde_json::from_value(req_json).expect("harness outcome shape must deserialize");
assert_eq!(req.run_id, "r1");
assert_eq!(req.outcome.status, car_ir::OutcomeStatus::Success);
assert_eq!(req.outcome.summary, "Created file");
assert_eq!(req.outcome.metrics.turns, 3);
assert_eq!(req.outcome.metrics.tool_calls, 3);
assert_eq!(req.outcome.metrics.actions_succeeded, 3);
assert_eq!(req.outcome.metrics.actions_failed, 0);
assert_eq!(req.outcome.metrics.duration_ms, 0.0);
assert_eq!(req.outcome.metrics.retries, 0);
}
}