#[cfg(any(feature = "file", feature = "skill"))]
use std::path::PathBuf;
use std::sync::Arc;
use agent_base::{ConsecutiveFailureRecovery, Language, ReasoningConfig, ReasoningEffort, UpdatePlanTool};
#[cfg(feature = "compression")]
use agent_works::compression::{CompressionConfig, CompressionMiddleware, ContextCompactor};
#[cfg(feature = "compression")]
static COMPACTOR: std::sync::Mutex<Option<ContextCompactor>> = std::sync::Mutex::new(None);
pub fn clear_compression_cache() {
#[cfg(feature = "compression")]
{
if let Ok(guard) = COMPACTOR.lock()
&& let Some(ref compactor) = *guard
{
compactor.clear_cache();
tracing::info!("compression cache cleared");
return;
}
tracing::warn!("clear_compression_cache: no compactor registered");
}
}
pub async fn run_compact_session(
runtime: &agent_base::AgentRuntime,
session_id: &agent_base::SessionId,
emit_fn: Option<std::sync::Arc<dyn Fn(agent_base::UserEvent) + Send + Sync>>,
) -> agent_base::AgentResult<Option<bool>> {
#[cfg(feature = "compression")]
{
let handle = {
let guard = COMPACTOR
.lock()
.map_err(|e| agent_base::AgentError::internal(format!("COMPACTOR lock poisoned: {e}")))?;
guard.as_ref().map(|c| c.clone_handle())
};
if let Some(compactor) = handle {
return compactor.compact_session(runtime, session_id, emit_fn).await.map(Some);
}
tracing::warn!("run_compact_session: no compactor registered");
Ok(None)
}
#[cfg(not(feature = "compression"))]
{
let _ = (runtime, session_id, emit_fn);
Ok(None)
}
}
#[allow(unused_mut)]
pub fn base_agent_builder(llm_client: Arc<dyn agent_base::llm_trait::LlmProvider>) -> agent_works::AgentBuilder {
base_agent_builder_with_excludes(llm_client, Vec::new())
}
#[allow(unused_mut)]
pub fn base_agent_builder_with_excludes(
llm_client: Arc<dyn agent_base::llm_trait::LlmProvider>,
file_excludes: Vec<String>,
) -> agent_works::AgentBuilder {
base_agent_builder_with_options(llm_client, file_excludes, None)
}
#[allow(unused_mut)]
pub fn base_agent_builder_with_options(
llm_client: Arc<dyn agent_base::llm_trait::LlmProvider>,
file_excludes: Vec<String>,
compression_config: Option<CompressionConfig>,
) -> agent_works::AgentBuilder {
base_agent_builder_with_options_inner(llm_client, file_excludes, compression_config, false)
}
#[allow(unused_mut)]
pub fn base_agent_builder_no_compression(
llm_client: Arc<dyn agent_base::llm_trait::LlmProvider>,
file_excludes: Vec<String>,
) -> agent_works::AgentBuilder {
base_agent_builder_with_options_inner(llm_client, file_excludes, None, true)
}
#[allow(unused_mut)]
fn base_agent_builder_with_options_inner(
llm_client: Arc<dyn agent_base::llm_trait::LlmProvider>,
file_excludes: Vec<String>,
compression_config: Option<CompressionConfig>,
skip_compression: bool,
) -> agent_works::AgentBuilder {
let max_tool_output_chars = match std::env::var("PHI_MAX_TOOL_OUTPUT_CHARS") {
Ok(value) => match value.trim().parse::<usize>() {
Ok(n) => n,
Err(_) => {
tracing::warn!(
value = %value,
"PHI_MAX_TOOL_OUTPUT_CHARS is not a valid integer; falling back to default 4000"
);
4000
},
},
Err(_) => 4000,
};
#[cfg(feature = "file")]
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let mut builder = agent_works::AgentBuilder::new(llm_client.clone())
.language(Language::En)
.reasoning(ReasoningConfig { effort: Some(ReasoningEffort::Medium), ..Default::default() })
.enable_thought(true)
.enable_thinking(true)
.max_sessions(50)
.max_turns_per_session(100)
.execution_max_turns(200)
.max_message_tokens(120_000)
.max_tool_output_chars(max_tool_output_chars)
.error_recovery(Arc::new(ConsecutiveFailureRecovery::new(3)));
#[cfg(feature = "compression")]
if !skip_compression {
let compactor = ContextCompactor::new(llm_client.clone(), compression_config.unwrap_or_default());
let handle = compactor.clone_handle();
if let Ok(mut guard) = COMPACTOR.lock() {
*guard = Some(handle);
}
builder = builder.middleware(CompressionMiddleware::from_compactor(compactor));
}
#[cfg(feature = "file")]
{
use phi_kernel_tools::file::{EditFileTool, ListFilesTool, ReadFileTool, WriteFileTool};
builder = builder
.register_tool_arc(Arc::new(ReadFileTool::new(cwd.clone())))
.register_tool_arc(Arc::new(WriteFileTool::new(cwd.clone())))
.register_tool_arc(Arc::new(EditFileTool::new(cwd.clone())))
.register_tool_arc(Arc::new(ListFilesTool::with_excludes(cwd.clone(), file_excludes)));
}
builder = builder.register_tool_arc(Arc::new(UpdatePlanTool::new()));
#[cfg(feature = "multi-agent")]
{
use agent_works::multi_agent::MultiAgentConfig;
let ma_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
builder = builder.with_multi_agent(MultiAgentConfig::default()).with_multi_agent_tool_factory(Arc::new(
move |runtime| phi_kernel_tools::multi_agent::create_all_tools(runtime, ma_cwd.clone()),
));
}
#[cfg(feature = "skill")]
{
use agent_works::skill::Skill;
use agent_works::skill::prompt_skill::PromptSkill;
let skill_dirs: Vec<PathBuf> = vec![
dirs_next().join(".claude").join("skills"),
PathBuf::from(".claude/skills"),
];
for dir in &skill_dirs {
if dir.is_dir() {
match PromptSkill::scan_dir(dir) {
Ok(skills) => {
for skill in skills {
tracing::debug!(
name = skill.name(),
dir = %dir.display(),
"auto-loaded skill (prompt-injection mode)"
);
builder = builder.register_skill(skill);
}
},
Err(e) => {
tracing::warn!(dir = %dir.display(), error = %e, "failed to scan skills directory");
},
}
}
}
}
builder
}
#[cfg(feature = "skill")]
fn dirs_next() -> std::path::PathBuf {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."))
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use futures_core::Stream;
use std::pin::Pin;
use std::task::{Context, Poll};
struct StubClient;
struct EmptyStream;
impl Stream for EmptyStream {
type Item = Result<agent_base::StreamChunk, agent_base::llm_trait::LlmError>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Ready(None)
}
}
#[async_trait]
impl agent_base::llm_trait::LlmProvider for StubClient {
async fn stream(
&self,
_request: agent_base::llm_trait::ChatRequest,
) -> Result<agent_base::llm_trait::ChatStream, agent_base::llm_trait::LlmError> {
Ok(agent_base::llm_trait::ChatStream::new(Box::pin(EmptyStream)))
}
async fn chat(
&self,
_request: agent_base::llm_trait::ChatRequest,
) -> Result<agent_base::llm_trait::ChatResponse, agent_base::llm_trait::LlmError> {
Ok(agent_base::llm_trait::ChatResponse {
content: "stub".to_string(),
reasoning_content: None,
tool_calls: vec![],
usage: agent_base::UsageInfo::default(),
finish_reason: agent_base::llm_trait::FinishReason::Stop,
raw: None,
thinking_signature: None,
})
}
fn capabilities(&self) -> agent_base::llm_trait::Capabilities {
agent_base::llm_trait::Capabilities::default()
}
fn info(&self) -> agent_base::llm_trait::ProviderInfo {
agent_base::llm_trait::ProviderInfo { name: "stub".to_string(), model: "stub".to_string(), version: None }
}
}
#[test]
fn test_max_tool_output_chars_default() {
unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
let builder = base_agent_builder(Arc::new(StubClient));
let _ = builder;
}
#[test]
fn test_max_tool_output_chars_custom() {
unsafe { std::env::set_var("PHI_MAX_TOOL_OUTPUT_CHARS", "8000") };
let builder = base_agent_builder(Arc::new(StubClient));
let _ = builder;
unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
}
#[test]
fn test_max_tool_output_chars_invalid_fallback() {
unsafe { std::env::set_var("PHI_MAX_TOOL_OUTPUT_CHARS", "not-a-number") };
let builder = base_agent_builder(Arc::new(StubClient));
let _ = builder;
unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
}
#[cfg(feature = "multi-agent")]
#[tokio::test(flavor = "multi_thread")]
async fn test_base_agent_builder_registers_multi_agent_tools() {
let builder = base_agent_builder(Arc::new(StubClient)).system_prompt("test");
let runtime = builder.build().unwrap();
let tools = tokio::task::block_in_place(|| {
let tools = runtime.tools_mut();
let guard = tools.blocking_read();
guard.metadatas().into_iter().map(|m| m.name).collect::<Vec<String>>()
});
assert!(tools.contains(&"spawn_agent".to_string()), "expected spawn_agent tool");
assert!(tools.contains(&"send_message".to_string()), "expected send_message tool");
assert!(!tools.contains(&"followup_task".to_string()), "followup_task must not be registered");
assert!(!tools.contains(&"wait_agent".to_string()), "wait_agent must not be registered");
assert!(tools.contains(&"list_agents".to_string()), "expected list_agents tool");
assert!(tools.contains(&"close_agent".to_string()), "expected close_agent tool");
}
#[cfg(feature = "multi-agent")]
#[tokio::test(flavor = "multi_thread")]
async fn test_base_agent_builder_without_multi_agent() {
let builder = base_agent_builder(Arc::new(StubClient)).system_prompt("test").without_multi_agent();
let runtime = builder.build().unwrap();
let tools = tokio::task::block_in_place(|| {
let tools = runtime.tools_mut();
let guard = tools.blocking_read();
guard.metadatas().into_iter().map(|m| m.name).collect::<Vec<String>>()
});
assert!(!tools.contains(&"spawn_agent".to_string()), "spawn_agent should not be registered");
assert!(!tools.contains(&"list_agents".to_string()), "list_agents should not be registered");
}
}