# Advanced Usage
Middleware, sessions, event logging, and more.
## Middleware
Middleware hooks into the agent loop before and after LLM calls:
```rust
use agent_base::{TurnFactMiddleware, TurnToolLimitMiddleware};
let builder = base_agent_builder(llm_client)
.system_prompt(system_prompt)
.middleware(TurnFactMiddleware::new())
.middleware(TurnToolLimitMiddleware::from_config(&safety));
```
Built-in middleware:
- `TurnFactMiddleware` — injects facts/context at the start of each turn
- `TurnToolLimitMiddleware` — enforces `max_tool_calls_per_turn`
### Custom Middleware
Implement the `Middleware` trait to hook into the agent loop at three points:
```rust
use phi_agent::{AgentResult, Middleware, PreLlmCtx, PostLlmCtx, UserMessageCtx};
use async_trait::async_trait;
struct LoggingMiddleware;
#[async_trait]
impl Middleware for LoggingMiddleware {
// 1. Called when user sends a message (before anything else)
async fn on_user_message(&self, ctx: &mut UserMessageCtx) -> AgentResult<()> {
tracing::info!(session = ?ctx.session_id, input = %ctx.user_input, "user message");
Ok(())
}
// 2. Called just before the LLM call (can modify messages or tools)
async fn on_pre_llm(&self, ctx: &mut PreLlmCtx) -> AgentResult<()> {
tracing::info!(session = ?ctx.session_id, msg_count = ctx.messages.len(), "pre-llm");
Ok(())
}
// 3. Called after the LLM responds (can suppress output, inject follow-up)
async fn on_post_llm(&self, ctx: &mut PostLlmCtx) -> AgentResult<()> {
tracing::info!(
session = ?ctx.session_id,
is_tool_call = ctx.is_tool_call,
tool_count = ctx.tool_calls.len(),
"post-llm"
);
Ok(())
}
}
builder = builder.middleware(LoggingMiddleware);
```
Key `PostLlmCtx` fields:
| `full_text` | `String` | LLM's text response (empty if pure tool call) |
| `is_tool_call` | `bool` | Whether the LLM requested tool calls |
| `tool_calls` | `Vec<(id, name, args)>` | Parsed tool call list |
| `available_tools` | `Vec<String>` | Tools currently registered |
| `total_tool_calls` | `usize` | Total tool calls executed this turn so far |
| `skip_push` | `bool` | Set to `true` to suppress the LLM response from the session |
| `follow_up_message` | `Option<String>` | Inject a follow-up User message into the loop |
### Custom ToolPolicy
Implement the `ToolPolicy` trait to control tool execution behavior — approval, pre-execution checks, and post-execution auditing:
```rust
use phi_agent::{AgentResult, ApprovalRequest, RiskLevel, ToolContext, ToolOutput, ToolPolicy};
use async_trait::async_trait;
use serde_json::Value;
struct RiskAwarePolicy;
#[async_trait]
impl ToolPolicy for RiskAwarePolicy {
// 1. Decide whether a tool call needs user approval (async)
async fn evaluate_approval(&self, tool_name: &str, args: &Value) -> Option<ApprovalRequest> {
let command = args.get("command").and_then(Value::as_str).unwrap_or("");
if command.contains("rm ") || command.contains("sudo") {
return Some(ApprovalRequest {
title: "Destructive command".into(),
message: format!("AI wants to run: {command}"),
action_key: Some(format!("cmd:{command}")),
risk_level: RiskLevel::Destructive,
raw: Some(args.clone()),
});
}
None // safe commands run without approval
}
// 2. Sync check just before tool execution — return Err to abort
fn before_call(&self, tool_name: &str, _args: &Value, _ctx: &ToolContext) -> AgentResult<()> {
tracing::info!("about to execute tool: {tool_name}");
Ok(())
}
// 3. Sync hook after successful execution — for auditing or metrics
fn after_call(
&self, tool_name: &str, _args: &Value, result: &ToolOutput, _ctx: &ToolContext,
) -> AgentResult<()> {
tracing::info!(tool = tool_name, summary = %result.summary, "tool executed");
Ok(())
}
}
builder = builder.tool_policy(Arc::new(RiskAwarePolicy));
```
The execution pipeline runs: `evaluate_approval` → (wait for user if needed) → `before_call` → `tool.call()` → `after_call`. If `before_call` returns `Err`, the tool is aborted and `after_call` is skipped.
> 💡 See [`examples/custom-policy.rs`](https://github.com/hibuka-labs/phi-agent/blob/master/examples/custom-policy.rs)
> for a complete runnable demo covering both custom Middleware and ToolPolicy.
> Run with `cargo run --example custom-policy` — no API key required.
## Approval Handlers
Control which tool calls require human confirmation:
```rust
// Auto-approve everything (CI / automation)
use phi_agent::{AutoApprovalHandler, ApprovalMode};
builder = builder.approval_handler(Arc::new(
AutoApprovalHandler::new(ApprovalMode::Auto)
));
// Deny all (read-only / preview mode)
builder = builder.approval_handler(Arc::new(
AutoApprovalHandler::new(ApprovalMode::DenyAll)
));
```
For interactive CLI approval, see `CliApprovalHandler` in the phi binary.
## Session Management
Sessions persist conversation history and tool call results:
```rust
use phi_agent::session::{resolve_session, cleanup_expired_sessions};
// Create or reuse a session
let ctx = resolve_session(Some("my-session"), &base_dir)?;
println!("Session: {} (new: {})", ctx.session_id, ctx.is_new_session);
// Clean up old sessions (> 7 days)
let cleaned = cleanup_expired_sessions(&base_dir, 7)?;
println!("Cleaned {} expired sessions", cleaned);
```
Session directory layout:
```
~/.phi-agent/sessions/<id>/
├── session_id # Session ID marker
├── session.lock # Exclusive file lock
├── session_meta.json # Created at, last active at
└── turn_001.jsonl # Per-turn event log (JSONL)
```
## Event Logging
Every turn is persisted as JSONL for replay and analysis:
```rust
use phi_agent::{save_turn_log, event_to_jsonl};
// Save turn events
save_turn_log(&session_ctx, 1, &events, "user query")?;
// Convert a single event to JSONL
let line = event_to_jsonl(&event);
```
Event types in the log:
- `thought_delta` — LLM thinking content
- `text_delta` — Assistant text output
- `tool_call_started` / `tool_call_finished` — Tool invocations
- `approval_request` — When a tool needs approval
- `plan_updated` — Task plan changes
- `turn_finished` — Turn summary with duration and stats
## System Prompts
phi-agent provides two system prompt variants:
```rust
use phi_agent::{build_system_prompt, build_system_prompt_cn};
// Default (international)
let prompt = build_system_prompt();
// China-aware variant (prefers domestic services, handles GFW)
let prompt_cn = build_system_prompt_cn();
```
You can also pass a fully custom prompt via `builder.system_prompt(...)`.
## Reasoning / Thinking
Control the LLM's chain-of-thought behavior:
```rust
use agent_base::{ReasoningConfig, ReasoningEffort};
// Builder-level default
builder = builder.reasoning(ReasoningConfig {
effort: Some(ReasoningEffort::High),
..Default::default()
});
// Per-turn override
agent.set_reasoning_effort(ReasoningEffort::XHigh).await;
```
Effort levels and when to use them:
- `Low` — simple tasks, fast responses
- `Medium` — default, balanced
- `High` — complex multi-step tasks
- `XHigh` — hardest problems, longest think time
## Programmatic Renderers
Use renderers outside the CLI:
```rust
use phi_agent::{
TerminalRenderer, JsonStreamRenderer, NullRenderer, EventRenderer,
};
use std::io;
// Terminal
let mut renderer = TerminalRenderer::new(true, true, true, Box::new(io::stdout()));
// JSON stream (for IDE integration)
let mut renderer = JsonStreamRenderer::stdout();
// Silent (for web backends)
let mut renderer = NullRenderer;
```
## Error Recovery
phi-agent configures consecutive failure recovery by default:
```rust
use agent_base::ConsecutiveFailureRecovery;
// 3 consecutive failures → stop and explain
builder = builder.error_recovery(Arc::new(
ConsecutiveFailureRecovery::new(3)
));
```
## Further Reading
Runnable examples in the repository:
| [`custom-policy`](https://github.com/hibuka-labs/phi-agent/blob/master/examples/custom-policy.rs) | Custom Middleware + ToolPolicy, event hooks | ❌ |
| [`hello-agent`](https://github.com/hibuka-labs/phi-agent/blob/master/examples/hello-agent.rs) | Minimal agent setup | ✅ |
| [`custom-tool`](https://github.com/hibuka-labs/phi-agent/blob/master/examples/custom-tool.rs) | Implement a custom Tool | ✅ |
| [`multi-tool`](https://github.com/hibuka-labs/phi-agent/blob/master/examples/multi-tool.rs) | Register multiple tools | ✅ |
| [`focus-demo`](https://github.com/hibuka-labs/phi-agent/blob/master/examples/focus-demo.rs) | Focus feature | ✅ |
Run any example with `cargo run --example <name>`, e.g.:
```bash
cargo run --example custom-policy
```