Skip to main content

phi_agent/
lib.rs

1//! phi-agent: Rust AI Agent runtime framework — orchestration, sessions, streaming all built-in.
2//! You only define tools, prompts, and domain knowledge.
3//!
4//! Built on agent-base and agent-works, providing builder factory, renderer,
5//! config resolution, session management, and other infrastructure.
6//! **Ships with zero application tools.** Kernel tools (file I/O, shell,
7//! multi-agent) are available via `phi-kernel-tools` behind feature flags —
8//! all off by default. Application tools are injected by consumers.
9
10#![warn(missing_docs)]
11
12pub mod agent;
13pub mod bridge;
14pub mod cli;
15pub mod config;
16pub mod event_log;
17/// System prompt generation (EN/CN).
18pub mod prompt;
19pub mod render;
20/// Session management — ID resolution, locking, snapshots, cleanup.
21pub mod session;
22
23// ── Common agent-base types ──
24// Only re-export the types consumers use most often.
25// For the full type set, import directly from agent-base.
26//
27// Note: AgentBuilder re-exports agent_works::AgentBuilder (not agent_base::AgentBuilder)
28// because phi-agent is a full-stack framework that includes multi-agent, skills, MCP, etc.
29// For the bare runtime builder, use agent_base::AgentBuilder directly.
30pub use agent_base::{
31    AgentError, AgentResult, AgentRuntime, AllowAllApprovalHandler, ApprovalDecision, ApprovalHandler, ApprovalRequest,
32    ChatMessage, CheckpointData, CheckpointStep, CompactionKind, CompactionOutcome, ConsecutiveFailureRecovery,
33    Content, ContextCompaction, ContextWindowManager, DenyAllApprovalHandler, FinishReason, Language, Middleware,
34    PlanItem, PlanStepStatus, PostLlmCtx, PreLlmCtx, ReasoningConfig, ReasoningEffort, RetryOnError, RiskLevel,
35    RunOutcome, RuntimeEvent, SafetyConfig, SessionId, Tool, ToolContext, ToolDecision, ToolMetadata, ToolPolicy,
36    ToolRegistry, TurnFactMiddleware, TurnToolLimitMiddleware, UpdatePlanTool, UserMessageCtx,
37    estimate_messages_tokens, first_system_prompt,
38};
39// Token-budget window strategy — a pure strategy in agent-works (agent-base
40// stays strategy-free: contract + primitives only).
41pub use agent_works::AgentBuilder;
42pub use agent_works::rotation_policy::{
43    DEFAULT_SEED_MESSAGE, TokenBudgetAction, TokenBudgetConfig, TokenBudgetCore, TokenBudgetState,
44    build_context_window_info, token_budget_base_overhead,
45};
46
47// ── phi-telemetry (metrics types and storage) ──
48#[cfg(feature = "telemetry")]
49pub use phi_telemetry::{
50    SessionMetrics, SessionOutcome, SessionSummary, TurnMetrics, TurnOutcome, list_all_metrics, load_metrics,
51    save_metrics, try_load_metrics,
52};
53
54// ── agent-works ──
55#[cfg(feature = "focus")]
56pub use agent_works::focus::{Context as FocusContext, Focus, FocusError, FocusInput, FocusOutput};
57#[cfg(feature = "multi-agent")]
58pub use agent_works::multi_agent::{
59    CapabilityResolution, ChildPermissionMode, ChildReport, ChildResultEvent, ChildToolCapability, ControlConfig,
60    MultiAgentConfig, resolve_capability,
61};
62// Child-result fan-in delivery policy — data-only routes; the consumer renders
63// the words. Sunk down from phimint's UI so any multi-agent UI reuses it.
64#[cfg(feature = "multi-agent")]
65pub use agent_works::multi_agent::fan_in::{ChildResultRoute, ChildResultRouter};
66#[cfg(feature = "multi-agent")]
67pub use agent_works::multi_agent::registry::{AgentSnapshot, RegistrySnapshot};
68
69// ── Persistent auto-memory (Phase 9b, feature-gated) ──
70/// Memory configuration for persistent auto-memory: consumer-injected policy
71/// (storage root, index filename, prompt template). Hand it to
72/// `AgentBuilder::memory_config` to register the four `memory_*` tools and
73/// append the MEMORY.md index snapshot + tool guidance to the system prompt.
74#[cfg(feature = "memory")]
75pub use agent_works::memory::MemoryConfig;
76
77// ── Framework pass-through (facade completion) ──
78// These are the remaining types a product needs to name directly, so its
79// Cargo.toml can depend on `phi-agent` alone. Only actually-consumed items
80// are re-exported — no catch-all facade.
81/// Plan / user lifecycle event types surfaced to consumers.
82pub use agent_base::UserEvent;
83/// Middleware that nudges the model when it nears the turn limit.
84pub use agent_base::engine::max_turns_nudge::{MaxTurnsNudgeConfig, MaxTurnsNudgeMiddleware};
85/// Middleware that breaks repeated identical tool-call loops (e.g. polling).
86pub use agent_base::engine::repeat_tool_limit::{RepeatToolLimitConfig, RepeatToolLimitMiddleware};
87/// LLM provider trait family (`LlmProvider`, `Protocol`, `config::LlmConfig`):
88/// build a provider with [`create_provider`] and hand it to the runtime.
89pub use agent_base::llm_trait;
90/// Notice kinds for `UserEvent::Notice` — consumers match on these to decide
91/// rendering (e.g. Warning → persistent red line).
92pub use agent_base::types::NoticeKind;
93/// Guard policies from agent-works (tool gating, reasoning-only enforcement).
94pub use agent_works::guard::{DefaultGuard, DefaultGuardConfig, ReasoningOnlyAction};
95/// LLM provider factory (resolves protocol/client from an [`llm_trait`] config).
96pub use llm_unified::create_provider;
97
98// ── Kernel tools (feature-gated) ──
99#[cfg(feature = "shell")]
100pub use phi_kernel_tools::local_shell::LocalShellTool;
101
102// ── Skills (feature-gated) ──
103#[cfg(feature = "skill")]
104pub use agent_works::skill::{
105    MAX_CATALOG_SKILLS, Skill, SkillCatalogRefreshMiddleware, SkillResolver, SkillTelemetry, SkillTool,
106    catalog::demote_h2_headings, prompt_skill::PromptSkill, prompt_skill::SkillScope, refresh_catalog, render_catalog,
107    strip_catalog,
108};
109
110// ── MCP (feature-gated) ──
111#[cfg(feature = "mcp")]
112pub use agent_works::mcp::{McpServeConfig, McpServer, McpServerConfig, McpServerTransport, McpTransport};
113
114// ── phi-agent types ──
115#[cfg(feature = "compression")]
116pub use agent::CompressionMiddleware;
117pub use agent::{
118    PhiAgent, PhiAgentConfig, base_agent_builder, base_agent_builder_no_compression, base_agent_builder_with_excludes,
119    base_agent_builder_with_options, clear_compression_cache, run_compact_session,
120};
121pub use agent_works::prompt::{
122    DynamicToolsFragment, EnvironmentFragment, FragmentContext, PromptFragment, compose_fragments,
123};
124pub use cli::{ApprovalItem, ApprovalMode, AutoApprovalHandler, QueuedApprovalHandler};
125pub use config::{LlmConfig, resolve_llm_config};
126pub use event_log::{event_to_jsonl, event_to_value, save_turn_log};
127pub use prompt::{build_system_prompt, build_system_prompt_cn, build_system_prompt_with_fragments};
128pub use render::{
129    EventRenderer, JsonStreamRenderer, NullRenderer, OutputFormat, create_renderer, create_stdout_renderer,
130};
131pub use session::{
132    SessionContext, SessionInfo, SnapshotInfo, cleanup_expired_sessions, clear_messages_jsonl, create_snapshot,
133    delete_snapshot, list_sessions, list_snapshots, load_session_messages, persist_window_messages, read_session_title,
134    resolve_session, restore_snapshot, validate_session_id, validate_snapshot_name, write_session_title,
135};
136
137/// Format a number with K/M suffixes for display.
138///
139/// - `n < 1_000` → plain number
140/// - `1_000 <= n < 1_000_000` → `{:.1}K`
141/// - `n >= 1_000_000` → `{:.1}M`
142pub fn format_number(n: u64) -> String {
143    if n >= 1_000_000 {
144        format!("{:.1}M", n as f64 / 1_000_000.0)
145    } else if n >= 1_000 {
146        format!("{:.1}K", n as f64 / 1_000.0)
147    } else {
148        n.to_string()
149    }
150}
151
152#[cfg(test)]
153mod proptests {
154    use super::*;
155
156    proptest::proptest! {
157        #[test]
158        fn format_number_never_panics(n: u64) {
159            let _ = format_number(n);
160        }
161
162        #[test]
163        fn format_number_correct_suffix(n: u64) {
164            let s = format_number(n);
165            if n < 1_000 {
166                proptest::prop_assert_eq!(s, n.to_string());
167            } else if n < 1_000_000 {
168                proptest::prop_assert!(s.ends_with('K'), "expected K suffix for {}, got '{}'", n, s);
169            } else {
170                proptest::prop_assert!(s.ends_with('M'), "expected M suffix for {}, got '{}'", n, s);
171            }
172        }
173
174        #[test]
175        fn format_number_boundary_correct(n in 0u64..1_001) {
176            // At the exact boundary (1000), must switch to K
177            let s = format_number(n);
178            if n < 1_000 {
179                proptest::prop_assert!(!s.contains('K'));
180                proptest::prop_assert!(!s.contains('M'));
181            } else {
182                proptest::prop_assert!(s.ends_with('K'));
183            }
184        }
185    }
186
187    #[test]
188    fn format_number_renders_exact_boundaries() {
189        // Concrete boundary values pinned by the former metrics.rs tests: the
190        // rounding rule (999_999 -> "1000.0K") is intentional and locked in.
191        assert_eq!(format_number(0), "0");
192        assert_eq!(format_number(42), "42");
193        assert_eq!(format_number(999), "999");
194        assert_eq!(format_number(1_000), "1.0K");
195        assert_eq!(format_number(1_500), "1.5K");
196        assert_eq!(format_number(999_999), "1000.0K");
197        assert_eq!(format_number(1_000_000), "1.0M");
198        assert_eq!(format_number(2_500_000), "2.5M");
199    }
200}