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::subagent::AgentRegistry;
24use crate::tools::{ToolContext, ToolExecutor};
25use anyhow::Result;
26use async_trait::async_trait;
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29use std::sync::Arc;
30
31mod auto_delegation;
32mod completion_runtime;
33mod context_perception;
34mod execution_entry;
35mod execution_mode;
36mod execution_state;
37mod hook_runtime;
38mod llm_turn;
39mod loop_builder;
40mod loop_runtime;
41mod parallel_tool_runtime;
42mod plan_execution;
43mod planning_runtime;
44mod project_context;
45mod prompt_runtime;
46mod queue_forwarder;
47mod telemetry_runtime;
48mod tool_completion_runtime;
49mod tool_execution_runtime;
50mod tool_gate_runtime;
51mod tool_guard_runtime;
52mod tool_memory_runtime;
53mod tool_result_runtime;
54mod tool_turn;
55mod turn_context;
56
57pub(crate) const MAX_TOOL_ROUNDS: usize = 50;
59pub(crate) const DEFAULT_MAX_PARALLEL_TASKS: usize = 8;
60
61#[derive(Clone)]
63pub(crate) struct AgentConfig {
64 pub prompt_slots: SystemPromptSlots,
70 pub tools: Vec<ToolDefinition>,
71 pub max_tool_rounds: usize,
72 pub security_provider: Option<Arc<dyn crate::security::SecurityProvider>>,
74 pub permission_checker: Option<Arc<dyn PermissionChecker>>,
76 pub permission_policy: Option<PermissionPolicy>,
78 pub confirmation_manager: Option<Arc<dyn ConfirmationProvider>>,
80 pub confirmation_policy: Option<crate::hitl::ConfirmationPolicy>,
82 pub queue_config: Option<SessionQueueConfig>,
84 pub context_providers: Vec<Arc<dyn ContextProvider>>,
86 pub planning_mode: PlanningMode,
88 pub goal_tracking: bool,
90 pub hook_engine: Option<Arc<dyn HookExecutor>>,
92 pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
94 pub max_parse_retries: u32,
100 pub tool_timeout_ms: Option<u64>,
106 pub max_parallel_tasks: usize,
109 pub auto_delegation: crate::config::AutoDelegationConfig,
111 pub agent_registry: Option<Arc<AgentRegistry>>,
113 pub circuit_breaker_threshold: u32,
120 pub duplicate_tool_call_threshold: u32,
126 pub auto_compact: bool,
128 pub auto_compact_threshold: f32,
131 pub max_context_tokens: usize,
134 pub memory: Option<Arc<crate::memory::AgentMemory>>,
136 pub continuation_enabled: bool,
144 pub max_continuation_turns: u32,
148 pub max_execution_time_ms: Option<u64>,
154}
155
156impl std::fmt::Debug for AgentConfig {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 f.debug_struct("AgentConfig")
159 .field("prompt_slots", &self.prompt_slots)
160 .field("tools", &self.tools)
161 .field("max_tool_rounds", &self.max_tool_rounds)
162 .field("security_provider", &self.security_provider.is_some())
163 .field("permission_checker", &self.permission_checker.is_some())
164 .field("permission_policy", &self.permission_policy.is_some())
165 .field("confirmation_manager", &self.confirmation_manager.is_some())
166 .field("confirmation_policy", &self.confirmation_policy.is_some())
167 .field("queue_config", &self.queue_config.is_some())
168 .field("context_providers", &self.context_providers.len())
169 .field("planning_mode", &self.planning_mode)
170 .field("goal_tracking", &self.goal_tracking)
171 .field("hook_engine", &self.hook_engine.is_some())
172 .field(
173 "skill_registry",
174 &self.skill_registry.as_ref().map(|r| r.len()),
175 )
176 .field("max_parse_retries", &self.max_parse_retries)
177 .field("tool_timeout_ms", &self.tool_timeout_ms)
178 .field("max_parallel_tasks", &self.max_parallel_tasks)
179 .field("auto_delegation", &self.auto_delegation)
180 .field(
181 "agent_registry",
182 &self.agent_registry.as_ref().map(|registry| registry.len()),
183 )
184 .field("circuit_breaker_threshold", &self.circuit_breaker_threshold)
185 .field(
186 "duplicate_tool_call_threshold",
187 &self.duplicate_tool_call_threshold,
188 )
189 .field("auto_compact", &self.auto_compact)
190 .field("auto_compact_threshold", &self.auto_compact_threshold)
191 .field("max_context_tokens", &self.max_context_tokens)
192 .field("continuation_enabled", &self.continuation_enabled)
193 .field("max_continuation_turns", &self.max_continuation_turns)
194 .field("memory", &self.memory.is_some())
195 .finish()
196 }
197}
198
199impl Default for AgentConfig {
200 fn default() -> Self {
201 Self {
202 prompt_slots: SystemPromptSlots::default(),
203 tools: Vec::new(), max_tool_rounds: MAX_TOOL_ROUNDS,
205 security_provider: None,
206 permission_checker: None,
207 permission_policy: None,
208 confirmation_manager: None,
209 confirmation_policy: None,
210 queue_config: None,
211 context_providers: Vec::new(),
212 planning_mode: PlanningMode::default(),
213 goal_tracking: false,
214 hook_engine: None,
215 skill_registry: Some(Arc::new(crate::skills::SkillRegistry::with_builtins())),
216 max_parse_retries: 2,
217 tool_timeout_ms: None,
218 max_parallel_tasks: DEFAULT_MAX_PARALLEL_TASKS,
219 auto_delegation: crate::config::AutoDelegationConfig::default(),
220 agent_registry: None,
221 circuit_breaker_threshold: 3,
222 duplicate_tool_call_threshold: 3,
223 auto_compact: false,
224 auto_compact_threshold: 0.80,
225 max_context_tokens: 200_000,
226 memory: None,
227 continuation_enabled: true,
228 max_continuation_turns: 3,
229 max_execution_time_ms: None,
230 }
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
240#[serde(tag = "type")]
241#[non_exhaustive]
242pub enum AgentEvent {
243 #[serde(rename = "agent_start")]
245 Start { prompt: String },
246
247 #[serde(rename = "agent_mode_changed")]
249 AgentModeChanged {
250 mode: String,
252 agent: String,
254 description: String,
256 },
257
258 #[serde(rename = "turn_start")]
260 TurnStart { turn: usize },
261
262 #[serde(rename = "text_delta")]
264 TextDelta { text: String },
265
266 #[serde(rename = "reasoning_delta")]
268 ReasoningDelta { text: String },
269
270 #[serde(rename = "tool_start")]
272 ToolStart { id: String, name: String },
273
274 #[serde(rename = "tool_input_delta")]
276 ToolInputDelta { delta: String },
277
278 #[serde(rename = "tool_end")]
280 ToolEnd {
281 id: String,
282 name: String,
283 output: String,
284 exit_code: i32,
285 #[serde(skip_serializing_if = "Option::is_none")]
286 metadata: Option<serde_json::Value>,
287 #[serde(skip_serializing_if = "Option::is_none")]
292 error_kind: Option<crate::tools::ToolErrorKind>,
293 },
294
295 #[serde(rename = "tool_output_delta")]
297 ToolOutputDelta {
298 id: String,
299 name: String,
300 delta: String,
301 },
302
303 #[serde(rename = "turn_end")]
305 TurnEnd { turn: usize, usage: TokenUsage },
306
307 #[serde(rename = "agent_end")]
309 End {
310 text: String,
311 usage: TokenUsage,
312 verification_summary: Box<crate::verification::VerificationSummary>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 meta: Option<crate::llm::LlmResponseMeta>,
315 },
316
317 #[serde(rename = "error")]
319 Error { message: String },
320
321 #[serde(rename = "confirmation_required")]
323 ConfirmationRequired {
324 tool_id: String,
325 tool_name: String,
326 args: serde_json::Value,
327 timeout_ms: u64,
328 },
329
330 #[serde(rename = "confirmation_received")]
332 ConfirmationReceived {
333 tool_id: String,
334 approved: bool,
335 reason: Option<String>,
336 },
337
338 #[serde(rename = "confirmation_timeout")]
340 ConfirmationTimeout {
341 tool_id: String,
342 action_taken: String, },
344
345 #[serde(rename = "external_task_pending")]
347 ExternalTaskPending {
348 task_id: String,
349 session_id: String,
350 lane: crate::queue::SessionLane,
351 command_type: String,
352 payload: serde_json::Value,
353 timeout_ms: u64,
354 },
355
356 #[serde(rename = "external_task_completed")]
358 ExternalTaskCompleted {
359 task_id: String,
360 session_id: String,
361 success: bool,
362 },
363
364 #[serde(rename = "permission_denied")]
366 PermissionDenied {
367 tool_id: String,
368 tool_name: String,
369 args: serde_json::Value,
370 reason: String,
371 },
372
373 #[serde(rename = "context_resolving")]
375 ContextResolving { providers: Vec<String> },
376
377 #[serde(rename = "context_resolved")]
379 ContextResolved {
380 total_items: usize,
381 total_tokens: usize,
382 },
383
384 #[serde(rename = "command_dead_lettered")]
389 CommandDeadLettered {
390 command_id: String,
391 command_type: String,
392 lane: String,
393 error: String,
394 attempts: u32,
395 },
396
397 #[serde(rename = "command_retry")]
399 CommandRetry {
400 command_id: String,
401 command_type: String,
402 lane: String,
403 attempt: u32,
404 delay_ms: u64,
405 },
406
407 #[serde(rename = "queue_alert")]
409 QueueAlert {
410 level: String,
411 alert_type: String,
412 message: String,
413 },
414
415 #[serde(rename = "task_updated")]
420 TaskUpdated {
421 session_id: String,
422 tasks: Vec<crate::planning::Task>,
423 },
424
425 #[serde(rename = "memory_stored")]
430 MemoryStored {
431 memory_id: String,
432 memory_type: String,
433 importance: f32,
434 tags: Vec<String>,
435 },
436
437 #[serde(rename = "memory_recalled")]
439 MemoryRecalled {
440 memory_id: String,
441 content: String,
442 relevance: f32,
443 },
444
445 #[serde(rename = "memories_searched")]
447 MemoriesSearched {
448 query: Option<String>,
449 tags: Vec<String>,
450 result_count: usize,
451 },
452
453 #[serde(rename = "memory_cleared")]
455 MemoryCleared {
456 tier: String, count: u64,
458 },
459
460 #[serde(rename = "subagent_start")]
465 SubagentStart {
466 task_id: String,
468 session_id: String,
470 parent_session_id: String,
472 agent: String,
474 description: String,
476 },
477
478 #[serde(rename = "subagent_progress")]
480 SubagentProgress {
481 task_id: String,
483 session_id: String,
485 status: String,
487 metadata: serde_json::Value,
489 },
490
491 #[serde(rename = "subagent_end")]
493 SubagentEnd {
494 task_id: String,
496 session_id: String,
498 agent: String,
500 output: String,
502 success: bool,
504 },
505
506 #[serde(rename = "planning_start")]
511 PlanningStart { prompt: String },
512
513 #[serde(rename = "planning_end")]
515 PlanningEnd {
516 plan: ExecutionPlan,
517 estimated_steps: usize,
518 },
519
520 #[serde(rename = "step_start")]
522 StepStart {
523 step_id: String,
524 description: String,
525 step_number: usize,
526 total_steps: usize,
527 },
528
529 #[serde(rename = "step_end")]
531 StepEnd {
532 step_id: String,
533 status: TaskStatus,
534 step_number: usize,
535 total_steps: usize,
536 },
537
538 #[serde(rename = "goal_extracted")]
540 GoalExtracted { goal: AgentGoal },
541
542 #[serde(rename = "goal_progress")]
544 GoalProgress {
545 goal: String,
546 progress: f32,
547 completed_steps: usize,
548 total_steps: usize,
549 },
550
551 #[serde(rename = "goal_achieved")]
553 GoalAchieved {
554 goal: String,
555 total_steps: usize,
556 duration_ms: i64,
557 },
558
559 #[serde(rename = "context_compacted")]
564 ContextCompacted {
565 session_id: String,
566 before_messages: usize,
567 after_messages: usize,
568 percent_before: f32,
569 },
570
571 #[serde(rename = "persistence_failed")]
576 PersistenceFailed {
577 session_id: String,
578 operation: String,
579 error: String,
580 },
581}
582
583#[derive(Debug, Clone)]
585pub struct AgentResult {
586 pub text: String,
587 pub messages: Vec<Message>,
588 pub usage: TokenUsage,
589 pub tool_calls_count: usize,
590 pub verification_reports: Vec<crate::verification::VerificationReport>,
591}
592
593impl AgentResult {
594 pub fn verification_summary(&self) -> crate::verification::VerificationSummary {
595 crate::verification::VerificationSummary::from_reports(&self.verification_reports)
596 }
597
598 pub fn verification_summary_text(&self) -> String {
599 crate::verification::format_verification_summary(&self.verification_summary())
600 }
601
602 pub fn has_pending_verification(&self) -> bool {
603 matches!(
604 self.verification_summary().status,
605 crate::verification::VerificationStatus::NeedsReview
606 )
607 }
608}
609
610pub struct ToolCommand {
618 tool_executor: Arc<ToolExecutor>,
619 tool_name: String,
620 tool_args: Value,
621 tool_context: ToolContext,
622 skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
623}
624
625impl ToolCommand {
626 pub fn new(
628 tool_executor: Arc<ToolExecutor>,
629 tool_name: String,
630 tool_args: Value,
631 tool_context: ToolContext,
632 skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
633 ) -> Self {
634 Self {
635 tool_executor,
636 tool_name,
637 tool_args,
638 tool_context,
639 skill_registry,
640 }
641 }
642}
643
644#[async_trait]
645impl SessionCommand for ToolCommand {
646 async fn execute(&self) -> Result<Value> {
647 if let Some(registry) = &self.skill_registry {
649 let restricting_skills = registry.global_tool_restricting_skills();
651
652 if !restricting_skills.is_empty() {
653 let mut allowed = false;
654
655 for skill in &restricting_skills {
656 if skill.is_tool_allowed(&self.tool_name) {
657 allowed = true;
658 break;
659 }
660 }
661
662 if !allowed {
663 return Err(anyhow::anyhow!(
664 "Tool '{}' is not allowed by any active skill. Active skills restrict tools to their allowed-tools lists.",
665 self.tool_name
666 ));
667 }
668 }
669 }
670
671 let result = self
673 .tool_executor
674 .execute_with_context(&self.tool_name, &self.tool_args, &self.tool_context)
675 .await?;
676 Ok(serde_json::json!({
677 "output": result.output,
678 "exit_code": result.exit_code,
679 "metadata": result.metadata,
680 }))
681 }
682
683 fn command_type(&self) -> &str {
684 &self.tool_name
685 }
686
687 fn payload(&self) -> Value {
688 self.tool_args.clone()
689 }
690}
691
692#[derive(Clone)]
698pub(crate) struct AgentLoop {
699 llm_client: Arc<dyn LlmClient>,
700 tool_executor: Arc<ToolExecutor>,
701 tool_context: ToolContext,
702 config: AgentConfig,
703 command_queue: Option<Arc<SessionLaneQueue>>,
705}
706
707#[cfg(test)]
708pub(crate) mod tests;
709
710#[cfg(test)]
711mod extra_agent_tests;