diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index bdf9ff0204..14f8be56be 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -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 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) diff --git a/agent/turn_iteration_prep.py b/agent/turn_iteration_prep.py new file mode 100644 index 0000000000..e880077493 --- /dev/null +++ b/agent/turn_iteration_prep.py @@ -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 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") diff --git a/agent/turn_preflight_gate.py b/agent/turn_preflight_gate.py new file mode 100644 index 0000000000..738965719d --- /dev/null +++ b/agent/turn_preflight_gate.py @@ -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") diff --git a/tests/agent/test_send_path_history_isolation.py b/tests/agent/test_send_path_history_isolation.py index 1f5f6727a6..200efea5c4 100644 --- a/tests/agent/test_send_path_history_isolation.py +++ b/tests/agent/test_send_path_history_isolation.py @@ -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))) ]