1pub mod agent_tool;
5pub mod auto_dream;
6pub mod compact;
7pub mod context_analyzer;
8pub mod coordinator;
9pub mod delegate;
10pub mod delegate_tool;
11pub mod effort;
12pub mod events;
13pub mod reporters;
14mod runner;
15pub mod session_memory;
16pub mod system_prompt;
17
18pub use runner::apply_tool_result_budget;
20
21use cersei_hooks::Hook;
22use cersei_mcp::McpServerConfig;
23use cersei_memory::Memory;
24use cersei_provider::Provider;
25use cersei_tools::permissions::{AllowAll, PermissionPolicy};
26use cersei_tools::{CostTracker, Tool};
27use cersei_types::*;
28use events::AgentEvent;
29use std::path::PathBuf;
30use std::sync::Arc;
31use std::time::Duration;
32use tokio::sync::{broadcast, mpsc};
33
34pub use events::{AgentStream, CompactReason, WarningState};
36pub use reporters::Reporter;
37
38#[derive(Debug, Clone)]
41pub struct AgentOutput {
42 pub message: Message,
43 pub usage: Usage,
44 pub stop_reason: StopReason,
45 pub turns: u32,
46 pub tool_calls: Vec<ToolCallRecord>,
47}
48
49impl AgentOutput {
50 pub fn text(&self) -> &str {
51 self.message.get_text().unwrap_or("")
52 }
53}
54
55#[derive(Debug, Clone)]
56pub struct ToolCallRecord {
57 pub name: String,
58 pub id: String,
59 pub input: serde_json::Value,
60 pub result: String,
61 pub is_error: bool,
62 pub duration: Duration,
63}
64
65#[allow(dead_code)]
68pub struct Agent {
69 provider: Box<dyn Provider>,
70 tools: Vec<Box<dyn Tool>>,
71 system_prompt: Option<String>,
72 append_system_prompt: Option<String>,
73 model: Option<String>,
74 max_turns: u32,
75 max_tokens: u32,
76 temperature: Option<f32>,
77 thinking_budget: Option<u32>,
78 working_dir: PathBuf,
79 permission_policy: Arc<dyn PermissionPolicy>,
80 memory: Option<Arc<dyn Memory>>,
81 session_id: Option<String>,
82 hooks: Vec<Arc<dyn Hook>>,
83 mcp_manager: Option<Arc<cersei_mcp::McpManager>>,
84 event_handler: Option<Box<dyn Fn(&AgentEvent) + Send + Sync>>,
85 broadcast_tx: Option<broadcast::Sender<AgentEvent>>,
86 reporters: Vec<Arc<dyn Reporter>>,
87 event_filter: Option<Box<dyn Fn(&AgentEvent) -> bool + Send + Sync>>,
88 cost_tracker: Arc<CostTracker>,
89 auto_compact: bool,
90 compact_threshold: f64,
91 tool_result_budget: usize,
92 pub(crate) turns_elapsed_cadence: u32,
96 pub(crate) compression_level: Arc<parking_lot::Mutex<cersei_compression::CompressionLevel>>,
97 pub benchmark_mode: bool,
98 messages: Arc<parking_lot::Mutex<Vec<Message>>>,
99 cumulative_usage: Arc<parking_lot::Mutex<Usage>>,
100 cancel_token: tokio_util::sync::CancellationToken,
101}
102
103impl Agent {
104 pub fn builder() -> AgentBuilder {
105 AgentBuilder::default()
106 }
107
108 pub async fn run(&self, prompt: &str) -> cersei_types::Result<AgentOutput> {
110 runner::run_agent(self, prompt).await
111 }
112
113 pub fn run_stream(self: &Arc<Self>, prompt: &str) -> AgentStream {
116 let (event_tx, event_rx) = mpsc::channel(512);
117 let (control_tx, control_rx) = mpsc::channel(64);
118
119 let prompt = prompt.to_string();
120 let agent = Arc::clone(self);
121
122 tokio::spawn(async move {
123 let result =
124 runner::run_agent_streaming(&agent, &prompt, event_tx.clone(), control_rx).await;
125 match result {
126 Ok(output) => {
127 let _ = event_tx.send(AgentEvent::Complete(output)).await;
128 }
129 Err(e) => {
130 let _ = event_tx.send(AgentEvent::Error(e.to_string())).await;
131 }
132 }
133 });
134
135 AgentStream::new(event_rx, control_tx)
136 }
137
138 pub async fn reply(&self, message: &str) -> cersei_types::Result<AgentOutput> {
140 runner::run_agent(self, message).await
141 }
142
143 pub fn messages(&self) -> Vec<Message> {
145 self.messages.lock().clone()
146 }
147
148 pub fn usage(&self) -> Usage {
150 self.cumulative_usage.lock().clone()
151 }
152
153 pub fn cancel(&self) {
155 self.cancel_token.cancel();
156 }
157
158 pub fn compression_level(&self) -> cersei_compression::CompressionLevel {
160 *self.compression_level.lock()
161 }
162
163 pub fn set_compression_level(&self, level: cersei_compression::CompressionLevel) {
166 *self.compression_level.lock() = level;
167 }
168
169 pub fn subscribe(&self) -> Option<broadcast::Receiver<AgentEvent>> {
171 self.broadcast_tx.as_ref().map(|tx| tx.subscribe())
172 }
173
174 pub(crate) fn emit(&self, event: AgentEvent) {
176 if let Some(filter) = &self.event_filter {
178 if !filter(&event) {
179 return;
180 }
181 }
182
183 if let Some(handler) = &self.event_handler {
185 handler(&event);
186 }
187
188 if let Some(tx) = &self.broadcast_tx {
190 let _ = tx.send(event.clone());
191 }
192
193 for reporter in &self.reporters {
195 let reporter = Arc::clone(reporter);
196 let event = event.clone();
197 tokio::spawn(async move {
198 reporter.on_event(&event).await;
199 });
200 }
201 }
202}
203
204pub struct AgentBuilder {
207 provider: Option<Box<dyn Provider>>,
208 tools: Vec<Box<dyn Tool>>,
209 system_prompt: Option<String>,
210 append_system_prompt: Option<String>,
211 model: Option<String>,
212 max_turns: u32,
213 max_tokens: u32,
214 temperature: Option<f32>,
215 thinking_budget: Option<u32>,
216 working_dir: Option<PathBuf>,
217 permission_policy: Option<Arc<dyn PermissionPolicy>>,
218 memory: Option<Arc<dyn Memory>>,
219 session_id: Option<String>,
220 hooks: Vec<Arc<dyn Hook>>,
221 mcp_servers: Vec<McpServerConfig>,
222 event_handler: Option<Box<dyn Fn(&AgentEvent) + Send + Sync>>,
223 broadcast_capacity: Option<usize>,
224 reporters: Vec<Arc<dyn Reporter>>,
225 event_filter: Option<Box<dyn Fn(&AgentEvent) -> bool + Send + Sync>>,
226 cancel_token: Option<tokio_util::sync::CancellationToken>,
227 auto_compact: bool,
228 compact_threshold: f64,
229 tool_result_budget: usize,
230 turns_elapsed_cadence: u32,
231 compression_level: cersei_compression::CompressionLevel,
232 initial_messages: Option<Vec<Message>>,
233 benchmark_mode: bool,
234}
235
236impl Default for AgentBuilder {
237 fn default() -> Self {
238 Self {
239 provider: None,
240 tools: Vec::new(),
241 system_prompt: None,
242 append_system_prompt: None,
243 model: None,
244 max_turns: 10,
245 max_tokens: 16384,
246 temperature: None,
247 thinking_budget: None,
248 working_dir: None,
249 permission_policy: None,
250 memory: None,
251 session_id: None,
252 hooks: Vec::new(),
253 mcp_servers: Vec::new(),
254 event_handler: None,
255 broadcast_capacity: None,
256 reporters: Vec::new(),
257 event_filter: None,
258 cancel_token: None,
259 auto_compact: true,
260 compact_threshold: 0.9,
261 tool_result_budget: 50_000,
262 turns_elapsed_cadence: 10,
263 compression_level: cersei_compression::CompressionLevel::Off,
264 initial_messages: None,
265 benchmark_mode: false,
266 }
267 }
268}
269
270impl AgentBuilder {
271 pub fn provider(mut self, p: impl Provider + 'static) -> Self {
272 self.provider = Some(Box::new(p));
273 self
274 }
275
276 pub fn provider_boxed(mut self, p: Box<dyn Provider>) -> Self {
280 self.provider = Some(p);
281 self
282 }
283
284 pub fn tool(mut self, t: impl Tool + 'static) -> Self {
285 self.tools.push(Box::new(t));
286 self
287 }
288
289 pub fn tools(mut self, ts: Vec<Box<dyn Tool>>) -> Self {
290 self.tools.extend(ts);
291 self
292 }
293
294 pub fn system_prompt(mut self, s: impl Into<String>) -> Self {
295 self.system_prompt = Some(s.into());
296 self
297 }
298
299 pub fn append_system_prompt(mut self, s: impl Into<String>) -> Self {
300 self.append_system_prompt = Some(s.into());
301 self
302 }
303
304 pub fn model(mut self, m: impl Into<String>) -> Self {
305 self.model = Some(m.into());
306 self
307 }
308
309 pub fn max_turns(mut self, n: u32) -> Self {
310 self.max_turns = n;
311 self
312 }
313
314 pub fn max_tokens(mut self, n: u32) -> Self {
315 self.max_tokens = n;
316 self
317 }
318
319 pub fn temperature(mut self, t: f32) -> Self {
320 self.temperature = Some(t);
321 self
322 }
323
324 pub fn thinking_budget(mut self, tokens: u32) -> Self {
325 self.thinking_budget = Some(tokens);
326 self
327 }
328
329 pub fn working_dir(mut self, p: impl Into<PathBuf>) -> Self {
330 self.working_dir = Some(p.into());
331 self
332 }
333
334 pub fn permission_policy(mut self, p: impl PermissionPolicy + 'static) -> Self {
335 self.permission_policy = Some(Arc::new(p));
336 self
337 }
338
339 pub fn memory(mut self, m: impl Memory + 'static) -> Self {
340 self.memory = Some(Arc::new(m));
341 self
342 }
343
344 pub fn session_id(mut self, id: impl Into<String>) -> Self {
345 self.session_id = Some(id.into());
346 self
347 }
348
349 pub fn hook(mut self, h: impl Hook + 'static) -> Self {
350 self.hooks.push(Arc::new(h));
351 self
352 }
353
354 pub fn mcp_server(mut self, config: McpServerConfig) -> Self {
355 self.mcp_servers.push(config);
356 self
357 }
358
359 pub fn on_event(mut self, f: impl Fn(&AgentEvent) + Send + Sync + 'static) -> Self {
360 self.event_handler = Some(Box::new(f));
361 self
362 }
363
364 pub fn enable_broadcast(mut self, capacity: usize) -> Self {
365 self.broadcast_capacity = Some(capacity);
366 self
367 }
368
369 pub fn reporter(mut self, r: impl Reporter + 'static) -> Self {
370 self.reporters.push(Arc::new(r));
371 self
372 }
373
374 pub fn event_filter(mut self, f: impl Fn(&AgentEvent) -> bool + Send + Sync + 'static) -> Self {
375 self.event_filter = Some(Box::new(f));
376 self
377 }
378
379 pub fn cancel_token(mut self, token: tokio_util::sync::CancellationToken) -> Self {
380 self.cancel_token = Some(token);
381 self
382 }
383
384 pub fn auto_compact(mut self, enabled: bool) -> Self {
385 self.auto_compact = enabled;
386 self
387 }
388
389 pub fn compact_threshold(mut self, threshold: f64) -> Self {
390 self.compact_threshold = threshold;
391 self
392 }
393
394 pub fn tool_result_budget(mut self, chars: usize) -> Self {
395 self.tool_result_budget = chars;
396 self
397 }
398
399 pub fn turns_elapsed_cadence(mut self, n: u32) -> Self {
405 self.turns_elapsed_cadence = n;
406 self
407 }
408
409 pub fn compression_level(mut self, level: cersei_compression::CompressionLevel) -> Self {
410 self.compression_level = level;
411 self
412 }
413
414 pub fn with_messages(mut self, msgs: Vec<Message>) -> Self {
416 self.initial_messages = Some(msgs);
417 self
418 }
419
420 pub fn benchmark_mode(mut self, enabled: bool) -> Self {
422 self.benchmark_mode = enabled;
423 self
424 }
425
426 pub fn build(self) -> cersei_types::Result<Agent> {
427 let provider = self
428 .provider
429 .ok_or_else(|| CerseiError::Config("Provider is required".into()))?;
430
431 let working_dir = self
432 .working_dir
433 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
434
435 let broadcast_tx = self.broadcast_capacity.map(|cap| {
436 let (tx, _) = broadcast::channel(cap);
437 tx
438 });
439
440 Ok(Agent {
441 provider,
442 tools: self.tools,
443 system_prompt: self.system_prompt,
444 append_system_prompt: self.append_system_prompt,
445 model: self.model,
446 max_turns: self.max_turns,
447 max_tokens: self.max_tokens,
448 temperature: self.temperature,
449 thinking_budget: self.thinking_budget,
450 working_dir,
451 permission_policy: self.permission_policy.unwrap_or_else(|| Arc::new(AllowAll)),
452 memory: self.memory,
453 session_id: self.session_id,
454 hooks: self.hooks,
455 mcp_manager: None, event_handler: self.event_handler,
457 broadcast_tx,
458 reporters: self.reporters,
459 event_filter: self.event_filter,
460 cost_tracker: Arc::new(CostTracker::new()),
461 auto_compact: self.auto_compact,
462 compact_threshold: self.compact_threshold,
463 tool_result_budget: self.tool_result_budget,
464 turns_elapsed_cadence: if self.turns_elapsed_cadence == 0 {
465 u32::MAX
466 } else {
467 self.turns_elapsed_cadence
468 },
469 compression_level: Arc::new(parking_lot::Mutex::new(self.compression_level)),
470 benchmark_mode: self.benchmark_mode,
471 messages: Arc::new(parking_lot::Mutex::new(
472 self.initial_messages.unwrap_or_default(),
473 )),
474 cumulative_usage: Arc::new(parking_lot::Mutex::new(Usage::default())),
475 cancel_token: self
476 .cancel_token
477 .unwrap_or_else(tokio_util::sync::CancellationToken::new),
478 })
479 }
480
481 pub async fn run_with(self, prompt: &str) -> cersei_types::Result<AgentOutput> {
483 self.build()?.run(prompt).await
484 }
485}