RunState is the serialization mechanism that enables pausing, resuming, and inspecting agent runs. Think of RunState as a "save point" or "checkpoint" in a video game - it captures the complete state of an agent run at any moment, allowing you to pause execution, save that state, and resume it later. This is essential for human-in-the-loop workflows, long-running tasks, and debugging.
Core Concepts
What is RunState?
RunState is a complete snapshot of an agent run that includes:
Current agent - Which agent is currently active
Conversation history - All messages and tool calls so far
Model responses - History of model responses
Tool use tracker - Which tools have been used
Guardrail results - Results of guardrail checks
Session state - Session-related information
Sandbox state - Sandbox execution state (if applicable)
state = RunState.from_json(json_str)
if state._schema_version > CURRENT_SCHEMA_VERSION:
raise RunStateVersionError(
f"State version {state._schema_version} is newer than SDK version {CURRENT_SCHEMA_VERSION}"
)
RunState Serialization
to_json()
Serialize RunState to JSON:
Python
result = await Runner.run(agent, input)
state = result.to_state()
json_str = state.to_json()
What gets serialized:
Agent metadata (name, instructions, etc.)
All run items
Model responses
Tool use tracker
Guardrail results
Trace state
Sandbox state (if applicable)
Context (if serializer provided)
Run configuration
from_json()
Deserialize RunState from JSON:
Python
state = RunState.from_json(json_str)
What gets deserialized:
All serialized fields
Agent is reconstructed from metadata
Context is deserialized (if deserializer provided)
Context Serialization
Custom context requires serializers:
Python
from dataclasses import dataclass
import json
@dataclassclassMyContext:
user_id: str
data: dictdefserialize_context(context: MyContext) -> str:
"""Serialize context to JSON."""return json.dumps({
"user_id": context.user_id,
"data": context.data,
})
defdeserialize_context(data: str) -> MyContext:
"""Deserialize context from JSON."""
parsed = json.loads(data)
return MyContext(
user_id=parsed["user_id"],
data=parsed["data"],
)
context = MyContext(user_id="123", data={"key": "value"})
result = await Runner.run(
agent,
input,
context=context,
context_serializer=serialize_context,
context_deserializer=deserialize_context,
)
state = result.to_state()
json_str = state.to_json() # Context is serialized# Later
restored_state = RunState.from_json(json_str)
# Context is deserialized automatically
RunState and Human-in-the-Loop
Pausing for Approval
RunState enables approval workflows:
Python
@function_tool(needs_approval=True)defsensitive_operation() -> str:
"""Operation requiring approval."""return"Done"
result = await Runner.run(agent, "Perform sensitive operation")
# Check for interruptionsif result.interruptions:
# Save state
state = result.to_state()
# Human reviewsfor interruption in result.interruptions:
print(f"Tool: {interruption.tool_name}")
print(f"Args: {interruption.tool_arguments}")
if should_approve(interruption):
state.context.approve_tool(interruption)
else:
state.context.reject_tool(interruption)
# Resume
result = await Runner.run(agent, state)
# First run with session
result1 = await Runner.run(
agent,
input,
session=session,
conversation_id="conv-123",
)
state = result1.to_state()
# Resume - session is automatically used
result2 = await Runner.run(agent, state)
RunState and Sandbox
Sandbox State in RunState
Sandbox state is included:
Python
config = SandboxRunConfig(client=UnixLocalSandboxClient())
result = await Runner.run(
agent,
input,
run_config=config,
)
state = result.to_state()
# state._sandbox contains sandbox state
Sandbox Resumption
Resume with sandbox state:
Python
# First run with sandbox
result1 = await Runner.run(agent, input, run_config=config)
state = result1.to_state()
# Resume - sandbox is restored
result2 = await Runner.run(agent, state)
RunState and Errors
Error Details
RunState captures error details:
Python
try:
result = await Runner.run(agent, input)
except MaxTurnsExceeded as e:
state = e.run_state
# state.error_details contains error information
Error Recovery
Recover from errors using RunState:
Python
try:
result = await Runner.run(agent, input)
except MaxTurnsExceeded as e:
state = e.run_state
# Increase max_turns
state.run_config.max_turns = 20# Resume
result = await Runner.run(agent, state)
RunState Inspection
Inspecting Conversation History
Python
state = result.to_state()
# View all itemsfor item in state.all_items:
print(f"{type(item).__name__}: {item}")
# View only messages
messages = [i for i in state.all_items ifisinstance(i, MessageOutputItem)]
# View tool calls
tool_calls = [i for i in state.all_items ifisinstance(i, ToolCallItem)]
Inspecting Tool Usage
Python
state = result.to_state()
# Tool use tracker
tracker = state.tool_use_tracker
print(f"Tools used: {tracker.tool_counts}")
print(f"Total calls: {tracker.total_calls}")
Inspecting Guardrail Results
Python
state = result.to_state()
# Input guardrailsfor result in state.input_guardrail_results:
print(f"Guardrail: {result.guardrail.get_name()}")
print(f"Triggered: {result.output.tripwire_triggered}")
# Output guardrailsfor result in state.output_guardrail_results:
print(f"Guardrail: {result.guardrail.get_name()}")
print(f"Triggered: {result.output.tripwire_triggered}")
Inspecting Model Responses
Python
state = result.to_state()
for response in state.model_responses:
print(f"Model: {response.agent.name}")
print(f"Usage: {response.usage}")
print(f"Items: {len(response.response.output)}")
RunState and Tracing
Trace State
RunState includes trace state:
Python
state = result.to_state()
# Trace state
trace_state = state._trace_state
print(f"Trace ID: {trace_state.trace_id}")
print(f"Spans: {len(trace_state.spans)}")
Resuming with Trace
Trace is continued when resuming:
Python
# First run
result1 = await Runner.run(agent, input)
state = result1.to_state()
# Resume - trace continues
result2 = await Runner.run(agent, state)
# result2.trace is a continuation of result1.trace
RunState Best Practices
1. Serialize Context Properly
Always provide serializers for custom context:
Python
# Good
result = await Runner.run(
agent,
input,
context=my_context,
context_serializer=serialize_context,
context_deserializer=deserialize_context,
)
# Avoid - context won't serialize
result = await Runner.run(
agent,
input,
context=my_context,
)
2. Handle Schema Versions
Be aware of schema version changes:
Python
# Good - check version
state = RunState.from_json(json_str)
if state._schema_version > CURRENT_SCHEMA_VERSION:
# Handle newer versionpass# Avoid - assume compatibility
state = RunState.from_json(json_str)
# Might fail if version is incompatible
3. Clean Up Old States
Implement cleanup for old states:
Python
asyncdefcleanup_old_states(days: int = 30):
"""Clean up states older than N days."""
cutoff = datetime.now() - timedelta(days=days)
await state_store.delete_before(cutoff)
4. Validate State Before Resumption
Validate state before resuming:
Python
defvalidate_state(state: RunState) -> bool:
"""Validate state before resumption."""# Check schema versionif state._schema_version > CURRENT_SCHEMA_VERSION:
returnFalse# Check required fieldsifnot state.agent:
returnFalse# Check context deserializerif state.context andnot state.context_deserializer:
returnFalsereturnTrue# Use before resumptionif validate_state(state):
result = await Runner.run(agent, state)
5. Encrypt Sensitive States
Encrypt sensitive state data:
Python
defencrypt_state(state: RunState, key: str) -> str:
"""Encrypt state to JSON."""
json_str = state.to_json()
encrypted = encrypt(json_str, key)
return encrypted
defdecrypt_state(encrypted: str, key: str) -> RunState:
"""Decrypt state from JSON."""
json_str = decrypt(encrypted, key)
return RunState.from_json(json_str)
Common RunState Patterns
1. Approval Workflow
Python
result = await Runner.run(agent, input)
if result.interruptions:
state = result.to_state()
# UI for approval
approved = get_user_approval(result.interruptions)
if approved:
state.context.approve_tool(result.interruptions[0])
else:
state.context.reject_tool(result.interruptions[0])
result = await Runner.run(agent, state)
2. Long-Running Task
Python
# Start task
result = await Runner.run(agent, "Start long task")
state = result.to_state()
# Save stateawait save_state_to_db(task_id, state.to_json())
# ... time passes ...# Resume task
json_str = await load_state_from_db(task_id)
state = RunState.from_json(json_str)
result = await Runner.run(agent, state)
3. Error Recovery
Python
try:
result = await Runner.run(agent, input, max_turns=10)
except MaxTurnsExceeded as e:
state = e.run_state
# Adjust configuration
state.run_config.max_turns = 20# Resume
result = await Runner.run(agent, state)
4. Debugging
Python
result = await Runner.run(agent, input)
state = result.to_state()
# Inspect stateprint(f"Agent: {state.agent.name}")
print(f"Turn: {state._current_turn}")
print(f"Items: {len(state.all_items)}")
# Find what went wrongfor item in state.all_items:
ifisinstance(item, ToolCallOutputItem):
if"error"in item.output.lower():
print(f"Error in tool: {item}")
5. State Sharing
Share state across processes:
Python
# Process 1
result = await Runner.run(agent, input)
state = result.to_state()
await redis.set(f"state:{run_id}", state.to_json())
# Process 2
json_str = await redis.get(f"state:{run_id}")
state = RunState.from_json(json_str)
result = await Runner.run(agent, state)
RunState and Version Migration
Migrating Between Versions
When the schema changes, implement migration:
Python
defmigrate_state(state: RunState, target_version: str) -> RunState:
"""Migrate state to target version."""
current = state._schema_version
while current != target_version:
# Apply migration for current -> current+1
state = apply_migration(state, current, str(int(current) + 1))
current = state._schema_version
return state
Backward Compatibility
Maintain backward compatibility:
Python
defload_state_with_migration(json_str: str) -> RunState:
"""Load state with automatic migration."""
state = RunState.from_json(json_str)
# Migrate to current version if neededif state._schema_version < CURRENT_SCHEMA_VERSION:
state = migrate_state(state, CURRENT_SCHEMA_VERSION)
return state