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