1#[cfg(feature = "alloc")]
7use alloc::{collections::BTreeMap, string::String};
8
9#[cfg(feature = "alloc")]
10use crate::{
11 identity::{AgentId, SessionId},
12 time::Timestamp,
13 GovernanceLevel,
14};
15
16#[cfg(feature = "alloc")]
25#[derive(Debug, Clone, PartialEq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct AgentContext {
28 pub agent_id: AgentId,
30 pub session_id: SessionId,
32 pub pid: u32,
34 pub started_at: Timestamp,
36 pub metadata: BTreeMap<String, String>,
41 #[cfg_attr(feature = "serde", serde(default))]
49 pub governance_level: GovernanceLevel,
50 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none", default))]
52 pub parent_agent_id: Option<AgentId>,
53 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none", default))]
55 pub team_id: Option<String>,
56 #[cfg_attr(feature = "serde", serde(default))]
58 pub depth: u32,
59 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none", default))]
61 pub delegation_reason: Option<String>,
62 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none", default))]
64 pub spawned_by_tool: Option<String>,
65 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none", default))]
71 pub root_agent_id: Option<AgentId>,
72}
73
74#[cfg(all(feature = "alloc", feature = "std"))]
75impl AgentContext {
76 pub fn now(agent_id: AgentId, session_id: SessionId, pid: u32) -> Self {
80 Self {
81 started_at: Timestamp::from(std::time::SystemTime::now()),
82 agent_id,
83 session_id,
84 pid,
85 metadata: BTreeMap::new(),
86 governance_level: GovernanceLevel::default(),
87 parent_agent_id: None,
88 team_id: None,
89 depth: 0,
90 delegation_reason: None,
91 spawned_by_tool: None,
92 root_agent_id: None,
93 }
94 }
95
96 pub fn builder() -> AgentContextBuilder {
98 AgentContextBuilder::new()
99 }
100}
101
102#[cfg(feature = "alloc")]
107pub struct AgentContextBuilder {
108 parent_agent_id: Option<AgentId>,
109 team_id: Option<String>,
110 depth: u32,
111 delegation_reason: Option<String>,
112 spawned_by_tool: Option<String>,
113 root_agent_id: Option<AgentId>,
114}
115
116#[cfg(feature = "alloc")]
117impl AgentContextBuilder {
118 fn new() -> Self {
119 Self {
120 parent_agent_id: None,
121 team_id: None,
122 depth: 0,
123 delegation_reason: None,
124 spawned_by_tool: None,
125 root_agent_id: None,
126 }
127 }
128
129 pub fn parent_agent_id(mut self, id: AgentId) -> Self {
131 self.parent_agent_id = Some(id);
132 self
133 }
134
135 pub fn team_id(mut self, id: String) -> Self {
137 self.team_id = Some(id);
138 self
139 }
140
141 pub fn depth(mut self, d: u32) -> Self {
143 self.depth = d;
144 self
145 }
146
147 pub fn delegation_reason(mut self, r: String) -> Self {
149 self.delegation_reason = Some(r);
150 self
151 }
152
153 pub fn spawned_by_tool(mut self, t: String) -> Self {
155 self.spawned_by_tool = Some(t);
156 self
157 }
158
159 pub fn root_agent_id(mut self, id: AgentId) -> Self {
161 self.root_agent_id = Some(id);
162 self
163 }
164}
165
166#[cfg(all(feature = "alloc", feature = "std"))]
167impl AgentContextBuilder {
168 pub fn build(self, agent_id: AgentId, session_id: SessionId, pid: u32) -> AgentContext {
171 AgentContext {
172 started_at: Timestamp::from(std::time::SystemTime::now()),
173 agent_id,
174 session_id,
175 pid,
176 metadata: BTreeMap::new(),
177 governance_level: GovernanceLevel::default(),
178 parent_agent_id: self.parent_agent_id,
179 team_id: self.team_id,
180 depth: self.depth,
181 delegation_reason: self.delegation_reason,
182 spawned_by_tool: self.spawned_by_tool,
183 root_agent_id: self.root_agent_id,
184 }
185 }
186}
187
188#[cfg(all(test, feature = "alloc"))]
189mod tests {
190 use super::*;
191
192 const AGENT_BYTES: [u8; 16] = [1; 16];
193 const SESSION_BYTES: [u8; 16] = [2; 16];
194
195 fn make_context() -> AgentContext {
196 AgentContext {
197 agent_id: AgentId::from_bytes(AGENT_BYTES),
198 session_id: SessionId::from_bytes(SESSION_BYTES),
199 pid: 42,
200 started_at: Timestamp::from_nanos(1_000_000),
201 metadata: BTreeMap::new(),
202 governance_level: GovernanceLevel::default(),
203 parent_agent_id: None,
204 team_id: None,
205 depth: 0,
206 delegation_reason: None,
207 spawned_by_tool: None,
208 root_agent_id: None,
209 }
210 }
211
212 #[test]
213 fn field_access() {
214 let ctx = make_context();
215 assert_eq!(ctx.agent_id.as_bytes(), &AGENT_BYTES);
216 assert_eq!(ctx.session_id.as_bytes(), &SESSION_BYTES);
217 assert_eq!(ctx.pid, 42);
218 assert_eq!(ctx.started_at.as_nanos(), 1_000_000);
219 assert!(ctx.metadata.is_empty());
220 }
221
222 #[test]
223 fn clone_equals_original() {
224 let ctx = make_context();
225 assert_eq!(ctx.clone(), ctx);
226 }
227
228 #[test]
229 fn equality() {
230 let a = make_context();
231 let b = make_context();
232 assert_eq!(a, b);
233 }
234
235 #[test]
236 fn inequality_on_different_pid() {
237 let a = make_context();
238 let mut b = make_context();
239 b.pid = 99;
240 assert_ne!(a, b);
241 }
242
243 #[cfg(feature = "std")]
244 #[test]
245 fn now_constructor_sets_nonzero_timestamp() {
246 let ctx = AgentContext::now(
247 AgentId::from_bytes(AGENT_BYTES),
248 SessionId::from_bytes(SESSION_BYTES),
249 std::process::id(),
250 );
251 assert!(ctx.started_at.as_nanos() > 0);
252 assert!(ctx.metadata.is_empty());
253 }
254
255 #[cfg(feature = "std")]
256 #[test]
257 fn builder_defaults_give_root_agent() {
258 let ctx = AgentContext::builder().build(
259 AgentId::from_bytes(AGENT_BYTES),
260 SessionId::from_bytes(SESSION_BYTES),
261 42,
262 );
263 assert_eq!(ctx.depth, 0);
264 assert!(ctx.parent_agent_id.is_none());
265 assert!(ctx.team_id.is_none());
266 assert!(ctx.delegation_reason.is_none());
267 assert!(ctx.spawned_by_tool.is_none());
268 assert!(ctx.root_agent_id.is_none());
269 }
270
271 #[cfg(feature = "std")]
272 #[test]
273 fn builder_sets_parent_and_team() {
274 let parent = AgentId::from_bytes([9u8; 16]);
275 let ctx = AgentContext::builder()
276 .parent_agent_id(parent)
277 .team_id("team-alpha".into())
278 .depth(1)
279 .build(
280 AgentId::from_bytes(AGENT_BYTES),
281 SessionId::from_bytes(SESSION_BYTES),
282 42,
283 );
284 assert_eq!(ctx.parent_agent_id, Some(parent));
285 assert_eq!(ctx.team_id.as_deref(), Some("team-alpha"));
286 assert_eq!(ctx.depth, 1);
287 }
288
289 #[cfg(feature = "std")]
290 #[test]
291 fn builder_sets_delegation_fields() {
292 let ctx = AgentContext::builder()
293 .delegation_reason("summarise results".into())
294 .spawned_by_tool("langgraph.subgraph".into())
295 .build(
296 AgentId::from_bytes(AGENT_BYTES),
297 SessionId::from_bytes(SESSION_BYTES),
298 42,
299 );
300 assert_eq!(ctx.delegation_reason.as_deref(), Some("summarise results"));
301 assert_eq!(ctx.spawned_by_tool.as_deref(), Some("langgraph.subgraph"));
302 }
303
304 #[cfg(feature = "serde")]
305 #[test]
306 fn serde_round_trip() {
307 let original = make_context();
308 let json = serde_json::to_string(&original).expect("serialize");
309 let restored: AgentContext = serde_json::from_str(&json).expect("deserialize");
310 assert_eq!(original, restored);
311 }
312
313 #[cfg(feature = "serde")]
314 #[test]
315 fn serde_round_trip_with_topology_fields() {
316 let root = AgentId::from_bytes([7u8; 16]);
317 let original = AgentContext {
318 agent_id: AgentId::from_bytes(AGENT_BYTES),
319 session_id: SessionId::from_bytes(SESSION_BYTES),
320 pid: 42,
321 started_at: Timestamp::from_nanos(1_000_000),
322 metadata: BTreeMap::new(),
323 governance_level: GovernanceLevel::default(),
324 parent_agent_id: Some(AgentId::from_bytes([9u8; 16])),
325 team_id: Some("team-alpha".into()),
326 depth: 2,
327 delegation_reason: Some("summarise results".into()),
328 spawned_by_tool: Some("langgraph.subgraph".into()),
329 root_agent_id: Some(root),
330 };
331 let json = serde_json::to_string(&original).expect("serialize");
332 let restored: AgentContext = serde_json::from_str(&json).expect("deserialize");
333 assert_eq!(original, restored);
334 }
335
336 #[cfg(feature = "serde")]
337 #[test]
338 fn serde_backward_compat_missing_topology_fields() {
339 let json = r#"{
342 "agent_id":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
343 "session_id":[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2],
344 "pid":42,
345 "started_at":1000000,
346 "metadata":{}
347 }"#;
348 let restored: AgentContext = serde_json::from_str(json).expect("deserialize");
349 assert_eq!(restored.depth, 0);
350 assert!(restored.parent_agent_id.is_none());
351 assert!(restored.team_id.is_none());
352 assert!(restored.delegation_reason.is_none());
353 assert!(restored.spawned_by_tool.is_none());
354 assert!(restored.root_agent_id.is_none());
355 }
356
357 #[cfg(feature = "serde")]
358 #[test]
359 fn agent_context_defaults_to_l0_when_governance_level_missing() {
360 let json = r#"{
365 "agent_id":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
366 "session_id":[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2],
367 "pid":42,
368 "started_at":1000000,
369 "metadata":{}
370 }"#;
371 let restored: AgentContext = serde_json::from_str(json).expect("deserialize");
372 assert_eq!(restored.governance_level, GovernanceLevel::L0Discover);
373 }
374}