codeactai-agentstool-usepython-executionopenhandstutorial

CodeAct Guide: Executable Python Code for AI Agents

CodeAct Architecture: Replacing JSON Tool Calling with Sandboxed Python Execution

The Problem: The Inefficiency and Brittleness of JSON Tool Calling

In modern enterprise automation, automated web research, and autonomous data-fetching engines like AiFetchly, deploying AI agents to interact with external software systems has historically relied on two rigid interaction paradigms: JSON-RPC Function Calling (such as OpenAI/Anthropic tool schemas) and Text-Based ReAct prompting (Action: tool_name[arg]).

While structured JSON schemas provided a necessary step away from unstructured conversational text, scaling JSON-based agents to handle real-world software engineering, multi-page data extraction, and complex analytical workflows exposes three severe architectural roadblocks:

1. The Token Tax and Latency of Multi-Turn Control Flow

JSON-RPC tools are purely declarative and static: they represent a single point-to-point function invocation with discrete argument key-value pairs. Consequently, JSON tool calling provides zero native control flow primitives—no for loops, no if/else conditionals, no exception handlers, and no variable assignments:

  • The Iterative Multiplication Tax: If an agent needs to paginate through 40 pages of search results, fetch status across 25 database shards, or inspect 30 files in a repository, a JSON-based agent must execute 40 to 50 sequential LLM round-trips.
  • Latency Compounding: In each turn, the model receives context, generates an argument object, serializes JSON, awaits external execution, deserializes the response, and re-invokes the model. At 2 to 4 seconds per forward pass, a simple batch operation consumes 2 to 3 minutes of wall-clock time.
  • Compounded Context Exhaustion: Every single intermediate tool call and result remains appended to the conversation history, rapidly filling the prompt context window and causing attention degradation.

2. Context Window Pollution and Intermediate Data Bloat

In data extraction and systems integration, tool outputs are frequently massive. When querying an enterprise REST API, scraping a modern web document, or querying a SQL database:

  • A raw response routinely yields 50,000 to 200,000 characters of unparsed JSON, HTML, or table rows.
  • In standard tool-calling setups, this raw payload is dumped directly into the agent prompt context as a tool output message.
  • However, the agent often needs only a tiny fraction of that data (e.g., extracting 3 specific pricing fields from a 50KB payload or verifying a single boolean flag).
  • Stuffing intermediate data into prompt tokens inflates inference costs by orders of magnitude and introduces context dilution, causing the model to miss crucial instructions buried in surrounding boilerplate.

3. Inflexible Tool Definitions vs. The Vast Python Ecosystem

Under traditional function calling, every tool must be predefined in advance within the API request payload:

  • If an agent requires a capability not explicitly exposed in the static schema (such as computing a fast matrix correlation, parsing an XML feed, or running a regex match), it stalls.
  • Attempting to give an agent hundreds of fine-grained API functions overloads the model’s system prompt with massive JSON schema definitions, degrading tool selection accuracy.
  • When an API call fails due to minor parameter format mismatches (e.g., passing a string instead of an integer), the model cannot run an interactive try/except block or inspect an interactive traceback. It repeatedly fails across consecutive turns.

The Architecture: CodeAct (Executable Python Code as Actions)

To liberate autonomous agents from the constraints of JSON schemas, researchers introduced CodeAct (Executable Code as Actions for Language Agents) (Wang et al., ICLR 2024 / foundations of the OpenHands framework).

CodeAct fundamentally rethinks the agent action space: instead of emitting structured JSON payloads, the agent writes and executes arbitrary, multi-line Python code directly inside an isolated, stateful REPL (Read-Eval-Print Loop) environment.

CodeAct 4-Stage Interactive Execution Pipeline

1. Python as the Universal Unified Action Space

Python is already the universal lingua franca of modern computation, data science, and scripting. Foundation models (such as Claude 3.5 Sonnet, GPT-4o, and DeepSeek-V3) are pre-trained on hundreds of billions of lines of open-source Python code. Their procedural synthesis, debugging, and algorithmic reasoning abilities in Python vastly exceed their reliability in generating arbitrary, bespoke JSON schemas.

