use super::tenant::TenantContext;
use super::trace::TraceContext;
use crate::kernel::{CancellationPolicy, ExecutionId, ParentLink, ParentType, SpawnMode, StepId};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionContext {
pub session_id: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub last_active_at: Option<chrono::DateTime<chrono::Utc>>,
pub metadata: HashMap<String, serde_json::Value>,
}
impl SessionContext {
pub fn new(session_id: impl Into<String>) -> Self {
Self {
session_id: session_id.into(),
created_at: chrono::Utc::now(),
last_active_at: None,
metadata: HashMap::new(),
}
}
pub fn touch(&mut self) {
self.last_active_at = Some(chrono::Utc::now());
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeContext {
pub execution_id: ExecutionId,
pub step_id: Option<StepId>,
pub parent: ParentLink,
pub tenant: TenantContext,
pub trace: TraceContext,
pub session: Option<SessionContext>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub spawn_mode: Option<SpawnMode>,
pub cancellation_policy: CancellationPolicy,
pub parent_execution_id: Option<ExecutionId>,
pub metadata: HashMap<String, serde_json::Value>,
}
impl RuntimeContext {
pub fn new(execution_id: ExecutionId, parent: ParentLink, tenant: TenantContext) -> Self {
Self {
execution_id,
step_id: None,
parent,
tenant,
trace: TraceContext::new(),
session: None,
created_at: chrono::Utc::now(),
spawn_mode: None,
cancellation_policy: CancellationPolicy::default(),
parent_execution_id: None,
metadata: HashMap::new(),
}
}
pub fn from_user_message(
execution_id: ExecutionId,
message_id: impl Into<String>,
tenant: TenantContext,
) -> Self {
Self::new(
execution_id,
ParentLink::from_user_message(message_id),
tenant,
)
}
pub fn with_step(mut self, step_id: StepId) -> Self {
self.step_id = Some(step_id);
self
}
pub fn with_trace(mut self, trace: TraceContext) -> Self {
self.trace = trace;
self
}
pub fn with_session(mut self, session: SessionContext) -> Self {
self.session = Some(session);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn with_spawn_mode(mut self, spawn_mode: SpawnMode) -> Self {
self.spawn_mode = Some(spawn_mode);
self
}
pub fn with_cancellation_policy(mut self, policy: CancellationPolicy) -> Self {
self.cancellation_policy = policy;
self
}
pub fn child_context(&self, child_execution_id: ExecutionId, parent_step_id: &StepId) -> Self {
self.child_context_with_spawn_mode(
child_execution_id,
parent_step_id,
SpawnMode::child(false, false),
)
}
pub fn child_context_with_spawn_mode(
&self,
child_execution_id: ExecutionId,
parent_step_id: &StepId,
spawn_mode: SpawnMode,
) -> Self {
Self {
execution_id: child_execution_id,
step_id: None,
parent: ParentLink::from_step(parent_step_id),
tenant: self.tenant.child_context(None),
trace: self.trace.child_span(),
session: self.session.clone(),
created_at: chrono::Utc::now(),
spawn_mode: Some(spawn_mode),
cancellation_policy: CancellationPolicy::default(),
parent_execution_id: Some(self.execution_id.clone()),
metadata: HashMap::new(), }
}
pub fn enter_step(&self, step_id: StepId) -> Self {
let mut ctx = self.clone();
ctx.step_id = Some(step_id);
ctx.trace = ctx.trace.child_span();
ctx
}
pub fn execution_id(&self) -> &ExecutionId {
&self.execution_id
}
pub fn step_id(&self) -> Option<&StepId> {
self.step_id.as_ref()
}
pub fn tenant(&self) -> &TenantContext {
&self.tenant
}
pub fn trace_id(&self) -> &str {
&self.trace.trace_id
}
pub fn span_id(&self) -> &str {
&self.trace.span_id
}
pub fn is_root(&self) -> bool {
!matches!(self.parent.parent_type, ParentType::StepExecution)
}
}
pub struct RuntimeContextBuilder {
execution_id: ExecutionId,
parent: ParentLink,
tenant: TenantContext,
trace: TraceContext,
session: Option<SessionContext>,
spawn_mode: Option<SpawnMode>,
cancellation_policy: CancellationPolicy,
parent_execution_id: Option<ExecutionId>,
metadata: HashMap<String, serde_json::Value>,
}
impl RuntimeContextBuilder {
pub fn new(execution_id: ExecutionId, parent: ParentLink, tenant: TenantContext) -> Self {
Self {
execution_id,
parent,
tenant,
trace: TraceContext::new(),
session: None,
spawn_mode: None,
cancellation_policy: CancellationPolicy::default(),
parent_execution_id: None,
metadata: HashMap::new(),
}
}
pub fn trace(mut self, trace: TraceContext) -> Self {
self.trace = trace;
self
}
pub fn session(mut self, session: SessionContext) -> Self {
self.session = Some(session);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn spawn_mode(mut self, spawn_mode: SpawnMode) -> Self {
self.spawn_mode = Some(spawn_mode);
self
}
pub fn cancellation_policy(mut self, policy: CancellationPolicy) -> Self {
self.cancellation_policy = policy;
self
}
pub fn parent_execution_id(mut self, parent_exec_id: ExecutionId) -> Self {
self.parent_execution_id = Some(parent_exec_id);
self
}
pub fn build(self) -> RuntimeContext {
RuntimeContext {
execution_id: self.execution_id,
step_id: None,
parent: self.parent,
tenant: self.tenant,
trace: self.trace,
session: self.session,
created_at: chrono::Utc::now(),
spawn_mode: self.spawn_mode,
cancellation_policy: self.cancellation_policy,
parent_execution_id: self.parent_execution_id,
metadata: self.metadata,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::{TenantId, UserId};
#[test]
fn test_session_context_new() {
let session = SessionContext::new("sess_123");
assert_eq!(session.session_id, "sess_123");
assert!(session.last_active_at.is_none());
assert!(session.metadata.is_empty());
}
#[test]
fn test_session_context_new_owned_string() {
let session = SessionContext::new(String::from("sess_owned"));
assert_eq!(session.session_id, "sess_owned");
}
#[test]
fn test_session_context_touch() {
let mut session = SessionContext::new("sess_touch");
assert!(session.last_active_at.is_none());
session.touch();
assert!(session.last_active_at.is_some());
let first_touch = session.last_active_at.unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
session.touch();
assert!(session.last_active_at.unwrap() >= first_touch);
}
#[test]
fn test_session_context_metadata() {
let mut session = SessionContext::new("sess_meta");
session
.metadata
.insert("key".to_string(), serde_json::json!("value"));
assert_eq!(
session.metadata.get("key"),
Some(&serde_json::json!("value"))
);
}
#[test]
fn test_session_context_serde() {
let session = SessionContext::new("sess_serde");
let json = serde_json::to_string(&session).unwrap();
let parsed: SessionContext = serde_json::from_str(&json).unwrap();
assert_eq!(session.session_id, parsed.session_id);
}
fn create_test_tenant() -> TenantContext {
TenantContext::new(TenantId::from_string("tenant_test"))
}
#[test]
fn test_runtime_context_new() {
let exec_id = ExecutionId::from_string("exec_test");
let parent = ParentLink::from_user_message("msg_123");
let tenant = create_test_tenant();
let ctx = RuntimeContext::new(exec_id.clone(), parent, tenant);
assert_eq!(ctx.execution_id.as_str(), "exec_test");
assert!(ctx.step_id.is_none());
assert!(ctx.session.is_none());
assert!(ctx.metadata.is_empty());
}
#[test]
fn test_runtime_context_from_user_message() {
let exec_id = ExecutionId::from_string("exec_msg");
let tenant = create_test_tenant();
let ctx = RuntimeContext::from_user_message(exec_id, "msg_456", tenant);
assert_eq!(ctx.parent.parent_type, ParentType::UserMessage);
assert_eq!(ctx.parent.parent_id, "msg_456");
}
#[test]
fn test_runtime_context_with_step() {
let exec_id = ExecutionId::from_string("exec_step");
let parent = ParentLink::system();
let tenant = create_test_tenant();
let step_id = StepId::from_string("step_ctx");
let ctx = RuntimeContext::new(exec_id, parent, tenant).with_step(step_id.clone());
assert!(ctx.step_id.is_some());
assert_eq!(ctx.step_id.unwrap().as_str(), "step_ctx");
}
#[test]
fn test_runtime_context_with_trace() {
let exec_id = ExecutionId::from_string("exec_trace");
let parent = ParentLink::system();
let tenant = create_test_tenant();
let trace = TraceContext::from_traceparent(
"00-0123456789abcdef0123456789abcdef-0123456789abcdef-01",
)
.unwrap();
let ctx = RuntimeContext::new(exec_id, parent, tenant).with_trace(trace);
assert_eq!(ctx.trace_id(), "0123456789abcdef0123456789abcdef");
}
#[test]
fn test_runtime_context_with_session() {
let exec_id = ExecutionId::from_string("exec_sess");
let parent = ParentLink::system();
let tenant = create_test_tenant();
let session = SessionContext::new("sess_runtime");
let ctx = RuntimeContext::new(exec_id, parent, tenant).with_session(session);
assert!(ctx.session.is_some());
assert_eq!(ctx.session.unwrap().session_id, "sess_runtime");
}
#[test]
fn test_runtime_context_with_metadata() {
let exec_id = ExecutionId::from_string("exec_meta");
let parent = ParentLink::system();
let tenant = create_test_tenant();
let ctx = RuntimeContext::new(exec_id, parent, tenant)
.with_metadata("key1", serde_json::json!("value1"))
.with_metadata("key2", serde_json::json!(42));
assert_eq!(ctx.metadata.len(), 2);
assert_eq!(ctx.metadata.get("key1"), Some(&serde_json::json!("value1")));
assert_eq!(ctx.metadata.get("key2"), Some(&serde_json::json!(42)));
}
#[test]
fn test_runtime_context_child_context() {
let exec_id = ExecutionId::from_string("exec_parent");
let parent = ParentLink::from_user_message("msg_parent");
let tenant = TenantContext::new(TenantId::from_string("tenant_parent"))
.with_user(UserId::from_string("user_parent"));
let parent_ctx = RuntimeContext::new(exec_id, parent, tenant)
.with_session(SessionContext::new("sess_inherit"))
.with_metadata("parent_key", serde_json::json!("should_not_inherit"));
let child_exec_id = ExecutionId::from_string("exec_child");
let parent_step_id = StepId::from_string("step_that_spawned");
let child_ctx = parent_ctx.child_context(child_exec_id.clone(), &parent_step_id);
assert_eq!(child_ctx.execution_id.as_str(), "exec_child");
assert_eq!(child_ctx.parent.parent_type, ParentType::StepExecution);
assert_eq!(child_ctx.parent.parent_id, "step_that_spawned");
assert_eq!(child_ctx.tenant.tenant_id().as_str(), "tenant_parent");
assert!(child_ctx.session.is_some());
assert_eq!(child_ctx.session.unwrap().session_id, "sess_inherit");
assert_eq!(child_ctx.trace.trace_id, parent_ctx.trace.trace_id);
assert_ne!(child_ctx.trace.span_id, parent_ctx.trace.span_id);
assert!(child_ctx.metadata.is_empty());
}
#[test]
fn test_runtime_context_enter_step() {
let exec_id = ExecutionId::from_string("exec_enter");
let parent = ParentLink::system();
let tenant = create_test_tenant();
let original_ctx = RuntimeContext::new(exec_id, parent, tenant);
let step_id = StepId::from_string("step_enter");
let step_ctx = original_ctx.enter_step(step_id.clone());
assert!(step_ctx.step_id.is_some());
assert_eq!(step_ctx.step_id.unwrap().as_str(), "step_enter");
assert_eq!(step_ctx.trace.trace_id, original_ctx.trace.trace_id);
assert_ne!(step_ctx.trace.span_id, original_ctx.trace.span_id);
assert!(original_ctx.step_id.is_none());
}
#[test]
fn test_runtime_context_accessors() {
let exec_id = ExecutionId::from_string("exec_access");
let step_id = StepId::from_string("step_access");
let parent = ParentLink::system();
let tenant = TenantContext::new(TenantId::from_string("tenant_access"))
.with_user(UserId::from_string("user_access"));
let ctx = RuntimeContext::new(exec_id, parent, tenant).with_step(step_id);
assert_eq!(ctx.execution_id().as_str(), "exec_access");
assert_eq!(ctx.step_id().unwrap().as_str(), "step_access");
assert_eq!(ctx.tenant().tenant_id().as_str(), "tenant_access");
assert!(!ctx.trace_id().is_empty());
assert!(!ctx.span_id().is_empty());
}
#[test]
fn test_runtime_context_is_root_user_message() {
let exec_id = ExecutionId::from_string("exec_root");
let parent = ParentLink::from_user_message("msg_root");
let tenant = create_test_tenant();
let ctx = RuntimeContext::new(exec_id, parent, tenant);
assert!(ctx.is_root());
}
#[test]
fn test_runtime_context_is_root_system() {
let exec_id = ExecutionId::from_string("exec_root");
let parent = ParentLink::system();
let tenant = create_test_tenant();
let ctx = RuntimeContext::new(exec_id, parent, tenant);
assert!(ctx.is_root());
}
#[test]
fn test_runtime_context_is_not_root_step_execution() {
let exec_id = ExecutionId::from_string("exec_child");
let parent_step = StepId::from_string("step_parent");
let parent = ParentLink::from_step(&parent_step);
let tenant = create_test_tenant();
let ctx = RuntimeContext::new(exec_id, parent, tenant);
assert!(!ctx.is_root());
}
#[test]
fn test_runtime_context_serde() {
let exec_id = ExecutionId::from_string("exec_serde");
let parent = ParentLink::system();
let tenant = create_test_tenant();
let ctx = RuntimeContext::new(exec_id, parent, tenant);
let json = serde_json::to_string(&ctx).unwrap();
let parsed: RuntimeContext = serde_json::from_str(&json).unwrap();
assert_eq!(ctx.execution_id.as_str(), parsed.execution_id.as_str());
}
#[test]
fn test_builder_new() {
let exec_id = ExecutionId::from_string("exec_builder");
let parent = ParentLink::system();
let tenant = create_test_tenant();
let builder = RuntimeContextBuilder::new(exec_id.clone(), parent, tenant);
let ctx = builder.build();
assert_eq!(ctx.execution_id.as_str(), "exec_builder");
}
#[test]
fn test_builder_with_trace() {
let exec_id = ExecutionId::from_string("exec_builder_trace");
let parent = ParentLink::system();
let tenant = create_test_tenant();
let trace = TraceContext::from_traceparent(
"00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1-bbbbbbbbbbbbbb11-01",
)
.unwrap();
let ctx = RuntimeContextBuilder::new(exec_id, parent, tenant)
.trace(trace)
.build();
assert_eq!(ctx.trace.trace_id, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1");
}
#[test]
fn test_builder_with_session() {
let exec_id = ExecutionId::from_string("exec_builder_sess");
let parent = ParentLink::system();
let tenant = create_test_tenant();
let session = SessionContext::new("sess_builder");
let ctx = RuntimeContextBuilder::new(exec_id, parent, tenant)
.session(session)
.build();
assert!(ctx.session.is_some());
assert_eq!(ctx.session.unwrap().session_id, "sess_builder");
}
#[test]
fn test_builder_with_metadata() {
let exec_id = ExecutionId::from_string("exec_builder_meta");
let parent = ParentLink::system();
let tenant = create_test_tenant();
let ctx = RuntimeContextBuilder::new(exec_id, parent, tenant)
.metadata("build_key", serde_json::json!("build_value"))
.build();
assert_eq!(
ctx.metadata.get("build_key"),
Some(&serde_json::json!("build_value"))
);
}
#[test]
fn test_builder_full_chain() {
let exec_id = ExecutionId::from_string("exec_full");
let parent = ParentLink::from_user_message("msg_full");
let tenant = TenantContext::new(TenantId::from_string("tenant_full"))
.with_user(UserId::from_string("user_full"));
let session = SessionContext::new("sess_full");
let trace = TraceContext::new();
let ctx = RuntimeContextBuilder::new(exec_id, parent, tenant)
.trace(trace.clone())
.session(session)
.metadata("key1", serde_json::json!(1))
.metadata("key2", serde_json::json!(2))
.build();
assert_eq!(ctx.execution_id.as_str(), "exec_full");
assert_eq!(ctx.parent.parent_id, "msg_full");
assert_eq!(ctx.tenant.tenant_id().as_str(), "tenant_full");
assert!(ctx.session.is_some());
assert_eq!(ctx.metadata.len(), 2);
}
#[test]
fn test_nested_execution_hierarchy() {
let root_exec_id = ExecutionId::from_string("exec_root");
let tenant = TenantContext::new(TenantId::from_string("tenant_hier"))
.with_user(UserId::from_string("user_hier"));
let root_ctx = RuntimeContext::from_user_message(root_exec_id, "msg_root", tenant);
assert!(root_ctx.is_root());
let step1_id = StepId::from_string("step_1");
let child1_ctx = root_ctx.child_context(ExecutionId::from_string("exec_child1"), &step1_id);
assert!(!child1_ctx.is_root());
assert_eq!(child1_ctx.trace.trace_id, root_ctx.trace.trace_id);
let step2_id = StepId::from_string("step_2");
let child2_ctx =
child1_ctx.child_context(ExecutionId::from_string("exec_child2"), &step2_id);
assert!(!child2_ctx.is_root());
assert_eq!(child2_ctx.trace.trace_id, root_ctx.trace.trace_id);
assert_ne!(child2_ctx.trace.span_id, child1_ctx.trace.span_id);
assert_ne!(child1_ctx.trace.span_id, root_ctx.trace.span_id);
}
#[test]
fn test_step_execution_creates_correct_spans() {
let exec_id = ExecutionId::from_string("exec_spans");
let tenant = create_test_tenant();
let root_ctx = RuntimeContext::new(exec_id, ParentLink::system(), tenant);
let step1 = StepId::from_string("step_a");
let step2 = StepId::from_string("step_b");
let ctx_step1 = root_ctx.enter_step(step1.clone());
let ctx_step2 = root_ctx.enter_step(step2.clone());
assert_eq!(ctx_step1.trace.trace_id, ctx_step2.trace.trace_id);
assert_ne!(ctx_step1.trace.span_id, ctx_step2.trace.span_id);
assert_ne!(
ctx_step1.step_id.unwrap().as_str(),
ctx_step2.step_id.unwrap().as_str()
);
}
}