1use a2a_protocol_types::message::Message;
13use a2a_protocol_types::task::{Task, TaskId};
14use tokio_util::sync::CancellationToken;
15
16use crate::call_context::CallContext;
17
18#[derive(Debug, Clone)]
37#[non_exhaustive]
38pub struct RequestContext {
39 pub message: Message,
41
42 pub task_id: TaskId,
44
45 pub context_id: String,
47
48 pub stored_task: Option<Task>,
50
51 pub metadata: Option<serde_json::Value>,
53
54 pub cancellation_token: CancellationToken,
59
60 pub call_context: Option<CallContext>,
71}
72
73impl RequestContext {
74 #[must_use]
76 pub fn new(message: Message, task_id: TaskId, context_id: String) -> Self {
77 Self {
78 message,
79 task_id,
80 context_id,
81 stored_task: None,
82 metadata: None,
83 cancellation_token: CancellationToken::new(),
84 call_context: None,
85 }
86 }
87
88 #[must_use]
90 pub fn with_stored_task(mut self, task: Task) -> Self {
91 self.stored_task = Some(task);
92 self
93 }
94
95 #[must_use]
97 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
98 self.metadata = Some(metadata);
99 self
100 }
101
102 #[must_use]
104 pub fn with_call_context(mut self, call_context: CallContext) -> Self {
105 self.call_context = Some(call_context);
106 self
107 }
108
109 #[must_use]
115 pub fn caller_identity(&self) -> Option<&str> {
116 self.call_context
117 .as_ref()
118 .and_then(CallContext::caller_identity)
119 }
120
121 #[must_use]
130 pub fn tenant(&self) -> Option<&str> {
131 self.call_context.as_ref().and_then(CallContext::tenant)
132 }
133
134 #[must_use]
136 pub fn http_header(&self, name: &str) -> Option<&str> {
137 self.call_context
138 .as_ref()?
139 .http_headers()
140 .get(&name.to_ascii_lowercase())
141 .map(String::as_str)
142 }
143
144 #[must_use]
147 pub fn activated_extensions(&self) -> &[String] {
148 self.call_context
149 .as_ref()
150 .map_or(&[] as &[String], CallContext::extensions)
151 }
152
153 #[must_use]
171 pub fn trace_context(&self) -> Option<&a2a_protocol_types::trace_context::TraceContext> {
172 self.call_context
173 .as_ref()
174 .and_then(CallContext::trace_context)
175 }
176
177 #[must_use]
179 pub fn request_id(&self) -> Option<&str> {
180 self.call_context.as_ref().and_then(CallContext::request_id)
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use a2a_protocol_types::message::{MessageId, MessageRole, Part};
188 use a2a_protocol_types::task::{ContextId, TaskState, TaskStatus};
189 use a2a_protocol_types::trace_context::TraceContext;
190
191 fn make_message(text: &str) -> Message {
193 Message {
194 id: MessageId::new("msg-1"),
195 role: MessageRole::User,
196 parts: vec![Part::text(text)],
197 task_id: None,
198 context_id: None,
199 reference_task_ids: None,
200 extensions: None,
201 metadata: None,
202 }
203 }
204
205 fn make_task() -> Task {
207 Task {
208 id: TaskId::new("task-1"),
209 context_id: ContextId::new("ctx-1"),
210 status: TaskStatus::new(TaskState::Submitted),
211 history: None,
212 artifacts: None,
213 metadata: None,
214 }
215 }
216
217 #[test]
220 fn new_sets_required_fields() {
221 let msg = make_message("hello");
222 let ctx = RequestContext::new(msg.clone(), TaskId::new("t-1"), "ctx-1".to_owned());
223
224 assert_eq!(ctx.message, msg, "message should match the input");
225 assert_eq!(ctx.task_id, TaskId::new("t-1"), "task_id should match");
226 assert_eq!(ctx.context_id, "ctx-1", "context_id should match");
227 }
228
229 #[test]
230 fn new_defaults_optional_fields_to_none() {
231 let ctx = RequestContext::new(make_message("hi"), TaskId::new("t-2"), "ctx-2".to_owned());
232
233 assert!(
234 ctx.stored_task.is_none(),
235 "stored_task should default to None"
236 );
237 assert!(ctx.metadata.is_none(), "metadata should default to None");
238 }
239
240 #[test]
241 fn new_provides_uncancelled_token() {
242 let ctx = RequestContext::new(make_message("hi"), TaskId::new("t-3"), "ctx-3".to_owned());
243 assert!(
244 !ctx.cancellation_token.is_cancelled(),
245 "fresh token should not be cancelled"
246 );
247 }
248
249 #[test]
252 fn with_stored_task_sets_task() {
253 let task = make_task();
254 let ctx = RequestContext::new(make_message("hi"), TaskId::new("t-4"), "ctx-4".to_owned())
255 .with_stored_task(task);
256
257 assert_eq!(
258 ctx.stored_task.as_ref().map(|t| &t.id),
259 Some(&TaskId::new("task-1")),
260 "stored_task should contain the provided task"
261 );
262 }
263
264 #[test]
265 fn with_stored_task_preserves_other_fields() {
266 let ctx = RequestContext::new(make_message("hi"), TaskId::new("t-5"), "ctx-5".to_owned())
267 .with_stored_task(make_task());
268
269 assert_eq!(
270 ctx.task_id,
271 TaskId::new("t-5"),
272 "task_id should be unchanged"
273 );
274 assert_eq!(ctx.context_id, "ctx-5", "context_id should be unchanged");
275 }
276
277 #[test]
280 fn with_metadata_sets_value() {
281 let meta = serde_json::json!({"key": "value", "num": 42});
282 let ctx = RequestContext::new(make_message("hi"), TaskId::new("t-6"), "ctx-6".to_owned())
283 .with_metadata(meta.clone());
284
285 assert_eq!(
286 ctx.metadata.as_ref(),
287 Some(&meta),
288 "metadata should match the provided value"
289 );
290 }
291
292 #[test]
295 fn builder_methods_can_be_chained() {
296 let task = make_task();
297 let meta = serde_json::json!({"chained": true});
298 let ctx = RequestContext::new(
299 make_message("chain"),
300 TaskId::new("t-7"),
301 "ctx-7".to_owned(),
302 )
303 .with_stored_task(task)
304 .with_metadata(meta.clone());
305
306 assert!(
307 ctx.stored_task.is_some(),
308 "stored_task should be set after chaining"
309 );
310 assert_eq!(
311 ctx.metadata,
312 Some(meta),
313 "metadata should be set after chaining"
314 );
315 }
316
317 #[test]
320 fn request_context_is_cloneable() {
321 let ctx = RequestContext::new(
322 make_message("clone me"),
323 TaskId::new("t-8"),
324 "ctx-8".to_owned(),
325 );
326 let cloned = ctx.clone();
327 assert_eq!(
328 cloned.task_id, ctx.task_id,
329 "cloned context should have same task_id"
330 );
331 }
332
333 #[test]
334 fn request_context_is_debug() {
335 let ctx = RequestContext::new(
336 make_message("debug"),
337 TaskId::new("t-9"),
338 "ctx-9".to_owned(),
339 );
340 let debug_str = format!("{ctx:?}");
341 assert!(
342 debug_str.contains("RequestContext"),
343 "Debug output should contain the struct name"
344 );
345 }
346
347 #[test]
355 fn trace_context_returns_the_trace_the_caller_sent() {
356 const TRACEPARENT: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
357 let trace = TraceContext::parse(TRACEPARENT).expect("a valid traceparent");
358
359 let ctx = RequestContext::new(make_message("hi"), TaskId::new("t-10"), "ctx-10".to_owned())
360 .with_call_context(CallContext::new("message/send").with_trace_context(trace.clone()));
361
362 assert_eq!(
363 ctx.trace_context(),
364 Some(&trace),
365 "the accessor must hand back the caller's trace, not a fresh one"
366 );
367 assert_eq!(
368 ctx.trace_context().map(TraceContext::traceparent),
369 Some(TRACEPARENT.to_owned()),
370 "and it must re-emit byte-for-byte, or the delegation chain forks"
371 );
372 }
373
374 #[test]
375 fn trace_context_is_none_when_the_caller_was_not_tracing() {
376 let untraced =
377 RequestContext::new(make_message("hi"), TaskId::new("t-11"), "ctx-11".to_owned())
378 .with_call_context(CallContext::new("message/send"));
379 assert!(
380 untraced.trace_context().is_none(),
381 "a call with no traceparent has no trace; the SDK never invents one"
382 );
383
384 let unserved =
385 RequestContext::new(make_message("hi"), TaskId::new("t-12"), "ctx-12".to_owned());
386 assert!(
387 unserved.trace_context().is_none(),
388 "nor does one appear when there is no CallContext at all"
389 );
390 }
391}