Перейти к содержанию

Глава 16. Базовая схема рантайма

Как читать эту главу

Полезно держать в голове не абстрактный вопрос “как устроен рантайм”, а очень практичную задачу:

  • где именно должен жить основной цикл того же агента поддержки;
  • как не смешать слой политик, память, исполнение и телеметрию в один обработчик;
  • как собрать каркас, который выдержит не только демо, но и последующую выкладку.

Если на эти вопросы нет ясного ответа, система обычно продолжает работать только до первого серьезного изменения или инцидента.

1. Зачем нужна эталонная схема рантайма, если у тебя уже есть архитектура

Архитектурные главы полезны, потому что они дают язык и рамку. Но в какой-то момент почти у всех возникает один и тот же вопрос: “Хорошо, а как это должно выглядеть как система, которую реально можно собрать?”

Именно в этом состоит главная задача этой главы. Она должна помочь читателю перейти через важную границу: от согласия с аргументом книги к пониманию того, как этот аргумент превращается в работающую структуру системы.

В нашем сквозном support-кейсе это уже не теоретический вопрос. Агент умеет проверять статус пользователя, обращаться к памяти, открывать тикет через gateway и оставлять трассы. Но без явной схемы рантайма все эти шаги очень быстро расползаются по локальным хендлерам, ad hoc retries и случайным интеграционным обходам.

Вот тут и нужна эталонная схема рантайма.

Ее задача не в том, чтобы стать единственной возможной реализацией. Ее задача:

  • зафиксировать основные модули;
  • показать поток одного запуска;
  • отделить обязательные слои от необязательных улучшений;
  • дать команде отправную точку без лишней магии.

Поэтому эту главу полезно читать не только как главу про границы модулей, но и как главу про работающую структуру под давлением изменений. Настоящий вопрос в том, появилась ли у рантайма такая форма, которая выдержит новые политики, новые инструменты, длинные запуски, interrupts и давление раскатки, не распадаясь снова на хендлеры и исключения.

2. Минимально взрослый рантайм уже состоит не из одной модели

Очень полезно сразу отказаться от образа “агент = один вызов модели плюс инструменты”.

Минимально взрослый рантайм обычно включает:

  • слой входа;
  • координатор запуска;
  • policy hooks;
  • слой доступа к памяти;
  • слой исполнения инструментов и возможностей;
  • эмиттер телеметрии;
  • сборку результата.

То есть рантайм это не “место, где вызывается LLM”. Это управляемый цикл вокруг модели.

3. Как выглядит базовый поток одного запуска

Для эталонной реализации удобно мыслить один запуск примерно так:

  1. принять запрос и построить контекст запуска;
  2. выполнить предварительные проверки политик;
  3. собрать нужный контекст из памяти и retrieval;
  4. вызвать модель;
  5. если нужен вызов инструмента, прогнать его через слой исполнения;
  6. записать телеметрию;
  7. собрать финальный результат;
  8. запланировать фоновые обновления.

Это уже очень далеко от “просто чат с функциями”, и именно так и должно быть.

У базового рантайма уже есть несколько обязательных контрольных точек

flowchart LR
    A["Вход"] --> B["Контекст запуска"]
    B --> C["Предварительная проверка политик"]
    C --> D["Память / retrieval"]
    D --> E["Шаг модели"]
    E --> F{"Нужен инструмент?"}
    F -->|Нет| G["Сборка результата"]
    F -->|Да| H["Слой исполнения"]
    H --> I["Результат инструмента"]
    I --> E
    G --> J["Телеметрия + фоновые задачи"]

4. Какие модули полезно держать отдельными сразу

Есть несколько границ, которые выгодно выделить в коде уже в первой версии:

  • runtime.py или orchestrator.py для основного цикла;
  • policy.py для решений политик;
  • memory.py для retrieval и записей памяти;
  • catalog.py для реестра возможностей;
  • execution.py для вызова инструментов;
  • telemetry.py для spans и structured events.

