Persistence
A ready-to-run example is available here.
How to use persistence
Save conversation state to disk and restore it later for long-running or multi-session workflows.
Saving state
Create a conversation with a unique ID to enable persistence:
import uuid
conversation_id = uuid.uuid4()
persistence_dir = "./.conversations"
conversation = Conversation(
agent=agent,
callbacks=[conversation_callback],
workspace=cwd,
persistence_dir=persistence_dir,
conversation_id=conversation_id,
)
conversation.send_message("Start long task")
conversation.run() # State automatically saved
Restoring state
Restore a conversation using the same ID and persistence directory:
# Later, in a different session
del conversation
# Deserialize the conversation
print("Deserializing conversation...")
conversation = Conversation(
agent=agent,
callbacks=[conversation_callback],
workspace=cwd,
persistence_dir=persistence_dir,
conversation_id=conversation_id,
)
conversation.send_message("Continue task")
conversation.run() # Continues from saved state
What gets persisted
The conversation state includes information that allows seamless restoration:
- Message History: Complete event log including user messages, agent responses, and system events
- Agent Configuration: LLM settings, tools, MCP servers, and agent parameters
- Execution State: Current agent status (idle, running, paused, etc.), iteration count, and stuck detection settings
- Tool Outputs: Results from bash commands, file operations, and other tool executions
- Statistics: LLM usage metrics like token counts and API calls
- Workspace Context: Working directory and file system state
- Activated Skills: Skills that have been enabled during the conversation
- Secrets: Managed credentials and API keys
- Agent State: Custom runtime state stored by agents (see Agent State below)
Persistence directory structure
When you set a persistence_dir, your conversation will be persisted to a directory structure where each
conversation has its own subdirectory. By default, the persistence directory is workspace/conversations/
(unless you specify a custom path).
Directory structure:
- workspace/conversations
- `<conversation-id-1>`
- base_state.json
- events
- event-00000-`<event-id>`.json
- event-00001-`<event-id>`.json
- ...
- `<conversation-id-2>`
- base_state.json
- events
- ...
- ...
- `<conversation-id-1>`
Each conversation directory contains:
base_state.json: The core conversation state including agent configuration, execution status, statistics, and metadataevents/: A subdirectory containing individual event files, each named with a sequential index and event ID (e.g.,event-00000-abc123.json)
The collection of event files in the events/ directory represents the same trajectory data you would find in the trajectory.json file from Faheem Code V0, but split into individual files for better performance and granular access.
Ready-to-run example
import os
import uuid
from pydantic import SecretStr
from faheemcode.sdk import (
LLM,
Agent,
Conversation,
Event,
LLMConvertibleEvent,
get_logger,
)
from faheemcode.sdk.tool import Tool
from faheemcode.tools.file_editor import FileEditorTool
from faheemcode.tools.terminal import TerminalTool
logger = get_logger(__name__)
# Configure LLM
api_key = os.getenv("LLM_API_KEY")
assert api_key is not None, "LLM_API_KEY environment variable is not set."
model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")
base_url = os.getenv("LLM_BASE_URL")
llm = LLM(
usage_id="agent",
model=model,
base_url=base_url,
api_key=SecretStr(api_key),
)
# Tools
cwd = os.getcwd()
tools = [
Tool(name=TerminalTool.name),
Tool(name=FileEditorTool.name),
]
# Add MCP Tools
mcp_config = {
"mcpServers": {
"fetch": {"command": "uvx", "args": ["mcp-server-fetch"]},
}
}
# Agent
agent = Agent(llm=llm, tools=tools, mcp_config=mcp_config)
llm_messages = [] # collect raw LLM messages
def conversation_callback(event: Event):
if isinstance(event, LLMConvertibleEvent):
llm_messages.append(event.to_llm_message())
conversation_id = uuid.uuid4()
persistence_dir = "./.conversations"
conversation = Conversation(
agent=agent,
callbacks=[conversation_callback],
workspace=cwd,
persistence_dir=persistence_dir,
conversation_id=conversation_id,
)
conversation.send_message(
"Read https://github.com/SMART-National-Solution/faheem-code-app. Then write 3 facts "
"about the project into FACTS.txt."
)
conversation.run()
conversation.send_message("Great! Now delete that file.")
conversation.run()
print("=" * 100)
print("Conversation finished. Got the following LLM messages:")
for i, message in enumerate(llm_messages):
print(f"Message {i}: {str(message)[:200]}")
# Conversation persistence
print("Serializing conversation...")
del conversation
# Deserialize the conversation
print("Deserializing conversation...")
conversation = Conversation(
agent=agent,
callbacks=[conversation_callback],
workspace=cwd,
persistence_dir=persistence_dir,
conversation_id=conversation_id,
)
print("Sending message to deserialized conversation...")
conversation.send_message("Hey what did you create? Return an agent finish action")
conversation.run()
# Report cost
cost = llm.metrics.accumulated_cost
print(f"EXAMPLE_COST: {cost}")
You can run the example code as-is.
export LLM_API_KEY="your-api-key"
export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929" # or openai/gpt-4o, etc.
cd software-agent-sdk
uv run python examples/01_standalone_sdk/10_persistence.py
# https://app.faheemcode.ai/settings/api-keys
export LLM_API_KEY="example-user-api-key"
export LLM_MODEL="faheemcode/claude-sonnet-4-5-20250929"
cd software-agent-sdk
uv run python examples/01_standalone_sdk/10_persistence.py
Reading serialized events
Convert persisted events into LLM-ready messages for reuse or analysis.
"""Load persisted events and convert them into LLM-ready messages."""
import json
import os
import uuid
from pathlib import Path
from pydantic import SecretStr
conversation_id = uuid.uuid4()
persistence_root = Path(".conversations")
log_dir = (
persistence_root / "logs" / "event-json-to-openai-messages" / conversation_id.hex
)
os.environ.setdefault("LOG_JSON", "true")
os.environ.setdefault("LOG_TO_FILE", "true")
os.environ.setdefault("LOG_DIR", str(log_dir))
os.environ.setdefault("LOG_LEVEL", "INFO")
from faheemcode.sdk import ( # noqa: E402
LLM,
Agent,
Conversation,
Event,
LLMConvertibleEvent,
Tool,
)
from faheemcode.sdk.logger import get_logger, setup_logging # noqa: E402
from faheemcode.tools.terminal import TerminalTool # noqa: E402
setup_logging(log_to_file=True, log_dir=str(log_dir))
logger = get_logger(__name__)
api_key = os.getenv("LLM_API_KEY")
if not api_key:
raise RuntimeError("LLM_API_KEY environment variable is not set.")
llm = LLM(
usage_id="agent",
model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"),
base_url=os.getenv("LLM_BASE_URL"),
api_key=SecretStr(api_key),
)
agent = Agent(
llm=llm,
tools=[Tool(name=TerminalTool.name)],
)
######
# Create a conversation that persists its events
######
conversation = Conversation(
agent=agent,
workspace=os.getcwd(),
persistence_dir=str(persistence_root),
conversation_id=conversation_id,
)
conversation.send_message(
"Use the terminal tool to run `pwd` and write the output to tool_output.txt. "
"Reply with a short confirmation once done."
)
conversation.run()
conversation.send_message(
"Without using any tools, summarize in one sentence what you did."
)
conversation.run()
assert conversation.state.persistence_dir is not None
persistence_dir = Path(conversation.state.persistence_dir)
event_dir = persistence_dir / "events"
event_paths = sorted(event_dir.glob("event-*.json"))
if not event_paths:
raise RuntimeError("No event files found. Was persistence enabled?")
######
# Read from serialized events
######
events = [Event.model_validate_json(path.read_text()) for path in event_paths]
convertible_events = [
event for event in events if isinstance(event, LLMConvertibleEvent)
]
llm_messages = LLMConvertibleEvent.events_to_messages(convertible_events)
if llm.uses_responses_api():
logger.info("Formatting messages for the OpenAI Responses API.")
instructions, input_items = llm.format_messages_for_responses(llm_messages)
logger.info("Responses instructions:\n%s", instructions)
logger.info("Responses input:\n%s", json.dumps(input_items, indent=2))
else:
logger.info("Formatting messages for the OpenAI Chat Completions API.")
chat_messages = llm.format_messages_for_llm(llm_messages)
logger.info("Chat Completions messages:\n%s", json.dumps(chat_messages, indent=2))
# Report cost
cost = llm.metrics.accumulated_cost
print(f"EXAMPLE_COST: {cost}")
You can run the example code as-is.
export LLM_API_KEY="your-api-key"
export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929" # or openai/gpt-4o, etc.
cd software-agent-sdk
uv run python examples/01_standalone_sdk/36_event_json_to_openai_messages.py
# https://app.faheemcode.ai/settings/api-keys
export LLM_API_KEY="example-user-api-key"
export LLM_MODEL="faheemcode/claude-sonnet-4-5-20250929"
cd software-agent-sdk
uv run python examples/01_standalone_sdk/36_event_json_to_openai_messages.py
How state persistence works
The SDK uses an automatic persistence system that saves state changes immediately when they occur. This ensures that conversation state is always recoverable, even if the process crashes unexpectedly.
Auto-save mechanism
When you modify any public field on ConversationState, the SDK automatically:
- Detects the field change via a custom
__setattr__implementation - Serializes the entire base state to
base_state.json - Triggers any registered state change callbacks
This happens transparently—you don't need to call any save methods manually.
# These changes are automatically persisted:
conversation.state.execution_status = ConversationExecutionStatus.RUNNING
conversation.state.max_iterations = 100
Events vs base state
The persistence system separates data into two categories:
| Category | Storage | Contents |
|---|---|---|
| Base State | base_state.json | Agent configuration, execution status, statistics, secrets, agent_state |
| Events | events/event-*.json | Message history, tool calls, observations, all conversation events |
Events are appended incrementally (one file per event), while base state is overwritten on each change. This design optimizes for:
- Fast event appends: No need to rewrite the entire history
- Atomic state updates: Base state is always consistent
- Efficient restoration: Events can be loaded lazily
Next steps
- Pause and Resume - Control execution flow
- Async Operations - Non-blocking operations