Runtime model
This page explains how a freeact agent runs: how configuration becomes a runtime, how events flow from stream(), what happens within a turn, and how subagents and cancellation fit in. For a hands-on introduction, see First agent with the SDK.
Configuration flow
Configuration flows one way: file, then schema, then resolution. config.load() reads .freeact/config.toml into a FreeactConfig (defaults when the file is missing); config.init() additionally initializes the .freeact/ workspace. config.resolve() turns the parsed config into a ResolvedRuntime: it expands tool presets into server configs, substitutes ${VAR} references, and prepares the model for pydantic-ai. The Agent is constructed from the resolved runtime.
There is no programmatic save: freeact writes config.toml only at init and never rewrites it. Edit the file to change configuration. See the Configuration reference for the file format and the .freeact/ directory structure.
Lifecycle
The agent manages MCP server connections and an IPython kernel via ipybox. On entering the async context manager, the IPython kernel starts and MCP servers configured for JSON tool calling connect. MCP servers configured for programmatic tool calling connect lazily on first tool call.
runtime = config.resolve(config.load())
async with Agent(runtime) as agent:
async for event in agent.stream(prompt):
...
# Connections closed, kernel stopped
Without using the async context manager:
runtime = config.resolve(config.load())
agent = Agent(runtime)
await agent.start()
try:
async for event in agent.stream(prompt):
...
finally:
await agent.stop()
Events
Each stream() call runs a single agent turn, with the agent managing conversation history across calls. The stream yields events as they occur:
| Event | Description |
|---|---|
ThoughtsChunk |
Partial model thoughts (content streaming) |
Thoughts |
Complete model thoughts at a given step |
ResponseChunk |
Partial model response (content streaming) |
Response |
Complete model response |
ApprovalRequest |
Pending code action or tool call approval |
CodeExecutionOutputChunk |
Partial code execution output (content streaming) |
CodeExecutionOutput |
Complete code execution output |
ToolOutput |
JSON tool call or built-in operation output |
Cancelled |
Agent turn was cancelled |
All yielded events inherit from AgentEvent and carry agent_id. Tool-related events additionally carry a corr_id correlating chunks, outputs, and approval requests of the same tool call; events produced by a subagent carry the parent task's correlation id in parent_corr_id.
ApprovalRequest events suspend execution until the application resolves them; Approvals and permissions explains this part of the model. A rejected code action or intercepted sub-command is signaled structurally: the final CodeExecutionOutput has approval_rejected=True.
Internal tools
The agent uses a small set of internal tools for reading and writing files, executing code and commands, spawning subagents, and discovering tools:
| Tool | Implementation | Description |
|---|---|---|
| read, write, edit | filesystem MCP server |
Reading, writing, and editing files via JSON tool calls (filesystem_read_text_file, filesystem_read_media_file, filesystem_write_text_file, filesystem_edit_text_file) |
| execute | ipybox_execute_ipython_cell |
Execution of Python code and shell commands (via ! syntax), delegated to ipybox's CodeExecutor, with shell commands and programmatic MCP tool calls intercepted at runtime for approval |
| subagent | subagent_task |
Task delegation to child agents |
| tool discovery | pytools MCP server for basic and hybrid discovery |
Tool discovery via category browsing or hybrid search; enabled with the discovery preset |
Turn limits
Use max_turns to limit the number of tool-execution rounds before the stream stops:
async for event in agent.stream(prompt, max_turns=50):
...
If max_turns=None (default), the loop continues until the model produces a final response.
Timeouts
The agent supports two timeout settings in config.toml:
execution_timeout: Maximum time in seconds for each code execution. Approval wait time is excluded from this budget, so the timeout only counts actual execution time. Defaults to 300 seconds. Set to0to disable.approval_timeout: Timeout in seconds for approval requests. An approval request that is not resolved within this time is rejected. Defaults to0(wait forever).
[agent]
execution_timeout = 60.0
approval_timeout = 30.0
Subagents
The built-in subagent_task tool delegates a subtask to a child agent with a fresh IPython kernel and fresh MCP server connections. The child inherits model, system prompt, and sandbox settings from the parent. Its events flow through the parent's stream using the same approval mechanism, with agent_id identifying the source:
async for event in agent.stream(prompt):
match event:
case ApprovalRequest(agent_id=agent_id, tool_call=tool_call) as request:
print(f"[{agent_id}] Approve {tool_call}?")
request.approve(True)
case Response(content=content, agent_id=agent_id):
print(f"[{agent_id}] {content}")
The main agent's agent_id is main, subagent IDs use the form sub-xxxx. Each delegated task defaults to max_turns=100. The max_subagents setting in config.toml limits concurrent subagents (default 5).
Cancellation
Call cancel() to stop a running agent turn. The active stream() stops at the next phase boundary and yields a Cancelled event whose phase field is a Phase value identifying the interrupted phase. Running kernel executions, including those in subagents, are interrupted immediately. Partial responses and synthetic tool returns are preserved in message history, so the conversation remains consistent for subsequent turns.
# From another coroutine or callback:
agent.cancel()
async for event in agent.stream(prompt):
match event:
case Cancelled(phase=phase):
print(f"Turn cancelled during {phase}")
case Response(content=content):
print(content)
Sessions
The agent persists message history incrementally to .freeact/sessions/<session-id>/<agent-id>.jsonl while it runs (unless enable_persistence is false). On resume, only the main agent's history (main.jsonl) is rehydrated; subagent histories (sub-xxxx.jsonl) are persisted for auditing only. Results exceeding an inline size threshold are stored as files in the session directory and referenced from the history. See Persist and resume sessions for creating, resuming, and configuring sessions.