mod approval;
mod args;
mod init;
mod metrics;
mod run;
mod serve;
mod tools;
use std::sync::Arc;
use anyhow::Result;
use approval::CliApprovalHandler;
use args::{CliArgs, OutputFormatArg, SubCommand};
use clap::Parser;
use phi_agent::config::resolve_llm_config;
use phi_agent::render::OutputFormat;
use phi_agent::{ApprovalMode, AutoApprovalHandler};
use phi_agent::{
OpenAiClient, PhiAgent, SafetyConfig, TurnFactMiddleware, TurnToolLimitMiddleware, base_agent_builder,
build_system_prompt,
};
use run::{default_node_id, init_logging, run_one_shot, run_repl};
use tools::LocalShellTool;
#[tokio::main]
async fn main() -> Result<()> {
dotenvy::dotenv().ok();
let args = CliArgs::parse();
if let Some(cmd) = &args.command {
match cmd {
SubCommand::Init { name, lib } => return init::run(name, *lib),
SubCommand::Metrics { cmd } => return metrics::handle_metrics(cmd, &args),
SubCommand::Serve => return serve::run().await,
}
}
let log_dir = args.log_dir.replace("~", &std::env::var("HOME").unwrap_or_default());
let log_dir_path = std::path::PathBuf::from(&log_dir);
if !args.no_log {
match phi_agent::session::cleanup_expired_sessions(&log_dir_path, 7) {
Ok(count) => {
if count > 0 {
eprintln!("[phi] cleaned up {} expired session(s)", count);
}
},
Err(e) => {
eprintln!("[phi] warning: failed to cleanup sessions: {}", e);
},
}
}
let session_ctx = phi_agent::session::resolve_session(args.session_id.as_deref(), &log_dir_path)?;
let session_id_str = session_ctx.session_id.clone();
let is_new_session = session_ctx.is_new_session;
if !args.no_log {
init_logging(&session_ctx, &args.log_level).await?;
}
tracing::info!(
session_id = %session_id_str,
is_new = is_new_session,
format = ?args.format,
"phi starting"
);
let llm_config = resolve_llm_config(args.model.as_deref(), args.base_url.as_deref())?;
let llm_client = Arc::new(OpenAiClient::new(
llm_config.api_key.clone(),
llm_config.model.clone(),
Some(llm_config.base_url.clone()),
));
let system_prompt = build_system_prompt();
let approval_handler: Arc<dyn phi_agent::ApprovalHandler> = if args.auto_approve {
Arc::new(AutoApprovalHandler::new(ApprovalMode::Auto))
} else {
Arc::new(CliApprovalHandler::new())
};
let safety_config = SafetyConfig {
max_tool_calls_per_turn: args.max_tool_calls.unwrap_or(64),
max_consecutive_failures: args.max_failures.unwrap_or(3),
};
let output_format = match args.format {
OutputFormatArg::Terminal => OutputFormat::Terminal {
show_thinking: !args.no_thinking,
show_tool_args: !args.no_tool_args,
color: !args.no_color,
},
OutputFormatArg::Json => OutputFormat::Json,
OutputFormatArg::Quiet => OutputFormat::Quiet,
};
let agent_config = phi_agent::PhiAgentConfig {
model: llm_config.model.clone(),
enable_thinking: !args.no_thinking,
thinking_budget: args.thinking_budget,
thinking_effort: args.thinking_effort.clone().into(),
safety: safety_config.clone(),
max_turns: args.max_turns,
};
let builder = base_agent_builder(llm_client)
.system_prompt(system_prompt)
.register_tool(LocalShellTool::new(args.shell_timeout_ms))
.register_tool(
phi_agent::UpdatePlanTool::new().with_description(
"Create or update a task plan to show the user a checklist with progress. \
This is a presentation protocol.\n\n\
[When to Use]\n\
- Complex tasks (usually 3+ steps): call update_plan first to show the plan, \
then execute step by step.\n\
- Simple tasks, Q&A, one-shot operations: do NOT call — handle directly.\n\n\
[Requirements]\n\
- Must provide an objective when creating a plan for the first time.\n\
- plan is a full snapshot, not an incremental patch.\n\
- At most one step can be in_progress at a time.\n\
- Step text should be human-readable task descriptions only.\n\n\
[Update Conventions]\n\
- Update status promptly as you progress: pending → in_progress → completed.\n\
- If blocked, explain the reason honestly in the explanation field."
.to_string(),
),
)
.approval_handler(approval_handler)
.middleware(TurnFactMiddleware::new())
.middleware(TurnToolLimitMiddleware::from_config(&safety_config))
.apply_if(args.thinking_budget, |b, budget| b.thinking_budget(budget))
.apply_if(args.max_turns, |b, n| b.execution_max_turns(n));
let agent = PhiAgent::build(builder, agent_config)?;
let agent_session_id = agent.create_session().await;
tracing::info!(
agent_session_id = %agent_session_id.id,
session_id = %session_id_str,
model = %llm_config.model,
"session created"
);
if let Some(query) = args.query {
let node_id = std::env::var("PHI_NODE_ID").unwrap_or_else(|_| default_node_id());
let metrics_enabled = std::env::var("PHI_METRICS_ENABLED")
.map(|v| {
let v = v.to_lowercase();
!matches!(v.as_str(), "false" | "0" | "no" | "off" | "")
})
.unwrap_or(true);
let mut telemetry = if metrics_enabled {
Some(phi_telemetry::init_telemetry(
agent.runtime(),
session_id_str.clone(),
node_id,
llm_config.model.clone(),
))
} else {
None
};
let (result, run_outcome) = run_one_shot(&agent, &agent_session_id, &session_ctx, &query, &output_format).await;
if let Some(handle) = &mut telemetry {
handle.shutdown().await;
let session = handle.session.read().await;
let mut session = session.clone();
session.finalize(phi_telemetry::types::run_outcome_to_session_outcome(&run_outcome));
let _ = phi_telemetry::save_metrics(&session, &session_ctx.session_dir);
}
result?;
Ok(())
} else {
run_repl(&agent, &agent_session_id, &session_ctx, &output_format).await
}
}