PromptisePromptise
Why PromptiseDocs
GitHubGet started
Promptise - AI Framework LogoPromptise

The foundation layer for agentic intelligence. Build, secure and operate autonomous AI systems with Promptise Foundry.

pip install promptise

[01] Foundry

  • MCPcast
  • The Promptise Agent
  • Reasoning Engine
  • MCP
  • Agent Runtime
  • Prompt Engineering
  • Execution Engine
  • Agent Identity

[02] Resources

  • Documentation
  • GitHub
  • Guides
  • Learning Paths
  • Questions

[03] Company

  • About
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Subprocessors

© 2026 Promptise by Manser Ventures. All rights reserved.

Open source · Python

← Guides> Testing and evals

Who Decides When an AI Agent Is Done? Checks and Judges

Who decides when an AI agent is done? Stop conditions, self-checks, success criteria in code, a separate LLM judge and budgets as a backstop, with code.

Level
Intermediate
Reading time
16 min
Published
Oct 23, 2026
Last updated
Oct 23, 2026
Tested with
Promptise 1.7.0
By
Promptise Team
  • LLM-as-a-Judge
  • Self-Correction and Reflection
  • Agent Runtime
  • Evals and Testing
Contents · 14 sections↓
  1. 01Who decides when an AI agent is done?
  2. 02Stopping is not finishing: who decides what
  3. 03Self-verification: what a self-check prompt can and can't do
  4. 04When to use a code check, a judge or both
  5. 05What you need
  6. 06Step 1: Let the agent check itself
  7. 07Step 2: Put the success criteria in code and loop
  8. 08Step 3: Add a separate judge for what code can't check
  9. 09Step 4: Make budgets the backstop
  10. 10Running it in production
  11. 11Common problems and fixes
  12. 12Limitations
  13. 13FAQ
  14. 14Next steps

Who decides when an AI agent is done? By default, the model does: its run ends when it stops calling tools and writes an answer. That answer is a claim about the task, not proof that the task is finished. This guide separates the two decisions, shows the layers that verify completion (a self-check in the prompt, success criteria in code, a separate LLM judge, budgets as a backstop), and builds them on one small onboarding task with Promptise Foundry, including a mission with a judge.

[01]

Who decides when an AI agent is done?

The model decides when to *stop*; your system should decide whether the task is *done*. Put every success criterion you can express in code into a check that reads the real state (records, files, sent messages). Use a separate judge model, with a fresh context and the evidence, only for criteria code can't check, such as tone or policy. Feed what is still missing back to the agent and let it try again, inside a budget of attempts, time and cost. When the budget runs out, a person decides.

Key takeaways:

  • "The model stopped" is not "the task is done". A run ends when the model answers; it can end with work left over, honestly reported or not.

  • A self-check in the prompt makes the report more honest, but the same model is grading its own work, from its own transcript.

  • Code is the best judge where it can judge. A check against the real state is cheap, certain and the same every time.

  • A separate judge covers the rest, if it gets the right evidence. Without the facts the code verified, it marks finished work as unfinished.

  • Budgets decide when to give up. Attempts, iterations, time and cost limits turn an endless retry into an escalation.


[02]

Stopping is not finishing: who decides what

Decider

What it decides

Strength

Weakness

The model

When to stop calling tools and answer

Free, built into every loop

Grades its own work; can stop early or claim success

Self-check in the prompt

Whether to report gaps before answering

Cheap; improves the final report

Same model, same context, same blind spots

Code check

Whether hard criteria are met in the real state

Certain, repeatable, free

Only for what code can measure

Separate judge

Whether fuzzy criteria are met

Handles tone, policy, quality

Costs a model call; only as good as its evidence

Budget and limits

When to stop trying

Bounds cost and time

Says nothing about quality

A person

What happens after the budget runs out

Judgment and accountability

Slow; needs a clear handoff

Flow of who decides an agent is done: the agent's run ends with a claim, a code check and then a separate judge decide, gaps go back to the agent while budget remains, and a person takes over when it runs out.
FigureWho decides an AI agent is done: the agent stops, code and a judge decide, a budget ends it

The layers stack. The model's stop is the signal to check; the code check runs first because it is free and certain; the judge runs only when the code has nothing left to object to; the budget sits around the whole loop.


[03]

Self-verification: what a self-check prompt can and can't do

The cheapest layer is a checklist in the agent's instructions: before you answer, check each item and report anything that failed. It works on the *report*. In the run below, the agent said plainly that one email failed instead of claiming the task was complete.

