1use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum RuntimeSignal {
15 Interrupt,
16 Terminate,
17 Hangup,
18}
19
20impl RuntimeSignal {
21 pub fn as_str(self) -> &'static str {
22 match self {
23 RuntimeSignal::Interrupt => "interrupt",
24 RuntimeSignal::Terminate => "terminate",
25 RuntimeSignal::Hangup => "hangup",
26 }
27 }
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct RuntimeTimelineEvent {
33 pub kind: RuntimeTimelineKind,
34 pub message: String,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum RuntimeTimelineKind {
40 Signal,
41 Process,
42 Tool,
43 Provider,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct ProviderCapabilitySnapshot {
49 pub provider: String,
50 pub model: String,
51 pub supports_tools: bool,
52 pub supports_vision: bool,
53 pub reasoning: String,
54 pub max_context_tokens: Option<usize>,
55}
56
57impl ProviderCapabilitySnapshot {
58 pub fn from_model_id(model_id: &str) -> Self {
63 let (provider, model) = match model_id.split_once('/') {
64 Some((provider, model)) if !provider.is_empty() && !model.is_empty() => {
65 (provider.to_ascii_lowercase(), model.to_string())
66 },
67 _ => ("ollama".to_string(), model_id.to_string()),
68 };
69
70 let (supports_tools, supports_vision, reasoning) = match provider.as_str() {
71 "anthropic" => (true, true, "adaptive".to_string()),
72 "gemini" => (true, true, "thinking_level".to_string()),
73 "ollama" => (true, false, "binary".to_string()),
74 _ => (true, false, "effort".to_string()),
75 };
76
77 let max_context_tokens = infer_static_context_window(&provider, &model);
78
79 Self {
80 provider,
81 model,
82 supports_tools,
83 supports_vision,
84 reasoning,
85 max_context_tokens,
86 }
87 }
88}
89
90fn infer_static_context_window(provider: &str, model: &str) -> Option<usize> {
91 let model = model.to_ascii_lowercase();
92 match provider {
93 "anthropic" => Some(200_000),
94 "gemini" => Some(1_000_000),
95 "openai" if model.contains("gpt-4.1") || model.contains("gpt-5") => Some(400_000),
96 "openrouter" if model.contains("claude") => Some(200_000),
97 _ => None,
98 }
99}
100
101pub fn infer_static_context_window_for_model_id(model_id: &str) -> Option<usize> {
102 let (provider, model) = match model_id.split_once('/') {
103 Some((provider, model)) if !provider.is_empty() && !model.is_empty() => {
104 (provider.to_ascii_lowercase(), model.to_string())
105 },
106 _ => ("ollama".to_string(), model_id.to_string()),
107 };
108 infer_static_context_window(&provider, &model)
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub enum ManagedProcessStatus {
116 Running,
117 Exited,
118 Unknown,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct ManagedProcess {
124 pub id: String,
125 pub pid: u32,
126 pub command: String,
127 pub cwd: Option<String>,
128 pub log_path: String,
129 pub detected_url: Option<String>,
130 pub status: ManagedProcessStatus,
131}
132
133#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
135pub struct ToolRunMetadata {
136 #[serde(default)]
137 pub detail: ToolMetadata,
138 pub line_count: Option<usize>,
139 pub byte_count: Option<usize>,
140 pub result_count: Option<usize>,
141 pub duration_secs: Option<f64>,
142 pub process: Option<ManagedProcess>,
143 #[serde(default)]
147 pub display_diff: Option<String>,
148 #[serde(default)]
149 pub diff_truncated: bool,
150 #[serde(default)]
151 pub artifacts: Vec<ToolArtifact>,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "snake_case")]
157pub enum ToolStatus {
158 Success,
159 Error,
160 Cancelled,
161}
162
163#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
165#[serde(tag = "kind", rename_all = "snake_case")]
166pub enum ToolMetadata {
167 #[default]
168 None,
169 ReadFile {
170 paths: Vec<String>,
171 line_count: usize,
172 byte_count: usize,
173 truncated: bool,
174 },
175 WriteFile {
176 path: String,
177 line_count: usize,
178 byte_count: usize,
179 created: Option<bool>,
180 },
181 EditFile {
182 path: String,
183 replacements: usize,
184 },
185 DeleteFile {
186 path: String,
187 },
188 CreateDirectory {
189 path: String,
190 },
191 WebSearch {
192 queries: Vec<String>,
193 requested_count: usize,
194 result_count: usize,
195 sources: Vec<String>,
196 },
197 WebFetch {
198 url: String,
199 title: Option<String>,
200 line_count: usize,
201 byte_count: usize,
202 },
203 ExecuteCommand {
204 command: String,
205 working_dir: Option<String>,
206 exit_code: Option<i32>,
207 timed_out: bool,
208 background: bool,
209 stdout_lines: usize,
210 stderr_lines: usize,
211 detected_urls: Vec<String>,
212 pid: Option<u32>,
213 log_path: Option<String>,
214 },
215 ComputerUse {
216 action: String,
217 params: Value,
218 },
219 Mcp {
220 server: String,
221 tool: String,
222 },
223 Subagent {
224 model_id: String,
225 },
226 Custom {
227 name: String,
228 data: Value,
229 },
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(tag = "kind", rename_all = "snake_case")]
236pub enum ToolArtifact {
237 Image { data: String },
238 File { path: String },
239 Log { path: String },
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct RuntimeState {
246 pub provider_capabilities: ProviderCapabilitySnapshot,
247 #[serde(default)]
248 pub processes: Vec<ManagedProcess>,
249 #[serde(default)]
250 pub timeline: Vec<RuntimeTimelineEvent>,
251 #[serde(default)]
257 pub builtin_tool_schema_tokens: usize,
258}
259
260impl RuntimeState {
261 pub fn new(model_id: &str) -> Self {
262 Self {
263 provider_capabilities: ProviderCapabilitySnapshot::from_model_id(model_id),
264 processes: Vec::new(),
265 timeline: Vec::new(),
266 builtin_tool_schema_tokens: 0,
267 }
268 }
269
270 pub fn set_model(&mut self, model_id: &str) {
271 self.provider_capabilities = ProviderCapabilitySnapshot::from_model_id(model_id);
272 self.timeline.push(RuntimeTimelineEvent {
273 kind: RuntimeTimelineKind::Provider,
274 message: format!("model set to {}", model_id),
275 });
276 }
277
278 pub fn record_signal(&mut self, signal: RuntimeSignal) {
279 self.timeline.push(RuntimeTimelineEvent {
280 kind: RuntimeTimelineKind::Signal,
281 message: format!("received {}", signal.as_str()),
282 });
283 }
284
285 pub fn register_process(&mut self, process: ManagedProcess) {
286 if let Some(existing) = self.processes.iter_mut().find(|p| p.pid == process.pid) {
287 *existing = process.clone();
288 } else {
289 self.processes.push(process.clone());
290 }
291 self.timeline.push(RuntimeTimelineEvent {
292 kind: RuntimeTimelineKind::Process,
293 message: format!("registered process {} ({})", process.pid, process.command),
294 });
295 }
296}
297
298impl Default for RuntimeState {
299 fn default() -> Self {
300 Self::new("ollama/unknown")
301 }
302}