0.4.11
19 · Техническая документация

19. Agent loop и жизненный цикл turn'а

Этот документ — подробная разборка того, что происходит в ядре AstraCode за один ход диалога: от прихода SendMessage до записи TurnComplete в rollout. Для общей архитектуры см. 01-architecture.md, для протокола TUI↔server — 20-protocol.md.

1. Сущности

Сущность Файл Назначение
Session core/src/session/session.rs:10 Корневой объект сессии. Один на тред. Держит state, mailbox, активный turn, goal runtime, MCP, skills.
SessionConfiguration session.rs:34 Снимок конфигурации сессии (provider, model, sandbox, permissions, cwd, base instructions). Меняется через apply().
SessionTask tasks/mod.rs:185 Trait для одной фоновой задачи в сессии (Regular/Review/Compact/Index).
TurnContext session/turn_context.rs:44 Всё state, нужное одному turn'у: модель, reasoning effort, permissions, tools-config, environment, skills.
ActiveTurn session/session.rs Активный turn (один в каждый момент времени).
ToolRouter core/src/tools/router.rs:54 Маршрутизатор tool-вызовов на handler'ы.
ModelClientSession core/src/client.rs:378 Stream'инг-обёртка над ModelProvider для одного turn'а.
RolloutRecorder rollout/src/recorder.rs Запись событий turn'а в JSONL.

2. Точка входа

Клиент (TUI) шлёт turn/start (JSON-RPC, см. 20-protocol.md). Сервер маршрутизирует в AstraCodeMessageProcessor::process_turn_start(), который вызывает AstraCode::start_turn() на корневом state-объекте.

Внутри: 1. Создаётся новый RegularTask (или ReviewTask/CompactTask для других потоков). 2. Через Session::spawn_task() запускается фон с cancellation_token. 3. SessionTask::run() вызывает run_turn() (session/turn.rs:139) — главный цикл.

rust
// tasks/regular.rs:40-86
async fn run(self, session, ctx, input, cancellation_token) -> Option<String> {
    run_turn(session.session.clone(), ctx, input, prewarm, cancellation_token.child_token())
        .await
}

3. TurnContext

Создаётся при старте turn'а из текущего SessionConfiguration + per-turn overrides:

rust
pub struct TurnContext {
    pub model_info: ModelInfo,             // slug, context_window, input/output modalities
    pub reasoning_effort: ReasoningEffort, // none/low/medium/high
    pub reasoning_summary: ReasoningSummaryConfig,
    pub cwd: AbsolutePathBuf,
    pub environments: Vec<TurnEnvironmentSelection>,
    pub permission_profile: PermissionProfile,
    pub approval_policy: AskForApproval,
    pub tools_config: ToolsConfig,
    pub turn_metadata_state: Mutex<TurnMetadataState>, // sticky model routing
    pub turn_timing_state: Mutex<TurnTimingState>,
    pub turn_skills: TurnSkillsContext,    // загруженные skills для этого turn'а
    pub config: Arc<Config>,
    pub session_telemetry: TelemetryHandle,
    ...
}

