# freeact > Freeact code action agent General-purpose AI agent that acts via code actions through a unified execution interface. Freeact is a lightweight, general-purpose agent that acts via code actions in a stateful execution environment provided by ipybox. A unified execution interface allows code actions to contain any combination of Python code, shell commands, and programmatic MCP tool calls, generated in one LLM inference pass. For programmatic MCP tool calling ("code mode"), freeact generates typed Python APIs from MCP server schemas. The agent inspects generated APIs prior to execution and composes them within code actions based on available type information. Successful code actions can be saved as reusable tools, capturing agent experience as executable knowledge, optionally combined with agent skills. Freeact supports tool discovery via agentic and semantic search, loading only task-relevant tool information into the context window. It can enforce application-level approval of code actions, shell commands, and programmatic tool calls, originating from both main agents and subagents. Freeact runs locally on your computer and is available as a CLI tool and Python SDK. # Documentation # freeact General-purpose AI agent that acts via code actions through a unified execution interface. ## Overview Freeact is a lightweight, general-purpose agent that acts via code actions in a stateful execution environment provided by [ipybox](https://github.com/gradion-ai/ipybox). A unified execution interface allows code actions to contain any combination of Python code, shell commands, and programmatic MCP tool calls, generated in one LLM inference pass. For programmatic MCP tool calling ("code mode"), freeact generates typed Python APIs from MCP server schemas. The agent inspects generated APIs prior to execution and composes them within code actions based on available type information. Successful code actions can be saved as reusable tools, capturing agent experience as executable knowledge, optionally combined with agent skills. Freeact supports tool discovery via agentic and semantic search, loading only task-relevant tool information into the context window. It can enforce application-level approval of code actions, shell commands, and programmatic tool calls, originating from both main agents and subagents. Freeact runs locally on your computer and is available as a CLI tool and Python SDK. Supported models Freeact supports any model compatible with [Pydantic AI](https://ai.pydantic.dev/). See [Models](https://gradion-ai.github.io/freeact/models/index.md) for provider configuration and examples. ## Capabilities | Capability | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Unified execution** | Freeact agents act by executing Python code, shell commands, and programmatic MCP tool calls. These can be combined within a code action, generated in a single LLM inference pass. | | **Action approval** | Application-level approval of code actions, shell commands, and programmatic tool calls originating from both main agents and subagents. | | **MCP code mode** | Freeact calls MCP server tools programmatically[1](#fn:1) via generated Python APIs. This enables composition of tool calls and intermediate result processing in code actions, reducing LLM roundtrips. | | **Local execution** | Freeact executes code and shell commands locally in an IPython kernel provided by [ipybox](https://github.com/gradion-ai/ipybox). Data, configuration, and generated tools live in local workspaces. | | **Sandbox mode** | IPython kernels optionally run in a sandbox environment based on Anthropic's [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime), enforcing filesystem and network restrictions at OS level. | | **Tool discovery** | Tools are discovered via category browsing or hybrid BM25/vector search. On-demand loading frees the context window and scales to larger tool libraries. | | **Subagent delegation** | Tasks can be delegated to subagents, each using their own execution environment. This enables specialization and parallelization without cluttering the main agent's context. | | **Agent skills** | [agentskills.io](https://agentskills.io/)-based skills add specialized knowledge and workflows, composing naturally with code actions and agent-authored tools. | | **Tool authoring** | Agents can create new tools, enhance existing tools, and save code actions as reusable tools. This captures agent experiences as executable knowledge. | | **Session persistence** | Freeact persists agent state incrementally. Persisted sessions can be resumed and serve as a record for debugging, evaluation, and improvement. | ## Usage | Component | Description | | ------------------------------------------------------------------ | ---------------------------------------------------------------------- | | **[Agent SDK](https://gradion-ai.github.io/freeact/sdk/index.md)** | Agent harness and Python API for building freeact applications. | | **[CLI tool](https://gradion-ai.github.io/freeact/cli/index.md)** | Terminal interface for interactive conversations with a freeact agent. | ______________________________________________________________________ 1. Freeact also supports MCP server integration via JSON tool calling but the recommended approach is programmatic tool calling. [↩](#fnref:1 "Jump back to footnote 1 in the text") # Installation ## Prerequisites - Python 3.11+ - [uv](https://docs.astral.sh/uv/) package manager - Node.js 20+ (for MCP servers) ## Workspace Setup A workspace is a directory where freeact stores configuration, tools, and other resources. Both setup options below require their own workspace directory. ### Option 1: Minimal The fastest way to get started is using `uvx`, which keeps the virtual environment separate from the workspace: ``` mkdir my-workspace && cd my-workspace uvx freeact ``` This is ideal when you don't need to install additional Python packages in the workspace. ### Option 2: With Virtual Environment To create a workspace with its own virtual environment: ``` mkdir my-workspace && cd my-workspace uv init --bare --python 3.13 uv add freeact ``` Then run freeact with: ``` uv run freeact ``` This approach lets you install additional packages (e.g., `uv add pandas`) that will be available to the agent. ## API Key Freeact uses `google-gla:gemini-3.5-flash` as the [default model](https://gradion-ai.github.io/freeact/models/index.md). Set the API key in your environment: ``` export GEMINI_API_KEY="your-api-key" ``` Alternatively, place it in a `.env` file in the workspace directory: .env ``` GEMINI_API_KEY=your-api-key ``` ## Sandbox Mode Prerequisites For running freeact in sandbox mode, install Anthropic's [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime): ``` npm install -g @anthropic-ai/sandbox-runtime@0.0.21 ``` Higher versions should also work, but 0.0.21 is the version used in current tests. Required OS-level packages are: ### macOS ``` brew install ripgrep ``` macOS uses the native `sandbox-exec` for process isolation. ### Linux ``` apt-get install bubblewrap socat ripgrep ``` Work in progress Sandboxing on Linux is currently work in progress. # Quickstart This guide shows how to run a simple task using the freeact [CLI tool](#cli-tool) and the [Agent SDK](#agent-sdk). ## CLI Tool Freeact provides a [CLI tool](https://gradion-ai.github.io/freeact/cli/index.md) for running the agent in a terminal. ### Starting Freeact Create a workspace directory, set your API key, and start the agent: ``` mkdir my-workspace && cd my-workspace echo "GEMINI_API_KEY=your-api-key" > .env uvx freeact ``` See [Installation](https://gradion-ai.github.io/freeact/installation/index.md) for alternative setup options and sandbox mode prerequisites. Using a different model The current default model is `google-gla:gemini-3.5-flash`. Freeact supports any model compatible with Pydantic AI. To switch providers or configure model settings, see [Models](https://gradion-ai.github.io/freeact/models/index.md). ### Generating MCP Tool APIs On first start, the CLI tool auto-generates Python APIs for [configured](https://gradion-ai.github.io/freeact/configuration/#ptc_servers) MCP servers. For example, it creates `.freeact/generated/mcptools/google/web_search.py` for the `web_search` tool of the bundled `google` MCP server. With the generated Python API, the agent can import and call this tool programmatically. Custom MCP servers For calling the tools of your own MCP servers programmatically, add them to the [`ptc_servers`](https://gradion-ai.github.io/freeact/configuration/#ptc_servers) section in `.freeact/agent.json`. Freeact auto-generates a Python API for them when the CLI tool starts. ### Running a Task With this setup and a question like > who is F1 world champion 2025? the CLI tool should produce an end result similar to the following screenshot: The screenshot shows: - **Progressive tool loading**: The agent progressively loads tool information: lists categories, lists tools in the `google` category, then reads `web_search.py` to understand the generated interface. - **Programmatic tool calling**: The agent writes Python code that imports the `web_search` tool from `mcptools.google` and calls it programmatically (PTC) with the user's query. The code execution output shows the search result with source URLs. The agent response is a summary of it. ### Approval Prompt Freeact can prompt for approval before running code actions. Shell commands and programmatic tool calls within code actions are intercepted during execution and approved individually. The screenshot below shows the approval prompt for a programmatic tool call (PTC): Code actions and tool calls can also be pre-approved. See [Approval Prompt](https://gradion-ai.github.io/freeact/cli/#approval-prompt) for prompt options and behavior. ## Agent SDK The CLI tool is built on the [Agent SDK](https://gradion-ai.github.io/freeact/sdk/index.md) that you can use directly in your applications. The following minimal example shows how to run the same task programmatically, with code actions and tool calls auto-approved by the application: ``` import asyncio from freeact.agent import ( Agent, ApprovalRequest, CodeAction, CodeExecutionOutput, Response, ShellAction, Thoughts, ToolOutput, ) from freeact.agent.config import Config from freeact.tools.pytools.apigen import generate_mcp_sources async def main() -> None: config = await Config.init() # Generate Python APIs for MCP servers in ptc_servers for server_name, params in config.ptc_servers.items(): if not (config.generated_dir / "mcptools" / server_name).exists(): await generate_mcp_sources({server_name: params}, config.generated_dir) async with Agent(config=config) as agent: prompt = "Who is the F1 world champion 2025?" async for event in agent.stream(prompt): match event: case ApprovalRequest(tool_call=CodeAction(code=code)) as request: print(f"Code action:\n{code}") request.approve(True) case ApprovalRequest(tool_call=ShellAction(command=cmd)) as request: print(f"Shell command: {cmd}") request.approve(True) case ApprovalRequest(tool_call=tool_call) as request: print(f"Tool: {tool_call.tool_name}") request.approve(True) case Thoughts(content=content): print(f"Thinking: {content}") case CodeExecutionOutput(text=text): print(f"Code execution output: {text}") case ToolOutput(content=content): print(f"Tool call result: {content}") case Response(content=content): print(content) if __name__ == "__main__": asyncio.run(main()) ``` # Configuration Freeact configuration is stored in the `.freeact/` directory. This page describes the directory structure and configuration formats. It also describes the structure of [tool directories](#tool-directories). ## Initialization The `.freeact/` directory is initialized through CLI entry points: | Entry Point | Description | | -------------------------- | ------------------------------------------------------------ | | `freeact` or `freeact run` | Initializes `.freeact/` on first run, then starts the agent. | | `freeact init` | Initializes `.freeact/` without starting the agent. | Both CLI entry points initialize only when `.freeact/` is missing. For programmatic agent configuration, see the [Agent SDK](https://gradion-ai.github.io/freeact/sdk/index.md) and [Configuration API](https://gradion-ai.github.io/freeact/api/config/index.md). ## Directory Structure Freeact stores agent configuration and runtime state in `.freeact/`. Project-level customization uses `AGENTS.md` for [project instructions](#project-instructions) and `.agents/skills/` for [custom skills](#custom-skills). ``` / ├── AGENTS.md # Project instructions (injected into system prompt) ├── .agents/ │ └── skills/ # Custom skills │ └── / │ ├── SKILL.md │ └── ... └── .freeact/ ├── agent.json # Configuration and MCP server definitions ├── terminal.json # Terminal UI behavior and keybindings ├── skills/ # Bundled skills │ └── / │ ├── SKILL.md # Skill metadata and instructions │ └── ... # Further skill resources ├── generated/ # Generated tool sources (on PYTHONPATH) │ ├── mcptools/ # Generated Python APIs from ptc_servers │ └── gentools/ # User-defined tools saved from code actions ├── plans/ # Task plan storage ├── sessions/ # Session trace storage │ └── / │ ├── main.jsonl │ ├── sub-xxxx.jsonl │ └── tool-results/ │ └── . # Large tool results stored as files └── permissions.json # Persisted approval decisions ``` ## Configuration File The `agent.json` file contains agent settings and MCP server configurations: ``` { "model": "google-gla:gemini-3.5-flash", "model_settings": { ... }, "tool_search": "basic", "images_dir": null, "execution_timeout": 300, "approval_timeout": null, "tool_result_inline_max_bytes": 32768, "tool_result_preview_chars": 2048, "enable_persistence": true, "enable_subagents": true, "max_subagents": 5, "kernel_env": {}, "mcp_servers": {}, "ptc_servers": { "server-name": { ... } } } ``` ### Agent Settings | Setting | Default | Description | | ------------------------------ | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | `google-gla:gemini-3.5-flash` | [Model identifier](https://gradion-ai.github.io/freeact/models/#model-identifier) in `provider:model-name` format | | `model_settings` | [Google thinking config](https://gradion-ai.github.io/freeact/models/#google-default) | Provider-specific [model settings](https://gradion-ai.github.io/freeact/models/#model-settings) (e.g., thinking config, temperature) | | `provider_settings` | `null` | Custom API credentials, endpoints, or other [provider-specific options](https://gradion-ai.github.io/freeact/models/#provider-settings) | | `images_dir` | `null` | Directory for saving generated images to disk. `null` defaults to `images` in the working directory. | | `execution_timeout` | `300` | Maximum time in seconds for [code execution](https://gradion-ai.github.io/freeact/execution/index.md). Approval wait time is excluded. `null` means no timeout. | | `approval_timeout` | `null` | Timeout in seconds for PTC approval requests. `null` means no timeout. | | `tool_result_inline_max_bytes` | `32768` | Inline size threshold in bytes for tool results. Larger results are saved to `.freeact/sessions//tool-results/` and replaced with a file reference notice plus preview characters. | | `tool_result_preview_chars` | `2048` | Number of preview characters included from both the beginning and end of large text results in the file reference notice. | | `enable_persistence` | `true` | Persist message history to `.freeact/sessions/` and allow session resume with `session_id`. If `false`, history stays in memory for the process lifetime only. | | `enable_subagents` | `true` | Whether to enable subagent delegation | | `max_subagents` | `5` | Maximum number of concurrent subagents | | `kernel_env` | `{}` | Environment variables passed to the IPython kernel. Supports `${VAR}` placeholders resolved against the host environment. | ### `tool_search` Controls how the agent discovers Python tools: | Mode | Description | | -------- | --------------------------------------------------------------------------- | | `basic` | Category browsing with `pytools_list_categories` and `pytools_list_tools` | | `hybrid` | BM25/vector search with `pytools_search_tools` for natural language queries | For hybrid mode environment variables, see [Hybrid Search](#hybrid-search). ### `mcp_servers` MCP servers called directly via JSON tool calls. Internal servers (`pytools` for basic or hybrid tool search and filesystem for file operations) are provided automatically and do not need to be configured. User-defined servers in this section are merged with the internal defaults. If a user entry uses the same key as an internal server, the user entry takes precedence. Custom MCP servers Application-specific MCP servers for JSON tool calls can be added to this section as needed. ### `ptc_servers` MCP servers called programmatically via generated Python APIs. This is freeact's implementation of *code mode*[1](#fn:1), where the agent calls MCP tools by writing code against generated APIs rather than through JSON tool calls. This allows composing multiple tool calls, processing intermediate results, and using control flow within a single code action. Python APIs must be generated from `ptc_servers` to `.freeact/generated/mcptools//.py` before the agent can use them. The [CLI tool](https://gradion-ai.github.io/freeact/cli/index.md) handles this automatically. When using the [Agent SDK](https://gradion-ai.github.io/freeact/sdk/index.md), call generate_mcp_sources() explicitly. Code actions can then import and call the generated APIs because `.freeact/generated/` is on the kernel's `PYTHONPATH`. The default configuration includes the bundled `google` (web search via Gemini) and `fetch` (URL content retrieval) MCP servers. Additional bundled servers can be added manually: ``` { "ptc_servers": { "google": { "command": "python", "args": ["-m", "freeact.tools.gsearch", "--thinking-level", "medium"], "env": {"GEMINI_API_KEY": "${GEMINI_API_KEY}"} }, "brave": { "command": "python", "args": ["-m", "freeact.tools.bsearch"], "env": {"BRAVE_API_KEY": "${BRAVE_API_KEY}"} }, "fetch": { "command": "python", "args": ["-m", "freeact.tools.fetch"], "env": {} } } } ``` | Server | Module | Required env var | Description | | -------- | ----------------------- | ---------------- | --------------------------------------------------------------------------------------------------- | | `google` | `freeact.tools.gsearch` | `GEMINI_API_KEY` | Web search via Gemini with Google Search grounding | | `brave` | `freeact.tools.bsearch` | `BRAVE_API_KEY` | Web search via [Brave Search API](https://brave.com/search/api/) with "web" and "llm-context" modes | | `fetch` | `freeact.tools.fetch` | -- | Fetch and extract readable content from URLs via [trafilatura](https://trafilatura.readthedocs.io/) | Custom MCP servers Application-specific MCP servers can be added as needed to `ptc_servers` for programmatic tool calling. ### Server Formats Both `mcp_servers` and `ptc_servers` support stdio servers and streamable HTTP servers. ### Environment Variables Server configurations support environment variable references using `${VAR_NAME}` syntax. Config() validates that all referenced variables are set. If a variable is missing, loading fails with an error. ## Hybrid Search When `tool_search` is set to `"hybrid"` in `agent.json`, the hybrid search server reads additional configuration from environment variables. Default values are provided for all optional variables: | Variable | Default | Description | | ------------------------- | --------------------------------- | ----------------------------------------------------- | | `GEMINI_API_KEY` | *(required)* | API key for the default embedding model | | `PYTOOLS_DIR` | `.freeact/generated` | Base directory containing `mcptools/` and `gentools/` | | `PYTOOLS_DB_PATH` | `.freeact/search.db` | Path to SQLite database for search index | | `PYTOOLS_EMBEDDING_MODEL` | `google-gla:gemini-embedding-001` | Embedding model identifier | | `PYTOOLS_EMBEDDING_DIM` | `3072` | Embedding vector dimensions | | `PYTOOLS_SYNC` | `true` | Sync index with tool directories on startup | | `PYTOOLS_WATCH` | `true` | Watch tool directories for changes | | `PYTOOLS_BM25_WEIGHT` | `1.0` | Weight for BM25 (keyword) results in hybrid fusion | | `PYTOOLS_VEC_WEIGHT` | `1.0` | Weight for vector (semantic) results in hybrid fusion | To use a different embedding provider, change `PYTOOLS_EMBEDDING_MODEL` to a supported [pydantic-ai embedder](https://ai.pydantic.dev/embeddings/) identifier. Testing without an API key Set `PYTOOLS_EMBEDDING_MODEL=test` to use a test embedder that generates deterministic embeddings. This is useful for development and testing but produces meaningless search results. ## System Prompt The system prompt is an internal resource bundled with the package ([`system.md`](https://github.com/gradion-ai/freeact/blob/main/freeact/agent/config/prompts/system.md)). The template supports placeholders: | Placeholder | Description | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | | `{working_dir}` | The agent's workspace directory | | `{generated_rel_dir}` | Relative path to the generated tool sources directory | | `{project_instructions}` | Content from `AGENTS.md`, wrapped in `` tags. Omitted if the file is absent or empty. | | `{skills}` | Rendered metadata from bundled skills (`.freeact/skills/`) and custom skills (`.agents/skills/`). Omitted if no skills exist. | ## Project Instructions The agent loads project-specific instructions from an `AGENTS.md` file in the working directory. If the file exists and is non-empty, its content is injected into the system prompt. If the file is absent or empty, the section is omitted. `AGENTS.md` provides project context to the agent: domain-specific conventions, workflow preferences, or any instructions relevant to the agent's tasks. ## Skills Skills are filesystem-based capability packages that specialize agent behavior. A skill is a directory containing a `SKILL.md` file with metadata in YAML frontmatter, and optionally further skill resources. Skills follow the [agentskills.io](https://agentskills.io/specification/) specification. Skills are loaded on demand: only metadata is in context initially, full instructions load when relevant. ### Bundled Skills Freeact contributes three bundled skills to `.freeact/skills/`: | Skill | Description | | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | | [output-parsers](https://github.com/gradion-ai/freeact/tree/main/freeact/agent/config/templates/skills/output-parsers) | Generate output parsers for `mcptools/` with unstructured return types | | [saving-codeacts](https://github.com/gradion-ai/freeact/tree/main/freeact/agent/config/templates/skills/saving-codeacts) | Save generated code actions as reusable tools in `gentools/` | | [task-planning](https://github.com/gradion-ai/freeact/tree/main/freeact/agent/config/templates/skills/task-planning) | Basic task planning and tracking workflows | Bundled skills are auto-created from templates on [initialization](#initialization). User modifications persist across restarts. Tool authoring The `output-parsers` and `saving-codeacts` skills enable tool authoring. See [Enhancing Tools](https://gradion-ai.github.io/freeact/examples/output-parser/index.md) and [Code Action Reuse](https://gradion-ai.github.io/freeact/examples/saving-codeacts/index.md) for walkthroughs. ### Custom Skills Custom skills are loaded from `.agents/skills/` in the working directory. Each subdirectory containing a `SKILL.md` file is registered as a skill. Metadata of custom skills appears in the system prompt after bundled skills. The `.agents/skills/` directory is not managed by freeact and is not auto-created. Example See [Custom Agent Skills](https://gradion-ai.github.io/freeact/examples/agent-skills/index.md) for a walkthrough of installing and using a custom skill. ## Permissions [Permissions](https://gradion-ai.github.io/freeact/sdk/#permissions-api) control which code actions, shell commands, and tool calls require approval or are auto-approved. They are stored in `.freeact/permissions.json` as typed entries with glob-style patterns. `tool_name` and `command` fields support `*` (any characters) and `?` (single character). Path fields (`path`) use path-aware matching where `*` matches within a single directory and `**` matches across directory boundaries. Bypassing permissions The CLI supports a [`--skip-permissions`](https://gradion-ai.github.io/freeact/cli/#options) flag to run tools without prompting for approval, effectively auto-approving all actions. ``` { "ask": [ {"type": "ShellAction", "tool_name": "bash", "command": "rm *"} ], "allow": [ {"type": "GenericCall", "tool_name": "github_*"}, {"type": "ShellAction", "tool_name": "bash", "command": "git *"}, {"type": "FileRead", "tool_name": "filesystem_*", "path": ".freeact/**"}, {"type": "FileWrite", "tool_name": "filesystem_*", "path": "src/**"} ] } ``` Each entry has a `type` field that determines which fields are matched: | Type | Matched fields | | ------------- | ---------------------- | | `GenericCall` | `tool_name` | | `ShellAction` | `tool_name`, `command` | | `CodeAction` | `tool_name` | | `FileRead` | `tool_name`, `path` | | `FileWrite` | `tool_name`, `path` | | `FileEdit` | `tool_name`, `path` | ### Tiers Permissions are organized into two tiers: | Tier | Description | | ------- | ------------------------------------ | | `allow` | Tool call executes without prompting | | `ask` | User is always prompted for approval | Evaluation order is **ask then allow**: if an entry matches both tiers, the user is prompted. Each tier supports two persistence scopes: **always** (persisted to `permissions.json`) and **session** (in-memory, cleared when the session ends). ### Tool call patterns Tool call patterns match against MCP tool names (e.g. `github_search_repositories`, `filesystem_read_file`). Filesystem tools (`FileRead`, `FileWrite`, `FileEdit`) additionally match on path fields, enabling path-specific rules like allowing reads only from `src/**`. ### Shell command patterns `ShellAction` entries match on `tool_name` (always `bash`) and `command`. The `command` field supports glob matching with `*` and `?`. Shell commands are intercepted during code execution with Python variables fully resolved, so patterns match against the actual command values. | Pattern | Matches | | --------------- | ----------------------------------------------------- | | `git *` | Any `git` command | | `git commit *` | `git commit` with any arguments | | `pip install *` | `pip install` with any package | | `rm *` | Any `rm` command (use in `ask` tier to always prompt) | When saving a permission rule via the [approval prompt](https://gradion-ai.github.io/freeact/cli/#approval-prompt), the CLI pre-fills a suggested pattern. The heuristic uses `cmd subcmd *` for known multi-word tools (git, pip, docker, kubectl, npm, uv, cargo, etc.) and `cmd *` for others. The user can edit the pattern before saving. Composite shell commands joined with `&&`, `||`, `|`, or `;` are decomposed into individual sub-commands, each checked independently. If any sub-command is denied, the entire command is blocked. ### Path wildcards Path fields (`path`) in `FileRead`, `FileWrite`, and `FileEdit` entries use path-aware matching. Paths are normalized relative to the working directory before matching: absolute paths under the working directory become relative, paths outside stay absolute. | Wildcard | Scope | | -------- | ----------------------------------- | | `*` | Matches within a single directory | | `**` | Matches across directory boundaries | The leading `/` determines whether a pattern targets paths inside or outside the working directory: | Pattern | Inside working dir | Outside working dir | | -------- | ------------------ | ------------------- | | `**` | Yes | No | | `/**` | No | Yes | | `src/**` | Yes (under `src/`) | No | `tool_name` and `command` fields use standard glob matching where `*` matches any characters and `?` matches a single character. ### Default rules On first run, `permissions.json` is seeded with a generous set of read-only allow rules (file reads inside the working directory, common shell inspection commands like `ls`, `cat`, `git status`, and read-only built-in tools) plus an ask rule that always prompts for `.env` reads. Inspect `.freeact/permissions.json` after the first run for the full list, and edit it to tighten or extend the defaults. ## Tool Directories The agent discovers tools from two directories under `.freeact/generated/`: ### `mcptools/` Generated Python APIs from `ptc_servers` schemas: ``` .freeact/generated/mcptools/ └── / └── .py # Generated tool module ``` ### `gentools/` User-defined tools saved from successful code actions: ``` .freeact/generated/gentools/ └── / └── / ├── __init__.py ├── api.py # Public interface └── impl.py # Implementation ``` ## Terminal UI The `terminal.json` file configures terminal UI collapse behavior and keybindings. ``` { "collapse_thoughts_on_complete": true, "collapse_exec_output_on_complete": true, "collapse_approved_code_actions": false, "collapse_approved_tool_calls": true, "collapse_completed_subagent_tasks": true, "collapse_tool_outputs": true, "keep_rejected_actions_expanded": true, "pin_pending_approval_action_expanded": true, "expand_all_toggle_key": "ctrl+o" } ``` ### Initialization The CLI creates `terminal.json` during `freeact`, `freeact run`, and `freeact init` when the file is missing. SDK integrations can load or initialize this file by calling `await freeact.terminal.config.Config.init()`. ### Settings | Setting | Default | Description | | -------------------------------------- | -------- | -------------------------------------------------------------------------------------------------- | | `collapse_thoughts_on_complete` | `true` | Collapse `Thinking` boxes after a completed `Thoughts` event. | | `collapse_exec_output_on_complete` | `true` | Collapse `Execution Output` boxes after a completed `CodeExecutionOutput` event. | | `collapse_approved_code_actions` | `false` | Collapse approved code action previews after approval. | | `collapse_approved_tool_calls` | `true` | Collapse approved tool call previews after approval. | | `collapse_completed_subagent_tasks` | `true` | Collapse completed root-level `subagent_task` widgets after the parent task `Tool Output` arrives. | | `collapse_tool_outputs` | `true` | Render `Tool Output` boxes collapsed by default. | | `keep_rejected_actions_expanded` | `true` | Keep rejected action previews expanded after rejection. | | `pin_pending_approval_action_expanded` | `true` | Keep the current pending approval action expanded until a decision is made. | | `expand_all_toggle_key` | `ctrl+o` | Toggle all collapsible boxes between expanded and configured state. | ______________________________________________________________________ 1. [Code Mode: the better way to use MCP](https://blog.cloudflare.com/code-mode/) [↩](#fnref:1 "Jump back to footnote 1 in the text") # Code Execution Freeact executes Python code, shell commands, and programmatic MCP tool calls in an IPython kernel provided by [ipybox](https://github.com/gradion-ai/ipybox). A [unified execution interface](https://gradion-ai.github.io/ipybox/codeexec/) enables their combination within a single code action. Code actions, shell commands, and programmatic tool calls are subject to [action approval](#action-approval) before execution. ## Python Code Given a prompt like *"what is 17 raised to the power of 0.13"*, the agent generates and executes Python code directly: ``` print(17 ** 0.13) ``` ``` 1.4453011884051326 ``` ## Shell Commands Given a prompt like *"which .py files in tests/ contain ipybox"*, the agent uses a shell command with the `!` prefix: ``` !grep -r "ipybox" tests/ --include="*.py" -l ``` ``` tests/unit/test_agent.py tests/conftest.py tests/integration/test_agent.py tests/integration/test_subagents.py ``` Each `!` line spawns a separate subprocess. Multi-line shell scripts can use the `%%bash` cell magic, which runs as a single subprocess: ``` %%bash cd /tmp echo "Now in $(pwd)" ls -la ``` Shell state (working directory, variables) does not persist across `!` lines but persists within a `%%bash` block. Neither carries state to the next cell execution. Their results can be stored in variables though. `%%bash` approval Approval support for `%%bash` cell magic is not implemented yet (coming soon). ## Programmatic Tool Calls [Generated Python APIs](https://gradion-ai.github.io/freeact/sdk/#generation-api) for MCP server tools can be imported and called like regular packages: ``` from mcptools.google.web_search import run, Params result = run(Params(query="python async tutorial")) print(result) ``` ## Combining Them Python code, shell commands, and programmatic tool calls can be freely combined within a single code action: ``` !pip install pandas import pandas as pd from mcptools.fetch.web_fetch import run, Params result = run(Params(url="https://example.com/data.csv")) df = pd.read_csv(result) print(df.describe()) ``` Shell output can be captured into Python variables: ``` files = !ls /data/*.csv print(f"Found {len(files)} CSV files") ``` Python variables can be interpolated into shell commands: ``` filename = "report.pdf" !cp /tmp/{filename} /output/ ``` ## Action Approval Code actions, contained shell commands, and programmatic tool calls require approval before execution. Shell commands and programmatic tool calls are intercepted during code action execution for individual approval. Composite shell commands (using `&&`, `||`, `|`, `;`) are decomposed into individual sub-commands, each approved separately. Python variables in shell commands are resolved before the approval request, so the approval request shows actual values. See the Agent SDK for programmatic [approval](https://gradion-ai.github.io/freeact/sdk/#approval) control, [permission configuration](https://gradion-ai.github.io/freeact/configuration/#permissions) for action pre-approval, and the CLI tool for the interactive [approval prompt](https://gradion-ai.github.io/freeact/cli/#approval-prompt). ## Working Directory The kernel starts in the agent's workspace directory. After each code action, the working directory is reset to this location. If code changes the directory via `os.chdir()` or `%cd`, the change is undone before the next execution and the kernel prints a `[ipybox] cwd reset to ` message. # Sandbox Mode Freeact can restrict filesystem and network access for [code execution](https://gradion-ai.github.io/freeact/execution/index.md) and MCP servers using [ipybox sandbox](https://gradion-ai.github.io/ipybox/sandbox/) and Anthropic's [sandbox-runtime](https://github.com/anthropic-experimental/sandbox-runtime). Prerequisites Check the installation instructions for [sandbox mode prerequisites](https://gradion-ai.github.io/freeact/installation/#sandbox-mode-prerequisites). ## Code Execution Scope Sandbox restrictions apply equally to Python code and shell commands, as both [execute](https://gradion-ai.github.io/freeact/execution/index.md) in the same IPython kernel. ### CLI Tool The `--sandbox` option enables sandboxed [code execution](https://gradion-ai.github.io/freeact/execution/index.md): ``` freeact --sandbox ``` A custom configuration file can override the [default restrictions](#default-restrictions): ``` freeact --sandbox --sandbox-config sandbox-config.json ``` ### Agent SDK The `sandbox` and `sandbox_config` parameters of the Agent constructor provide the same functionality: ``` from pathlib import Path agent = Agent( ... sandbox=True, sandbox_config=Path("sandbox-config.json"), ) ``` ### Default Restrictions Without a custom configuration file, sandbox mode applies these defaults: - **Filesystem**: Read all files except `.env`, write to current directory and subdirectories - **Network**: Internet access blocked, local network access to tool execution server permitted ### Custom Configuration sandbox-config.json ``` { "allowPty": true, "network": { "allowedDomains": ["example.org"], "deniedDomains": [], "allowLocalBinding": true }, "filesystem": { "denyRead": ["sandbox-config.json"], "allowWrite": [".", "~/Library/Jupyter/", "~/.ipython/"], "denyWrite": ["sandbox-config.json"] } } ``` This macOS-specific example configuration allows additional network access to `example.org`. Filesystem settings permit writes to `~/Library/Jupyter/` and `~/.ipython/`, which is required for running a sandboxed IPython kernel. The sandbox configuration file itself is protected from reads and writes. ## MCP Servers MCP servers run as separate processes and are not affected by [code execution sandboxing](#code-execution). Local stdio servers can be sandboxed independently by wrapping the server command with the `srt` tool from sandbox-runtime. This applies to both [`mcp_servers`](https://gradion-ai.github.io/freeact/configuration/#mcp_servers) and [`ptc_servers`](https://gradion-ai.github.io/freeact/configuration/#ptc_servers) in the [configuration file](https://gradion-ai.github.io/freeact/configuration/#configuration-file). ### Fetch MCP Server This example shows a sandboxed [fetch MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/fetch). First, install it locally with: ``` uv add mcp-server-fetch uv add "httpx[socks]>=0.28.1" ``` Then add it to the `ptc_servers` section: .freeact/agent.json ``` { "ptc_servers": { "fetch": { "command": "srt", "args": [ "--settings", "sandbox-fetch-mcp.json", "python", "-m", "mcp_server_fetch" ] } } } ``` The sandbox configuration blocks `.env` reads and restricts the MCP server to fetch only from `example.com`. Access to the npm registry is required for the server's internal operations: sandbox-fetch-mcp.json ``` { "filesystem": { "denyRead": [".env"], "allowWrite": [".", "~/.npm", "/tmp/**", "/private/tmp/**"], "denyWrite": [] }, "network": { "allowedDomains": ["registry.npmjs.org", "example.com"], "deniedDomains": [], "allowLocalBinding": true } } ``` # Models Freeact supports any model compatible with [Pydantic AI](https://ai.pydantic.dev/models/). The model is configured in [`.freeact/agent.json`](https://gradion-ai.github.io/freeact/configuration/#configuration-file) through three settings: | Setting | Required | Description | | ------------------- | -------- | ------------------------------------------------------------------------------- | | `model` | yes | Model identifier in `provider:model-name` format | | `model_settings` | no | Model behavior settings (for example thinking settings or `temperature`) | | `provider_settings` | no | Provider options (for example `api_key`, `base_url`, `app_url`, or `app_title`) | ## Model Identifier The `model` field uses Pydantic AI's `provider:model-name` format. Common providers: | Provider | Prefix | Example | | ------------------- | ---------------- | ---------------------------------------- | | Google (Gemini API) | `google-gla:` | `google-gla:gemini-3.5-flash` | | Google (Vertex AI) | `google-vertex:` | `google-vertex:gemini-3.5-flash` | | Anthropic | `anthropic:` | `anthropic:claude-sonnet-4-6` | | OpenAI | `openai:` | `openai:gpt-5.2` | | OpenRouter | `openrouter:` | `openrouter:anthropic/claude-sonnet-4.6` | See Pydantic AI's [model documentation](https://ai.pydantic.dev/models/) for the full list of supported providers and model names. ## Provider Examples ### Google (default) The default configuration uses Google's Gemini API with dynamic thinking enabled: ``` { "model": "google-gla:gemini-3.5-flash", "model_settings": { "google_thinking_config": { "thinking_level": "medium", "include_thoughts": true } } } ``` Set the `GEMINI_API_KEY` environment variable to authenticate. ### Anthropic ``` { "model": "anthropic:claude-sonnet-4-6", "model_settings": { "anthropic_thinking": { "type": "adaptive" } } } ``` Set the `ANTHROPIC_API_KEY` environment variable to authenticate. ### OpenAI ``` { "model": "openai:gpt-5.2", "model_settings": { "openai_reasoning_effort": "medium" } } ``` Set the `OPENAI_API_KEY` environment variable to authenticate. ### OpenRouter For providers like OpenRouter, put provider-specific options in `provider_settings` (for example `api_key`, `app_url`, and `app_title`): ``` { "model": "openrouter:anthropic/claude-sonnet-4.6", "model_settings": { "anthropic_thinking": { "type": "adaptive" } }, "provider_settings": { "api_key": "${OPENROUTER_API_KEY}", "app_url": "https://my-app.example.com", "app_title": "freeact" } } ``` ### OpenAI-Compatible Endpoints Any OpenAI-compatible API can be used by setting `base_url` in `provider_settings`: ``` { "model": "openai:my-custom-model", "model_settings": { "temperature": 0.7 }, "provider_settings": { "base_url": "https://my-api.example.com/v1", "api_key": "${CUSTOM_API_KEY}" } } ``` ## Model Settings `model_settings` is passed directly to Pydantic AI's model request. Available settings depend on the provider. ### Extended Thinking Freeact streams thinking content when the model supports it. Thinking is configured through provider-specific settings in `model_settings`. **Google (Gemini)**: ``` "model_settings": { "google_thinking_config": { "thinking_level": "medium", "include_thoughts": true } } ``` `thinking_level` accepts `"minimal"`, `"low"`, `"medium"`, or `"high"`. Set `include_thoughts` to `true` to stream thinking content. **Anthropic** (Opus 4.6, Sonnet 4.6): ``` "model_settings": { "anthropic_thinking": { "type": "adaptive" }, "anthropic_effort": "high" } ``` Adaptive thinking lets the model decide when and how much to think. `anthropic_effort` accepts `"low"`, `"medium"`, `"high"`, or `"max"` (Opus only). The default is `"high"`. **OpenAI**: ``` "model_settings": { "openai_reasoning_effort": "medium" } ``` `openai_reasoning_effort` accepts `"low"`, `"medium"`, or `"high"`. ### Common Settings | Setting | Description | | ------------- | --------------------------------- | | `temperature` | Controls randomness (e.g., `0.7`) | | `max_tokens` | Maximum response tokens | See Pydantic AI's [settings documentation](https://ai.pydantic.dev/api/settings/) for the full reference. ## Provider Settings Use `provider_settings` for provider-specific options such as `api_key`, `base_url`, `app_url`, or `app_title`. # Agent SDK The Agent SDK provides four main APIs: - [Configuration API](#configuration-api) for initializing and loading configuration from `.freeact/` - [Generation API](#generation-api) for generating Python APIs for MCP server tools - [Agent API](#agent-api) for running the agentic code action loop - [Permissions API](#permissions-api) for managing approval decisions ## Configuration API Use Config.init() to load persisted config from `.freeact/` when present, or create and save defaults on first run. Use save() and load() when explicit persistence control is needed: ``` from freeact.agent.config import Config config = await Config.init() ``` See the [Configuration](https://gradion-ai.github.io/freeact/configuration/index.md) reference for details on the `.freeact/` directory structure. ## Generation API MCP servers [configured](https://gradion-ai.github.io/freeact/configuration/#ptc_servers) as `ptc_servers` in `agent.json` require Python API generation with generate_mcp_sources() before the agent can call their tools programmatically: ``` from freeact.tools.pytools.apigen import generate_mcp_sources # Generate Python APIs for MCP servers in ptc_servers for server_name, params in config.ptc_servers.items(): if not (config.generated_dir / "mcptools" / server_name).exists(): await generate_mcp_sources({server_name: params}, config.generated_dir) ``` Generated APIs are stored as `.freeact/generated/mcptools//.py` modules and persist across agent sessions. The `.freeact/generated/` directory is on the kernel's `PYTHONPATH`, so the agent can import them directly: ``` from mcptools.google.web_search import run, Params result = run(Params(query="python async tutorial")) ``` ## Agent API The Agent class implements the agentic code action loop, handling code action generation, [code execution](https://gradion-ai.github.io/freeact/execution/index.md), tool calls, and the approval workflow. Each stream() call runs a single agent turn, with the agent managing conversation history across calls. Use `stream()` to iterate over [events](#events) and handle them with pattern matching: ``` from freeact.agent import ( Agent, ApprovalRequest, CodeAction, CodeExecutionOutput, Response, ShellAction, Thoughts, ToolOutput, ) async with Agent(config=config) as agent: prompt = "Who is the F1 world champion 2025?" async for event in agent.stream(prompt): match event: case ApprovalRequest(tool_call=CodeAction(code=code)) as request: print(f"Code action:\n{code}") request.approve(True) case ApprovalRequest(tool_call=ShellAction(command=cmd)) as request: print(f"Shell command: {cmd}") request.approve(True) case ApprovalRequest(tool_call=tool_call) as request: print(f"Tool: {tool_call.tool_name}") request.approve(True) case Thoughts(content=content): print(f"Thinking: {content}") case CodeExecutionOutput(text=text): print(f"Code execution output: {text}") case ToolOutput(content=content): print(f"Tool call result: {content}") case Response(content=content): print(content) ``` For processing output incrementally, match the `*Chunk` event variants listed below. ### Events The Agent.stream() method 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`. ### 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 tools calls intercepted at runtime for approval | | subagent | [`subagent_task`](#subagents) | Task delegation to child agents | | tool search | `pytools` MCP server for basic search and hybrid search | Tool discovery via category browsing or hybrid search | ### 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. ### Cancellation Call cancel() to stop a running agent turn. The active `stream()` stops at the next phase boundary and yields a Cancelled event. 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) ``` ### 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](#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`](https://gradion-ai.github.io/freeact/configuration/#agent-settings) setting in `agent.json` limits concurrent subagents (default 5). ### Approval The agent yields ApprovalRequest for code actions and each shell command and programmatic tool call within them. Each request carries a tool_call field identifying the pending action. Execution is suspended until `approve()` is called. Calling `approve(True)` executes the action; `approve(False)` rejects it and ends the current agent turn. ``` async for event in agent.stream(prompt): match event: case ApprovalRequest(tool_call=CodeAction(code=code)) as request: print(f"Code action:\n{code}") request.approve(True) case ApprovalRequest(tool_call=ShellAction(command=cmd)) as request: print(f"Shell command: {cmd}") request.approve(True) case ApprovalRequest(tool_call=GenericCall(tool_name=name, ptc=True)) as request: print(f"Programmatic tool call: {name}") request.approve(True) case ApprovalRequest(tool_call=GenericCall(tool_name=name)) as request: print(f"JSON tool call: {name}") request.approve(True) case Response(content=content): print(content) ``` The `tool_call` type determines what is being approved: | `tool_call` type | Trigger | | ----------------------------- | ----------------------------------------------------------------------------------- | | CodeAction | Code action containing Python code and shell commands to execute | | ShellAction | Shell command (`!cmd`) intercepted during code action execution | | GenericCall | Programmatic tool call (intercepted during code action execution) or JSON tool call | | FileRead, FileWrite, FileEdit | Filesystem operation via built-in MCP server | Shell commands and programmatic tool calls within code actions are intercepted during execution and yield separate `ApprovalRequest` events. Composite shell commands (using `&&`, `||`, `|`, `;`) are decomposed into individual sub-commands, each requiring separate approval. Python variables in shell commands are resolved before the approval request. ### Lifecycle The agent manages MCP server connections and an IPython kernel via [ipybox](https://gradion-ai.github.io/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. ``` config = await Config.init() async with Agent(config=config) as agent: async for event in agent.stream(prompt): ... # Connections closed, kernel stopped ``` Without using the async context manager: ``` config = await Config.init() agent = Agent(config=config) await agent.start() try: async for event in agent.stream(prompt): ... finally: await agent.stop() ``` ### Timeouts The agent supports two timeout settings in [`agent.json`](https://gradion-ai.github.io/freeact/configuration/#agent-settings): - `execution_timeout`: Maximum time in seconds for each [code execution](https://gradion-ai.github.io/freeact/execution/index.md). Approval wait time is excluded from this budget, so the timeout only counts actual execution time. Defaults to 300 seconds. Set to `null` to disable. - `approval_timeout`: Timeout for approval requests during programmatic tool calls. If an approval request is not accepted or rejected within this time, the tool call fails. Defaults to `null` (no timeout). ``` { "execution_timeout": 60, "approval_timeout": 30 } ``` ### Persistence #### Sessions Config controls session persistence via `enable_persistence`. - Default: `true`. The agent persists history to `.freeact/sessions//.jsonl`. - `false`: The agent keeps history in memory only. Passing `session_id` to Agent raises `ValueError`. When persistence is enabled, construct an agent without `session_id` to create a new session ID internally. Read it from `agent.session_id`: ``` # No session_id: agent creates a new session ID internally. async with Agent(config=config) as agent: print(f"Generated session ID: {agent.session_id}") await handle_events(agent, "What is the capital of France?") await handle_events(agent, "What about Germany?") ``` Construct an agent with an explicit `session_id` for create-or-resume behavior: ``` # Choose an explicit session ID. session_id = "countries-session" # Create-or-resume behavior: resume if present, otherwise start new. async with Agent(config=config, session_id=session_id) as agent: await handle_events(agent, "What is the capital of Spain?") ``` If that `session_id` already exists, the persisted history is resumed. If it does not exist, a new session starts with that ID. To resume later, create another agent with the same `session_id`: ``` # Resume the same session ID later. async with Agent(config=config, session_id=session_id) as agent: # Previous message history is restored automatically await handle_events(agent, "And what country did we discuss in this session?") ``` Only the main agent's message history (`main.jsonl`) is loaded on resume. Subagent messages are persisted to separate files (`sub-xxxx.jsonl`) for auditing but are not rehydrated. The [CLI tool](https://gradion-ai.github.io/freeact/cli/index.md) accepts `--session-id` to resume a session from the command line when `enable_persistence` is `true`. #### Results Tool call results, code execution outputs, and subagent responses are checked against an inline size threshold before being added to the message history. When a result exceeds the threshold, the full content is saved to disk and replaced inline with a file reference notice that includes a preview. This prevents large outputs from bloating context. Controlled by two config options: - `tool_result_inline_max_bytes`: Maximum inline payload size in bytes. - `tool_result_preview_chars`: Number of preview characters from both the beginning and end of large text results included in the file reference notice. ### Prompt tags Prompts passed to `stream()` may contain skill tags that the agent processes. Skill tags explicitly invoke a skill by name. The [CLI tool](https://gradion-ai.github.io/freeact/cli/#skill-invocation) generates these from `/skill-name` syntax. ``` the auth module ``` Without an explicit tag, the agent can still autonomously select a skill when the request matches a skill's description. Skills are discovered from `.freeact/skills/` and `.agents/skills/` directories. ## Permissions API The agent does not enforce permissions itself. It yields [`ApprovalRequest`](#approval) events and leaves the decision to the application. PermissionManager is an optional utility that applications can use to automate those decisions based on stored rules. The [CLI tool](https://gradion-ai.github.io/freeact/cli/#approval-prompt) uses it internally; SDK applications can use it the same way. The manager loads and saves rules from `.freeact/permissions.json`. Rules are glob-style [patterns](https://gradion-ai.github.io/freeact/configuration/#permissions) organized into two tiers (allow and ask) and two persistence scopes (always and session). See [Permissions](https://gradion-ai.github.io/freeact/configuration/#permissions) for the file format, pattern syntax, and matching semantics. The core API: - init() loads rules from `.freeact/permissions.json` if present, otherwise saves defaults. - is_allowed(tool_call) checks a concrete tool call (literal values, no wildcards) against stored rules. - allow_always(tool_call) adds a rule and persists it to `permissions.json`. The tool call fields may contain glob wildcards to match broadly (e.g., `command="git *"`). - allow_session(tool_call) adds an in-memory rule for the current session. Same wildcard support as `allow_always`. ``` import asyncio from freeact.permissions import PermissionManager from freeact.agent import ApprovalRequest, suggest_pattern loop = asyncio.get_running_loop() manager = PermissionManager() await loop.run_in_executor(None, manager.init) async for event in agent.stream(prompt): match event: case ApprovalRequest() as request: if manager.is_allowed(request.tool_call): request.approve(True) else: pattern = suggest_pattern(request.tool_call) choice = input(f"Allow [{pattern}]? [Y/n/a/s]: ") match choice: case "a": await loop.run_in_executor( None, manager.allow_always, request.tool_call, ) request.approve(True) case "s": manager.allow_session(request.tool_call) request.approve(True) case "n": request.approve(False) case _: request.approve(True) ``` For shell commands, suggest_display(tool_call) returns the verbatim command (or a first-line summary for `%%bash` shell magic) and is suitable for displaying in the prompt instead of the permission pattern. It returns an empty string for non-shell tool calls, so applications can fall back to `suggest_pattern()` in that case. # CLI tool The `freeact` or `freeact run` command starts the [interactive mode](#interactive-mode): ``` freeact ``` A `.freeact/` [configuration](https://gradion-ai.github.io/freeact/configuration/index.md) directory is created automatically if it does not exist yet. The `init` subcommand initializes the configuration directory without starting the interactive mode: ``` freeact init ``` ## Options | Option | Description | | ----------------------- | -------------------------------------------------------------------------------------------- | | `--sandbox` | Run code execution in [sandbox mode](https://gradion-ai.github.io/freeact/sandbox/index.md). | | `--sandbox-config PATH` | Path to sandbox configuration file. | | `--session-id UUID` | Resume a previous session by its UUID. | | `--skip-permissions` | Run tools without prompting for approval. | | `--log-level LEVEL` | Set logging level: `debug`, `info` (default), `warning`, `error`, `critical`. | ## Examples Running code execution in [sandbox mode](https://gradion-ai.github.io/freeact/sandbox/index.md): ``` freeact --sandbox ``` Running with a [custom sandbox configuration](https://gradion-ai.github.io/freeact/sandbox/#custom-configuration): ``` freeact --sandbox --sandbox-config sandbox-config.json ``` Resuming a previous [session](https://gradion-ai.github.io/freeact/sdk/#persistence): ``` freeact --session-id 550e8400-e29b-41d4-a716-446655440000 ``` If `enable_persistence` is `false` in `.freeact/agent.json`, passing `--session-id` exits with an error. ## Interactive Mode The interactive mode provides a conversation interface with the agent in a terminal window. ### User messages | Key | Action | | -------------------------------------------------- | ------------------------------------------------------- | | `Enter` | Send message | | `Ctrl+J` | Insert newline | | `Option+Enter` (macOS) `Alt+Enter` (Linux/Windows) | Insert newline (`Ctrl+J` fallback) | | `Escape` | Cancel the current agent turn, or clear input when idle | | `Ctrl+Q` | Quit | ### Clipboard Clipboard behavior depends on terminal key forwarding. - Paste into the prompt input: `Cmd+V` or `Ctrl+V`. - Copy selected text from Freeact widgets: `Cmd+C` may not work in some terminals. Use `Ctrl+C` instead. - Additional terminal fallbacks: `Ctrl+Shift+C` / `Ctrl+Insert` for copy, `Ctrl+Shift+V` / `Shift+Insert` for paste. ### Expand and Collapse Use `Ctrl+O` to toggle all collapsible boxes between expanded and configured state. The shortcut is configured in `.freeact/terminal.json` under `expand_all_toggle_key`. ### File References Include file paths directly in the prompt. The model uses built-in [filesystem tools](https://gradion-ai.github.io/freeact/sdk/#internal-tools) to read them when needed. ``` screenshot.png What does this show? Transcribe this voice-note.wav images/ Describe these images ``` Type `@` in the prompt to open a file picker for easier path insertion. The `@` prefix is stripped before sending, so `@screenshot.png` and `screenshot.png` are equivalent. ### Skill Invocation The agent automatically uses skills when a request matches a skill's description. The `/skill-name` syntax is a shortcut to invoke a specific skill explicitly: ``` /plan my project requirements /commit fix login bug ``` - Type `/` at the start of a prompt to open a skill picker. - Select a skill to insert its name, then type arguments after it. - Text after the skill name is passed as arguments to the skill. - Skill locations: `.agents/skills/` (project) and `.freeact/skills/` (bundled). ### Cancellation Press `Escape` during an active agent turn to cancel it. This interrupts the current operation (LLM streaming, code execution, or approval wait), stops the turn, and re-enables the prompt input. ### Approval Prompt Before executing code actions, shell commands, and tool calls, the agent requests approval. Shell commands and programmatic tool calls within code actions are intercepted during execution and approved individually. For shell commands the prompt displays the verbatim command being executed; for other action types it displays a suggested permission pattern that summarizes the action: ``` Approve? [Y/n/a/s] git add src/main.py ``` | Key | Action | | ------------- | --------------------------------------------- | | `y` / `Enter` | Approve this invocation only | | `n` | Reject (ends the current agent turn) | | `a` | Edit pattern, then save as always-allow rule | | `s` | Edit pattern, then save as session-allow rule | Pressing `a` or `s` opens an editable input pre-filled with the suggested permission pattern (not the verbatim text shown above). Edit the pattern to broaden or narrow the rule (e.g. change `filesystem_read_file src/main.py` to `filesystem_* src/**`), then press `Enter` to save the rule and approve. While editing, approval hotkeys are disabled so you can type freely. Always-allow rules persist to `.freeact/permissions.json` across sessions. Session-allow rules are in-memory and cleared when the session ends. Future actions matching a saved rule are auto-approved without prompting. What the bar displays depends on the action type: | Action | Bar shows | Suggested pattern (when pressing `a`/`s`) | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------ | | Shell command (`!cmd` and split sub-commands) | Verbatim command, e.g. `git add src/main.py` | `cmd subcmd *` heuristic, e.g. `git add *` | | Shell magic (`%%bash`) | First non-empty line + `(+N more lines)` summary; full script is shown in the Shell Script box above the bar | Full script with newlines escaped as `\n` | | Code action | Tool name, e.g. `ipybox_execute_ipython_cell` | Same | | File read/write/edit | Tool name + path, e.g. `filesystem_write_text_file src/main.py` | Same | | Other tool calls | Tool name, e.g. `github_search_repositories` | Same | See [Permissions](https://gradion-ai.github.io/freeact/configuration/#permissions) for the persisted format and pattern syntax. Automatic approval Use the [`--skip-permissions`](#options) CLI option to run the agent with full action auto-approval. # API Reference ## freeact.agent.Agent ``` Agent( config: Config, agent_id: str | None = None, session_id: str | None = None, sandbox: bool = False, sandbox_config: Path | None = None, ) ``` Code action agent that executes Python code and shell commands. Fulfills user requests by writing code and running it in a stateful IPython kernel provided by ipybox. Variables persist across executions. MCP server tools can be called in two ways: - JSON tool calls: MCP servers called directly via structured arguments - Programmatic tool calls (PTC): agent writes Python code that imports and calls tool APIs, auto-generated from MCP schemas (`mcptools/`) or user-defined (`gentools/`) All code actions and tool calls require approval. The `stream()` method yields ApprovalRequest events that must be resolved before execution proceeds. Use as an async context manager or call `start()`/`stop()` explicitly. Initialize the agent. Parameters: | Name | Type | Description | Default | | ---------------- | -------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `config` | `Config` | Agent configuration containing model, system prompt, MCP servers, kernel env, timeouts, and subagent settings. | *required* | | `agent_id` | \`str | None\` | Identifier for this agent instance. Defaults to "main" when not provided. | | `session_id` | \`str | None\` | Optional session identifier for persistence. If None and persistence is enabled, a new session ID is generated. If provided and persistence is enabled, that session ID is used. Existing session history is resumed when present; otherwise a new session starts with that ID. | | `sandbox` | `bool` | Run the kernel in sandbox mode. | `False` | | `sandbox_config` | \`Path | None\` | Path to custom sandbox configuration. | Raises: | Type | Description | | ------------ | ------------------------------------------------------------------- | | `ValueError` | If session_id is provided while config.enable_persistence is False. | ### config ``` config: Config ``` Agent configuration. ### session_id ``` session_id: str | None ``` Session ID used by this agent, or `None` when persistence is disabled. ### \_reject_ipybox_approval ``` _reject_ipybox_approval(item: ApprovalRequest) -> None ``` Reject an ipybox approval request, suppressing errors. Called during generator cleanup (GeneratorExit) to ensure the approval channel on the tool server is unblocked before the ApprovalClient disconnects. ### cancel ``` cancel() -> None ``` Cancel the current agent turn. Sets an internal cancellation flag and interrupts any running kernel execution. The active `stream()` call will stop at the next phase boundary and yield a Cancelled event. ### start ``` start() -> None ``` Restore persisted history, start the code executor and MCP servers. Automatically called when entering the async context manager. ### stop ``` stop() -> None ``` Stop the code executor and MCP servers. Automatically called when exiting the async context manager. ### stream ``` stream( prompt: str | Sequence[UserContent], max_turns: int | None = None, ) -> AsyncIterator[AgentEvent] ``` Run a single agent turn, yielding events as they occur. Loops through model responses and tool executions until the model produces a response without tool calls. All code actions and tool calls yield an ApprovalRequest that must be resolved before execution proceeds. Parameters: | Name | Type | Description | Default | | ----------- | ----- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `prompt` | \`str | Sequence[UserContent]\` | User message as text or multimodal content sequence. | | `max_turns` | \`int | None\` | Maximum number of tool-execution rounds. Each round consists of a model response followed by tool execution. If None, runs until the model stops calling tools. | Returns: | Type | Description | | --------------------------- | ------------------------ | | `AsyncIterator[AgentEvent]` | An async event iterator. | ## freeact.agent.AgentEvent ``` AgentEvent( *, agent_id: str = "", corr_id: str = "", parent_corr_id: str = "" ) ``` Base class for all agent stream events. Carries the `agent_id` of the agent that produced the event, allowing callers to distinguish events from a parent agent vs. its subagents. ## freeact.agent.Response ``` Response( content: str, *, agent_id: str = "", corr_id: str = "", parent_corr_id: str = "" ) ``` Bases: `AgentEvent` Complete model response at a given step. ## freeact.agent.ResponseChunk ``` ResponseChunk( content: str, *, agent_id: str = "", corr_id: str = "", parent_corr_id: str = "" ) ``` Bases: `AgentEvent` Partial model response text (content streaming). ## freeact.agent.Thoughts ``` Thoughts( content: str, *, agent_id: str = "", corr_id: str = "", parent_corr_id: str = "" ) ``` Bases: `AgentEvent` Complete model thoughts at a given step. ## freeact.agent.ThoughtsChunk ``` ThoughtsChunk( content: str, *, agent_id: str = "", corr_id: str = "", parent_corr_id: str = "" ) ``` Bases: `AgentEvent` Partial model thinking text (content streaming). ## freeact.agent.CodeExecutionOutput ``` CodeExecutionOutput( text: str | None, images: list[Path], truncated: bool = False, *, agent_id: str = "", corr_id: str = "", parent_corr_id: str = "" ) ``` Bases: `AgentEvent` Complete code execution output. ## freeact.agent.CodeExecutionOutputChunk ``` CodeExecutionOutputChunk( text: str, *, agent_id: str = "", corr_id: str = "", parent_corr_id: str = "" ) ``` Bases: `AgentEvent` Partial code execution output (content streaming). ## freeact.agent.ApprovalRequest ``` ApprovalRequest( tool_call: ToolCall, _future: Future[bool] = Future(), *, agent_id: str = "", corr_id: str = "", parent_corr_id: str = "" ) ``` Bases: `AgentEvent` Pending code action or tool call awaiting user approval. Yielded by Agent.stream() before executing any code action, programmatic tool call, or JSON tool call. The stream is suspended until `approve()` is called. ### approve ``` approve(decision: bool) -> None ``` Resolve this approval request. No-op if already resolved (e.g. by cancellation). Parameters: | Name | Type | Description | Default | | ---------- | ------ | ---------------------------------------------------------------- | ---------- | | `decision` | `bool` | True to execute, False to reject and end the current agent turn. | *required* | ### approved ``` approved() -> bool ``` Await until `approve()` is called and return the decision. ## freeact.agent.ToolOutput ``` ToolOutput( content: ToolResult, *, agent_id: str = "", corr_id: str = "", parent_corr_id: str = "" ) ``` Bases: `AgentEvent` JSON tool call or built-in operation output. ## freeact.agent.Cancelled ``` Cancelled( *, agent_id: str = "", corr_id: str = "", parent_corr_id: str = "", phase: Literal[ "between_turns", "llm_streaming", "tool_execution" ] ) ``` Bases: `AgentEvent` Agent execution was cancelled by the user. ## freeact.agent.ToolCall ``` ToolCall(tool_name: str) ``` Base class for typed tool call representations. ### from_pattern ``` from_pattern(pattern: str) -> ToolCall ``` Reconstruct a ToolCall from a user-edited pattern string. ### from_raw ``` from_raw( tool_name: str, tool_args: dict[str, Any] ) -> ToolCall ``` Construct the appropriate ToolCall subclass from raw API data. Parameters: | Name | Type | Description | Default | | ----------- | ---------------- | -------------------------------------------- | ---------- | | `tool_name` | `str` | Tool identifier from the agent event stream. | *required* | | `tool_args` | `dict[str, Any]` | Raw tool argument payload. | *required* | Returns: | Type | Description | | ---------- | ------------------------ | | `ToolCall` | Typed ToolCall instance. | ### matches_entry ``` matches_entry( entry: dict[str, Any], working_dir: Path ) -> bool ``` Check if a permission entry matches this tool call. ### to_display ``` to_display() -> str ``` Return display text for the approval bar. Empty string means "fall back to the suggested pattern". Subclasses override this to surface the verbatim action being approved (e.g. a shell command) instead of the permission pattern. ### to_entry ``` to_entry() -> dict[str, Any] ``` Serialize this tool call's pattern-relevant fields to a dict entry. ### to_pattern ``` to_pattern() -> str ``` Suggest a permission pattern string for this tool call. ## freeact.agent.GenericCall ``` GenericCall( tool_name: str, tool_args: dict[str, Any], ptc: bool = False, ) ``` Bases: `ToolCall` Fallback for tool calls without specialized handling. ## freeact.agent.ShellAction ``` ShellAction(tool_name: str, command: str) ``` Bases: `ToolCall` Shell command extracted from a code cell. ## freeact.agent.CodeAction ``` CodeAction(tool_name: str, code: str) ``` Bases: `ToolCall` Code execution action. ## freeact.agent.FileRead ``` FileRead( tool_name: str, path: str, offset: int | None, limit: int | None, ) ``` Bases: `ToolCall` File read action. ## freeact.agent.FileWrite ``` FileWrite(tool_name: str, path: str, content: str) ``` Bases: `ToolCall` File write action. ## freeact.agent.FileEdit ``` FileEdit( tool_name: str, path: str, old_text: str, new_text: str ) ``` Bases: `ToolCall` File edit action. ## freeact.config.PersistentConfig Bases: `BaseModel` Base class for JSON-persisted configuration models. Subclasses set `_config_filename` to their JSON filename and override `model_post_init` / `_save_sync` for domain-specific logic. Config: - `extra`: `forbid` - `validate_assignment`: `True` - `frozen`: `True` Fields: - `working_dir` (`Path`) ### init ``` init(working_dir: Path | None = None) -> Self ``` Load config when present, otherwise save defaults. ### load ``` load(working_dir: Path | None = None) -> Self ``` Load persisted config if present, otherwise return defaults. ### save ``` save() -> None ``` Persist config to the `.freeact/` directory. ## freeact.agent.config.Config Bases: `PersistentConfig` Agent configuration. Config: - `arbitrary_types_allowed`: `True` Fields: - `working_dir` (`Path`) - `model` (`str | Model`) - `model_settings` (`dict[str, Any]`) - `provider_settings` (`dict[str, Any] | None`) - `tool_search` (`Literal['basic', 'hybrid']`) - `images_dir` (`Path | None`) - `execution_timeout` (`float | None`) - `approval_timeout` (`float | None`) - `tool_result_inline_max_bytes` (`int`) - `tool_result_preview_chars` (`int`) - `enable_persistence` (`bool`) - `enable_subagents` (`bool`) - `max_subagents` (`int`) - `kernel_env` (`dict[str, str]`) - `mcp_servers` (`dict[str, dict[str, Any]]`) - `ptc_servers` (`dict[str, dict[str, Any]]`) ## freeact.agent.config.SkillMetadata Bases: `BaseModel` Metadata parsed from a skill's SKILL.md frontmatter. Config: - `frozen`: `True` Fields: - `name` (`str`) - `description` (`str`) - `path` (`Path`) ## freeact.agent.config.DEFAULT_MODEL_NAME ``` DEFAULT_MODEL_NAME = 'google-gla:gemini-3.5-flash' ``` ## freeact.agent.config.DEFAULT_MODEL_SETTINGS ``` DEFAULT_MODEL_SETTINGS: dict[str, Any] = { "google_thinking_config": { "thinking_level": "medium", "include_thoughts": True, } } ``` ## freeact.agent.config.BASIC_SEARCH_MCP_SERVER_CONFIG ``` BASIC_SEARCH_MCP_SERVER_CONFIG: dict[str, Any] = { "command": "python", "args": ["-m", "freeact.tools.pytools.search.basic"], "env": {"PYTOOLS_DIR": "${PYTOOLS_DIR}"}, } ``` ## freeact.agent.config.HYBRID_SEARCH_MCP_SERVER_CONFIG ``` HYBRID_SEARCH_MCP_SERVER_CONFIG: dict[str, Any] = { "command": "python", "args": ["-m", "freeact.tools.pytools.search.hybrid"], "env": { "GEMINI_API_KEY": "${GEMINI_API_KEY}", "PYTOOLS_DIR": "${PYTOOLS_DIR}", "PYTOOLS_DB_PATH": "${PYTOOLS_DB_PATH}", "PYTOOLS_EMBEDDING_MODEL": "${PYTOOLS_EMBEDDING_MODEL}", "PYTOOLS_EMBEDDING_DIM": "${PYTOOLS_EMBEDDING_DIM}", "PYTOOLS_SYNC": "${PYTOOLS_SYNC}", "PYTOOLS_WATCH": "${PYTOOLS_WATCH}", "PYTOOLS_BM25_WEIGHT": "${PYTOOLS_BM25_WEIGHT}", "PYTOOLS_VEC_WEIGHT": "${PYTOOLS_VEC_WEIGHT}", }, } ``` ## freeact.agent.config.GOOGLE_SEARCH_MCP_SERVER_CONFIG ``` GOOGLE_SEARCH_MCP_SERVER_CONFIG: dict[str, Any] = { "command": "python", "args": [ "-m", "freeact.tools.gsearch", "--thinking-level", "medium", ], "env": {"GEMINI_API_KEY": "${GEMINI_API_KEY}"}, } ``` ## freeact.agent.config.FILESYSTEM_MCP_SERVER_CONFIG ``` FILESYSTEM_MCP_SERVER_CONFIG: dict[str, Any] = { "command": "python", "args": ["-m", "freeact.tools.filesystem"], } ``` ## freeact.agent.config.FETCH_MCP_SERVER_CONFIG ``` FETCH_MCP_SERVER_CONFIG: dict[str, Any] = { "command": "python", "args": ["-m", "freeact.tools.fetch"], "env": {}, } ``` ## freeact.tools.pytools.apigen.generate_mcp_sources ``` generate_mcp_sources( config: dict[str, dict[str, Any]], generated_dir: Path ) -> None ``` Generate Python API for MCP servers in `config`. For servers not already in `mcptools/` categories, generates Python API using `mcpygen.generate_mcp_sources`. Parameters: | Name | Type | Description | Default | | --------------- | --------------------------- | --------------------------------------------------------- | ---------- | | `config` | `dict[str, dict[str, Any]]` | Dictionary mapping server names to server configurations. | *required* | | `generated_dir` | `Path` | Directory for generated tool sources. | *required* | ## freeact.permissions.PermissionManager ``` PermissionManager( working_dir: Path | None = None, freeact_dir: Path = Path(".freeact"), ) ``` Tool call permission gating with type-specific pattern rules. Rules are `ToolCall` instances whose fields may contain glob wildcards (`*`, `?`). Path fields (`path`, `paths`) use path-aware matching where `*` matches within a single directory and `**` matches across directory boundaries. Non-path fields (`tool_name`, `command`) use simple glob matching. Use allow_always and allow_session to store pattern rules. Use is_allowed to check concrete (no wildcards) tool calls against stored rules. Evaluation order: ask-session, ask-always, allow-session, allow-always. First match wins. Ask takes priority over allow. ### allow_always ``` allow_always(tool_call: ToolCall) -> None ``` Add a pattern rule to the always-allow list and persist. The tool call's fields may contain glob wildcards. For example, `ShellAction(tool_name="bash", command="git *")` allows all git subcommands, and `FileRead(tool_name="filesystem_*", paths=("src/**",))` allows reading any file under `src/`. ### allow_session ``` allow_session(tool_call: ToolCall) -> None ``` Add a pattern rule to the session-allow list (not persisted). The tool call's fields may contain glob wildcards, same as allow_always. Session rules are cleared when the process ends. ### init ``` init() -> None ``` Load permissions when present, otherwise save defaults. ### is_allowed ``` is_allowed(tool_call: ToolCall) -> bool ``` Check if a concrete tool call is pre-approved. The tool call should contain literal values (no wildcards). Its fields are matched against the glob patterns in stored rules. Parameters: | Name | Type | Description | Default | | ----------- | ---------- | ---------------------------- | ---------- | | `tool_call` | `ToolCall` | Concrete tool call to check. | *required* | Returns: | Type | Description | | ------ | --------------------------------------------------- | | `bool` | True if an allow rule matches and no ask rule takes | | `bool` | precedence, False otherwise. | ### load ``` load() -> None ``` Load permissions from `.freeact/permissions.json`. ### save ``` save() -> None ``` Persist always-tier permissions to `.freeact/permissions.json`.