systemprompt_models/execution/context/
mod.rs1mod call_source;
4mod context_error;
5mod context_types;
6
7pub use call_source::CallSource;
8pub use context_error::{ContextExtractionError, ContextIdSource, TASK_BASED_CONTEXT_MARKER};
9pub use context_types::{
10 AuthContext, ExecutionContext, ExecutionSettings, RequestMetadata, UserInteractionMode,
11};
12
13use crate::ai::ToolModelConfig;
14use crate::auth::{AuthenticatedUser, RateLimitTier, UserType};
15use anyhow::anyhow;
16use http::{HeaderMap, HeaderValue};
17use serde::{Deserialize, Serialize};
18use std::str::FromStr;
19use std::time::{Duration, Instant};
20use systemprompt_identifiers::{
21 headers, AgentName, AiToolCallId, ClientId, ContextId, JwtToken, McpExecutionId, SessionId,
22 TaskId, TraceId, UserId,
23};
24use systemprompt_traits::{ContextPropagation, InjectContextHeaders};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct RequestContext {
28 pub auth: AuthContext,
29 pub request: RequestMetadata,
30 pub execution: ExecutionContext,
31 pub settings: ExecutionSettings,
32
33 #[serde(skip)]
34 pub user: Option<AuthenticatedUser>,
35
36 #[serde(skip, default = "Instant::now")]
37 pub start_time: Instant,
38}
39
40impl RequestContext {
41 pub fn new(
79 session_id: SessionId,
80 trace_id: TraceId,
81 context_id: ContextId,
82 agent_name: AgentName,
83 ) -> Self {
84 Self {
85 auth: AuthContext {
86 auth_token: JwtToken::new(""),
87 user_id: UserId::anonymous(),
88 user_type: UserType::Anon,
89 },
90 request: RequestMetadata {
91 session_id,
92 timestamp: Instant::now(),
93 client_id: None,
94 is_tracked: true,
95 fingerprint_hash: None,
96 },
97 execution: ExecutionContext {
98 trace_id,
99 context_id,
100 task_id: None,
101 ai_tool_call_id: None,
102 mcp_execution_id: None,
103 call_source: None,
104 agent_name,
105 tool_model_config: None,
106 },
107 settings: ExecutionSettings::default(),
108 user: None,
109 start_time: Instant::now(),
110 }
111 }
112
113 pub fn with_user(mut self, user: AuthenticatedUser) -> Self {
114 self.auth.user_id = UserId::new(user.id.to_string());
115 self.user = Some(user);
116 self
117 }
118
119 pub fn with_user_id(mut self, user_id: UserId) -> Self {
120 self.auth.user_id = user_id;
121 self
122 }
123
124 pub fn with_agent_name(mut self, agent_name: AgentName) -> Self {
125 self.execution.agent_name = agent_name;
126 self
127 }
128
129 pub fn with_context_id(mut self, context_id: ContextId) -> Self {
130 self.execution.context_id = context_id;
131 self
132 }
133
134 pub fn with_task_id(mut self, task_id: TaskId) -> Self {
135 self.execution.task_id = Some(task_id);
136 self
137 }
138
139 pub fn with_task(mut self, task_id: TaskId, call_source: CallSource) -> Self {
140 self.execution.task_id = Some(task_id);
141 self.execution.call_source = Some(call_source);
142 self
143 }
144
145 pub fn with_ai_tool_call_id(mut self, ai_tool_call_id: AiToolCallId) -> Self {
146 self.execution.ai_tool_call_id = Some(ai_tool_call_id);
147 self
148 }
149
150 pub fn with_mcp_execution_id(mut self, mcp_execution_id: McpExecutionId) -> Self {
151 self.execution.mcp_execution_id = Some(mcp_execution_id);
152 self
153 }
154
155 pub fn with_client_id(mut self, client_id: ClientId) -> Self {
156 self.request.client_id = Some(client_id);
157 self
158 }
159
160 pub const fn with_user_type(mut self, user_type: UserType) -> Self {
161 self.auth.user_type = user_type;
162 self
163 }
164
165 pub fn with_auth_token(mut self, token: impl Into<String>) -> Self {
166 self.auth.auth_token = JwtToken::new(token.into());
167 self
168 }
169
170 pub const fn with_call_source(mut self, call_source: CallSource) -> Self {
171 self.execution.call_source = Some(call_source);
172 self
173 }
174
175 pub const fn with_budget(mut self, cents: i32) -> Self {
176 self.settings.max_budget_cents = Some(cents);
177 self
178 }
179
180 pub const fn with_interaction_mode(mut self, mode: UserInteractionMode) -> Self {
181 self.settings.user_interaction_mode = Some(mode);
182 self
183 }
184
185 pub const fn with_tracked(mut self, is_tracked: bool) -> Self {
186 self.request.is_tracked = is_tracked;
187 self
188 }
189
190 pub fn with_fingerprint_hash(mut self, hash: impl Into<String>) -> Self {
191 self.request.fingerprint_hash = Some(hash.into());
192 self
193 }
194
195 pub fn fingerprint_hash(&self) -> Option<&str> {
196 self.request.fingerprint_hash.as_deref()
197 }
198
199 pub fn with_tool_model_config(mut self, config: ToolModelConfig) -> Self {
200 self.execution.tool_model_config = Some(config);
201 self
202 }
203
204 pub const fn tool_model_config(&self) -> Option<&ToolModelConfig> {
205 self.execution.tool_model_config.as_ref()
206 }
207
208 pub const fn session_id(&self) -> &SessionId {
209 &self.request.session_id
210 }
211
212 pub const fn user_id(&self) -> &UserId {
213 &self.auth.user_id
214 }
215
216 pub const fn trace_id(&self) -> &TraceId {
217 &self.execution.trace_id
218 }
219
220 pub const fn context_id(&self) -> &ContextId {
221 &self.execution.context_id
222 }
223
224 pub const fn agent_name(&self) -> &AgentName {
225 &self.execution.agent_name
226 }
227
228 pub const fn auth_token(&self) -> &JwtToken {
229 &self.auth.auth_token
230 }
231
232 pub const fn user_type(&self) -> UserType {
233 self.auth.user_type
234 }
235
236 pub const fn rate_limit_tier(&self) -> RateLimitTier {
237 self.auth.user_type.rate_tier()
238 }
239
240 pub const fn task_id(&self) -> Option<&TaskId> {
241 self.execution.task_id.as_ref()
242 }
243
244 pub const fn client_id(&self) -> Option<&ClientId> {
245 self.request.client_id.as_ref()
246 }
247
248 pub const fn ai_tool_call_id(&self) -> Option<&AiToolCallId> {
249 self.execution.ai_tool_call_id.as_ref()
250 }
251
252 pub const fn mcp_execution_id(&self) -> Option<&McpExecutionId> {
253 self.execution.mcp_execution_id.as_ref()
254 }
255
256 pub const fn call_source(&self) -> Option<CallSource> {
257 self.execution.call_source
258 }
259
260 pub const fn is_authenticated(&self) -> bool {
261 self.user.is_some()
262 }
263
264 pub fn is_system(&self) -> bool {
265 self.auth.user_id.is_system() && self.execution.context_id.is_system()
266 }
267
268 pub fn elapsed(&self) -> Duration {
269 self.start_time.elapsed()
270 }
271
272 pub fn validate_task_execution(&self) -> Result<(), String> {
273 if self.execution.task_id.is_none() {
274 return Err("Missing task_id for task execution".to_string());
275 }
276 if self.execution.context_id.as_str().is_empty() {
277 return Err("Missing context_id for task execution".to_string());
278 }
279 Ok(())
280 }
281
282 pub fn validate_authenticated(&self) -> Result<(), String> {
283 if self.auth.auth_token.as_str().is_empty() {
284 return Err("Missing authentication token".to_string());
285 }
286 if self.auth.user_id.is_anonymous() {
287 return Err("User is not authenticated".to_string());
288 }
289 Ok(())
290 }
291}
292
293fn insert_header(headers: &mut HeaderMap, name: &'static str, value: &str) {
294 if let Ok(val) = HeaderValue::from_str(value) {
295 headers.insert(name, val);
296 }
297}
298
299fn insert_header_if_present(headers: &mut HeaderMap, name: &'static str, value: Option<&str>) {
300 if let Some(v) = value {
301 insert_header(headers, name, v);
302 }
303}
304
305impl InjectContextHeaders for RequestContext {
306 fn inject_headers(&self, hdrs: &mut HeaderMap) {
307 insert_header(hdrs, headers::SESSION_ID, self.request.session_id.as_str());
308 insert_header(hdrs, headers::TRACE_ID, self.execution.trace_id.as_str());
309 insert_header(hdrs, headers::USER_ID, self.auth.user_id.as_str());
310 insert_header(hdrs, headers::USER_TYPE, self.auth.user_type.as_str());
311 insert_header(
312 hdrs,
313 headers::AGENT_NAME,
314 self.execution.agent_name.as_str(),
315 );
316
317 let context_id = self.execution.context_id.as_str();
318 if !context_id.is_empty() {
319 insert_header(hdrs, headers::CONTEXT_ID, context_id);
320 }
321
322 insert_header_if_present(
323 hdrs,
324 headers::TASK_ID,
325 self.execution.task_id.as_ref().map(TaskId::as_str),
326 );
327 insert_header_if_present(
328 hdrs,
329 headers::AI_TOOL_CALL_ID,
330 self.execution.ai_tool_call_id.as_ref().map(AsRef::as_ref),
331 );
332 insert_header_if_present(
333 hdrs,
334 headers::CALL_SOURCE,
335 self.execution.call_source.as_ref().map(CallSource::as_str),
336 );
337 insert_header_if_present(
338 hdrs,
339 headers::CLIENT_ID,
340 self.request.client_id.as_ref().map(ClientId::as_str),
341 );
342
343 let auth_token = self.auth.auth_token.as_str();
344 if auth_token.is_empty() {
345 tracing::trace!(user_id = %self.auth.user_id, "No auth_token to inject - Authorization header not added");
346 } else {
347 let auth_value = format!("Bearer {}", auth_token);
348 insert_header(hdrs, headers::AUTHORIZATION, &auth_value);
349 tracing::trace!(user_id = %self.auth.user_id, "Injected Authorization header for proxy");
350 }
351 }
352}
353
354impl ContextPropagation for RequestContext {
355 fn from_headers(hdrs: &HeaderMap) -> anyhow::Result<Self> {
356 let session_id = hdrs
357 .get(headers::SESSION_ID)
358 .and_then(|v| v.to_str().ok())
359 .ok_or_else(|| anyhow!("Missing {} header", headers::SESSION_ID))?;
360
361 let trace_id = hdrs
362 .get(headers::TRACE_ID)
363 .and_then(|v| v.to_str().ok())
364 .ok_or_else(|| anyhow!("Missing {} header", headers::TRACE_ID))?;
365
366 let user_id = hdrs
367 .get(headers::USER_ID)
368 .and_then(|v| v.to_str().ok())
369 .ok_or_else(|| anyhow!("Missing {} header", headers::USER_ID))?;
370
371 let context_id = hdrs
372 .get(headers::CONTEXT_ID)
373 .and_then(|v| v.to_str().ok())
374 .map_or_else(
375 || ContextId::new(String::new()),
376 |s| ContextId::new(s.to_string()),
377 );
378
379 let agent_name = hdrs
380 .get(headers::AGENT_NAME)
381 .and_then(|v| v.to_str().ok())
382 .ok_or_else(|| {
383 anyhow!(
384 "Missing {} header - all requests must have agent context",
385 headers::AGENT_NAME
386 )
387 })?;
388
389 let task_id = hdrs
390 .get(headers::TASK_ID)
391 .and_then(|v| v.to_str().ok())
392 .map(|s| TaskId::new(s.to_string()));
393
394 let ai_tool_call_id = hdrs
395 .get(headers::AI_TOOL_CALL_ID)
396 .and_then(|v| v.to_str().ok())
397 .map(|s| AiToolCallId::from(s.to_string()));
398
399 let call_source = hdrs
400 .get(headers::CALL_SOURCE)
401 .and_then(|v| v.to_str().ok())
402 .and_then(|s| CallSource::from_str(s).ok());
403
404 let client_id = hdrs
405 .get(headers::CLIENT_ID)
406 .and_then(|v| v.to_str().ok())
407 .map(|s| ClientId::new(s.to_string()));
408
409 let mut ctx = Self::new(
410 SessionId::new(session_id.to_string()),
411 TraceId::new(trace_id.to_string()),
412 context_id,
413 AgentName::new(agent_name.to_string()),
414 )
415 .with_user_id(UserId::new(user_id.to_string()));
416
417 if let Some(tid) = task_id {
418 ctx = ctx.with_task_id(tid);
419 }
420
421 if let Some(ai_id) = ai_tool_call_id {
422 ctx = ctx.with_ai_tool_call_id(ai_id);
423 }
424
425 if let Some(cs) = call_source {
426 ctx = ctx.with_call_source(cs);
427 }
428
429 if let Some(cid) = client_id {
430 ctx = ctx.with_client_id(cid);
431 }
432
433 Ok(ctx)
434 }
435
436 fn to_headers(&self) -> HeaderMap {
437 let mut headers = HeaderMap::new();
438 self.inject_headers(&mut headers);
439 headers
440 }
441}