Коли AI-агент має зупинитися у Python: runtime-ліміти (повний приклад)

Навчальний runnable приклад, який показує stop conditions, контрольований фінал і явний stop_reason.
На цій сторінці
  1. Що цей приклад демонструє
  2. Структура проєкту
  3. Як запустити
  4. Що ми будуємо в коді
  5. Код
  6. tools.py — навчальні tools із контрольованим фейлом
  7. llm.py — простий навчальний вибір наступної дії
  8. agent.py — цикл агента з policy-driven stop conditions
  9. main.py — три сценарії з різним фіналом
  10. requirements.txt
  11. Приклад виводу
  12. Що видно на практиці
  13. Що змінити в цьому прикладі
  14. Повний код на GitHub

Це повна навчальна реалізація прикладу зі статті Коли агент має зупинитись (і хто це вирішує).

Якщо ти ще не читав статтю, почни з неї. Тут фокус лише на коді: як система перевіряє stop conditions після кожного кроку і завершує роботу контрольовано.


Що цей приклад демонструє

  • Як агент рухається кроками, а рішення про зупинку приймає не модель, а policy на runtime-рівні
  • Як працюють базові stop conditions: goal_reached, step_limit, too_many_errors, no_progress
  • Які умови спрацьовують саме в цьому демо: goal_reached, too_many_errors, step_limit
  • Чому агент має повертати stop_reason, навіть якщо задача не завершена
  • Як три різні ситуації дають різний фінал: успіх vs аварійна зупинка

Структура проєкту

TEXT
foundations/
└── stop-conditions/
    └── python/
        ├── main.py            # запускає сценарії і друкує підсумок
        ├── agent.py           # цикл агента + перевірка stop conditions
        ├── llm.py             # простий шар рішень: next action
        ├── tools.py           # навчальні tools і контрольовані збої
        └── requirements.txt

Як запустити

1. Клонуй репозиторій і перейди в папку:

BASH
git clone https://github.com/AgentPatterns-tech/agentpatterns.git
cd foundations/stop-conditions/python

2. Встанови залежності (для цього прикладу зовнішніх пакетів немає):

BASH
pip install -r requirements.txt

3. Запусти демо:

BASH
python main.py

Що ми будуємо в коді

Ми будуємо простий агентний цикл з окремим контуром зупинки.

  • модель вибирає наступну дію
  • інструмент виконується і змінює стан
  • runtime рахує метрики (steps, errors, no_progress)
  • після кожного кроку policy перевіряє stop conditions
  • якщо спрацювала умова, цикл завершується з явним stop_reason

Ключова ідея: модель не вирішує, коли "достатньо". Це робить система.


Код

tools.py — навчальні tools із контрольованим фейлом

PYTHON
from typing import Any


def make_initial_state(user_id: int, fail_fetch_times: int) -> dict[str, Any]:
    return {
        "user_id": user_id,
        "fetch_calls": 0,
        "fail_fetch_times": fail_fetch_times,
    }


def fetch_orders(state: dict[str, Any]) -> dict[str, Any]:
    state["fetch_calls"] += 1

    if state["fetch_calls"] <= state["fail_fetch_times"]:
        return {"ok": False, "error": "orders_api_timeout"}

    orders = [
        {"id": "ord-2001", "total": 49.9, "status": "paid"},
        {"id": "ord-2002", "total": 19.0, "status": "shipped"},
    ]
    state["orders"] = orders
    return {"ok": True, "orders": orders}


def build_summary(state: dict[str, Any]) -> dict[str, Any]:
    orders = state.get("orders")
    if not orders:
        return {"ok": False, "error": "missing_orders"}

    summary = f"Prepared report for {len(orders)} recent orders."
    state["summary"] = summary
    return {"ok": True, "summary": summary}

llm.py — простий навчальний вибір наступної дії

PYTHON
from typing import Any


def choose_next_action(task: str, state: dict[str, Any]) -> dict[str, Any]:
    # Learning version: fixed policy keeps behavior easy to reason about.
    _ = task

    if "orders" not in state:
        return {"action": "fetch_orders", "parameters": {}}
    return {"action": "build_summary", "parameters": {}}

agent.py — цикл агента з policy-driven stop conditions

PYTHON
from dataclasses import dataclass
from typing import Any

from llm import choose_next_action
from tools import build_summary, fetch_orders, make_initial_state

TOOLS = {
    "fetch_orders": fetch_orders,
    "build_summary": build_summary,
}


@dataclass
class StopPolicy:
    max_steps: int
    max_errors: int
    max_no_progress: int


def evaluate_stop_conditions(
    state: dict[str, Any],
    steps: int,
    errors: int,
    no_progress: int,
    policy: StopPolicy,
) -> str | None:
    if "summary" in state:
        return "goal_reached"
    if steps >= policy.max_steps:
        return "step_limit"
    if errors >= policy.max_errors:
        return "too_many_errors"
    if no_progress >= policy.max_no_progress:
        return "no_progress"
    return None


