1use std::sync::{Arc, RwLock};
7
8use crate::compact::Compactor;
9use crate::error::Result;
10use crate::event::{EventSink, NullSink};
11use crate::hooks::HookRegistry;
12use crate::kernel::AgentKernelBuilder;
13use crate::llm::ChatProvider;
14use crate::message::Message;
15use crate::tools::plan_mode::{
16 EnterPlanModeTool, ExitPlanModeTool, PlanApprovalGate, PlanModeRequestGate, RequestPlanModeTool,
17};
18use crate::tools::{TodoItem, TodoWriteTool, ToolRegistry};
19
20use super::{AgentRuntime, CheckpointState, SessionLifecycle};
21
22pub struct AgentRuntimeBuilder {
29 kernel_builder: AgentKernelBuilder,
30 system_prompt: Option<String>,
31 seed: Vec<Message>,
32 streaming: bool,
33 saved_event_sink: Option<Arc<dyn EventSink>>,
34 compactor: Option<Compactor>,
35 microcompactor: Option<crate::compact::Microcompactor>,
36 with_plan_mode_tools: bool,
44 goal_eval_transcript_tail: usize,
46 skills: Vec<crate::skills::Skill>,
48 prompt_segments: Option<crate::system_prompt::PromptSegments>,
51 file_reinjector: Option<crate::compact::FileReinjector>,
54 skill_reinjector: Option<crate::compact::SkillReinjector>,
57}
58
59impl std::fmt::Debug for AgentRuntimeBuilder {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct("AgentRuntimeBuilder")
62 .field("kernel_builder", &self.kernel_builder)
63 .field("system_prompt", &self.system_prompt)
64 .field("seed", &self.seed)
65 .field("streaming", &self.streaming)
66 .field(
67 "event_sink",
68 &self.saved_event_sink.as_ref().map(|_| "<EventSink>"),
69 )
70 .field("goal_eval_transcript_tail", &self.goal_eval_transcript_tail)
71 .field("file_reinjector", &self.file_reinjector.is_some())
72 .field("skill_reinjector", &self.skill_reinjector.is_some())
73 .finish()
74 }
75}
76
77impl Default for AgentRuntimeBuilder {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83impl AgentRuntimeBuilder {
84 pub fn new() -> Self {
86 Self {
87 kernel_builder: AgentKernelBuilder::default(),
88 system_prompt: None,
89 seed: Vec::new(),
90 streaming: false,
91 saved_event_sink: None,
92 compactor: None,
93 microcompactor: None,
94 with_plan_mode_tools: false,
95 goal_eval_transcript_tail: 12,
96 skills: Vec::new(),
97 prompt_segments: None,
98 file_reinjector: None,
99 skill_reinjector: None,
100 }
101 }
102
103 pub fn prompt_segments(mut self, segments: crate::system_prompt::PromptSegments) -> Self {
117 self.prompt_segments = Some(segments);
118 self
119 }
120
121 pub fn with_plan_mode_tools(mut self, enabled: bool) -> Self {
126 self.with_plan_mode_tools = enabled;
127 self
128 }
129
130 pub fn llm(mut self, llm: Arc<dyn ChatProvider>) -> Self {
132 self.kernel_builder = self.kernel_builder.llm(llm);
133 self
134 }
135
136 pub fn tools(mut self, tools: ToolRegistry) -> Self {
138 self.kernel_builder = self.kernel_builder.tools(tools);
139 self
140 }
141
142 pub fn skills(mut self, skills: Vec<crate::skills::Skill>) -> Self {
144 self.skills = skills;
145 self
146 }
147
148 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
152 self.system_prompt = Some(prompt.into());
153 self
154 }
155
156 pub fn max_steps(mut self, n: usize) -> Self {
158 self.kernel_builder = self.kernel_builder.max_steps(n);
159 self
160 }
161
162 pub fn max_transcript_chars(mut self, n: usize) -> Self {
164 self.kernel_builder = self.kernel_builder.max_transcript_chars(n);
165 self
166 }
167
168 pub fn compactor(mut self, compactor: Compactor) -> Self {
170 self.kernel_builder = self.kernel_builder.compactor(compactor.clone());
174 self.compactor = Some(compactor);
175 self
176 }
177
178 pub fn microcompactor(mut self, microcompactor: crate::compact::Microcompactor) -> Self {
181 self.kernel_builder = self.kernel_builder.microcompactor(microcompactor.clone());
182 self.microcompactor = Some(microcompactor);
183 self
184 }
185
186 pub fn streaming(mut self, enabled: bool) -> Self {
188 self.streaming = enabled;
189 self
190 }
191
192 pub fn hooks(mut self, hooks: HookRegistry) -> Self {
194 self.kernel_builder = self.kernel_builder.hooks(hooks);
195 self
196 }
197
198 pub fn seed_transcript(mut self, messages: Vec<Message>) -> Self {
203 self.seed = messages;
204 self
205 }
206
207 pub fn event_sink(mut self, sink: Arc<dyn EventSink>) -> Self {
209 self.saved_event_sink = Some(sink);
210 self
211 }
212
213 pub fn shutdown_token(mut self, token: tokio_util::sync::CancellationToken) -> Self {
219 self.kernel_builder = self.kernel_builder.shutdown_token(token);
220 self
221 }
222
223 pub fn stuck_window(mut self, n: usize) -> Self {
225 self.kernel_builder = self.kernel_builder.stuck_window(n);
226 self
227 }
228
229 pub fn stuck_error_rate(mut self, rate: f64) -> Self {
231 self.kernel_builder = self.kernel_builder.stuck_error_rate(rate);
232 self
233 }
234
235 pub fn goal_eval_transcript_tail(mut self, n: usize) -> Self {
244 self.goal_eval_transcript_tail = n;
245 self
246 }
247
248 pub fn file_reinjector(mut self, r: crate::compact::FileReinjector) -> Self {
251 self.file_reinjector = Some(r);
252 self
253 }
254
255 pub fn skill_reinjector(mut self, r: crate::compact::SkillReinjector) -> Self {
257 self.skill_reinjector = Some(r);
258 self
259 }
260
261 pub fn build(self) -> Result<AgentRuntime> {
265 let kernel_builder = self.kernel_builder.skills(self.skills);
266 let mut kernel = kernel_builder.build()?;
267
268 let mut transcript = Vec::new();
269 if let Some(sys) = self.system_prompt {
270 transcript.push(Message::system(sys));
271 }
272 transcript.extend(self.seed);
273
274 let event_sink: Arc<dyn EventSink> =
275 self.saved_event_sink.unwrap_or_else(|| Arc::new(NullSink));
276
277 let todo_list = Arc::new(RwLock::new(Vec::<TodoItem>::new()));
280 kernel.tools_mut().register_mut(Arc::new(TodoWriteTool::new(
281 todo_list.clone(),
282 event_sink.clone(),
283 )));
284
285 let plan_approval_gate = Arc::new(PlanApprovalGate::new());
291 let plan_mode_request_gate = Arc::new(PlanModeRequestGate::new());
292 if self.with_plan_mode_tools {
293 let permissions_arc = kernel.tools().permissions_config().map(Arc::new);
294 kernel.tools_mut().register_mut({
295 let mut tool = EnterPlanModeTool::new(plan_approval_gate.clone());
296 if let Some(ref perms) = permissions_arc {
297 tool = tool.with_permissions(perms.clone());
298 }
299 Arc::new(tool)
300 });
301 kernel.tools_mut().register_mut({
302 let mut tool =
303 ExitPlanModeTool::new(plan_approval_gate.clone(), event_sink.clone());
304 if let Some(ref perms) = permissions_arc {
305 tool = tool.with_permissions(perms.clone());
306 }
307 Arc::new(tool)
308 });
309 kernel
310 .tools_mut()
311 .register_mut(Arc::new(RequestPlanModeTool::new(
312 plan_mode_request_gate.clone(),
313 event_sink.clone(),
314 )));
315 }
316
317 let plan_todo_reinjector = Some(crate::compact::PlanTodoReinjector::new(
320 todo_list.clone(),
321 plan_approval_gate.clone(),
322 ));
323
324 if kernel.llm().supports_deferred_tools() {
328 kernel.tools_mut().freeze_deferred_specs();
329 }
330
331 Ok(AgentRuntime {
332 kernel,
333 transcript: Arc::new(transcript),
334 event_sink,
335 streaming: self.streaming,
336 compactor: self.compactor,
337 microcompactor: self.microcompactor,
338 consecutive_compact_failures: 0,
339 checkpoints: CheckpointState::disabled(),
340 todo_list,
341 plan_approval_gate,
342 plan_mode_request_gate,
343 goal_state: Arc::new(RwLock::new(None)),
344 message_queue: std::collections::VecDeque::new(),
345 deferred_turn_finished: None,
346 session: SessionLifecycle::open(),
347 goal_eval_transcript_tail: self.goal_eval_transcript_tail,
348 prompt_segments: self.prompt_segments,
349 file_reinjector: self.file_reinjector,
350 skill_reinjector: self.skill_reinjector,
351 plan_todo_reinjector,
352 last_compact_turn: None,
353 permission_hook: None,
354 })
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use crate::llm::MockProvider;
362 use crate::message::Role;
363
364 fn mock_llm() -> Arc<dyn ChatProvider> {
365 Arc::new(MockProvider::new(vec![]))
366 }
367
368 #[test]
369 fn build_with_minimum_config_succeeds() {
370 let rt = AgentRuntimeBuilder::new()
371 .llm(mock_llm())
372 .build()
373 .expect("build() with just an LLM must succeed");
374
375 assert_eq!(rt.kernel.max_steps, 0);
378 assert_eq!(rt.kernel.max_transcript_chars, None);
379 assert!(rt.compactor.is_none());
380 assert!(rt.microcompactor.is_none());
381 assert!(!rt.streaming);
382 assert!(rt.transcript.is_empty());
383 assert!(!rt.checkpoints.enabled());
384 assert_eq!(rt.goal_eval_transcript_tail, 12);
385 }
386
387 #[test]
388 fn build_without_llm_errors() {
389 let result = AgentRuntimeBuilder::new().build();
390 let err = result.expect_err("build() without an LLM must fail");
391 assert!(
392 matches!(err, crate::error::Error::Config { .. }),
393 "expected Error::Config, got {err:?}"
394 );
395 }
396
397 #[test]
398 fn builder_setters_round_trip() {
399 let compactor = Compactor::new(1000);
400 let rt = AgentRuntimeBuilder::new()
401 .llm(mock_llm())
402 .max_steps(7)
403 .max_transcript_chars(5000)
404 .compactor(compactor)
405 .streaming(true)
406 .stuck_window(4)
407 .seed_transcript(vec![
408 Message::user("seed user"),
409 Message::assistant("seed assistant"),
410 ])
411 .build()
412 .expect("build() with setters must succeed");
413
414 assert_eq!(rt.kernel.max_steps, 7);
415 assert_eq!(rt.kernel.max_transcript_chars, Some(5000));
416 assert!(rt.compactor.is_some());
417 assert!(rt.streaming);
418 assert_eq!(rt.kernel.stuck_window, 4);
419 assert_eq!(rt.transcript.len(), 2);
421 assert_eq!(rt.transcript[0].role, Role::User);
422 assert_eq!(rt.transcript[1].role, Role::Assistant);
423 assert_eq!(rt.transcript[0].content, "seed user");
424 assert_eq!(rt.transcript[1].content, "seed assistant");
425 }
426
427 #[test]
428 fn file_reinjector_and_skill_reinjector_wire_through() {
429 let read_state = Arc::new(std::sync::Mutex::new(crate::tools::fs::ReadFileState::new()));
430 let rt = AgentRuntimeBuilder::new()
431 .llm(mock_llm())
432 .file_reinjector(crate::compact::FileReinjector::new(read_state))
433 .skill_reinjector(crate::compact::SkillReinjector::new(vec![]))
434 .build()
435 .expect("build() with reinjectors must succeed");
436
437 assert!(rt.file_reinjector.is_some());
438 assert!(rt.skill_reinjector.is_some());
439 assert!(rt.plan_todo_reinjector.is_some());
441 }
442}