What it can't do is make the model a reliable judge of its own work. It checks from its own transcript, so a tool that returned "sent" for a message that never arrived looks fine to it. It also shares the model's blind spots: if the model misread the task, its checklist is misread the same way. Techniques that verify the *content* of an answer, such as Chain-of-Verification and SelfCheckGPT, are covered in Chain-of-Verification and SelfCheckGPT: Catch Hallucinations; self-critique and retry loops in LLM Self-Correction: Self-Refine, Reflexion and CRITIC. This guide is about the *task*: did the work actually happen?


[04]

When to use a code check, a judge or both

Use a code check when the criterion is about state you can read: a record exists, a file compiles, tests pass, a payment has a status, every item in a list was processed. Use a judge when it is about meaning: an email is polite, a summary is faithful, a reply follows policy. Most real tasks have both kinds of criteria, so run the code first and ask the judge only about what is left.

Use a judge on its own only when nothing can be checked in code, and then treat its verdict as a probability, not a fact. LLM as a Judge vs a Jury: Making AI Evaluations Reliable covers how to make judges more reliable.


[05]

What you need

  • Python 3.10 or newer and uv pip install "promptise==1.7.0" python-dotenv.

  • An OpenAI key in .env (OPENAI_API_KEY); the agent and the judge use gpt-5-mini.

  • The example task in crm.py: onboard four customers (a CRM record and a welcome email each), with a mail outage for one customer's domain. missing() is the success criteria written as code.

Pythoncrm.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
"""A tiny CRM and mail outbox. Mail to one domain fails until the outage ends."""
from promptise import ToolError, tool

NEW_CUSTOMERS = [
    ("Ana Ruiz", "ana@example.com", "Pro"),
    ("Ben Okafor", "ben@example.com", "Starter"),
    ("Dana Lee", "dana@lee-family.net", "Pro"),
    ("Eli Novak", "eli@example.com", "Team"),
]
CRM: dict[str, dict] = {}
OUTBOX: list[dict] = []
OUTAGE = {"lee-family.net"}  # the mail provider rejects this domain for now


@tool
def create_customer(name: str, email: str, plan: str) -> str:
    """Create a customer record in the CRM."""
    CRM[email] = {"name": name, "plan": plan}
    return f"created {email}"


