import type { FastMode } from "@openclaw/normalization-core/string-coerce"; import type { AgentRunTerminalOutcome } from "../agents/agent-run-terminal-outcome.js"; /** Public option types for reply generation callbacks, streaming, and delivery policy. */ import type { ExecutionIdentityAdmissionToken } from "../audit/execution-identity-admission.js"; import type { AgentPlanStep } from "../channels/streaming.js"; import type { TranscriptEntryAnchor } from "../config/sessions/transcript-entry-anchor.js"; import type { ImageContent } from "../llm/types.js"; import type { MediaFact } from "../media/media-facts.js"; import type { PromptImageOrderEntry } from "../media/prompt-image-order.js"; import type { UserTurnTranscriptRecorder } from "../sessions/user-turn-transcript.types.js"; import type { ReplyPayload } from "./reply-payload.js"; import type { TypingController } from "./reply/typing.js"; import type { SourceReplyDeliveryMode } from "./source-reply-delivery-mode.types.js"; export type { SourceReplyDeliveryMode } from "./source-reply-delivery-mode.types.js"; /** A successful runtime append, independent of optional active-path projection anchors. */ export type ReplyDispatchAssistantTranscript = Pick< TranscriptEntryAnchor, "agentId" | "sessionId" | "sessionKey" | "storePath" > & { messageId: string; anchor?: TranscriptEntryAnchor; idempotencyKey: string; }; export type ReplyDispatchRun = { completionSource: "reply-dispatch"; getResult: () => { assistantTranscript?: ReplyDispatchAssistantTranscript; terminalOutcome?: AgentRunTerminalOutcome; }; }; export type BlockReplyContext = { abortSignal?: AbortSignal; timeoutMs?: number; /** Source assistant message index from the upstream stream, when available. */ assistantMessageIndex?: number; /** @internal Stable durable outbound intent owned by the producing runtime. */ deliveryIntentId?: string; }; /** Context passed to onModelSelected callback with actual model used. */ type ModelSelectedContext = { provider: string; model: string; thinkLevel: string | undefined; }; /** Typing indicator class for channel-owned UX policy. */ export type TypingPolicy = | "auto" | "user_message" | "system_event" | "internal_webchat" | "heartbeat"; /** Per-turn policy for source-message reply threading. */ export type ReplyThreadingPolicy = { /** Override implicit reply-to-current behavior for the current turn. */ implicitCurrentMessage?: "default" | "allow" | "deny"; }; /** Action sink available for model-proposed follow-up tasks during this turn. */ export type TaskSuggestionDeliveryMode = "gateway"; /** Correlates queued reply ownership transfer with later delivery drains. */ export type QueuedReplyDeliveryCorrelation = { begin: () => (() => void) | void; }; /** * Exclusive: each lifecycle is its own collect-admission identity. * Cancel-only: share collect identity via ownerKey (gateway chat.send). */ type TurnAdoptionAdmission = "exclusive" | "cancel-only"; /** * Canonical turn-ownership lifecycle (adopt / defer / abandon / settle). * Single surface for durable ingress, gateway cancel identity, and reply-lane transfer. */ export type TurnAdoptionLifecycle = { /** * Admission isolation mode (closed). Exclusive isolates collect identity per * lifecycle; cancel-only shares via ownerKey. Never inferred from onAbandoned. * Durable ingress sets exclusive; gateway cancel identity sets cancel-only. */ admission?: TurnAdoptionAdmission; /** Transcript branch leaf from which this turn was admitted. */ originatingLeafEntryId?: string | null; onAdopted: () => void | Promise; /** Return false to reject followup enqueue. */ onDeferred?: () => boolean | void; /** Reports that a deferred turn is still queued behind an active turn. */ onDeferredHeartbeat?: () => void; /** Requested cadence for queue-owned deferred heartbeats. */ deferredHeartbeatIntervalMs?: number; /** Deferred turn finished without owning the reply lane. */ onAbandoned?: () => void; /** Always fires when the followup ownership cycle ends (admitted or not). Gateway cleanup. */ onSettled?: () => void; /** Retires cancellation ownership while retaining live identity. */ onCancellationRetired?: () => void; /** Stable cancellation owner for collect-mode batches. */ ownerKey?: string; abortSignal?: AbortSignal; /** Ephemeral fact: a direct local operator turn lost fresh cron authority when queued. */ cronCreatorAuthorityUnavailable?: "queued-local-operator"; }; /** Partial assistant payload emitted during streaming or replacement updates. */ export type PartialReplyPayload = { /** * Sanitized text, which may be an enumerable memoized getter. Content materializes on first * read: direct-delivery consumers pay per partial, while throttled consumers pay per flush. */ text?: ReplyPayload["text"]; mediaUrls?: ReplyPayload["mediaUrls"]; delta?: string; replace?: true; }; type ReasoningStreamPayload = Pick< ReplyPayload, "text" | "mediaUrls" | "isReasoning" | "isReasoningSnapshot" > & { requiresReasoningProgressOptIn?: boolean; }; type ReasoningProgressPayload = { progressTokens: number; }; /** Return false until the channel has accepted operator-visible progress. */ type ProgressCallbackResult = boolean | void; /** Reply generation options shared by auto-reply, webchat, channels, and tests. */ export type GetReplyOptions = { /** Channel-owned participant name encoding for source replies sent through message actions. */ groupThreadReplyFormatter?: ( text: string, participant: { agentId: string; name: string }, ) => string; /** Override run id for agent events (defaults to random UUID). */ runId?: string; /** Stable provider prompt-cache affinity key; distinct from run id/idempotency. */ promptCacheKey?: string; /** Abort signal for the underlying agent run. */ abortSignal?: AbortSignal; /** Ephemeral channel owner check for a targeted Stop; never serialized as authority. */ isCommandTargetCurrent?: () => boolean; /** Optional inbound images (used for webchat attachments). */ images?: ImageContent[]; /** Original inline/offloaded attachment order for inbound images. */ imageOrder?: PromptImageOrderEntry[]; /** Ordered media facts whose model-facing text projection is already present in the prompt. */ media?: MediaFact[]; /** * Notifies when an agent run starts. Return "reply-dispatch" synchronously to accept * completion ownership offered in options; all other legacy callback results are ignored. */ onAgentRunStart?: ( runId: string, executionIdentityToken?: ExecutionIdentityAdmissionToken, options?: ReplyDispatchRun, ) => unknown; /** Reports the terminal agent-run classification to the shared dispatch owner. */ onAgentRunTerminalOutcome?: (outcome: "completed" | "failed") => void; /** * Canonical adoption lifecycle (adopted / deferred / abandoned / settled + pre-adoption abort). */ turnAdoptionLifecycle?: TurnAdoptionLifecycle; /** Shared lifecycle owner for the current user-turn transcript append. */ userTurnTranscriptRecorder?: UserTurnTranscriptRecorder; /** Gateway-owned start-or-steer decision for this turn. */ messageInjectionDisposition?: "none" | "accepted" | "rejected"; /** Current user turn is already durable; replay it without appending another copy. */ suppressNextUserMessagePersistence?: boolean; onReplyStart?: () => Promise | void; /** Called when the typing controller cleans up (e.g., run ended with NO_REPLY). */ onTypingCleanup?: () => void; onTypingController?: (typing: TypingController) => void; /** If false, send only the initial typing signal without periodic keepalive refreshes. */ typingKeepalive?: boolean; isHeartbeat?: boolean; /** Policy-level typing control for run classes (user/system/internal/heartbeat). */ typingPolicy?: TypingPolicy; /** Force-disable typing indicators for this run (system/internal/cross-channel routes). */ suppressTyping?: boolean; /** Resolved heartbeat model override (provider/model string from merged per-agent config). */ heartbeatModelOverride?: string; /** One-shot thinking level override for this run; does not persist to the session. */ thinkingLevelOverride?: string; /** One-shot fast-mode override for this run; does not persist to the session. */ fastModeOverride?: FastMode; /** One-shot auto fast-mode cutoff override in seconds; does not persist to the session. */ fastModeAutoOnSecondsOverride?: number; /** Controls bootstrap workspace context injection (default: full). */ bootstrapContextMode?: "full" | "lightweight"; /** If true, run the model without OpenClaw tools for this turn. */ disableTools?: boolean; /** Runtime tool allow-list for this turn. Empty means no tools. */ toolsAllow?: string[]; /** If true, include the heartbeat response tool for structured heartbeat outcomes. */ enableHeartbeatTool?: boolean; /** If true, keep the heartbeat response tool available even under narrow tool profiles. */ forceHeartbeatTool?: boolean; /** * @deprecated Ignored. The tool-failure warning is delivered whenever a run ends * without a reply and cannot be suppressed. Kept only so plugin-sdk callers that * still pass it keep compiling; removed in the first stable release after 2026.10. */ suppressToolErrorWarnings?: boolean; /** * If true, dispatch skips default tool/progress text messages and expects the * channel to surface progress via its own streaming/edit UX. */ suppressDefaultToolProgressMessages?: boolean; /** Suppress standalone tool/progress text even when verbose progress is enabled. */ suppressToolProgressMessages?: boolean; /** Allow channel-owned tool lifecycle feedback while text progress remains hidden. */ allowToolLifecycleWhenProgressHidden?: boolean; /** * Called before dispatch with a live getter for whether verbose standalone * progress messages are active for this run. Channels that render tool or * commentary progress inside an ephemeral streaming draft should yield those * draft lines while the getter returns true, so progress is not rendered in * both lanes at once. */ onVerboseProgressVisibility?: (isActive: () => boolean) => void; /** Preserve source-event callback start order for stateful channel progress renderers. */ preserveProgressCallbackStartOrder?: boolean; onPartialReply?: ( payload: PartialReplyPayload, ) => Promise | ProgressCallbackResult; onReasoningStream?: ( payload: ReasoningStreamPayload, ) => Promise | ProgressCallbackResult; onReasoningProgress?: (payload: ReasoningProgressPayload) => Promise | void; streamReasoningInNonStreamModes?: boolean; /** Called when a thinking/reasoning block ends. */ onReasoningEnd?: () => Promise | ProgressCallbackResult; /** Called when a new assistant message starts (e.g., after tool call or thinking block). */ onAssistantMessageStart?: () => Promise | ProgressCallbackResult; /** Called synchronously when a block reply is logically emitted, before async * delivery drains. Useful for channels that need to rotate preview state at * block boundaries without waiting for transport acks. */ onBlockReplyQueued?: ( payload: ReplyPayload, context?: BlockReplyContext, ) => Promise | ProgressCallbackResult; onBlockReply?: (payload: ReplyPayload, context?: BlockReplyContext) => Promise | void; onToolResult?: ( payload: ReplyPayload, ) => Promise | ProgressCallbackResult; /** Called when a tool phase starts/updates, before summary payloads are emitted. */ onToolStart?: (payload: { itemId?: string; toolCallId?: string; name?: string; phase?: string; args?: Record; detailMode?: "explain" | "raw"; }) => Promise | ProgressCallbackResult; /** Called when a concrete work item starts, updates, or completes. */ onItemEvent?: (payload: { itemId?: string; toolCallId?: string; kind?: string; title?: string; name?: string; phase?: string; status?: string; summary?: string; progressText?: string; meta?: string; commandBearing?: boolean; approvalId?: string; approvalSlug?: string; suppressDurableProgress?: true; }) => Promise | ProgressCallbackResult; /** * Called when the utility-model narration of the in-progress turn changes. * Providing this callback opts the channel into progress narration; core * only generates narration when a utility model resolves (explicit * config or the provider-declared default; utilityModel: "" disables). * An empty text clears narration; a retained model preamble still wins before * the channel falls back to raw tool progress. */ onNarrationUpdate?: (payload: { text: string }) => Promise | void; /** Channel-owned final and queued-turn boundaries for the current narrator. */ onProgressNarratorLifecycle?: (lifecycle: { beginTurn: () => void; stopTurn: () => void; }) => void; /** False while utility-model narration has no visible progress draft. */ isProgressDraftVisible?: () => boolean; /** * Omit exec/bash command text from narration model input, mirroring the * channel's `streaming.progress.commandText: "status"` display policy so * narration never receives more command detail than the draft shows. */ narrationHideCommandText?: boolean; /** In progress mode, classify Claude pre-tool text; true also renders it as commentary. */ commentaryProgressEnabled?: boolean; /** Bridge typed preambles to a channel-owned progress headline without commentary. */ progressPreambleEnabled?: boolean; /** Deliver durable reasoning payloads to channels that own a separate reasoning lane. */ reasoningPayloadsEnabled?: boolean; /** Deliver durable commentary (💬) payloads to channels that own a separate commentary lane. */ commentaryPayloadsEnabled?: boolean; /** Optional turn-frozen commentary owner; visibility is live by default. * With the static opt-in and this callback, core freezes, evaluates once, and snapshots. */ shouldDeliverCommentaryPayloads?: () => boolean; /** Called when the agent emits a structured plan update. */ onPlanUpdate?: (payload: { phase?: string; title?: string; explanation?: string; /** Prepared literal text; unmarked explanations retain authored Markdown. */ explanationFormat?: "plain"; steps?: AgentPlanStep[]; source?: string; }) => Promise | ProgressCallbackResult; /** Called when an approval becomes pending or resolves. */ onApprovalEvent?: (payload: { phase?: string; kind?: string; status?: string; title?: string; itemId?: string; toolCallId?: string; approvalId?: string; approvalSlug?: string; command?: string; host?: string; reason?: string; scope?: "turn" | "session"; message?: string; }) => Promise | ProgressCallbackResult; /** Called when command output streams or completes. */ onCommandOutput?: (payload: { itemId?: string; phase?: string; title?: string; toolCallId?: string; name?: string; output?: string; status?: string; exitCode?: number | null; durationMs?: number; cwd?: string; }) => Promise | ProgressCallbackResult; /** Called when a patch completes with a file summary. */ onPatchSummary?: (payload: { itemId?: string; phase?: string; title?: string; toolCallId?: string; name?: string; added?: string[]; modified?: string[]; deleted?: string[]; summary?: string; }) => Promise | ProgressCallbackResult; /** Called when context auto-compaction starts (allows UX feedback during the pause). */ onCompactionStart?: () => Promise | ProgressCallbackResult; /** Called when context auto-compaction ends; omitted outcome means completed for legacy callers. */ onCompactionEnd?: (payload?: { completed: boolean; }) => Promise | ProgressCallbackResult; /** Called when the actual model is selected (including after fallback). * Use this to get model/provider/thinkLevel for responsePrefix template interpolation. */ onModelSelected?: (ctx: ModelSelectedContext) => void; /** * Controls whether normal assistant replies are automatically delivered to * the source conversation. `message_tool_only` prefers message-tool visible * delivery and keeps normal final text, block output, and preview output * private unless dispatch explicitly marks a source reply as deliverable. */ sourceReplyDeliveryMode?: SourceReplyDeliveryMode; /** Enables task-suggestion tools only when the initiating surface can action Gateway events. */ taskSuggestionDeliveryMode?: TaskSuggestionDeliveryMode; /** Starts delivery tracking when this turn later drains as a queued followup. */ queuedDeliveryCorrelations?: QueuedReplyDeliveryCorrelation[]; /** Called after a queued followup owns the reply lane, before its model run starts. */ onQueuedFollowupAdmitted?: () => Promise | void; /** Called after an admitted queued followup finishes, including failed attempts. */ onQueuedFollowupSettled?: () => Promise | void; /** Allow channel-owned progress UI while final/source reply delivery remains message-tool-only. */ allowProgressCallbacksWhenSourceDeliverySuppressed?: boolean; /** Called when a suppressed source reply mode observes visible delivery through another path. */ onObservedReplyDelivery?: () => Promise | void; /** Emit tool result summaries for channel-owned progress UI even when verbose is off. */ forceToolResultProgress?: boolean; disableBlockStreaming?: boolean; /** Timeout for block reply delivery (ms). */ blockReplyTimeoutMs?: number; /** If provided, only load these skills for this session (empty = no skills). */ skillFilter?: string[]; /** Mutable ref to track if a reply was sent (for Slack "first" threading mode). */ hasRepliedRef?: { value: boolean }; /** Override agent timeout in seconds (0 = no timeout). Threads through to resolveAgentTimeoutMs. */ timeoutOverrideSeconds?: number; /** Millisecond run timeout override; takes precedence over seconds (0 = no timeout). */ timeoutOverrideMs?: number; };