use super::ids::{ArtifactId, ExecutionId, ParentLink, StepId, StepType, TenantId, UserId};
use serde::{Deserialize, Serialize};
use svix_ksuid::{Ksuid, KsuidLike};
fn new_event_id() -> String {
format!("evt_{}", Ksuid::new(None, None))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionEventType {
ExecutionStart,
ExecutionEnd,
ExecutionFailed,
ExecutionCancelled,
StepStart,
StepEnd,
StepFailed,
StepDiscovered,
ArtifactCreated,
StateSnapshot,
DecisionMade,
ControlPause,
ControlResume,
ControlCancel,
InboxMessage,
ToolCallStart,
ToolCallEnd,
CheckpointSaved,
GoalEvaluated,
LlmCallStart,
LlmCallEnd,
LlmCallFailed,
TokenUsageRecorded,
MemoryRecalled,
MemoryStored,
GuardrailEvaluated,
ReasoningTrace,
ContextSnapshot,
FeedbackReceived,
}
impl ExecutionEventType {
pub fn as_str(&self) -> &'static str {
match self {
Self::ExecutionStart => "execution.start",
Self::ExecutionEnd => "execution.end",
Self::ExecutionFailed => "execution.failed",
Self::ExecutionCancelled => "execution.cancelled",
Self::StepStart => "step.start",
Self::StepEnd => "step.end",
Self::StepFailed => "step.failed",
Self::StepDiscovered => "step.discovered",
Self::ArtifactCreated => "artifact.created",
Self::StateSnapshot => "state.snapshot",
Self::DecisionMade => "decision.made",
Self::ControlPause => "control.pause",
Self::ControlResume => "control.resume",
Self::ControlCancel => "control.cancel",
Self::InboxMessage => "inbox.message",
Self::ToolCallStart => "tool.start",
Self::ToolCallEnd => "tool.end",
Self::CheckpointSaved => "state.checkpoint",
Self::GoalEvaluated => "goal.evaluated",
Self::LlmCallStart => "llm.call.start",
Self::LlmCallEnd => "llm.call.end",
Self::LlmCallFailed => "llm.call.failed",
Self::TokenUsageRecorded => "token.usage",
Self::MemoryRecalled => "memory.recalled",
Self::MemoryStored => "memory.stored",
Self::GuardrailEvaluated => "guardrail.evaluated",
Self::ReasoningTrace => "reasoning.trace",
Self::ContextSnapshot => "context.snapshot",
Self::FeedbackReceived => "feedback.received",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionContext {
pub execution_id: ExecutionId,
pub step_id: Option<StepId>,
pub artifact_id: Option<ArtifactId>,
pub parent: Option<ParentLink>,
pub tenant_id: Option<TenantId>,
pub user_id: Option<UserId>,
}
impl ExecutionContext {
pub fn new(execution_id: ExecutionId) -> Self {
Self {
execution_id,
step_id: None,
artifact_id: None,
parent: None,
tenant_id: None,
user_id: None,
}
}
pub fn with_step(mut self, step_id: StepId) -> Self {
self.step_id = Some(step_id);
self
}
pub fn with_artifact(mut self, artifact_id: ArtifactId) -> Self {
self.artifact_id = Some(artifact_id);
self
}
pub fn with_parent(mut self, parent: ParentLink) -> Self {
self.parent = Some(parent);
self
}
pub fn with_tenant(mut self, tenant_id: TenantId, user_id: Option<UserId>) -> Self {
self.tenant_id = Some(tenant_id);
self.user_id = user_id;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionEvent {
pub event_id: String,
pub event_type: ExecutionEventType,
pub context: ExecutionContext,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub duration_ms: Option<u64>,
pub payload: Option<serde_json::Value>,
}
impl ExecutionEvent {
pub fn new(event_type: ExecutionEventType, context: ExecutionContext) -> Self {
Self {
event_id: new_event_id(),
event_type,
context,
timestamp: chrono::Utc::now(),
duration_ms: None,
payload: None,
}
}
pub fn with_duration(mut self, ms: u64) -> Self {
self.duration_ms = Some(ms);
self
}
pub fn with_payload(mut self, payload: serde_json::Value) -> Self {
self.payload = Some(payload);
self
}
pub fn execution_start(execution_id: ExecutionId, parent: Option<ParentLink>) -> Self {
let mut ctx = ExecutionContext::new(execution_id);
if let Some(p) = parent {
ctx = ctx.with_parent(p);
}
Self::new(ExecutionEventType::ExecutionStart, ctx)
}
pub fn execution_end(execution_id: ExecutionId, duration_ms: Option<u64>) -> Self {
let ctx = ExecutionContext::new(execution_id);
let mut event = Self::new(ExecutionEventType::ExecutionEnd, ctx);
event.duration_ms = duration_ms;
event
}
pub fn step_start(
execution_id: ExecutionId,
step_id: StepId,
step_type: StepType,
name: &str,
) -> Self {
let ctx = ExecutionContext::new(execution_id).with_step(step_id);
Self::new(ExecutionEventType::StepStart, ctx).with_payload(serde_json::json!({
"step_type": step_type.to_string(),
"name": name,
}))
}
pub fn step_end(execution_id: ExecutionId, step_id: StepId, duration_ms: u64) -> Self {
let ctx = ExecutionContext::new(execution_id).with_step(step_id);
Self::new(ExecutionEventType::StepEnd, ctx).with_duration(duration_ms)
}
pub fn artifact_created(
execution_id: ExecutionId,
step_id: StepId,
artifact_id: ArtifactId,
artifact_type: &str,
) -> Self {
let ctx = ExecutionContext::new(execution_id)
.with_step(step_id)
.with_artifact(artifact_id);
Self::new(ExecutionEventType::ArtifactCreated, ctx).with_payload(serde_json::json!({
"artifact_type": artifact_type,
}))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionType {
Routing,
Branch,
Approval,
Escalation,
Retry,
Fallback,
PolicyEvaluation,
ToolSelection,
Rejection,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionInput {
pub facts: Vec<String>,
pub constraints: Vec<String>,
pub evidence_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionAlternative {
pub option: String,
pub score: f64,
pub rejected_reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelContext {
pub provider: String,
pub model: String,
pub version: Option<String>,
pub temperature: Option<f64>,
pub max_tokens: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionRecord {
pub decision_id: String,
pub execution_id: ExecutionId,
pub step_id: Option<StepId>,
pub decision_type: DecisionType,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub outcome: String,
pub confidence: f64,
pub inputs: DecisionInput,
pub alternatives: Option<Vec<DecisionAlternative>>,
pub model_context: Option<ModelContext>,
pub reasoning: Option<String>,
}
impl DecisionRecord {
pub fn new(
decision_type: DecisionType,
execution_id: ExecutionId,
outcome: impl Into<String>,
confidence: f64,
inputs: DecisionInput,
) -> Self {
Self {
decision_id: format!("dec_{}", Ksuid::new(None, None)),
execution_id,
step_id: None,
decision_type,
timestamp: chrono::Utc::now(),
outcome: outcome.into(),
confidence,
inputs,
alternatives: None,
model_context: None,
reasoning: None,
}
}
pub fn with_step(mut self, step_id: StepId) -> Self {
self.step_id = Some(step_id);
self
}
pub fn with_alternatives(mut self, alternatives: Vec<DecisionAlternative>) -> Self {
self.alternatives = Some(alternatives);
self
}
pub fn with_model_context(mut self, model_context: ModelContext) -> Self {
self.model_context = Some(model_context);
self
}
pub fn with_reasoning(mut self, reasoning: impl Into<String>) -> Self {
self.reasoning = Some(reasoning.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ControlActor {
System,
User,
Agent,
PolicyEngine,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ControlAction {
Stop,
Pause,
Resume,
Cancel,
Approve,
Deny,
Escalate,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ControlOutcome {
Allowed,
Denied,
Modified,
Escalated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ControlEvent {
pub event_id: String,
pub execution_id: ExecutionId,
pub step_id: Option<StepId>,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub actor: ControlActor,
pub action: ControlAction,
pub reason: String,
pub outcome: ControlOutcome,
pub actor_id: Option<String>,
}
impl ControlEvent {
pub fn new(
actor: ControlActor,
action: ControlAction,
execution_id: ExecutionId,
reason: impl Into<String>,
outcome: ControlOutcome,
) -> Self {
Self {
event_id: format!("ctrl_{}", Ksuid::new(None, None)),
execution_id,
step_id: None,
timestamp: chrono::Utc::now(),
actor,
action,
reason: reason.into(),
outcome,
actor_id: None,
}
}
pub fn with_step(mut self, step_id: StepId) -> Self {
self.step_id = Some(step_id);
self
}
pub fn with_actor_id(mut self, actor_id: impl Into<String>) -> Self {
self.actor_id = Some(actor_id.into());
self
}
pub fn pause(
execution_id: ExecutionId,
actor: ControlActor,
reason: impl Into<String>,
) -> Self {
Self::new(
actor,
ControlAction::Pause,
execution_id,
reason,
ControlOutcome::Allowed,
)
}
pub fn resume(
execution_id: ExecutionId,
actor: ControlActor,
reason: impl Into<String>,
) -> Self {
Self::new(
actor,
ControlAction::Resume,
execution_id,
reason,
ControlOutcome::Allowed,
)
}
pub fn cancel(
execution_id: ExecutionId,
actor: ControlActor,
reason: impl Into<String>,
) -> Self {
Self::new(
actor,
ControlAction::Cancel,
execution_id,
reason,
ControlOutcome::Allowed,
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub id: String,
pub run_id: ExecutionId,
pub node_id: Option<StepId>,
pub author: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub content: Option<String>,
pub is_final: bool,
}
impl Event {
pub fn new(run_id: ExecutionId, author: impl Into<String>) -> Self {
Self {
id: new_event_id(),
run_id,
node_id: None,
author: author.into(),
timestamp: chrono::Utc::now(),
content: None,
is_final: false,
}
}
pub fn with_content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
pub fn with_node(mut self, node_id: StepId) -> Self {
self.node_id = Some(node_id);
self
}
pub fn final_response(mut self) -> Self {
self.is_final = true;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_execution_event_type_as_str_execution_start() {
assert_eq!(
ExecutionEventType::ExecutionStart.as_str(),
"execution.start"
);
}
#[test]
fn test_execution_event_type_as_str_execution_end() {
assert_eq!(ExecutionEventType::ExecutionEnd.as_str(), "execution.end");
}
#[test]
fn test_execution_event_type_as_str_execution_failed() {
assert_eq!(
ExecutionEventType::ExecutionFailed.as_str(),
"execution.failed"
);
}
#[test]
fn test_execution_event_type_as_str_execution_cancelled() {
assert_eq!(
ExecutionEventType::ExecutionCancelled.as_str(),
"execution.cancelled"
);
}
#[test]
fn test_execution_event_type_as_str_step_start() {
assert_eq!(ExecutionEventType::StepStart.as_str(), "step.start");
}
#[test]
fn test_execution_event_type_as_str_step_end() {
assert_eq!(ExecutionEventType::StepEnd.as_str(), "step.end");
}
#[test]
fn test_execution_event_type_as_str_step_failed() {
assert_eq!(ExecutionEventType::StepFailed.as_str(), "step.failed");
}
#[test]
fn test_execution_event_type_as_str_artifact_created() {
assert_eq!(
ExecutionEventType::ArtifactCreated.as_str(),
"artifact.created"
);
}
#[test]
fn test_execution_event_type_as_str_state_snapshot() {
assert_eq!(ExecutionEventType::StateSnapshot.as_str(), "state.snapshot");
}
#[test]
fn test_execution_event_type_as_str_decision_made() {
assert_eq!(ExecutionEventType::DecisionMade.as_str(), "decision.made");
}
#[test]
fn test_execution_event_type_as_str_control_pause() {
assert_eq!(ExecutionEventType::ControlPause.as_str(), "control.pause");
}
#[test]
fn test_execution_event_type_as_str_control_resume() {
assert_eq!(ExecutionEventType::ControlResume.as_str(), "control.resume");
}
#[test]
fn test_execution_event_type_as_str_control_cancel() {
assert_eq!(ExecutionEventType::ControlCancel.as_str(), "control.cancel");
}
#[test]
fn test_execution_event_type_serde() {
let event_type = ExecutionEventType::ExecutionStart;
let json = serde_json::to_string(&event_type).unwrap();
let parsed: ExecutionEventType = serde_json::from_str(&json).unwrap();
assert_eq!(event_type, parsed);
}
#[test]
fn test_execution_event_type_equality() {
assert_eq!(ExecutionEventType::StepStart, ExecutionEventType::StepStart);
assert_ne!(ExecutionEventType::StepStart, ExecutionEventType::StepEnd);
}
#[test]
fn test_execution_context_new() {
let exec_id = ExecutionId::from_string("exec_test");
let ctx = ExecutionContext::new(exec_id.clone());
assert_eq!(ctx.execution_id.as_str(), "exec_test");
assert!(ctx.step_id.is_none());
assert!(ctx.artifact_id.is_none());
assert!(ctx.parent.is_none());
assert!(ctx.tenant_id.is_none());
assert!(ctx.user_id.is_none());
}
#[test]
fn test_execution_context_with_step() {
let exec_id = ExecutionId::from_string("exec_test");
let step_id = StepId::from_string("step_test");
let ctx = ExecutionContext::new(exec_id).with_step(step_id.clone());
assert!(ctx.step_id.is_some());
assert_eq!(ctx.step_id.unwrap().as_str(), "step_test");
}
#[test]
fn test_execution_context_with_artifact() {
let exec_id = ExecutionId::from_string("exec_test");
let artifact_id = ArtifactId::from_string("artifact_test");
let ctx = ExecutionContext::new(exec_id).with_artifact(artifact_id);
assert!(ctx.artifact_id.is_some());
assert_eq!(ctx.artifact_id.unwrap().as_str(), "artifact_test");
}
#[test]
fn test_execution_context_with_parent() {
let exec_id = ExecutionId::from_string("exec_test");
let parent = ParentLink::from_user_message("msg_123");
let ctx = ExecutionContext::new(exec_id).with_parent(parent);
assert!(ctx.parent.is_some());
assert_eq!(ctx.parent.unwrap().parent_id, "msg_123");
}
#[test]
fn test_execution_context_with_tenant() {
let exec_id = ExecutionId::from_string("exec_test");
let tenant_id = TenantId::from_string("tenant_test");
let user_id = UserId::from_string("user_test");
let ctx = ExecutionContext::new(exec_id).with_tenant(tenant_id, Some(user_id));
assert!(ctx.tenant_id.is_some());
assert!(ctx.user_id.is_some());
assert_eq!(ctx.tenant_id.unwrap().as_str(), "tenant_test");
assert_eq!(ctx.user_id.unwrap().as_str(), "user_test");
}
#[test]
fn test_execution_context_builder_chain() {
let exec_id = ExecutionId::from_string("exec_chain");
let step_id = StepId::from_string("step_chain");
let artifact_id = ArtifactId::from_string("artifact_chain");
let parent = ParentLink::system();
let tenant_id = TenantId::from_string("tenant_chain");
let ctx = ExecutionContext::new(exec_id)
.with_step(step_id)
.with_artifact(artifact_id)
.with_parent(parent)
.with_tenant(tenant_id, None);
assert!(ctx.step_id.is_some());
assert!(ctx.artifact_id.is_some());
assert!(ctx.parent.is_some());
assert!(ctx.tenant_id.is_some());
assert!(ctx.user_id.is_none());
}
#[test]
fn test_execution_context_serde() {
let exec_id = ExecutionId::from_string("exec_serde");
let ctx = ExecutionContext::new(exec_id);
let json = serde_json::to_string(&ctx).unwrap();
let parsed: ExecutionContext = serde_json::from_str(&json).unwrap();
assert_eq!(ctx.execution_id.as_str(), parsed.execution_id.as_str());
}
#[test]
fn test_execution_event_new() {
let ctx = ExecutionContext::new(ExecutionId::from_string("exec_test"));
let event = ExecutionEvent::new(ExecutionEventType::ExecutionStart, ctx);
assert!(event.event_id.starts_with("evt_"));
assert_eq!(event.event_type, ExecutionEventType::ExecutionStart);
assert!(event.duration_ms.is_none());
assert!(event.payload.is_none());
}
#[test]
fn test_execution_event_with_duration() {
let ctx = ExecutionContext::new(ExecutionId::from_string("exec_test"));
let event = ExecutionEvent::new(ExecutionEventType::StepEnd, ctx).with_duration(1500);
assert_eq!(event.duration_ms, Some(1500));
}
#[test]
fn test_execution_event_with_payload() {
let ctx = ExecutionContext::new(ExecutionId::from_string("exec_test"));
let payload = serde_json::json!({"key": "value"});
let event =
ExecutionEvent::new(ExecutionEventType::StepStart, ctx).with_payload(payload.clone());
assert!(event.payload.is_some());
assert_eq!(event.payload.unwrap(), payload);
}
#[test]
fn test_execution_event_execution_start() {
let exec_id = ExecutionId::from_string("exec_start");
let event = ExecutionEvent::execution_start(exec_id, None);
assert_eq!(event.event_type, ExecutionEventType::ExecutionStart);
assert!(event.context.parent.is_none());
}
#[test]
fn test_execution_event_execution_start_with_parent() {
let exec_id = ExecutionId::from_string("exec_start");
let parent = ParentLink::from_user_message("msg_trigger");
let event = ExecutionEvent::execution_start(exec_id, Some(parent));
assert_eq!(event.event_type, ExecutionEventType::ExecutionStart);
assert!(event.context.parent.is_some());
}
#[test]
fn test_execution_event_execution_end() {
let exec_id = ExecutionId::from_string("exec_end");
let event = ExecutionEvent::execution_end(exec_id, Some(5000));
assert_eq!(event.event_type, ExecutionEventType::ExecutionEnd);
assert_eq!(event.duration_ms, Some(5000));
}
#[test]
fn test_execution_event_step_start() {
let exec_id = ExecutionId::from_string("exec_step");
let step_id = StepId::from_string("step_start");
let event = ExecutionEvent::step_start(exec_id, step_id, StepType::LlmNode, "test_step");
assert_eq!(event.event_type, ExecutionEventType::StepStart);
assert!(event.payload.is_some());
let payload = event.payload.unwrap();
assert_eq!(payload["name"], "test_step");
}
#[test]
fn test_execution_event_step_end() {
let exec_id = ExecutionId::from_string("exec_step");
let step_id = StepId::from_string("step_end");
let event = ExecutionEvent::step_end(exec_id, step_id, 1000);
assert_eq!(event.event_type, ExecutionEventType::StepEnd);
assert_eq!(event.duration_ms, Some(1000));
}
#[test]
fn test_execution_event_artifact_created() {
let exec_id = ExecutionId::from_string("exec_artifact");
let step_id = StepId::from_string("step_artifact");
let artifact_id = ArtifactId::from_string("artifact_created");
let event = ExecutionEvent::artifact_created(exec_id, step_id, artifact_id, "code");
assert_eq!(event.event_type, ExecutionEventType::ArtifactCreated);
assert!(event.context.artifact_id.is_some());
assert!(event.payload.is_some());
assert_eq!(event.payload.unwrap()["artifact_type"], "code");
}
#[test]
fn test_execution_event_serde() {
let ctx = ExecutionContext::new(ExecutionId::from_string("exec_serde"));
let event = ExecutionEvent::new(ExecutionEventType::ExecutionStart, ctx);
let json = serde_json::to_string(&event).unwrap();
let parsed: ExecutionEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event.event_type, parsed.event_type);
}
#[test]
fn test_decision_type_variants() {
let variants = vec![
DecisionType::Routing,
DecisionType::Branch,
DecisionType::Approval,
DecisionType::Escalation,
DecisionType::Retry,
DecisionType::Fallback,
DecisionType::PolicyEvaluation,
DecisionType::ToolSelection,
DecisionType::Rejection,
];
for variant in variants {
let json = serde_json::to_string(&variant).unwrap();
let parsed: DecisionType = serde_json::from_str(&json).unwrap();
assert_eq!(variant, parsed);
}
}
#[test]
fn test_decision_type_equality() {
assert_eq!(DecisionType::Routing, DecisionType::Routing);
assert_ne!(DecisionType::Routing, DecisionType::Branch);
}
#[test]
fn test_decision_input_creation() {
let input = DecisionInput {
facts: vec!["fact1".to_string(), "fact2".to_string()],
constraints: vec!["constraint1".to_string()],
evidence_ids: vec!["ev_123".to_string()],
};
assert_eq!(input.facts.len(), 2);
assert_eq!(input.constraints.len(), 1);
assert_eq!(input.evidence_ids.len(), 1);
}
#[test]
fn test_decision_input_serde() {
let input = DecisionInput {
facts: vec!["fact1".to_string()],
constraints: vec![],
evidence_ids: vec![],
};
let json = serde_json::to_string(&input).unwrap();
let parsed: DecisionInput = serde_json::from_str(&json).unwrap();
assert_eq!(input.facts, parsed.facts);
}
#[test]
fn test_decision_alternative_creation() {
let alt = DecisionAlternative {
option: "option_b".to_string(),
score: 0.75,
rejected_reason: "Lower confidence".to_string(),
};
assert_eq!(alt.option, "option_b");
assert_eq!(alt.score, 0.75);
}
#[test]
fn test_decision_alternative_serde() {
let alt = DecisionAlternative {
option: "alt".to_string(),
score: 0.5,
rejected_reason: "reason".to_string(),
};
let json = serde_json::to_string(&alt).unwrap();
let parsed: DecisionAlternative = serde_json::from_str(&json).unwrap();
assert_eq!(alt.option, parsed.option);
assert_eq!(alt.score, parsed.score);
}
#[test]
fn test_model_context_creation() {
let ctx = ModelContext {
provider: "anthropic".to_string(),
model: "claude-3-opus".to_string(),
version: Some("2024".to_string()),
temperature: Some(0.7),
max_tokens: Some(1000),
};
assert_eq!(ctx.provider, "anthropic");
assert_eq!(ctx.model, "claude-3-opus");
assert!(ctx.temperature.is_some());
}
#[test]
fn test_model_context_minimal() {
let ctx = ModelContext {
provider: "openai".to_string(),
model: "gpt-4".to_string(),
version: None,
temperature: None,
max_tokens: None,
};
assert!(ctx.version.is_none());
assert!(ctx.temperature.is_none());
}
#[test]
fn test_model_context_serde() {
let ctx = ModelContext {
provider: "test".to_string(),
model: "test-model".to_string(),
version: None,
temperature: Some(0.5),
max_tokens: None,
};
let json = serde_json::to_string(&ctx).unwrap();
let parsed: ModelContext = serde_json::from_str(&json).unwrap();
assert_eq!(ctx.provider, parsed.provider);
assert_eq!(ctx.temperature, parsed.temperature);
}
#[test]
fn test_decision_record_new() {
let exec_id = ExecutionId::from_string("exec_decision");
let input = DecisionInput {
facts: vec!["fact".to_string()],
constraints: vec![],
evidence_ids: vec![],
};
let record = DecisionRecord::new(DecisionType::Routing, exec_id, "agent_a", 0.95, input);
assert!(record.decision_id.starts_with("dec_"));
assert_eq!(record.decision_type, DecisionType::Routing);
assert_eq!(record.outcome, "agent_a");
assert_eq!(record.confidence, 0.95);
}
#[test]
fn test_decision_record_with_step() {
let exec_id = ExecutionId::from_string("exec_decision");
let step_id = StepId::from_string("step_decision");
let input = DecisionInput {
facts: vec![],
constraints: vec![],
evidence_ids: vec![],
};
let record = DecisionRecord::new(DecisionType::Branch, exec_id, "yes", 0.8, input)
.with_step(step_id);
assert!(record.step_id.is_some());
}
#[test]
fn test_decision_record_with_alternatives() {
let exec_id = ExecutionId::from_string("exec_decision");
let input = DecisionInput {
facts: vec![],
constraints: vec![],
evidence_ids: vec![],
};
let alternatives = vec![DecisionAlternative {
option: "option_b".to_string(),
score: 0.6,
rejected_reason: "Lower score".to_string(),
}];
let record = DecisionRecord::new(DecisionType::Routing, exec_id, "option_a", 0.9, input)
.with_alternatives(alternatives);
assert!(record.alternatives.is_some());
assert_eq!(record.alternatives.unwrap().len(), 1);
}
#[test]
fn test_decision_record_with_model_context() {
let exec_id = ExecutionId::from_string("exec_decision");
let input = DecisionInput {
facts: vec![],
constraints: vec![],
evidence_ids: vec![],
};
let model_ctx = ModelContext {
provider: "anthropic".to_string(),
model: "claude".to_string(),
version: None,
temperature: None,
max_tokens: None,
};
let record = DecisionRecord::new(DecisionType::Approval, exec_id, "approved", 1.0, input)
.with_model_context(model_ctx);
assert!(record.model_context.is_some());
}
#[test]
fn test_decision_record_with_reasoning() {
let exec_id = ExecutionId::from_string("exec_decision");
let input = DecisionInput {
facts: vec![],
constraints: vec![],
evidence_ids: vec![],
};
let record =
DecisionRecord::new(DecisionType::Escalation, exec_id, "escalated", 0.5, input)
.with_reasoning("Confidence too low for autonomous action");
assert!(record.reasoning.is_some());
assert!(record.reasoning.unwrap().contains("Confidence"));
}
#[test]
fn test_decision_record_serde() {
let exec_id = ExecutionId::from_string("exec_serde");
let input = DecisionInput {
facts: vec!["f".to_string()],
constraints: vec![],
evidence_ids: vec![],
};
let record = DecisionRecord::new(DecisionType::Retry, exec_id, "retry", 0.7, input);
let json = serde_json::to_string(&record).unwrap();
let parsed: DecisionRecord = serde_json::from_str(&json).unwrap();
assert_eq!(record.decision_type, parsed.decision_type);
}
#[test]
fn test_control_actor_variants() {
let variants = vec![
ControlActor::System,
ControlActor::User,
ControlActor::Agent,
ControlActor::PolicyEngine,
];
for variant in variants {
let json = serde_json::to_string(&variant).unwrap();
let parsed: ControlActor = serde_json::from_str(&json).unwrap();
assert_eq!(variant, parsed);
}
}
#[test]
fn test_control_actor_equality() {
assert_eq!(ControlActor::System, ControlActor::System);
assert_ne!(ControlActor::System, ControlActor::User);
}
#[test]
fn test_control_action_variants() {
let variants = vec![
ControlAction::Stop,
ControlAction::Pause,
ControlAction::Resume,
ControlAction::Cancel,
ControlAction::Approve,
ControlAction::Deny,
ControlAction::Escalate,
];
for variant in variants {
let json = serde_json::to_string(&variant).unwrap();
let parsed: ControlAction = serde_json::from_str(&json).unwrap();
assert_eq!(variant, parsed);
}
}
#[test]
fn test_control_outcome_variants() {
let variants = vec![
ControlOutcome::Allowed,
ControlOutcome::Denied,
ControlOutcome::Modified,
ControlOutcome::Escalated,
];
for variant in variants {
let json = serde_json::to_string(&variant).unwrap();
let parsed: ControlOutcome = serde_json::from_str(&json).unwrap();
assert_eq!(variant, parsed);
}
}
#[test]
fn test_control_event_new() {
let exec_id = ExecutionId::from_string("exec_ctrl");
let event = ControlEvent::new(
ControlActor::User,
ControlAction::Pause,
exec_id,
"User requested pause",
ControlOutcome::Allowed,
);
assert!(event.event_id.starts_with("ctrl_"));
assert_eq!(event.actor, ControlActor::User);
assert_eq!(event.action, ControlAction::Pause);
assert_eq!(event.outcome, ControlOutcome::Allowed);
}
#[test]
fn test_control_event_with_step() {
let exec_id = ExecutionId::from_string("exec_ctrl");
let step_id = StepId::from_string("step_ctrl");
let event = ControlEvent::new(
ControlActor::System,
ControlAction::Cancel,
exec_id,
"Timeout",
ControlOutcome::Allowed,
)
.with_step(step_id);
assert!(event.step_id.is_some());
}
#[test]
fn test_control_event_with_actor_id() {
let exec_id = ExecutionId::from_string("exec_ctrl");
let event = ControlEvent::new(
ControlActor::User,
ControlAction::Approve,
exec_id,
"Approved by admin",
ControlOutcome::Allowed,
)
.with_actor_id("user_admin_123");
assert!(event.actor_id.is_some());
assert_eq!(event.actor_id.unwrap(), "user_admin_123");
}
#[test]
fn test_control_event_pause_factory() {
let exec_id = ExecutionId::from_string("exec_pause");
let event = ControlEvent::pause(exec_id, ControlActor::User, "Pausing for review");
assert_eq!(event.action, ControlAction::Pause);
assert_eq!(event.actor, ControlActor::User);
assert_eq!(event.outcome, ControlOutcome::Allowed);
}
#[test]
fn test_control_event_resume_factory() {
let exec_id = ExecutionId::from_string("exec_resume");
let event = ControlEvent::resume(exec_id, ControlActor::User, "Resuming after review");
assert_eq!(event.action, ControlAction::Resume);
}
#[test]
fn test_control_event_cancel_factory() {
let exec_id = ExecutionId::from_string("exec_cancel");
let event = ControlEvent::cancel(exec_id, ControlActor::System, "System timeout");
assert_eq!(event.action, ControlAction::Cancel);
assert_eq!(event.actor, ControlActor::System);
}
#[test]
fn test_control_event_serde() {
let exec_id = ExecutionId::from_string("exec_serde");
let event = ControlEvent::pause(exec_id, ControlActor::Agent, "Self-regulation");
let json = serde_json::to_string(&event).unwrap();
let parsed: ControlEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event.action, parsed.action);
assert_eq!(event.actor, parsed.actor);
}
#[test]
fn test_event_new() {
let run_id = ExecutionId::from_string("exec_legacy");
let event = Event::new(run_id, "test_author");
assert!(event.id.starts_with("evt_"));
assert_eq!(event.author, "test_author");
assert!(!event.is_final);
assert!(event.content.is_none());
assert!(event.node_id.is_none());
}
#[test]
fn test_event_with_content() {
let run_id = ExecutionId::from_string("exec_legacy");
let event = Event::new(run_id, "author").with_content("Hello, world!");
assert!(event.content.is_some());
assert_eq!(event.content.unwrap(), "Hello, world!");
}
#[test]
fn test_event_with_node() {
let run_id = ExecutionId::from_string("exec_legacy");
let node_id = StepId::from_string("step_legacy");
let event = Event::new(run_id, "author").with_node(node_id.clone());
assert!(event.node_id.is_some());
assert_eq!(event.node_id.unwrap().as_str(), "step_legacy");
}
#[test]
fn test_event_final_response() {
let run_id = ExecutionId::from_string("exec_legacy");
let event = Event::new(run_id, "author").final_response();
assert!(event.is_final);
}
#[test]
fn test_event_builder_chain() {
let run_id = ExecutionId::from_string("exec_chain");
let node_id = StepId::from_string("step_chain");
let event = Event::new(run_id, "test")
.with_content("Content here")
.with_node(node_id)
.final_response();
assert!(event.content.is_some());
assert!(event.node_id.is_some());
assert!(event.is_final);
}
#[test]
fn test_event_serde() {
let run_id = ExecutionId::from_string("exec_serde");
let event = Event::new(run_id, "author").with_content("test");
let json = serde_json::to_string(&event).unwrap();
let parsed: Event = serde_json::from_str(&json).unwrap();
assert_eq!(event.author, parsed.author);
}
#[test]
fn test_event_id_format() {
let ctx = ExecutionContext::new(ExecutionId::from_string("exec_test"));
let event = ExecutionEvent::new(ExecutionEventType::ExecutionStart, ctx);
assert!(event.event_id.starts_with("evt_"));
assert_eq!(event.event_id.len(), 31);
}
#[test]
fn test_decision_id_format() {
let exec_id = ExecutionId::from_string("exec_test");
let input = DecisionInput {
facts: vec![],
constraints: vec![],
evidence_ids: vec![],
};
let record = DecisionRecord::new(DecisionType::Routing, exec_id, "outcome", 0.9, input);
assert!(record.decision_id.starts_with("dec_"));
}
#[test]
fn test_control_event_id_format() {
let exec_id = ExecutionId::from_string("exec_test");
let event = ControlEvent::pause(exec_id, ControlActor::User, "test");
assert!(event.event_id.starts_with("ctrl_"));
}
}