refactor(turn): extract preflight gate + per-iteration transcript prep into agent/turn_preflight_gate.py, agent/turn_iteration_prep.py

This commit is contained in:
Teknium
2026-09-02 16:27:44 -07:00
parent 9780739e12
commit a56f731ac6
4 changed files with 421 additions and 228 deletions
+32 -226
View File
@@ -23,12 +23,10 @@ from agent.fast_mode import begin_turn as begin_fast_mode_turn
from agent.message_metadata import append_message
from agent.turn_context import (
PreflightCompressionTimedOut,
_compression_warrants_another_preflight_pass,
build_turn_context,
reanchor_current_turn_user_idx,
)
from agent.turn_retry_state import TurnRetryState
from agent.turn_preflight import run_preflight_compression
from agent.runtime_cwd import resolve_agent_cwd
from agent.message_sanitization import (
_repair_tool_call_arguments,
@@ -63,6 +61,8 @@ from agent.turn_recovery import ( # noqa: F401 — resolved lazily by agent.tur
# Bind before the turn starts so a source-tree swap cannot load a skewed
# finalizer at turn end.
from agent.turn_finalizer import finalize_turn
from agent.turn_iteration_prep import prepare_iteration
from agent.turn_preflight_gate import run_preflight_gate
from agent.turn_request_assembly import assemble_api_request
from agent.turn_api_request import build_api_request
from agent.turn_api_call import perform_api_call
@@ -1781,144 +1781,13 @@ def run_conversation(
agent._safe_print(f"\n⚠️ Iteration budget exhausted ({agent.iteration_budget.used}/{agent.iteration_budget.max_total} iterations used)")
break
# Fire step_callback for gateway hooks (agent:step event)
if agent.step_callback is not None:
try:
prev_tools = []
for _idx, _m in enumerate(reversed(messages)):
if _m.get("role") == "assistant" and _m.get("tool_calls"):
_fwd_start = len(messages) - _idx
_results_by_id = {}
for _tm in messages[_fwd_start:]:
if _tm.get("role") != "tool":
break
_tcid = _tm.get("tool_call_id")
if _tcid:
_results_by_id[_tcid] = _tm.get("content", "")
prev_tools = [
{
"name": tc["function"]["name"],
"result": _results_by_id.get(tc.get("id")),
"arguments": tc["function"].get("arguments"),
}
for tc in _m["tool_calls"]
if isinstance(tc, dict)
]
break
agent.step_callback(api_call_count, prev_tools)
except Exception as _step_err:
logger.debug("step_callback error (iteration %s): %s", api_call_count, _step_err)
# Track tool-calling iterations for skill nudge.
# Counter resets whenever skill_manage is actually used.
if (agent._skill_nudge_interval > 0
and "skill_manage" in agent.valid_tool_names):
agent._iters_since_skill += 1
# ── Pre-API-call /steer drain ──────────────────────────────────
# Drain a /steer sent during the last API call into the newest tool message so
# it lands THIS iteration. Never put in a user message (breaks alternation).
_pre_api_steer = agent._drain_pending_steer()
if _pre_api_steer:
_injected = False
for _si in range(len(messages) - 1, -1, -1):
_sm = messages[_si]
if isinstance(_sm, dict) and _sm.get("role") == "tool":
from agent.prompt_builder import format_steer_marker
marker = format_steer_marker(_pre_api_steer)
existing = _sm.get("content", "")
if isinstance(existing, str):
_sm["content"] = existing + marker
else:
# Multimodal content blocks — append text block
try:
blocks = list(existing) if existing else []
blocks.append({"type": "text", "text": marker})
_sm["content"] = blocks
except Exception:
pass
_injected = True
logger.debug(
"Pre-API-call steer drain: injected into tool msg at index %d",
_si,
)
break
if not _injected:
# No tool message to inject into — put it back so
# the post-tool-execution drain picks it up later.
_lock = getattr(agent, "_pending_steer_lock", None)
if _lock is not None:
with _lock:
if agent._pending_steer:
agent._pending_steer = agent._pending_steer + "\n" + _pre_api_steer
else:
agent._pending_steer = _pre_api_steer
else:
existing = getattr(agent, "_pending_steer", None)
agent._pending_steer = (existing + "\n" + _pre_api_steer) if existing else _pre_api_steer
# ── Wall-clock run-budget wrap-up notice ───────────────────────
# One-shot at 80% of agent.run_budget_seconds: ask the model to wrap up via the
# same cache-safe channel as /steer (newest tool result); off with no budget.
if getattr(agent, "run_budget_seconds", None):
_maybe_inject_run_budget_wrapup(agent, messages)
# Reasoning lives in content via <think> tags for trajectory storage, but some
# providers (Moonshot) also need a 'reasoning_content' field; handle both here.
request_logger = getattr(agent, "logger", None) or logging.getLogger(__name__)
# Per-agent validation cursor skips re-parsing tool_call args already validated.
# Identity-keyed; a rewritten list breaks the prefix match and forces a re-scan.
_sanitize_cursor = getattr(agent, "_sanitize_args_cursor", None)
if _sanitize_cursor is None:
_sanitize_cursor = {}
try:
agent._sanitize_args_cursor = _sanitize_cursor
except Exception:
pass
repaired_tool_calls = agent._sanitize_tool_call_arguments(
messages,
logger=request_logger,
session_id=agent.session_id,
cursor=_sanitize_cursor,
_ip = prepare_iteration(
agent,
messages=messages,
api_call_count=api_call_count,
)
if repaired_tool_calls > 0:
request_logger.info(
"Sanitized %s corrupted tool_call arguments before request (session=%s)",
repaired_tool_calls,
agent.session_id or "-",
)
# Drop legacy hidden assistant placeholders carrying the raw interrupt scaffold
# before repair: replayed, the model echoes/self-replicates (#81841).
messages = [
msg for msg in messages
if not (
msg.get("display_kind") == "hidden"
and msg.get("role") == "assistant"
and (
(
isinstance(msg.get("content"), str)
and msg["content"].strip() == _INTERRUPT_SCAFFOLD_MARKER
)
or (
isinstance(msg.get("api_content"), str)
and msg["api_content"].strip() == _INTERRUPT_SCAFFOLD_MARKER
)
)
)
]
# Repair malformed role alternation (tool→user / user→user tails): providers
# return empty content on them and the empty-retry loop spins. The _with_cursor
# variant also recomputes the SessionDB flush cursor after compaction (#44837).
from agent.agent_runtime_helpers import repair_message_sequence_with_cursor
repaired_seq = repair_message_sequence_with_cursor(agent, messages)
if repaired_seq > 0:
request_logger.info(
"Repaired %s message-alternation violations before request (session=%s)",
repaired_seq,
agent.session_id or "-",
)
messages = _ip.messages
request_logger = _ip.request_logger
_rr = assemble_api_request(
agent,
@@ -1940,77 +1809,10 @@ def run_conversation(
request_pressure_tokens = _rr.request_pressure_tokens
total_chars = _rr.total_chars
_runtime_context_error = _ollama_context_limit_error(
agent, request_pressure_tokens
)
if _runtime_context_error:
final_response = _runtime_context_error
failed = True
_turn_exit_reason = "ollama_runtime_context_too_small"
append_message(messages, {"role": "assistant", "content": final_response})
agent._emit_status("❌ Ollama runtime context is too small for Hermes tool use")
api_call_count -= 1
agent._api_call_count = api_call_count
try:
agent.iteration_budget.refund()
except Exception:
pass
break
# Pre-API pressure check: tool results grow a turn and last_prompt_tokens lags
# them. Mirror the turn-prologue guard chain: defer on noisy estimate, skip in
# failure cooldown, then should_compress() (#11529).
_compressor = agent.context_compressor
_preflight_threshold = int(
getattr(_compressor, "threshold_tokens", 0) or 0
)
_provider_overflow_preflight = (
_provider_overflow_recovery_pending
and (
_preflight_threshold <= 0
or request_pressure_tokens >= _preflight_threshold
)
)
if (
_provider_overflow_recovery_pending
and not _provider_overflow_preflight
):
# The outer-loop rebuild includes system prompt, request-only injections and
# tool schemas; only that full request with output runway may be sent.
_provider_overflow_recovery_pending = False
# Compare fully assembled requests, not raw ``messages`` (which omit
# api_content, plugin injections, prefills, MoA context, ephemeral system text).
_previous_preflight_pressure = _last_preflight_pressure
_last_preflight_pressure = None
if (
_previous_preflight_pressure is not None
and request_pressure_tokens >= _preflight_threshold
and not _compression_warrants_another_preflight_pass(
_previous_preflight_pressure,
request_pressure_tokens,
_preflight_threshold,
)
):
# Stop proactive retries this turn without consuming the shared overflow-
# recovery budget; the provider's error handler may still compact.
_preflight_compression_blocked = True
logger.warning(
"Pre-API compression made insufficient progress: ~%s -> "
"~%s request tokens; skipping additional preflight passes",
f"{_previous_preflight_pressure:,}",
f"{request_pressure_tokens:,}",
)
_defer_preflight = getattr(
_compressor, "should_defer_preflight_to_real_usage", lambda _t: False
)
_pf = run_preflight_compression(
_pg = run_preflight_gate(
agent,
compressor=_compressor,
request_pressure_tokens=request_pressure_tokens,
provider_overflow_preflight=_provider_overflow_preflight,
preflight_compression_blocked=_preflight_compression_blocked,
defer_preflight=_defer_preflight,
moa_prepared_request=_moa_prepared_request,
_moa_prepared_request=_moa_prepared_request,
pending_moa_prepared_request=pending_moa_prepared_request,
messages=messages,
system_message=system_message,
@@ -2023,26 +1825,30 @@ def run_conversation(
effective_task_id=effective_task_id,
final_response=final_response,
failed=failed,
compression_timeout_exhausted=_compression_timeout_exhausted,
turn_exit_reason=_turn_exit_reason,
_turn_exit_reason=_turn_exit_reason,
_compression_timeout_exhausted=_compression_timeout_exhausted,
_preflight_compression_blocked=_preflight_compression_blocked,
_provider_overflow_recovery_pending=_provider_overflow_recovery_pending,
_last_preflight_pressure=_last_preflight_pressure,
)
messages = _pf.messages
active_system_prompt = _pf.active_system_prompt
conversation_history = _pf.conversation_history
api_call_count = _pf.api_call_count
compression_attempts = _pf.compression_attempts
pending_moa_prepared_request = _pf.pending_moa_prepared_request
final_response = _pf.final_response
failed = _pf.failed
_compression_timeout_exhausted = _pf.compression_timeout_exhausted
_turn_exit_reason = _pf.turn_exit_reason
if _pf.last_preflight_pressure is not None:
_last_preflight_pressure = _pf.last_preflight_pressure
if _pf.action == "return":
return _pf.result
if _pf.action == "break":
pending_moa_prepared_request = _pg.pending_moa_prepared_request
messages = _pg.messages
active_system_prompt = _pg.active_system_prompt
conversation_history = _pg.conversation_history
api_call_count = _pg.api_call_count
compression_attempts = _pg.compression_attempts
final_response = _pg.final_response
failed = _pg.failed
_turn_exit_reason = _pg._turn_exit_reason
_compression_timeout_exhausted = _pg._compression_timeout_exhausted
_preflight_compression_blocked = _pg._preflight_compression_blocked
_provider_overflow_recovery_pending = _pg._provider_overflow_recovery_pending
_last_preflight_pressure = _pg._last_preflight_pressure
if _pg.action == "return":
return _pg.result
if _pg.action == "break":
break
if _pf.action == "continue":
if _pg.action == "continue":
continue
# Thinking spinner for quiet mode (animated during API call)
+189
View File
@@ -0,0 +1,189 @@
"""Per-iteration transcript preparation for the conversation turn loop, run before the request
is assembled: the ``agent:step`` callback, skill-nudge counter, pre-API ``/steer`` drain into
the newest tool result (never a user message — alternation), run-budget wrap-up notice,
tool_call argument sanitization, legacy interrupt-scaffold ghost-row drop and role-alternation
repair. Extracted from ``run_conversation``; nothing here imports
``agent.conversation_loop`` at module level (cycle).
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Any, Dict, Optional
logger = logging.getLogger("agent.conversation_loop")
@dataclass
class IterationPrep:
"""Always ``action == "fallthrough"``. ``messages`` is the (possibly filtered) transcript
and ``request_logger`` the per-request logger the caller keeps using."""
action: str
messages: Any
request_logger: Any
def prepare_iteration(
agent: Any,
*,
messages: Any,
api_call_count: Any,
) -> IterationPrep:
"""Prepare ``messages`` for this iteration in the original order. Every mutation here is
cache-safe by construction: steer text lands in the newest tool result, the ghost-row
filter only drops hidden scaffold placeholders, and repair runs BEFORE the request build."""
from agent.conversation_loop import (
_INTERRUPT_SCAFFOLD_MARKER,
_maybe_inject_run_budget_wrapup,
)
def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> IterationPrep:
return IterationPrep(
action=action,
messages=messages,
request_logger=request_logger,
)
# Fire step_callback for gateway hooks (agent:step event)
if agent.step_callback is not None:
try:
prev_tools = []
for _idx, _m in enumerate(reversed(messages)):
if _m.get("role") == "assistant" and _m.get("tool_calls"):
_fwd_start = len(messages) - _idx
_results_by_id = {}
for _tm in messages[_fwd_start:]:
if _tm.get("role") != "tool":
break
_tcid = _tm.get("tool_call_id")
if _tcid:
_results_by_id[_tcid] = _tm.get("content", "")
prev_tools = [
{
"name": tc["function"]["name"],
"result": _results_by_id.get(tc.get("id")),
"arguments": tc["function"].get("arguments"),
}
for tc in _m["tool_calls"]
if isinstance(tc, dict)
]
break
agent.step_callback(api_call_count, prev_tools)
except Exception as _step_err:
logger.debug("step_callback error (iteration %s): %s", api_call_count, _step_err)
# Track tool-calling iterations for skill nudge.
# Counter resets whenever skill_manage is actually used.
if (agent._skill_nudge_interval > 0
and "skill_manage" in agent.valid_tool_names):
agent._iters_since_skill += 1
# ── Pre-API-call /steer drain ──────────────────────────────────
# Drain a /steer sent during the last API call into the newest tool message so
# it lands THIS iteration. Never put in a user message (breaks alternation).
_pre_api_steer = agent._drain_pending_steer()
if _pre_api_steer:
_injected = False
for _si in range(len(messages) - 1, -1, -1):
_sm = messages[_si]
if isinstance(_sm, dict) and _sm.get("role") == "tool":
from agent.prompt_builder import format_steer_marker
marker = format_steer_marker(_pre_api_steer)
existing = _sm.get("content", "")
if isinstance(existing, str):
_sm["content"] = existing + marker
else:
# Multimodal content blocks — append text block
try:
blocks = list(existing) if existing else []
blocks.append({"type": "text", "text": marker})
_sm["content"] = blocks
except Exception:
pass
_injected = True
logger.debug(
"Pre-API-call steer drain: injected into tool msg at index %d",
_si,
)
break
if not _injected:
# No tool message to inject into — put it back so
# the post-tool-execution drain picks it up later.
_lock = getattr(agent, "_pending_steer_lock", None)
if _lock is not None:
with _lock:
if agent._pending_steer:
agent._pending_steer = agent._pending_steer + "\n" + _pre_api_steer
else:
agent._pending_steer = _pre_api_steer
else:
existing = getattr(agent, "_pending_steer", None)
agent._pending_steer = (existing + "\n" + _pre_api_steer) if existing else _pre_api_steer
# ── Wall-clock run-budget wrap-up notice ───────────────────────
# One-shot at 80% of agent.run_budget_seconds: ask the model to wrap up via the
# same cache-safe channel as /steer (newest tool result); off with no budget.
if getattr(agent, "run_budget_seconds", None):
_maybe_inject_run_budget_wrapup(agent, messages)
# Reasoning lives in content via <think> tags for trajectory storage, but some
# providers (Moonshot) also need a 'reasoning_content' field; handle both here.
request_logger = getattr(agent, "logger", None) or logger # same name as the origin module
# Per-agent validation cursor skips re-parsing tool_call args already validated.
# Identity-keyed; a rewritten list breaks the prefix match and forces a re-scan.
_sanitize_cursor = getattr(agent, "_sanitize_args_cursor", None)
if _sanitize_cursor is None:
_sanitize_cursor = {}
try:
agent._sanitize_args_cursor = _sanitize_cursor
except Exception:
pass
repaired_tool_calls = agent._sanitize_tool_call_arguments(
messages,
logger=request_logger,
session_id=agent.session_id,
cursor=_sanitize_cursor,
)
if repaired_tool_calls > 0:
request_logger.info(
"Sanitized %s corrupted tool_call arguments before request (session=%s)",
repaired_tool_calls,
agent.session_id or "-",
)
# Drop legacy hidden assistant placeholders carrying the raw interrupt scaffold
# before repair: replayed, the model echoes/self-replicates (#81841).
messages = [
msg for msg in messages
if not (
msg.get("display_kind") == "hidden"
and msg.get("role") == "assistant"
and (
(
isinstance(msg.get("content"), str)
and msg["content"].strip() == _INTERRUPT_SCAFFOLD_MARKER
)
or (
isinstance(msg.get("api_content"), str)
and msg["api_content"].strip() == _INTERRUPT_SCAFFOLD_MARKER
)
)
)
]
# Repair malformed role alternation (tool→user / user→user tails): providers
# return empty content on them and the empty-retry loop spins. The _with_cursor
# variant also recomputes the SessionDB flush cursor after compaction (#44837).
from agent.agent_runtime_helpers import repair_message_sequence_with_cursor
repaired_seq = repair_message_sequence_with_cursor(agent, messages)
if repaired_seq > 0:
request_logger.info(
"Repaired %s message-alternation violations before request (session=%s)",
repaired_seq,
agent.session_id or "-",
)
return _verdict("fallthrough")
+197
View File
@@ -0,0 +1,197 @@
"""Pre-API pressure gate for the conversation turn loop: the Ollama runtime-context floor,
the provider-overflow re-check arming, the insufficient-progress blocker (compares fully
assembled requests, not raw ``messages``) and the call into
``turn_preflight.run_preflight_compression``. Extracted from ``run_conversation``; nothing
here imports ``agent.conversation_loop`` at module level (cycle).
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Any, Dict, Optional
from agent.message_metadata import append_message
from agent.turn_context import _compression_warrants_another_preflight_pass
from agent.turn_preflight import run_preflight_compression
logger = logging.getLogger("agent.conversation_loop")
@dataclass
class PreflightGateVerdict:
"""``action``: ``"fallthrough"`` (make the API call), ``"continue"`` (compaction/window
growth — re-measure), ``"break"`` (turn ends) or ``"return"`` (``result`` is the turn's
result dict). The other fields are the loop locals the gate rebinds."""
action: str
pending_moa_prepared_request: Any
messages: Any
active_system_prompt: Any
conversation_history: Any
api_call_count: Any
compression_attempts: Any
final_response: Any
failed: Any
_turn_exit_reason: Any
_compression_timeout_exhausted: Any
_preflight_compression_blocked: Any
_provider_overflow_recovery_pending: Any
_last_preflight_pressure: Any
result: Optional[Dict[str, Any]] = None
def run_preflight_gate(
agent: Any,
*,
request_pressure_tokens: Any,
_moa_prepared_request: Any,
pending_moa_prepared_request: Any,
messages: Any,
system_message: Any,
user_message: Any,
active_system_prompt: Any,
conversation_history: Any,
api_call_count: Any,
compression_attempts: Any,
max_compression_attempts: Any,
effective_task_id: Any,
final_response: Any,
failed: Any,
_turn_exit_reason: Any,
_compression_timeout_exhausted: Any,
_preflight_compression_blocked: Any,
_provider_overflow_recovery_pending: Any,
_last_preflight_pressure: Any,
) -> PreflightGateVerdict:
"""Run the pre-API guard chain in the original order (#11529). ``_last_preflight_pressure``
is consumed here (set to None) and re-armed only by a compression pass, so a blocked
preflight never compares against a stale figure."""
from agent.conversation_loop import (
_ollama_context_limit_error,
)
def _verdict(action: str, result: Optional[Dict[str, Any]] = None) -> PreflightGateVerdict:
return PreflightGateVerdict(
action=action,
pending_moa_prepared_request=pending_moa_prepared_request,
messages=messages,
active_system_prompt=active_system_prompt,
conversation_history=conversation_history,
api_call_count=api_call_count,
compression_attempts=compression_attempts,
final_response=final_response,
failed=failed,
_turn_exit_reason=_turn_exit_reason,
_compression_timeout_exhausted=_compression_timeout_exhausted,
_preflight_compression_blocked=_preflight_compression_blocked,
_provider_overflow_recovery_pending=_provider_overflow_recovery_pending,
_last_preflight_pressure=_last_preflight_pressure,
result=result,
)
_runtime_context_error = _ollama_context_limit_error(
agent, request_pressure_tokens
)
if _runtime_context_error:
final_response = _runtime_context_error
failed = True
_turn_exit_reason = "ollama_runtime_context_too_small"
append_message(messages, {"role": "assistant", "content": final_response})
agent._emit_status("❌ Ollama runtime context is too small for Hermes tool use")
api_call_count -= 1
agent._api_call_count = api_call_count
try:
agent.iteration_budget.refund()
except Exception:
pass
return _verdict("break")
# Pre-API pressure check: tool results grow a turn and last_prompt_tokens lags
# them. Mirror the turn-prologue guard chain: defer on noisy estimate, skip in
# failure cooldown, then should_compress() (#11529).
_compressor = agent.context_compressor
_preflight_threshold = int(
getattr(_compressor, "threshold_tokens", 0) or 0
)
_provider_overflow_preflight = (
_provider_overflow_recovery_pending
and (
_preflight_threshold <= 0
or request_pressure_tokens >= _preflight_threshold
)
)
if (
_provider_overflow_recovery_pending
and not _provider_overflow_preflight
):
# The outer-loop rebuild includes system prompt, request-only injections and
# tool schemas; only that full request with output runway may be sent.
_provider_overflow_recovery_pending = False
# Compare fully assembled requests, not raw ``messages`` (which omit
# api_content, plugin injections, prefills, MoA context, ephemeral system text).
_previous_preflight_pressure = _last_preflight_pressure
_last_preflight_pressure = None
if (
_previous_preflight_pressure is not None
and request_pressure_tokens >= _preflight_threshold
and not _compression_warrants_another_preflight_pass(
_previous_preflight_pressure,
request_pressure_tokens,
_preflight_threshold,
)
):
# Stop proactive retries this turn without consuming the shared overflow-
# recovery budget; the provider's error handler may still compact.
_preflight_compression_blocked = True
logger.warning(
"Pre-API compression made insufficient progress: ~%s -> "
"~%s request tokens; skipping additional preflight passes",
f"{_previous_preflight_pressure:,}",
f"{request_pressure_tokens:,}",
)
_defer_preflight = getattr(
_compressor, "should_defer_preflight_to_real_usage", lambda _t: False
)
_pf = run_preflight_compression(
agent,
compressor=_compressor,
request_pressure_tokens=request_pressure_tokens,
provider_overflow_preflight=_provider_overflow_preflight,
preflight_compression_blocked=_preflight_compression_blocked,
defer_preflight=_defer_preflight,
moa_prepared_request=_moa_prepared_request,
pending_moa_prepared_request=pending_moa_prepared_request,
messages=messages,
system_message=system_message,
user_message=user_message,
active_system_prompt=active_system_prompt,
conversation_history=conversation_history,
api_call_count=api_call_count,
compression_attempts=compression_attempts,
max_compression_attempts=max_compression_attempts,
effective_task_id=effective_task_id,
final_response=final_response,
failed=failed,
compression_timeout_exhausted=_compression_timeout_exhausted,
turn_exit_reason=_turn_exit_reason,
)
messages = _pf.messages
active_system_prompt = _pf.active_system_prompt
conversation_history = _pf.conversation_history
api_call_count = _pf.api_call_count
compression_attempts = _pf.compression_attempts
pending_moa_prepared_request = _pf.pending_moa_prepared_request
final_response = _pf.final_response
failed = _pf.failed
_compression_timeout_exhausted = _pf.compression_timeout_exhausted
_turn_exit_reason = _pf.turn_exit_reason
if _pf.last_preflight_pressure is not None:
_last_preflight_pressure = _pf.last_preflight_pressure
if _pf.action == "return":
return _verdict("return", _pf.result)
if _pf.action == "break":
return _verdict("break")
if _pf.action == "continue":
return _verdict("continue")
return _verdict("fallthrough")
@@ -173,12 +173,13 @@ class TestSendPathBuildIsWiredToTheClone:
import inspect
import agent.turn_context as tc
import agent.turn_request_assembly as ra
# The history build lives in turn_context.build_api_messages; the
# prefill insert stays in conversation_loop. Scan both homes.
# prefill insert in turn_request_assembly.assemble_api_request. Scan all homes.
nodes = [
node
for mod in (cl, tc)
for mod in (cl, tc, ra)
for node in ast.walk(ast.parse(inspect.getsource(mod)))
]