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::{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(ListFilesTool::new(cwd.clone())));
86 }
87
88 #[cfg(feature = "multi-agent")]
90 {
91 use agent_works::multi_agent::MultiAgentConfig;
92 builder =
93 builder.with_multi_agent(MultiAgentConfig::default()).with_multi_agent_tool_factory(Arc::new(|runtime| {
94 phi_kernel_tools::multi_agent::create_all_tools(runtime)
95 }));
96 }
97
98 #[cfg(feature = "file")]
100 {
101 use agent_works::skill::Skill;
102 use agent_works::skill::prompt_skill::PromptSkill;
103 let skill_dirs: Vec<PathBuf> = vec![
104 dirs_next().join(".phi").join("skills"),
106 PathBuf::from(".phi/skills"),
108 ];
109
110 for dir in &skill_dirs {
111 if dir.is_dir() {
112 match PromptSkill::scan_dir(dir) {
113 Ok(skills) => {
114 for skill in skills {
115 tracing::debug!(
116 name = skill.name(),
117 dir = %dir.display(),
118 "auto-loaded skill (prompt-injection mode)"
119 );
120 builder = builder.register_skill(skill);
121 }
122 },
123 Err(e) => {
124 tracing::warn!(dir = %dir.display(), error = %e, "failed to scan skills directory");
125 },
126 }
127 }
128 }
129 }
130
131 builder
132}
133
134#[cfg(feature = "file")]
136fn dirs_next() -> std::path::PathBuf {
137 std::env::var("HOME")
138 .or_else(|_| std::env::var("USERPROFILE"))
139 .map(PathBuf::from)
140 .unwrap_or_else(|_| PathBuf::from("."))
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use async_trait::async_trait;
147 use futures_core::Stream;
148 use std::pin::Pin;
149 use std::task::{Context, Poll};
150
151 struct StubClient;
152 struct EmptyStream;
153
154 impl Stream for EmptyStream {
155 type Item = agent_base::AgentResult<agent_base::StreamChunk>;
156 fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
157 Poll::Ready(None)
158 }
159 }
160
161 #[async_trait]
162 impl agent_base::LlmClient for StubClient {
163 async fn chat(
164 &self,
165 _messages: &[agent_base::ChatMessage],
166 _tools: &[serde_json::Value],
167 _reasoning: Option<&agent_base::ReasoningConfig>,
168 _response_format: Option<&agent_base::ResponseFormat>,
169 ) -> agent_base::AgentResult<serde_json::Value> {
170 Ok(serde_json::json!({"choices":[{"message":{"content":"stub"}}]}))
171 }
172 async fn chat_stream(
173 &self,
174 _messages: &[agent_base::ChatMessage],
175 _tools: &[serde_json::Value],
176 _reasoning: Option<&agent_base::ReasoningConfig>,
177 _response_format: Option<&agent_base::ResponseFormat>,
178 ) -> agent_base::AgentResult<Pin<Box<dyn Stream<Item = agent_base::AgentResult<agent_base::StreamChunk>> + Send>>>
179 {
180 Ok(Box::pin(EmptyStream))
181 }
182 fn capabilities(&self) -> agent_base::LlmCapabilities {
183 agent_base::LlmCapabilities {
184 supports_streaming: true,
185 supports_tools: true,
186 supports_vision: false,
187 supports_thinking: true,
188 max_context_tokens: Some(128_000),
189 max_output_tokens: Some(16_384),
190 }
191 }
192 }
193
194 #[test]
195 fn test_max_tool_output_chars_default() {
196 unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
197 let builder = base_agent_builder(agent_base::llm::adapt(Arc::new(StubClient)));
198 let _ = builder;
199 }
200
201 #[test]
202 fn test_max_tool_output_chars_custom() {
203 unsafe { std::env::set_var("PHI_MAX_TOOL_OUTPUT_CHARS", "8000") };
204 let builder = base_agent_builder(agent_base::llm::adapt(Arc::new(StubClient)));
205 let _ = builder;
206 unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
207 }
208
209 #[test]
210 fn test_max_tool_output_chars_invalid_fallback() {
211 unsafe { std::env::set_var("PHI_MAX_TOOL_OUTPUT_CHARS", "not-a-number") };
212 let builder = base_agent_builder(agent_base::llm::adapt(Arc::new(StubClient)));
213 let _ = builder;
214 unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
215 }
216
217 #[cfg(feature = "multi-agent")]
219 #[tokio::test(flavor = "multi_thread")]
220 async fn test_base_agent_builder_registers_multi_agent_tools() {
221 let builder = base_agent_builder(agent_base::llm::adapt(Arc::new(StubClient))).system_prompt("test");
222
223 let runtime = builder.build().unwrap();
224
225 let tools = tokio::task::block_in_place(|| {
226 let tools = runtime.tools_mut();
227 let guard = tools.blocking_read();
228 guard.metadatas().into_iter().map(|m| m.name).collect::<Vec<String>>()
229 });
230
231 assert!(tools.contains(&"spawn_agent".to_string()), "expected spawn_agent tool");
232 assert!(tools.contains(&"send_message".to_string()), "expected send_message tool");
233 assert!(tools.contains(&"followup_task".to_string()), "expected followup_task tool");
234 assert!(tools.contains(&"wait_agent".to_string()), "expected wait_agent tool");
235 assert!(tools.contains(&"list_agents".to_string()), "expected list_agents tool");
236 assert!(tools.contains(&"close_agent".to_string()), "expected close_agent tool");
237 }
238
239 #[cfg(feature = "multi-agent")]
241 #[tokio::test(flavor = "multi_thread")]
242 async fn test_base_agent_builder_without_multi_agent() {
243 let builder = base_agent_builder(agent_base::llm::adapt(Arc::new(StubClient)))
244 .system_prompt("test")
245 .without_multi_agent();
246
247 let runtime = builder.build().unwrap();
248
249 let tools = tokio::task::block_in_place(|| {
250 let tools = runtime.tools_mut();
251 let guard = tools.blocking_read();
252 guard.metadatas().into_iter().map(|m| m.name).collect::<Vec<String>>()
253 });
254
255 assert!(!tools.contains(&"spawn_agent".to_string()), "spawn_agent should not be registered");
256 assert!(!tools.contains(&"list_agents".to_string()), "list_agents should not be registered");
257 }
258}