phi-core
A Rust library for building stateful, multi-turn AI coding agents. Provides a unified abstraction over 20+ LLM providers, a robust agent loop with tool execution, automatic context management, and real-time event streaming.
Features
- Multi-provider support — Anthropic (Claude), OpenAI, Gemini, Azure OpenAI, AWS Bedrock, Vertex AI, and 15+ OpenAI-compatible backends (Groq, Together, DeepSeek, Mistral, Fireworks, xAI, etc.)
- Stateful agent loop — Multi-turn conversation with automatic tool call execution, steering injection, and follow-up queuing
- Built-in tools — Bash execution, file read/write/edit, directory listing, and code search
- Real-time event streaming — Token-level streaming via async channels
- Context management — Tiered compaction strategy to handle large conversations without hitting token limits
- MCP integration — Connect to any Model Context Protocol server via stdio or HTTP
- OpenAPI integration — Auto-generate tools from any OpenAPI 3.0 spec
- Sub-agents — Delegate tasks to isolated child agent instances
- Skills system — Load prompt skills from the AgentSkills standard
- Retry logic — Exponential backoff with jitter for rate limits and network errors
- Prompt caching — Anthropic prompt cache support
Installation
Add to your Cargo.toml:
[]
= "0.6"
= { = "1", = ["full"] }
To enable OpenAPI tool generation:
[]
= { = "0.6", = ["openapi"] }
Minimum Supported Rust Version: 1.75
Quick Start
Basic prompt
use BasicAgent;
use ModelConfig;
use ;
async
With built-in tools
use ;
use ModelConfig;
let api_key = var.unwrap;
let mut agent = new
.with_system_prompt
.with_tools;
let mut rx = agent.prompt.await;
Custom tool
use ;
use ModelConfig;
use async_trait;
use ;
;
let api_key = var.unwrap;
let mut agent = new
.with_tools;
Architecture
phi-core is structured in two layers:
┌─────────────────────────────────────────────────────────┐
│ Agent (agent.rs) — stateful wrapper │
│ Manages: history, tools, provider, steering queue │
├─────────────────────────────────────────────────────────┤
│ agent_loop / agent_loop_continue — stateless functions │
│ Core: stream → tool calls → execute → repeat │
└─────────────────────────────────────────────────────────┘
The agent loop is the heartbeat:
- Send user messages to LLM via
StreamProvider - Stream response tokens in real time, emitting
AgentEvents - Extract tool calls from the completed response
- Execute tools (parallel by default), collect results
- Append tool results to conversation history
- Repeat until
StopReason::Stopwith no pending follow-ups
Provider System
All providers are selected by ModelConfig.api: ApiProtocol and resolved automatically via
ProviderRegistry. You never name a provider struct directly — just pass a ModelConfig:
ApiProtocol variant |
Wire format | Factory method |
|---|---|---|
AnthropicMessages |
Anthropic Messages API | ModelConfig::anthropic(id, name, key) |
OpenAiCompletions |
OpenAI Chat Completions (15+ backends) | ModelConfig::openai(id, name, key) / ModelConfig::local(url, id, key) / ModelConfig::openrouter(id, key) |
OpenAiResponses |
OpenAI Responses API | Direct struct construction |
AzureOpenAiResponses |
Azure OpenAI | Direct struct construction |
GoogleGenerativeAi |
Gemini | ModelConfig::google(id, name, key) |
GoogleVertex |
Vertex AI | Direct struct construction |
BedrockConverseStream |
AWS Bedrock | Direct struct construction |
OpenAiCompat flags handle the 15+ OpenAI-compatible provider quirks (auth style, reasoning
format, max_tokens field name, etc.) without needing a separate provider per service.
Key Types
| Type | Description |
|---|---|
Content |
Atomic message unit: Text, Image, Thinking, ToolCall |
Message |
LLM conversation turn: User, Assistant, ToolResult |
AgentMessage |
Routing envelope: Llm(LlmMessage) or Extension(...) (app-only, never sent to LLM). LlmMessage wraps Message + optional TurnId for turn tracking |
AgentEvent |
Real-time event stream emitted to callers |
StreamDelta |
Token-level streaming updates: Text, Thinking, ToolCallDelta |
StopReason |
Why the LLM stopped: Stop, ToolUse, Length, Error, Aborted, MaxTurns, etc. |
AgentContext |
Loop execution state: history, tools, system prompt |
Agent API
Construction
use BasicAgent;
use ModelConfig;
let api_key = var.unwrap;
let mut agent = new
.with_system_prompt
.with_tools
.with_thinking
.with_max_tokens
.with_context_config
.with_execution_limits
.with_retry_config
.with_tool_execution
.with_cache_config;
// Temperature is a public field (no builder method):
agent.temperature = Some;
Conversation methods
// Start a new prompt
let mut rx = agent.prompt.await;
// Provide a caller-owned sender (for concurrent use)
agent.prompt_with_sender.await;
// Build messages manually
agent.prompt_messages.await;
// Inject a message mid-run (processed before next LLM turn)
agent.steer.await;
// Queue a message to send after the agent stops
agent.follow_up.await;
// Abort a running loop
agent.abort;
// Reset conversation history
agent.reset;
// Persist and restore conversation state
let json = agent.save_messages;
agent.restore_messages?;
Integrations
// MCP servers
agent.with_mcp_server_stdio
agent.with_mcp_server_http
// OpenAPI tools (requires `openapi` feature)
agent.with_openapi_file
agent.with_openapi_url
// Skills
let skills = load;
agent.with_skills
Event Streaming
Consume events from the returned receiver:
let mut rx = agent.prompt.await;
while let Some = rx.recv.await
Full event lifecycle
AgentStart
└─ TurnStart
├─ MessageStart
│ └─ MessageUpdate (repeated per token)
└─ MessageEnd
└─ ToolExecutionStart (per tool)
└─ ToolExecutionUpdate (progress, optional)
└─ ToolExecutionEnd (per tool)
└─ TurnEnd
AgentEnd
Built-in Tools
All six built-in tools are returned by default_tools():
| Tool | Description |
|---|---|
BashTool |
Execute shell commands with timeout and output capture |
ReadFileTool |
Read text or image files, with optional line range |
WriteFileTool |
Create or overwrite files, creating parent directories as needed |
EditFileTool |
Surgical search-and-replace edits |
ListFilesTool |
List directory contents with glob filtering |
SearchTool |
Grep/ripgrep-based code search |
Context Management
phi-core automatically manages the context window to prevent token limit errors. Configuration:
ContextConfig
When the budget is approached, compaction runs in tiers:
- Level 1 — Truncate long tool outputs
- Level 2 — Summarize old conversation turns
- Level 3 — Drop middle turns entirely
The modern system uses non-destructive CompactionBlock overlays — see docs/concepts/compaction.md for the current design.
Execution limits
ExecutionLimits
Tool Execution Strategies
Control how concurrent tool calls are handled:
// All tools run concurrently (default)
.with_tool_execution
// One tool at a time, checks steering queue between each
.with_tool_execution
// Concurrent within batches, steering check between batches
.with_tool_execution
Low-level API
For advanced use cases, use the stateless free functions directly:
use ;
use ModelConfig;
use ;
let api_key = var.unwrap;
let config = AgentLoopConfig ;
let mut context = AgentContext ;
let = unbounded_channel;
let cancel = new;
let new_messages = agent_loop.await;
Providers
ModelConfig is the single descriptor for every provider connection — it bundles the model ID,
API key, base URL, and any per-provider quirk flags. Pass it to BasicAgent::new() or
SubAgentTool::new().
Anthropic
use BasicAgent;
use ModelConfig;
let api_key = var.unwrap;
new
// Enable extended thinking
.with_thinking
// Enable prompt caching
.with_cache_config
OpenAI
use ModelConfig;
let api_key = var.unwrap;
new
OpenAI-compatible (Groq, Together, DeepSeek, etc.)
use ;
// Groq — pass the base URL via ModelConfig::local()
let api_key = var.unwrap;
new
// OpenRouter — dedicated factory with correct compat flags
let or_key = var.unwrap;
new
Google Gemini
use ModelConfig;
let api_key = var.unwrap;
new
AWS Bedrock
use ;
// Bedrock uses "access_key:secret[:session_token]" as api_key, or "" for IAM roles
let creds = var.unwrap_or_default;
new
Azure OpenAI
use ;
let api_key = var.unwrap;
new
MCP Integration
Connect to any Model Context Protocol server:
use BasicAgent;
use ModelConfig;
let api_key = var.unwrap;
let model_config = anthropic;
// stdio (local process)
let mut agent = new
.with_mcp_server_stdio
.await?;
// HTTP (remote server)
let mut agent = new
.with_mcp_server_http
.await?;
MCP tools are exposed transparently as AgentTool instances — the agent loop treats them identically to built-in tools.
OpenAPI Integration
Auto-generate tools from any OpenAPI 3.0 spec (requires openapi feature):
use BasicAgent;
use ModelConfig;
use ;
let api_key = var.unwrap;
let mut agent = new
.with_openapi_file
.await?;
// Filter to specific operations
.with_openapi_url
Sub-agents
Delegate tasks to isolated child agent instances:
use BasicAgent;
use SubAgentTool;
use ModelConfig;
use default_tools;
let api_key = var.unwrap;
let researcher = new
.with_description
.with_tools;
let mut agent = new
.with_sub_agent;
Sub-agents get their own isolated conversation context and cannot themselves spawn further sub-agents (depth limiting is enforced automatically).
Skills
Load skills from the AgentSkills standard — SKILL.md files with YAML frontmatter:
name: code-review
description: Perform a thorough code review
Review the provided code for correctness, performance, security, and style...
use ;
use ModelConfig;
let api_key = var.unwrap;
let skills = load;
let mut agent = new
.with_skills;
// Skills are injected as an <available_skills> block in the system prompt
Conversation Persistence
Save and restore conversation state across sessions:
// Save
let json = agent.save_messages;
write?;
// Restore
let json = read_to_string?;
agent.restore_messages?;
Callbacks
Hook into the agent loop with before/after turn callbacks via the builder API:
use BasicAgent;
use ModelConfig;
let api_key = var.unwrap;
let agent = new
.on_before_turn
.on_after_turn;
For the low-level API, callbacks live on AgentLoopConfig:
use AgentLoopConfig;
use Arc;
let config = AgentLoopConfig ;
Development
Build and test
Linting and formatting
CI runs with RUSTFLAGS="-Dwarnings" — all clippy warnings are treated as errors.
Integration tests
Integration tests in tests/integration_anthropic.rs require a live ANTHROPIC_API_KEY and are skipped by default. To run them:
ANTHROPIC_API_KEY=sk-ant-...
Examples
| Example | Description |
|---|---|
basic.rs |
Minimal text prompt with Anthropic |
cli.rs |
Full interactive multi-turn REPL with tools and streaming |
callbacks.rs |
Demonstrates before_turn / after_turn hooks |
persistence.rs |
Save and restore conversation history |
sub_agent.rs |
Task delegation with SubAgentTool |
License
MIT — see LICENSE for details.