Under CodeAct, the agent interaction cycle simplifies into an elegant conversational loop:

$$\text{Observation}{t} \longrightarrow \text{LLM Reasoning} \longrightarrow \text{Python Code Action}{t} \longrightarrow \text{REPL Execution} \longrightarrow \text{Observation}_{t+1}$$

Within a single generated code block, the agent can:

  • Execute Complex Control Flow: Write loops to iterate across paginated APIs, conditionally branch based on intermediate values, and handle exceptions locally.
  • Import Any Python Library: Dynamically leverage requests, pandas, beautifulsoup4, numpy, re, json, and domain-specific SDKs without requiring pre-registered server-side tool schemas.
  • Define Custom Abstractions: Create temporary helper functions, classes, and pipeline utilities on the fly to modularize complex subtasks.

2. Local In-Memory Data Distillation

The most profound operational advantage of CodeAct in high-throughput data extraction platforms like AiFetchly is local data distillation:

# Traditional JSON Tool Calling: # 1. Fetch 50KB JSON -> Dumped to Context # 2. Model reads 50KB -> Decides to filter -> Next turn # 3. Model requests next page -> Repeat 20 times # CodeAct: Single Turn Local In-Memory Distillation import requests import pandas as pd results = [] for page in range(1, 11): resp = requests.get(f"https://api.example.com/items?page={page}").json() # Filter 10,000 items locally in memory - DO NOT dump to LLM context! for item in resp.get("data", []): if item["price"] < 100.0 and item["in_stock"]: results.append({"id": item["id"], "price": item["price"]}) df = pd.DataFrame(results) print(f"Total matching items found: {len(df)}") print(df.head(5).to_string())

In the CodeAct paradigm, 500KB of raw network payloads are fetched, processed, and filtered entirely within the sandbox memory space. The LLM context window never sees the raw, unparsed data. It receives only the final, distilled summary output printed to stdout (Total matching items found: 42), saving over 90% of prompt tokens.

3. Stateful Kernel Persistence Across Turns

Unlike ephemeral stateless functions, CodeAct runtimes execute inside a persistent Python session (such as an IPython kernel or Jupyter client):

  • Variables, imported modules, database connections, and loaded DataFrames defined in Turn 1 remain resident in memory for Turn 2 and Turn 3.
  • If an agent loads a 200MB CSV file into a variable df during its initial exploration, subsequent queries (df.describe(), df.groupby()) execute instantaneously against the existing in-memory object without reloading the file from disk or network storage.

Deep Dive: Comparing Agent Action Spaces

To understand why CodeAct has become the primary execution engine behind frontier agent frameworks (including OpenHands, SWE-agent, and autonomous coding assistants), consider the structural differences across common agent paradigms:

Architectural DimensionText-Based ReActJSON-RPC Tool CallingCodeAct (Executable Code)
Action SyntaxFree-form text (Action: tool[arg])Structured JSON objectsMulti-line executable Python code
Control FlowNone (Single action per turn)None (Single/Parallel calls without logic)Full Turing-complete logic (Loops, branches)
Intermediate FilteringImpossible (Raw text returns to prompt)Impossible (Raw payloads enter context)Native in-memory (pandas, regex, list comps)
Multi-Step Execution1 action per LLM generation turn1 to $N$ independent calls per turnArbitrary multi-step algorithms in 1 turn
Error FeedbackUnstructured error stringsJSON error responsesStandard Python tracebacks with line numbers
ExtensibilityHardcoded text parsersPre-registered static schemasDynamic package imports (import pkg)
State RetentionStateless across turnsStateless across turnsStateful REPL memory variables
Token EfficiencyPoor ($O(N)$ prompt growth per step)Moderate (Structured overhead)Superior (Up to 60-80% token savings)

Self-Correction via Traceback Introspection

When a traditional JSON tool call fails—for example, if a database query returns an unexpected data type—the model receives an opaque JSON error string. The agent must guess whether the failure stemmed from parameter hallucination, server downtime, or invalid syntax.

