Goal-Gated Autonomous Loop
Run an autonomous tool-using loop that only stops when an independent judge confirms your goal is met
The goal loop runs your agent with its tools and only stops when an independent judge confirms the goal is met — no more “done”-keyword false positives.
Quick Start
- Simple goal
Pass a task and a goal — the judge decides when the goal is really met.
python from praisonaiagents import Agent agent = Agent(instructions="You are an ops assistant", autonomy=True) result = agent.run_goal( "Tidy the repo and open a PR", goal="A PR is open with formatting fixes and green CI", ) print(result.success, result.completion_reason) # True goal_met - With acceptance criteria
Add structured criteria so the judge anchors to concrete evidence.
python from praisonaiagents import Agent from praisonaiagents.goal import GoalCriteria agent = Agent(instructions="You are an ops assistant", autonomy=True) result = agent.run_goal( "Tidy the repo and open a PR", goal="Repo formatted, tests green, PR opened", criteria=GoalCriteria( outcome="A PR is open with formatting fixes and passing CI", verification="A PR URL exists and the test command exits 0", constraints=["do not touch CHANGELOG.md", "no force-push"], ), max_turns=15, ) - Resume a paused or crashed run
Give the agent a fixed
memory.session_idandrun_goal()picks up where the last run stopped — even from a brand-new process.turns_usedis kept, never reset.python from praisonaiagents import Agent agent = Agent( instructions="You are an ops assistant", autonomy=True, memory={"session_id": "repo-tidy", "history": True}, ) agent.run_goal("Continue the task", goal="Repo formatted, tests green, PR opened")Two cases resume:
- Budget-paused — the last run hit
max_turns(budget_paused). - Crashed — the process died mid-run, so the saved state is still
active. A freshAgentin a new process resumes it instead of restarting atturns_used=0(the behaviour before PR #6050).
- Budget-paused — the last run hit
How It Works
The loop iterates with tools; after each iteration an independent judge reads the goal plus the latest output and returns done or continue.
The judge is strict about evidence:
Do NOT accept mere claims of completion (e.g. the word ‘done’) as evidence. Require concrete evidence that the goal is actually met.
How resume matches a saved run
Goal state is saved in the session store after every turn. On run_goal(..., resume=True) the agent loads it — opening the store first, so a fresh process sees it too.
A saved run resumes only when all of these match:
| Check | Requirement |
|---|---|
| Goal | Same goal text (exact match) — a different goal starts fresh |
| Session | Same memory.session_id, so the store resolves to the same session |
| Status | paused or active (crashed) — finished runs are never resumed |
Acceptance Criteria
GoalCriteria describes what “done” means, how to check it, and what must never happen.
| Field | Type | Default | Description |
|---|---|---|---|
outcome | str | "" | What “done” means in one line — the definition the judge anchors to |
verification | str | "" | The concrete evidence bar the judge requires before returning done |
constraints | List[str] | [] | Must-not-violate conditions; any violation forces continue |
run_goal() Options
| Option | Type | Default | Description |
|---|---|---|---|
task | str | — | The task/prompt the agent executes |
goal | str | — | The goal text the judge evaluates against |
criteria | Optional[GoalCriteria] | None | Structured acceptance criteria (recommended) |
max_turns | int | 20 | Judged iterations before a recoverable pause |
judge_model | Optional[str] | OPENAI_MODEL_NAME env or "gpt-4o-mini" | Independent judge model — use a different model from the actor to avoid self-enhancement bias |
resume | bool | True | Resume a persisted paused or still-active (crashed mid-run) run for the same goal instead of restarting. Works across processes — a fresh Agent sees state saved by an earlier process |
Completion Reasons
The loop returns an AutonomyResult whose completion_reason tells you why it stopped.
completion_reason | Meaning |
|---|---|
"goal_met" | Judge returned done with concrete evidence — success=True |
"budget_paused" | max_turns exhausted, or the judge returned 3 consecutive unparseable responses — success=False, run is recoverable via a second run_goal(..., resume=True) call |
| (other) | Falls back to the standard run_autonomous heuristics — see Autonomy |
The Independent Judge
The judge is deliberately simple, cheap, and safe to fail.
Independent
Uses a separate judge model so the acting model never self-grades — this avoids self-enhancement bias.
Fail-open
A broken or timed-out judge yields continue, so a weak judge never wedges progress.
Cheap
Only the goal plus the last ~4000 characters of agent output are judged, not the whole transcript.
Constraint-strict
Any constraint listed in GoalCriteria.constraints blocks a done verdict.
Auto-pause safety valve
3 consecutive unparseable judge responses auto-pause the loop with budget_paused, so a wedged judge cannot silently burn the whole budget.
Async Variant
run_goal_async() mirrors run_goal() for async code.
result = await agent.run_goal_async("Tidy the repo", goal="A PR is open with green CI")run_until(goal=...) Delegation
Passing goal= to run_until() now delegates to run_goal() — the acceptance-criteria judge gates a real tool-iteration loop instead of re-generating a whole answer.
from praisonaiagents import Agent
from praisonaiagents.goal import GoalCriteria
agent = Agent(instructions="You are an ops assistant", autonomy=True)
result = agent.run_until(
"Do the task",
goal="A PR is open with green CI",
goal_criteria=GoalCriteria(outcome="PR merged", verification="PR URL exists"),
)
# Returns AutonomyResult (not EvaluationLoopResult)
print(result.completion_reason) # "goal_met" | "budget_paused"Best Practices
Use a different judge model
Set judge_model to a different model than the actor so the run is not self-graded.
Write verification as a checkable condition
Use concrete, checkable evidence (“exit code 0”, “PR URL exists”), not a feeling.
Keep max_turns low for first runs
Start with max_turns=10–20 and let resume=True continue a paused run when needed.
Fill constraints with forbidden actions
constraints is the only field that can block a done verdict — list what must never happen.