Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

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"]