Skip to main content

car_multi/
types.rs

1//! Core types for multi-agent coordination.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7
8/// Blueprint for an agent in a multi-agent system.
9///
10/// The runtime doesn't own the model — `metadata` carries provider/model info
11/// that the caller's `AgentRunner` implementation uses to select the right model.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AgentSpec {
14    pub name: String,
15    pub system_prompt: String,
16    /// Tool names this agent is allowed to use.
17    pub tools: Vec<String>,
18    pub max_turns: u32,
19    /// Opaque metadata (provider, model, temperature, etc.).
20    pub metadata: HashMap<String, Value>,
21    /// Enable prompt caching for this agent's API calls.
22    /// When true, the system prompt is marked for Anthropic prompt cache reuse.
23    #[serde(default)]
24    pub cache_control: bool,
25}
26
27impl AgentSpec {
28    pub fn new(name: &str, system_prompt: &str) -> Self {
29        Self {
30            name: name.to_string(),
31            system_prompt: system_prompt.to_string(),
32            tools: Vec::new(),
33            max_turns: 10,
34            metadata: HashMap::new(),
35            cache_control: false,
36        }
37    }
38
39    pub fn with_tools(mut self, tools: Vec<String>) -> Self {
40        self.tools = tools;
41        self
42    }
43
44    pub fn with_max_turns(mut self, max_turns: u32) -> Self {
45        self.max_turns = max_turns;
46        self
47    }
48
49    pub fn with_metadata(mut self, key: &str, value: Value) -> Self {
50        self.metadata.insert(key.to_string(), value);
51        self
52    }
53
54    pub fn with_cache_control(mut self) -> Self {
55        self.cache_control = true;
56        self
57    }
58}
59
60/// Token and cost accounting contributed by an AgentRunner. Runners opt in by
61/// setting `AgentOutput::tokens`; benchmarks that need economic metrics read it.
62///
63/// Self-reported by the runner; not verified by CAR. Do not use as a trust
64/// boundary — for benchmarking and observability only.
65#[derive(Debug, Clone, Default, Serialize, Deserialize)]
66pub struct TokenAccounting {
67    #[serde(default)]
68    pub input_tokens: u64,
69    #[serde(default)]
70    pub output_tokens: u64,
71    #[serde(default)]
72    pub cost_usd: f64,
73    /// Per-run latency, when the runner measured it. Rides here (rather than a
74    /// new `AgentOutput` field) because every `AgentOutput` already carries
75    /// `Option<TokenAccounting>` and consumers already read it. `None` for
76    /// runners that don't measure it — so the FFI JSON of multi-agent results
77    /// stays unchanged for them.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub latency: Option<RunLatency>,
80}
81
82/// Per-run latency a runner reports alongside token/cost accounting.
83/// Self-reported, not verified — benchmarking/observability only.
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85pub struct RunLatency {
86    /// Sum of model GENERATION (decode) wall-clock across turns, in ms,
87    /// EXCLUDING tool execution. The honest denominator for decode tokens/sec
88    /// (`output_tokens / generation_ms`) — total wall-clock would conflate decode
89    /// with tool latency.
90    #[serde(default)]
91    pub generation_ms: f64,
92    /// Time-to-first-token of the FIRST generation call, in ms, when the backend
93    /// measures it (`InferenceEngine::generate_tracked` → `time_to_first_token_ms`;
94    /// `None` on paths that don't surface it).
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub ttft_ms: Option<u64>,
97}
98
99impl TokenAccounting {
100    pub fn new(input_tokens: u64, output_tokens: u64, cost_usd: f64) -> Self {
101        debug_assert!(cost_usd.is_finite(), "cost_usd must be finite");
102        debug_assert!(cost_usd >= 0.0, "cost_usd must be non-negative");
103        Self {
104            input_tokens,
105            output_tokens,
106            cost_usd,
107            latency: None,
108        }
109    }
110
111    /// Attach measured per-run latency. Builder so the 4 `::new()` call sites
112    /// that don't measure latency are unaffected.
113    pub fn with_latency(mut self, generation_ms: f64, ttft_ms: Option<u64>) -> Self {
114        self.latency = Some(RunLatency {
115            generation_ms,
116            ttft_ms,
117        });
118        self
119    }
120}
121
122/// Output from one agent's execution.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct AgentOutput {
125    pub name: String,
126    pub answer: String,
127    pub turns: u32,
128    pub tool_calls: u32,
129    pub duration_ms: f64,
130    pub error: Option<String>,
131    /// Structured outcome (when available). Products can use this instead of
132    /// checking answer/error fields for completion semantics.
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub outcome: Option<car_ir::AgentOutcome>,
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub tokens: Option<TokenAccounting>,
137    /// Names of the tools the agent invoked, in first-use order. Empty when the
138    /// runner doesn't track tools (most aggregate/combinator outputs). Lets
139    /// downstream consumers (e.g. benchmark failure mining) reason about which
140    /// tools a run actually used.
141    #[serde(default, skip_serializing_if = "Vec::is_empty")]
142    pub tools_used: Vec<String>,
143}
144
145impl AgentOutput {
146    pub fn succeeded(&self) -> bool {
147        self.error.is_none() && !self.answer.is_empty()
148    }
149}
150
151/// A message between agents in a multi-agent system.
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct Message {
154    pub from: String,
155    pub to: String,
156    pub kind: MessageKind,
157    pub payload: Value,
158    pub timestamp: DateTime<Utc>,
159}
160
161impl Message {
162    pub fn new(from: &str, to: &str, kind: MessageKind, payload: Value) -> Self {
163        Self {
164            from: from.to_string(),
165            to: to.to_string(),
166            kind,
167            payload,
168            timestamp: Utc::now(),
169        }
170    }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174#[serde(rename_all = "snake_case")]
175pub enum MessageKind {
176    /// Task assignment from orchestrator.
177    TaskAssignment,
178    /// Agent's completed result.
179    Result,
180    /// Feedback from supervisor.
181    Feedback,
182    /// Delegation request.
183    DelegateRequest,
184    /// Delegation response.
185    DelegateResponse,
186    /// Custom inter-agent message.
187    Custom,
188}