1use crate::context::ContextProvider;
13use crate::hitl::ConfirmationProvider;
14use crate::hooks::HookExecutor;
15#[cfg(test)]
16use crate::llm::LlmResponse;
17use crate::llm::{LlmClient, Message, TokenUsage, ToolDefinition};
18use crate::permissions::{PermissionChecker, PermissionPolicy};
19use crate::planning::{AgentGoal, ExecutionPlan, TaskStatus};
20use crate::prompts::{PlanningMode, SystemPromptSlots};
21use crate::queue::{SessionCommand, SessionQueueConfig};
22use crate::session_lane_queue::SessionLaneQueue;
23use crate::tools::{ToolContext, ToolExecutor};
24use anyhow::Result;
25use async_trait::async_trait;
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28use std::sync::Arc;
29
30mod completion_runtime;
31mod context_perception;
32mod execution_entry;
33mod execution_mode;
34mod execution_state;
35mod hook_runtime;
36mod llm_turn;
37mod loop_builder;
38mod loop_runtime;
39mod parallel_tool_runtime;
40mod plan_execution;
41mod planning_runtime;
42mod project_context;
43mod prompt_runtime;
44mod queue_forwarder;
45mod telemetry_runtime;
46mod tool_completion_runtime;
47mod tool_execution_runtime;
48mod tool_gate_runtime;
49mod tool_guard_runtime;
50mod tool_memory_runtime;
51mod tool_result_runtime;
52mod tool_turn;
53mod turn_context;
54
55const MAX_TOOL_ROUNDS: usize = 50;
57
58#[derive(Clone)]
60pub(crate) struct AgentConfig {
61 pub prompt_slots: SystemPromptSlots,
67 pub tools: Vec<ToolDefinition>,
68 pub max_tool_rounds: usize,
69 pub security_provider: Option<Arc<dyn crate::security::SecurityProvider>>,
71 pub permission_checker: Option<Arc<dyn PermissionChecker>>,
73 pub permission_policy: Option<PermissionPolicy>,
75 pub confirmation_manager: Option<Arc<dyn ConfirmationProvider>>,
77 pub confirmation_policy: Option<crate::hitl::ConfirmationPolicy>,
79 pub queue_config: Option<SessionQueueConfig>,
81 pub context_providers: Vec<Arc<dyn ContextProvider>>,
83 pub planning_mode: PlanningMode,
85 pub goal_tracking: bool,
87 pub hook_engine: Option<Arc<dyn HookExecutor>>,
89 pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
91 pub max_parse_retries: u32,
97 pub tool_timeout_ms: Option<u64>,
103 pub circuit_breaker_threshold: u32,
110 pub duplicate_tool_call_threshold: u32,
116 pub auto_compact: bool,
118 pub auto_compact_threshold: f32,
121 pub max_context_tokens: usize,
124 pub memory: Option<Arc<crate::memory::AgentMemory>>,
126 pub continuation_enabled: bool,
134 pub max_continuation_turns: u32,
138 pub max_execution_time_ms: Option<u64>,
144}
145
146impl std::fmt::Debug for AgentConfig {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 f.debug_struct("AgentConfig")
149 .field("prompt_slots", &self.prompt_slots)
150 .field("tools", &self.tools)
151 .field("max_tool_rounds", &self.max_tool_rounds)
152 .field("security_provider", &self.security_provider.is_some())
153 .field("permission_checker", &self.permission_checker.is_some())
154 .field("permission_policy", &self.permission_policy.is_some())
155 .field("confirmation_manager", &self.confirmation_manager.is_some())
156 .field("confirmation_policy", &self.confirmation_policy.is_some())
157 .field("queue_config", &self.queue_config.is_some())
158 .field("context_providers", &self.context_providers.len())
159 .field("planning_mode", &self.planning_mode)
160 .field("goal_tracking", &self.goal_tracking)
161 .field("hook_engine", &self.hook_engine.is_some())
162 .field(
163 "skill_registry",
164 &self.skill_registry.as_ref().map(|r| r.len()),
165 )
166 .field("max_parse_retries", &self.max_parse_retries)
167 .field("tool_timeout_ms", &self.tool_timeout_ms)
168 .field("circuit_breaker_threshold", &self.circuit_breaker_threshold)
169 .field(
170 "duplicate_tool_call_threshold",
171 &self.duplicate_tool_call_threshold,
172 )
173 .field("auto_compact", &self.auto_compact)
174 .field("auto_compact_threshold", &self.auto_compact_threshold)
175 .field("max_context_tokens", &self.max_context_tokens)
176 .field("continuation_enabled", &self.continuation_enabled)
177 .field("max_continuation_turns", &self.max_continuation_turns)
178 .field("memory", &self.memory.is_some())
179 .finish()
180 }
181}
182
183impl Default for AgentConfig {
184 fn default() -> Self {
185 Self {
186 prompt_slots: SystemPromptSlots::default(),
187 tools: Vec::new(), max_tool_rounds: MAX_TOOL_ROUNDS,
189 security_provider: None,
190 permission_checker: None,
191 permission_policy: None,
192 confirmation_manager: None,
193 confirmation_policy: None,
194 queue_config: None,
195 context_providers: Vec::new(),
196 planning_mode: PlanningMode::default(),
197 goal_tracking: false,
198 hook_engine: None,
199 skill_registry: Some(Arc::new(crate::skills::SkillRegistry::with_builtins())),
200 max_parse_retries: 2,
201 tool_timeout_ms: None,
202 circuit_breaker_threshold: 3,
203 duplicate_tool_call_threshold: 3,
204 auto_compact: false,
205 auto_compact_threshold: 0.80,
206 max_context_tokens: 200_000,
207 memory: None,
208 continuation_enabled: true,
209 max_continuation_turns: 3,
210 max_execution_time_ms: None,
211 }
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(tag = "type")]
222#[non_exhaustive]
223pub enum AgentEvent {
224 #[serde(rename = "agent_start")]
226 Start { prompt: String },
227
228 #[serde(rename = "agent_mode_changed")]
230 AgentModeChanged {
231 mode: String,
233 agent: String,
235 description: String,
237 },
238
239 #[serde(rename = "turn_start")]
241 TurnStart { turn: usize },
242
243 #[serde(rename = "text_delta")]
245 TextDelta { text: String },
246
247 #[serde(rename = "reasoning_delta")]
249 ReasoningDelta { text: String },
250
251 #[serde(rename = "tool_start")]
253 ToolStart { id: String, name: String },
254
255 #[serde(rename = "tool_input_delta")]
257 ToolInputDelta { delta: String },
258
259 #[serde(rename = "tool_end")]
261 ToolEnd {
262 id: String,
263 name: String,
264 output: String,
265 exit_code: i32,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 metadata: Option<serde_json::Value>,
268 },
269
270 #[serde(rename = "tool_output_delta")]
272 ToolOutputDelta {
273 id: String,
274 name: String,
275 delta: String,
276 },
277
278 #[serde(rename = "turn_end")]
280 TurnEnd { turn: usize, usage: TokenUsage },
281
282 #[serde(rename = "agent_end")]
284 End {
285 text: String,
286 usage: TokenUsage,
287 verification_summary: Box<crate::verification::VerificationSummary>,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 meta: Option<crate::llm::LlmResponseMeta>,
290 },
291
292 #[serde(rename = "error")]
294 Error { message: String },
295
296 #[serde(rename = "confirmation_required")]
298 ConfirmationRequired {
299 tool_id: String,
300 tool_name: String,
301 args: serde_json::Value,
302 timeout_ms: u64,
303 },
304
305 #[serde(rename = "confirmation_received")]
307 ConfirmationReceived {
308 tool_id: String,
309 approved: bool,
310 reason: Option<String>,
311 },
312
313 #[serde(rename = "confirmation_timeout")]
315 ConfirmationTimeout {
316 tool_id: String,
317 action_taken: String, },
319
320 #[serde(rename = "external_task_pending")]
322 ExternalTaskPending {
323 task_id: String,
324 session_id: String,
325 lane: crate::queue::SessionLane,
326 command_type: String,
327 payload: serde_json::Value,
328 timeout_ms: u64,
329 },
330
331 #[serde(rename = "external_task_completed")]
333 ExternalTaskCompleted {
334 task_id: String,
335 session_id: String,
336 success: bool,
337 },
338
339 #[serde(rename = "permission_denied")]
341 PermissionDenied {
342 tool_id: String,
343 tool_name: String,
344 args: serde_json::Value,
345 reason: String,
346 },
347
348 #[serde(rename = "context_resolving")]
350 ContextResolving { providers: Vec<String> },
351
352 #[serde(rename = "context_resolved")]
354 ContextResolved {
355 total_items: usize,
356 total_tokens: usize,
357 },
358
359 #[serde(rename = "command_dead_lettered")]
364 CommandDeadLettered {
365 command_id: String,
366 command_type: String,
367 lane: String,
368 error: String,
369 attempts: u32,
370 },
371
372 #[serde(rename = "command_retry")]
374 CommandRetry {
375 command_id: String,
376 command_type: String,
377 lane: String,
378 attempt: u32,
379 delay_ms: u64,
380 },
381
382 #[serde(rename = "queue_alert")]
384 QueueAlert {
385 level: String,
386 alert_type: String,
387 message: String,
388 },
389
390 #[serde(rename = "task_updated")]
395 TaskUpdated {
396 session_id: String,
397 tasks: Vec<crate::planning::Task>,
398 },
399
400 #[serde(rename = "memory_stored")]
405 MemoryStored {
406 memory_id: String,
407 memory_type: String,
408 importance: f32,
409 tags: Vec<String>,
410 },
411
412 #[serde(rename = "memory_recalled")]
414 MemoryRecalled {
415 memory_id: String,
416 content: String,
417 relevance: f32,
418 },
419
420 #[serde(rename = "memories_searched")]
422 MemoriesSearched {
423 query: Option<String>,
424 tags: Vec<String>,
425 result_count: usize,
426 },
427
428 #[serde(rename = "memory_cleared")]
430 MemoryCleared {
431 tier: String, count: u64,
433 },
434
435 #[serde(rename = "subagent_start")]
440 SubagentStart {
441 task_id: String,
443 session_id: String,
445 parent_session_id: String,
447 agent: String,
449 description: String,
451 },
452
453 #[serde(rename = "subagent_progress")]
455 SubagentProgress {
456 task_id: String,
458 session_id: String,
460 status: String,
462 metadata: serde_json::Value,
464 },
465
466 #[serde(rename = "subagent_end")]
468 SubagentEnd {
469 task_id: String,
471 session_id: String,
473 agent: String,
475 output: String,
477 success: bool,
479 },
480
481 #[serde(rename = "planning_start")]
486 PlanningStart { prompt: String },
487
488 #[serde(rename = "planning_end")]
490 PlanningEnd {
491 plan: ExecutionPlan,
492 estimated_steps: usize,
493 },
494
495 #[serde(rename = "step_start")]
497 StepStart {
498 step_id: String,
499 description: String,
500 step_number: usize,
501 total_steps: usize,
502 },
503
504 #[serde(rename = "step_end")]
506 StepEnd {
507 step_id: String,
508 status: TaskStatus,
509 step_number: usize,
510 total_steps: usize,
511 },
512
513 #[serde(rename = "goal_extracted")]
515 GoalExtracted { goal: AgentGoal },
516
517 #[serde(rename = "goal_progress")]
519 GoalProgress {
520 goal: String,
521 progress: f32,
522 completed_steps: usize,
523 total_steps: usize,
524 },
525
526 #[serde(rename = "goal_achieved")]
528 GoalAchieved {
529 goal: String,
530 total_steps: usize,
531 duration_ms: i64,
532 },
533
534 #[serde(rename = "context_compacted")]
539 ContextCompacted {
540 session_id: String,
541 before_messages: usize,
542 after_messages: usize,
543 percent_before: f32,
544 },
545
546 #[serde(rename = "persistence_failed")]
551 PersistenceFailed {
552 session_id: String,
553 operation: String,
554 error: String,
555 },
556}
557
558#[derive(Debug, Clone)]
560pub struct AgentResult {
561 pub text: String,
562 pub messages: Vec<Message>,
563 pub usage: TokenUsage,
564 pub tool_calls_count: usize,
565 pub verification_reports: Vec<crate::verification::VerificationReport>,
566}
567
568impl AgentResult {
569 pub fn verification_summary(&self) -> crate::verification::VerificationSummary {
570 crate::verification::VerificationSummary::from_reports(&self.verification_reports)
571 }
572
573 pub fn verification_summary_text(&self) -> String {
574 crate::verification::format_verification_summary(&self.verification_summary())
575 }
576
577 pub fn has_pending_verification(&self) -> bool {
578 matches!(
579 self.verification_summary().status,
580 crate::verification::VerificationStatus::NeedsReview
581 )
582 }
583}
584
585pub struct ToolCommand {
593 tool_executor: Arc<ToolExecutor>,
594 tool_name: String,
595 tool_args: Value,
596 tool_context: ToolContext,
597 skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
598}
599
600impl ToolCommand {
601 pub fn new(
603 tool_executor: Arc<ToolExecutor>,
604 tool_name: String,
605 tool_args: Value,
606 tool_context: ToolContext,
607 skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
608 ) -> Self {
609 Self {
610 tool_executor,
611 tool_name,
612 tool_args,
613 tool_context,
614 skill_registry,
615 }
616 }
617}
618
619#[async_trait]
620impl SessionCommand for ToolCommand {
621 async fn execute(&self) -> Result<Value> {
622 if let Some(registry) = &self.skill_registry {
624 let instruction_skills = registry.by_kind(crate::skills::SkillKind::Instruction);
625
626 let has_restrictions = instruction_skills.iter().any(|s| s.allowed_tools.is_some());
628
629 if has_restrictions {
630 let mut allowed = false;
631
632 for skill in &instruction_skills {
633 if skill.is_tool_allowed(&self.tool_name) {
634 allowed = true;
635 break;
636 }
637 }
638
639 if !allowed {
640 return Err(anyhow::anyhow!(
641 "Tool '{}' is not allowed by any active skill. Active skills restrict tools to their allowed-tools lists.",
642 self.tool_name
643 ));
644 }
645 }
646 }
647
648 let result = self
650 .tool_executor
651 .execute_with_context(&self.tool_name, &self.tool_args, &self.tool_context)
652 .await?;
653 Ok(serde_json::json!({
654 "output": result.output,
655 "exit_code": result.exit_code,
656 "metadata": result.metadata,
657 }))
658 }
659
660 fn command_type(&self) -> &str {
661 &self.tool_name
662 }
663
664 fn payload(&self) -> Value {
665 self.tool_args.clone()
666 }
667}
668
669#[derive(Clone)]
675pub(crate) struct AgentLoop {
676 llm_client: Arc<dyn LlmClient>,
677 tool_executor: Arc<ToolExecutor>,
678 tool_context: ToolContext,
679 config: AgentConfig,
680 command_queue: Option<Arc<SessionLaneQueue>>,
682}
683
684#[cfg(test)]
685mod tests;
686
687#[cfg(test)]
688mod extra_agent_tests;