def run_agent(task: str, user_id: int, fail_fetch_times: int, policy: StopPolicy) -> dict[str, Any]:
    state = make_initial_state(user_id=user_id, fail_fetch_times=fail_fetch_times)
    history: list[dict[str, Any]] = []

    steps = 0
    errors = 0
    no_progress = 0
    stop_reason: str | None = None

    while True:
        stop_reason = evaluate_stop_conditions(
            state=state,
            steps=steps,
            errors=errors,
            no_progress=no_progress,
            policy=policy,
        )
        if stop_reason is not None:
            break

        steps += 1

        call = choose_next_action(task, state)
        action = call["action"]
        history.append({"step": steps, "action": action, "status": "requested"})

        tool = TOOLS.get(action)
        if not tool:
            errors += 1
            no_progress += 1
            state["last_error"] = f"unknown_action:{action}"
            history.append({"step": steps, "action": action, "status": "error"})
        else:
            before_keys = set(state.keys())
            result = tool(state)

            if result.get("ok"):
                after_keys = set(state.keys())
                progress = len(after_keys - before_keys) > 0
                no_progress = 0 if progress else no_progress + 1
                state.pop("last_error", None)
                history.append({"step": steps, "action": action, "status": "ok"})
            else:
                errors += 1
                no_progress += 1
                state["last_error"] = result.get("error", "unknown_error")
                history.append({"step": steps, "action": action, "status": "error"})

    return {
        "done": stop_reason == "goal_reached",
        "stop_reason": stop_reason,
        "steps": steps,
        "errors": errors,
        "no_progress": no_progress,
        "summary": state.get("summary"),
        "history": history,
    }

main.py — три сценарії з різним фіналом

PYTHON
import json

from agent import StopPolicy, run_agent

TASK = "Build weekly orders summary"
POLICY = StopPolicy(max_steps=6, max_errors=2, max_no_progress=3)
STEP_LIMIT_POLICY = StopPolicy(max_steps=1, max_errors=2, max_no_progress=3)


def compact_result(result: dict) -> str:
    return (
        "{"
        f"\"done\": {str(bool(result.get('done'))).lower()}, "
        f"\"stop_reason\": {json.dumps(result.get('stop_reason'), ensure_ascii=False)}, "
        f"\"steps\": {int(result.get('steps', 0))}, "
        f"\"errors\": {int(result.get('errors', 0))}, "
        f"\"no_progress\": {int(result.get('no_progress', 0))}, "
        f"\"summary\": {json.dumps(result.get('summary'), ensure_ascii=False)}, "
        "\"history\": [{...}]"
        "}"
    )


def print_policy(policy: StopPolicy) -> None:
    print(
        "Policy:",
        json.dumps(
            {
                "max_steps": policy.max_steps,
                "max_errors": policy.max_errors,
                "max_no_progress": policy.max_no_progress,
            },
            ensure_ascii=False,
        ),
    )


def main() -> None:
    print("=== SCENARIO 1: GOAL REACHED ===")
    print_policy(POLICY)
    result_ok = run_agent(
        task=TASK,
        user_id=42,
        fail_fetch_times=1,
        policy=POLICY,
    )
    print("Run result:", compact_result(result_ok))

    print("\n=== SCENARIO 2: STOPPED BY ERROR LIMIT ===")
    print_policy(POLICY)
    result_stopped = run_agent(
        task=TASK,
        user_id=42,
        fail_fetch_times=10,
        policy=POLICY,
    )
    print("Run result:", compact_result(result_stopped))

    print("\n=== SCENARIO 3: STOPPED BY STEP LIMIT ===")
    print_policy(STEP_LIMIT_POLICY)
    result_step_limit = run_agent(
        task=TASK,
        user_id=42,
        fail_fetch_times=0,
        policy=STEP_LIMIT_POLICY,
    )
    print("Run result:", compact_result(result_step_limit))


if __name__ == "__main__":
    main()

requirements.txt

TEXT
# No external dependencies for this learning example.

Приклад виводу

TEXT
python main.py

=== SCENARIO 1: GOAL REACHED ===
Policy: {"max_steps": 6, "max_errors": 2, "max_no_progress": 3}
Run result: {"done": true, "stop_reason": "goal_reached", "steps": 3, "errors": 1, "no_progress": 0, "summary": "Prepared report for 2 recent orders.", "history": [{...}]}

=== SCENARIO 2: STOPPED BY ERROR LIMIT ===
Policy: {"max_steps": 6, "max_errors": 2, "max_no_progress": 3}
Run result: {"done": false, "stop_reason": "too_many_errors", "steps": 2, "errors": 2, "no_progress": 2, "summary": null, "history": [{...}]}

=== SCENARIO 3: STOPPED BY STEP LIMIT ===
Policy: {"max_steps": 1, "max_errors": 2, "max_no_progress": 3}
Run result: {"done": false, "stop_reason": "step_limit", "steps": 1, "errors": 0, "no_progress": 0, "summary": null, "history": [{...}]}

Примітка: у виводі history навмисно показано у скороченому вигляді — "history": [{...}].
Критерій коректності прикладу: агент завжди завершує цикл із явним stop_reason, а не працює нескінченно.


Що видно на практиці

SCENARIO 1SCENARIO 2SCENARIO 3
Фіналgoal_reachedtoo_many_errorsstep_limit
Задача завершена успішно
Цикл обмежений policy
Є явне пояснення, чому зупинились

Що змінити в цьому прикладі

  • Додай max_duration_sec і зупинку за wall-clock timeout
  • Додай окремий бюджет на tool-виклики (max_tool_calls)
  • Додай stop_reason_details, щоб логувати причину точніше
  • Додай четвертий сценарій, де спрацьовує no_progress

Повний код на GitHub

У репозиторії лежить повна версія цього демо: цикл агента, policy stop conditions і контрольований фінал.

Переглянути повний код на GitHub ↗
⏱️ 6 хв читанняОновлено 4 березня 2026 р.Складність: ★☆☆
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Дозволами на інструменти (allowlist / blocklist)
  • Kill switch та аварійна зупинка
  • Ідемпотентність і dedupe
  • Audit logs та трасування
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.

Автор

Микола — інженер, який будує інфраструктуру для продакшн AI-агентів.

Фокус: патерни агентів, режими відмов, контроль рантайму та надійність систем.

🔗 GitHub: https://github.com/mykolademyanov


Редакційна примітка

Ця документація підготовлена з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Контент базується на реальних відмовах, постмортемах та операційних інцидентах у розгорнутих AI-агентних системах.