Когда все это слеплено в один большой обработчик, первые демо получаются быстро, но взросление системы становится болезненным почти сразу.

Сквозной кейс: где живет защита от дублей

В support-triage рантайме защита от дубля тикета не должна быть спрятана в helpdesk adapter. runtime.py должен управлять контекстом запуска и веткой повтора, execution.py — выполнять write tool через идемпотентный контракт, telemetry.py — фиксировать side_effect_unknown, а policy.py и шлюз раскатки — решать, можно ли продолжать. Тогда один и тот же инцидент не расползается по обработчикам.

5. Не смешивай оркестрацию и business adapters

Одна из самых дорогих ошибок стартовой реализации: рантайм напрямую знает слишком много про конкретные внешние системы.

Тогда код оркестрации начинает содержать:

  • условную логику по конкретным инструментам;
  • знание о внешних формах payload;
  • локальные ретраи под конкретный API;
  • ad hoc redaction;
  • специальные обходы для отдельных интеграций.

Эталонный рантайм должен показывать обратную идею: оркестрация работает через contracts, а adapters живут на краю системы.

6. Пример минимальной структуры проекта

Ниже очень приземленный вариант стартовой структуры:

agent_runtime/
  orchestrator.py
  policy.py
  memory.py
  catalog.py
  execution.py
  telemetry.py
  models.py
  background.py

Это не “единственно правильная” структура. Но она уже помогает не свалить все в один файл и не смешать контрольные слои между собой.

7. Простой кодовый каркас orchestrator

Ниже не production runtime, а именно каркас-схема. Он показывает, как разделяются шаги запуска и где должны проходить ключевые контрольные точки.

from dataclasses import dataclass


@dataclass
class RunRequest:
    user_input: str
    tenant_id: str
    principal_id: str


@dataclass
class RunResult:
    output_text: str
    status: str


def run_agent(request: RunRequest) -> RunResult:
    policy_check(request)
    context = retrieve_context(request)
    model_output = call_model(request, context)

    if model_output.get("tool_request"):
        tool_result = execute_tool(model_output["tool_request"])
        emit_event("tool_execution", tool_result)
        model_output = call_model(request, context + [tool_result])

    schedule_background_updates(request, model_output)
    return RunResult(output_text=model_output["text"], status="success")

Идея здесь очень простая: даже базовый рантайм уже должен явно показывать policy, retrieval, исполнение инструментов и background updates как отдельные этапы.

8. Длинные запуски не необязательная надстройка, а часть baseline

Частая ошибка рантайма в том, что команда молча предполагает: любой полезный запуск должен завершаться в одном синхронном запросе. Это верно только пока система остается заточенной под демо.

В реальном support-кейсе часть запусков по природе длиннее:

  • ожидание approval;
  • ожидание инструментов с нестабильной задержкой;
  • ожидание второго model pass после исполнения инструментов;
  • ожидание deferred follow-up или background update.

Свежий материал OpenAI полезен тем, что рассматривает background execution как first-class concern рантайма, а не как обход проблем с timeout.3

Именно так на это и стоит смотреть в baseline runtime. Рантайм должен уже на старте различать:

  • synchronous runs, которые безопасно завершаются в одном foreground pass;
  • background runs, которые продолжаются после первого ответа;
  • resumable runs, которые ставятся на паузу из-за approval, внешнего ввода или отложенной работы.

Таксономия workflow-паттернов у Anthropic делает это еще острее, потому что разные orchestration patterns создают разные checkpoint-needs.1 У prompt chaining checkpoint обычно нужен между фиксированными стадиями, у routing он часто нужен только на границе классификации и handoff, parallelization требует видимости join-state, а orchestrator-workers требует parent/worker coordination state, который переживает частичное завершение.

LangGraph persistence показывает тот же принцип на уровне checkpoint granularity: durable state организуется по thread, checkpoints сохраняются на границах super-step, а успешные node writes внутри упавшего super-step могут сохраняться как pending writes, чтобы при resume не пересчитывать уже выполненные узлы.4 Архитектурный вывод: “checkpointing” — это не один boolean. Runtime должен явно назвать cursor для resume, границу допустимого replay и partial writes, которые нельзя продублировать после сбоя.

