Execution
Control agent iteration limits, retries, rate limiting, and code execution
Execution configuration controls how long an agent runs, how many times it retries, and what code it can execute.
from praisonaiagents import Agent
from praisonaiagents.config.presets import EXECUTION_PRESETS
# All spellings apply the 'thorough' preset (max_iter=50):
for value in ("thorough", " thorough ", "THOROUGH"):
agent = Agent(instructions="t", execution=value)
assert agent.max_iter == EXECUTION_PRESETS["thorough"]["max_iter"]| Call (Python) | Before PR #4186 | After PR #4186 |
|---|---|---|
Agent(execution="thorough") | ✅ max_iter=50 | ✅ max_iter=50 |
Agent(execution=" thorough ") | ⚠ validated → max_iter=20 (default) | ✅ max_iter=50 (thorough) |
Agent(execution="THOROUGH") | ⚠ validated → max_iter=20 (default) | ✅ max_iter=50 (thorough) |
from praisonaiagents import Agent
agent = Agent(
name="Assistant",
instructions="You are a thorough research assistant.",
execution="thorough",
)
agent.start("Analyze the competitive landscape of the electric vehicle market.")The user sends a research prompt; iteration limits, retries, and rate limits govern how long the agent runs.
Quick Start
- Level 1 — String (preset)
Pick a named execution profile — the shortest way to set sensible limits.
python from praisonaiagents import Agent agent = Agent( name="Researcher", instructions="You are a research assistant.", execution="thorough", ) agent.start("Compare five programming languages for web development.") - Level 2 — Config class (full control)
Use
ExecutionConfigto set exact iteration, time, and retry limits.python from praisonaiagents import Agent, ExecutionConfig agent = Agent( name="Researcher", instructions="You are a research assistant.", execution=ExecutionConfig( max_iter=50, max_execution_time=300, max_retry_limit=3, ), ) agent.start("Research and summarize the latest AI safety papers.")
Execution Presets
How It Works
| Phase | What happens |
|---|---|
| 1. Budget check | Execution tracks iterations against max_iter |
| 2. Rate limit | Enforces max_rpm requests-per-minute |
| 3. Time limit | Once a turn has run max_execution_time seconds, it stops at the next loop boundary (next LLM call or tool dispatch) and agent.last_stop_reason == "timeout". See Execution Time Caps. |
| 4. Retry | Retries failed steps up to max_retry_limit times |
| 5. Step-limit wrap-up | On the final permitted step (max_steps, or max_iter when unset), the agent injects a wrap-up instruction so the model produces a coherent final answer summarising progress instead of being hard-cut. Detect truncation with agent.last_stop_reason == "max_steps". See Step Budget. |
Configuration Options
CardFull list of options, types, and defaults — ExecutionConfig
The most common options at a glance:
| Option | Type | Default | Description | |
|---|---|---|---|---|
max_steps | `int \ | None` | None | Unified outer-loop step budget honoured by both tool-execution loops (OpenAI-native and LiteLLM). None falls back to max_iter. Must be >= 1 when set. See Step Budget. |
output_continuation | Union[bool, int] | True | Auto-continue a final answer cut off by the output-token limit. See Output Continuation. | |
max_iter | int | 20 | Legacy per-loop iteration cap, used when max_steps is unset. On the final step the agent receives a graceful wrap-up instruction and returns a coherent summary — not the old canned "Task completed." placeholder. | |
max_tool_calls_per_turn | int | 10 | Cap on tool calls within a single LLM response (parallel-tool guardrail). Independent of max_steps. | |
max_rpm | `int \ | None` | None | Max requests per minute — when set and no rate_limiter is passed, the Agent auto-builds one. max_rpm <= 0 raises ValueError |
max_execution_time | `int \ | None` | None | Wall-clock seconds per chat turn. The loop guard halts the next tool call, and the LLM loop stops at its next boundary, so a long turn stops even without tool calls. Sets agent.last_stop_reason == "timeout". Sync and async. See Execution Time Caps. |
max_retry_limit | int | 2 | Max retries on failure | |
code_execution | bool | False | Grants the agent an execute_code tool (not just a flag). The tool is classed "critical", so it is approval-gated under every preset except "full". | |
code_mode | str | "safe" | "safe" runs code in an isolated subprocess with resource limits; "unsafe" runs in-process with restricted builtins so code can reach the agent’s tools. Any other value raises ValueError at construction. | |
code_tools | bool | False | Lets model-generated code call the agent’s tools via proxies. Only meaningful with code_mode="unsafe" — setting it in "safe" mode warns and is ignored. Model-generated code calls the agent’s tools through the same policy / guardrail / approval gate as the normal tool-call loop (agent-wide guardrails=, per-tool @tool(input_guardrails=/output_guardrails=), PolicyEngine, PermissionManager deny-rules, require_approval). | |
code_tools_allow | `List[str] \ | None` | None | Per-run allow-list of tool names callable from code. A named tool must also be present in Agent(tools=[...]). None/empty = no tools exposed. |
max_budget | `float \ | None` | None | Hard USD spend limit per run |
Code-execution options
Setting code_execution=True appends a real tool named execute_code to the agent’s tool list — it is not just a flag. Because the tool is named execute_code, the approval registry classes it critical, so it is approval-gated under every preset except "full".
| Option | Type | Default | Description | |
|---|---|---|---|---|
code_execution | bool | False | Appends the execute_code tool to the agent | |
code_mode | str | "safe" | "safe" runs code in a subprocess with resource limits and a clean env (tools not reachable). "unsafe" runs in-process with restricted builtins so code_tools_allow tools become callable. Any other value raises ValueError at construction | |
code_tools | bool | False | Let model-generated code call the agent’s registered tools (only meaningful with code_mode="unsafe"). Calls run the same policy / guardrail / approval gate as the normal tool-call loop (agent-wide guardrails=, per-tool @tool(input_guardrails=/output_guardrails=), PolicyEngine, PermissionManager deny-rules, require_approval). | |
code_tools_allow | `List[str] \ | None` | None | Per-run allow-list of tool names callable from code. The tool must also be in Agent(tools=[...]). None/empty exposes no tools |
timeout | `int \ | None` | None | In "safe" mode this is a per-call timeout for execute_code (enforced by the subprocess). In "unsafe" mode there is no hard timeout — code runs in-process |
Common Patterns
Pattern 1 — Budget-capped agent
from praisonaiagents import Agent, ExecutionConfig
agent = Agent(
instructions="You are a research assistant.",
execution=ExecutionConfig(max_budget=0.50, on_budget_exceeded="warn"),
)
response = agent.start("Research the history of computing.")
print(response)Pattern 2 — Code execution agent
from praisonaiagents import Agent, ExecutionConfig
agent = Agent(
instructions="You are a Python coding assistant that runs code to verify answers.",
execution=ExecutionConfig(
code_execution=True,
code_mode="safe",
max_iter=30,
),
)
agent.start("Write and test a Python function to find prime numbers.")Pattern 3 — Parallel tool calls for speed
from praisonaiagents import Agent, ExecutionConfig
agent = Agent(
instructions="You are a multi-source research agent.",
execution=ExecutionConfig(parallel_tool_calls=True, max_iter=40),
)
agent.start("Gather weather, news, and stock prices for New York.")import asyncio
from praisonaiagents import Agent, ExecutionConfig
agent = Agent(
instructions="You are a multi-source research agent.",
execution=ExecutionConfig(parallel_tool_calls=True, max_iter=40),
)
# Since PraisonAI PR #4634, parallel_tool_calls is honoured on the async
# tool loop too — independent tool calls in one turn are dispatched
# concurrently via asyncio.gather instead of running one-at-a-time.
asyncio.run(agent.astart("Gather weather, news, and stock prices for New York."))Pattern 4 — Code that calls your tools
from praisonaiagents import Agent, ExecutionConfig
agent = Agent(
instructions="You solve tasks by writing code that calls the provided tools.",
tools=[search_web, calculate],
execution=ExecutionConfig(
code_execution=True,
code_mode="unsafe", # required for code to reach the agent's tools
code_tools=True,
code_tools_allow=["search_web", "calculate"],
),
)
agent.start("Search for the population of three cities and sum them.")Pattern 5 — The execute_code tool and its approval gate
Setting code_execution=True appends an execute_code tool to the agent. Because that tool is classed "critical", the approval framework prompts before it runs under every preset except "full".
from praisonaiagents import Agent, ExecutionConfig
agent = Agent(
instructions="You are a Python assistant that runs code to verify answers.",
execution=ExecutionConfig(code_execution=True), # code_mode="safe" by default
)
# The agent now has an `execute_code` tool. When it decides to run code,
# the approval prompt fires first (unless the "full" preset is active).
agent.start("Compute the 20th Fibonacci number by running Python.")Best Practices
Use presets as a starting point
execution="thorough" works well for most research and multi-step tasks. Only switch to custom ExecutionConfig when you need specific limits like budget caps or code execution.
Set max_budget for cost control
For any agent making many API calls, set max_budget=0.50 to cap spend at 50 cents per run. Use on_budget_exceeded="warn" in development and "stop" in production.
Enable parallel_tool_calls for speed
When your agent calls multiple independent tools per turn (e.g., search + fetch + calculate), enable parallel_tool_calls=True to run them concurrently and cut latency. This is the single source of truth for parallel tool calls; ToolConfig.parallel is a deprecated alias for it. Parallel tool calls apply to both sync (chat() / start()) and async (achat() / astart()) tool loops as of PR #4634 — before that, async ran tools sequentially even with parallel_tool_calls=True. Concurrent writes to the same file fall back to sequential — see the write-conflict guard.
Code execution safety
code_mode="safe" is the right default for code_execution=True. It runs code in a subprocess with resource limits, and the timeout on your ExecutionConfig is enforced there as a hard per-call limit. "unsafe" mode runs in the current process — it enforces no hard timeout, so a runaway loop can block the worker; only choose it when code must reach your tools, and prefer "safe" whenever the timeout must be a guarantee. Agent(sandbox=…) only isolates explicit agent.execute_code(...) calls — it does not give the model a tool. For isolation of code the model runs, use AgentFlow(run_on="docker") / run_on="e2b" (whole workflow) or LocalAgent(compute="docker") (per-agent). See Placement.
Scope code_tools with an explicit allow-list
When code_tools=True, always set code_tools_allow to the exact tool names code may call. Leaving it None exposes no tools (the safe default), so name only what the task needs — never grant blanket access.
Detect a timeout stop
When a turn is cut by max_execution_time (or by run_autonomous(timeout_seconds=...)), agent.last_stop_reason == "timeout". Branch on that string instead of parsing the reply. Autonomous runs report the same stop as AutonomyResult(completion_reason="timeout", success=False).
Detecting truncated runs
Set max_steps and check agent.last_stop_reason == "max_steps" to detect truncation — no string-matching needed. The returned text is a real LLM-authored wrap-up (“Here’s what I accomplished… here’s what remains…”), not a placeholder. See Step Budget.
A length_truncated outcome is now reached only after output_continuation continuations are exhausted; usually the agent stitches the pieces into one completed answer first. See Output Continuation.