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.

Diagram

Autonomous Loops

📋 User Request

⚙️ Autonomous Loops

✅ Result

python
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.

Diagram

Yes + response

No

Promise Found

Max Iterations

Doom Loop

Continue

Start Loop

Execute Task

Auto-Save Session

Tools used\nthis turn?

✅ Tool Completion

Check Other Signals

✅ Promise Match

⚠️ Limit Reached

🛑 Stopped

Quick Start

  1. 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")
  2. 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

Diagram
LLMInner Tool Loopchat()run_autonomous()agent.start()UserLLMInner Tool Loopchat()run_autonomous()agent.start()Userautonomy=True detectedloop[Model calls tools until satisfied]No tool_calls = model is doneTools were used + response > 100 charsagent.start(prompt)Route to autonomous loopchat(prompt)_chat_completion(messages, tools)prompt + toolstool_calls=[search]Execute tool, append resultsresults + tools“Here’s your answer…” (no more tools)Final responseResponse text✅ AutonomyResult(reason=”tool_completion”)

Key Features

Configuration

AutonomyConfig Options

OptionTypeDefaultDescription
max_iterationsint20Maximum loop iterations
completion_promisestrNoneText to detect in promise tags
clear_contextboolFalseClear chat history between iterations
doom_loop_thresholdint3Repeated actions before stopping
levelstr"suggest"Autonomy level: suggest, auto_edit, full_auto
auto_escalateboolTrueAutomatically escalate task complexity
observeboolFalseEmit observability events
max_budget_usdOptional[float]NoneCumulative USD spend cap for the run. None = unlimited.
max_tokensOptional[int]NoneCumulative token cap (input + output). None = unlimited.
budget_actionstr"pause""pause" (recoverable) or "stop" (terminal) when a cap is hit.
snapshotboolFalseTake 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).

Diagram

Under cap

Over cap

pause

stop

⚙️ Iteration

Spend or tokens\nover cap?

budget_action

⏸️ budget_exhausted\nstatus=paused\nraise cap + resume

🛑 budget_exhausted\nstatus=stopped\nterminal

Using Config Dict

python
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

bash
praisonai loop "Build a REST API" -n 5

With Promise

bash
praisonai loop "Refactor auth module" -p DONE -v

With Context Clearing

bash
praisonai loop "Implement feature X" -p COMPLETE -c -n 20

With Timeout

bash
praisonai loop "Debug the issue" -t 300 -v

With Budget Cap

bash
praisonai loop "Research AI trends" --max-budget 0.50 -v

Budget — warn only

bash
praisonai loop "Keep refining" --max-budget 2.0 --on-budget-exceeded warn

CLI Options

FlagDescription
-n, --max-iterationsMaximum iterations (default: 10)
-p, --completion-promisePromise text to signal completion
-c, --clear-contextClear chat history between iterations
-t, --timeoutTimeout in seconds. On expiry the run returns success=False, completion_reason="timeout". Only forwarded when set
--max-budgetHard 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-exceededWhat 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, --modelLLM model to use
-v, --verboseShow verbose output

Result Object

python
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_exhausted

On a budget_exhausted run, result.metadata carries the spend snapshot:

python
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.

python
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"
python
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

Diagram

Yes + response > 100 chars

No

Yes

No

Yes

No

Yes

No

📝 Agent produces response

Used tools\nthis turn?

✅ tool_completion\n(most common)

Promise tag\nin response?

✅ promise

Completion\nkeywords?

✅ goal

2+ turns with\nno tool calls?

✅ no_tool_calls

🔄 Next iteration

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

  1. Use Completion Promises

    Always set a completion_promise for reliable termination instead of relying on keyword detection.

  2. Enable Context Clearing for Long Tasks

    Use clear_context=True for tasks that should rely on file state rather than conversation memory.

  3. Set Reasonable Limits

    Configure max_iterations based on task complexity. Start low and increase if needed.

  4. 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:

Diagram

Start

Agent 1

Agent 2

Agent 3

Results

Done

Single Async Agent (Unified API)
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())
Concurrent Agents
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.

Diagram
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:#fff
python
from 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 iteration

Example: Self-Improving Code

python
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.

Related