Их более поздняя работа про harness design добавляет сюда еще один практический урок для рантайма: в длинных application-run часто нужно явно различать compaction и context reset.5 Compaction оставляет того же агента на укороченной истории, поэтому continuity сохраняется, но context anxiety и накопленный drift могут остаться. Reset запускает нового агента с чистого листа и требует структурированный handoff artifact, который переносит состояние, следующие шаги и evaluation context. Это не просто prompt-трюк, а часть архитектуры рантайма, потому что как только resets входят в harness, платформа должна решить, какое состояние достаточно устойчиво, чтобы пережить reset, и какой review artifact получает следующий агент.

То есть bounded autonomy это не только вопрос policy. Это еще и вопрос дизайна runtime state: каждый разрешенный execution pattern приносит с собой собственную семантику pause, resume, reset и completion.

Если у рантайма нет явной формы для этих случаев, длинная работа почти всегда утекает в ad hoc retries, дублирующиеся запросы и скрытые переходы состояния.

8.1. Sandbox session state тоже является runtime state

У Sandbox Agents в OpenAI Agents SDK есть полезное разделение, которое стоит перенести в baseline runtime design: Manifest описывает fresh workspace contract, а конкретный run может получить live sandbox session, serialized session_state или стартовать из snapshot.9

Для эталонного рантайма это означает, что sandbox state нельзя прятать внутри tool adapter. Минимально полезная модель должна уметь хранить рядом с run_id и trace_id хотя бы:

  • sandbox_session_id;
  • sandbox_manifest_version;
  • sandbox_permissions_profile;
  • snapshot_id, если запуск стартовал из сохраненного workspace;
  • список materialized workspace entries или ссылку на проверенный manifest;
  • признак, можно ли этот sandbox resume, snapshot или нужно пересоздать.

Тогда длительная работа с файлами, shell и memory не превращается в непрозрачную папку на диске. Она становится частью того же runtime-control слоя, где уже живут approvals, background runs, capability sessions и trace evidence.

8.2. Stateful named agent instance как отдельная runtime topology

Cloudflare Agents SDK показывает другой полезный baseline-паттерн: агент может быть не только transient execution loop, но и именованный долговечный runtime object. В их модели каждый agent instance работает поверх Durable Object: у него есть собственное durable SQL/key-value state, WebSocket-соединения, scheduled tasks, возможность проснуться по событию и снова hibernate, когда он idle.8

В книгу это стоит переносить не как рекомендацию “используйте именно Cloudflare”, а как архитектурную форму. Если агент привязан к стабильному имени реальной сущности — customer case, project, device, tenant workspace, room, thread, research dossier, — runtime должен явно разделять:

  • agent_instance_id, который живет дольше одного run;
  • run_id, который описывает конкретное выполнение;
  • session_id, который описывает user-facing или transport session;
  • durable agent state, который переживает disconnect, deploy, hibernation и background wake-up;
  • external knowledge store, который не является private mutable state одного instance.

Такой паттерн особенно полезен для chat, voice, workflow и monitoring agents, где пользователь ожидает continuity, а не stateless request/response. Но он же добавляет риски, которые baseline runtime должен сделать видимыми: tenant isolation для named instances, leakage между WebSocket sessions, replay/resume после hibernation, scheduled side effects без активного пользователя и миграции durable state при изменении версии агента.

Поэтому эталонный runtime не обязан реализовывать Durable Objects, но ему нужна абстракция вроде AgentInstanceStore и SchedulerBoundary: место, где видно, какой named instance владеет состоянием, какие runs его меняли, какие scheduled tasks могут его разбудить и какие трассы доказывают безопасный resume.

