refactor: share reply prefix context

This commit is contained in:
Peter Steinberger
2026-01-23 23:04:09 +00:00
parent 8252ae2da1
commit 1113f17d4c
11 changed files with 145 additions and 167 deletions

View File

@@ -1,6 +1,7 @@
import type { LocationMessageEventContent, MatrixClient } from "matrix-bot-sdk"; import type { LocationMessageEventContent, MatrixClient } from "matrix-bot-sdk";
import { import {
createReplyPrefixContext,
createTypingCallbacks, createTypingCallbacks,
formatAllowlistMatchMeta, formatAllowlistMatchMeta,
type RuntimeEnv, type RuntimeEnv,
@@ -553,6 +554,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
channel: "matrix", channel: "matrix",
accountId: route.accountId, accountId: route.accountId,
}); });
const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
const typingCallbacks = createTypingCallbacks({ const typingCallbacks = createTypingCallbacks({
start: () => sendTypingMatrix(roomId, true, undefined, client), start: () => sendTypingMatrix(roomId, true, undefined, client),
stop: () => sendTypingMatrix(roomId, false, undefined, client), stop: () => sendTypingMatrix(roomId, false, undefined, client),
@@ -565,8 +567,8 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
}); });
const { dispatcher, replyOptions, markDispatchIdle } = const { dispatcher, replyOptions, markDispatchIdle } =
core.channel.reply.createReplyDispatcherWithTyping({ core.channel.reply.createReplyDispatcherWithTyping({
responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId) responsePrefix: prefixContext.responsePrefix,
.responsePrefix, responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId), humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload) => { deliver: async (payload) => {
await deliverMatrixReplies({ await deliverMatrixReplies({
@@ -596,6 +598,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
replyOptions: { replyOptions: {
...replyOptions, ...replyOptions,
skillFilter: roomConfig?.skills, skillFilter: roomConfig?.skills,
onModelSelected: prefixContext.onModelSelected,
}, },
}); });
markDispatchIdle(); markDispatchIdle();

View File

@@ -7,6 +7,7 @@ import type {
RuntimeEnv, RuntimeEnv,
} from "clawdbot/plugin-sdk"; } from "clawdbot/plugin-sdk";
import { import {
createReplyPrefixContext,
createTypingCallbacks, createTypingCallbacks,
buildPendingHistoryContextFromMap, buildPendingHistoryContextFromMap,
clearHistoryEntriesIfEnabled, clearHistoryEntriesIfEnabled,
@@ -31,12 +32,9 @@ import {
} from "./client.js"; } from "./client.js";
import { import {
createDedupeCache, createDedupeCache,
extractShortModelName,
formatInboundFromLabel, formatInboundFromLabel,
rawDataToString, rawDataToString,
resolveIdentityName,
resolveThreadSessionKeys, resolveThreadSessionKeys,
type ResponsePrefixContext,
} from "./monitor-helpers.js"; } from "./monitor-helpers.js";
import { sendMessageMattermost } from "./send.js"; import { sendMessageMattermost } from "./send.js";
@@ -710,9 +708,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
accountId: account.accountId, accountId: account.accountId,
}); });
let prefixContext: ResponsePrefixContext = { const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
identityName: resolveIdentityName(cfg, route.agentId),
};
const typingCallbacks = createTypingCallbacks({ const typingCallbacks = createTypingCallbacks({
start: () => sendTypingIndicator(channelId, threadRootId), start: () => sendTypingIndicator(channelId, threadRootId),
@@ -722,9 +718,8 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
}); });
const { dispatcher, replyOptions, markDispatchIdle } = const { dispatcher, replyOptions, markDispatchIdle } =
core.channel.reply.createReplyDispatcherWithTyping({ core.channel.reply.createReplyDispatcherWithTyping({
responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId) responsePrefix: prefixContext.responsePrefix,
.responsePrefix, responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
responsePrefixContextProvider: () => prefixContext,
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId), humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload: ReplyPayload) => { deliver: async (payload: ReplyPayload) => {
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
@@ -766,12 +761,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
...replyOptions, ...replyOptions,
disableBlockStreaming: disableBlockStreaming:
typeof account.blockStreaming === "boolean" ? !account.blockStreaming : undefined, typeof account.blockStreaming === "boolean" ? !account.blockStreaming : undefined,
onModelSelected: (ctx) => { onModelSelected: prefixContext.onModelSelected,
prefixContext.provider = ctx.provider;
prefixContext.model = extractShortModelName(ctx.model);
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
},
}, },
}); });
markDispatchIdle(); markDispatchIdle();

