1use crate::message::{ContentBlock, Message};
36use crate::provider::{FinishReason, ModelOptions, Usage};
37use serde::{Deserialize, Serialize};
38use std::collections::BTreeMap;
39use std::fmt;
40use std::sync::OnceLock;
41use std::sync::atomic::{AtomicU64, Ordering};
42use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
43use tokio_util::sync::CancellationToken;
44
45pub type RunMetadata = BTreeMap<String, serde_json::Value>;
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[non_exhaustive]
55pub enum UserInput {
56 Text(String),
58 Blocks(Vec<ContentBlock>),
60}
61
62impl UserInput {
63 pub fn text(input: impl Into<String>) -> Self {
65 Self::Text(input.into())
66 }
67
68 pub fn blocks(blocks: Vec<ContentBlock>) -> Self {
70 Self::Blocks(blocks)
71 }
72
73 pub fn into_message(self) -> Message {
75 match self {
76 Self::Text(input) => Message::user(input),
77 Self::Blocks(blocks) => Message::user_blocks(blocks),
78 }
79 }
80
81 pub fn as_text(&self) -> Option<&str> {
86 match self {
87 Self::Text(input) => Some(input),
88 Self::Blocks(_) => None,
89 }
90 }
91}
92
93impl From<String> for UserInput {
94 fn from(input: String) -> Self {
95 Self::Text(input)
96 }
97}
98
99impl From<&str> for UserInput {
100 fn from(input: &str) -> Self {
101 Self::Text(input.to_string())
102 }
103}
104
105impl From<Vec<ContentBlock>> for UserInput {
106 fn from(blocks: Vec<ContentBlock>) -> Self {
107 Self::Blocks(blocks)
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct RunRequest {
114 pub input: UserInput,
116 pub options: Option<ModelOptions>,
122 pub metadata: RunMetadata,
124}
125
126impl RunRequest {
127 pub fn text(input: impl Into<String>) -> Self {
129 Self {
130 input: UserInput::text(input),
131 options: None,
132 metadata: RunMetadata::new(),
133 }
134 }
135
136 pub fn blocks(blocks: Vec<ContentBlock>) -> Self {
138 Self {
139 input: UserInput::blocks(blocks),
140 options: None,
141 metadata: RunMetadata::new(),
142 }
143 }
144
145 pub fn with_options(mut self, options: ModelOptions) -> Self {
147 self.options = Some(options);
148 self
149 }
150
151 pub fn with_metadata(mut self, metadata: RunMetadata) -> Self {
153 self.metadata = metadata;
154 self
155 }
156}
157
158impl From<String> for RunRequest {
159 fn from(input: String) -> Self {
160 Self::text(input)
161 }
162}
163
164impl From<&str> for RunRequest {
165 fn from(input: &str) -> Self {
166 Self::text(input)
167 }
168}
169
170impl From<UserInput> for RunRequest {
171 fn from(input: UserInput) -> Self {
172 Self {
173 input,
174 options: None,
175 metadata: RunMetadata::new(),
176 }
177 }
178}
179
180#[derive(Clone)]
182pub struct RunContext {
183 pub run_id: String,
185 pub cancellation: CancellationToken,
187 pub deadline: Option<Instant>,
189 pub metadata: RunMetadata,
191}
192
193impl fmt::Debug for RunContext {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 f.debug_struct("RunContext")
196 .field("run_id", &self.run_id)
197 .field("cancellation", &"CancellationToken")
198 .field("deadline", &self.deadline)
199 .field("metadata", &self.metadata)
200 .finish()
201 }
202}
203
204impl RunContext {
205 pub fn generated() -> Self {
207 Self::new(generated_run_id())
208 }
209
210 pub fn new(run_id: impl Into<String>) -> Self {
212 Self {
213 run_id: run_id.into(),
214 cancellation: CancellationToken::new(),
215 deadline: None,
216 metadata: RunMetadata::new(),
217 }
218 }
219
220 pub fn with_cancellation(mut self, token: CancellationToken) -> Self {
222 self.cancellation = token;
223 self
224 }
225
226 pub fn with_deadline(mut self, deadline: Instant) -> Self {
228 self.deadline = Some(deadline);
229 self
230 }
231
232 pub fn with_timeout(self, timeout: Duration) -> Self {
234 self.with_deadline(Instant::now() + timeout)
235 }
236
237 pub fn with_metadata(mut self, metadata: RunMetadata) -> Self {
239 self.metadata = metadata;
240 self
241 }
242
243 pub fn is_cancelled(&self) -> bool {
245 self.cancellation.is_cancelled()
246 }
247
248 pub fn is_expired(&self) -> bool {
250 self.deadline
251 .is_some_and(|deadline| Instant::now() >= deadline)
252 }
253
254 pub fn remaining(&self) -> Option<Duration> {
259 self.deadline
260 .map(|deadline| deadline.saturating_duration_since(Instant::now()))
261 }
262}
263
264pub(crate) fn generated_run_id() -> String {
266 static START_NANOS: OnceLock<u128> = OnceLock::new();
267 static PROCESS_RUN_COUNTER: AtomicU64 = AtomicU64::new(0);
268
269 let start_nanos = *START_NANOS.get_or_init(|| {
270 SystemTime::now()
271 .duration_since(UNIX_EPOCH)
272 .unwrap_or_default()
273 .as_nanos()
274 });
275 let n = PROCESS_RUN_COUNTER.fetch_add(1, Ordering::Relaxed);
276 format!("run-{start_nanos}-{n}")
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284pub struct Artifact {
285 pub id: String,
287 pub label: Option<String>,
289 pub mime_type: Option<String>,
291 pub uri: Option<String>,
293 pub metadata: RunMetadata,
295}
296
297#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
299pub struct RunSummary {
300 pub rounds: usize,
302 pub tool_calls: usize,
304 pub usage: Usage,
311 pub usage_omitted: bool,
315 pub finish_reason: Option<FinishReason>,
317 pub latency: Duration,
319 pub provider_model: Option<String>,
321}
322
323#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
325pub struct RunOutput {
326 pub run_id: String,
328 pub answer: String,
330 pub summary: RunSummary,
332 pub final_message: Message,
334 pub artifacts: Vec<Artifact>,
336 pub metadata: RunMetadata,
338}
339
340#[derive(Debug, Clone, PartialEq)]
342pub struct TypedRunOutput<T> {
343 pub value: T,
345 pub output: RunOutput,
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use serde_json::json;
353
354 #[test]
355 fn user_input_converts_to_message() {
356 assert_eq!(UserInput::text("hi").into_message(), Message::user("hi"));
357 let blocks = vec![ContentBlock::Text("hi".into())];
358 assert_eq!(
359 UserInput::blocks(blocks.clone()).into_message(),
360 Message::user_blocks(blocks)
361 );
362 }
363
364 #[test]
365 fn run_request_builders_set_fields() {
366 let mut metadata = RunMetadata::new();
367 metadata.insert("trace".into(), json!("abc"));
368 let request = RunRequest::text("hi")
369 .with_options(ModelOptions {
370 temperature: Some(0.1),
371 ..Default::default()
372 })
373 .with_metadata(metadata.clone());
374
375 assert_eq!(request.input, UserInput::text("hi"));
376 assert_eq!(
377 request.options.as_ref().and_then(|o| o.temperature),
378 Some(0.1)
379 );
380 assert_eq!(request.metadata, metadata);
381 }
382
383 #[test]
384 fn run_context_helpers() {
385 let a = RunContext::generated();
386 let b = RunContext::generated();
387 assert_ne!(a.run_id, b.run_id);
388 assert!(a.run_id.starts_with("run-"));
389
390 let named = RunContext::new("request-42");
391 assert_eq!(named.run_id, "request-42");
392
393 let token = CancellationToken::new();
394 let cancelled = RunContext::new("cancel").with_cancellation(token.clone());
395 token.cancel();
396 assert!(cancelled.is_cancelled());
397
398 let expired = RunContext::new("expired").with_deadline(Instant::now());
399 assert!(expired.is_expired());
400 assert_eq!(expired.remaining(), Some(Duration::ZERO));
401 }
402}