1use std::sync::Arc;
4use tokio::sync::broadcast;
5use tracing::{info, instrument, error, debug};
6use anyhow;
7
8use crate::error::{Error, Result};
9use crate::agent::context::ContextInjector;
10use crate::agent::message::{Message, Role, Content};
11use crate::agent::provider::Provider;
12use crate::agent::memory::Memory;
13use crate::agent::session::SessionStatus;
14use crate::skills::tool::{Tool, ToolSet};
15use crate::agent::streaming::StreamingResponse;
16use crate::skills::tool::memory::{SearchHistoryTool, RememberThisTool, TieredSearchTool, FetchDocumentTool}; use crate::agent::context::{ContextManager, ContextConfig}; use crate::agent::multi_agent::{Coordinator, AgentRole, MultiAgent, AgentMessage};
19use crate::agent::personality::{Persona, PersonalityManager};
20use crate::agent::cache::Cache;
21use crate::agent::scheduler::Scheduler;
22use crate::skills::tool::{DelegateTool, CronTool};
23use crate::infra::notification::{Notifier, NotifyChannel};
24
25#[derive(Debug, Clone)]
27pub struct AgentConfig {
28 pub name: String,
30 pub model: String,
32 pub preamble: String,
34 pub temperature: Option<f64>,
36 pub max_tokens: Option<u64>,
38 pub extra_params: Option<serde_json::Value>,
40 pub tool_policy: RiskyToolPolicy,
42 pub max_history_messages: usize,
44 pub max_tool_output_chars: usize,
46 pub json_mode: bool,
48 pub persona: Option<Persona>,
50 pub role: AgentRole,
52 pub max_parallel_tools: usize,
54}
55
56impl Default for AgentConfig {
57 fn default() -> Self {
58 Self {
59 name: "agent".to_string(),
60 model: "gpt-4o".to_string(),
61 preamble: "You are a helpful AI assistant.".to_string(),
62 temperature: Some(0.7),
63 max_tokens: Some(4096),
64 extra_params: None,
65 tool_policy: RiskyToolPolicy::default(),
66 max_history_messages: 20,
67 max_tool_output_chars: 4096,
68 json_mode: false,
69 persona: None,
70 role: AgentRole::Assistant,
71 max_parallel_tools: 5,
72 }
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum ToolPolicy {
80 Auto,
82 RequiresApproval,
84 Disabled,
86}
87
88#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
90pub struct RiskyToolPolicy {
91 pub default_policy: ToolPolicy,
93 pub overrides: std::collections::HashMap<String, ToolPolicy>,
95}
96
97impl Default for RiskyToolPolicy {
98 fn default() -> Self {
99 Self {
100 default_policy: ToolPolicy::Auto,
101 overrides: std::collections::HashMap::new(),
102 }
103 }
104}
105
106#[derive(Debug, Clone, serde::Serialize)]
108#[serde(tag = "type", content = "data", rename_all = "snake_case")]
109pub enum AgentEvent {
110 Thinking { prompt: String },
112 ToolCall { tool: String, input: String },
114 ApprovalPending { tool: String, input: String },
116 ToolResult { tool: String, output: String },
118 Response { content: String },
120 Error { message: String },
122}
123
124#[async_trait::async_trait]
126pub trait ApprovalHandler: Send + Sync {
127 async fn approve(&self, tool_name: &str, arguments: &str) -> anyhow::Result<bool>;
129}
130
131pub struct RejectAllApprovalHandler;
133
134#[async_trait::async_trait]
135impl ApprovalHandler for RejectAllApprovalHandler {
136 async fn approve(&self, _tool: &str, _args: &str) -> anyhow::Result<bool> {
137 Ok(false)
138 }
139}
140
141#[derive(Debug)]
143pub struct ApprovalRequest {
144 pub id: String,
146 pub tool_name: String,
148 pub arguments: String,
150 pub responder: tokio::sync::oneshot::Sender<bool>,
152}
153
154pub struct ChannelApprovalHandler {
156 sender: tokio::sync::mpsc::Sender<ApprovalRequest>,
157}
158
159#[async_trait::async_trait]
161pub trait InteractionHandler: Send + Sync {
162 async fn ask(&self, question: &str) -> anyhow::Result<String>;
164}
165
166#[derive(serde::Deserialize, schemars::JsonSchema)]
167struct AskUserArgs {
168 question: String,
170}
171
172struct AskUserTool {
173 handler: Arc<dyn InteractionHandler>,
174}
175
176#[async_trait::async_trait]
177impl crate::skills::tool::Tool for AskUserTool {
178 fn name(&self) -> String {
179 "ask_user".to_string()
180 }
181
182 async fn definition(&self) -> crate::skills::tool::ToolDefinition {
183 let gen = schemars::gen::SchemaSettings::openapi3().into_generator();
184 let schema = gen.into_root_schema_for::<AskUserArgs>();
185 let schema_json = serde_json::to_value(schema).unwrap_or_default();
186
187 crate::skills::tool::ToolDefinition {
188 name: "ask_user".to_string(),
189 description: "Ask the user for clarification, additional information, or a final decision. Use this when you are stuck or need human input.".to_string(),
190 parameters: schema_json,
191 parameters_ts: Some("interface AskUserArgs {\n /** The question to ask the user */\n question: string;\n}".to_string()),
192 is_binary: false,
193 is_verified: true,
194 }
195 }
196
197 async fn call(&self, arguments: &str) -> anyhow::Result<String> {
198 let args: AskUserArgs = serde_json::from_str(arguments)?;
199 self.handler.ask(&args.question).await
200 }
201}
202
203impl ChannelApprovalHandler {
204 pub fn new(sender: tokio::sync::mpsc::Sender<ApprovalRequest>) -> Self {
206 Self { sender }
207 }
208}
209
210#[async_trait::async_trait]
211impl ApprovalHandler for ChannelApprovalHandler {
212 async fn approve(&self, tool_name: &str, arguments: &str) -> anyhow::Result<bool> {
213 let (tx, rx) = tokio::sync::oneshot::channel();
214
215 let request = ApprovalRequest {
216 id: uuid::Uuid::new_v4().to_string(),
217 tool_name: tool_name.to_string(),
218 arguments: arguments.to_string(),
219 responder: tx,
220 };
221
222 self.sender.send(request).await
223 .map_err(|_| Error::Internal("Approval channel closed".to_string()))?;
224
225 let approved = rx.await
227 .map_err(|_| Error::Internal("Approval responder dropped".to_string()))?;
228
229 Ok(approved)
230 }
231}
232
233pub struct Agent<P: Provider> {
237 provider: Arc<P>,
238 tools: ToolSet,
239 config: AgentConfig,
240 context_manager: ContextManager,
241 events: broadcast::Sender<AgentEvent>,
242 approval_handler: Arc<dyn ApprovalHandler>,
243 cache: Option<Arc<dyn Cache>>,
244 notifier: Option<Arc<dyn Notifier>>,
245 memory: Option<Arc<dyn Memory>>,
246 session_id: Option<String>,
247}
248
249impl<P: Provider> Agent<P> {
250 pub fn builder(provider: P) -> AgentBuilder<P> {
252 AgentBuilder::new(provider)
253 }
254
255 pub fn subscribe(&self) -> broadcast::Receiver<AgentEvent> {
257 self.events.subscribe()
258 }
259
260 fn emit(&self, event: AgentEvent) {
262 if let Err(e) = self.events.send(event) {
263 tracing::debug!("Failed to emit event (no receivers): {}", e);
264 }
265 }
266
267 pub async fn notify(&self, channel: NotifyChannel, message: &str) -> Result<()> {
269 if let Some(notifier) = &self.notifier {
270 notifier.notify(channel, message).await
271 } else {
272 tracing::warn!("Agent tried to notify but no notifier is configured: {}", message);
274 Ok(())
275 }
276 }
277
278 pub async fn checkpoint(&self, messages: &[Message], step: usize, status: SessionStatus) -> Result<()> {
280 if let (Some(memory), Some(session_id)) = (&self.memory, &self.session_id) {
281 let session = crate::agent::session::AgentSession {
282 id: session_id.clone(),
283 messages: messages.to_vec(),
284 step,
285 status,
286 updated_at: chrono::Utc::now(),
287 };
288 memory.store_session(session).await?;
289 debug!("Agent checkpoint saved for session: {}", session_id);
290 }
291 Ok(())
292 }
293
294 pub async fn resume(&self, session_id: &str) -> Result<String> {
296 if let Some(memory) = &self.memory {
297 if let Some(session) = memory.retrieve_session(session_id).await? {
298 info!("Resuming agent session: {}", session_id);
299 return self.chat(session.messages).await;
301 }
302 }
303 Err(Error::Internal(format!("Session not found: {}", session_id)))
304 }
305
306 #[instrument(skip(self, prompt), fields(model = %self.config.model))]
308 pub async fn prompt(&self, prompt: impl Into<String>) -> Result<String> {
309 let prompt_str = prompt.into();
310 self.emit(AgentEvent::Thinking { prompt: prompt_str.clone() });
311
312 let messages = vec![
313 Message::user(prompt_str)
314 ];
315
316 self.chat(messages).await
317 }
318
319 #[instrument(skip(self, messages), fields(model = %self.config.model, message_count = messages.len()))]
321 pub async fn chat(&self, mut messages: Vec<Message>) -> Result<String> {
322 let mut steps = 0;
323 const MAX_STEPS: usize = 15;
324
325 loop {
326 if steps >= MAX_STEPS {
327 return Err(Error::agent_config("Max agent steps exceeded"));
328 }
329 steps += 1;
330
331 if let Some(last) = messages.last() {
332 if last.role == Role::User {
333 self.emit(AgentEvent::Thinking { prompt: last.content.as_text() });
334 }
335 }
336
337 self.checkpoint(&messages, steps, SessionStatus::Thinking).await?;
339
340 info!("Agent starting chat completion (step {})", steps);
341
342 if let Some(cache) = &self.cache {
344 if let Ok(Some(cached_response)) = cache.get(&messages).await {
345 info!("Cache hit! Returning cached response.");
346 return Ok(cached_response);
347 }
348 }
349
350 let context_messages = self.context_manager.build_context(&messages).await
352 .map_err(|e| Error::agent_config(format!("Failed to build context: {}", e)))?;
353
354 let stream = self.stream_chat(context_messages).await?;
355
356 let mut full_text = String::new();
357 let mut tool_calls = Vec::new(); let mut stream_inner = stream.into_inner();
360
361 use futures::StreamExt;
363 while let Some(chunk) = stream_inner.next().await {
364 match chunk? {
365 crate::agent::streaming::StreamingChoice::Message(text) => {
366 full_text.push_str(&text);
367 }
368 crate::agent::streaming::StreamingChoice::ToolCall { id, name, arguments } => {
369 tool_calls.push((id, name, arguments));
370 }
371 crate::agent::streaming::StreamingChoice::ParallelToolCalls(map) => {
372 let mut sorted: Vec<_> = map.into_iter().collect();
373 sorted.sort_by_key(|(k,_)| *k);
374 for (_, tc) in sorted {
375 tool_calls.push((tc.id, tc.name, tc.arguments));
376 }
377 }
378 _ => {}
379 }
380 }
381
382 if tool_calls.is_empty() {
384 self.emit(AgentEvent::Response { content: full_text.clone() });
385
386 if let Some(cache) = &self.cache {
388 let _ = cache.set(&messages, full_text.clone()).await;
389 }
390
391 return Ok(full_text);
392 }
393
394 let mut parts = Vec::new();
397 if !full_text.is_empty() {
398 parts.push(crate::agent::message::ContentPart::Text { text: full_text.clone() });
399 }
400 for (id, name, args) in &tool_calls {
401 parts.push(crate::agent::message::ContentPart::ToolCall {
402 id: id.clone(),
403 name: name.clone(),
404 arguments: args.clone(),
405 });
406 }
407 messages.push(Message {
408 role: Role::Assistant,
409 name: None,
410 content: Content::Parts(parts),
411 });
412
413 let tools = &self.tools;
415 let policy = &self.config.tool_policy;
416 let events = &self.events;
417 let approval_handler = &self.approval_handler;
418 let max_parallel = self.config.max_parallel_tools;
419
420 use futures::stream;
421
422 let current_messages = Arc::new(messages.clone());
423
424 let results: Vec<crate::error::Result<(String, String, String)>> = stream::iter(tool_calls)
425 .map(|(id, name, args)| {
426 let name_clone = name.clone();
427 let id_clone = id.clone();
428 let args_str = args.to_string();
429 let msgs = Arc::clone(¤t_messages);
430
431 async move {
432 let tool_ref = tools.get(&name_clone).ok_or_else(|| Error::ToolNotFound(name_clone.clone()))?;
434
435 let def = tool_ref.definition().await;
436
437 let mut effective_policy = policy.overrides.get(&name_clone)
439 .unwrap_or(&policy.default_policy).clone();
440
441 if def.is_binary && !def.is_verified {
443 if effective_policy != ToolPolicy::Disabled {
444 tracing::warn!(tool = %name_clone, "Unverified binary skill detected. Enforcing manual approval.");
445 effective_policy = ToolPolicy::RequiresApproval;
446 }
447 }
448
449 let result = match effective_policy {
450 ToolPolicy::Disabled => {
451 Err(Error::tool_execution(name_clone.clone(), "Tool execution is disabled by policy".to_string()))
452 }
453 ToolPolicy::RequiresApproval => {
454 let _ = events.send(AgentEvent::ApprovalPending {
455 tool: name_clone.clone(),
456 input: args_str.clone()
457 });
458
459 self.checkpoint(&msgs, steps, SessionStatus::AwaitingApproval {
461 tool_name: name_clone.clone(),
462 arguments: args_str.clone()
463 }).await?;
464
465 match approval_handler.approve(&name_clone, &args_str).await {
467 Ok(true) => {
468 let _ = events.send(AgentEvent::ToolCall {
469 tool: name_clone.clone(),
470 input: args_str.clone()
471 });
472 tools.call(&name_clone, &args_str).await
473 .map_err(|e| Error::tool_execution(name_clone.clone(), e.to_string()))
474 }
475 Ok(false) => {
476 Err(Error::ToolApprovalRequired { tool_name: name_clone.clone() })
477 }
478 Err(e) => {
479 Err(Error::tool_execution(name_clone.clone(), format!("Approval check failed: {}", e)))
480 }
481 }
482 }
483 ToolPolicy::Auto => {
484 let _ = events.send(AgentEvent::ToolCall {
485 tool: name_clone.clone(),
486 input: args_str.clone()
487 });
488 tools.call(&name_clone, &args_str).await
489 .map_err(|e| Error::tool_execution(name_clone.clone(), e.to_string()))
490 }
491 };
492
493 match result {
494 Ok(output) => {
495 let _ = events.send(AgentEvent::ToolResult {
496 tool: name_clone.clone(),
497 output: output.clone()
498 });
499 Ok((id_clone, name_clone, output))
500 },
501 Err(e) => {
502 let _ = events.send(AgentEvent::Error { message: e.to_string() });
503 Ok((id_clone, name_clone, format!("Error: {}", e)))
504 }
505 }
506 }
507 })
508 .buffer_unordered(max_parallel)
509 .collect()
510 .await;
511
512 for res in results {
514 let (id, name, output) = res.unwrap(); messages.push(Message {
516 role: Role::Tool,
517 name: None,
518 content: Content::Parts(vec![crate::agent::message::ContentPart::ToolResult {
519 tool_call_id: id,
520 content: output,
521 name: Some(name),
522 }]),
523 });
524 }
525 }
526 }
527
528 pub async fn stream(&self, prompt: impl Into<String>) -> Result<StreamingResponse> {
530 let messages = vec![Message::user(prompt.into())];
531 self.stream_chat(messages).await
532 }
533
534 pub async fn stream_chat(&self, messages: Vec<Message>) -> Result<StreamingResponse> {
536 let mut extra = self.config.extra_params.clone().unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
537
538 if self.config.json_mode {
540 if let serde_json::Value::Object(ref mut map) = extra {
541 if !map.contains_key("response_format") {
542 map.insert("response_format".to_string(), serde_json::json!({ "type": "json_object" }));
543 }
544 }
545 }
546
547 let request = crate::agent::provider::ChatRequest {
548 model: self.config.model.clone(),
549 system_prompt: Some(self.config.preamble.clone()),
550 messages,
551 tools: self.tools.definitions().await,
552 temperature: self.config.temperature,
553 max_tokens: self.config.max_tokens,
554 extra_params: Some(extra),
555 };
556
557 self.provider.stream_completion(request).await
558 }
559
560 #[instrument(skip(self, arguments), fields(tool_name = %name))]
562 pub async fn call_tool(&self, name: &str, arguments: &str) -> Result<String> {
563 let policy = self.config.tool_policy.overrides.get(name)
565 .unwrap_or(&self.config.tool_policy.default_policy);
566
567 match policy {
568 ToolPolicy::Disabled => {
569 return Err(Error::tool_execution(name.to_string(), "Tool execution is disabled by policy".to_string()));
570 }
571 ToolPolicy::RequiresApproval => {
572 self.emit(AgentEvent::ApprovalPending { tool: name.to_string(), input: arguments.to_string() });
573
574 match self.approval_handler.approve(name, arguments).await {
575 Ok(true) => {}, Ok(false) => return Err(Error::ToolApprovalRequired { tool_name: name.to_string() }),
577 Err(e) => return Err(Error::tool_execution(name.to_string(), format!("Approval check failed: {}", e)))
578 }
579 }
580 ToolPolicy::Auto => {} }
582
583 self.emit(AgentEvent::ToolCall { tool: name.to_string(), input: arguments.to_string() });
584
585 let result = self.tools.call(name, arguments).await;
586
587 match result {
588 Ok(mut output) => {
589 if output.len() > self.config.max_tool_output_chars {
591 let original_len = output.len();
592 output.truncate(self.config.max_tool_output_chars);
593 output.push_str(&format!("\n\n(Note: Output truncated from {} to {} chars to save tokens)",
594 original_len, self.config.max_tool_output_chars));
595 }
596
597 self.emit(AgentEvent::ToolResult { tool: name.to_string(), output: output.clone() });
598 Ok(output)
599 },
600 Err(e) => {
601 self.emit(AgentEvent::Error { message: e.to_string() });
602 Err(Error::tool_execution(name.to_string(), e.to_string()))
604 }
605 }
606 }
607
608 pub fn has_tool(&self, name: &str) -> bool {
610 self.tools.contains(name)
611 }
612
613 pub async fn tool_definitions(&self) -> Vec<crate::skills::tool::ToolDefinition> {
615 self.tools.definitions().await
616 }
617
618 pub fn config(&self) -> &AgentConfig {
620 &self.config
621 }
622
623 pub fn model(&self) -> &str {
625 &self.config.model
626 }
627
628 pub async fn listen(
630 &self,
631 mut user_input: tokio::sync::mpsc::Receiver<String>,
632 mut external_events: tokio::sync::mpsc::Receiver<AgentMessage>
633 ) -> Result<()> {
634 info!("Agent {} starting proactive loop", self.config.name);
635
636 loop {
637 tokio::select! {
638 input = user_input.recv() => {
640 match input {
641 Some(text) => {
642 if let Err(e) = self.process(&text).await {
643 error!("Error in proactive user task: {}", e);
644 }
645 }
646 None => {
647 info!("User input channel closed, exiting proactive loop");
648 break;
649 }
650 }
651 }
652
653 msg = external_events.recv() => {
655 match msg {
656 Some(message) => {
657 if let Err(e) = self.handle_message(message).await {
658 error!("Error in proactive external task: {}", e);
659 }
660 }
661 None => {
662 info!("External events channel closed, exiting proactive loop");
663 break;
664 }
665 }
666 }
667 }
668 }
669
670 Ok(())
671 }
672}
673
674pub struct AgentBuilder<P: Provider> {
676 provider: P,
677 tools: ToolSet,
678 config: AgentConfig,
679 injectors: Vec<Box<dyn ContextInjector>>,
680 approval_handler: Option<Arc<dyn ApprovalHandler>>,
681 interaction_handler: Option<Arc<dyn InteractionHandler>>,
682 notifier: Option<Arc<dyn Notifier>>,
683 cache: Option<Arc<dyn Cache>>,
684 has_sidecar: bool,
686 has_dynamic_skill: bool,
688 memory: Option<Arc<dyn Memory>>,
689 session_id: Option<String>,
690}
691
692impl<P: Provider> AgentBuilder<P> {
693 pub fn new(provider: P) -> Self {
695 Self {
696 provider,
697 tools: ToolSet::new(),
698 config: AgentConfig::default(),
699 injectors: Vec::new(),
700 approval_handler: None,
701 interaction_handler: None,
702 notifier: None,
703 cache: None,
704 has_sidecar: false,
705 has_dynamic_skill: false,
706 memory: None,
707 session_id: None,
708 }
709 }
710
711 pub fn model(mut self, model: impl Into<String>) -> Self {
713 self.config.model = model.into();
714 self
715 }
716
717 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
719 self.config.preamble = prompt.into();
720 self
721 }
722
723 pub fn preamble(self, prompt: impl Into<String>) -> Self {
725 self.system_prompt(prompt)
726 }
727
728 pub fn temperature(mut self, temp: f64) -> Self {
730 self.config.temperature = Some(temp);
731 self
732 }
733
734 pub fn max_tokens(mut self, tokens: u64) -> Self {
736 self.config.max_tokens = Some(tokens);
737 self
738 }
739
740 pub fn extra_params(mut self, params: serde_json::Value) -> Self {
742 self.config.extra_params = Some(params);
743 self
744 }
745
746 pub fn tool_policy(mut self, policy: RiskyToolPolicy) -> Self {
748 self.config.tool_policy = policy;
749 self
750 }
751
752 pub fn approval_handler(mut self, handler: impl ApprovalHandler + 'static) -> Self {
754 self.approval_handler = Some(Arc::new(handler));
755 self
756 }
757
758 pub fn interaction_handler(mut self, handler: impl InteractionHandler + 'static) -> Self {
760 self.interaction_handler = Some(Arc::new(handler));
761 self
762 }
763
764 pub fn max_history_messages(mut self, count: usize) -> Self {
766 self.config.max_history_messages = count;
767 self
768 }
769
770 pub fn max_tool_output_chars(mut self, count: usize) -> Self {
772 self.config.max_tool_output_chars = count;
773 self
774 }
775
776 pub fn json_mode(mut self, enable: bool) -> Self {
778 self.config.json_mode = enable;
779 self
780 }
781
782 pub fn persona(mut self, persona: Persona) -> Self {
784 self.config.persona = Some(persona);
785 self
786 }
787
788 pub fn notifier(mut self, notifier: impl Notifier + 'static) -> Self {
790 self.notifier = Some(Arc::new(notifier));
791 self
792 }
793
794 pub fn session_id(mut self, id: impl Into<String>) -> Self {
796 self.session_id = Some(id.into());
797 self
798 }
799
800 pub fn role(mut self, role: AgentRole) -> Self {
802 self.config.role = role;
803 self
804 }
805
806 pub fn context_injector(mut self, injector: impl ContextInjector + 'static) -> Self {
808 self.injectors.push(Box::new(injector));
809 self
810 }
811
812 pub fn tool<T: Tool + 'static>(mut self, tool: T) -> Self {
814 self.tools.add(tool);
815 self
816 }
817
818 pub fn shared_tool(mut self, tool: Arc<dyn Tool>) -> Self {
820 self.tools.add_shared(tool);
821 self
822 }
823
824 pub fn tools(mut self, tools: ToolSet) -> Self {
826 for (_, tool) in tools.iter() {
827 self.tools.add_shared(Arc::clone(tool));
828 }
829 self
830 }
831
832 pub fn with_memory(mut self, memory: Arc<dyn crate::agent::memory::Memory>) -> Self {
834 self.tools.add(SearchHistoryTool::new(memory.clone()));
835 self.tools.add(RememberThisTool::new(memory.clone()));
836 self.tools.add(TieredSearchTool::new(memory.clone()));
837 self.tools.add(FetchDocumentTool::new(memory.clone()));
838
839 self.memory = Some(memory);
840 self
841 }
842
843 pub fn with_dynamic_skills(mut self, skill_loader: Arc<crate::skills::SkillLoader>) -> Result<Self> {
856 if self.has_sidecar {
858 return Err(Error::agent_config(
859 "Security Error: Cannot enable DynamicSkill when Python Sidecar is configured. \
860 These are mutually exclusive due to context pollution risks. \
861 See SECURITY.md for details."
862 ));
863 }
864
865 for skill_ref in skill_loader.skills.iter() {
867 self.tools.add_shared(Arc::clone(skill_ref.value()) as Arc<dyn crate::skills::tool::Tool>);
868 }
869
870 self.tools.add(crate::skills::ClawHubTool::new(Arc::clone(&skill_loader)));
872 self.tools.add(crate::skills::ReadSkillDoc::new(skill_loader));
873
874 self.has_dynamic_skill = true;
875
876 Ok(self)
877 }
878
879 pub async fn with_code_interpreter(mut self, address: impl Into<String>) -> Result<Self> {
892 if self.has_dynamic_skill {
894 return Err(Error::agent_config(
895 "Security Error: Cannot enable Python Sidecar when DynamicSkill is configured. \
896 These are mutually exclusive due to context pollution risks. \
897 See SECURITY.md for details."
898 ));
899 }
900
901 let sidecar = crate::skills::capabilities::Sidecar::connect(address.into()).await?;
902 let shared_sidecar = Arc::new(tokio::sync::Mutex::new(sidecar));
903
904 self.tools.add(crate::skills::tool::code_interpreter::CodeInterpreter::new(shared_sidecar));
905 self.has_sidecar = true;
906
907 Ok(self)
908 }
909
910 pub fn build(mut self) -> Result<Agent<P>> {
921 if self.config.model.is_empty() {
923 return Err(Error::agent_config("model name cannot be empty"));
924 }
925 if self.config.max_history_messages == 0 {
926 return Err(Error::agent_config("max_history_messages must be at least 1"));
927 }
928
929 if !self.has_sidecar && !self.has_dynamic_skill {
931 info!("No execution model configured. Auto-enabling DynamicSkill (default)...");
932
933 let skill_loader = Arc::new(crate::skills::SkillLoader::new("./skills"));
935
936 match tokio::task::block_in_place(|| {
938 tokio::runtime::Handle::current().block_on(skill_loader.load_all())
939 }) {
940 Ok(_) => {
941 info!("Loaded DynamicSkills from ./skills");
942
943 for skill_ref in skill_loader.skills.iter() {
945 self.tools.add_shared(Arc::clone(skill_ref.value()) as Arc<dyn crate::skills::tool::Tool>);
946 }
947
948 self.tools.add(crate::skills::ClawHubTool::new(Arc::clone(&skill_loader)));
950 self.tools.add(crate::skills::ReadSkillDoc::new(skill_loader));
951
952 self.has_dynamic_skill = true;
953 },
954 Err(e) => {
955 info!("DynamicSkill auto-enable skipped (no skills found): {}", e);
957 }
959 }
960 }
961
962 let (tx, _) = broadcast::channel(1000);
963
964 let mut context_config = ContextConfig::default();
965 context_config.max_history_messages = self.config.max_history_messages;
966 if let Some(tokens) = self.config.max_tokens {
967 context_config.response_reserve = tokens as usize;
971 }
972
973 let mut context_manager = ContextManager::new(context_config);
974 context_manager.set_system_prompt(self.config.preamble.clone());
975
976 context_manager.add_injector(Box::new(self.tools.clone()));
979
980 for injector in self.injectors {
981 context_manager.add_injector(injector);
982 }
983
984 if let Some(persona) = &self.config.persona {
985 context_manager.add_injector(Box::new(PersonalityManager::new(persona.clone())));
986 }
987
988 let mut tools = self.tools;
990 if let Some(handler) = &self.interaction_handler {
991 tools.add(AskUserTool { handler: Arc::clone(handler) });
992 }
993
994 Ok(Agent {
995 provider: Arc::new(self.provider),
996 tools,
997 config: self.config,
998 context_manager,
999 events: tx,
1000 approval_handler: self.approval_handler.unwrap_or_else(|| Arc::new(RejectAllApprovalHandler)),
1001 cache: self.cache,
1002 notifier: self.notifier,
1003 memory: self.memory,
1004 session_id: self.session_id,
1005 })
1006 }
1007
1008 pub fn with_delegation(mut self, coordinator: Arc<Coordinator>) -> Self {
1010 self.tools.add(DelegateTool::new(Arc::downgrade(&coordinator)));
1011 self
1012 }
1013
1014 pub fn with_scheduler(mut self, scheduler: Arc<Scheduler>) -> Self {
1016 self.tools.add(CronTool::new(Arc::downgrade(&scheduler)));
1017 self
1018 }
1019}
1020
1021#[async_trait::async_trait]
1022impl<P: Provider> MultiAgent for Agent<P> {
1023 fn role(&self) -> AgentRole {
1024 self.config.role.clone()
1025 }
1026
1027 async fn handle_message(&self, message: AgentMessage) -> Result<Option<AgentMessage>> {
1028 info!("Agent {:?} handling message from {:?}", self.role(), message.from);
1029 let response = self.prompt(message.content).await?;
1030
1031 Ok(Some(AgentMessage {
1032 from: self.role(),
1033 to: Some(message.from),
1034 content: response,
1035 msg_type: crate::agent::multi_agent::MessageType::Response,
1036 }))
1037 }
1038
1039 async fn process(&self, input: &str) -> Result<String> {
1040 self.prompt(input).await
1041 }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047
1048 #[test]
1049 fn test_agent_config_default() {
1050 let config = AgentConfig::default();
1051 assert_eq!(config.model, "gpt-4o");
1052 assert_eq!(config.max_tokens, Some(4096));
1053 }
1054}