In contrast, CodeAct leverages Python’s standardized exception hierarchy. When a script encounters an issue, the sandbox captures the complete stderr traceback:

Traceback (most recent call last): File "<ipython-input-4-9b2f7a1c>", line 8, in <module> target_value = record["metrics"]["conversion_rate"] KeyError: 'conversion_rate'

Because foundation models have analyzed millions of debugging discussions and StackOverflow threads, a Python traceback acts as an optimal cognitive prompt. The model immediately isolates the exact failing line (line 8), identifies the missing dictionary key (conversion_rate), inspects the keys present in record["metrics"], and repairs the code in the very next turn.

End-to-End Implementation: Building a Production CodeAct Agent

Let’s build a modular, production-ready CodeAct agent in Python. This implementation features:

  1. An isolated, stateful IPython execution harness with stdout/stderr capture.
  2. Execution timeout protection against infinite loops.
  3. An autonomous agent loop that maintains conversational state, synthesizes executable actions, and iteratively recovers from runtime exceptions.
import sys import io import time import queue import threading from typing import Dict, Any, List, Optional from IPython.core.interactiveshell import InteractiveShell class SandboxedPythonREPL: """ Stateful Python execution harness using an embedded IPython shell. Persists global variables across executions while capturing stdout/stderr. """ def __init__(self, timeout_seconds: int = 15): self.timeout_seconds = timeout_seconds # Initialize an in-memory IPython shell instance self.shell = InteractiveShell.instance() # Pre-seed safe common utilities self.shell.run_cell("import math, sys, os, json, re") self.shell.run_cell("import datetime, collections") def execute(self, code: str) -> Dict[str, Any]: """ Executes multi-line Python code with a strict timeout. Returns captured stdout, stderr, execution success status, and execution time. """ result_queue: queue.Queue = queue.Queue() def _target(): old_stdout = sys.stdout old_stderr = sys.stderr captured_out = io.StringIO() captured_err = io.StringIO() sys.stdout = captured_out sys.stderr = captured_err start_t = time.perf_counter() exec_result = None try: # Execute inside the stateful IPython kernel exec_result = self.shell.run_cell(code, store_history=True) elapsed = time.perf_counter() - start_t output = captured_out.getvalue() error_msg = captured_err.getvalue() # If an unhandled exception occurred, extract the formatted traceback if exec_result.error_in_exec: error_msg += f"\nRuntime Error: {exec_result.error_in_exec}" result_queue.put({ "success": exec_result.success, "stdout": output, "stderr": error_msg, "elapsed_sec": round(elapsed, 4) }) except Exception as e: result_queue.put({ "success": False, "stdout": captured_out.getvalue(), "stderr": f"System Interception Error: {str(e)}", "elapsed_sec": round(time.perf_counter() - start_t, 4) }) finally: sys.stdout = old_stdout sys.stderr = old_stderr exec_thread = threading.Thread(target=_target, daemon=True) exec_thread.start() exec_thread.join(timeout=self.timeout_seconds) if exec_thread.is_alive(): return { "success": False, "stdout": "", "stderr": f"ExecutionTimeout: Code execution exceeded {self.timeout_seconds} seconds limit.", "elapsed_sec": float(self.timeout_seconds) } return result_queue.get() class CodeActAgent: """ Autonomous agent driver using CodeAct principles: Translates instructions into executable Python code and self-corrects via REPL feedback. """ def __init__(self, repl: SandboxedPythonREPL): self.repl = repl self.history: List[Dict[str, str]] = [] self._init_system_prompt() def _init_system_prompt(self): system_prompt = ( "You are an expert autonomous software engineer and data analyst operating via CodeAct.\n" "Instead of rigid JSON schemas, you solve problems by writing executable Python code.\n\n" "Execution Guidelines:\n" "1. Output your thoughts followed by executable Python code inside ```python ... ``` blocks.\n" "2. Your Python code executes in a stateful IPython environment. Variables persist across turns.\n" "3. Filter, transform, and aggregate data locally inside Python. Never print giant raw dumps.\n" "4. Always use `print()` to output final observations or intermediate results you need to see.\n" "5. If you receive an error traceback, analyze the error line and output corrected code.\n" "6. When the task is fully accomplished, provide your final response starting with 'FINAL ANSWER:'." ) self.history.append({"role": "system", "content": system_prompt}) def extract_code(self, response_text: str) -> Optional[str]: """Extracts python code blocks from agent response.""" import re pattern = r"```python(.*?)```" matches = re.findall(pattern, response_text, re.DOTALL) if matches: return matches[-1].strip() return None def step(self, agent_llm_response: str) -> str: """ Processes one turn of the agent: parses code, executes in REPL, and formats observation. """ code = self.extract_code(agent_llm_response) if not code: return "No executable code block detected. If finished, state 'FINAL ANSWER:'." result = self.repl.execute(code) # Build structured observation string obs_lines = [] if result["stdout"].strip(): obs_lines.append(f"[STDOUT]:\n{result['stdout'].strip()}") if result["stderr"].strip(): obs_lines.append(f"[STDERR / TRACEBACK]:\n{result['stderr'].strip()}") if not obs_lines: obs_lines.append("[Execution succeeded with no stdout. Remember to use print() to observe values.]") obs_lines.append(f"(Execution completed in {result['elapsed_sec']}s)") return "\n\n".join(obs_lines) # Verification & Interactive Testing if __name__ == "__main__": repl = SandboxedPythonREPL(timeout_seconds=5) agent = CodeActAgent(repl) print("--- Test 1: Complex Multi-Step In-Memory Filtering ---") simulated_agent_action_1 = ''' I need to aggregate sales by region from raw records and find the top performing division. ```python records = [ &#123;"region": "North", "sales": 14200, "status": "closed"&#125;, &#123;"region": "South", "sales": 8900, "status": "pending"&#125;, &#123;"region": "North", "sales": 23100, "status": "closed"&#125;, &#123;"region": "East", "sales": 31000, "status": "closed"&#125;, &#123;"region": "West", "sales": 19500, "status": "closed"&#125;, &#123;"region": "East", "sales": 12400, "status": "closed"&#125;, ] from collections import defaultdict totals = defaultdict(int) for r in records: if r["status"] == "closed": totals[r["region"]] += r["sales"] top_region = max(totals.items(), key=lambda x: x[1]) print(f"Aggregated Sales: &#123;dict(totals)&#125;") print(f"Top Performing Region: &#123;top_region[0]&#125; with $&#123;top_region[1]:,&#125;")