View File

@@ -1,4 +1,5 @@
import { import {
createReplyPrefixContext,
createTypingCallbacks, createTypingCallbacks,
resolveChannelMediaMaxBytes, resolveChannelMediaMaxBytes,
type ClawdbotConfig, type ClawdbotConfig,
@@ -48,17 +49,20 @@ export function createMSTeamsReplyDispatcher(params: {
// Typing indicator is best-effort. // Typing indicator is best-effort.
}, },
}); });
const prefixContext = createReplyPrefixContext({
cfg: params.cfg,
agentId: params.agentId,
});
return core.channel.reply.createReplyDispatcherWithTyping({ const { dispatcher, replyOptions, markDispatchIdle } =
responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig( core.channel.reply.createReplyDispatcherWithTyping({
params.cfg, responsePrefix: prefixContext.responsePrefix,
params.agentId, responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
).responsePrefix, humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId), deliver: async (payload) => {
deliver: async (payload) => { const tableMode = core.channel.text.resolveMarkdownTableMode({
const tableMode = core.channel.text.resolveMarkdownTableMode({ cfg: params.cfg,
cfg: params.cfg, channel: "msteams",
channel: "msteams",
}); });
const messages = renderReplyPayloadsToMessages([payload], { const messages = renderReplyPayloadsToMessages([payload], {
textChunkLimit: params.textLimit, textChunkLimit: params.textLimit,
@@ -90,21 +94,27 @@ export function createMSTeamsReplyDispatcher(params: {
mediaMaxBytes, mediaMaxBytes,
}); });
if (ids.length > 0) params.onSentMessageIds?.(ids); if (ids.length > 0) params.onSentMessageIds?.(ids);
}, },
onError: (err, info) => { onError: (err, info) => {
const errMsg = formatUnknownError(err); const errMsg = formatUnknownError(err);
const classification = classifyMSTeamsSendError(err); const classification = classifyMSTeamsSendError(err);
const hint = formatMSTeamsSendErrorHint(classification); const hint = formatMSTeamsSendErrorHint(classification);
params.runtime.error?.( params.runtime.error?.(
`msteams ${info.kind} reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`, `msteams ${info.kind} reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`,
); );
params.log.error("reply failed", { params.log.error("reply failed", {
kind: info.kind, kind: info.kind,
error: errMsg, error: errMsg,
classification, classification,
hint, hint,
}); });
}, },
onReplyStart: typingCallbacks.onReplyStart, onReplyStart: typingCallbacks.onReplyStart,
}); });
return {
dispatcher,
replyOptions: { ...replyOptions, onModelSelected: prefixContext.onModelSelected },
markDispatchIdle,
};
} }

View File

@@ -0,0 +1,41 @@
import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../agents/identity.js";
import type { ClawdbotConfig } from "../config/config.js";
import type { GetReplyOptions } from "../auto-reply/types.js";
import {
extractShortModelName,
type ResponsePrefixContext,
} from "../auto-reply/reply/response-prefix-template.js";
type ModelSelectionContext = Parameters<NonNullable<GetReplyOptions["onModelSelected"]>>[0];
export type ReplyPrefixContextBundle = {
prefixContext: ResponsePrefixContext;
responsePrefix?: string;
responsePrefixContextProvider: () => ResponsePrefixContext;
onModelSelected: (ctx: ModelSelectionContext) => void;
};
export function createReplyPrefixContext(params: {
cfg: ClawdbotConfig;
agentId: string;
}): ReplyPrefixContextBundle {
const { cfg, agentId } = params;
const prefixContext: ResponsePrefixContext = {
identityName: resolveIdentityName(cfg, agentId),
};
const onModelSelected = (ctx: ModelSelectionContext) => {
// Mutate the object directly instead of reassigning to ensure closures see updates.
prefixContext.provider = ctx.provider;
prefixContext.model = extractShortModelName(ctx.model);
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
};
return {
prefixContext,
responsePrefix: resolveEffectiveMessagesConfig(cfg, agentId).responsePrefix,
responsePrefixContextProvider: () => prefixContext,
onModelSelected,
};
}

View File