@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email."""
    if to.split("@")[1] in OUTAGE:
        raise ToolError(f"Mail service unavailable for {to.split('@')[1]}. Try again later.")
    OUTBOX.append({"to": to, "subject": subject, "body": body})
    return f"sent to {to}"


def task() -> str:
    people = "\n".join(f"- {n}, {e}, plan {p}" for n, e, p in NEW_CUSTOMERS)
    return f"Onboard these new customers: create each in the CRM and send each a welcome email.\n{people}"


def missing() -> list[str]:
    """The success criteria, in code: what is not done yet."""
    sent = {m["to"] for m in OUTBOX}
    gaps = [f"no CRM record for {e}" for _, e, _ in NEW_CUSTOMERS if e not in CRM]
    gaps += [f"no welcome email to {e}" for _, e, _ in NEW_CUSTOMERS if e not in sent]
    return gaps

[06]

Step 1: Let the agent check itself

The agent gets a checklist in its instructions and the task. When its run ends, the script prints what it says and what the code check finds:

Pythonself_check.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"""Step 1: the agent decides it is done, with a self-check in its instructions."""
import asyncio

from dotenv import load_dotenv
from promptise import build_agent

import crm

load_dotenv()

INSTRUCTIONS = """You onboard customers.
Before you give your final answer, check your work:
1. Every customer in the task has a CRM record.
2. Every customer got a welcome email that names their plan.
3. If a step failed, say which one and why. Do not report it as done."""


async def main():
    agent = await build_agent(model="openai:gpt-5-mini", instructions=INSTRUCTIONS,
                              extra_tools=[crm.create_customer, crm.send_email])
    try:
        result = await agent.ainvoke({"messages": [{"role": "user", "content": crm.task()}]})
    finally:
        await agent.shutdown()
    calls = [c["name"] for m in result["messages"] for c in getattr(m, "tool_calls", None) or []]
    print(f"tool calls: {len(calls)}")
    print("agent says:", result["messages"][-1].content)
    print("code check:", crm.missing() or "all done")


if __name__ == "__main__":
    asyncio.run(main())
Output
tool calls: 8
agent says: Summary
- CRM records: created for all 4 customers (Ana, Ben, Dana, Eli).
- Welcome emails: sent for 3 customers (Ana, Ben, Eli). Failed for 1 (Dana).
- Failure details: Dana's welcome email failed to send because the mail service for lee-family.net was unavailable (error: "Mail service unavailable for lee-family.net. Try again later." [...]
[per-customer status and next-step options left out]
code check: ['no welcome email to dana@lee-family.net']

(The answer's per-customer details and its offer of next steps are left out above.)

The self-check did its job: the report is honest about Dana's email. The run still *ended* with the task unfinished, because the model stopped and asked what to do. Nothing in the run decides whether to retry, wait or give up. If your code treats "the run returned" as "the task is done", this task is marked complete with a customer who never got their email. The ToolError here used the default retryable=False; marking a transient failure ToolError(..., retryable=True) tells the model it is worth trying again, but the decision still belongs outside the run.


[07]

Step 2: Put the success criteria in code and loop

The verification loop runs the agent, checks the real state with crm.missing(), and, if something is missing, runs the agent again with exactly what is missing. MAX_ATTEMPTS is the budget:

Pythonverify_loop.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"""Step 2: code decides when the task is done; the agent gets the gaps and tries again."""
import asyncio

from dotenv import load_dotenv
from promptise import build_agent

import crm
from self_check import INSTRUCTIONS

load_dotenv()
MAX_ATTEMPTS = 3  # the backstop: stop and escalate instead of retrying forever


async def main():
    agent = await build_agent(model="openai:gpt-5-mini", instructions=INSTRUCTIONS,
                              extra_tools=[crm.create_customer, crm.send_email],
                              max_agent_iterations=15)
    request = crm.task()
    try:
        for attempt in range(1, MAX_ATTEMPTS + 1):
            result = await agent.ainvoke({"messages": [{"role": "user", "content": request}]})
            gaps = crm.missing()
            print(f"attempt {attempt}: agent finished, code check: {gaps or 'all done'}")
            if not gaps:
                break
            crm.OUTAGE.clear()  # in real life: time passes, the mail service recovers
            request = (crm.task() + "\n\nSome of this is already done. Still missing:\n"
                       + "\n".join(f"- {g}" for g in gaps) + "\nDo only what is missing.")
        else:
            print(f"not done after {MAX_ATTEMPTS} attempts: escalate to a person with {gaps}")
    finally:
        await agent.shutdown()
    print("emails sent:", [m["to"] for m in crm.OUTBOX])
    print("last answer:", result["messages"][-1].content[:300])


asyncio.run(main())
Output
attempt 1: agent finished, code check: ['no welcome email to dana@lee-family.net']
attempt 2: agent finished, code check: all done
emails sent: ['ana@example.com', 'ben@example.com', 'eli@example.com', 'dana@lee-family.net']

(The script also prints the start of the agent's last answer, left out above.)

On the first attempt, the code check found the missing email. The outage then ended (the script clears it, standing in for time passing), and the second attempt got a request that listed only the gap. The agent sent the one missing email and the check passed.

Three details make this loop work:

  • The check reads the state, not the transcript. missing() looks at the CRM and the outbox, so it can't be fooled by a summary that says "done".

  • The feedback is specific. "Still missing: no welcome email to dana@lee-family.net. Do only what is missing." keeps the agent from redoing finished work. Pair it with idempotent tools, so a repeated call can't create a duplicate; see Idempotent Tool Calls: Make AI Agent Retries Safe.

  • The loop has an exit. After MAX_ATTEMPTS, it stops and escalates instead of retrying forever.


[08]

Step 3: Add a separate judge for what code can't check

The task has a criterion code can't check: every welcome email names the customer's plan and promises nothing the plan doesn't include. That needs a reader. Promptise Foundry's mission judge is a separate, tool-less agent that gets the objective, the success criteria and an evidence bundle, and answers with achieved, confidence and reasoning. MissionTracker runs your success_check first and only calls the judge when the check returns None:

Pythonjudge.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
"""Step 3: code checks what code can check; a separate judge checks the rest."""
import asyncio
import copy

from dotenv import load_dotenv
from promptise import build_agent
from promptise.runtime import MissionConfig
from promptise.runtime.mission import MissionEvidence, MissionTracker

import crm
from self_check import INSTRUCTIONS

load_dotenv()


def records_check(evidence: MissionEvidence) -> bool | None:
    """Hard criteria in code. None means: the code can't decide, ask the judge."""
    return False if crm.missing() else None


