From 40719bcb74621820b55475f43b0ceb20d7ba1c21 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 06:02:43 +0100 Subject: [PATCH] refactor: move cron output policy to channel plugins --- .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- extensions/telegram/src/channel.ts | 7 + .../reply/dispatch-acp-delivery.test.ts | 70 ++++----- src/auto-reply/reply/dispatch-acp-delivery.ts | 3 - src/channels/plugins/outbound.types.ts | 1 + src/channels/plugins/types.core.ts | 4 + .../channel-output-policy.test.ts | 63 ++++++++ .../isolated-agent/channel-output-policy.ts | 45 ++++++ src/cron/isolated-agent/run-executor.ts | 33 ++-- .../run.message-tool-policy.test.ts | 142 +++++++++++------- src/cron/isolated-agent/run.test-harness.ts | 7 +- src/cron/isolated-agent/run.ts | 5 +- 12 files changed, 264 insertions(+), 120 deletions(-) create mode 100644 src/cron/isolated-agent/channel-output-policy.test.ts create mode 100644 src/cron/isolated-agent/channel-output-policy.ts diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 2f552d7ba51..df980c8b9e1 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -fc00be212cab9fa24cf625fd9afb8f6d0871509afcc42baa6653d3ef26a991d1 plugin-sdk-api-baseline.json -efe8884ee3a296ae77b80f1485d17744397c5868c110b23eb5cf99ce2587a03f plugin-sdk-api-baseline.jsonl +bd14f9118c8359c8ab0a7da984be28a319e82fadb004f55dc5888c0a07d411d3 plugin-sdk-api-baseline.json +ef09464bba3712998c0accf9a4e551ba31af4d7a2f77ce01120a1f4b48ca4ac5 plugin-sdk-api-baseline.jsonl diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 701e953ff8c..e7eff8477bf 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -991,6 +991,12 @@ export const telegramPlugin = createChatChannelPlugin({ topLevelReplyToMode: "telegram", buildToolContext: (params) => buildTelegramThreadingToolContext(params), resolveAutoThreadId: ({ to, toolContext }) => resolveTelegramAutoThreadId({ to, toolContext }), + resolveCurrentChannelId: ({ to, threadId }) => { + if (threadId == null) { + return to; + } + return to.includes(":topic:") ? to : `${to}:topic:${threadId}`; + }, }, outbound: { base: { @@ -1020,6 +1026,7 @@ export const telegramPlugin = createChatChannelPlugin({ }, shouldSkipPlainTextSanitization: ({ payload }) => Boolean(payload.channelData), shouldTreatDeliveredTextAsVisible: shouldTreatTelegramDeliveredTextAsVisible, + preferFinalAssistantVisibleText: true, targetsMatchForReplySuppression: targetsMatchTelegramReplySuppression, resolveEffectiveTextChunkLimit: ({ fallbackLimit }) => typeof fallbackLimit === "number" ? Math.min(fallbackLimit, 4096) : 4096, diff --git a/src/auto-reply/reply/dispatch-acp-delivery.test.ts b/src/auto-reply/reply/dispatch-acp-delivery.test.ts index fcd6d38bbb1..de04a958eee 100644 --- a/src/auto-reply/reply/dispatch-acp-delivery.test.ts +++ b/src/auto-reply/reply/dispatch-acp-delivery.test.ts @@ -31,7 +31,7 @@ const channelPluginMocks = vi.hoisted(() => ({ | ((params: { kind: "tool" | "block" | "final"; text?: string }) => boolean) | undefined, getChannelPlugin: vi.fn((channelId: string) => { - if (channelId !== "discord" && channelId !== "telegram") { + if (channelId !== "visiblechat") { return undefined; } return { @@ -76,8 +76,8 @@ function createCoordinator(onReplyStart?: (...args: unknown[]) => Promise) return createAcpDispatchDeliveryCoordinator({ cfg: createAcpTestConfig(), ctx: buildTestCtx({ - Provider: "discord", - Surface: "discord", + Provider: "visiblechat", + Surface: "visiblechat", SessionKey: "agent:codex-acp:session-1", }), dispatcher: createDispatcher(), @@ -87,33 +87,33 @@ function createCoordinator(onReplyStart?: (...args: unknown[]) => Promise) }); } -function createDiscordAcpCoordinator(cfg: OpenClawConfig) { +function createVisibleChatAcpCoordinator(cfg: OpenClawConfig) { return createAcpDispatchDeliveryCoordinator({ cfg, ctx: buildTestCtx({ - Provider: "discord", - Surface: "discord", + Provider: "visiblechat", + Surface: "visiblechat", SessionKey: "agent:codex-acp:session-1", }), dispatcher: createDispatcher(), inboundAudio: false, shouldRouteToOriginating: true, - originatingChannel: "discord", + originatingChannel: "visiblechat", originatingTo: "channel:thread-1", }); } -async function expectDiscordBlockRoutesToAccount( +async function expectVisibleChatBlockRoutesToAccount( cfg: OpenClawConfig, accountId: string | undefined, ): Promise { - const coordinator = createDiscordAcpCoordinator(cfg); + const coordinator = createVisibleChatAcpCoordinator(cfg); await coordinator.deliver("block", { text: "hello" }, { skipTts: true }); expect(deliveryMocks.routeReply).toHaveBeenCalledWith( expect.objectContaining({ - channel: "discord", + channel: "visiblechat", to: "channel:thread-1", accountId, }), @@ -142,8 +142,8 @@ describe("createAcpDispatchDeliveryCoordinator", () => { const coordinator = createAcpDispatchDeliveryCoordinator({ cfg: createAcpTestConfig(), ctx: buildTestCtx({ - Provider: "discord", - Surface: "discord", + Provider: "visiblechat", + Surface: "visiblechat", SessionKey: "agent:codex-acp:session-1", }), dispatcher, @@ -178,8 +178,8 @@ describe("createAcpDispatchDeliveryCoordinator", () => { const coordinator = createAcpDispatchDeliveryCoordinator({ cfg: createAcpTestConfig(), ctx: buildTestCtx({ - Provider: "telegram", - Surface: "telegram", + Provider: "visiblechat", + Surface: "visiblechat", SessionKey: "agent:codex-acp:session-1", }), dispatcher: createDispatcher(), @@ -196,11 +196,11 @@ describe("createAcpDispatchDeliveryCoordinator", () => { expect(coordinator.getRoutedCounts().block).toBe(0); }); - it("prefers provider over surface when detecting direct telegram visibility", async () => { + it("prefers provider over surface when detecting direct channel visibility", async () => { const coordinator = createAcpDispatchDeliveryCoordinator({ cfg: createAcpTestConfig(), ctx: buildTestCtx({ - Provider: "telegram", + Provider: "visiblechat", Surface: "webchat", SessionKey: "agent:codex-acp:session-1", }), @@ -220,8 +220,8 @@ describe("createAcpDispatchDeliveryCoordinator", () => { const coordinator = createAcpDispatchDeliveryCoordinator({ cfg: createAcpTestConfig(), ctx: buildTestCtx({ - Provider: "whatsapp", - Surface: "whatsapp", + Provider: "plainchat", + Surface: "plainchat", SessionKey: "agent:codex-acp:session-1", }), dispatcher: createDispatcher(), @@ -238,7 +238,7 @@ describe("createAcpDispatchDeliveryCoordinator", () => { expect(coordinator.getRoutedCounts().block).toBe(0); }); - it("treats direct discord block text as visible", async () => { + it("treats direct plugin-owned block text as visible", async () => { const coordinator = createCoordinator(); await coordinator.deliver("block", { text: "hello" }, { skipTts: true }); @@ -266,7 +266,7 @@ describe("createAcpDispatchDeliveryCoordinator", () => { expect(coordinator.hasFailedVisibleTextDelivery()).toBe(false); }); - it("tracks failed visible telegram block delivery separately", async () => { + it("tracks failed visible block delivery separately", async () => { const dispatcher: ReplyDispatcher = { sendToolResult: vi.fn(() => true), sendBlockReply: vi.fn(() => false), @@ -279,8 +279,8 @@ describe("createAcpDispatchDeliveryCoordinator", () => { const coordinator = createAcpDispatchDeliveryCoordinator({ cfg: createAcpTestConfig(), ctx: buildTestCtx({ - Provider: "telegram", - Surface: "telegram", + Provider: "visiblechat", + Surface: "visiblechat", SessionKey: "agent:codex-acp:session-1", }), dispatcher, @@ -351,8 +351,8 @@ describe("createAcpDispatchDeliveryCoordinator", () => { const coordinator = createAcpDispatchDeliveryCoordinator({ cfg: createAcpTestConfig(), ctx: buildTestCtx({ - Provider: "discord", - Surface: "discord", + Provider: "visiblechat", + Surface: "visiblechat", SessionKey: "agent:codex-acp:session-1", }), dispatcher, @@ -377,16 +377,16 @@ describe("createAcpDispatchDeliveryCoordinator", () => { const coordinator = createAcpDispatchDeliveryCoordinator({ cfg: createAcpTestConfig(), ctx: buildTestCtx({ - Provider: "telegram", - Surface: "telegram", + Provider: "visiblechat", + Surface: "visiblechat", SessionKey: "agent:codex-acp:session-1", }), dispatcher, inboundAudio: false, suppressUserDelivery: true, shouldRouteToOriginating: true, - originatingChannel: "telegram", - originatingTo: "telegram:123", + originatingChannel: "visiblechat", + originatingTo: "visiblechat:123", }); const blockDelivered = await coordinator.deliver("block", { text: "working on it" }); @@ -402,10 +402,10 @@ describe("createAcpDispatchDeliveryCoordinator", () => { }); it("routes ACP replies through the configured default account when AccountId is omitted", async () => { - await expectDiscordBlockRoutesToAccount( + await expectVisibleChatBlockRoutesToAccount( createAcpTestConfig({ channels: { - discord: { + visiblechat: { defaultAccount: "work", }, }, @@ -415,21 +415,21 @@ describe("createAcpDispatchDeliveryCoordinator", () => { }); it("routes ACP replies when cfg.channels is missing", async () => { - await expectDiscordBlockRoutesToAccount({} as OpenClawConfig, undefined); + await expectVisibleChatBlockRoutesToAccount({} as OpenClawConfig, undefined); }); - it("treats routed discord block text as visible", async () => { + it("treats routed plugin-owned block text as visible", async () => { const coordinator = createAcpDispatchDeliveryCoordinator({ cfg: createAcpTestConfig(), ctx: buildTestCtx({ - Provider: "discord", - Surface: "discord", + Provider: "visiblechat", + Surface: "visiblechat", SessionKey: "agent:codex-acp:session-1", }), dispatcher: createDispatcher(), inboundAudio: false, shouldRouteToOriginating: true, - originatingChannel: "discord", + originatingChannel: "visiblechat", originatingTo: "channel:thread-1", }); diff --git a/src/auto-reply/reply/dispatch-acp-delivery.ts b/src/auto-reply/reply/dispatch-acp-delivery.ts index 0403e2098f2..10e04300e2d 100644 --- a/src/auto-reply/reply/dispatch-acp-delivery.ts +++ b/src/auto-reply/reply/dispatch-acp-delivery.ts @@ -82,9 +82,6 @@ async function shouldTreatDeliveredTextAsVisible(params: { text: params.text, }); } - if (!params.routed) { - return channelId === "telegram"; - } return false; } diff --git a/src/channels/plugins/outbound.types.ts b/src/channels/plugins/outbound.types.ts index 98879380f4c..938998cfa05 100644 --- a/src/channels/plugins/outbound.types.ts +++ b/src/channels/plugins/outbound.types.ts @@ -115,6 +115,7 @@ export type ChannelOutboundAdapter = { kind: "tool" | "block" | "final"; text?: string; }) => boolean; + preferFinalAssistantVisibleText?: boolean; targetsMatchForReplySuppression?: (params: { originTarget: string; targetKey: string; diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index 4b0baaa3da0..a5e1cc31c7f 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -385,6 +385,10 @@ export type ChannelThreadingAdapter = { toolContext?: ChannelThreadingToolContext; replyToId?: string | null; }) => string | undefined; + resolveCurrentChannelId?: (params: { + to: string; + threadId?: string | number | null; + }) => string | undefined; resolveReplyTransport?: (params: { cfg: OpenClawConfig; accountId?: string | null; diff --git a/src/cron/isolated-agent/channel-output-policy.test.ts b/src/cron/isolated-agent/channel-output-policy.test.ts new file mode 100644 index 00000000000..1f028dec6a6 --- /dev/null +++ b/src/cron/isolated-agent/channel-output-policy.test.ts @@ -0,0 +1,63 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + resolveCronChannelOutputPolicy, + resolveCurrentChannelTarget, +} from "./channel-output-policy.js"; + +const channelPluginMocks = vi.hoisted(() => ({ + getChannelPlugin: vi.fn((channelId: string) => { + if (channelId !== "topicchat") { + return undefined; + } + return { + threading: { + resolveCurrentChannelId: ({ + to, + threadId, + }: { + to: string; + threadId?: string | number | null; + }) => (threadId == null ? to : `${to}#${threadId}`), + }, + outbound: { + preferFinalAssistantVisibleText: true, + }, + }; + }), +})); + +vi.mock("../../channels/plugins/index.js", () => ({ + getChannelPlugin: (channelId: string) => channelPluginMocks.getChannelPlugin(channelId), +})); + +describe("cron channel output policy", () => { + beforeEach(() => { + channelPluginMocks.getChannelPlugin.mockClear(); + }); + + it("reads final visible text preference from the channel plugin", async () => { + await expect(resolveCronChannelOutputPolicy("topicchat")).resolves.toEqual({ + preferFinalAssistantVisibleText: true, + }); + await expect(resolveCronChannelOutputPolicy("plainchat")).resolves.toEqual({ + preferFinalAssistantVisibleText: false, + }); + }); + + it("lets channel plugins format current tool context targets", async () => { + await expect( + resolveCurrentChannelTarget({ + channel: "topicchat", + to: "room", + threadId: 42, + }), + ).resolves.toBe("room#42"); + await expect( + resolveCurrentChannelTarget({ + channel: "plainchat", + to: "room", + threadId: 42, + }), + ).resolves.toBe("room"); + }); +}); diff --git a/src/cron/isolated-agent/channel-output-policy.ts b/src/cron/isolated-agent/channel-output-policy.ts new file mode 100644 index 00000000000..935cdbf4cce --- /dev/null +++ b/src/cron/isolated-agent/channel-output-policy.ts @@ -0,0 +1,45 @@ +import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; + +type ChannelPluginRuntime = typeof import("../../channels/plugins/index.js"); + +let channelPluginRuntimePromise: Promise | undefined; + +async function loadChannelPluginRuntime() { + channelPluginRuntimePromise ??= import("../../channels/plugins/index.js"); + return await channelPluginRuntimePromise; +} + +export async function resolveCronChannelOutputPolicy(channel: string | undefined): Promise<{ + preferFinalAssistantVisibleText: boolean; +}> { + const channelId = normalizeOptionalLowercaseString(channel); + if (!channelId) { + return { preferFinalAssistantVisibleText: false }; + } + const { getChannelPlugin } = await loadChannelPluginRuntime(); + return { + preferFinalAssistantVisibleText: + getChannelPlugin(channelId)?.outbound?.preferFinalAssistantVisibleText === true, + }; +} + +export async function resolveCurrentChannelTarget(params: { + channel?: string; + to?: string; + threadId?: string | number | null; +}): Promise { + if (!params.to) { + return undefined; + } + const channelId = normalizeOptionalLowercaseString(params.channel); + if (!channelId) { + return params.to; + } + const { getChannelPlugin } = await loadChannelPluginRuntime(); + return ( + getChannelPlugin(channelId)?.threading?.resolveCurrentChannelId?.({ + to: params.to, + threadId: params.threadId, + }) ?? params.to + ); +} diff --git a/src/cron/isolated-agent/run-executor.ts b/src/cron/isolated-agent/run-executor.ts index 26013e1bfbb..3cb9c792ac8 100644 --- a/src/cron/isolated-agent/run-executor.ts +++ b/src/cron/isolated-agent/run-executor.ts @@ -3,6 +3,10 @@ import type { ThinkLevel, VerboseLevel } from "../../auto-reply/thinking.js"; import type { AgentDefaultsConfig } from "../../config/types.agent-defaults.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { CronJob } from "../types.js"; +import { + resolveCronChannelOutputPolicy, + resolveCurrentChannelTarget, +} from "./channel-output-policy.js"; import { resolveCronPayloadOutcome } from "./helpers.js"; import { getCliSessionId, @@ -33,20 +37,6 @@ type CronSubagentRegistryRuntime = typeof import("./run-subagent-registry.runtim let cronEmbeddedRuntimePromise: Promise | undefined; let cronSubagentRegistryRuntimePromise: Promise | undefined; -function resolveCurrentChannelTarget(params: { - channel?: string; - to?: string; - threadId?: string | number; -}): string | undefined { - if (!params.to) { - return undefined; - } - if (params.channel !== "telegram" || params.threadId == null) { - return params.to; - } - return params.to.includes(":topic:") ? params.to : `${params.to}:topic:${params.threadId}`; -} - async function loadCronEmbeddedRuntime() { cronEmbeddedRuntimePromise ??= import("./run-embedded.runtime.js"); return await cronEmbeddedRuntimePromise; @@ -160,6 +150,11 @@ export function createCronPromptExecutor(params: { } const { resolveFastModeState, resolveNestedAgentLane, runEmbeddedPiAgent } = await loadCronEmbeddedRuntime(); + const currentChannelId = await resolveCurrentChannelTarget({ + channel: params.messageChannel, + to: params.resolvedDelivery.to, + threadId: params.resolvedDelivery.threadId, + }); const result = await runEmbeddedPiAgent({ sessionId: params.cronSession.sessionEntry.sessionId, sessionKey: params.agentSessionKey, @@ -171,11 +166,7 @@ export function createCronPromptExecutor(params: { agentAccountId: params.resolvedDelivery.accountId, messageTo: params.resolvedDelivery.to, messageThreadId: params.resolvedDelivery.threadId, - currentChannelId: resolveCurrentChannelTarget({ - channel: params.messageChannel, - to: params.resolvedDelivery.to, - threadId: params.resolvedDelivery.threadId, - }), + currentChannelId, sessionFile, agentDir: params.agentDir, workspaceDir: params.workspaceDir, @@ -355,7 +346,9 @@ export async function executeCronRun(params: { payloads: interimPayloads, runLevelError: runResult.meta?.error, finalAssistantVisibleText: runResult.meta?.finalAssistantVisibleText, - preferFinalAssistantVisibleText: params.resolvedDelivery.channel === "telegram", + preferFinalAssistantVisibleText: ( + await resolveCronChannelOutputPolicy(params.resolvedDelivery.channel) + ).preferFinalAssistantVisibleText, }); const interimText = interimOutputText?.trim() ?? ""; const shouldRetryInterimAck = diff --git a/src/cron/isolated-agent/run.message-tool-policy.test.ts b/src/cron/isolated-agent/run.message-tool-policy.test.ts index b2fb4a3b35d..0eeee20586c 100644 --- a/src/cron/isolated-agent/run.message-tool-policy.test.ts +++ b/src/cron/isolated-agent/run.message-tool-policy.test.ts @@ -5,6 +5,7 @@ import type { MutableCronSession } from "./run-session-state.js"; import { clearFastTestEnv, dispatchCronDeliveryMock, + getChannelPluginMock, isHeartbeatOnlyResponseMock, loadRunCronIsolatedAgentTurn, makeCronSession, @@ -96,7 +97,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { expect(runEmbeddedPiAgentMock.mock.calls[0]?.[0]).toMatchObject({ disableMessageTool: false, forceMessageTool: true, - messageChannel: "telegram", + messageChannel: "messagechat", messageTo: "123", currentChannelId: "123", }); @@ -105,9 +106,32 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { beforeEach(() => { previousFastTestEnv = clearFastTestEnv(); resetRunCronIsolatedAgentTurnHarness(); + getChannelPluginMock.mockImplementation((channelId: string) => + channelId === "topicchat" + ? { + threading: { + resolveCurrentChannelId: ({ + to, + threadId, + }: { + to: string; + threadId?: string | number | null; + }) => { + if (threadId == null) { + return to; + } + return to.includes("#") ? to : `${to}#${threadId}`; + }, + }, + outbound: { + preferFinalAssistantVisibleText: true, + }, + } + : undefined, + ); resolveDeliveryTargetMock.mockResolvedValue({ ok: true, - channel: "telegram", + channel: "messagechat", to: "123", accountId: undefined, error: undefined, @@ -137,7 +161,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { resolvedVerboseLevel: "off", thinkLevel: undefined, timeoutMs: 60_000, - messageChannel: "telegram", + messageChannel: "messagechat", toolPolicy: { requireExplicitMessageTarget: false, disableMessageTool: false, @@ -172,14 +196,14 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { resolveCronDeliveryPlanMock.mockReturnValue({ requested: false, mode: "none", - channel: "telegram", - to: "123:topic:42", + channel: "topicchat", + to: "room#42", threadId: 42, }); resolveDeliveryTargetMock.mockResolvedValue({ ok: true, - channel: "telegram", - to: "123:topic:42", + channel: "topicchat", + to: "room#42", threadId: 42, accountId: undefined, error: undefined, @@ -193,17 +217,17 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { schedule: { kind: "every", everyMs: 60_000 }, sessionTarget: "isolated", payload: { kind: "agentTurn", message: "send a message" }, - delivery: { mode: "none", channel: "telegram", to: "123:topic:42", threadId: 42 }, + delivery: { mode: "none", channel: "topicchat", to: "room#42", threadId: 42 }, } as never, }); expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); expect(runEmbeddedPiAgentMock.mock.calls[0]?.[0]).toMatchObject({ disableMessageTool: false, - messageChannel: "telegram", - messageTo: "123:topic:42", + messageChannel: "topicchat", + messageTo: "room#42", messageThreadId: 42, - currentChannelId: "123:topic:42", + currentChannelId: "room#42", }); }); @@ -236,7 +260,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { expect(runEmbeddedPiAgentMock.mock.calls[0]?.[0]).toMatchObject({ disableMessageTool: false, forceMessageTool: true, - messageChannel: "telegram", + messageChannel: "messagechat", messageTo: "123", currentChannelId: "123", }); @@ -259,9 +283,10 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { it("forwards explicit message targets into the embedded run", async () => { mockRunCronFallbackPassthrough(); const executor = createMessageToolExecutor({ + messageChannel: "topicchat", resolvedDelivery: { accountId: "ops", - to: "123:topic:42", + to: "room#42", threadId: 42, }, }); @@ -270,20 +295,21 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); expect(runEmbeddedPiAgentMock.mock.calls[0]?.[0]).toMatchObject({ - messageChannel: "telegram", + messageChannel: "topicchat", agentAccountId: "ops", - messageTo: "123:topic:42", + messageTo: "room#42", messageThreadId: 42, - currentChannelId: "123:topic:42", + currentChannelId: "room#42", }); }); - it("preserves topic routing when inferred currentChannelId is built from split delivery fields", async () => { + it("lets channels build currentChannelId from split delivery fields", async () => { mockRunCronFallbackPassthrough(); const executor = createMessageToolExecutor({ + messageChannel: "topicchat", resolvedDelivery: { accountId: "ops", - to: "123", + to: "room", threadId: 42, }, }); @@ -292,11 +318,11 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); expect(runEmbeddedPiAgentMock.mock.calls[0]?.[0]).toMatchObject({ - messageChannel: "telegram", + messageChannel: "topicchat", agentAccountId: "ops", - messageTo: "123", + messageTo: "room", messageThreadId: 42, - currentChannelId: "123:topic:42", + currentChannelId: "room#42", }); }); @@ -304,7 +330,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { await expectMessageToolEnabledForPlan({ requested: true, mode: "announce", - channel: "telegram", + channel: "messagechat", to: "123", }); }); @@ -335,7 +361,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { resolveCronDeliveryPlanMock.mockReturnValue({ requested: true, mode: "announce", - channel: "telegram", + channel: "messagechat", to: "123", }); isHeartbeatOnlyResponseMock.mockReturnValue(true); @@ -348,7 +374,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { schedule: { kind: "every", everyMs: 60_000 }, sessionTarget: "isolated", payload: { kind: "agentTurn", message: "send a message" }, - delivery: { mode: "announce", channel: "telegram", to: "123" }, + delivery: { mode: "announce", channel: "messagechat", to: "123" }, } as never, }); @@ -370,18 +396,18 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { schedule: { kind: "every", everyMs: 60_000 }, sessionTarget: "isolated", payload: { kind: "agentTurn", message: "send a message" }, - delivery: { mode: "announce", channel: "telegram", to: "123" }, + delivery: { mode: "announce", channel: "messagechat", to: "123" }, } as const; resolveCronDeliveryPlanMock.mockReturnValue({ requested: true, mode: "announce", - channel: "telegram", + channel: "messagechat", to: "123", }); runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "sent" }], didSendViaMessagingTool: true, - messagingToolSentTargets: [{ tool: "message", provider: "telegram", to: "123" }], + messagingToolSentTargets: [{ tool: "message", provider: "messagechat", to: "123" }], meta: { agentMeta: { usage: { input: 10, output: 20 } } }, }); @@ -399,9 +425,9 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { ); expect(result.delivery).toEqual( expect.objectContaining({ - intended: { channel: "telegram", to: "123", source: "explicit" }, - resolved: { ok: true, channel: "telegram", to: "123", source: "explicit" }, - messageToolSentTo: [{ channel: "telegram", to: "123" }], + intended: { channel: "messagechat", to: "123", source: "explicit" }, + resolved: { ok: true, channel: "messagechat", to: "123", source: "explicit" }, + messageToolSentTo: [{ channel: "messagechat", to: "123" }], fallbackUsed: false, delivered: true, }), @@ -417,12 +443,12 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { schedule: { kind: "every", everyMs: 60_000 }, sessionTarget: "isolated", payload: { kind: "agentTurn", message: "send a message" }, - delivery: { mode: "announce", channel: "telegram", to: "123" }, + delivery: { mode: "announce", channel: "messagechat", to: "123" }, } as const; resolveCronDeliveryPlanMock.mockReturnValue({ requested: true, mode: "announce", - channel: "telegram", + channel: "messagechat", to: "123", }); runEmbeddedPiAgentMock.mockResolvedValue({ @@ -446,9 +472,9 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { ); expect(result.delivery).toEqual( expect.objectContaining({ - intended: { channel: "telegram", to: "123", source: "explicit" }, - resolved: { ok: true, channel: "telegram", to: "123", source: "explicit" }, - messageToolSentTo: [{ channel: "telegram", to: "123" }], + intended: { channel: "messagechat", to: "123", source: "explicit" }, + resolved: { ok: true, channel: "messagechat", to: "123", source: "explicit" }, + messageToolSentTo: [{ channel: "messagechat", to: "123" }], fallbackUsed: false, delivered: true, }), @@ -460,7 +486,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { resolveCronDeliveryPlanMock.mockReturnValue({ requested: true, mode: "announce", - channel: "telegram", + channel: "messagechat", to: "123", }); runEmbeddedPiAgentMock.mockResolvedValue({ @@ -478,14 +504,14 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { schedule: { kind: "every", everyMs: 60_000 }, sessionTarget: "isolated", payload: { kind: "agentTurn", message: "send a message" }, - delivery: { mode: "announce", channel: "telegram", to: "123" }, + delivery: { mode: "announce", channel: "messagechat", to: "123" }, } as never, }); expect(result.delivery).toEqual( expect.objectContaining({ - resolved: { ok: true, channel: "telegram", to: "123", source: "explicit" }, - messageToolSentTo: [{ channel: "telegram", to: "123" }], + resolved: { ok: true, channel: "messagechat", to: "123", source: "explicit" }, + messageToolSentTo: [{ channel: "messagechat", to: "123" }], }), ); }); @@ -495,13 +521,13 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { resolveCronDeliveryPlanMock.mockReturnValue({ requested: true, mode: "announce", - channel: "telegram", + channel: "messagechat", to: "123", accountId: "bot-a", }); resolveDeliveryTargetMock.mockResolvedValue({ ok: true, - channel: "telegram", + channel: "messagechat", to: "123", accountId: "bot-a", threadId: undefined, @@ -524,13 +550,13 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { schedule: { kind: "every", everyMs: 60_000 }, sessionTarget: "isolated", payload: { kind: "agentTurn", message: "send a message" }, - delivery: { mode: "announce", channel: "telegram", to: "123", accountId: "bot-a" }, + delivery: { mode: "announce", channel: "messagechat", to: "123", accountId: "bot-a" }, } as never, }); expect(result.delivery).toEqual( expect.objectContaining({ - messageToolSentTo: [{ channel: "telegram", to: "123", accountId: "bot-a" }], + messageToolSentTo: [{ channel: "messagechat", to: "123", accountId: "bot-a" }], }), ); }); @@ -540,13 +566,13 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { resolveCronDeliveryPlanMock.mockReturnValue({ requested: true, mode: "announce", - channel: "telegram", + channel: "messagechat", to: "123", accountId: "bot-a", }); resolveDeliveryTargetMock.mockResolvedValue({ ok: true, - channel: "telegram", + channel: "messagechat", to: "123", accountId: "bot-a", threadId: undefined, @@ -567,13 +593,13 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { schedule: { kind: "every", everyMs: 60_000 }, sessionTarget: "isolated", payload: { kind: "agentTurn", message: "send a message" }, - delivery: { mode: "announce", channel: "telegram", to: "123", accountId: "bot-a" }, + delivery: { mode: "announce", channel: "messagechat", to: "123", accountId: "bot-a" }, } as never, }); expect(result.delivery).toEqual( expect.objectContaining({ - messageToolSentTo: [{ channel: "telegram", to: "123" }], + messageToolSentTo: [{ channel: "messagechat", to: "123" }], }), ); }); @@ -583,13 +609,13 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { resolveCronDeliveryPlanMock.mockReturnValue({ requested: true, mode: "announce", - channel: "telegram", + channel: "messagechat", to: "123", accountId: "bot-a", }); resolveDeliveryTargetMock.mockResolvedValue({ ok: true, - channel: "telegram", + channel: "messagechat", to: "123", accountId: "bot-a", threadId: undefined, @@ -612,7 +638,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { schedule: { kind: "every", everyMs: 60_000 }, sessionTarget: "isolated", payload: { kind: "agentTurn", message: "send a message" }, - delivery: { mode: "announce", channel: "telegram", to: "123", accountId: "bot-a" }, + delivery: { mode: "announce", channel: "messagechat", to: "123", accountId: "bot-a" }, } as never, }); @@ -642,7 +668,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "sent" }], didSendViaMessagingTool: true, - messagingToolSentTargets: [{ tool: "message", provider: "telegram", to: "123" }], + messagingToolSentTargets: [{ tool: "message", provider: "messagechat", to: "123" }], meta: { agentMeta: { usage: { input: 10, output: 20 } } }, }); @@ -664,7 +690,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { source: "last", error: "sessionKey is required to resolve delivery.channel=last", }), - messageToolSentTo: [{ channel: "telegram", to: "123" }], + messageToolSentTo: [{ channel: "messagechat", to: "123" }], fallbackUsed: false, delivered: false, }), @@ -681,7 +707,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { runEmbeddedPiAgentMock.mockResolvedValue({ payloads: [{ text: "sent" }], didSendViaMessagingTool: true, - messagingToolSentTargets: [{ tool: "message", provider: "telegram", to: "123" }], + messagingToolSentTargets: [{ tool: "message", provider: "messagechat", to: "123" }], meta: { agentMeta: { usage: { input: 10, output: 20 } } }, }); @@ -707,7 +733,7 @@ describe("runCronIsolatedAgentTurn delivery instruction", () => { resetRunCronIsolatedAgentTurnHarness(); resolveDeliveryTargetMock.mockResolvedValue({ ok: true, - channel: "telegram", + channel: "messagechat", to: "123", accountId: undefined, error: undefined, @@ -723,7 +749,7 @@ describe("runCronIsolatedAgentTurn delivery instruction", () => { resolveCronDeliveryPlanMock.mockReturnValue({ requested: true, mode: "announce", - channel: "telegram", + channel: "messagechat", to: "123", }); @@ -741,14 +767,14 @@ describe("runCronIsolatedAgentTurn delivery instruction", () => { resolveCronDeliveryPlanMock.mockReturnValue({ requested: true, mode: "announce", - channel: "telegram", + channel: "messagechat", to: "123", }); await runCronIsolatedAgentTurn({ ...makeParams(), job: makeMessageToolPolicyJob( - { mode: "announce", channel: "telegram", to: "123" }, + { mode: "announce", channel: "messagechat", to: "123" }, { kind: "agentTurn", message: "send a message", toolsAllow: ["read"] }, ), }); @@ -779,7 +805,7 @@ describe("runCronIsolatedAgentTurn delivery instruction", () => { resolveCronDeliveryPlanMock.mockReturnValue({ requested: true, mode: "announce", - channel: "telegram", + channel: "messagechat", to: "123", }); diff --git a/src/cron/isolated-agent/run.test-harness.ts b/src/cron/isolated-agent/run.test-harness.ts index bb539920273..47a98a55d29 100644 --- a/src/cron/isolated-agent/run.test-harness.ts +++ b/src/cron/isolated-agent/run.test-harness.ts @@ -68,6 +68,7 @@ export const isHeartbeatOnlyResponseMock = createMock(); export const resolveHeartbeatAckMaxCharsMock = createMock(); export const resolveSessionAuthProfileOverrideMock = createMock(); export const resolveFastModeStateMock = createMock(); +export const getChannelPluginMock = createMock(); const resolveBootstrapWarningSignaturesSeenMock = createMock(); const resolveCronStyleNowMock = createMock(); @@ -224,6 +225,10 @@ vi.mock("./helpers.js", () => ({ resolveHeartbeatAckMaxChars: resolveHeartbeatAckMaxCharsMock, })); +vi.mock("../../channels/plugins/index.js", () => ({ + getChannelPlugin: getChannelPluginMock, +})); + vi.mock("./session.js", () => ({ resolveCronSession: resolveCronSessionMock, })); @@ -375,7 +380,7 @@ function resetRunOutcomeMocks(): void { resolveDeliveryTargetMock.mockReset(); resolveDeliveryTargetMock.mockResolvedValue({ ok: true, - channel: "discord", + channel: "messagechat", to: "test-target", accountId: undefined, threadId: undefined, diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 3abad6a4ace..6a2904c24fc 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -13,6 +13,7 @@ import type { CronJob, CronRunTelemetry, } from "../types.js"; +import { resolveCronChannelOutputPolicy } from "./channel-output-policy.js"; import { isHeartbeatOnlyResponse, resolveCronPayloadOutcome, @@ -785,7 +786,9 @@ async function finalizeCronRun(params: { payloads, runLevelError: finalRunResult.meta?.error, finalAssistantVisibleText: finalRunResult.meta?.finalAssistantVisibleText, - preferFinalAssistantVisibleText: prepared.resolvedDelivery.channel === "telegram", + preferFinalAssistantVisibleText: ( + await resolveCronChannelOutputPolicy(prepared.resolvedDelivery.channel) + ).preferFinalAssistantVisibleText, }); const resolveRunOutcome = (result?: { delivered?: boolean;