@@ -1,17 +1,9 @@
import { import { resolveAckReaction, resolveHumanDelayConfig } from "../../agents/identity.js";
resolveAckReaction,
resolveEffectiveMessagesConfig,
resolveHumanDelayConfig,
resolveIdentityName,
} from "../../agents/identity.js";
import {
extractShortModelName,
type ResponsePrefixContext,
} from "../../auto-reply/reply/response-prefix-template.js";
import { import {
removeAckReactionAfterReply, removeAckReactionAfterReply,
shouldAckReaction as shouldAckReactionGate, shouldAckReaction as shouldAckReactionGate,
} from "../../channels/ack-reactions.js"; } from "../../channels/ack-reactions.js";
import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
import { createTypingCallbacks } from "../../channels/typing.js"; import { createTypingCallbacks } from "../../channels/typing.js";
import { import {
formatInboundEnvelope, formatInboundEnvelope,
@@ -318,10 +310,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
? deliverTarget.slice("channel:".length) ? deliverTarget.slice("channel:".length)
: message.channelId; : message.channelId;
// Create mutable context for response prefix template interpolation const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
let prefixContext: ResponsePrefixContext = {
identityName: resolveIdentityName(cfg, route.agentId),
};
const tableMode = resolveMarkdownTableMode({ const tableMode = resolveMarkdownTableMode({
cfg, cfg,
channel: "discord", channel: "discord",
@@ -329,8 +318,8 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
}); });
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({ const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: () => prefixContext, responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
humanDelay: resolveHumanDelayConfig(cfg, route.agentId), humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload: ReplyPayload) => { deliver: async (payload: ReplyPayload) => {
const replyToId = replyReference.use(); const replyToId = replyReference.use();
@@ -371,11 +360,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
? !discordConfig.blockStreaming ? !discordConfig.blockStreaming
: undefined, : undefined,
onModelSelected: (ctx) => { onModelSelected: (ctx) => {
// Mutate the object directly instead of reassigning to ensure the closure sees updates prefixContext.onModelSelected(ctx);
prefixContext.provider = ctx.provider;
prefixContext.model = extractShortModelName(ctx.model);
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
}, },
}, },
}); });

View File

