Skip to main content

phi_agent/agent/
builder.rs

1//! General-purpose AgentBuilder factory — provides default configuration
2//! shared across consumers.
3//!
4//! Returns a pre-configured AgentBuilder; callers then register tools and
5//! approval handlers on top.
6
7use std::sync::Arc;
8
9use agent_base::{AgentBuilder, ConsecutiveFailureRecovery, Language, ReasoningConfig, ReasoningEffort};
10
11use crate::agent::compression::SummarizingMiddleware;
12
13/// Returns an AgentBuilder with sensible defaults:
14/// - English
15/// - Medium reasoning effort
16/// - Thinking enabled
17/// - Consecutive failure recovery (default 3 retries)
18/// - Session limits (50 sessions / 100 turns per session / 50k per-message cap)
19/// - Per-run react-loop cap (200 iterations for one user input)
20/// - LLM-based context compression for long tool-heavy conversations
21///
22/// Callers are responsible for: registering tools, setting the approval
23/// handler, setting the system prompt, then calling `.build()`.
24pub fn base_agent_builder(llm_client: Arc<dyn agent_base::LlmClient>) -> AgentBuilder {
25    // Tool-output cap (default 4000 chars). Tune via PHI_MAX_TOOL_OUTPUT_CHARS for large
26    // outputs (HTML, base64 images, long lists). Truncated results carry an explicit
27    // "...(truncated)" suffix plus structured TruncationInfo from agent-base.
28    let max_tool_output_chars = match std::env::var("PHI_MAX_TOOL_OUTPUT_CHARS") {
29        Ok(value) => match value.trim().parse::<usize>() {
30            Ok(n) => n,
31            Err(_) => {
32                tracing::warn!(
33                    value = %value,
34                    "PHI_MAX_TOOL_OUTPUT_CHARS is not a valid integer; falling back to default 4000"
35                );
36                4000
37            },
38        },
39        Err(_) => 4000,
40    };
41
42    AgentBuilder::new(llm_client.clone())
43        .language(Language::En)
44        .reasoning(ReasoningConfig { effort: Some(ReasoningEffort::Medium), ..Default::default() })
45        .enable_thought(true)
46        .enable_thinking(true)
47        .max_sessions(50)
48        .max_turns_per_session(100)
49        .execution_max_turns(200)
50        .max_message_tokens(50_000)
51        .max_tool_output_chars(max_tool_output_chars)
52        .error_recovery(Arc::new(ConsecutiveFailureRecovery::new(3)))
53        // Summarise the earlier part of long conversations so per-call LLM context
54        // stays bounded (see compression.rs). Override via the returned builder, or
55        // build your own AgentBuilder to opt out.
56        .middleware(SummarizingMiddleware::new(llm_client))
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use async_trait::async_trait;
63    use futures_core::Stream;
64    use std::pin::Pin;
65    use std::task::{Context, Poll};
66
67    struct StubClient;
68    struct EmptyStream;
69
70    impl Stream for EmptyStream {
71        type Item = agent_base::AgentResult<agent_base::StreamChunk>;
72        fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
73            Poll::Ready(None)
74        }
75    }
76
77    #[async_trait]
78    impl agent_base::LlmClient for StubClient {
79        async fn chat(
80            &self, _messages: &[agent_base::ChatMessage], _tools: &[serde_json::Value],
81            _reasoning: Option<&agent_base::ReasoningConfig>,
82            _response_format: Option<&agent_base::ResponseFormat>,
83        ) -> agent_base::AgentResult<serde_json::Value> {
84            Ok(serde_json::json!({"choices":[{"message":{"content":"stub"}}]}))
85        }
86        async fn chat_stream(
87            &self, _messages: &[agent_base::ChatMessage], _tools: &[serde_json::Value],
88            _reasoning: Option<&agent_base::ReasoningConfig>,
89            _response_format: Option<&agent_base::ResponseFormat>,
90        ) -> agent_base::AgentResult<Pin<Box<dyn Stream<Item = agent_base::AgentResult<agent_base::StreamChunk>> + Send>>> {
91            Ok(Box::pin(EmptyStream))
92        }
93        fn capabilities(&self) -> agent_base::LlmCapabilities {
94            agent_base::LlmCapabilities {
95                supports_streaming: true, supports_tools: true, supports_vision: false,
96                supports_thinking: true, max_context_tokens: Some(128_000), max_output_tokens: Some(16_384),
97            }
98        }
99    }
100
101    #[test]
102    fn test_max_tool_output_chars_default() {
103        unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
104        let builder = base_agent_builder(Arc::new(StubClient));
105        let _ = builder;
106    }
107
108    #[test]
109    fn test_max_tool_output_chars_custom() {
110        unsafe { std::env::set_var("PHI_MAX_TOOL_OUTPUT_CHARS", "8000") };
111        let builder = base_agent_builder(Arc::new(StubClient));
112        let _ = builder;
113        unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
114    }
115
116    #[test]
117    fn test_max_tool_output_chars_invalid_fallback() {
118        unsafe { std::env::set_var("PHI_MAX_TOOL_OUTPUT_CHARS", "not-a-number") };
119        let builder = base_agent_builder(Arc::new(StubClient));
120        let _ = builder;
121        unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
122    }
123}