use crate::message::{ContentBlock, Message};
use crate::provider::{FinishReason, ModelOptions, Usage};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio_util::sync::CancellationToken;
pub type RunMetadata = BTreeMap<String, serde_json::Value>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum UserInput {
Text(String),
Blocks(Vec<ContentBlock>),
}
impl UserInput {
pub fn text(input: impl Into<String>) -> Self {
Self::Text(input.into())
}
pub fn blocks(blocks: Vec<ContentBlock>) -> Self {
Self::Blocks(blocks)
}
pub fn into_message(self) -> Message {
match self {
Self::Text(input) => Message::user(input),
Self::Blocks(blocks) => Message::user_blocks(blocks),
}
}
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(input) => Some(input),
Self::Blocks(_) => None,
}
}
}
impl From<String> for UserInput {
fn from(input: String) -> Self {
Self::Text(input)
}
}
impl From<&str> for UserInput {
fn from(input: &str) -> Self {
Self::Text(input.to_string())
}
}
impl From<Vec<ContentBlock>> for UserInput {
fn from(blocks: Vec<ContentBlock>) -> Self {
Self::Blocks(blocks)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunRequest {
pub input: UserInput,
pub options: Option<ModelOptions>,
pub metadata: RunMetadata,
}
impl RunRequest {
pub fn text(input: impl Into<String>) -> Self {
Self {
input: UserInput::text(input),
options: None,
metadata: RunMetadata::new(),
}
}
pub fn blocks(blocks: Vec<ContentBlock>) -> Self {
Self {
input: UserInput::blocks(blocks),
options: None,
metadata: RunMetadata::new(),
}
}
pub fn with_options(mut self, options: ModelOptions) -> Self {
self.options = Some(options);
self
}
pub fn with_metadata(mut self, metadata: RunMetadata) -> Self {
self.metadata = metadata;
self
}
}
impl From<String> for RunRequest {
fn from(input: String) -> Self {
Self::text(input)
}
}
impl From<&str> for RunRequest {
fn from(input: &str) -> Self {
Self::text(input)
}
}
impl From<UserInput> for RunRequest {
fn from(input: UserInput) -> Self {
Self {
input,
options: None,
metadata: RunMetadata::new(),
}
}
}
#[derive(Clone)]
pub struct RunContext {
pub run_id: String,
pub cancellation: CancellationToken,
pub deadline: Option<Instant>,
pub metadata: RunMetadata,
}
impl fmt::Debug for RunContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RunContext")
.field("run_id", &self.run_id)
.field("cancellation", &"CancellationToken")
.field("deadline", &self.deadline)
.field("metadata", &self.metadata)
.finish()
}
}
impl RunContext {
pub fn generated() -> Self {
Self::new(generated_run_id())
}
pub fn new(run_id: impl Into<String>) -> Self {
Self {
run_id: run_id.into(),
cancellation: CancellationToken::new(),
deadline: None,
metadata: RunMetadata::new(),
}
}
pub fn with_cancellation(mut self, token: CancellationToken) -> Self {
self.cancellation = token;
self
}
pub fn with_deadline(mut self, deadline: Instant) -> Self {
self.deadline = Some(deadline);
self
}
pub fn with_timeout(self, timeout: Duration) -> Self {
self.with_deadline(Instant::now() + timeout)
}
pub fn with_metadata(mut self, metadata: RunMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn is_cancelled(&self) -> bool {
self.cancellation.is_cancelled()
}
pub fn is_expired(&self) -> bool {
self.deadline
.is_some_and(|deadline| Instant::now() >= deadline)
}
pub fn remaining(&self) -> Option<Duration> {
self.deadline
.map(|deadline| deadline.saturating_duration_since(Instant::now()))
}
}
pub(crate) fn generated_run_id() -> String {
static START_NANOS: OnceLock<u128> = OnceLock::new();
static PROCESS_RUN_COUNTER: AtomicU64 = AtomicU64::new(0);
let start_nanos = *START_NANOS.get_or_init(|| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
});
let n = PROCESS_RUN_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("run-{start_nanos}-{n}")
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Artifact {
pub id: String,
pub label: Option<String>,
pub mime_type: Option<String>,
pub uri: Option<String>,
pub metadata: RunMetadata,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunSummary {
pub rounds: usize,
pub tool_calls: usize,
pub usage: Usage,
pub usage_omitted: bool,
pub finish_reason: Option<FinishReason>,
pub latency: Duration,
pub provider_model: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunOutput {
pub run_id: String,
pub answer: String,
pub summary: RunSummary,
pub final_message: Message,
pub artifacts: Vec<Artifact>,
pub metadata: RunMetadata,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TypedRunOutput<T> {
pub value: T,
pub output: RunOutput,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn user_input_converts_to_message() {
assert_eq!(UserInput::text("hi").into_message(), Message::user("hi"));
let blocks = vec![ContentBlock::Text("hi".into())];
assert_eq!(
UserInput::blocks(blocks.clone()).into_message(),
Message::user_blocks(blocks)
);
}
#[test]
fn run_request_builders_set_fields() {
let mut metadata = RunMetadata::new();
metadata.insert("trace".into(), json!("abc"));
let request = RunRequest::text("hi")
.with_options(ModelOptions {
temperature: Some(0.1),
..Default::default()
})
.with_metadata(metadata.clone());
assert_eq!(request.input, UserInput::text("hi"));
assert_eq!(
request.options.as_ref().and_then(|o| o.temperature),
Some(0.1)
);
assert_eq!(request.metadata, metadata);
}
#[test]
fn run_context_helpers() {
let a = RunContext::generated();
let b = RunContext::generated();
assert_ne!(a.run_id, b.run_id);
assert!(a.run_id.starts_with("run-"));
let named = RunContext::new("request-42");
assert_eq!(named.run_id, "request-42");
let token = CancellationToken::new();
let cancelled = RunContext::new("cancel").with_cancellation(token.clone());
token.cancel();
assert!(cancelled.is_cancelled());
let expired = RunContext::new("expired").with_deadline(Instant::now());
assert!(expired.is_expired());
assert_eq!(expired.remaining(), Some(Duration::ZERO));
}
}