import type { ChannelId } from "../channels/plugins/types.js"; import type { MediaUnderstandingDecision, MediaUnderstandingOutput, } from "../media-understanding/types.js"; import type { InputProvenance } from "../sessions/input-provenance.js"; import type { StickerMetadata } from "../telegram/bot/types.js"; import type { InternalMessageChannel } from "../utils/message-channel.js"; import type { CommandArgs } from "./commands-registry.types.js"; /** Valid message channels for routing. */ export type OriginatingChannelType = ChannelId | InternalMessageChannel; export type MsgContext = { Body?: string; /** * Agent prompt body (may include envelope/history/context). Prefer this for prompt shaping. * Should use real newlines (`\n`), not escaped `\\n`. */ BodyForAgent?: string; /** * Recent chat history for context (untrusted user content). Prefer passing this * as structured context blocks in the user prompt rather than rendering plaintext envelopes. */ InboundHistory?: Array<{ sender: string; body: string; timestamp?: number; }>; /** * Raw message body without structural context (history, sender labels). * Legacy alias for CommandBody. Falls back to Body if not set. */ RawBody?: string; /** * Prefer for command detection; RawBody is treated as legacy alias. */ CommandBody?: string; /** * Command parsing body. Prefer this over CommandBody/RawBody when set. * Should be the "clean" text (no history/sender context). */ BodyForCommands?: string; CommandArgs?: CommandArgs; From?: string; To?: string; SessionKey?: string; /** Provider account id (multi-account). */ AccountId?: string; ParentSessionKey?: string; MessageSid?: string; /** Provider-specific full message id when MessageSid is a shortened alias. */ MessageSidFull?: string; MessageSids?: string[]; MessageSidFirst?: string; MessageSidLast?: string; ReplyToId?: string; /** * Root message id for thread reconstruction (used by Feishu for root_id). * When a message is part of a thread, this is the id of the first message. */ RootMessageId?: string; /** Provider-specific full reply-to id when ReplyToId is a shortened alias. */ ReplyToIdFull?: string; ReplyToBody?: string; ReplyToSender?: string; ReplyToIsQuote?: boolean; /** Forward origin from the reply target (when reply_to_message is a forwarded message). */ ReplyToForwardedFrom?: string; ReplyToForwardedFromType?: string; ReplyToForwardedFromId?: string; ReplyToForwardedFromUsername?: string; ReplyToForwardedFromTitle?: string; ReplyToForwardedDate?: number; ForwardedFrom?: string; ForwardedFromType?: string; ForwardedFromId?: string; ForwardedFromUsername?: string; ForwardedFromTitle?: string; ForwardedFromSignature?: string; ForwardedFromChatType?: string; ForwardedFromMessageId?: number; ForwardedDate?: number; ThreadStarterBody?: string; /** Full thread history when starting a new thread session. */ ThreadHistoryBody?: string; IsFirstThreadTurn?: boolean; ThreadLabel?: string; MediaPath?: string; MediaUrl?: string; MediaType?: string; MediaDir?: string; MediaPaths?: string[]; MediaUrls?: string[]; MediaTypes?: string[]; /** Telegram sticker metadata (emoji, set name, file IDs, cached description). */ Sticker?: StickerMetadata; /** True when current-turn sticker media is present in MediaPaths (false for cached-description path). */ StickerMediaIncluded?: boolean; OutputDir?: string; OutputBase?: string; /** Remote host for SCP when media lives on a different machine (e.g., openclaw@192.168.64.3). */ MediaRemoteHost?: string; Transcript?: string; MediaUnderstanding?: MediaUnderstandingOutput[]; MediaUnderstandingDecisions?: MediaUnderstandingDecision[]; LinkUnderstanding?: string[]; Prompt?: string; MaxChars?: number; ChatType?: string; /** Human label for envelope headers (conversation label, not sender). */ ConversationLabel?: string; GroupSubject?: string; /** Human label for channel-like group conversations (e.g. #general, #support). */ GroupChannel?: string; GroupSpace?: string; GroupMembers?: string; GroupSystemPrompt?: string; /** Untrusted metadata that must not be treated as system instructions. */ UntrustedContext?: string[]; /** System-attached provenance for the current inbound message. */ InputProvenance?: InputProvenance; /** Explicit owner allowlist overrides (trusted, configuration-derived). */ OwnerAllowFrom?: Array; SenderName?: string; SenderId?: string; SenderUsername?: string; SenderTag?: string; SenderE164?: string; Timestamp?: number; /** Provider label (e.g. whatsapp, telegram). */ Provider?: string; /** Provider surface label (e.g. discord, slack). Prefer this over `Provider` when available. */ Surface?: string; /** Platform bot username when command mentions should be normalized. */ BotUsername?: string; WasMentioned?: boolean; CommandAuthorized?: boolean; CommandSource?: "text" | "native"; CommandTargetSessionKey?: string; /** * Internal flag: command handling prepared trailing prompt text for ACP dispatch. * Used for `/new ` and `/reset ` on ACP-bound sessions. */ AcpDispatchTailAfterReset?: boolean; /** Gateway client scopes when the message originates from the gateway. */ GatewayClientScopes?: string[]; /** Thread identifier (Telegram topic id or Matrix thread event id). */ MessageThreadId?: string | number; /** Platform-native channel/conversation id (e.g. Slack DM channel "D…" id). */ NativeChannelId?: string; /** Telegram forum supergroup marker. */ IsForum?: boolean; /** Warning: DM has topics enabled but this message is not in a topic. */ TopicRequiredButMissing?: boolean; /** * Originating channel for reply routing. * When set, replies should be routed back to this provider * instead of using lastChannel from the session. */ OriginatingChannel?: OriginatingChannelType; /** * Originating destination for reply routing. * The chat/channel/user ID where the reply should be sent. */ OriginatingTo?: string; /** * True when the current turn intentionally requested external delivery to * OriginatingChannel/OriginatingTo, rather than inheriting stale session route metadata. */ ExplicitDeliverRoute?: boolean; /** * Provider-specific parent conversation id for threaded contexts. * For Discord threads, this is the parent channel id. */ ThreadParentId?: string; /** * Messages from hooks to be included in the response. * Used for hook confirmation messages like "Session context saved to memory". */ HookMessages?: string[]; }; export type FinalizedMsgContext = Omit & { /** * Always set by finalizeInboundContext(). * Default-deny: missing/undefined becomes false. */ CommandAuthorized: boolean; }; export type TemplateContext = MsgContext & { BodyStripped?: string; SessionId?: string; IsNewSession?: string; }; function formatTemplateValue(value: unknown): string { if (value == null) { return ""; } if (typeof value === "string") { return value; } if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") { return String(value); } if (typeof value === "symbol" || typeof value === "function") { return value.toString(); } if (Array.isArray(value)) { return value .flatMap((entry) => { if (entry == null) { return []; } if (typeof entry === "string") { return [entry]; } if (typeof entry === "number" || typeof entry === "boolean" || typeof entry === "bigint") { return [String(entry)]; } return []; }) .join(","); } if (typeof value === "object") { return ""; } return ""; } // Simple {{Placeholder}} interpolation using inbound message context. export function applyTemplate(str: string | undefined, ctx: TemplateContext) { if (!str) { return ""; } return str.replace(/{{\s*(\w+)\s*}}/g, (_, key) => { const value = ctx[key as keyof TemplateContext]; return formatTemplateValue(value); }); }