def tracker() -> MissionTracker:
    return MissionTracker(
        MissionConfig(
            enabled=True,
            objective="Onboard the new customers: CRM record and welcome email for each",
            success_criteria=("Every welcome email names the customer's plan and promises nothing "
                              "the plan doesn't include: no discounts, no free months."),
            eval_model="openai:gpt-5-mini",
        ),
        process_id="onboarding",
        success_check=records_check,
    )


def evidence(outbox, verified=None) -> MissionEvidence:
    """What the judge reads: facts the code verified, and one message per email
    (the judge sees 1,000 characters of state and 500 of each message)."""
    return MissionEvidence(state=verified or {}, conversation=[
        {"role": "tool", "content": f"to: {m['to']}\nsubject: {m['subject']}\n{m['body']}"} for m in outbox
    ])


async def main():
    print("before the work:", (await tracker().evaluate(evidence([]), "openai:gpt-5-mini")).reasoning)

    crm.OUTAGE.clear()
    agent = await build_agent(model="openai:gpt-5-mini", instructions=INSTRUCTIONS,
                              extra_tools=[crm.create_customer, crm.send_email])
    try:
        await agent.ainvoke({"messages": [{"role": "user", "content": crm.task()}]})
    finally:
        await agent.shutdown()

    verdict = await tracker().evaluate(evidence(crm.OUTBOX), "openai:gpt-5-mini")
    print(f"emails only:    achieved={verdict.achieved} confidence={verdict.confidence} "
          f"source={verdict.source}\n  {verdict.reasoning}")

    facts = {"verified_by_code": f"CRM records and sent welcome emails exist for all "
                                 f"{len(crm.NEW_CUSTOMERS)} customers"}
    verdict = await tracker().evaluate(evidence(crm.OUTBOX, facts), "openai:gpt-5-mini")
    print(f"with facts:     achieved={verdict.achieved} confidence={verdict.confidence} "
          f"source={verdict.source}\n  {verdict.reasoning}")

    bad = copy.deepcopy(crm.OUTBOX)  # the same outbox with one email that breaks the rule
    bad[1]["body"] += "\nAs a thank-you, your first 3 months are free."
    verdict = await tracker().evaluate(evidence(bad, facts), "openai:gpt-5-mini")
    print(f"one bad email:  achieved={verdict.achieved} confidence={verdict.confidence} "
          f"source={verdict.source}\n  {verdict.reasoning}")


asyncio.run(main())
Output
before the work: Programmatic success check returned False
emails only:    achieved=False confidence=0.9 source=llm
  All four customers were sent welcome emails that name their plan and do not promise discounts or free months, so the email portion meets the success criteria. However, there is no evidence in the conversation that CRM records were created for any of the new customers. The objective requires both a CRM record and a welcome email for each customer, so the mission is not complete.
with facts:     achieved=True confidence=0.95 source=llm
  Verified by code that CRM records and sent welcome emails exist for all 4 customers. Each email explicitly names the customer's plan (Pro, Pro, Starter, Team) in subject/body and only describes plan features; none mention discounts, free months, or other promises outside standard plan features. Therefore the success criteria are met.
one bad email:  achieved=False confidence=0.95 source=llm
  CRM records and welcome emails were created and sent for all four customers (verified_by_code). However one welcome email (to dana@lee-family.net) includes the line "As a thank-you, your first 3 months are free.", which promises free months and therefore violates the success criteria that emails must not promise discounts or free months. The other three emails correctly name the customer's plan and do not make unauthorized promises.

Four evaluations, four lessons:

  • Before the work, the code check returned False and no judge was called. Code answers first, for free.

  • With only the emails as evidence, the judge said the emails were fine but the mission wasn't achieved, because nothing showed the CRM records existed. A judge evaluates the whole objective with the evidence it has; it can't see what your code already verified.

  • With the code's facts in the state, the judge confirmed the emails and the mission completed.

  • With one email that promises free months, the judge caught it and named the email.

So give the judge two things: the facts your code verified (in state, which the judge sees up to 1,000 characters) and the material it must assess (in conversation, the last 10 messages, 500 characters each). Keep both short. The judge also sees tool calls only as names and argument hashes, not their results, so anything it needs from a result has to be in the evidence you pass. The judge's verdict is still a model's opinion: confidence helps you decide when to ask a person, and the judge's own reliability is something to test like any other eval.


[09]

Step 4: Make budgets the backstop

