#[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::StreamClient>) -> 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::StreamClient>,
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::StreamClient>,
file_excludes: Vec<String>,
compression_config: Option<CompressionConfig>,
) -> 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(50_000)
.max_tool_output_chars(max_tool_output_chars)
.error_recovery(Arc::new(ConsecutiveFailureRecovery::new(3)));
#[cfg(feature = "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;
builder =
builder.with_multi_agent(MultiAgentConfig::default()).with_multi_agent_tool_factory(Arc::new(|runtime| {
phi_kernel_tools::multi_agent::create_all_tools(runtime)
}));
}
#[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 = agent_base::AgentResult<agent_base::StreamChunk>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Ready(None)
}
}
#[async_trait]
impl agent_base::LlmClient for StubClient {
async fn chat(
&self,
_messages: &[agent_base::ChatMessage],
_tools: &[serde_json::Value],
_reasoning: Option<&agent_base::ReasoningConfig>,
_response_format: Option<&agent_base::ResponseFormat>,
) -> agent_base::AgentResult<serde_json::Value> {
Ok(serde_json::json!({"choices":[{"message":{"content":"stub"}}]}))
}
async fn chat_stream(
&self,
_messages: &[agent_base::ChatMessage],
_tools: &[serde_json::Value],
_reasoning: Option<&agent_base::ReasoningConfig>,
_response_format: Option<&agent_base::ResponseFormat>,
) -> agent_base::AgentResult<Pin<Box<dyn Stream<Item = agent_base::AgentResult<agent_base::StreamChunk>> + Send>>>
{
Ok(Box::pin(EmptyStream))
}
fn capabilities(&self) -> agent_base::LlmCapabilities {
agent_base::LlmCapabilities {
supports_streaming: true,
supports_tools: true,
supports_vision: false,
supports_thinking: true,
max_context_tokens: Some(128_000),
max_output_tokens: Some(16_384),
}
}
}
#[test]
fn test_max_tool_output_chars_default() {
unsafe { std::env::remove_var("PHI_MAX_TOOL_OUTPUT_CHARS") };
let builder = base_agent_builder(agent_base::llm::adapt(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(agent_base::llm::adapt(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(agent_base::llm::adapt(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(agent_base::llm::adapt(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()), "expected followup_task tool");
assert!(tools.contains(&"wait_agent".to_string()), "expected wait_agent tool");
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(agent_base::llm::adapt(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");
}
}