Files
hermes-agent/agent/turn_preflight_gate.py
T

198 lines
7.8 KiB
Python

"""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
from dataclasses import dataclass
import logging
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")