/** * OutboundCoordinator — owns the post-turn outbound pipeline. * * Concentrates the responsibilities that used to be scattered across * `AgentService`: * - typing-on / typing-off lifecycle for channels that surface "is typing…" * - final assistant response delivery (silence guard + hook + bus publish) * - cross-cutting `webchat_turn_complete` hook + task review * - thin pass-through for extension `message_sending` / `message_sent` hooks * * The class is intentionally framework-agnostic: it pulls everything it needs * via the constructor config (no direct AgentService reference), so it is * unit-testable and lets the parent `AgentService` shrink to a coordinator. */ import type { MessageBus, InboundMessage } from '../../infra/bus/index.js'; import type { Config } from '../../config/schema.js'; import type { HookHandler } from '../lifecycle/hook-handler.js'; import type { SessionContext } from '../session/index.js'; import { type TypingController } from '../lifecycle/typing.js'; import type { StreamManager } from './stream-manager.js'; export interface OutboundCoordinatorConfig { bus: MessageBus; hookHandler: HookHandler; streamManager: StreamManager; /** Reads the effective config snapshot (honours runtime overrides). */ getConfig: () => Config | undefined; /** Resolves the last visible assistant text for a session (in-memory + agent fallback). */ getLastAssistantPlainText: (sessionKey: string) => string; reviewTaskTurn: (payload: SessionTurnCompletePayload) => Promise; } export interface SessionTurnCompletePayload { sessionKey: string; channel: string; chatId: string; inboundUserText: string; assistantPlainText: string; aborted: boolean; streamError?: string; skipTaskReview?: boolean; outboundMetadata?: Record; } export declare class OutboundCoordinator { private readonly bus; private readonly hookHandler; private readonly streamManager; private readonly getConfig; private readonly getLastAssistantPlainText; private readonly reviewTaskTurn; constructor(config: OutboundCoordinatorConfig); /** * Build the typing indicator controller for an inbound message. Returns * `null` for the CLI channel (no typing UI). Caller is responsible for * `start()` and `stop()` (`stop()` should run AFTER the final outbound so * Telegram/Weixin see `typing_off` only once the message is delivered). */ createTypingControllerForInbound(msg: InboundMessage): TypingController | null; /** * Publish the assistant's visible text as the final bus message. Honours the * stream-manager "channel already streamed the final text" hint, the heartbeat * silence guard, and the extension `message_sending` hook. */ sendFinalResponse(msg: InboundMessage, sessionContext: SessionContext): Promise; /** Run extension completion hooks and independently review an attached Task. */ emitSessionTurnComplete(payload: SessionTurnCompletePayload): Promise; /** Extension hook pass-through (Gateway ChannelManager). */ invokeOutboundMessageSending(to: string, content: string, channel: string): Promise<{ send: boolean; content?: string; reason?: string; }>; /** Extension hook pass-through (Gateway ChannelManager). */ invokeOutboundMessageSent(to: string, content: string, success: boolean, error: string | undefined, channel: string): Promise; }