TurnContext передаётся как Arc<TurnContext> во все sub-функции turn'а — он immutable для самого turn'а (изменения через apply() создают новый snapshot для следующего turn'а).

4. Построение prompt'а

build_prompt() (turn.rs:1039) собирает payload для LLM:

rust
Prompt {
    input: history.for_prompt(modalities),   // ResponseItem'ы из message_history
    tools: router.model_visible_specs(),     // tools, видимые модели в этом turn'е
    base_instructions: ctx.base_instructions, // system prompt
    personality: ctx.personality,
    output_schema: ctx.output_schema,
    parallel_tool_calls: ctx.tools_config.parallel_tool_calls,
}

Что входит в history

  • User messages, assistant messages, reasoning блоки, tool calls — все прошлые turn'ы.
  • Initial context (см. ниже).
  • Goal context, если активна цель.
  • MCP server resource snapshots, если модель их подтянула.

Initial context (первый turn сессии)

Перед первым user message в history попадает «начальная развёртка» (session/mod.rs:build_initial_context):

  1. Base instructionscore/src/review_prompt.md для review, иначе сборная из: - системного промпта (из templates) - developer_instructions из config - user_instructions (AGENTS.md и т.п.)
  2. Memory — в developer instructions подмешивается содержимое ~/.astracode/memories/memory_summary.md (memories/read/src/prompts.rs:32-33,42,49), если [memories].use_memories = true. Сам файл MEMORY.md в контекст не подставляется — это поисковый индекс (searchable registry), по которому агент ищет нужные записи (memories/read/templates/memories/read_path.md). Важно: сама функция памяти выключена по умолчанию (feature-флаг Feature::MemoryTool, default_enabled: false) — настройки [memories] не дают эффекта, пока флаг не включён. И даже при включённой функции агент только читает память, запись делает отдельный фоновый пайплайн (см. 07-memories.md).
  3. Skill metadata — короткие описания всех скиллов (для обнаружения).
  4. MCP server descriptions — какие серверы доступны.
  5. Apps/connectors — список настроенных connector'ов.
  6. Environment — git branch, cwd, OS, sandbox-mode.
  7. Collaboration mode hints — если включён нестандартный mode.

Tools, видимые модели

router.model_visible_specs() фильтрует tools по: - Включённым feature flags (Feature::*). - Доступным MCP-серверам. - Активным скиллам (turn_skills). - Текущим permissions. - Deferred tools — спрятаны в начале, появляются по tool_search запросу.

5. Вызов LLM и streaming

rust
// turn.rs:1908
let mut stream = client_session
    .stream(prompt, &ctx.model_info, &ctx.session_telemetry,
            ctx.reasoning_effort, ctx.reasoning_summary, ...)
    .await??;

ModelClientSession::stream() (см. model-provider/) делает HTTP POST к провайдеру (OpenAI/Anthropic/local) и парсит SSE-stream в события ResponseEvent.

ResponseEvent варианты

Loop в try_run_sampling_request() (turn.rs:1936-2293) обрабатывает:

ResponseEvent Что делает
Created Инициализация — создаётся response_id, эмитится TurnStarted
OutputItemAdded Начало нового item'а (assistant message / tool call / reasoning)
OutputItemDone item завершён — финализируем (записываем в history, вызываем tool)
TextDelta Кусок текста ассистента → AgentMessageContentDeltaEvent
ReasoningDelta Кусок reasoning → ReasoningContentDeltaEvent / ReasoningRawContentDeltaEvent
ServerModel Сервер сообщил, какую модель реально использовал (для fallback'ов)
ModelVerifications Метаданные про fingerprints, safety и т.п.

Plan mode

Если tools_config.plan_mode — текст ассистента дополнительно прогоняется через PlanModeStreamState (turn.rs:1934), который выделяет блоки /plan ... /plan и эмитит PlanDelta события.

6. Tool dispatch

Когда приходит OutputItemDone с ResponseItem::FunctionCall или CustomToolCall:

  1. Парсинг имени и аргументов JSON.
  2. Lookup в ToolRouter (tools/router.rs:54) — какой handler соответствует имени.
  3. Approval (astracode_delegate.rs:35-40): - Для local_shell/exec_commandExecApprovalRequest. - Для apply_patchApplyPatchApprovalRequest. - Для MCP — по default_tools_approval_mode сервера. - Approval отправляется как Server Request через JSON-RPC; UI отвечает approve|deny|allowOnce|block.
  4. Выполнение через соответствующий handler (см. enum ToolHandlerKind в 18-tools-catalog.md): - Shell → spawn'ит exec через ExecServer (sandbox). - ApplyPatch → крейт apply-patch. - Mcp → RPC к MCP-серверу. - SpawnAgentV2astracode_delegate::run_astracode_thread_interactive(). - Goal → мутация goal_runtime сессии.
  5. Эмиссия событий — Begin/Updated/End (например, ExecCommandBegin → много ExecCommandOutputDeltaExecCommandEnd).
  6. Возврат результата в model — output добавляется в history как ResponseItem::FunctionCallOutput для следующей итерации turn'а.

Parallel tool calls

Если модель эмитит несколько FunctionCall в одном OutputItemDone пакете И tools_config.parallel_tool_calls = true И все вовлечённые tools имеют supports_parallel_tool_calls = true — они выполняются конкурентно через tokio::join_all.

Иначе — последовательно.

7. Процесс подтверждения в деталях

text
Tool call requires approval
    ↓
core posts ServerRequest::item/commandExecution/requestApproval
    ↓ (через app-server → JSON-RPC)
TUI получает request
    ↓
ChatWidget показывает approval dialog
    ↓
Пользователь жмёт клавишу (по умолчанию, tui/src/keymap.rs:509-514):
  y — подтвердить, a — подтвердить для сессии, p — подтвердить для префикса,
  d — отклонить, Esc/n — отменить, c — прервать
    ↓
TUI отправляет response с decision: approve | deny | allowOnce | block
    ↓
core продолжает (выполняет tool / отказывает / эскалирует)

Если выбрано Allow always — путь/команда добавляются в ApprovedCommandPrefixSaved, и в этой сессии больше не спрашивается.

8. SessionTask trait и его варианты

rust
pub(crate) trait SessionTask: Send + Sync {
    fn kind(&self) -> TaskKind;
    fn span_name(&self) -> &'static str;
    fn run(self: Arc<Self>, session, ctx, input, cancellation_token) -> impl Future<Output = Option<String>>;
    fn abort(&self, session, ctx) -> impl Future<Output = ()>;
}

Реализации:

TaskKind Файл Что делает
Regular tasks/regular.rs:40 Стандартный turn по user message
Review tasks/review.rs:49 /review — запускает sub-agent с REVIEW_PROMPT, специальный output schema
Compact tasks/compact.rs /compact — генерирует резюме и заменяет history
Index tasks/index.rs Индексация (фоновая)

Одна сессия — максимум один активный ActiveTurn в каждый момент. Старт нового turn'а, пока работает старый, переключает через abort_all_tasks(TurnAbortReason::Replaced) (tasks/mod.rs:300).

Варианты TurnAbortReason (protocol/src/protocol.rs): Interrupted, Replaced, ReviewEnded, BudgetLimited.

9. Sub-agent / multi-agent

run_astracode_thread_interactive() (astracode_delegate.rs:65-150) создаёт независимый AstraCode thread как sub-agent:

  • Наследует от parent: environment_manager, auth_manager, mcp_manager, skills_manager (astracode_delegate.rs:81-85).
  • Forward events: события sub-agent'а перенаправляются как Event в parent session (astracode_delegate.rs:132-142) — UI видит их с префиксом.
  • Forward ops: parent может отправлять команды sub-agent'у (например, прервать).
  • Approval routing: approval-запросы sub-agent'а маршрутизируются на parent session (не на отдельный UI).

Используется в: - SpawnAgentV2 (multi-agent работа). - ReviewTask (review запускается как sub-agent с другим prompt'ом). - AgentJobs (CSV-batch workers).

run_astracode_thread_one_shot() — упрощённая версия для одноразовых вызовов.

10. Окончание turn'а

rust
// turn.rs:532
if !needs_follow_up {
    // 1. after_agent hooks
    fire_after_agent_hooks(...).await;
    // 2. эмит TurnCompleteEvent
    return Ok(TurnComplete);
}

При TurnComplete: - В rollout пишется событие с финальным last_agent_message. - В state.sqlite обновляется threads.updated_at и tokens_used. - ActiveTurn зачищается. - UI получает TurnCompletedNotification.

При TurnAborted (отмена, ошибка): - В history вставляется маркер InterruptedTurnHistoryMarker::ContextualUser или ::Developer (tasks/mod.rs:68-111) — чтобы следующая модель видела «turn был прерван». - Аналогично пишется в rollout как TurnAborted event. - Tool-вызовы, начатые в этом turn'е, остаются с маркером tool was aborted в output.

11. Прерывания (Esc, Ctrl-C)

text
Пользователь жмёт Esc в TUI
    ↓
TUI отправляет turn/interrupt (или ServerRequest cancel)
    ↓
Session::abort_all_tasks(TurnAbortReason::Interrupted)
    ↓
cancellation_token.cancel()
    ↓
В turn loop'е: проверка is_cancelled() в нескольких точках
    ↓
Если LLM stream в полёте → drop'ается; reqwest закрывает соединение
Если tool в работе → отдельный cancellation_token для tool, корректный shutdown
Если процесс exec → SIGTERM (через graceful timeout 100ms), потом SIGKILL
    ↓
Эмитится TurnAborted в rollout, history-маркер вставляется

Параметр GRACEFULL_INTERRUPTION_TIMEOUT_MS = 100 (tasks/mod.rs:65) — сколько ждать корректного завершения tool'а перед force kill.

12. История сообщений и компакция

message_history (внутри Session, защищён mutex'ом) — это Vec<HistoryEntry>, где каждая запись — ResponseItem (user/assistant/reasoning/tool_call/tool_output).

При приближении к model_context_window сессия: 1. Auto-warning в UI (когда tokens_used > model_auto_compact_token_limit). 2. По команде /compact или auto — запускается CompactTask: - Берёт всю историю кроме последних N turn'ов. - Просит модель сгенерировать сжатое резюме. - Заменяет старые сообщения на один ResponseItem::Message с резюме. - В rollout пишется event_msg типа Compacted.

Подробнее — см. core/src/compact.rs (если есть) и 04-slash-commands.md.

13. Hooks в turn loop'е

hook_engine.fire(Event::PreToolUse {...}) вызывается (см. 08-hooks.md) в нескольких точках:

Hook event Когда
SessionStart При создании сессии
UserPromptSubmit После получения нового user message, до build_prompt
PreToolUse До каждого tool dispatch
PermissionRequest До показа approval-диалога (можно auto-approve/deny)
PostToolUse После завершения tool'а (успех или ошибка)
Stop При завершении сессии

Hook может вернуть exit-code, который повлияет на дальнейшее (например, ненулевой exit на PreToolUse блокирует tool).

14. Memory интеграция

Phase 1 (извлечение) запускается асинхронно при SessionStart для прошлых rollout'ов (см. 07-memories.md).

Phase 2 (консолидация) — внутренний sub-agent без сетевого доступа, который агрегирует raw_memories.md в MEMORY.md (поисковый индекс) и memory_summary.md (краткая сводка).

В initial context при use_memories = true подмешивается memory_summary.md; MEMORY.md же служит поисковым индексом, по которому агент ищет нужные записи (см. memories/read/templates/memories/read_path.md).

15. Связь с rollout

На протяжении turn'а RolloutRecorder пишет:

text
event_msg: TurnStarted
event_msg: UserMessage
event_msg: ReasoningStart / ReasoningDelta / ReasoningEnd
event_msg: AssistantMessageDelta (много)
event_msg: AssistantMessage (финальное)
event_msg: ToolUseStart
event_msg: ToolUseEnd (с output)
... (повтор tool циклов)
event_msg: TurnComplete или TurnAborted

Запись асинхронна, буферизуется через MPSC канал в фоновый writer. Подробно — 21-rollout-format.md.

16. Связь с state DB

После каждого turn'а: - threads.updated_at, tokens_used обновляются. - tool_uses — записываются (имя, duration, success). - approvals — audit-журнал approval-решений.

Подробно — 23-state-db.md.