''' observation_1 = agent.step(simulated_agent_action_1) print(observation_1)

print(“\n--- Test 2: Stateful Variable Persistence Across Turns ---”) simulated_agent_action_2 = '''

Now, let’s calculate the percentage contribution of the top region using the persistent totals dict.

total_sales = sum(totals.values()) pct = (top_region[1] / total_sales) * 100 print(f"Total Closed Sales: $&#123;total_sales:,&#125;") print(f"Top Region Contribution: &#123;pct:.2f&#125;%")

''' observation_2 = agent.step(simulated_agent_action_2) print(observation_2)

print(“\n--- Test 3: Traceback Capture & Recovery ---”) simulated_failing_action = '''

Let’s try accessing a missing key to demonstrate traceback capture:

value = totals["Antarctica"] result = value / 0

''' observation_3 = agent.step(simulated_failing_action) print(observation_3)

Running this script verifies the three architectural tenets: 1. **Multi-Step Aggregation:** Complex filtering and summation complete in a single turn without intermediate LLM round-trips. 2. **State Retention:** In Turn 2, `totals` and `top_region` remain directly accessible in memory without recomputation. 3. **Traceback Capture:** In Turn 3, standard Python errors are cleanly captured and structured for immediate self-correction. <div class="section-marker" data-section="Benchmarks"></div> ## Production Benchmarks: Empirical Performance Across Agent Tasks The CodeAct framework was evaluated extensively against traditional JSON-based tool calling and text-based ReAct across established multi-turn benchmarks, including **MINT** (Multi-turn Interaction benchmark for tool-augmented LLMs), **HumanEval**, and real-world repository tasks in **SWE-bench**. ### 1. Task Success Rate by Action Paradigm When evaluated across open-weights models (such as LLaMA-3 70B and CodeLLaMA) and frontier proprietary models (GPT-4o, Claude 3.5 Sonnet): | Agent Interaction Paradigm | Tool-Use Success Rate (MINT) | Avg Turns to Completion | Prompt Token Consumption | | :--- | :--- | :--- | :--- | | **Standard ReAct (Text)** | 38.4% | 14.8 turns | Baseline ($1.00\times$) | | **JSON-RPC Function Calling** | 47.9% | 11.2 turns | $0.84\times$ (-16%) | | **CodeAct (Executable Python)** | **68.3%** | **4.2 turns** | **$0.38\times$ (-62%)** | CodeAct improves overall task success rates by **over 20 percentage points** while cutting the average number of interaction turns by nearly $65\%$. Because loops and conditional evaluations happen locally in the REPL, the agent converges to the solution in a fraction of the turns required by JSON-RPC. ### 2. Context Window Consumption and Financial Cost By filtering raw payloads in Python memory rather than dumping responses into the context window, CodeAct drastically compresses prompt tokens: ```text Prompt Token Growth Over 10-Step Extraction Workflow: Tokens ▲ 60k ┼───────────────────────────────────────────────────────────── JSON-RPC (Unfiltered Bloat) │ ╭─────── 50k ┼─────────────────────────────────────────────╭───────╯ │ ╭───────╯ 40k ┼─────────────────────────────╭───────╯ │ ╭───────╯ 30k ┼─────────────╭───────╯ │ ╭───────╯ 20k ┼─────╯ │ 10k ┼───────────────────────────────────────────────────────────── CodeAct (In-Memory Distillation) │ ──────────────────────────────────────────────────────── 0k ┴─────┴───────┴───────┴───────┴───────┴───────┴───────┴───────► Turn 1 Turn 2 Turn 3 Turn 4 Turn 5 Turn 6 Turn 7 Turn 8 Turn 9 Turn 10

Across a 10-turn data ingestion workflow, JSON-RPC accumulates over 60,000 prompt tokens as unparsed responses compound. CodeAct maintains a flat context curve under 12,000 tokens, yielding a $62%$ to $75%$ reduction in total API token expenditures.

Strategic Recommendations: Deploying CodeAct in 2026

Adopting CodeAct transforms AI agents from rigid API wrappers into autonomous software developers. When architecting production agent platforms in 2026, adhere to these three core deployment principles:

1. Enforce Multi-Layered Sandbox Isolation

Allowing language models to generate and execute arbitrary Python code requires an uncompromising security architecture:

  • Never Run Bare-Metal: Never execute agent-generated scripts directly on host operating systems or production containers with mounted credentials.
  • MicroVM Isolation: Deploy ephemeral guest microVMs (such as AWS Firecracker or gVisor runtimes) that provision in under 150ms and guarantee hardware-level KVM kernel boundaries.
  • Egress Proxying: Restrict network interfaces with strict default-deny policies, allowing outbound HTTP requests only through authenticated, rate-limited proxy egress filters.

2. Pair CodeAct with Domain-Specific SDK Injections

Maximize agent effectiveness by pre-seeding the REPL environment with typed client SDKs:

  • Pre-install and import your platform’s core libraries (e.g., import aifetchly_sdk, import pandas as pd, import requests).
  • Provide clean docstrings and minimal API examples in the agent’s system prompt. The model will invoke SDK functions programmatically within its loops, completely bypassing complex JSON serialization.

3. Implement Guardrails for Non-Terminating Code

Language models can inadvertently generate non-terminating while True: loops or memory-exhaustive recursions:

  • Strict Execution Timeouts: Enforce hard timeouts (e.g., 10 to 30 seconds) on every REPL execution thread.
  • Memory Capping: Use Linux cgroups or resource limits (ulimit -v) to prevent out-of-memory kernel panics.
  • AST Safety Scans: Run fast static analysis via Python’s built-in ast module to detect and flag potentially malicious patterns before passing code to the interpreter.