phi_agent/agent/
builder.rs1#[cfg(feature = "file")]
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use agent_base::{ConsecutiveFailureRecovery, Language, ReasoningConfig, ReasoningEffort};
12
13use crate::agent::compression::SummarizingMiddleware;
14
15#[allow(unused_mut)]
41pub fn base_agent_builder(llm_client: Arc<dyn agent_base::StreamClient>) -> agent_works::AgentBuilder {
42 let max_tool_output_chars = match std::env::var("PHI_MAX_TOOL_OUTPUT_CHARS") {
46 Ok(value) => match value.trim().parse::<usize>() {
47 Ok(n) => n,
48 Err(_) => {
49 tracing::warn!(
50 value = %value,
51 "PHI_MAX_TOOL_OUTPUT_CHARS is not a valid integer; falling back to default 4000"
52 );
53 4000
54 },
55 },
56 Err(_) => 4000,
57 };
58
59 #[cfg(feature = "file")]
60 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
61
62 let mut builder = agent_works::AgentBuilder::new(llm_client.clone())
63 .language(Language::En)
64 .reasoning(ReasoningConfig { effort: Some(ReasoningEffort::Medium), ..Default::default() })
65 .enable_thought(true)
66 .enable_thinking(true)
67 .max_sessions(50)
68 .max_turns_per_session(100)
69 .execution_max_turns(200)
70 .max_message_tokens(50_000)
71 .max_tool_output_chars(max_tool_output_chars)
72 .error_recovery(Arc::new(ConsecutiveFailureRecovery::new(3)))
73 .middleware(SummarizingMiddleware::new(llm_client));
77
78 #[cfg(feature = "file")]
80 {
81 use phi_kernel_tools::file::{EditFileTool, ListFilesTool, ReadFileTool, WriteFileTool};
82 builder = builder
83 .register_tool_arc(Arc::new(ReadFileTool::new(cwd.clone())))
84 .register_tool_arc(Arc::new(WriteFileTool::new(cwd.clone())))
85 .register_tool_arc(Arc::new(EditFileTool::new(cwd.clone())))
86 .register_tool_arc(Arc::new(ListFilesTool::new(cwd.clone())));
87 }
88
89 #[cfg(feature = "multi-agent")]
91 {
92 use agent_works::multi_agent::MultiAgentConfig;
93 builder =
94 builder.with_multi_agent(MultiAgentConfig::default()).with_multi_agent_tool_factory(Arc::new(|runtime| {
95 phi_kernel_tools::multi_agent::create_all_tools(runtime)
96 }));
97 }
98
99 #[cfg(feature = "file")]
101 {
102 use agent_works::skill::Skill;
103 use agent_works::skill::prompt_skill::PromptSkill;
104 let skill_dirs: Vec<PathBuf> = vec![
105 dirs_next().join(".phi").join("skills"),
107 PathBuf::from(".phi/skills"),
109 ];
110
111 for dir in &skill_dirs {
112 if dir.is_dir() {
113 match PromptSkill::scan_dir(dir) {
114 Ok(skills) => {
115 for skill in skills {
116 tracing::debug!(
117 name = skill.name(),
118 dir = %dir.display(),
119 "auto-loaded skill (prompt-injection mode)"
120 );
121 builder = builder.register_skill(skill);
122 }
123 },
124 Err(e) => {
125 tracing::warn!(dir = %dir.display(), error = %e, "failed to scan skills directory");
126 },
127 }
128 }
129 }
130 }
131
132 builder
133}
134
135#[cfg(feature = "file")]
137fn dirs_next() -> std::path::PathBuf {
138 std::env::var("HOME")
139 .or_else(|_| std::env::var("USERPROFILE"))
140 .map(PathBuf::from)
141 .unwrap_or_else(|_| PathBuf::from("."))
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use async_trait::async_trait;
148 use futures_core::Stream;
149 use std::pin::Pin;
150 use std::task::{Context, Poll};
151
152 struct StubClient;
153 struct EmptyStream;
154
155 impl Stream for EmptyStream {
156 type Item = agent_base::AgentResult<agent_base::StreamChunk>;
157 fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
158 Poll::Ready(None)
159 }
160 }
161
162 #[async_trait]
163 impl agent_base::LlmClient for StubClient {
164 async fn chat(
165 &self,
166 _messages: &[agent_base::ChatMessage],
167 _tools: &[serde_json::Value],
168 _reasoning: Option<&agent_base::ReasoningConfig>,
169 _response_format: Option<&agent_base::ResponseFormat>,
170 ) -> agent_base::AgentResult<serde_json::Value> {
171 Ok(serde_json::json!({"choices":[{"message":{"content":"stub"}}]}))
172 }
173 async fn chat_stream(
174 &self,
175 _messages: &[agent_base::ChatMessage],
176 _tools: &[serde_json::Value],
177 _reasoning: Option<&agent_base::ReasoningConfig>,
178 _response_format: Option<&agent_base::ResponseFormat>,
179 ) -> agent_base::AgentResult<Pin<Box<dyn Stream<Item = agent_base::AgentResult<agent_base::StreamChunk>> + Send>>>
180 {
181 Ok(Box::pin(EmptyStream))
182 }
183 fn capabilities(&self) -> agent_base::LlmCapabilities {
184 agent_base::LlmCapabilities {
185 supports_streaming: true,
186 supports_tools: true,
187 supports_vision: false,
188 supports_thinking: true,
189 max_context_tokens: Some(128_000),
190 max_output_tokens: Some(16_384),
191 }
192 }
193 }
194
195 #[test]
196 fn test_max_tool_output_chars_default() {
197 unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
198 let builder = base_agent_builder(agent_base::llm::adapt(Arc::new(StubClient)));
199 let _ = builder;
200 }
201
202 #[test]
203 fn test_max_tool_output_chars_custom() {
204 unsafe { std::env::set_var("PHI_MAX_TOOL_OUTPUT_CHARS", "8000") };
205 let builder = base_agent_builder(agent_base::llm::adapt(Arc::new(StubClient)));
206 let _ = builder;
207 unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
208 }
209
210 #[test]
211 fn test_max_tool_output_chars_invalid_fallback() {
212 unsafe { std::env::set_var("PHI_MAX_TOOL_OUTPUT_CHARS", "not-a-number") };
213 let builder = base_agent_builder(agent_base::llm::adapt(Arc::new(StubClient)));
214 let _ = builder;
215 unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
216 }
217
218 #[cfg(feature = "multi-agent")]
220 #[tokio::test(flavor = "multi_thread")]
221 async fn test_base_agent_builder_registers_multi_agent_tools() {
222 let builder = base_agent_builder(agent_base::llm::adapt(Arc::new(StubClient))).system_prompt("test");
223
224 let runtime = builder.build().unwrap();
225
226 let tools = tokio::task::block_in_place(|| {
227 let tools = runtime.tools_mut();
228 let guard = tools.blocking_read();
229 guard.metadatas().into_iter().map(|m| m.name).collect::<Vec<String>>()
230 });
231
232 assert!(tools.contains(&"spawn_agent".to_string()), "expected spawn_agent tool");
233 assert!(tools.contains(&"send_message".to_string()), "expected send_message tool");
234 assert!(tools.contains(&"followup_task".to_string()), "expected followup_task tool");
235 assert!(tools.contains(&"wait_agent".to_string()), "expected wait_agent tool");
236 assert!(tools.contains(&"list_agents".to_string()), "expected list_agents tool");
237 assert!(tools.contains(&"close_agent".to_string()), "expected close_agent tool");
238 }
239
240 #[cfg(feature = "multi-agent")]
242 #[tokio::test(flavor = "multi_thread")]
243 async fn test_base_agent_builder_without_multi_agent() {
244 let builder = base_agent_builder(agent_base::llm::adapt(Arc::new(StubClient)))
245 .system_prompt("test")
246 .without_multi_agent();
247
248 let runtime = builder.build().unwrap();
249
250 let tools = tokio::task::block_in_place(|| {
251 let tools = runtime.tools_mut();
252 let guard = tools.blocking_read();
253 guard.metadatas().into_iter().map(|m| m.name).collect::<Vec<String>>()
254 });
255
256 assert!(!tools.contains(&"spawn_agent".to_string()), "spawn_agent should not be registered");
257 assert!(!tools.contains(&"list_agents".to_string()), "list_agents should not be registered");
258 }
259}