Every layer above needs a limit around it:

  • Per run: build_agent(max_agent_iterations=15) caps model calls in one run; hitting it mid-loop raises GraphIterationLimitError.

  • Per task: MAX_ATTEMPTS in the verification loop caps retries.

  • For a long-running agent: a mission with max_invocations and timeout_hours, and an autonomy budget on tool calls and turns per run.

For an agent that runs on triggers until its goal is met, the same decision lives in the process configuration. This is the onboarding task as a mission: the code check first, then the judge, a confidence threshold below which the mission pauses and escalates, automatic stop when achieved, and two limits that fail it:

Pythonprocess_config.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
"""The same decision for a long-running process: a mission with a code check, a judge and limits."""
from promptise.runtime import (BudgetConfig, EscalationTarget, MissionConfig, ProcessConfig)
from promptise.runtime.mission import MissionEvidence

import crm


def records_check(evidence: MissionEvidence) -> bool | None:
    return False if crm.missing() else None  # code first; None asks the judge


config = ProcessConfig(
    model="openai:gpt-5-mini",
    instructions="You onboard new customers from the signup queue.",
    mission=MissionConfig(
        enabled=True,
        objective="Onboard every customer in the signup queue",
        success_criteria="Every welcome email names the customer's plan and promises nothing extra.",
        success_check=records_check,     # hard criteria in code, before any judge call
        eval_model="openai:gpt-5-mini",  # the judge: a separate, tool-less agent
        eval_every=1,                    # judge after every successful run
        confidence_threshold=0.7,        # below this (not achieved): pause and escalate
        auto_complete=True,              # achieved: stop the process
        max_invocations=10,              # backstop: fail after 10 runs
        timeout_hours=4,                 # backstop: fail after 4 hours
        escalation=EscalationTarget(webhook_url="https://hooks.example.com/agent-alerts"),
    ),
    budget=BudgetConfig(enabled=True, max_tool_calls_per_run=30, max_llm_turns_per_run=15),
)

if __name__ == "__main__":
    m = config.mission
    print(f"mission: judge={m.eval_model}, threshold={m.confidence_threshold}, "
          f"limits={m.max_invocations} runs / {m.timeout_hours} h, auto_complete={m.auto_complete}")
Output
mission: judge=openai:gpt-5-mini, threshold=0.7, limits=10 runs / 4.0 h, auto_complete=True

When a mission is achieved, auto_complete=True stops the process. A verdict of "not achieved" with confidence below confidence_threshold pauses the mission and notifies the escalation target. max_invocations and timeout_hours fail it with a mission.failed event. If the judge call itself fails, the evaluation counts as not achieved with confidence 0.5, which pauses the mission at the default threshold. Running a mission step by step, with the API to pause, resume or fail it, is covered in Goal-Driven AI Agents: Missions With an LLM Judge.


[10]

Running it in production

  • Store the verdicts. Log every check and judge result with its reasoning. When a customer says "your agent said it was done", that log is the answer. Foundry journals each mission evaluation.

  • Make "not done" visible. A run that ends with gaps should produce a status, not just a polite message. Route it to a queue, a retry or a person, as in AI Agent Escalation: Page a Human When Limits Hit.

  • Test the judge. Keep a few known-good and known-bad examples (like the email with free months) and check the judge's verdicts in CI. How to Evaluate AI Agents: Evals, LLM Judges and pass^k shows how.

  • Price the judge. Every evaluation is a model call. Run the code check first, raise eval_every for frequent triggers, and use a small model where it is accurate enough.

  • Cap the spend. Retries multiply cost; AI Agent Cost Control: Budgets That Stop Runaway Spend puts a ceiling on it.


[11]

Common problems and fixes

Symptom

Cause

Fix

Task marked done with work missing

"Run returned" treated as "task done"

Check success criteria in code after every run

Agent says it finished something it didn't

Self-report from its own transcript

Verify against real state, not the answer

Agent redoes finished work on retry

Retry prompt repeats the whole task

Send only what is missing; make tools idempotent

Judge says "not achieved" for finished work

Judge lacks evidence code already verified

Put verified facts in the judge's state

Mission paused with "Evaluation failed"

Judge call failed or replied with invalid JSON

Check the reasoning, fix the cause, resume

Retries never stop

No attempt, time or cost limit

Add a budget and an escalation path


[12]

Limitations

  • Code checks need checkable criteria. Many tasks have a fuzzy part, and the judge for it is a model that can be wrong.

  • Foundry's programmatic check is a yes, no or "ask the judge". It returns a boolean (or None), so the specific gaps for the retry prompt come from your own code, as in verify_loop.py.

  • The judge sees a bounded slice of evidence (state up to 1,000 characters, 10 messages of 500 characters). Long evidence must be summarized by your code first.

  • One small task. The outage, the four customers and the bad email are constructed to show each layer; real tasks have more criteria and messier state.


