1use crate::compaction::{apply_compaction, estimate_messages, CompactionConfig};
9use crate::guardrails::plan_tool_effect_batches;
10use crate::hooks::HookRegistry;
11use crate::mode::{self, Profile, Scope};
12use crate::permissions::{self, Approver, Decision, Policy};
13use crate::provider::{Message, Provider, Role};
14use moka::future::Cache;
15use parking_lot::RwLock;
16use serde::{Deserialize, Serialize};
17use std::future::Future;
18use std::pin::Pin;
19use std::sync::Arc;
20#[cfg(feature = "providers")]
21use tracing::error;
22use tracing::{debug, info, warn};
23
24#[cfg(feature = "ipc")]
25use cancellation_token::{CancellationToken, CancellationTokenSource};
26
27pub fn normalize_tool_name(name: &str) -> &str {
30 match name {
31 "read_file" | "read" => "read",
32 "write_file" | "write" => "write",
33 "list_dir" | "ls" => "ls",
34 "run_command" | "bash" => "bash",
35 "find_files" | "find" => "find",
36 "code_intel" | "grep" => "grep",
37 "hashline_edit" | "search_replace" | "apply_patch" | "edit" => "edit",
38 "spawn_agent" | "agent" => "spawn_agent",
39 "web_fetch" | "fetch" | "fetch_url" => "web_fetch",
40 "todo" | "todo_write" | "todo_list" => "todo",
41 "enter_plan_mode" | "plan_mode" => "enter_plan_mode",
42 "exit_plan_mode" => "exit_plan_mode",
43 "lsp_diagnostics" | "diagnostics" => "lsp_diagnostics",
44 "lsp_definition" | "definition" | "go_to_definition" => "lsp_definition",
45 "lsp_references" | "references" | "find_references" => "lsp_references",
46 _ => name,
47 }
48}
49
50pub type ToolFuture = Pin<Box<dyn Future<Output = ToolResult> + Send>>;
51
52#[cfg(feature = "ipc")]
53#[derive(Clone)]
54pub struct CancellationHandle {
55 source: Arc<RwLock<CancellationTokenSource>>,
56}
57
58#[cfg(feature = "ipc")]
59impl CancellationHandle {
60 fn new() -> Self {
61 Self {
62 source: Arc::new(RwLock::new(CancellationTokenSource::new())),
63 }
64 }
65
66 pub fn cancel(&self) {
67 self.source.read().cancel();
68 }
69
70 fn reset(&self) -> CancellationToken {
71 let source = CancellationTokenSource::new();
72 let token = source.token();
73 *self.source.write() = source;
74 token
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ToolCall {
80 pub id: String,
81 pub name: String,
82 pub arguments: String,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ToolResult {
87 pub id: String,
88 pub content: String,
89 pub is_error: bool,
90}
91
92impl ToolResult {
93 pub fn ok(id: impl Into<String>, content: impl Into<String>) -> Self {
94 Self {
95 id: id.into(),
96 content: content.into(),
97 is_error: false,
98 }
99 }
100 pub fn err(id: impl Into<String>, content: impl Into<String>) -> Self {
101 Self {
102 id: id.into(),
103 content: content.into(),
104 is_error: true,
105 }
106 }
107}
108
109pub struct ToolContext {
111 pub workspace_root: std::path::PathBuf,
112 #[cfg(feature = "ipc")]
113 pub cancellation: CancellationToken,
114 pub sandbox: Option<std::sync::Arc<crate::sandbox::SandboxManager>>,
115 pub os_sandbox: Option<std::sync::Arc<crate::sandbox::OsSandboxRunner>>,
116 pub provider: Option<Arc<dyn Provider>>,
118 pub tools: Option<Arc<ToolRegistry>>,
120 pub pending_scope: Option<Arc<parking_lot::Mutex<Option<Scope>>>>,
122 #[cfg(feature = "ipc")]
124 pub lsp: Option<Arc<crate::lsp::LspManager>>,
125}
126
127impl ToolContext {
128 pub fn new(workspace_root: impl Into<std::path::PathBuf>) -> Self {
129 Self {
130 workspace_root: workspace_root.into(),
131 #[cfg(feature = "ipc")]
132 cancellation: CancellationToken::new(false),
133 sandbox: None,
134 os_sandbox: None,
135 provider: None,
136 tools: None,
137 pending_scope: None,
138 #[cfg(feature = "ipc")]
139 lsp: None,
140 }
141 }
142
143 pub fn with_sandbox(mut self, sb: Arc<crate::sandbox::SandboxManager>) -> Self {
144 self.sandbox = Some(sb);
145 self
146 }
147
148 pub fn with_os_sandbox(mut self, os: Arc<crate::sandbox::OsSandboxRunner>) -> Self {
149 self.os_sandbox = Some(os);
150 self
151 }
152}
153
154pub type ToolExecuteFn = fn(Arc<ToolContext>, String) -> ToolFuture;
156
157pub type ToolExecuteBox = Box<dyn Fn(Arc<ToolContext>, String) -> ToolFuture + Send + Sync>;
159
160pub enum ToolExecutor {
162 Fn(ToolExecuteFn),
163 Boxed(ToolExecuteBox),
164}
165
166impl ToolExecutor {
167 pub fn call(&self, ctx: Arc<ToolContext>, args: String) -> ToolFuture {
169 match self {
170 ToolExecutor::Fn(f) => f(ctx, args),
171 ToolExecutor::Boxed(b) => b(ctx, args),
172 }
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum ToolEffect {
180 Read,
181 Write,
182 Network,
183 Process,
184}
185
186impl ToolEffect {
187 pub fn supports_parallel(self) -> bool {
189 matches!(self, ToolEffect::Read | ToolEffect::Network)
190 }
191}
192
193pub struct ToolDefinition {
194 pub name: String,
195 pub description: String,
196 pub parameters_json: String,
197 pub execute: ToolExecutor,
198 pub effect: ToolEffect,
199}
200
201impl ToolDefinition {
202 pub fn new_fn(
203 name: impl Into<String>,
204 description: impl Into<String>,
205 parameters_json: impl Into<String>,
206 execute: ToolExecuteFn,
207 ) -> Self {
208 Self {
209 name: name.into(),
210 description: description.into(),
211 parameters_json: parameters_json.into(),
212 execute: ToolExecutor::Fn(execute),
213 effect: ToolEffect::Read,
214 }
215 }
216
217 pub fn new_boxed(
219 name: impl Into<String>,
220 description: impl Into<String>,
221 parameters_json: impl Into<String>,
222 execute: ToolExecuteBox,
223 ) -> Self {
224 Self {
225 name: name.into(),
226 description: description.into(),
227 parameters_json: parameters_json.into(),
228 execute: ToolExecutor::Boxed(execute),
229 effect: ToolEffect::Read,
230 }
231 }
232
233 pub fn with_effect(mut self, effect: ToolEffect) -> Self {
234 self.effect = effect;
235 self
236 }
237}
238
239pub struct ToolRegistry {
241 tools: dashmap::DashMap<String, ToolDefinition>,
242}
243
244impl ToolRegistry {
245 pub fn new() -> Self {
246 Self {
247 tools: dashmap::DashMap::new(),
248 }
249 }
250
251 pub fn register(&mut self, tool: ToolDefinition) {
252 info!("registered tool: {}", tool.name);
253 self.tools.insert(tool.name.clone(), tool);
254 }
255
256 pub fn count(&self) -> usize {
257 self.tools.len()
258 }
259
260 pub fn definitions(&self) -> Vec<serde_json::Value> {
261 self.tools.iter().map(|t| serde_json::json!({
262 "name": t.name,
263 "description": t.description,
264 "parameters": serde_json::from_str::<serde_json::Value>(&t.parameters_json).unwrap_or(serde_json::Value::Null),
265 })).collect()
266 }
267
268 pub async fn execute(
269 &self,
270 name: &str,
271 ctx: &Arc<ToolContext>,
272 arguments: &str,
273 ) -> Option<ToolResult> {
274 let entry = self.tools.get(name)?;
275 Some(entry.execute.call(ctx.clone(), arguments.to_string()).await)
276 }
277
278 pub fn effect_of(&self, name: &str) -> ToolEffect {
280 self.tools
281 .get(name)
282 .map(|e| e.effect)
283 .unwrap_or(ToolEffect::Read)
284 }
285}
286
287impl Default for ToolRegistry {
288 fn default() -> Self {
289 Self::new()
290 }
291}
292
293#[derive(Debug, Clone, Serialize)]
295#[serde(tag = "type")]
296pub enum Event {
297 AgentStart,
298 TurnStart {
299 turn: usize,
300 },
301 MessageStart {
302 role: Role,
303 },
304 MessageDelta {
305 delta: String,
306 },
307 MessageEnd {
308 role: Role,
309 content: String,
310 },
311 ToolCall(ToolCall),
312 ApprovalRequired(crate::permissions::ApprovalRequest),
314 ToolExecutionStart(ToolCall),
315 ToolExecutionEnd(ToolResult),
316 TurnEnd {
317 turn: usize,
318 },
319 AgentEnd,
320 Error(String),
321}
322
323pub type Subscriber = Arc<dyn Fn(&Event) + Send + Sync>;
324
325pub struct Agent {
327 pub model: String,
328 pub system_prompt: Option<String>,
329 pub tools: Arc<ToolRegistry>,
330 pub policy: Policy,
331 pub scope: Scope,
332 scope_profile: Option<Profile>,
333 pub hooks: Option<HookRegistry>,
334 pub approver: Option<Arc<dyn Approver>>,
335 pub provider: Option<Arc<dyn Provider>>,
336 pub max_tool_iterations: usize,
337 pub auto_compact_after: usize,
338 pub workspace_root: std::path::PathBuf,
339 pub sandbox: Option<Arc<crate::sandbox::SandboxManager>>,
340 pub os_sandbox: Option<Arc<crate::sandbox::OsSandboxRunner>>,
341 #[cfg(feature = "skills")]
342 pub skill_registry: Option<crate::skill_engine::SkillRegistry>,
343 #[cfg(feature = "skills")]
344 pub skill_engine: Option<crate::skill_engine::SkillEngine>,
345 #[cfg(feature = "graph-memory")]
346 pub graph_memory: Option<crate::graph_memory::GraphMemory>,
347 #[cfg(feature = "graph-memory")]
349 pub auto_dream: bool,
350 #[cfg(feature = "ipc")]
351 turn_cancellation: CancellationHandle,
352 subscribers: Vec<Subscriber>,
353 pub messages: RwLock<Vec<Message>>,
354 tool_cache: Cache<String, ToolResult>,
355}
356
357impl Agent {
358 pub fn new() -> Self {
359 let mut agent = Self {
360 model: "gpt-4o".into(),
361 system_prompt: None,
362 tools: Arc::new(ToolRegistry::new()),
363 policy: Policy::workspace_write(),
364 scope: Scope::Coding,
365 scope_profile: None,
366 hooks: None,
367 approver: None,
368 provider: None,
369 max_tool_iterations: 50,
370 auto_compact_after: 80,
371 workspace_root: std::env::current_dir().unwrap_or_else(|_| ".".into()),
372 sandbox: None,
373 os_sandbox: None,
374 #[cfg(feature = "skills")]
375 skill_registry: None,
376 #[cfg(feature = "skills")]
377 skill_engine: None,
378 #[cfg(feature = "graph-memory")]
379 graph_memory: None,
380 #[cfg(feature = "graph-memory")]
381 auto_dream: false,
382 #[cfg(feature = "ipc")]
383 turn_cancellation: CancellationHandle::new(),
384 subscribers: Vec::new(),
385 messages: RwLock::new(Vec::new()),
386 tool_cache: Cache::builder()
387 .max_capacity(10_000)
388 .time_to_live(std::time::Duration::from_secs(3600))
389 .time_to_idle(std::time::Duration::from_secs(900))
390 .build(),
391 };
392 if agent.policy.enable_os_sandbox {
394 let _ = agent.enable_os_sandbox();
395 }
396 agent
397 }
398
399 pub fn set_model(&mut self, model: impl Into<String>) {
400 self.model = model.into();
401 }
402
403 pub fn set_system_prompt(&mut self, prompt: impl Into<String>) {
404 self.system_prompt = Some(prompt.into());
405 }
406
407 pub fn set_tools(&mut self, tools: ToolRegistry) {
408 self.tools = Arc::new(tools);
409 }
410
411 pub fn set_policy(&mut self, policy: Policy) {
412 self.policy = policy;
413 if self.policy.enable_os_sandbox && self.os_sandbox.is_none() {
414 let _ = self.enable_os_sandbox();
415 }
416 }
417
418 pub fn set_scope(&mut self, scope: Scope) {
419 self.scope = scope;
420 let profile = mode::profile(scope);
421 self.policy = profile.policy.clone();
422 let base = self.system_prompt.clone();
423 self.system_prompt = Some(mode::compose_prompt(base.as_deref(), &profile));
424 self.scope_profile = Some(profile);
425 }
426
427 pub fn set_hooks(&mut self, hooks: HookRegistry) {
428 self.hooks = Some(hooks);
429 }
430
431 pub fn set_approver(&mut self, approver: Arc<dyn Approver>) {
432 self.approver = Some(approver);
433 }
434
435 pub fn set_provider(&mut self, provider: Arc<dyn Provider>) {
436 self.provider = Some(provider);
437 }
438
439 pub fn set_workspace_root(&mut self, path: impl Into<std::path::PathBuf>) {
440 self.workspace_root = path.into();
441 }
442
443 #[cfg(feature = "ipc")]
444 pub fn cancel(&self) {
445 self.turn_cancellation.cancel();
446 }
447
448 #[cfg(feature = "ipc")]
449 pub fn cancellation_handle(&self) -> CancellationHandle {
450 self.turn_cancellation.clone()
451 }
452
453 pub fn load_project_context(&mut self) {
456 if let Some(instr) = crate::context::load_project_instructions(&self.workspace_root) {
457 self.system_prompt = crate::context::compose_system_prompt(
458 self.system_prompt.as_deref(),
459 &instr.content,
460 );
461 }
462 }
463
464 pub fn set_sandbox(&mut self, sb: Arc<crate::sandbox::SandboxManager>) {
465 self.sandbox = Some(sb);
466 }
467
468 pub fn set_os_sandbox(&mut self, os: Arc<crate::sandbox::OsSandboxRunner>) {
469 self.os_sandbox = Some(os);
470 }
471
472 pub fn enable_os_sandbox(&mut self) -> Result<(), crate::sandbox::SandboxError> {
474 let mode = crate::sandbox::detect_sandbox();
475 let config = crate::sandbox::OsSandboxConfig::new(mode, self.workspace_root.clone());
476 let runner = crate::sandbox::OsSandboxRunner::new(config)?;
477 self.os_sandbox = Some(Arc::new(runner));
478 Ok(())
479 }
480
481 #[cfg(feature = "skills")]
482 pub fn set_skill_registry(&mut self, registry: crate::skill_engine::SkillRegistry) {
483 self.skill_registry = Some(registry);
484 }
485
486 #[cfg(feature = "skills")]
488 pub fn set_skill_engine(&mut self, engine: crate::skill_engine::SkillEngine) {
489 self.skill_engine = Some(engine);
490 }
491
492 #[cfg(feature = "graph-memory")]
493 pub fn set_graph_memory(&mut self, graph: crate::graph_memory::GraphMemory) {
494 self.graph_memory = Some(graph);
495 }
496
497 #[cfg(feature = "graph-memory")]
499 pub fn enable_auto_dream(&mut self, enabled: bool) {
500 self.auto_dream = enabled;
501 }
502
503 pub fn subscribe(&mut self, callback: impl Fn(&Event) + Send + Sync + 'static) {
504 self.subscribers.push(Arc::new(callback));
505 }
506
507 fn emit(&self, event: Event) {
508 if self.subscribers.is_empty() {
509 return;
510 }
511 for sub in &self.subscribers {
512 sub(&event);
513 }
514 }
515
516 pub fn clear_messages(&self) {
517 self.messages.write().clear();
518 }
519
520 pub fn message_count(&self) -> usize {
521 self.messages.read().len()
522 }
523
524 pub async fn prompt(&mut self, text: &str) -> Result<(), AgentError> {
527 let tokens = estimate_messages(&self.messages.read());
528 if tokens >= self.auto_compact_after {
529 self.compact("auto-compact before prompt");
530 }
531
532 #[cfg(feature = "skills")]
534 if let Some(reg) = &self.skill_registry {
535 let activated = reg.auto_activate(text);
536 if !activated.is_empty() {
537 let block = activated.join("\n\n---\n\n");
538 let base = self.system_prompt.as_deref();
539 let merged = match base {
540 Some(b) => format!("{b}\n\n# Active Skills\n\n{block}"),
541 None => format!("# Active Skills\n\n{block}"),
542 };
543 self.system_prompt = Some(merged);
544 }
545 }
546
547 self.messages.write().push(Message::user(text));
548 self.emit(Event::AgentStart);
549
550 let provider = self.provider.clone().ok_or(AgentError::NoProvider)?;
551 let mut tool_ctx = ToolContext::new(self.workspace_root.clone());
552 #[cfg(feature = "ipc")]
553 {
554 tool_ctx.cancellation = self.turn_cancellation.reset();
555 }
556 if let Some(sb) = self.sandbox.clone() {
557 tool_ctx = tool_ctx.with_sandbox(sb);
558 }
559 if let Some(os) = self.os_sandbox.clone() {
560 tool_ctx = tool_ctx.with_os_sandbox(os);
561 }
562 tool_ctx.provider = Some(provider.clone());
563 tool_ctx.tools = Some(Arc::clone(&self.tools));
564 let pending_scope = Arc::new(parking_lot::Mutex::new(None));
565 tool_ctx.pending_scope = Some(Arc::clone(&pending_scope));
566 let ctx = Arc::new(tool_ctx);
567
568 for iteration in 0..self.max_tool_iterations {
569 self.emit(Event::TurnStart { turn: iteration });
570
571 let messages: Vec<Message> = self.messages.read().clone();
572 let system = self.system_prompt.clone();
573
574 #[allow(unused_mut)]
575 let mut tool_calls: Vec<ToolCall> = Vec::new();
576 #[allow(unused_assignments)]
577 let mut assistant_content = String::new();
578
579 self.emit(Event::MessageStart {
580 role: Role::Assistant,
581 });
582
583 #[cfg(feature = "providers")]
584 {
585 use crate::provider::StreamEvent;
586 use futures::StreamExt;
587 let mut attempts = 0;
588 let stream = loop {
589 #[cfg(feature = "ipc")]
590 let result = ctx
591 .cancellation
592 .run(provider.stream(
593 &messages,
594 &system,
595 &self.model,
596 &self.tools.definitions(),
597 ))
598 .await
599 .map_err(|_| AgentError::Cancelled)?;
600 #[cfg(not(feature = "ipc"))]
601 let result = provider
602 .stream(&messages, &system, &self.model, &self.tools.definitions())
603 .await;
604 match result {
605 Ok(stream) => break stream,
606 Err(e) if e.is_transient() && attempts < 2 => {
607 attempts += 1;
608 #[cfg(feature = "ipc")]
609 ctx.cancellation
610 .run(tokio::time::sleep(std::time::Duration::from_millis(
611 250 * (1 << attempts),
612 )))
613 .await
614 .map_err(|_| AgentError::Cancelled)?;
615 #[cfg(not(feature = "ipc"))]
616 tokio::time::sleep(std::time::Duration::from_millis(
617 250 * (1 << attempts),
618 ))
619 .await;
620 }
621 Err(e) => {
622 error!("provider stream error: {e}");
623 self.emit(Event::Error(e.to_string()));
624 return Err(AgentError::Provider(e.to_string()));
625 }
626 }
627 };
628
629 let mut stream = stream;
630 loop {
631 #[cfg(feature = "ipc")]
632 let next = ctx
633 .cancellation
634 .run(stream.next())
635 .await
636 .map_err(|_| AgentError::Cancelled)?;
637 #[cfg(not(feature = "ipc"))]
638 let next = stream.next().await;
639 let Some(event_result) = next else {
640 break;
641 };
642 match event_result {
643 Ok(StreamEvent::Delta(delta)) => {
644 assistant_content.push_str(&delta);
645 self.emit(Event::MessageDelta { delta });
646 }
647 Ok(StreamEvent::ToolCall(call)) => {
648 tool_calls.push(call.clone());
649 self.emit(Event::ToolCall(call));
650 }
651 Ok(StreamEvent::Done) => break,
652 Err(e) => {
653 error!("stream error: {e}");
654 self.emit(Event::Error(e.to_string()));
655 return Err(AgentError::Provider(e.to_string()));
656 }
657 }
658 }
659 }
660
661 #[cfg(not(feature = "providers"))]
662 {
663 let _ = (&provider, &messages, &system);
664 assistant_content =
665 "[providers feature not enabled — enable with --features providers]"
666 .to_string();
667 }
668
669 self.emit(Event::MessageEnd {
670 role: Role::Assistant,
671 content: assistant_content.clone(),
672 });
673
674 if !assistant_content.is_empty() {
675 self.messages
676 .write()
677 .push(Message::assistant(assistant_content));
678 }
679
680 if tool_calls.is_empty() {
681 self.emit(Event::TurnEnd { turn: iteration });
682 break;
683 }
684
685 let results = self.execute_tools_parallel(&tool_calls, &ctx).await;
686 for result in &results {
687 self.messages
688 .write()
689 .push(Message::tool(&result.id, &result.content));
690 }
691 if let Some(scope) = pending_scope.lock().take() {
692 self.set_scope(scope);
693 }
694
695 self.emit(Event::TurnEnd { turn: iteration });
696 }
697
698 #[cfg(feature = "graph-memory")]
699 if let Some(graph) = self.graph_memory.as_mut() {
700 let turns: Vec<crate::graph_memory::ConversationTurn> = self
701 .messages
702 .read()
703 .iter()
704 .map(|m| crate::graph_memory::ConversationTurn {
705 role: m.role.to_string(),
706 content: m.content.clone(),
707 })
708 .collect();
709 let extracted = crate::graph_memory::ConversationExtractor::new().extract(&turns);
710 for node in extracted.nodes {
711 graph.add_node(node);
712 }
713 for edge in extracted.edges {
714 let _ = graph.add_edge(edge);
715 }
716 if self.auto_dream {
717 let _ = crate::dream_scheduler::DreamScheduler::new().run_cycle(graph);
718 }
719 }
720
721 #[cfg(feature = "skills")]
723 if let Some(engine) = self.skill_engine.as_mut() {
724 let turns: Vec<crate::skill_engine::ConversationTurn> = self
725 .messages
726 .read()
727 .iter()
728 .map(|m| crate::skill_engine::ConversationTurn {
729 role: m.role.to_string(),
730 content: m.content.clone(),
731 tool_calls: Vec::new(),
732 })
733 .collect();
734 let mut reviewer = crate::background_review::BackgroundReviewer::new(engine);
735 if let Ok(reviews) =
736 reviewer.review_conversation(&turns, crate::skill_engine::SkillOutcome::Success)
737 {
738 let _ = reviewer.apply_review(&reviews);
739 }
740 }
741
742 self.emit(Event::AgentEnd);
743 Ok(())
744 }
745
746 async fn execute_tools_parallel(
748 &self,
749 calls: &[ToolCall],
750 ctx: &Arc<ToolContext>,
751 ) -> Vec<ToolResult> {
752 let effects: Vec<ToolEffect> = calls
753 .iter()
754 .map(|c| {
755 let name = normalize_tool_name(&c.name);
756 self.tools.effect_of(name)
757 })
758 .collect();
759 let batches = plan_tool_effect_batches(&effects);
760 let mut results: Vec<Option<ToolResult>> = vec![None; calls.len()];
761
762 for batch in batches {
763 if batch.len() == 1 {
764 let idx = batch[0];
765 let original = &calls[idx];
766 self.emit(Event::ToolExecutionStart(original.clone()));
767 let (call, result) = self.execute_single_tool(original, ctx).await;
768 if result.is_error && result.content == "approval required" {
769 self.emit(Event::ApprovalRequired(
770 crate::permissions::ApprovalRequest::from_call(&call, &self.policy),
771 ));
772 }
773 self.emit(Event::ToolExecutionEnd(result.clone()));
774 results[idx] = Some(result);
775 continue;
776 }
777
778 let tools = Arc::clone(&self.tools);
779 let policy = self.policy.clone();
780 let scope_profile = self.scope_profile.clone();
781 let approver = self.approver.clone();
782 let tool_cache = self.tool_cache.clone();
783 let mut join_set = tokio::task::JoinSet::new();
784
785 for idx in batch {
786 let original = &calls[idx];
787 let call = match self.apply_before_tool_hooks(original) {
788 Ok(c) => c,
789 Err(reason) => {
790 self.emit(Event::ToolExecutionStart(original.clone()));
791 let result = ToolResult::err(&original.id, reason);
792 self.emit(Event::ToolExecutionEnd(result.clone()));
793 results[idx] = Some(result);
794 continue;
795 }
796 };
797 self.emit(Event::ToolExecutionStart(call.clone()));
798 let ctx = Arc::clone(ctx);
799 let tools = Arc::clone(&tools);
800 let policy = policy.clone();
801 let scope_profile = scope_profile.clone();
802 let approver = approver.clone();
803 let tool_cache = tool_cache.clone();
804 join_set.spawn(async move {
805 let result = Agent::run_tool_call(
806 &tools,
807 &policy,
808 scope_profile.as_ref(),
809 approver.as_deref(),
810 &tool_cache,
811 &call,
812 &ctx,
813 )
814 .await;
815 (idx, call, result)
816 });
817 }
818
819 while let Some(joined) = join_set.join_next().await {
820 match joined {
821 Ok((idx, call, result)) => {
822 if result.is_error && result.content == "approval required" {
823 self.emit(Event::ApprovalRequired(
824 crate::permissions::ApprovalRequest::from_call(&call, &self.policy),
825 ));
826 }
827 self.emit(Event::ToolExecutionEnd(result.clone()));
828 results[idx] = Some(result);
829 }
830 Err(e) => {
831 warn!("parallel tool task join error: {e}");
832 }
833 }
834 }
835 }
836
837 results
838 .into_iter()
839 .enumerate()
840 .map(|(i, r)| {
841 r.unwrap_or_else(|| {
842 ToolResult::err(
843 calls.get(i).map(|c| c.id.as_str()).unwrap_or(""),
844 "tool execution failed",
845 )
846 })
847 })
848 .collect()
849 }
850
851 fn apply_before_tool_hooks(&self, call: &ToolCall) -> Result<ToolCall, String> {
852 match &self.hooks {
853 Some(hooks) => hooks.run_before_tool(call),
854 None => Ok(call.clone()),
855 }
856 }
857
858 async fn execute_single_tool(
859 &self,
860 call: &ToolCall,
861 ctx: &Arc<ToolContext>,
862 ) -> (ToolCall, ToolResult) {
863 let call = match self.apply_before_tool_hooks(call) {
864 Ok(c) => c,
865 Err(reason) => {
866 let id = call.id.clone();
867 return (call.clone(), ToolResult::err(&id, reason));
868 }
869 };
870 let result = Self::run_tool_call(
871 self.tools.as_ref(),
872 &self.policy,
873 self.scope_profile.as_ref(),
874 self.approver.as_deref(),
875 &self.tool_cache,
876 &call,
877 ctx,
878 )
879 .await;
880 (call, result)
881 }
882
883 async fn run_tool_call(
884 tools: &ToolRegistry,
885 policy: &Policy,
886 scope_profile: Option<&Profile>,
887 approver: Option<&dyn Approver>,
888 tool_cache: &Cache<String, ToolResult>,
889 call: &ToolCall,
890 ctx: &Arc<ToolContext>,
891 ) -> ToolResult {
892 let resolved_name = normalize_tool_name(&call.name).to_string();
893
894 if let Some(profile) = scope_profile {
895 if !mode::tool_allowed(profile, &call.name)
896 && !mode::tool_allowed(profile, &resolved_name)
897 {
898 let msg = format!("tool not in scope {}: {}", profile.scope.name(), call.name);
899 return ToolResult::err(&call.id, msg);
900 }
901 }
902
903 let decision = permissions::authorize_with_workspace(
904 policy,
905 &resolved_name,
906 &call.arguments,
907 approver,
908 Some(ctx.workspace_root.as_path()),
909 );
910
911 match decision {
912 Decision::Deny => ToolResult::err(&call.id, "denied by policy"),
913 Decision::Ask => {
914 ToolResult::err(&call.id, "approval required")
917 }
918 Decision::Allow => {
919 let effect = tools.effect_of(&resolved_name);
920 let cache_key = format!("{}:{}", resolved_name, call.arguments);
921 if effect == ToolEffect::Read {
922 if let Some(cached) = tool_cache.get(&cache_key).await {
923 debug!("tool cache hit: {}", resolved_name);
924 return ToolResult::ok(&call.id, cached.content);
925 }
926 }
927
928 let mut result = match tools.execute(&resolved_name, ctx, &call.arguments).await {
929 Some(r) => r,
930 None => ToolResult::err(&call.id, format!("unknown tool: {}", call.name)),
931 };
932
933 result.content = crate::secrets::Redactor::new().redact(&result.content);
934
935 if !result.is_error {
936 match effect {
937 ToolEffect::Read => {
938 tool_cache.insert(cache_key, result.clone()).await;
939 }
940 ToolEffect::Write | ToolEffect::Process => {
941 tool_cache.invalidate_all();
942 }
943 ToolEffect::Network => {}
944 }
945 }
946
947 result
948 }
949 }
950 }
951
952 pub fn compact(&self, reason: &str) {
953 info!("compacting context: {reason}");
954 let mut msgs = self.messages.write();
955 if msgs.len() <= 2 {
956 return;
957 }
958 let trigger = self.auto_compact_after.max(64);
959 let reserve = (trigger / 4).max(32);
960 let keep_recent = (trigger / 4).max(32);
961 let config = CompactionConfig::new(trigger + reserve, reserve, keep_recent);
962 let result = apply_compaction(&mut msgs, &config);
963 if !result.summary.is_empty() {
964 msgs.push(Message::system(format!("[compact reason: {reason}]")));
965 }
966 }
967}
968
969#[cfg(test)]
970mod tests {
971 use super::*;
972 use std::sync::atomic::{AtomicUsize, Ordering};
973 use std::time::Duration;
974
975 static PARALLEL_DELAY_CALLS: AtomicUsize = AtomicUsize::new(0);
976
977 fn delay_read_tool(name: &str) -> ToolDefinition {
978 ToolDefinition::new_boxed(
979 name,
980 "delay read",
981 "{}",
982 Box::new(|_ctx, _args| {
983 Box::pin(async {
984 PARALLEL_DELAY_CALLS.fetch_add(1, Ordering::SeqCst);
985 tokio::time::sleep(Duration::from_millis(40)).await;
986 ToolResult::ok("id", "ok")
987 })
988 }),
989 )
990 .with_effect(ToolEffect::Read)
991 }
992
993 #[tokio::test]
994 async fn parallel_read_tools_run_concurrently() {
995 PARALLEL_DELAY_CALLS.store(0, Ordering::SeqCst);
996 let mut registry = ToolRegistry::new();
997 registry.register(delay_read_tool("a"));
998 registry.register(delay_read_tool("b"));
999 let mut agent = Agent::new();
1000 agent.set_tools(registry);
1001 agent.set_policy(Policy::full_access());
1002 let ctx = Arc::new(ToolContext::new("."));
1003 let calls = vec![
1004 ToolCall {
1005 id: "1".into(),
1006 name: "a".into(),
1007 arguments: "{}".into(),
1008 },
1009 ToolCall {
1010 id: "2".into(),
1011 name: "b".into(),
1012 arguments: "{}".into(),
1013 },
1014 ];
1015 let start = std::time::Instant::now();
1016 let results = agent.execute_tools_parallel(&calls, &ctx).await;
1017 assert_eq!(results.len(), 2);
1018 assert!(results.iter().all(|r| !r.is_error));
1019 assert_eq!(PARALLEL_DELAY_CALLS.load(Ordering::SeqCst), 2);
1020 assert!(start.elapsed() < Duration::from_millis(70));
1021 }
1022
1023 static CACHE_READ_CALLS: AtomicUsize = AtomicUsize::new(0);
1024 static CACHE_WRITE_CALLS: AtomicUsize = AtomicUsize::new(0);
1025
1026 #[tokio::test]
1027 async fn cache_not_used_for_write_effect() {
1028 CACHE_READ_CALLS.store(0, Ordering::SeqCst);
1029 CACHE_WRITE_CALLS.store(0, Ordering::SeqCst);
1030 let mut registry = ToolRegistry::new();
1031 registry.register(
1032 ToolDefinition::new_boxed(
1033 "r",
1034 "read",
1035 "{}",
1036 Box::new(|_ctx, _args| {
1037 Box::pin(async {
1038 CACHE_READ_CALLS.fetch_add(1, Ordering::SeqCst);
1039 ToolResult::ok("id", "data")
1040 })
1041 }),
1042 )
1043 .with_effect(ToolEffect::Read),
1044 );
1045 registry.register(
1046 ToolDefinition::new_boxed(
1047 "w",
1048 "write",
1049 "{}",
1050 Box::new(|_ctx, _args| {
1051 Box::pin(async {
1052 CACHE_WRITE_CALLS.fetch_add(1, Ordering::SeqCst);
1053 ToolResult::ok("id", "wrote")
1054 })
1055 }),
1056 )
1057 .with_effect(ToolEffect::Write),
1058 );
1059 let mut agent = Agent::new();
1060 agent.set_tools(registry);
1061 agent.set_policy(Policy::full_access());
1062 let ctx = Arc::new(ToolContext::new("."));
1063 let read_call = ToolCall {
1064 id: "1".into(),
1065 name: "r".into(),
1066 arguments: "{}".into(),
1067 };
1068 let write_call = ToolCall {
1069 id: "2".into(),
1070 name: "w".into(),
1071 arguments: "{}".into(),
1072 };
1073
1074 agent.execute_single_tool(&read_call, &ctx).await;
1075 agent.execute_single_tool(&read_call, &ctx).await;
1076 assert_eq!(CACHE_READ_CALLS.load(Ordering::SeqCst), 1);
1077
1078 agent.execute_single_tool(&write_call, &ctx).await;
1079 agent.execute_single_tool(&write_call, &ctx).await;
1080 assert_eq!(CACHE_WRITE_CALLS.load(Ordering::SeqCst), 2);
1081
1082 agent.execute_single_tool(&read_call, &ctx).await;
1083 assert_eq!(CACHE_READ_CALLS.load(Ordering::SeqCst), 2);
1084 }
1085
1086 #[test]
1087 fn compact_uses_token_aware_compaction() {
1088 let mut agent = Agent::new();
1089 agent.auto_compact_after = 50;
1090 {
1091 let mut msgs = agent.messages.write();
1092 msgs.push(Message::system("sys"));
1093 for i in 0..20 {
1094 msgs.push(Message::user(
1095 format!("old message {i} ",) + &"x".repeat(80),
1096 ));
1097 msgs.push(Message::assistant("reply".repeat(40)));
1098 }
1099 msgs.push(Message::user("recent tail"));
1100 }
1101 agent.compact("test");
1102 let msgs = agent.messages.read();
1103 assert!(msgs.len() < 42);
1104 assert!(msgs.iter().any(|m| m.content.contains("context compacted")));
1105 assert!(msgs.iter().any(|m| m.content.contains("recent tail")));
1106 }
1107
1108 #[cfg(feature = "ipc")]
1109 #[test]
1110 fn cancellation_handle_cancels_reset_turn() {
1111 let handle = CancellationHandle::new();
1112 let external = handle.clone();
1113 let token = handle.reset();
1114 external.cancel();
1115 assert!(token.is_canceled());
1116 }
1117}
1118
1119impl Default for Agent {
1120 fn default() -> Self {
1121 Self::new()
1122 }
1123}
1124
1125#[derive(Debug, thiserror::Error)]
1126pub enum AgentError {
1127 #[error("provider error: {0}")]
1128 Provider(String),
1129 #[error("tool error: {0}")]
1130 Tool(String),
1131 #[error("no provider configured")]
1132 NoProvider,
1133 #[error("agent cancelled")]
1134 Cancelled,
1135}