use futures::stream::BoxStream; use serde::{Deserialize, Serialize}; use serde_json::Value; /// Provider-neutral conversation role. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ChatRole { User, Assistant, } /// Provider-neutral message content. Tool results travel as user-role parts, /// matching both wire formats' conventions. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ContentPart { // Struct variant (not newtype): internally-tagged enums cannot // serialize newtype primitives, and this type round-trips through // `agent_runs.checkpoint`. Text { text: String, }, ToolUse { id: String, name: String, input: Value, }, ToolResult { tool_use_id: String, content: Value, }, } impl ContentPart { /// Convenience constructor for plain text parts. pub fn text(value: impl Into) -> ContentPart { ContentPart::Text { text: value.into() } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ChatMessage { pub role: ChatRole, pub parts: Vec, } /// A tool offered to the model. `input_schema` is JSON Schema. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ToolDescriptor { pub name: String, pub description: String, pub input_schema: Value, } /// The checkpointable, provider-neutral request. This struct is what gets /// serialized into `agent_runs.checkpoint`, so resume works identically /// across providers. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ChatRequest { pub system: String, pub messages: Vec, pub tools: Vec, pub model: String, pub max_tokens: u32, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum StopReason { EndTurn, ToolUse, MaxTokens, } /// Streaming events every provider normalizes to. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum LlmEvent { TextDelta(String), ToolUse { id: String, name: String, input: Value, }, /// Token accounting for this provider call (drives credit metering). Usage { input_tokens: u32, output_tokens: u32, }, Stop(StopReason), } #[derive(Debug, thiserror::Error)] pub enum LlmError { #[error("scenario error: {0}")] Scenario(String), #[error("transport error: {0}")] Transport(String), #[error("provider returned an error: {0}")] Api(String), #[error("malformed provider response: {0}")] Wire(String), } pub type EventStream = BoxStream<'static, Result>; #[async_trait::async_trait] pub trait LlmProvider: Send + Sync { async fn stream(&self, request: ChatRequest) -> Result; }