Scheduling side особенно важен: Cloudflare показывает delayed, scheduled, cron и interval tasks, которые переживают restart, persist в SQLite и wake agent через Durable Object alarms.7 Архитектурный вывод для книги: schedule нельзя оставлять невидимым callback. Его нужно отражать как durable control record с owner instance, payload schema, idempotency key, overlap policy, next fire time и trace linkage.

Real-time side добавляет еще одну границу: connection state не равен agent state. В Cloudflare Agents WebSocket model у connection есть собственный id, uri, per-connection state, tags, lifecycle hooks и возможность выключить protocol messages вроде identity/state/MCP для конкретного connection.6 Для baseline runtime это означает, что broadcast, user presence, approval UI и streaming updates должны проходить через connection-scoped authorization и traceable fan-out, а не напрямую читать весь durable state агента.

9. Stateful tool sessions тоже должны входить в baseline

Как только execution layer начинает работать с stateful MCP-подобными capability, у baseline runtime появляется еще одна обязательная граница: состояние user-visible run не равно состоянию capability session.2

Это важно потому, что один пользовательский запуск теперь может включать:

  • один runtime run_id;
  • один или несколько MCP session_id для внешних capability;
  • progress notifications до финального ответа;
  • elicitation или промежуточные запросы, которые ставят run на паузу до нового ввода;
  • re-initialization, если capability session истекла до завершения run.

Если все это слепить в один непрозрачный state object, оператор уже не сможет объяснить, что именно resumed, что expired и что надо retry заново.

9.1. Рантайм должен относиться к lifecycle capability session как к first-class state

Минимально зрелый runtime обычно уже должен уметь хранить хотя бы:

  • run_id;
  • trace_id;
  • capability_session_id;
  • capability_session_status;
  • expires_at;
  • resume_token или другой continuation handle;
  • approval_state, если stateful tool flow был поставлен на паузу из-за approval.

Это не означает, что каждому инструменту нужна тяжелая session model. Это означает, что у рантайма должно быть место, где такой session state можно выразить, когда protocol этого требует.

9.2. Progress и elicitation должны входить в ту же модель resume-control

Еще один полезный вывод из stateful MCP guidance: progress events и elicitation requests нельзя считать экзотическим побочным каналом. Они должны входить в ту же runtime control model, что approvals и background resumption.

Это становится еще важнее, когда runtime поддерживает несколько orchestration patterns. Progress из ветки parallelization, из worker, делегированного через orchestrator-workers, или из gated-стадии prompt chaining не должен пропадать внутри pattern-specific adapters. Он должен попадать в одну общую control surface для status, resumption, expiry и operator visibility.

На практике baseline runtime выигрывает от одной общей модели состояний для:

  • in_progress работы, которая еще жива внутри capability session;
  • пауз waiting_for_input или waiting_for_approval;
  • resumable работы, которую можно продолжить в той же capability session;
  • reinitialize_required работы, где capability session истекла и ее нужно поднять заново перед продолжением.

Без этих различий истечение session обычно выглядит как случайная ошибка, хотя на деле это нормальное lifecycle event.

10. Что важно встроить в baseline с самого начала

Есть вещи, которые кажется соблазнительным “добавить потом”, но на деле их лучше заложить сразу:

  • trace_id на каждый run;
  • tenant/principal context;
  • policy decision hooks;
  • реестр возможностей вместо direct calls;
  • structured telemetry;
  • basic background task hook;
  • явную модель статусов запуска вроде queued / in_progress / completed / failed / canceled;
  • способ poll / resume / cancel для длинной работы без изобретения второго скрытого рантайма.

Если этого нет в baseline, потом система обычно дорастает до этого через болезненный retrofit.

11. Минимальный каркас для background и resumable work

Даже baseline runtime должен иметь простой способ представлять работу, которая живет дольше первого запроса.

from dataclasses import dataclass


@dataclass
class RunHandle:
    run_id: str
    status: str


def start_run(request: RunRequest) -> RunHandle:
    run_id = create_run_record(request)
    enqueue_run(run_id)
    return RunHandle(run_id=run_id, status="queued")