@@ -1,14 +1,6 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import { import { resolveHumanDelayConfig } from "../../agents/identity.js";
resolveEffectiveMessagesConfig,
resolveHumanDelayConfig,
resolveIdentityName,
} from "../../agents/identity.js";
import {
extractShortModelName,
type ResponsePrefixContext,
} from "../../auto-reply/reply/response-prefix-template.js";
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
import { hasControlCommand } from "../../auto-reply/command-detection.js"; import { hasControlCommand } from "../../auto-reply/command-detection.js";
import { import {
@@ -31,6 +23,7 @@ import {
} from "../../auto-reply/reply/history.js"; } from "../../auto-reply/reply/history.js";
import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js"; import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js";
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js"; import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
import { recordInboundSession } from "../../channels/session.js"; import { recordInboundSession } from "../../channels/session.js";
import { loadConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js";
import { import {
@@ -531,14 +524,11 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
); );
} }
// Create mutable context for response prefix template interpolation const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
let prefixContext: ResponsePrefixContext = {
identityName: resolveIdentityName(cfg, route.agentId),
};
const dispatcher = createReplyDispatcher({ const dispatcher = createReplyDispatcher({
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: () => prefixContext, responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
humanDelay: resolveHumanDelayConfig(cfg, route.agentId), humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload) => { deliver: async (payload) => {
await deliverReplies({ await deliverReplies({
@@ -565,13 +555,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
typeof accountInfo.config.blockStreaming === "boolean" typeof accountInfo.config.blockStreaming === "boolean"
? !accountInfo.config.blockStreaming ? !accountInfo.config.blockStreaming
: undefined, : undefined,
onModelSelected: (ctx) => { onModelSelected: prefixContext.onModelSelected,
// Mutate the object directly instead of reassigning to ensure the closure sees updates
prefixContext.provider = ctx.provider;
prefixContext.model = extractShortModelName(ctx.model);
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
},
}, },
}); });
if (!queuedFinal) { if (!queuedFinal) {

View File

@@ -130,6 +130,7 @@ export {
shouldAckReactionForWhatsApp, shouldAckReactionForWhatsApp,
} from "../channels/ack-reactions.js"; } from "../channels/ack-reactions.js";
export { createTypingCallbacks } from "../channels/typing.js"; export { createTypingCallbacks } from "../channels/typing.js";
export { createReplyPrefixContext } from "../channels/reply-prefix.js";
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js"; export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
export type { NormalizedLocation } from "../channels/location.js"; export type { NormalizedLocation } from "../channels/location.js";
export { formatLocationText, toLocationContext } from "../channels/location.js"; export { formatLocationText, toLocationContext } from "../channels/location.js";

View File

@@ -1,12 +1,4 @@
import { import { resolveHumanDelayConfig } from "../../agents/identity.js";
resolveEffectiveMessagesConfig,
resolveHumanDelayConfig,
resolveIdentityName,
} from "../../agents/identity.js";
import {
extractShortModelName,
type ResponsePrefixContext,
} from "../../auto-reply/reply/response-prefix-template.js";
import { hasControlCommand } from "../../auto-reply/command-detection.js"; import { hasControlCommand } from "../../auto-reply/command-detection.js";
import { import {
formatInboundEnvelope, formatInboundEnvelope,
@@ -24,6 +16,7 @@ import {
} from "../../auto-reply/reply/history.js"; } from "../../auto-reply/reply/history.js";
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js"; import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
import { recordInboundSession } from "../../channels/session.js"; import { recordInboundSession } from "../../channels/session.js";
import { createTypingCallbacks } from "../../channels/typing.js"; import { createTypingCallbacks } from "../../channels/typing.js";
import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js"; import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
@@ -178,10 +171,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
logVerbose(`signal inbound: from=${ctxPayload.From} len=${body.length} preview="${preview}"`); logVerbose(`signal inbound: from=${ctxPayload.From} len=${body.length} preview="${preview}"`);
} }
// Create mutable context for response prefix template interpolation const prefixContext = createReplyPrefixContext({ cfg: deps.cfg, agentId: route.agentId });
let prefixContext: ResponsePrefixContext = {
identityName: resolveIdentityName(deps.cfg, route.agentId),
};
const typingCallbacks = createTypingCallbacks({ const typingCallbacks = createTypingCallbacks({
start: async () => { start: async () => {
@@ -198,8 +188,8 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
}); });
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({ const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
responsePrefix: resolveEffectiveMessagesConfig(deps.cfg, route.agentId).responsePrefix, responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: () => prefixContext, responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
humanDelay: resolveHumanDelayConfig(deps.cfg, route.agentId), humanDelay: resolveHumanDelayConfig(deps.cfg, route.agentId),
deliver: async (payload) => { deliver: async (payload) => {
await deps.deliverReplies({ await deps.deliverReplies({
@@ -228,11 +218,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
disableBlockStreaming: disableBlockStreaming:
typeof deps.blockStreaming === "boolean" ? !deps.blockStreaming : undefined, typeof deps.blockStreaming === "boolean" ? !deps.blockStreaming : undefined,
onModelSelected: (ctx) => { onModelSelected: (ctx) => {
// Mutate the object directly instead of reassigning to ensure the closure sees updates prefixContext.onModelSelected(ctx);
prefixContext.provider = ctx.provider;
prefixContext.model = extractShortModelName(ctx.model);
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
}, },
}, },
}); });

View File

@@ -1,17 +1,11 @@
import { import { resolveHumanDelayConfig } from "../../../agents/identity.js";
resolveEffectiveMessagesConfig,
resolveHumanDelayConfig,
resolveIdentityName,
} from "../../../agents/identity.js";
import {
extractShortModelName,
type ResponsePrefixContext,
} from "../../../auto-reply/reply/response-prefix-template.js";
import { dispatchInboundMessage } from "../../../auto-reply/dispatch.js"; import { dispatchInboundMessage } from "../../../auto-reply/dispatch.js";
import { clearHistoryEntriesIfEnabled } from "../../../auto-reply/reply/history.js"; import { clearHistoryEntriesIfEnabled } from "../../../auto-reply/reply/history.js";
import { removeAckReactionAfterReply } from "../../../channels/ack-reactions.js"; import { removeAckReactionAfterReply } from "../../../channels/ack-reactions.js";
import { createReplyPrefixContext } from "../../../channels/reply-prefix.js";
import { createTypingCallbacks } from "../../../channels/typing.js"; import { createTypingCallbacks } from "../../../channels/typing.js";
import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js"; import { createReplyDispatcherWithTyping } from "../../../auto-reply/reply/reply-dispatcher.js";
import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js"; import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js";
import { removeSlackReaction } from "../../actions.js"; import { removeSlackReaction } from "../../actions.js";
import { resolveSlackThreadTargets } from "../../threading.js"; import { resolveSlackThreadTargets } from "../../threading.js";
@@ -25,6 +19,23 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
const cfg = ctx.cfg; const cfg = ctx.cfg;
const runtime = ctx.runtime; const runtime = ctx.runtime;
if (prepared.isDirectMessage) {
const sessionCfg = cfg.session;
const storePath = resolveStorePath(sessionCfg?.store, {
agentId: route.agentId,
});
await updateLastRoute({
storePath,
sessionKey: route.mainSessionKey,
deliveryContext: {
channel: "slack",
to: `user:${message.user}`,
accountId: route.accountId,
},
ctx: prepared.ctxPayload,
});
}
const { statusThreadTs } = resolveSlackThreadTargets({ const { statusThreadTs } = resolveSlackThreadTargets({
message, message,
replyToMode: ctx.replyToMode, replyToMode: ctx.replyToMode,
@@ -69,14 +80,11 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
}, },
}); });
// Create mutable context for response prefix template interpolation const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
let prefixContext: ResponsePrefixContext = {
identityName: resolveIdentityName(cfg, route.agentId),
};
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({ const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: () => prefixContext, responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
humanDelay: resolveHumanDelayConfig(cfg, route.agentId), humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
deliver: async (payload) => { deliver: async (payload) => {
const replyThreadTs = replyPlan.nextThreadTs(); const replyThreadTs = replyPlan.nextThreadTs();
@@ -112,11 +120,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
? !account.config.blockStreaming ? !account.config.blockStreaming
: undefined, : undefined,
onModelSelected: (ctx) => { onModelSelected: (ctx) => {
// Mutate the object directly instead of reassigning to ensure the closure sees updates prefixContext.onModelSelected(ctx);
prefixContext.provider = ctx.provider;
prefixContext.model = extractShortModelName(ctx.model);
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
}, },
}, },
}); });

View File

@@ -1,13 +1,9 @@
// @ts-nocheck // @ts-nocheck
import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../agents/identity.js";
import {
extractShortModelName,
type ResponsePrefixContext,
} from "../auto-reply/reply/response-prefix-template.js";
import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js"; import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js";
import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js"; import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
import { removeAckReactionAfterReply } from "../channels/ack-reactions.js"; import { removeAckReactionAfterReply } from "../channels/ack-reactions.js";
import { createReplyPrefixContext } from "../channels/reply-prefix.js";
import { createTypingCallbacks } from "../channels/typing.js"; import { createTypingCallbacks } from "../channels/typing.js";
import { danger, logVerbose } from "../globals.js"; import { danger, logVerbose } from "../globals.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
@@ -122,10 +118,7 @@ export const dispatchTelegramMessage = async ({
Boolean(draftStream) || Boolean(draftStream) ||
(typeof telegramCfg.blockStreaming === "boolean" ? !telegramCfg.blockStreaming : undefined); (typeof telegramCfg.blockStreaming === "boolean" ? !telegramCfg.blockStreaming : undefined);
// Create mutable context for response prefix template interpolation const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
let prefixContext: ResponsePrefixContext = {
identityName: resolveIdentityName(cfg, route.agentId),
};
const tableMode = resolveMarkdownTableMode({ const tableMode = resolveMarkdownTableMode({
cfg, cfg,
channel: "telegram", channel: "telegram",
@@ -136,8 +129,8 @@ export const dispatchTelegramMessage = async ({
ctx: ctxPayload, ctx: ctxPayload,
cfg, cfg,
dispatcherOptions: { dispatcherOptions: {
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix, responsePrefix: prefixContext.responsePrefix,
responsePrefixContextProvider: () => prefixContext, responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
deliver: async (payload, info) => { deliver: async (payload, info) => {
if (info.kind === "final") { if (info.kind === "final") {
await flushDraft(); await flushDraft();
@@ -176,11 +169,7 @@ export const dispatchTelegramMessage = async ({
: undefined, : undefined,
disableBlockStreaming, disableBlockStreaming,
onModelSelected: (ctx) => { onModelSelected: (ctx) => {
// Mutate the object directly instead of reassigning to ensure the closure sees updates prefixContext.onModelSelected(ctx);
prefixContext.provider = ctx.provider;
prefixContext.model = extractShortModelName(ctx.model);
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
}, },
}, },
}); });

View File

@@ -1,12 +1,4 @@
import { import { resolveIdentityNamePrefix } from "../../../agents/identity.js";
resolveEffectiveMessagesConfig,
resolveIdentityName,
resolveIdentityNamePrefix,
} from "../../../agents/identity.js";
import {
extractShortModelName,
type ResponsePrefixContext,
} from "../../../auto-reply/reply/response-prefix-template.js";
import { resolveTextChunkLimit } from "../../../auto-reply/chunk.js"; import { resolveTextChunkLimit } from "../../../auto-reply/chunk.js";
import { import {
formatInboundEnvelope, formatInboundEnvelope,
@@ -22,6 +14,7 @@ import type { ReplyPayload } from "../../../auto-reply/types.js";
import { shouldComputeCommandAuthorized } from "../../../auto-reply/command-detection.js"; import { shouldComputeCommandAuthorized } from "../../../auto-reply/command-detection.js";
import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js"; import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js";
import { toLocationContext } from "../../../channels/location.js"; import { toLocationContext } from "../../../channels/location.js";
import { createReplyPrefixContext } from "../../../channels/reply-prefix.js";
import type { loadConfig } from "../../../config/config.js"; import type { loadConfig } from "../../../config/config.js";
import { import {
readSessionUpdatedAt, readSessionUpdatedAt,
@@ -247,22 +240,20 @@ export async function processMessage(params: {
? await resolveWhatsAppCommandAuthorized({ cfg: params.cfg, msg: params.msg }) ? await resolveWhatsAppCommandAuthorized({ cfg: params.cfg, msg: params.msg })
: undefined; : undefined;
const configuredResponsePrefix = params.cfg.messages?.responsePrefix; const configuredResponsePrefix = params.cfg.messages?.responsePrefix;
const resolvedMessages = resolveEffectiveMessagesConfig(params.cfg, params.route.agentId); const prefixContext = createReplyPrefixContext({
cfg: params.cfg,
agentId: params.route.agentId,
});
const isSelfChat = const isSelfChat =
params.msg.chatType !== "group" && params.msg.chatType !== "group" &&
Boolean(params.msg.selfE164) && Boolean(params.msg.selfE164) &&
normalizeE164(params.msg.from) === normalizeE164(params.msg.selfE164 ?? ""); normalizeE164(params.msg.from) === normalizeE164(params.msg.selfE164 ?? "");
const responsePrefix = const responsePrefix =
resolvedMessages.responsePrefix ?? prefixContext.responsePrefix ??
(configuredResponsePrefix === undefined && isSelfChat (configuredResponsePrefix === undefined && isSelfChat
? (resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[clawdbot]") ? (resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[clawdbot]")
: undefined); : undefined);
// Create mutable context for response prefix template interpolation
let prefixContext: ResponsePrefixContext = {
identityName: resolveIdentityName(params.cfg, params.route.agentId),
};
const ctxPayload = finalizeInboundContext({ const ctxPayload = finalizeInboundContext({
Body: combinedBody, Body: combinedBody,
RawBody: params.msg.body, RawBody: params.msg.body,
@@ -334,7 +325,7 @@ export async function processMessage(params: {
replyResolver: params.replyResolver, replyResolver: params.replyResolver,
dispatcherOptions: { dispatcherOptions: {
responsePrefix, responsePrefix,
responsePrefixContextProvider: () => prefixContext, responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
onHeartbeatStrip: () => { onHeartbeatStrip: () => {
if (!didLogHeartbeatStrip) { if (!didLogHeartbeatStrip) {
didLogHeartbeatStrip = true; didLogHeartbeatStrip = true;
@@ -393,13 +384,7 @@ export async function processMessage(params: {
typeof params.cfg.channels?.whatsapp?.blockStreaming === "boolean" typeof params.cfg.channels?.whatsapp?.blockStreaming === "boolean"
? !params.cfg.channels.whatsapp.blockStreaming ? !params.cfg.channels.whatsapp.blockStreaming
: undefined, : undefined,
onModelSelected: (ctx) => { onModelSelected: prefixContext.onModelSelected,
// Mutate the object directly instead of reassigning to ensure the closure sees updates
prefixContext.provider = ctx.provider;
prefixContext.model = extractShortModelName(ctx.model);
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
},
}, },
}); });