Autonomous Loops
Run agents in self-referential execution loops with completion signals
Autonomous loops let agents work iteratively on complex tasks, automatically stopping when they signal completion or reach limits.
from praisonaiagents import Agent
agent = Agent(
name="worker",
instructions="Iterate until the task is complete.",
autonomy={"level": "full_auto"},
)
agent.start("Fix the failing unit tests.")The user assigns a multi-step task; the agent loops until tool completion, a promise match, or a safety limit stops the run.
Quick Start
- Simple Usage
Full Auto (iterative by default) from praisonaiagents import Agent # full_auto defaults to mode="iterative" agent = Agent( name="builder", instructions="Build the requested feature", autonomy="full_auto" ) result = agent.start("Create a REST API with user authentication") print(f"Success: {result.success}") print(f"Iterations: {result.iterations}")Explicit Iterative Mode from praisonaiagents import Agent agent = Agent( name="builder", instructions="""Build the feature. When done, output: <promise>DONE</promise>""", autonomy={ "mode": "iterative", "max_iterations": 20, "completion_promise": "DONE", } ) result = agent.start("Create a calculator module")With Context Clearing from praisonaiagents import Agent agent = Agent( name="builder", instructions="Work on the task. Use files for state.", autonomy={ "level": "full_auto", "max_iterations": 15, "completion_promise": "COMPLETE", "clear_context": True, # Fresh memory each iteration } ) result = agent.start("Refactor the auth module") - With Configuration
Use an explicit autonomy dict for iterative mode, completion_promise, and optional clear_context — see the additional tabs in the code group above.
How It Works
Key Features
Agent signals “done” with a promise tag containing TEXT
Context ClearingFresh memory each iteration forces file-based state
Doom Loop DetectionAutomatically stops on repeated identical actions
Iteration LimitsPrevents runaway execution with configurable max
Configuration
AutonomyConfig Options
| Option | Type | Default | Description |
|---|---|---|---|
max_iterations | int | 20 | Maximum loop iterations |
completion_promise | str | None | Text to detect in promise tags |
clear_context | bool | False | Clear chat history between iterations |
doom_loop_threshold | int | 3 | Repeated actions before stopping |
level | str | "suggest" | Autonomy level: suggest, auto_edit, full_auto |
auto_escalate | bool | True | Automatically escalate task complexity |
observe | bool | False | Emit observability events |
max_budget_usd | Optional[float] | None | Cumulative USD spend cap for the run. None = unlimited. |
max_tokens | Optional[int] | None | Cumulative token cap (input + output). None = unlimited. |
budget_action | str | "pause" | "pause" (recoverable) or "stop" (terminal) when a cap is hit. |
snapshot | bool | False | Take a pre-run workspace snapshot in run_autonomous() so agent.undo() can restore it. Requires track_changes. Off by default because snapshotting walks the whole project. |
agent.undo() only works with AutonomyConfig(track_changes=True, snapshot=True) — see File Snapshot.
The loop checks cumulative spend after every iteration. When a cap is hit, budget_action decides whether the run pauses (recoverable) or stops (terminal).
Using Config Dict
agent = Agent(
name="worker",
instructions="Complete the task",
autonomy={
"max_iterations": 30,
"completion_promise": "FINISHED",
"clear_context": True,
"doom_loop_threshold": 5
}
)CLI Usage
Basic
praisonai loop "Build a REST API" -n 5With Promise
praisonai loop "Refactor auth module" -p DONE -vWith Context Clearing
praisonai loop "Implement feature X" -p COMPLETE -c -n 20With Timeout
praisonai loop "Debug the issue" -t 300 -vWith Budget Cap
praisonai loop "Research AI trends" --max-budget 0.50 -vBudget — warn only
praisonai loop "Keep refining" --max-budget 2.0 --on-budget-exceeded warnCLI Options
| Flag | Description |
|---|---|
-n, --max-iterations | Maximum iterations (default: 10) |
-p, --completion-promise | Promise text to signal completion |
-c, --clear-context | Clear chat history between iterations |
-t, --timeout | Timeout in seconds. On expiry the run returns success=False, completion_reason="timeout". Only forwarded when set |
--max-budget | Hard USD spend cap for the whole loop (cumulative across iterations, not reset per iteration). Must be a positive, finite number — 0, negatives, nan, and inf are rejected with exit code 2. Omit for no cap |
--on-budget-exceeded | What happens when the cap is hit: stop (default — raises BudgetExceededError, CLI exits 1) or warn (logs and continues). Any other value exits 2. Only meaningful with --max-budget |
-m, --model | LLM model to use |
-v, --verbose | Show verbose output |
Result Object
result = agent.start(prompt) # when autonomy=True
# Available fields
result.success # bool - Task completed successfully
result.output # str - Final response
result.completion_reason # str - "tool_completion", "promise", "caller_mode", "max_iterations", "doom_loop", "timeout", "error"
result.iterations # int - Number of iterations executed
result.duration_seconds # float - Total execution time
result.started_at # str - ISO 8601 timestamp when execution started
result.actions # list - Actions taken each iteration
result.error # str | None - Error message if failed
result.metadata # dict | None - Extra data, e.g. spend on budget_exhaustedOn a budget_exhausted run, result.metadata carries the spend snapshot:
result.metadata = {
"spend_usd": 0.4231, # USD spent this run
"tokens": 12345, # tokens used this run
"max_budget_usd": 0.50, # the configured USD cap (or None)
"max_tokens": None, # the configured token cap (or None)
"status": "paused", # "paused" (budget_action="pause") or "stopped"
}Timeout
Pass timeout= (seconds) to agent.start() or agent.astart() to cap a run in both caller and iterative modes.
from praisonaiagents import Agent
agent = Agent(
name="worker",
instructions="Answer the question.",
autonomy={"level": "suggest"}, # caller mode
)
result = agent.start("Research topic X", timeout=30)
if result.completion_reason == "timeout":
print("Budget exceeded:", result.output) # "Task timed out"import asyncio
from praisonaiagents import Agent
async def main():
agent = Agent(name="worker", instructions="Answer the question.", autonomy=True)
result = await agent.astart("Research topic X", timeout=30)
print(result.success, result.completion_reason)
asyncio.run(main())On expiry the result has success=False, output="Task timed out", and completion_reason="timeout".
How Completion Detection Works
Completion Reasons
tool_completion (most common)
The model used tools to complete the task and then produced a summary response without requesting more tools. This is the most reliable completion signal — it’s the same mechanism used by ChatGPT and all major LLM APIs.
caller_mode
Agent was in caller mode (autonomy=True default, or level="suggest" / "auto_edit"). A single chat() / achat() call wrapped in AutonomyResult. If timeout= elapses first, the reason is timeout instead.
promise
Agent output contained a promise tag with TEXT matching the configured promise.
goal
Agent output contained completion keywords like “task completed” or “done”.
no_tool_calls
The model produced text without calling any tools for 2+ consecutive turns — indicates it has nothing left to do.
max_iterations
Reached the maximum iteration limit without completion signal.
doom_loop
Detected repeated identical actions (agent stuck in a loop). The session-level Escalation Doom-Loop Detection subsystem also catches NO_PROGRESS stalls — now using a recency window so an early success no longer suppresses it.
budget_exhausted
max_budget_usd or max_tokens was reached. result.metadata carries spend_usd, tokens, max_budget_usd, max_tokens, and status ("paused" or "stopped"). With budget_action="pause" the run is resumable — raise the cap and re-invoke run_autonomous. See Autonomy Budget.
timeout
Execution exceeded the configured timeout. Applies to both caller and iterative modes — see Timeout.
needs_help
Doom loop recovery exhausted — task needs human guidance.
error
An error occurred during execution.
Recommended Workflow
- Use Completion Promises
Always set a
completion_promisefor reliable termination instead of relying on keyword detection. - Enable Context Clearing for Long Tasks
Use
clear_context=Truefor tasks that should rely on file state rather than conversation memory. - Set Reasonable Limits
Configure
max_iterationsbased on task complexity. Start low and increase if needed. - Include Promise in Instructions
Tell the agent to output the promise tag when done:
python instructions="When finished, output DONE in a promise tag"
Async Execution
Run autonomous loops asynchronously for concurrent agent execution:
import asyncio
from praisonaiagents import Agent
async def main():
agent = Agent(
name="async_worker",
instructions="Complete the task. Output <promise>DONE</promise> when finished.",
autonomy={
"max_iterations": 10,
"completion_promise": "DONE",
}
)
# astart() automatically uses autonomous loop when autonomy=True
result = await agent.astart("Build a calculator")
print(f"Success: {result.success}")
print(f"Started: {result.started_at}")
asyncio.run(main())import asyncio
from praisonaiagents import Agent
async def main():
agent1 = Agent(
name="researcher",
instructions="Research the topic",
autonomy={"completion_promise": "DONE"}
)
agent2 = Agent(
name="writer",
instructions="Write content",
autonomy={"completion_promise": "DONE"}
)
# Run both agents concurrently with unified API
results = await asyncio.gather(
agent1.astart("Research AI trends"),
agent2.astart("Write a blog post")
)
for i, result in enumerate(results):
print(f"Agent {i+1}: {result.completion_reason}")
asyncio.run(main())Memory Integration
Autonomous loops automatically save sessions between iterations when memory is enabled. This ensures progress is persisted even if the loop is interrupted.
On the async loop (run_autonomous_async), the save is offloaded to a worker thread — matching the goal-completion-judge offload — so a slow file-lock + JSON write can’t stall other async agents sharing the event loop. The sync loop (run_autonomous) writes inline as before.
sequenceDiagram
participant A as Agent
participant L as LLM
participant W as Worker Thread
participant M as Memory/Session
loop Each Iteration (run_autonomous_async)
A->>L: Send prompt
L->>A: Response
A->>W: to_thread(_auto_save_session)
W->>M: FileLock + JSON write
M-->>W: saved
W-->>A: loop stays responsive
A->>A: Check completion
end
style A fill:#8B0000,color:#fff
style L fill:#189AB4,color:#fff
style W fill:#189AB4,color:#fff
style M fill:#F59E0B,color:#ffffrom praisonaiagents import Agent, MemoryConfig
agent = Agent(
name="persistent_worker",
instructions="Work on the task. Output <promise>DONE</promise> when finished.",
autonomy={
"completion_promise": "DONE",
"max_iterations": 20,
},
memory=MemoryConfig(auto_save="project_session"),
)
# Unified API — start() handles everything!
result = agent.start("Refactor the auth module")
# Session is auto-saved after every iterationExample: Self-Improving Code
from praisonaiagents import Agent
agent = Agent(
name="improver",
instructions="""You are a code improvement agent.
1. Read the current code
2. Identify one improvement
3. Make the change
4. Verify it works
5. If no more improvements needed, output <promise>OPTIMIZED</promise>
""",
autonomy={
"max_iterations": 10,
"completion_promise": "OPTIMIZED",
"clear_context": True,
},
tools=[read_file, write_file, run_tests]
)
# Just call start() - autonomy config handles everything!
result = agent.start("Optimize the performance of src/utils.py")
if result.success:
print(f"Code optimized in {result.iterations} iterations")
print(f"Started: {result.started_at}")
else:
print(f"Stopped: {result.completion_reason}")Best Practices
Choose the mode that matches the task
Loops only run in mode="iterative". autonomy=True uses mode="caller" (a single chat), so set level="full_auto" or pass mode="iterative" explicitly when you want the agent to keep working across turns. Use single-turn mode for quick answers to avoid unnecessary iterations.
Give the agent a clear completion signal
The loop stops on tool completion, a promise match, or a safety limit. Write instructions that produce a definite end state (“stop when all tests pass”) so the agent hits tool completion instead of drifting until it reaches the iteration cap.
Keep doom-loop detection on
Doom-loop detection halts an agent that repeats the same unproductive action. Leave it enabled for long-running tasks; it’s the safety net that turns a runaway loop into a clean completion_reason you can inspect.
Check the RunOutcome, don't assume success
start() returns a result with success, iterations, and completion_reason. Branch on result.success and log completion_reason so a limit-reached run is handled distinctly from genuine completion.