def continue_run(run_id: str):
    run = load_run(run_id)
    if run.status in {"canceled", "completed", "failed"}:
        return run

    update_status(run_id, "in_progress")
    result = execute_run_steps(run)
    update_status(run_id, result.status)
    return result

Смысл здесь не в усложнении. Смысл в том, чтобы длинная работа была достаточно явной: операторы могли ее наблюдать, клиенты могли ее опрашивать, а рантайм мог ее продолжать или отменять без догадок.

12. Что можно не усложнять в первой reference-версии

На старте не обязательно сразу добавлять:

  • сложный planner с множеством режимов;
  • многоступенчатую memory compaction pipeline;
  • сложную стратегию маршрутизации модели;
  • полный self-healing loop;
  • десяток golden paths.

Эталонный runtime полезен не максимальной мощностью, а ясностью формы. Лучше небольшая, но чистая реализация, чем “универсальный комбайн”, который никто не понимает.

13. Пример конфигурации рантайма

Ниже пример конфигурации, которая задает shape runtime, не вшивая все решения в код:

runtime:
  max_tool_hops: 3
  require_trace_id: true
  enable_background_updates: true
  default_model: gpt-5.4
  policy:
    precheck_required: true
  telemetry:
    emit_structured_events: true
  execution:
    gateway_required: true
  background:
    enabled: true
    resumable_runs: true
    allow_cancel: true
  capability_sessions:
    track_session_ids: true
    emit_progress_events: true
    support_reinit_on_expiry: true

Это полезно, потому что помогает держать контракт рантайма явным и переносимым между средами.

14. Частые ошибки

Очень типовые проблемы:

  • orchestration и adapters слеплены вместе;
  • policy checks вызываются не на каждом нужном пути;
  • memory подключена как случайный helper;
  • tool calls идут мимо catalog/gateway;
  • background updates отсутствуют;
  • telemetry добавлена по остаточному принципу;
  • длинная работа спрятана за ретраями, а не смоделирована явно;
  • background execution вроде бы есть, но операторы не могут нормально делать poll, resume или cancel.

То есть система вроде бы “работает”, но shape runtime уже мешает ее взрослению.

15. Быстрый тест зрелости для baseline runtime

Команде не стоит думать, что у нее уже есть reference runtime, только потому, что у нее есть working agent, несколько модулей и успешные демо.

Более сильная планка такая:

  • orchestration, policy, memory, execution и telemetry видимы как отдельные слои;
  • run context с самого начала несет identity и control metadata;
  • исполнение возможностей идет через contracts, а не через direct adapter calls;
  • tracing и background hooks встроены в base path, а не появляются как retrofit;
  • длинная работа имеет явную модель статусов и продолжения, а не прячется в скрытых ретраях;
  • один run можно объяснить как устойчивый skeleton, а не как рассыпанную local logic.

Если большинство этих условий не выполняется, у команды уже может быть implementation, но реального baseline runtime blueprint у нее пока нет.

16. Что сделать сразу

Сначала пройди по короткому списку и отдельно отметь все ответы «нет»:

  • Видны ли отдельные слои orchestration, policy, memory, execution и telemetry?
  • Есть ли единый run context с tenant/principal metadata?
  • Есть ли capability registry вместо прямых вызовов?
  • Встроены ли tracing hooks в базовый путь?
  • Есть ли безопасная точка для background updates?
  • Можно ли длинную работу явно поставить в очередь, наблюдать, продолжить и отменить?
  • Можно ли объяснить поток одного запуска без чтения десяти файлов сразу?

Если на несколько вопросов подряд ответ “нет”, у тебя пока не эталонный рантайм, а просто ранняя интеграция модели в продукт.

17. Что делать дальше

Сначала зафиксируй shape runtime, а потом добавляй поверх него policy layer и capability contracts.

Следующий логичный шаг в части VII: сделать поверх этой схемы явный слой политик и каталог возможностей, чтобы эталонная реализация стала уже почти эксплуатационным каркасом.