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