ab2f4602de
Breaks the two import cycles that forced Protocol stand-ins in the F821 sweep, so the two sites now name the real types. gateway/platforms/event.py (new leaf): MessageType, ProcessingOutcome, MessageEvent moved out of base.py verbatim. Their only dependency is gateway.session.SessionSource; base.py imported helpers.py at module level, so helpers could not name MessageEvent. Now TextBatchAggregator is typed by the real MessageEvent. 249 importers repointed (`from gateway.platforms.base import` -> `.event`, preserving each import's layout); gateway.platforms.__init__ re-exports from .event. The three revert-scheduled PLUGIN-COMPAT pointers that named these symbols (gateway.slash_commands → MessageType, dingtalk → MessageType, photon → ProcessingOutcome) and their COMPAT_MANIFEST rows now target gateway.platforms.event. Docs updated: ADDING_A_PLATFORM.md, adding-platform-adapters.md (en + zh-Hans). tools/mcp_tool_sampling.py: ElicitationHandler no longer holds a back-reference to its MCPServerTask (mcp_tool imports sampling, so the task type cannot be named there). It only ever read owner._pending_call_context, so it takes `call_context: Callable[[], Context | None]` and MCPServerTask passes `lambda: self._pending_call_context`. The consent call is one `functools.partial`, run directly or inside the captured Context. ty on the 11 touched production files vs origin/main: 0 new diagnostics, 14 resolved. (The one `source: SessionSource = None` diagnostic moves with the class; typing it Optional exposes ~60 unguarded call sites — separate follow-up.) Tests: tests/gateway + tests/plugins + tests/tools + touched files, 18,235 passed; the 31 failures reproduce identically on origin/main (macOS /private/tmp, systemd socket, long-path fixtures, live-service tests).
100 lines
4.4 KiB
Python
100 lines
4.4 KiB
Python
"""Relay transport protocol — the gateway<->connector wire contract. EXPERIMENTAL.
|
|
|
|
The ``RelayAdapter`` delegates all wire I/O to a ``RelayTransport``. The gateway
|
|
dials OUT to the connector, so production is a WebSocket client (``ws_transport.py``)
|
|
and tests use an in-memory stub (``tests/gateway/relay/stub_connector.py``). This
|
|
module defines the protocol surface only. May change without a deprecation cycle
|
|
until >=2 Class-1 platforms validate it. See docs/relay-connector-contract.md.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Awaitable, Callable, Dict, Optional, Protocol, runtime_checkable
|
|
|
|
from gateway.platforms.event import MessageEvent
|
|
from gateway.relay.descriptor import CapabilityDescriptor
|
|
|
|
# Callback the transport invokes for each inbound normalized event.
|
|
InboundHandler = Callable[[MessageEvent], Awaitable[None]]
|
|
|
|
# Callback for each forwarded passthrough request (§5.1): ``(forward, buffer_id)``.
|
|
# ``forward`` is a ws_transport.PassthroughForward, typed Any here because
|
|
# ws_transport imports FROM this module; ``buffer_id`` (§5.3 buffered flip) is
|
|
# acked by the handler after durable handoff.
|
|
PassthroughHandler = Callable[[Any, Optional[str]], Awaitable[None]]
|
|
|
|
|
|
@runtime_checkable
|
|
class RelayTransport(Protocol):
|
|
"""Full gateway<->connector transport contract."""
|
|
|
|
async def connect(self) -> bool:
|
|
"""Open the connection to the connector; return True on success."""
|
|
...
|
|
|
|
async def disconnect(self) -> None:
|
|
...
|
|
|
|
async def handshake(self) -> CapabilityDescriptor:
|
|
"""Return the capability descriptor the connector advertises."""
|
|
...
|
|
|
|
def set_inbound_handler(self, handler: InboundHandler) -> None:
|
|
...
|
|
|
|
def set_passthrough_handler(self, handler: "PassthroughHandler") -> None:
|
|
"""Register the callback for each forwarded passthrough request (§5.1).
|
|
|
|
The connector answers the provider's edge ACK itself, then forwards the real
|
|
request over this same outbound socket (a hosted gateway has no public
|
|
inbound port). Optional on a transport (a stub may not implement it).
|
|
"""
|
|
...
|
|
|
|
async def send_outbound(
|
|
self, action: Dict[str, Any], *, platform: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""Carry an outbound action (send/edit/typing) to the connector.
|
|
|
|
Returns a result dict; for ``op == "send"`` it carries ``success`` and
|
|
optionally ``message_id`` / ``error``. ``platform`` tags WHICH fronted
|
|
platform this reply targets (the transport resolves the matching botId);
|
|
omitted ⇒ the connector uses the session's default platform.
|
|
"""
|
|
...
|
|
|
|
async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
|
|
"""Proxy a chat-info lookup to the connector."""
|
|
...
|
|
|
|
async def send_interrupt(self, session_key: str, reason: Optional[str] = None) -> None:
|
|
"""Route a mid-turn /stop to the connector for ``session_key`` (OUTBOUND
|
|
direction; the actual cancellation happens when the connector echoes an
|
|
interrupt inbound down the socket owning that session)."""
|
|
...
|
|
|
|
async def go_idle(self, timeout_s: float = 10.0) -> bool:
|
|
"""Ask the connector to flip this instance to buffered-only (§5.3).
|
|
|
|
Sends ``going_idle`` and awaits the connector-authoritative ``going_idle_ack``
|
|
(live delivery stopped; inbound now buffers for replay on reconnect). True on
|
|
ack, False on timeout / not-connected — the caller closes regardless.
|
|
Optional on a transport; part of the gateway's EXISTING drain transition.
|
|
"""
|
|
...
|
|
|
|
async def send_follow_up(
|
|
self, action: Dict[str, Any], *, platform: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""Act on a shared-identity capability bound to a session (A2 outbound).
|
|
|
|
A credential acting on the SHARED bot identity (e.g. a Discord interaction
|
|
follow-up token) NEVER reaches the gateway: the connector vaults it keyed by
|
|
session and the gateway issues a SEMANTIC action (``op == "follow_up"``,
|
|
``session_key``, ``kind`` e.g. ``"discord.interaction_token"``, ``content``,
|
|
optional ``metadata``). Returns ``{success, message_id?, error?}``; ``success``
|
|
is False when the capability is absent/expired or the tenant mismatches —
|
|
nothing to retry with (a leaked gateway holds zero capability material).
|
|
"""
|
|
...
|