Canonical agent loop (chapter 5, Phase A foundation). The async generator yields messages and stream events to the consumer. The final ``Terminal`` is written to ``terminal_holder.value`` just before the generator returns (Python async generators cannot return values: PEP 525). Call
(
params: QueryParams,
*,
terminal_holder: TerminalHolder | None = None,
)
| 1017 | |
| 1018 | # Max tools to run in parallel (TS default: 10, configurable via env var). |
| 1019 | async def query( |
| 1020 | params: QueryParams, |
| 1021 | *, |
| 1022 | terminal_holder: TerminalHolder | None = None, |
| 1023 | ) -> AsyncGenerator[Message | StreamEvent, None]: |
| 1024 | """Canonical agent loop (chapter 5, Phase A foundation). |
| 1025 | |
| 1026 | The async generator yields messages and stream events to the consumer. |
| 1027 | The final ``Terminal`` is written to ``terminal_holder.value`` just |
| 1028 | before the generator returns (Python async generators cannot return |
| 1029 | values: PEP 525). Callers who care about the terminal pass their own |
| 1030 | ``TerminalHolder`` and read its ``.value`` after iteration. |
| 1031 | |
| 1032 | See :func:`run_query` for a convenience helper that consumes the |
| 1033 | generator and returns ``(messages, terminal)``. |
| 1034 | |
| 1035 | This PR (Phase A) introduces the typed Terminal infrastructure; |
| 1036 | recovery integration, stop hooks, token budget, model fallback, |
| 1037 | and continuation nudge land in subsequent PRs. |
| 1038 | """ |
| 1039 | _diag = os.environ.get("CLAWCODEX_DEBUG", "").lower() in ("1", "true", "yes") |
| 1040 | holder = terminal_holder or TerminalHolder() |
| 1041 | # Inner-only flag for the future outer two-layer wrapper (Phase G). |
| 1042 | # Until that lands, the flag is local; set_terminal still writes it |
| 1043 | # so every exit site uses the canonical helper. |
| 1044 | natural_termination: list[bool] = [False] |
| 1045 | state = QueryState( |
| 1046 | messages=list(params.messages), |
| 1047 | tool_use_context=params.tool_use_context, |
| 1048 | max_output_tokens_override=params.max_output_tokens_override, |
| 1049 | ) |
| 1050 | config = build_query_config() |
| 1051 | # Created once per query() call, persisting across turns — mirrors TS |
| 1052 | # query.ts:311 (state built before the while(true) at :327). Any |
| 1053 | # successful tool result resets the counters inside the guard. |
| 1054 | tool_failure_guard_state = create_tool_failure_loop_guard_state() |
| 1055 | # ch05 round-3 G2: snapshot the turn-token baseline + budget into the |
| 1056 | # bootstrap globals (zero callers before this — without the snapshot, |
| 1057 | # get_turn_output_tokens() returns SESSION-cumulative tokens and the |
| 1058 | # budget check is silently wrong after any prior output). Tracker is |
| 1059 | # once-per-query (TS query.ts:299) — per-iteration construction would |
| 1060 | # disable diminishing-returns detection. |
| 1061 | from ..bootstrap.state import snapshot_output_tokens_for_turn |
| 1062 | |
| 1063 | # Top-level queries only: a nested subagent query() (Agent tool runs |
| 1064 | # inside the main turn's tool phase) or a sidechannel must NOT |
| 1065 | # re-snapshot — it would null the budget, re-baseline the turn counter, |
| 1066 | # and zero the continuation count mid-turn. TS snapshots only at the |
| 1067 | # REPL surface (REPL.tsx:2944); agent_id mirrors check_token_budget's |
| 1068 | # own subagent discriminator. |
| 1069 | if ( |
| 1070 | getattr(params.tool_use_context, "agent_id", None) is None |
| 1071 | and params.query_source not in ("compact", "session_memory") |
| 1072 | ): |
| 1073 | snapshot_output_tokens_for_turn(params.token_budget) |
| 1074 | budget_tracker = create_budget_tracker() |
| 1075 | |
| 1076 | # ch07 round-3 G1: the orchestrator lane sources tool lookup AND the |