Voice replies had to come from the session's main model, so a heavy model made every spoken answer slow (#83894). A new auxiliary slot, voice_chat, picks the model for voice turns: - CLI/TUI voice mode, the Desktop voice conversation (prompt.submit voice_turn) and messaging voice notes mark the turn; agent/voice_turn_route.py binds the route after the prompt, row and preflight compaction were settled against the main model and undoes it before memory sync and background review, so the next typed turn and the review run on the main model. - A turn larger than the voice model's compression threshold stays on the main model with a one-time notice: switching to voice never compacts the conversation. - Usage lands in session_model_usage under task voice_chat; it never rewrites the session's recorded model/route, so resume keeps the model the user picked. - Slot appears in Desktop Settings -> Models, the dashboard, and `hermes model`; GPT-Live delegations keep using the session model.
103 lines
4.9 KiB
Python
103 lines
4.9 KiB
Python
"""Per-turn context shared between ``GatewayRunner._run_agent_inner`` and ``TurnRunner``.
|
|
|
|
Each ex-closure local is a field, written once by ``_run_agent_inner`` while wiring the turn.
|
|
``message`` (ex-``nonlocal``) is the only rebindable field; other mutable state uses
|
|
single-element lists so mutation stays visible to the outer body.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Callable, List, Optional
|
|
|
|
|
|
@dataclass
|
|
class TurnContext:
|
|
# read-only turn identity / wiring
|
|
source: Any = None
|
|
reply_expected: Optional[bool] = None
|
|
# Scheduled heartbeats are proactive work, not replies to the source message that
|
|
# registered the watch. Their routine delivery surfaces stay quiet.
|
|
scheduled_heartbeat: bool = False
|
|
_run_still_current: Callable[[], bool] = None # type: ignore[assignment]
|
|
_live_status_adapter: Any = None
|
|
_live_status_mode: str = "off"
|
|
_thinking_enabled: bool = False
|
|
progress_mode: str = "off"
|
|
progress_grouping: str = "grouped"
|
|
tool_progress_enabled: bool = False
|
|
progress_queue: Any = None
|
|
log_queue: Any = None
|
|
# mutable single-element containers (shared with the outer body)
|
|
last_progress_msg: list = field(default_factory=lambda: [None])
|
|
last_tool: list = field(default_factory=lambda: [None])
|
|
last_was_terminal_block: list = field(default_factory=lambda: [False])
|
|
repeat_count: list = field(default_factory=lambda: [0])
|
|
long_tool_hint_fired: list = field(default_factory=lambda: [False])
|
|
agent_holder: list = field(default_factory=lambda: [None])
|
|
_LONG_TOOL_THRESHOLD_S: float = 30.0
|
|
_cleanup_progress: bool = False
|
|
_cleanup_msg_ids: List[str] = field(default_factory=list)
|
|
_progress_metadata: Optional[dict] = None
|
|
_progress_reply_to: Optional[Any] = None
|
|
message: Optional[str] = None # the only rebindable field
|
|
# turn parameters / config snapshots (read-only in run_sync)
|
|
history: Any = None
|
|
context_prompt: Optional[str] = None
|
|
channel_prompt: Optional[str] = None
|
|
session_id: Optional[str] = None
|
|
session_key: Optional[str] = None
|
|
run_generation: Optional[int] = None
|
|
process_task_id: str = ""
|
|
process_baseline: frozenset[str] = field(default_factory=frozenset)
|
|
_interrupt_depth: int = 0
|
|
event_message_id: Optional[str] = None
|
|
# Raw inbound platform id (not the event_message_id reply anchor); stamped on the user turn.
|
|
inbound_message_id: Optional[str] = None
|
|
moa_config: Optional[dict] = None
|
|
title_user_message: Optional[str] = None
|
|
persist_user_message: Optional[Any] = None
|
|
persist_user_timestamp: Optional[float] = None
|
|
# display_kind of the persisted user row for a self-injected turn; DB-only, never sent.
|
|
# "internal_notification" for async-delegation/background notifications (#82888).
|
|
persist_user_display_kind: Optional[str] = None
|
|
persist_user_display_metadata: Optional[dict] = None
|
|
user_config: Any = None
|
|
mute_notification_reply: bool = False
|
|
voice_turn: bool = False # a voice-note turn: runs on auxiliary.voice_chat
|
|
enabled_toolsets: Any = None
|
|
disabled_toolsets: Any = None
|
|
log_mode_enabled: bool = False
|
|
interim_assistant_messages_enabled: bool = False
|
|
needs_progress_queue: bool = False
|
|
AIAgent: Any = None
|
|
resolve_display_setting: Any = None
|
|
result_holder: list = field(default_factory=lambda: [None])
|
|
tools_holder: list = field(default_factory=lambda: [None])
|
|
stream_consumer_holder: list = field(default_factory=lambda: [None])
|
|
streaming_tts_consumer_holder: list = field(default_factory=lambda: [None])
|
|
# voice-ack wiring
|
|
_voice_ack_fired: list = field(default_factory=lambda: [False])
|
|
_voice_ack_guild: list = field(default_factory=lambda: [None])
|
|
_voice_ack_loop: Any = None
|
|
# hook / status bridge wiring
|
|
_loop_for_step: Any = None
|
|
_hooks_ref: Any = None
|
|
_status_adapter: Any = None
|
|
_status_chat_id: Any = None
|
|
_status_thread_metadata: Optional[dict] = None
|
|
# bound TurnRunner callbacks read via ctx
|
|
progress_callback: Optional[Callable] = None
|
|
voice_ack_callback: Optional[Callable] = None
|
|
_step_callback_sync: Optional[Callable] = None
|
|
_event_callback_sync: Optional[Callable] = None
|
|
_status_callback_sync: Optional[Callable] = None
|
|
# Slack-native task cards (opt-in); ID-bearing callbacks correlate start/complete by call ID
|
|
# --- Slack-native task-card progress (opt-in; #29483) ------------------ True when the Slack adapter's
|
|
# ``native_task_cards_enabled()`` opt-in is set for this turn's platform. The ID-bearing lifecycle
|
|
# callbacks are published by TurnRunner (like voice_ack_callback above) so tool starts and completions
|
|
# correlate by real tool-call ID instead of tool name.
|
|
_native_slack_task_cards: bool = False
|
|
native_tool_start_callback: Optional[Callable] = None
|
|
native_tool_complete_callback: Optional[Callable] = None
|