Python SDK
The API lets you create and run agents from code. The core pattern is: configure an AgentConfig, create an Agent, open a Session, and call run().
Install in your project¶
Use a project dependency install (not uv tool install) so import rho_agent works in your code:
Optional extras:
Basic usage¶
import asyncio
from rho_agent import Agent, AgentConfig, Session, SessionStore
config = AgentConfig(
system_prompt="You are a research assistant.",
profile="developer",
model="gpt-5-mini",
working_dir="/tmp/work",
)
agent = Agent(config)
session = Session(agent)
async def main() -> None:
result = await session.run(prompt="Analyze recent failures and summarize root causes.")
print(result.text)
asyncio.run(main())
AgentConfig¶
A dataclass holding all configuration for an agent. Can be constructed directly or loaded from YAML.
@dataclass
class AgentConfig:
system_prompt: str | None = None
vars: dict[str, str] = field(default_factory=dict)
model: str = "gpt-5-mini"
profile: str | PermissionProfile | None = None
backend: str | DaytonaBackend = "local" # "local", "daytona", or DaytonaBackend
working_dir: str | None = None
base_url: str | None = None
service_tier: str | None = None
reasoning_effort: str | None = None
response_format: dict | None = None
auto_approve: bool = True
extras: dict[str, Any] = field(default_factory=dict)
Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
system_prompt |
str \| None |
None |
System prompt text or path to a prompt template file |
vars |
dict[str, str] |
{} |
Variables for prompt template substitution |
model |
str |
"gpt-5-mini" |
Model identifier passed to the LLM client |
profile |
str \| PermissionProfile \| None |
None |
Permission profile name, object, or path to YAML |
backend |
str \| DaytonaBackend |
"local" |
Execution backend: "local", "daytona", or a DaytonaBackend instance |
working_dir |
str \| None |
None |
Working directory for tool execution |
base_url |
str \| None |
None |
Custom API base URL |
service_tier |
str \| None |
None |
API service tier |
reasoning_effort |
str \| None |
None |
Reasoning effort level (model-specific) |
response_format |
dict \| None |
None |
Structured output format specification |
auto_approve |
bool |
True |
Auto-approve tool calls without prompting |
extras |
dict[str, Any] |
{} |
Arbitrary metadata passed through to session |
Loading and saving¶
# Load from a YAML file
config = AgentConfig.from_file("my-agent.yaml")
# Save to a YAML file
config.to_file("my-agent.yaml")
# Resolve system prompt (handles template files and variable substitution)
resolved = config.resolve_system_prompt()
Agent¶
A stateless, reusable agent definition. Holds the resolved config and tool registry. You can create multiple sessions from the same agent.
agent = Agent(config)
# Properties
agent.config # AgentConfig
agent.registry # ToolRegistry (built from profile)
agent.system_prompt # Resolved system prompt string
# Create an LLM client instance
client = agent.create_client()
Customizing tools before creating a session¶
agent = Agent(config)
# Add a custom tool handler before opening a session
agent.registry.register(my_custom_handler)
session = Session(agent)
State¶
The conversation trajectory — messages, token usage, cost, and status. State accumulates across multiple run() calls and can be inspected after the fact or loaded from a trace file without a live Session.
state = session.state
# Check cost and token usage after a run
state.usage["cost_usd"] # Cumulative cost across all runs
state.usage["input_tokens"] # Total input tokens
state.usage["output_tokens"] # Total output tokens
# Inspect what happened
state.status # "running", "completed", "error", "cancelled"
state.run_count # Number of run() calls completed
state.messages # Full message list (OpenAI chat format)
state.get_user_messages() # Extract just the user prompts
Loading a past session's state¶
Load a trace file to inspect a session offline — no live Session needed:
from pathlib import Path
from rho_agent import State
state = State.from_jsonl(Path("~/.config/rho-agent/sessions/abc123/trace.jsonl").expanduser().read_bytes())
print(f"Runs: {state.run_count}, Cost: ${state.usage['cost_usd']:.4f}")
# What tools did the agent call?
for msg in state.messages:
for tc in msg.get("tool_calls") or []:
print(f" {tc['function']['name']}")
Observing events in real time¶
Attach a StateObserver to get notified on every state mutation (message added, tool called, etc.):
class ToolAuditLog:
def on_event(self, event: dict) -> None:
if event.get("tool_calls"):
for tc in event["tool_calls"]:
print(f"[{event['ts']}] {tc['function']['name']}")
session.state.add_observer(ToolAuditLog())
Session¶
The execution context for running prompts. Created from an Agent, it owns the State and drives the agent loop.
Constructor¶
Settable attributes¶
session.approval_callback = my_callback # Called when tool needs approval
session.cancel_check = lambda: False # Polled to check for cancellation
session.auto_compact = True # Auto-compact when context is large
session.context_window = 128_000 # Context window size for compaction
Methods¶
# Run a prompt to completion
result = await session.run(prompt="Analyze the logs.")
# Cancel a running session
await session.cancel()
# Manually trigger context compaction
compact_result = await session.compact()
# Access state
session.state # State object
# Get the Daytona sandbox (see Daytona guide for details)
sandbox = await session.get_sandbox()
Async context manager (Daytona backend)¶
When using the Daytona backend, use Session as an async context manager to ensure the sandbox is cleaned up:
async with Session(Agent(AgentConfig(backend="daytona", profile="developer"))) as session:
result = await session.run(prompt="Check the deployment status.")
print(result.text)
# sandbox deleted automatically
For the local backend, close() is a no-op and the context manager is optional. See the Daytona guide for more.
SessionStore¶
Manages session directories at ~/.config/rho-agent/sessions/. Handles creation, listing, and resumption of sessions.
store = SessionStore() # Uses default path
store = SessionStore("/custom/sessions/dir")
# Create a new session with persistence
session = store.create_session(agent)
# Resume a previous session
session = store.resume(session_id, agent)
# List all sessions
infos: list[SessionInfo] = store.list()
# Get the most recent session ID
latest_id = store.get_latest_id()
SessionInfo¶
@dataclass
class SessionInfo:
id: str
status: str
created_at: datetime
model: str
profile: str
first_prompt: str
RunResult¶
Returned by session.run(). Contains the agent's response and metadata for the run.
@dataclass
class RunResult:
text: str # Final text response
events: list[AgentEvent] # All events from the run
status: str # "completed", "error", "cancelled"
usage: dict[str, int] # Token counts for this run
AgentEvent¶
Events emitted during a run. Each event has a type field and additional fields depending on the type.
@dataclass
class AgentEvent:
type: str
content: str | None = None
tool_name: str | None = None
tool_call_id: str | None = None
tool_args: dict | None = None
tool_result: str | None = None
tool_metadata: dict | None = None
usage: dict | None = None
Event types¶
| Type | Populated fields | Description |
|---|---|---|
text |
content |
Streamed text chunk from the model |
tool_start |
tool_name, tool_call_id, tool_args |
Tool execution begins |
tool_end |
tool_name, tool_call_id, tool_result, tool_metadata |
Tool execution completes |
tool_blocked |
tool_name, tool_call_id, tool_args |
Tool call denied by approval or permissions |
api_call_complete |
usage |
LLM API call finished with token counts |
turn_complete |
content, usage |
Agent turn completed (final response) |
compact_start |
— | Context compaction begins |
compact_end |
content |
Context compaction completes with summary |
error |
content |
Error occurred during execution |
cancelled |
content |
Run was cancelled |
interruption |
content |
Approval required (raises ApprovalInterrupt) |
CompactResult¶
Returned by session.compact():
@dataclass
class CompactResult:
summary: str
tokens_before: int
tokens_after: int
trigger: str # "auto" or "manual"
Patterns¶
Multi-run conversation¶
Run multiple prompts in a single session, building on prior context:
agent = Agent(AgentConfig(system_prompt="You are a database analyst.", profile="readonly"))
session = Session(agent)
r1 = await session.run(prompt="List all tables in the database.")
print(r1.text)
r2 = await session.run(prompt="Show me the schema for the users table.")
print(r2.text)
r3 = await session.run(prompt="Find users who signed up in the last 7 days.")
print(r3.text)
Parallel dispatch¶
Run multiple agents concurrently using asyncio.create_task and gather:
import asyncio
from rho_agent import Agent, AgentConfig, Session
async def analyze(task_prompt: str, work_dir: str) -> str:
config = AgentConfig(
system_prompt="You are an analyst.",
profile="readonly",
working_dir=work_dir,
)
session = Session(Agent(config))
result = await session.run(prompt=task_prompt)
return result.text
async def main() -> None:
tasks = [
asyncio.create_task(analyze("Check error rates.", "/var/log/app1")),
asyncio.create_task(analyze("Check latency metrics.", "/var/log/app2")),
asyncio.create_task(analyze("Check disk usage.", "/var/log/app3")),
]
results = await asyncio.gather(*tasks)
for text in results:
print(text)
asyncio.run(main())
Custom tools¶
Register custom tool handlers before creating a session:
from rho_agent import Agent, AgentConfig, Session
agent = Agent(AgentConfig(profile="developer"))
agent.registry.register(my_custom_handler)
session = Session(agent)
result = await session.run(prompt="Use my custom tool to process the data.")
Save and resume via SessionStore¶
from rho_agent import Agent, AgentConfig, SessionStore
store = SessionStore()
agent = Agent(AgentConfig(system_prompt="You are a research assistant.", profile="developer"))
# First session
session = store.create_session(agent)
await session.run(prompt="Start analyzing the codebase.")
session_id = session.state.session_id
# Later: resume
session = store.resume(session_id, agent)
await session.run(prompt="Continue where we left off.")
Daytona backend¶
See the Daytona guide for full CLI and Python API usage, including file uploads, setup commands, sandbox configuration, env_vars, Image objects, and DaytonaBackend.