#[cfg(feature = "alloc")]
use alloc::{collections::BTreeMap, string::String};
#[cfg(feature = "alloc")]
use crate::{
identity::{AgentId, SessionId},
time::Timestamp,
GovernanceLevel,
};
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AgentContext {
pub agent_id: AgentId,
pub session_id: SessionId,
pub pid: u32,
pub started_at: Timestamp,
pub metadata: BTreeMap<String, String>,
#[cfg_attr(feature = "serde", serde(default))]
pub governance_level: GovernanceLevel,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none", default))]
pub parent_agent_id: Option<AgentId>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none", default))]
pub team_id: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
pub depth: u32,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none", default))]
pub delegation_reason: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none", default))]
pub spawned_by_tool: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none", default))]
pub root_agent_id: Option<AgentId>,
}
#[cfg(all(feature = "alloc", feature = "std"))]
impl AgentContext {
pub fn now(agent_id: AgentId, session_id: SessionId, pid: u32) -> Self {
Self {
started_at: Timestamp::from(std::time::SystemTime::now()),
agent_id,
session_id,
pid,
metadata: BTreeMap::new(),
governance_level: GovernanceLevel::default(),
parent_agent_id: None,
team_id: None,
depth: 0,
delegation_reason: None,
spawned_by_tool: None,
root_agent_id: None,
}
}
pub fn builder() -> AgentContextBuilder {
AgentContextBuilder::new()
}
}
#[cfg(feature = "alloc")]
pub struct AgentContextBuilder {
parent_agent_id: Option<AgentId>,
team_id: Option<String>,
depth: u32,
delegation_reason: Option<String>,
spawned_by_tool: Option<String>,
root_agent_id: Option<AgentId>,
}
#[cfg(feature = "alloc")]
impl AgentContextBuilder {
fn new() -> Self {
Self {
parent_agent_id: None,
team_id: None,
depth: 0,
delegation_reason: None,
spawned_by_tool: None,
root_agent_id: None,
}
}
pub fn parent_agent_id(mut self, id: AgentId) -> Self {
self.parent_agent_id = Some(id);
self
}
pub fn team_id(mut self, id: String) -> Self {
self.team_id = Some(id);
self
}
pub fn depth(mut self, d: u32) -> Self {
self.depth = d;
self
}
pub fn delegation_reason(mut self, r: String) -> Self {
self.delegation_reason = Some(r);
self
}
pub fn spawned_by_tool(mut self, t: String) -> Self {
self.spawned_by_tool = Some(t);
self
}
pub fn root_agent_id(mut self, id: AgentId) -> Self {
self.root_agent_id = Some(id);
self
}
}
#[cfg(all(feature = "alloc", feature = "std"))]
impl AgentContextBuilder {
pub fn build(self, agent_id: AgentId, session_id: SessionId, pid: u32) -> AgentContext {
AgentContext {
started_at: Timestamp::from(std::time::SystemTime::now()),
agent_id,
session_id,
pid,
metadata: BTreeMap::new(),
governance_level: GovernanceLevel::default(),
parent_agent_id: self.parent_agent_id,
team_id: self.team_id,
depth: self.depth,
delegation_reason: self.delegation_reason,
spawned_by_tool: self.spawned_by_tool,
root_agent_id: self.root_agent_id,
}
}
}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::*;
const AGENT_BYTES: [u8; 16] = [1; 16];
const SESSION_BYTES: [u8; 16] = [2; 16];
fn make_context() -> AgentContext {
AgentContext {
agent_id: AgentId::from_bytes(AGENT_BYTES),
session_id: SessionId::from_bytes(SESSION_BYTES),
pid: 42,
started_at: Timestamp::from_nanos(1_000_000),
metadata: BTreeMap::new(),
governance_level: GovernanceLevel::default(),
parent_agent_id: None,
team_id: None,
depth: 0,
delegation_reason: None,
spawned_by_tool: None,
root_agent_id: None,
}
}
#[test]
fn field_access() {
let ctx = make_context();
assert_eq!(ctx.agent_id.as_bytes(), &AGENT_BYTES);
assert_eq!(ctx.session_id.as_bytes(), &SESSION_BYTES);
assert_eq!(ctx.pid, 42);
assert_eq!(ctx.started_at.as_nanos(), 1_000_000);
assert!(ctx.metadata.is_empty());
}
#[test]
fn clone_equals_original() {
let ctx = make_context();
assert_eq!(ctx.clone(), ctx);
}
#[test]
fn equality() {
let a = make_context();
let b = make_context();
assert_eq!(a, b);
}
#[test]
fn inequality_on_different_pid() {
let a = make_context();
let mut b = make_context();
b.pid = 99;
assert_ne!(a, b);
}
#[cfg(feature = "std")]
#[test]
fn now_constructor_sets_nonzero_timestamp() {
let ctx = AgentContext::now(
AgentId::from_bytes(AGENT_BYTES),
SessionId::from_bytes(SESSION_BYTES),
std::process::id(),
);
assert!(ctx.started_at.as_nanos() > 0);
assert!(ctx.metadata.is_empty());
}
#[cfg(feature = "std")]
#[test]
fn builder_defaults_give_root_agent() {
let ctx = AgentContext::builder().build(
AgentId::from_bytes(AGENT_BYTES),
SessionId::from_bytes(SESSION_BYTES),
42,
);
assert_eq!(ctx.depth, 0);
assert!(ctx.parent_agent_id.is_none());
assert!(ctx.team_id.is_none());
assert!(ctx.delegation_reason.is_none());
assert!(ctx.spawned_by_tool.is_none());
assert!(ctx.root_agent_id.is_none());
}
#[cfg(feature = "std")]
#[test]
fn builder_sets_parent_and_team() {
let parent = AgentId::from_bytes([9u8; 16]);
let ctx = AgentContext::builder()
.parent_agent_id(parent)
.team_id("team-alpha".into())
.depth(1)
.build(
AgentId::from_bytes(AGENT_BYTES),
SessionId::from_bytes(SESSION_BYTES),
42,
);
assert_eq!(ctx.parent_agent_id, Some(parent));
assert_eq!(ctx.team_id.as_deref(), Some("team-alpha"));
assert_eq!(ctx.depth, 1);
}
#[cfg(feature = "std")]
#[test]
fn builder_sets_delegation_fields() {
let ctx = AgentContext::builder()
.delegation_reason("summarise results".into())
.spawned_by_tool("langgraph.subgraph".into())
.build(
AgentId::from_bytes(AGENT_BYTES),
SessionId::from_bytes(SESSION_BYTES),
42,
);
assert_eq!(ctx.delegation_reason.as_deref(), Some("summarise results"));
assert_eq!(ctx.spawned_by_tool.as_deref(), Some("langgraph.subgraph"));
}
#[cfg(feature = "serde")]
#[test]
fn serde_round_trip() {
let original = make_context();
let json = serde_json::to_string(&original).expect("serialize");
let restored: AgentContext = serde_json::from_str(&json).expect("deserialize");
assert_eq!(original, restored);
}
#[cfg(feature = "serde")]
#[test]
fn serde_round_trip_with_topology_fields() {
let root = AgentId::from_bytes([7u8; 16]);
let original = AgentContext {
agent_id: AgentId::from_bytes(AGENT_BYTES),
session_id: SessionId::from_bytes(SESSION_BYTES),
pid: 42,
started_at: Timestamp::from_nanos(1_000_000),
metadata: BTreeMap::new(),
governance_level: GovernanceLevel::default(),
parent_agent_id: Some(AgentId::from_bytes([9u8; 16])),
team_id: Some("team-alpha".into()),
depth: 2,
delegation_reason: Some("summarise results".into()),
spawned_by_tool: Some("langgraph.subgraph".into()),
root_agent_id: Some(root),
};
let json = serde_json::to_string(&original).expect("serialize");
let restored: AgentContext = serde_json::from_str(&json).expect("deserialize");
assert_eq!(original, restored);
}
#[cfg(feature = "serde")]
#[test]
fn serde_backward_compat_missing_topology_fields() {
let json = r#"{
"agent_id":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
"session_id":[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2],
"pid":42,
"started_at":1000000,
"metadata":{}
}"#;
let restored: AgentContext = serde_json::from_str(json).expect("deserialize");
assert_eq!(restored.depth, 0);
assert!(restored.parent_agent_id.is_none());
assert!(restored.team_id.is_none());
assert!(restored.delegation_reason.is_none());
assert!(restored.spawned_by_tool.is_none());
assert!(restored.root_agent_id.is_none());
}
#[cfg(feature = "serde")]
#[test]
fn agent_context_defaults_to_l0_when_governance_level_missing() {
let json = r#"{
"agent_id":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
"session_id":[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2],
"pid":42,
"started_at":1000000,
"metadata":{}
}"#;
let restored: AgentContext = serde_json::from_str(json).expect("deserialize");
assert_eq!(restored.governance_level, GovernanceLevel::L0Discover);
}
}