4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
125 lines
4.9 KiB
Python
125 lines
4.9 KiB
Python
"""Gateway turn handler: dispatch one inbound message to the agent.
|
|
|
|
Transport-agnostic — it takes ``(text, session, sink, logger)`` and drives the
|
|
shared headless dispatch, then finalizes any outbound text on the sink. It knows
|
|
nothing about Telegram (or any specific transport); the composition root builds
|
|
one of these and hands it to whichever poller runs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from rich.console import Console
|
|
|
|
from core.agent_harness.accounting.run_record import DefaultRunRecordFactory
|
|
from core.agent_harness.accounting.turn_accounting import DefaultTurnAccounting
|
|
from core.agent_harness.error_reporting import DefaultErrorReporter
|
|
from core.agent_harness.prompts.prompt_context import DefaultPromptContextProvider
|
|
from core.agent_harness.session import SessionCore
|
|
from core.agent_harness.tools.tool_provider import DefaultToolProvider
|
|
from core.agent_harness.turns.default_reasoning_client import DefaultReasoningClientProvider
|
|
from core.agent_harness.turns.headless_dispatch import HeadlessAgent
|
|
from gateway.gateway_output_sink import GatewayOutputSink
|
|
from gateway.headless_subprocess_presenter import headless_subprocess_presenter_factory
|
|
from gateway.status_messages import status_from_tool_start
|
|
from platform.observability.trace.spans import traced_session
|
|
|
|
|
|
class _ToolStatusObserver:
|
|
"""Live tool-progress feedback for the gateway.
|
|
|
|
On each tool start it pushes a status line to the turn's sink — for Telegram
|
|
that surfaces the typing indicator and a ``running tool X`` preview, so the
|
|
user sees progress before the final answer instead of a silent wait.
|
|
"""
|
|
|
|
def __init__(self, sink: GatewayOutputSink) -> None:
|
|
self._sink = sink
|
|
|
|
def __call__(self, kind: str, data: dict[str, object]) -> None:
|
|
if kind != "tool_start":
|
|
return
|
|
tool_name = str(data.get("name") or "").strip()
|
|
if not tool_name or tool_name == "assistant_handoff":
|
|
return
|
|
self._sink.set_tool_status(status_from_tool_start(tool_name, data.get("input")))
|
|
|
|
|
|
class GatewayTurnHandler:
|
|
"""Services one inbound gateway message per call (a :data:`GatewayAgentCallback`).
|
|
|
|
``console`` is the only cross-turn state. The session, output sink, and
|
|
accounting are per-turn, so each call builds its own agent — there is no
|
|
persistent per-transport agent, and concurrent turns stay isolated.
|
|
"""
|
|
|
|
def __init__(self, *, console: Console) -> None:
|
|
self._console = console
|
|
|
|
def __call__(
|
|
self,
|
|
text: str,
|
|
session: SessionCore,
|
|
sink: GatewayOutputSink,
|
|
logger: logging.Logger,
|
|
) -> None:
|
|
with traced_session(getattr(session, "session_id", None), component="gateway_turn"):
|
|
agent = self._agent_for_turn(text=text, session=session, sink=sink, logger=logger)
|
|
turn_result = agent.dispatch(text)
|
|
outbound_text = (
|
|
turn_result.assistant_response_text or turn_result.action_result.response_text
|
|
).strip()
|
|
logger.debug(
|
|
"gateway_turn done intent=%s answered=%s outbound_chars=%s",
|
|
turn_result.final_intent,
|
|
turn_result.answered,
|
|
len(outbound_text),
|
|
)
|
|
# A streamed answer (answered=True) already resolved the placeholder status
|
|
# via the sink. Otherwise always finalize so the placeholder never hangs —
|
|
# even when the turn produced no text.
|
|
if not turn_result.answered:
|
|
sink.finalize(outbound_text or "I didn't have anything to add for that.")
|
|
|
|
def _agent_for_turn(
|
|
self,
|
|
*,
|
|
text: str,
|
|
session: SessionCore,
|
|
sink: GatewayOutputSink,
|
|
logger: logging.Logger,
|
|
) -> HeadlessAgent:
|
|
"""Build a fresh agent for a single gateway turn.
|
|
|
|
Action tools are resolved from the live session here so integration-scoped
|
|
tools stay available after ``SessionResolver`` hydrates the chat session.
|
|
"""
|
|
error_reporter = DefaultErrorReporter(logger)
|
|
observer = _ToolStatusObserver(sink)
|
|
return HeadlessAgent(
|
|
session=session,
|
|
output=sink,
|
|
tools=DefaultToolProvider(
|
|
session,
|
|
self._console,
|
|
tool_action_logger=logger,
|
|
observer_factory=lambda _message: observer,
|
|
subprocess_presenter_factory=headless_subprocess_presenter_factory,
|
|
),
|
|
prompts=DefaultPromptContextProvider(session),
|
|
reasoning=DefaultReasoningClientProvider(
|
|
output=sink,
|
|
error_reporter=error_reporter,
|
|
session=session,
|
|
),
|
|
run_factory=DefaultRunRecordFactory(session),
|
|
accounting=DefaultTurnAccounting(session, text),
|
|
error_reporter=error_reporter,
|
|
gather_enabled=True,
|
|
is_tty=False,
|
|
)
|
|
|
|
|
|
__all__ = ["GatewayTurnHandler"]
|