[13]

FAQ

Who decides when an AI agent is done?

The model decides when its run stops, by answering instead of calling another tool. Whether the task is done should be decided outside the model: by code that checks success criteria against the real state, by a separate judge for criteria code can't check, and by budgets that end retries and hand the task to a person.

Should an agent verify its own work?

Yes, as a first layer: a self-check in the prompt makes the final report more honest about failures. It isn't enough on its own, because the same model grades its work from its own transcript. Add a check that reads the real state, and a separate judge where needed.

What is a self-verification loop for AI agents?

A loop around the agent: run it, check the result against success criteria, and if something is missing, run it again with exactly what is missing, until the check passes or a budget runs out. The check should read the real state, such as records or files, not the agent's answer.

When should I use an LLM judge instead of code to check completion?

When the criterion is about meaning rather than state: tone, faithfulness, policy compliance, quality. Check everything else in code first, and give the judge the facts the code verified, so it only judges what code can't.

How does Promptise Foundry stop an agent automatically when its goal is met?

Give the process a mission with an objective and success criteria. After each run, your success_check runs first, then a separate judge model if needed. When the mission is achieved, auto_complete=True stops the process. A low-confidence "not achieved" pauses it and escalates; max_invocations and timeout_hours fail it.

What threshold should a judge's confidence have?

Foundry's default confidence_threshold is 0.7: below it, a "not achieved" verdict pauses the mission and escalates. Tune it on examples where you know the right answer. A higher threshold sends more cases to people; a lower one lets the agent keep going on weaker evidence.


[14]

Next steps

  • Run a mission end to end and control it over the API: Goal-Driven AI Agents: Missions With an LLM Judge.

  • Verify the content of answers, not just the task: Chain-of-Verification and SelfCheckGPT: Catch Hallucinations.

  • Make judges more reliable: LLM as a Judge vs a Jury: Making AI Evaluations Reliable.

  • Understand the loop whose stop you are checking: ReAct Agent Pattern: Tool Execution Loop Explained Simply.

  • Hand over cleanly when the budget runs out: AI Agent Escalation: Page a Human When Limits Hit.

Contents

  1. 01Who decides when an AI agent is done?
  2. 02Stopping is not finishing: who decides what
  3. 03Self-verification: what a self-check prompt can and can't do
  4. 04When to use a code check, a judge or both
  5. 05What you need
  6. 06Step 1: Let the agent check itself
  7. 07Step 2: Put the success criteria in code and loop
  8. 08Step 3: Add a separate judge for what code can't check
  9. 09Step 4: Make budgets the backstop
  10. 10Running it in production
  11. 11Common problems and fixes
  12. 12Limitations
  13. 13FAQ
  14. 14Next steps

Learning paths

Want more structure? Paths put guides in order, like a short course.

See the paths →

> Testing and evals

Related guides

All guides in this topic →
  • // 01Intermediate · 18 minHow to Review AI-Generated Code: A Practical ChecklistHow to review AI-generated code: a checklist for intent, tests, invented APIs and packages, security and dead code, plus a script for the first pass.
  • // 02Advanced · 20 minDoes the Harness Matter? Test One Model Across HarnessesDoes the agent harness matter? How to test one model across coding agent harnesses fairly: hidden tests, repeated trials, one cost meter, and a sample run.
  • // 03Beginner · 7 minPrompt Evaluation for Beginners: A Simple Eval LoopEvaluate prompts with a simple eval loop: build a golden set, write pass/fail checks, compare prompt versions and read the failures. Real Python output.
  • // 04Intermediate · 17 minAI Agent Production Checklist: What to Check Before LaunchAn AI agent production checklist: security, approvals, guardrails, limits, cost, observability, evals, deployment and incident response, with code.
  • // 05Intermediate · 4 minAI Code Hallucinations: Why Plausible Output Fools YouWhy LLM code looks right but fails: plausibility vs correctness, hallucinated packages, a real regex that broke, and how to verify output by running it.
  • // 06Intermediate · 20 minBest LLM for AI Agents (2026): Picks, Prices, How to TestThe best LLM for AI agents in 2026 by use case, from public agent benchmarks and vendor prices, why the boards disagree, and how to test models yourself.

Keep going.

Browse every guide by topic and level, or follow a learning path that puts them in order.

All guidesLearning paths