refactor!: rename chat providers to channels

This commit is contained in:
Peter Steinberger
2026-01-13 06:16:43 +00:00
parent 0cd632ba84
commit 90342a4f3a
393 changed files with 8004 additions and 6737 deletions

View File

@@ -0,0 +1,17 @@
import { listChannelPlugins } from "../channels/plugins/index.js";
import type { ChannelAgentTool } from "../channels/plugins/types.js";
import type { ClawdbotConfig } from "../config/config.js";
export function listChannelAgentTools(params: {
cfg?: ClawdbotConfig;
}): ChannelAgentTool[] {
// Channel docking: aggregate channel-owned tools (login, etc.).
const tools: ChannelAgentTool[] = [];
for (const plugin of listChannelPlugins()) {
const entry = plugin.agentTools;
if (!entry) continue;
const resolved = typeof entry === "function" ? entry(params) : entry;
if (Array.isArray(resolved)) tools.push(...resolved);
}
return tools;
}

View File

@@ -73,14 +73,14 @@ describe("sessions tools", () => {
kind: "direct",
sessionId: "s-main",
updatedAt: 10,
lastProvider: "whatsapp",
lastChannel: "whatsapp",
},
{
key: "discord:group:dev",
kind: "group",
sessionId: "s-group",
updatedAt: 11,
provider: "discord",
channel: "discord",
displayName: "discord:g-dev",
},
{
@@ -120,7 +120,7 @@ describe("sessions tools", () => {
};
expect(details.sessions).toHaveLength(3);
const main = details.sessions?.find((s) => s.key === "main");
expect(main?.provider).toBe("whatsapp");
expect(main?.channel).toBe("whatsapp");
expect(main?.messages?.length).toBe(1);
expect(main?.messages?.[0]?.role).toBe("assistant");
@@ -233,7 +233,7 @@ describe("sessions tools", () => {
const tool = createClawdbotTools({
agentSessionKey: requesterKey,
agentProvider: "discord",
agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_send");
expect(tool).toBeDefined();
if (!tool) throw new Error("missing sessions_send tool");
@@ -275,7 +275,7 @@ describe("sessions tools", () => {
for (const call of agentCalls) {
expect(call.params).toMatchObject({
lane: "nested",
provider: "webchat",
channel: "webchat",
});
}
expect(
@@ -321,7 +321,7 @@ describe("sessions tools", () => {
const replyByRunId = new Map<string, string>();
const requesterKey = "discord:group:req";
const targetKey = "discord:group:target";
let sendParams: { to?: string; provider?: string; message?: string } = {};
let sendParams: { to?: string; channel?: string; message?: string } = {};
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: unknown };
calls.push(request);
@@ -371,11 +371,11 @@ describe("sessions tools", () => {
}
if (request.method === "send") {
const params = request.params as
| { to?: string; provider?: string; message?: string }
| { to?: string; channel?: string; message?: string }
| undefined;
sendParams = {
to: params?.to,
provider: params?.provider,
channel: params?.channel,
message: params?.message,
};
return { messageId: "m-announce" };
@@ -385,7 +385,7 @@ describe("sessions tools", () => {
const tool = createClawdbotTools({
agentSessionKey: requesterKey,
agentProvider: "discord",
agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_send");
expect(tool).toBeDefined();
if (!tool) throw new Error("missing sessions_send tool");
@@ -407,7 +407,7 @@ describe("sessions tools", () => {
for (const call of agentCalls) {
expect(call.params).toMatchObject({
lane: "nested",
provider: "webchat",
channel: "webchat",
});
}
@@ -423,7 +423,7 @@ describe("sessions tools", () => {
expect(replySteps).toHaveLength(2);
expect(sendParams).toMatchObject({
to: "channel:target",
provider: "discord",
channel: "discord",
message: "announce now",
});
});

View File

@@ -37,12 +37,12 @@ describe("subagents", () => {
};
});
it("sessions_spawn announces back to the requester group provider", async () => {
it("sessions_spawn announces back to the requester group channel", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
const calls: Array<{ method?: string; params?: unknown }> = [];
let agentCallCount = 0;
let sendParams: { to?: string; provider?: string; message?: string } = {};
let sendParams: { to?: string; channel?: string; message?: string } = {};
let deletedKey: string | undefined;
let childRunId: string | undefined;
let childSessionKey: string | undefined;
@@ -58,7 +58,7 @@ describe("subagents", () => {
const params = request.params as {
message?: string;
sessionKey?: string;
provider?: string;
channel?: string;
timeout?: number;
};
const message = params?.message ?? "";
@@ -69,7 +69,7 @@ describe("subagents", () => {
childRunId = runId;
childSessionKey = sessionKey;
sessionLastAssistantText.set(sessionKey, "result");
expect(params?.provider).toBe("discord");
expect(params?.channel).toBe("discord");
expect(params?.timeout).toBe(1);
}
return {
@@ -96,11 +96,11 @@ describe("subagents", () => {
}
if (request.method === "send") {
const params = request.params as
| { to?: string; provider?: string; message?: string }
| { to?: string; channel?: string; message?: string }
| undefined;
sendParams = {
to: params?.to,
provider: params?.provider,
channel: params?.channel,
message: params?.message,
};
return { messageId: "m-announce" };
@@ -115,7 +115,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "discord:group:req",
agentProvider: "discord",
agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
@@ -153,22 +153,22 @@ describe("subagents", () => {
lane?: string;
deliver?: boolean;
sessionKey?: string;
provider?: string;
channel?: string;
}
| undefined;
expect(first?.lane).toBe("subagent");
expect(first?.deliver).toBe(false);
expect(first?.provider).toBe("discord");
expect(first?.channel).toBe("discord");
expect(first?.sessionKey?.startsWith("agent:main:subagent:")).toBe(true);
expect(childSessionKey?.startsWith("agent:main:subagent:")).toBe(true);
const second = agentCalls[1]?.params as
| { provider?: string; deliver?: boolean; lane?: string }
| { channel?: string; deliver?: boolean; lane?: string }
| undefined;
expect(second?.lane).toBe("nested");
expect(second?.deliver).toBe(false);
expect(second?.provider).toBe("webchat");
expect(second?.channel).toBe("webchat");
expect(sendParams.provider).toBe("discord");
expect(sendParams.channel).toBe("discord");
expect(sendParams.to).toBe("channel:req");
expect(sendParams.message ?? "").toContain("announce now");
expect(sendParams.message ?? "").toContain("Stats:");
@@ -180,7 +180,7 @@ describe("subagents", () => {
callGatewayMock.mockReset();
const calls: Array<{ method?: string; params?: unknown }> = [];
let agentCallCount = 0;
let sendParams: { to?: string; provider?: string; message?: string } = {};
let sendParams: { to?: string; channel?: string; message?: string } = {};
let deletedKey: string | undefined;
let childRunId: string | undefined;
let childSessionKey: string | undefined;
@@ -196,7 +196,7 @@ describe("subagents", () => {
const params = request.params as {
message?: string;
sessionKey?: string;
provider?: string;
channel?: string;
timeout?: number;
};
const message = params?.message ?? "";
@@ -207,7 +207,7 @@ describe("subagents", () => {
childRunId = runId;
childSessionKey = sessionKey;
sessionLastAssistantText.set(sessionKey, "result");
expect(params?.provider).toBe("discord");
expect(params?.channel).toBe("discord");
expect(params?.timeout).toBe(1);
}
return {
@@ -238,11 +238,11 @@ describe("subagents", () => {
}
if (request.method === "send") {
const params = request.params as
| { to?: string; provider?: string; message?: string }
| { to?: string; channel?: string; message?: string }
| undefined;
sendParams = {
to: params?.to,
provider: params?.provider,
channel: params?.channel,
message: params?.message,
};
return { messageId: "m-announce" };
@@ -257,7 +257,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "discord:group:req",
agentProvider: "discord",
agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
@@ -282,13 +282,13 @@ describe("subagents", () => {
const agentCalls = calls.filter((call) => call.method === "agent");
expect(agentCalls).toHaveLength(2);
const second = agentCalls[1]?.params as
| { provider?: string; deliver?: boolean; lane?: string }
| { channel?: string; deliver?: boolean; lane?: string }
| undefined;
expect(second?.lane).toBe("nested");
expect(second?.deliver).toBe(false);
expect(second?.provider).toBe("webchat");
expect(second?.channel).toBe("webchat");
expect(sendParams.provider).toBe("discord");
expect(sendParams.channel).toBe("discord");
expect(sendParams.to).toBe("channel:req");
expect(sendParams.message ?? "").toContain("announce now");
expect(sendParams.message ?? "").toContain("Stats:");
@@ -300,7 +300,7 @@ describe("subagents", () => {
callGatewayMock.mockReset();
const calls: Array<{ method?: string; params?: unknown }> = [];
let agentCallCount = 0;
let sendParams: { to?: string; provider?: string; message?: string } = {};
let sendParams: { to?: string; channel?: string; message?: string } = {};
let childRunId: string | undefined;
let childSessionKey: string | undefined;
const waitCalls: Array<{ runId?: string; timeoutMs?: number }> = [];
@@ -314,7 +314,7 @@ describe("subagents", () => {
sessions: [
{
key: "main",
lastProvider: "whatsapp",
lastChannel: "whatsapp",
lastTo: "+123",
},
],
@@ -360,11 +360,11 @@ describe("subagents", () => {
}
if (request.method === "send") {
const params = request.params as
| { to?: string; provider?: string; message?: string }
| { to?: string; channel?: string; message?: string }
| undefined;
sendParams = {
to: params?.to,
provider: params?.provider,
channel: params?.channel,
message: params?.message,
};
return { messageId: "m1" };
@@ -377,7 +377,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "main",
agentProvider: "whatsapp",
agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
@@ -407,7 +407,7 @@ describe("subagents", () => {
const childWait = waitCalls.find((call) => call.runId === childRunId);
expect(childWait?.timeoutMs).toBe(1000);
expect(sendParams.provider).toBe("whatsapp");
expect(sendParams.channel).toBe("whatsapp");
expect(sendParams.to).toBe("+123");
expect(sendParams.message ?? "").toContain("hello from sub");
expect(sendParams.message ?? "").toContain("Stats:");
@@ -420,7 +420,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "main",
agentProvider: "whatsapp",
agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
@@ -470,7 +470,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "main",
agentProvider: "whatsapp",
agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
@@ -522,7 +522,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "main",
agentProvider: "whatsapp",
agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
@@ -574,7 +574,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "main",
agentProvider: "whatsapp",
agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
@@ -612,7 +612,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "main",
agentProvider: "whatsapp",
agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
@@ -710,7 +710,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "agent:main:main",
agentProvider: "discord",
agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
@@ -754,7 +754,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "agent:research:main",
agentProvider: "discord",
agentChannel: "discord",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
@@ -804,7 +804,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "main",
agentProvider: "whatsapp",
agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
@@ -840,7 +840,7 @@ describe("subagents", () => {
const tool = createClawdbotTools({
agentSessionKey: "main",
agentProvider: "whatsapp",
agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");

View File

@@ -1,6 +1,6 @@
import type { ClawdbotConfig } from "../config/config.js";
import { resolvePluginTools } from "../plugins/tools.js";
import type { GatewayMessageProvider } from "../utils/message-provider.js";
import type { GatewayMessageChannel } from "../utils/message-channel.js";
import { resolveSessionAgentId } from "./agent-scope.js";
import { createAgentsListTool } from "./tools/agents-list-tool.js";
import { createBrowserTool } from "./tools/browser-tool.js";
@@ -28,7 +28,7 @@ export function createClawdbotTools(options?: {
allowedControlHosts?: string[];
allowedControlPorts?: number[];
agentSessionKey?: string;
agentProvider?: GatewayMessageProvider;
agentChannel?: GatewayMessageChannel;
agentAccountId?: string;
agentDir?: string;
sandboxRoot?: string;
@@ -93,12 +93,12 @@ export function createClawdbotTools(options?: {
}),
createSessionsSendTool({
agentSessionKey: options?.agentSessionKey,
agentProvider: options?.agentProvider,
agentChannel: options?.agentChannel,
sandboxed: options?.sandboxed,
}),
createSessionsSpawnTool({
agentSessionKey: options?.agentSessionKey,
agentProvider: options?.agentProvider,
agentChannel: options?.agentChannel,
sandboxed: options?.sandboxed,
}),
createSessionStatusTool({
@@ -121,7 +121,7 @@ export function createClawdbotTools(options?: {
config: options?.config,
}),
sessionKey: options?.agentSessionKey,
messageProvider: options?.agentProvider,
messageChannel: options?.agentChannel,
agentAccountId: options?.agentAccountId,
sandboxed: options?.sandboxed,
},

View File

@@ -1,7 +1,7 @@
import {
getProviderPlugin,
normalizeProviderId,
} from "../providers/plugins/index.js";
getChannelPlugin,
normalizeChannelId,
} from "../channels/plugins/index.js";
export type MessagingToolSend = {
tool: string;
@@ -15,8 +15,8 @@ const CORE_MESSAGING_TOOLS = new Set(["sessions_send", "message"]);
// Provider docking: any plugin with `actions` opts into messaging tool handling.
export function isMessagingTool(toolName: string): boolean {
if (CORE_MESSAGING_TOOLS.has(toolName)) return true;
const providerId = normalizeProviderId(toolName);
return Boolean(providerId && getProviderPlugin(providerId)?.actions);
const providerId = normalizeChannelId(toolName);
return Boolean(providerId && getChannelPlugin(providerId)?.actions);
}
export function isMessagingToolSendAction(
@@ -28,9 +28,9 @@ export function isMessagingToolSendAction(
if (toolName === "message") {
return action === "send" || action === "thread-reply";
}
const providerId = normalizeProviderId(toolName);
const providerId = normalizeChannelId(toolName);
if (!providerId) return false;
const plugin = getProviderPlugin(providerId);
const plugin = getChannelPlugin(providerId);
if (!plugin?.actions?.extractToolSend) return false;
return Boolean(plugin.actions.extractToolSend({ args })?.to);
}
@@ -40,8 +40,8 @@ export function normalizeTargetForProvider(
raw?: string,
): string | undefined {
if (!raw) return undefined;
const providerId = normalizeProviderId(provider);
const plugin = providerId ? getProviderPlugin(providerId) : undefined;
const providerId = normalizeChannelId(provider);
const plugin = providerId ? getChannelPlugin(providerId) : undefined;
const normalized =
plugin?.messaging?.normalizeTarget?.(raw) ??
(raw.trim().toLowerCase() || undefined);

View File

@@ -478,17 +478,23 @@ describe("getDmHistoryLimitFromSessionKey", () => {
});
it("returns dmHistoryLimit for telegram provider", () => {
const config = { telegram: { dmHistoryLimit: 15 } } as ClawdbotConfig;
const config = {
channels: { telegram: { dmHistoryLimit: 15 } },
} as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(15);
});
it("returns dmHistoryLimit for whatsapp provider", () => {
const config = { whatsapp: { dmHistoryLimit: 20 } } as ClawdbotConfig;
const config = {
channels: { whatsapp: { dmHistoryLimit: 20 } },
} as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("whatsapp:dm:123", config)).toBe(20);
});
it("returns dmHistoryLimit for agent-prefixed session keys", () => {
const config = { telegram: { dmHistoryLimit: 10 } } as ClawdbotConfig;
const config = {
channels: { telegram: { dmHistoryLimit: 10 } },
} as ClawdbotConfig;
expect(
getDmHistoryLimitFromSessionKey("agent:main:telegram:dm:123", config),
).toBe(10);
@@ -496,8 +502,10 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("returns undefined for non-dm session kinds", () => {
const config = {
slack: { dmHistoryLimit: 10 },
telegram: { dmHistoryLimit: 15 },
channels: {
telegram: { dmHistoryLimit: 15 },
slack: { dmHistoryLimit: 10 },
},
} as ClawdbotConfig;
expect(
getDmHistoryLimitFromSessionKey("agent:beta:slack:channel:C1", config),
@@ -508,14 +516,16 @@ describe("getDmHistoryLimitFromSessionKey", () => {
});
it("returns undefined for unknown provider", () => {
const config = { telegram: { dmHistoryLimit: 15 } } as ClawdbotConfig;
const config = {
channels: { telegram: { dmHistoryLimit: 15 } },
} as ClawdbotConfig;
expect(
getDmHistoryLimitFromSessionKey("unknown:dm:123", config),
).toBeUndefined();
});
it("returns undefined when provider config has no dmHistoryLimit", () => {
const config = { telegram: {} } as ClawdbotConfig;
const config = { channels: { telegram: {} } } as ClawdbotConfig;
expect(
getDmHistoryLimitFromSessionKey("telegram:dm:123", config),
).toBeUndefined();
@@ -533,7 +543,9 @@ describe("getDmHistoryLimitFromSessionKey", () => {
] as const;
for (const provider of providers) {
const config = { [provider]: { dmHistoryLimit: 5 } } as ClawdbotConfig;
const config = {
channels: { [provider]: { dmHistoryLimit: 5 } },
} as ClawdbotConfig;
expect(
getDmHistoryLimitFromSessionKey(`${provider}:dm:123`, config),
).toBe(5);
@@ -554,9 +566,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
for (const provider of providers) {
// Test per-DM override takes precedence
const configWithOverride = {
[provider]: {
dmHistoryLimit: 20,
dms: { user123: { historyLimit: 7 } },
channels: {
[provider]: {
dmHistoryLimit: 20,
dms: { user123: { historyLimit: 7 } },
},
},
} as ClawdbotConfig;
expect(
@@ -586,9 +600,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("returns per-DM override when set", () => {
const config = {
telegram: {
dmHistoryLimit: 15,
dms: { "123": { historyLimit: 5 } },
channels: {
telegram: {
dmHistoryLimit: 15,
dms: { "123": { historyLimit: 5 } },
},
},
} as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(5);
@@ -596,9 +612,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("falls back to provider default when per-DM not set", () => {
const config = {
telegram: {
dmHistoryLimit: 15,
dms: { "456": { historyLimit: 5 } },
channels: {
telegram: {
dmHistoryLimit: 15,
dms: { "456": { historyLimit: 5 } },
},
},
} as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(15);
@@ -606,9 +624,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("returns per-DM override for agent-prefixed keys", () => {
const config = {
telegram: {
dmHistoryLimit: 20,
dms: { "789": { historyLimit: 3 } },
channels: {
telegram: {
dmHistoryLimit: 20,
dms: { "789": { historyLimit: 3 } },
},
},
} as ClawdbotConfig;
expect(
@@ -618,9 +638,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("handles userId with colons (e.g., email)", () => {
const config = {
msteams: {
dmHistoryLimit: 10,
dms: { "user@example.com": { historyLimit: 7 } },
channels: {
msteams: {
dmHistoryLimit: 10,
dms: { "user@example.com": { historyLimit: 7 } },
},
},
} as ClawdbotConfig;
expect(
@@ -630,8 +652,10 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("returns undefined when per-DM historyLimit is not set", () => {
const config = {
telegram: {
dms: { "123": {} },
channels: {
telegram: {
dms: { "123": {} },
},
},
} as ClawdbotConfig;
expect(
@@ -641,9 +665,11 @@ describe("getDmHistoryLimitFromSessionKey", () => {
it("returns 0 when per-DM historyLimit is explicitly 0 (unlimited)", () => {
const config = {
telegram: {
dmHistoryLimit: 15,
dms: { "123": { historyLimit: 0 } },
channels: {
telegram: {
dmHistoryLimit: 15,
dms: { "123": { historyLimit: 0 } },
},
},
} as ClawdbotConfig;
expect(getDmHistoryLimitFromSessionKey("telegram:dm:123", config)).toBe(0);

View File

@@ -33,8 +33,8 @@ import type {
} from "../auto-reply/thinking.js";
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
import { isCacheEnabled, resolveCacheTtlMs } from "../config/cache-utils.js";
import { resolveChannelCapabilities } from "../config/channel-capabilities.js";
import type { ClawdbotConfig } from "../config/config.js";
import { resolveProviderCapabilities } from "../config/provider-capabilities.js";
import { getMachineDisplayName } from "../infra/machine-name.js";
import { registerUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
import { createSubsystemLogger } from "../logging.js";
@@ -42,7 +42,7 @@ import {
type enqueueCommand,
enqueueCommandInLane,
} from "../process/command-queue.js";
import { normalizeMessageProvider } from "../utils/message-provider.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import { isReasoningTagProvider } from "../utils/provider-utils.js";
import { resolveUserPath } from "../utils.js";
import { resolveClawdbotAgentDir } from "./agent-paths.js";
@@ -690,19 +690,19 @@ export function getDmHistoryLimitFromSessionKey(
// Map provider to config key
switch (provider) {
case "telegram":
return getLimit(config.telegram);
return getLimit(config.channels?.telegram);
case "whatsapp":
return getLimit(config.whatsapp);
return getLimit(config.channels?.whatsapp);
case "discord":
return getLimit(config.discord);
return getLimit(config.channels?.discord);
case "slack":
return getLimit(config.slack);
return getLimit(config.channels?.slack);
case "signal":
return getLimit(config.signal);
return getLimit(config.channels?.signal);
case "imessage":
return getLimit(config.imessage);
return getLimit(config.channels?.imessage);
case "msteams":
return getLimit(config.msteams);
return getLimit(config.channels?.msteams);
default:
return undefined;
}
@@ -1125,6 +1125,7 @@ function resolveModel(
export async function compactEmbeddedPiSession(params: {
sessionId: string;
sessionKey?: string;
messageChannel?: string;
messageProvider?: string;
agentAccountId?: string;
sessionFile: string;
@@ -1258,7 +1259,7 @@ export async function compactEmbeddedPiSession(params: {
elevated: params.bashElevated,
},
sandbox,
messageProvider: params.messageProvider,
messageProvider: params.messageChannel ?? params.messageProvider,
agentAccountId: params.agentAccountId,
sessionKey: params.sessionKey ?? params.sessionId,
agentDir,
@@ -1272,13 +1273,13 @@ export async function compactEmbeddedPiSession(params: {
});
logToolSchemasForGoogle({ tools, provider });
const machineName = await getMachineDisplayName();
const runtimeProvider = normalizeMessageProvider(
params.messageProvider,
const runtimeChannel = normalizeMessageChannel(
params.messageChannel ?? params.messageProvider,
);
const runtimeCapabilities = runtimeProvider
? (resolveProviderCapabilities({
const runtimeCapabilities = runtimeChannel
? (resolveChannelCapabilities({
cfg: params.config,
provider: runtimeProvider,
channel: runtimeChannel,
accountId: params.agentAccountId,
}) ?? [])
: undefined;
@@ -1288,7 +1289,7 @@ export async function compactEmbeddedPiSession(params: {
arch: os.arch(),
node: process.version,
model: `${provider}/${modelId}`,
provider: runtimeProvider,
channel: runtimeChannel,
capabilities: runtimeCapabilities,
};
const sandboxInfo = buildEmbeddedSandboxInfo(
@@ -1443,6 +1444,7 @@ export async function compactEmbeddedPiSession(params: {
export async function runEmbeddedPiAgent(params: {
sessionId: string;
sessionKey?: string;
messageChannel?: string;
messageProvider?: string;
agentAccountId?: string;
/** Current channel ID for auto-threading (Slack). */
@@ -1639,7 +1641,7 @@ export async function runEmbeddedPiAgent(params: {
attemptedThinking.add(thinkLevel);
log.debug(
`embedded run start: runId=${params.runId} sessionId=${params.sessionId} provider=${provider} model=${modelId} thinking=${thinkLevel} messageProvider=${params.messageProvider ?? "unknown"}`,
`embedded run start: runId=${params.runId} sessionId=${params.sessionId} provider=${provider} model=${modelId} thinking=${thinkLevel} messageChannel=${params.messageChannel ?? params.messageProvider ?? "unknown"}`,
);
await fs.mkdir(resolvedWorkspace, { recursive: true });
@@ -1698,7 +1700,7 @@ export async function runEmbeddedPiAgent(params: {
elevated: params.bashElevated,
},
sandbox,
messageProvider: params.messageProvider,
messageProvider: params.messageChannel ?? params.messageProvider,
agentAccountId: params.agentAccountId,
sessionKey: params.sessionKey ?? params.sessionId,
agentDir,

View File

@@ -6,13 +6,13 @@ import type { AgentSession } from "@mariozechner/pi-coding-agent";
import { parseReplyDirectives } from "../auto-reply/reply/reply-directives.js";
import type { ReasoningLevel } from "../auto-reply/thinking.js";
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
import {
getChannelPlugin,
normalizeChannelId,
} from "../channels/plugins/index.js";
import { resolveStateDir } from "../config/paths.js";
import { emitAgentEvent } from "../infra/agent-events.js";
import { createSubsystemLogger } from "../logging.js";
import {
getProviderPlugin,
normalizeProviderId,
} from "../providers/plugins/index.js";
import { truncateUtf16Safe } from "../utils.js";
import type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
@@ -124,15 +124,15 @@ function extractMessagingToolSend(
if (!toRaw) return undefined;
const providerRaw =
typeof args.provider === "string" ? args.provider.trim() : "";
const providerId = providerRaw ? normalizeProviderId(providerRaw) : null;
const providerId = providerRaw ? normalizeChannelId(providerRaw) : null;
const provider =
providerId ?? (providerRaw ? providerRaw.toLowerCase() : "message");
const to = normalizeTargetForProvider(provider, toRaw);
return to ? { tool: toolName, provider, accountId, to } : undefined;
}
const providerId = normalizeProviderId(toolName);
const providerId = normalizeChannelId(toolName);
if (!providerId) return undefined;
const plugin = getProviderPlugin(providerId);
const plugin = getChannelPlugin(providerId);
const extracted = plugin?.actions?.extractToolSend?.({ args });
if (!extracted?.to) return undefined;
const to = normalizeTargetForProvider(providerId, extracted.to);

View File

@@ -9,7 +9,7 @@ import {
import type { ClawdbotConfig } from "../config/config.js";
import { detectMime } from "../media/mime.js";
import { isSubagentSessionKey } from "../routing/session-key.js";
import { resolveGatewayMessageProvider } from "../utils/message-provider.js";
import { resolveGatewayMessageChannel } from "../utils/message-channel.js";
import {
resolveAgentConfig,
resolveAgentIdFromSessionKey,
@@ -21,9 +21,9 @@ import {
type ExecToolDefaults,
type ProcessToolDefaults,
} from "./bash-tools.js";
import { listChannelAgentTools } from "./channel-tools.js";
import { createClawdbotTools } from "./clawdbot-tools.js";
import type { ModelAuthMode } from "./model-auth.js";
import { listProviderAgentTools } from "./provider-tools.js";
import type { SandboxContext, SandboxToolPolicy } from "./sandbox.js";
import { assertSandboxPath } from "./sandbox-paths.js";
import { cleanSchemaForGemini } from "./schema/clean-for-gemini.js";
@@ -807,8 +807,8 @@ export function createClawdbotCodingTools(options?: {
execTool as unknown as AnyAgentTool,
bashTool,
processTool as unknown as AnyAgentTool,
// Provider docking: include provider-defined agent tools (login, etc.).
...listProviderAgentTools({ cfg: options?.config }),
// Channel docking: include channel-defined agent tools (login, etc.).
...listChannelAgentTools({ cfg: options?.config }),
...createClawdbotTools({
browserControlUrl: sandbox?.browser?.controlUrl,
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
@@ -816,7 +816,7 @@ export function createClawdbotCodingTools(options?: {
allowedControlHosts: sandbox?.browserAllowedControlHosts,
allowedControlPorts: sandbox?.browserAllowedControlPorts,
agentSessionKey: options?.sessionKey,
agentProvider: resolveGatewayMessageProvider(options?.messageProvider),
agentChannel: resolveGatewayMessageChannel(options?.messageProvider),
agentAccountId: options?.agentAccountId,
agentDir: options?.agentDir,
sandboxRoot,

View File

@@ -1,17 +0,0 @@
import type { ClawdbotConfig } from "../config/config.js";
import { listProviderPlugins } from "../providers/plugins/index.js";
import type { ProviderAgentTool } from "../providers/plugins/types.js";
export function listProviderAgentTools(params: {
cfg?: ClawdbotConfig;
}): ProviderAgentTool[] {
// Provider docking: aggregate provider-owned tools (login, etc.).
const tools: ProviderAgentTool[] = [];
for (const plugin of listProviderPlugins()) {
const entry = plugin.agentTools;
if (!entry) continue;
const resolved = typeof entry === "function" ? entry(params) : entry;
if (Array.isArray(resolved)) tools.push(...resolved);
}
return tools;
}

View File

@@ -14,6 +14,7 @@ import {
resolveProfile,
} from "../browser/config.js";
import { DEFAULT_CLAWD_BROWSER_COLOR } from "../browser/constants.js";
import { CHANNEL_IDS } from "../channels/registry.js";
import {
type ClawdbotConfig,
loadConfig,
@@ -23,7 +24,6 @@ import {
canonicalizeMainSessionAlias,
resolveAgentMainSessionKey,
} from "../config/sessions.js";
import { PROVIDER_IDS } from "../providers/registry.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { defaultRuntime } from "../runtime.js";
import { resolveUserPath } from "../utils.js";
@@ -188,7 +188,7 @@ const DEFAULT_TOOL_DENY = [
"nodes",
"cron",
"gateway",
...PROVIDER_IDS,
...CHANNEL_IDS,
];
export const DEFAULT_SANDBOX_BROWSER_IMAGE =
"clawdbot-sandbox-browser:bookworm-slim";

View File

@@ -8,7 +8,7 @@ import {
resolveStorePath,
} from "../config/sessions.js";
import { callGateway } from "../gateway/call.js";
import { INTERNAL_MESSAGE_PROVIDER } from "../utils/message-provider.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
import { AGENT_LANE_NESTED } from "./lanes.js";
import { readLatestAssistantReply, runAgentStep } from "./tools/agent-step.js";
import { resolveAnnounceTarget } from "./tools/sessions-announce-target.js";
@@ -139,7 +139,7 @@ async function buildSubagentStatsLine(params: {
export function buildSubagentSystemPrompt(params: {
requesterSessionKey?: string;
requesterProvider?: string;
requesterChannel?: string;
childSessionKey: string;
label?: string;
task?: string;
@@ -182,8 +182,8 @@ export function buildSubagentSystemPrompt(params: {
params.requesterSessionKey
? `- Requester session: ${params.requesterSessionKey}.`
: undefined,
params.requesterProvider
? `- Requester provider: ${params.requesterProvider}.`
params.requesterChannel
? `- Requester channel: ${params.requesterChannel}.`
: undefined,
`- Your session: ${params.childSessionKey}.`,
"",
@@ -195,7 +195,7 @@ export function buildSubagentSystemPrompt(params: {
function buildSubagentAnnouncePrompt(params: {
requesterSessionKey?: string;
requesterProvider?: string;
requesterChannel?: string;
announceChannel: string;
task: string;
subagentReply?: string;
@@ -205,10 +205,10 @@ function buildSubagentAnnouncePrompt(params: {
params.requesterSessionKey
? `Requester session: ${params.requesterSessionKey}.`
: undefined,
params.requesterProvider
? `Requester provider: ${params.requesterProvider}.`
params.requesterChannel
? `Requester channel: ${params.requesterChannel}.`
: undefined,
`Post target provider: ${params.announceChannel}.`,
`Post target channel: ${params.announceChannel}.`,
`Original task: ${params.task}`,
params.subagentReply
? `Sub-agent result: ${params.subagentReply}`
@@ -226,7 +226,7 @@ export async function runSubagentAnnounceFlow(params: {
childSessionKey: string;
childRunId: string;
requesterSessionKey: string;
requesterProvider?: string;
requesterChannel?: string;
requesterDisplayKey: string;
task: string;
timeoutMs: number;
@@ -269,8 +269,8 @@ export async function runSubagentAnnounceFlow(params: {
const announcePrompt = buildSubagentAnnouncePrompt({
requesterSessionKey: params.requesterSessionKey,
requesterProvider: params.requesterProvider,
announceChannel: announceTarget.provider,
requesterChannel: params.requesterChannel,
announceChannel: announceTarget.channel,
task: params.task,
subagentReply: reply,
});
@@ -280,7 +280,7 @@ export async function runSubagentAnnounceFlow(params: {
message: "Sub-agent announce step.",
extraSystemPrompt: announcePrompt,
timeoutMs: params.timeoutMs,
provider: INTERNAL_MESSAGE_PROVIDER,
channel: INTERNAL_MESSAGE_CHANNEL,
lane: AGENT_LANE_NESTED,
});
@@ -305,7 +305,7 @@ export async function runSubagentAnnounceFlow(params: {
params: {
to: announceTarget.to,
message,
provider: announceTarget.provider,
channel: announceTarget.channel,
accountId: announceTarget.accountId,
idempotencyKey: crypto.randomUUID(),
},

View File

@@ -8,7 +8,7 @@ export type SubagentRunRecord = {
runId: string;
childSessionKey: string;
requesterSessionKey: string;
requesterProvider?: string;
requesterChannel?: string;
requesterDisplayKey: string;
task: string;
cleanup: "delete" | "keep";
@@ -105,7 +105,7 @@ function ensureListener() {
childSessionKey: entry.childSessionKey,
childRunId: entry.runId,
requesterSessionKey: entry.requesterSessionKey,
requesterProvider: entry.requesterProvider,
requesterChannel: entry.requesterChannel,
requesterDisplayKey: entry.requesterDisplayKey,
task: entry.task,
timeoutMs: 30_000,
@@ -133,7 +133,7 @@ export function registerSubagentRun(params: {
runId: string;
childSessionKey: string;
requesterSessionKey: string;
requesterProvider?: string;
requesterChannel?: string;
requesterDisplayKey: string;
task: string;
cleanup: "delete" | "keep";
@@ -152,7 +152,7 @@ export function registerSubagentRun(params: {
runId: params.runId,
childSessionKey: params.childSessionKey,
requesterSessionKey: params.requesterSessionKey,
requesterProvider: params.requesterProvider,
requesterChannel: params.requesterChannel,
requesterDisplayKey: params.requesterDisplayKey,
task: params.task,
cleanup: params.cleanup,
@@ -191,7 +191,7 @@ async function waitForSubagentCompletion(runId: string, waitTimeoutMs: number) {
childSessionKey: entry.childSessionKey,
childRunId: entry.runId,
requesterSessionKey: entry.requesterSessionKey,
requesterProvider: entry.requesterProvider,
requesterChannel: entry.requesterChannel,
requesterDisplayKey: entry.requesterDisplayKey,
task: entry.task,
timeoutMs: 30_000,

View File

@@ -153,7 +153,7 @@ describe("buildAgentSystemPrompt", () => {
toolNames: ["message"],
});
expect(prompt).toContain("message: Send messages and provider actions");
expect(prompt).toContain("message: Send messages and channel actions");
expect(prompt).toContain("### message tool");
});
@@ -161,12 +161,12 @@ describe("buildAgentSystemPrompt", () => {
const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd",
runtimeInfo: {
provider: "telegram",
channel: "telegram",
capabilities: ["inlineButtons"],
},
});
expect(prompt).toContain("provider=telegram");
expect(prompt).toContain("channel=telegram");
expect(prompt).toContain("capabilities=inlineButtons");
});

View File

@@ -1,9 +1,9 @@
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { PROVIDER_IDS } from "../providers/registry.js";
import { CHANNEL_IDS } from "../channels/registry.js";
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
const MESSAGE_PROVIDER_OPTIONS = PROVIDER_IDS.join("|");
const MESSAGE_CHANNEL_OPTIONS = CHANNEL_IDS.join("|");
export function buildAgentSystemPrompt(params: {
workspaceDir: string;
@@ -26,7 +26,7 @@ export function buildAgentSystemPrompt(params: {
arch?: string;
node?: string;
model?: string;
provider?: string;
channel?: string;
capabilities?: string[];
};
sandboxInfo?: {
@@ -56,12 +56,12 @@ export function buildAgentSystemPrompt(params: {
ls: "List directory contents",
exec: "Run shell commands",
process: "Manage background exec sessions",
// Provider docking: add provider login tools here when a provider needs interactive linking.
// Channel docking: add login tools here when a channel needs interactive linking.
browser: "Control web browser",
canvas: "Present/eval/snapshot the Canvas",
nodes: "List/describe/notify/camera/screen on paired nodes",
cron: "Manage cron jobs and wake events (use for reminders)",
message: "Send messages and provider actions",
message: "Send messages and channel actions",
gateway:
"Restart, apply config, or run updates on the running Clawdbot process",
agents_list: "List agent ids allowed for sessions_spawn",
@@ -166,7 +166,7 @@ export function buildAgentSystemPrompt(params: {
? `Heartbeat prompt: ${heartbeatPrompt}`
: "Heartbeat prompt: (configured)";
const runtimeInfo = params.runtimeInfo;
const runtimeProvider = runtimeInfo?.provider?.trim().toLowerCase();
const runtimeChannel = runtimeInfo?.channel?.trim().toLowerCase();
const runtimeCapabilities = (runtimeInfo?.capabilities ?? [])
.map((cap) => String(cap).trim())
.filter(Boolean);
@@ -322,23 +322,23 @@ export function buildAgentSystemPrompt(params: {
"- [[reply_to_current]] replies to the triggering message.",
"- [[reply_to:<id>]] replies to a specific message id when you have it.",
"Whitespace inside the tag is allowed (e.g. [[ reply_to_current ]] / [[ reply_to: 123 ]]).",
"Tags are stripped before sending; support depends on the current provider config.",
"Tags are stripped before sending; support depends on the current channel config.",
"",
"## Messaging",
"- Reply in current session → automatically routes to the source provider (Signal, Telegram, etc.)",
"- Reply in current session → automatically routes to the source channel (Signal, Telegram, etc.)",
"- Cross-session messaging → use sessions_send(sessionKey, message)",
"- Never use exec/curl for provider messaging; Clawdbot handles all routing internally.",
availableTools.has("message")
? [
"",
"### message tool",
"- Use `message` for proactive sends + provider actions (polls, reactions, etc.).",
"- Use `message` for proactive sends + channel actions (polls, reactions, etc.).",
"- For `action=send`, include `to` and `message`.",
`- If multiple providers are configured, pass \`provider\` (${MESSAGE_PROVIDER_OPTIONS}).`,
`- If multiple channels are configured, pass \`channel\` (${MESSAGE_CHANNEL_OPTIONS}).`,
inlineButtonsEnabled
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."
: runtimeProvider
? `- Inline buttons not enabled for ${runtimeProvider}. If you need them, ask to add "inlineButtons" to ${runtimeProvider}.capabilities or ${runtimeProvider}.accounts.<id>.capabilities.`
: runtimeChannel
? `- Inline buttons not enabled for ${runtimeChannel}. If you need them, ask to add "inlineButtons" to ${runtimeChannel}.capabilities or ${runtimeChannel}.accounts.<id>.capabilities.`
: "",
]
.filter(Boolean)
@@ -397,8 +397,8 @@ export function buildAgentSystemPrompt(params: {
: "",
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
runtimeProvider ? `provider=${runtimeProvider}` : "",
runtimeProvider
runtimeChannel ? `channel=${runtimeChannel}` : "",
runtimeChannel
? `capabilities=${
runtimeCapabilities.length > 0
? runtimeCapabilities.join(",")

View File

@@ -1,7 +1,7 @@
import crypto from "node:crypto";
import { callGateway } from "../../gateway/call.js";
import { INTERNAL_MESSAGE_PROVIDER } from "../../utils/message-provider.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
import { AGENT_LANE_NESTED } from "../lanes.js";
import { extractAssistantText, stripToolMessages } from "./sessions-helpers.js";
@@ -25,7 +25,7 @@ export async function runAgentStep(params: {
message: string;
extraSystemPrompt: string;
timeoutMs: number;
provider?: string;
channel?: string;
lane?: string;
}): Promise<string | undefined> {
const stepIdem = crypto.randomUUID();
@@ -36,7 +36,7 @@ export async function runAgentStep(params: {
sessionKey: params.sessionKey,
idempotencyKey: stepIdem,
deliver: false,
provider: params.provider ?? INTERNAL_MESSAGE_PROVIDER,
channel: params.channel ?? INTERNAL_MESSAGE_CHANNEL,
lane: params.lane ?? AGENT_LANE_NESTED,
extraSystemPrompt: params.extraSystemPrompt,
},

View File

@@ -56,7 +56,7 @@ export async function handleDiscordAction(
cfg: ClawdbotConfig,
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const isActionEnabled = createActionGate(cfg.discord?.actions);
const isActionEnabled = createActionGate(cfg.channels?.discord?.actions);
if (messagingActions.has(action)) {
return await handleDiscordMessagingAction(action, params, isActionEnabled);

View File

@@ -2,7 +2,7 @@ import { callGateway } from "../../gateway/call.js";
import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
} from "../../utils/message-provider.js";
} from "../../utils/message-channel.js";
export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";

View File

@@ -1,5 +1,12 @@
import { Type } from "@sinclair/typebox";
import {
listChannelMessageActions,
supportsChannelMessageButtons,
} from "../../channels/plugins/message-actions.js";
import {
CHANNEL_MESSAGE_ACTION_NAMES,
type ChannelMessageActionName,
} from "../../channels/plugins/types.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import {
@@ -7,23 +14,15 @@ import {
GATEWAY_CLIENT_MODES,
} from "../../gateway/protocol/client-info.js";
import { runMessageAction } from "../../infra/outbound/message-action-runner.js";
import {
listProviderMessageActions,
supportsProviderMessageButtons,
} from "../../providers/plugins/message-actions.js";
import {
PROVIDER_MESSAGE_ACTION_NAMES,
type ProviderMessageActionName,
} from "../../providers/plugins/types.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import { stringEnum } from "../schema/typebox.js";
import type { AnyAgentTool } from "./common.js";
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
const AllMessageActions = PROVIDER_MESSAGE_ACTION_NAMES;
const AllMessageActions = CHANNEL_MESSAGE_ACTION_NAMES;
const MessageToolCommonSchema = {
provider: Type.Optional(Type.String()),
channel: Type.Optional(Type.String()),
to: Type.Optional(Type.String()),
message: Type.Optional(Type.String()),
media: Type.Optional(Type.String()),
@@ -131,8 +130,8 @@ type MessageToolOptions = {
};
function buildMessageToolSchema(cfg: ClawdbotConfig) {
const actions = listProviderMessageActions(cfg);
const includeButtons = supportsProviderMessageButtons(cfg);
const actions = listChannelMessageActions(cfg);
const includeButtons = supportsChannelMessageButtons(cfg);
return buildMessageToolSchemaFromActions(
actions.length > 0 ? actions : ["send"],
{ includeButtons },
@@ -155,14 +154,14 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
label: "Message",
name: "message",
description:
"Send messages and provider actions (polls, reactions, pins, threads, etc.) via configured provider plugins.",
"Send messages and channel actions (polls, reactions, pins, threads, etc.) via configured channel plugins.",
parameters: schema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
const cfg = options?.config ?? loadConfig();
const action = readStringParam(params, "action", {
required: true,
}) as ProviderMessageActionName;
}) as ChannelMessageActionName;
const accountId = readStringParam(params, "accountId") ?? agentAccountId;
const gateway = {

View File

@@ -322,8 +322,8 @@ export function createSessionStatusTool(opts?: {
const queueSettings = resolveQueueSettings({
cfg,
provider:
resolved.entry.provider ?? resolved.entry.lastProvider ?? "unknown",
channel:
resolved.entry.channel ?? resolved.entry.lastChannel ?? "unknown",
sessionEntry: resolved.entry,
});
const queueKey = resolved.key ?? resolved.entry.sessionId;

View File

@@ -17,7 +17,7 @@ describe("resolveAnnounceTarget", () => {
sessionKey: "agent:main:discord:group:dev",
displayKey: "agent:main:discord:group:dev",
});
expect(target).toEqual({ provider: "discord", to: "channel:dev" });
expect(target).toEqual({ channel: "discord", to: "channel:dev" });
expect(callGatewayMock).not.toHaveBeenCalled();
});
@@ -26,7 +26,7 @@ describe("resolveAnnounceTarget", () => {
sessions: [
{
key: "agent:main:whatsapp:group:123@g.us",
lastProvider: "whatsapp",
lastChannel: "whatsapp",
lastTo: "123@g.us",
lastAccountId: "work",
},
@@ -38,7 +38,7 @@ describe("resolveAnnounceTarget", () => {
displayKey: "agent:main:whatsapp:group:123@g.us",
});
expect(target).toEqual({
provider: "whatsapp",
channel: "whatsapp",
to: "123@g.us",
accountId: "work",
});

View File

@@ -1,8 +1,8 @@
import { callGateway } from "../../gateway/call.js";
import {
getProviderPlugin,
normalizeProviderId,
} from "../../providers/plugins/index.js";
getChannelPlugin,
normalizeChannelId,
} from "../../channels/plugins/index.js";
import { callGateway } from "../../gateway/call.js";
import type { AnnounceTarget } from "./sessions-send-helpers.js";
import { resolveAnnounceTargetFromKey } from "./sessions-send-helpers.js";
@@ -15,8 +15,8 @@ export async function resolveAnnounceTarget(params: {
const fallback = parsed ?? parsedDisplay ?? null;
if (fallback) {
const normalized = normalizeProviderId(fallback.provider);
const plugin = normalized ? getProviderPlugin(normalized) : null;
const normalized = normalizeChannelId(fallback.channel);
const plugin = normalized ? getChannelPlugin(normalized) : null;
if (!plugin?.meta?.preferSessionLookupForAnnounceTarget) {
return fallback;
}
@@ -35,14 +35,14 @@ export async function resolveAnnounceTarget(params: {
const match =
sessions.find((entry) => entry?.key === params.sessionKey) ??
sessions.find((entry) => entry?.key === params.displayKey);
const provider =
typeof match?.lastProvider === "string" ? match.lastProvider : undefined;
const channel =
typeof match?.lastChannel === "string" ? match.lastChannel : undefined;
const to = typeof match?.lastTo === "string" ? match.lastTo : undefined;
const accountId =
typeof match?.lastAccountId === "string"
? match.lastAccountId
: undefined;
if (provider && to) return { provider, to, accountId };
if (channel && to) return { channel, to, accountId };
} catch {
// ignore
}

View File

@@ -56,11 +56,11 @@ export function classifySessionKind(params: {
return "other";
}
export function deriveProvider(params: {
export function deriveChannel(params: {
key: string;
kind: SessionKind;
provider?: string | null;
lastProvider?: string | null;
channel?: string | null;
lastChannel?: string | null;
}): string {
if (
params.kind === "cron" ||
@@ -68,10 +68,10 @@ export function deriveProvider(params: {
params.kind === "node"
)
return "internal";
const provider = normalizeKey(params.provider ?? undefined);
if (provider) return provider;
const lastProvider = normalizeKey(params.lastProvider ?? undefined);
if (lastProvider) return lastProvider;
const channel = normalizeKey(params.channel ?? undefined);
if (channel) return channel;
const lastChannel = normalizeKey(params.lastChannel ?? undefined);
if (lastChannel) return lastChannel;
const parts = params.key.split(":").filter(Boolean);
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
return parts[0];

View File

@@ -13,7 +13,7 @@ import type { AnyAgentTool } from "./common.js";
import { jsonResult, readStringArrayParam } from "./common.js";
import {
classifySessionKind,
deriveProvider,
deriveChannel,
resolveDisplaySessionKey,
resolveInternalSessionKey,
resolveMainSessionAlias,
@@ -24,7 +24,7 @@ import {
type SessionListRow = {
key: string;
kind: SessionKind;
provider: string;
channel: string;
label?: string;
displayName?: string;
updatedAt?: number | null;
@@ -37,7 +37,7 @@ type SessionListRow = {
systemSent?: boolean;
abortedLastRun?: boolean;
sendPolicy?: string;
lastProvider?: string;
lastChannel?: string;
lastTo?: string;
lastAccountId?: string;
transcriptPath?: string;
@@ -178,21 +178,19 @@ export function createSessionsListTool(opts?: {
mainKey,
});
const entryProvider =
typeof entry.provider === "string" ? entry.provider : undefined;
const lastProvider =
typeof entry.lastProvider === "string"
? entry.lastProvider
: undefined;
const entryChannel =
typeof entry.channel === "string" ? entry.channel : undefined;
const lastChannel =
typeof entry.lastChannel === "string" ? entry.lastChannel : undefined;
const lastAccountId =
typeof entry.lastAccountId === "string"
? entry.lastAccountId
: undefined;
const derivedProvider = deriveProvider({
const derivedChannel = deriveChannel({
key,
kind,
provider: entryProvider,
lastProvider,
channel: entryChannel,
lastChannel,
});
const sessionId =
@@ -205,7 +203,7 @@ export function createSessionsListTool(opts?: {
const row: SessionListRow = {
key: displayKey,
kind,
provider: derivedProvider,
channel: derivedChannel,
label: typeof entry.label === "string" ? entry.label : undefined,
displayName:
typeof entry.displayName === "string"
@@ -241,7 +239,7 @@ export function createSessionsListTool(opts?: {
: undefined,
sendPolicy:
typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined,
lastProvider,
lastChannel,
lastTo: typeof entry.lastTo === "string" ? entry.lastTo : undefined,
lastAccountId,
transcriptPath,

View File

@@ -1,8 +1,8 @@
import type { ClawdbotConfig } from "../../config/config.js";
import {
getProviderPlugin,
normalizeProviderId,
} from "../../providers/plugins/index.js";
getChannelPlugin,
normalizeChannelId,
} from "../../channels/plugins/index.js";
import type { ClawdbotConfig } from "../../config/config.js";
const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP";
const REPLY_SKIP_TOKEN = "REPLY_SKIP";
@@ -10,7 +10,7 @@ const DEFAULT_PING_PONG_TURNS = 5;
const MAX_PING_PONG_TURNS = 5;
export type AnnounceTarget = {
provider: string;
channel: string;
to: string;
accountId?: string;
};
@@ -24,29 +24,29 @@ export function resolveAnnounceTargetFromKey(
? rawParts.slice(2)
: rawParts;
if (parts.length < 3) return null;
const [providerRaw, kind, ...rest] = parts;
const [channelRaw, kind, ...rest] = parts;
if (kind !== "group" && kind !== "channel") return null;
const id = rest.join(":").trim();
if (!id) return null;
if (!providerRaw) return null;
const normalizedProvider = normalizeProviderId(providerRaw);
const provider = normalizedProvider ?? providerRaw.toLowerCase();
const kindTarget = normalizedProvider
if (!channelRaw) return null;
const normalizedChannel = normalizeChannelId(channelRaw);
const channel = normalizedChannel ?? channelRaw.toLowerCase();
const kindTarget = normalizedChannel
? kind === "channel"
? `channel:${id}`
: `group:${id}`
: id;
const normalized = normalizedProvider
? getProviderPlugin(normalizedProvider)?.messaging?.normalizeTarget?.(
const normalized = normalizedChannel
? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(
kindTarget,
)
: undefined;
return { provider, to: normalized ?? kindTarget };
return { channel, to: normalized ?? kindTarget };
}
export function buildAgentToAgentMessageContext(params: {
requesterSessionKey?: string;
requesterProvider?: string;
requesterChannel?: string;
targetSessionKey: string;
}) {
const lines = [
@@ -54,8 +54,8 @@ export function buildAgentToAgentMessageContext(params: {
params.requesterSessionKey
? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
: undefined,
params.requesterProvider
? `Agent 1 (requester) provider: ${params.requesterProvider}.`
params.requesterChannel
? `Agent 1 (requester) channel: ${params.requesterChannel}.`
: undefined,
`Agent 2 (target) session: ${params.targetSessionKey}.`,
].filter(Boolean);
@@ -64,9 +64,9 @@ export function buildAgentToAgentMessageContext(params: {
export function buildAgentToAgentReplyContext(params: {
requesterSessionKey?: string;
requesterProvider?: string;
requesterChannel?: string;
targetSessionKey: string;
targetProvider?: string;
targetChannel?: string;
currentRole: "requester" | "target";
turn: number;
maxTurns: number;
@@ -82,12 +82,12 @@ export function buildAgentToAgentReplyContext(params: {
params.requesterSessionKey
? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
: undefined,
params.requesterProvider
? `Agent 1 (requester) provider: ${params.requesterProvider}.`
params.requesterChannel
? `Agent 1 (requester) channel: ${params.requesterChannel}.`
: undefined,
`Agent 2 (target) session: ${params.targetSessionKey}.`,
params.targetProvider
? `Agent 2 (target) provider: ${params.targetProvider}.`
params.targetChannel
? `Agent 2 (target) channel: ${params.targetChannel}.`
: undefined,
`If you want to stop the ping-pong, reply exactly "${REPLY_SKIP_TOKEN}".`,
].filter(Boolean);
@@ -96,9 +96,9 @@ export function buildAgentToAgentReplyContext(params: {
export function buildAgentToAgentAnnounceContext(params: {
requesterSessionKey?: string;
requesterProvider?: string;
requesterChannel?: string;
targetSessionKey: string;
targetProvider?: string;
targetChannel?: string;
originalMessage: string;
roundOneReply?: string;
latestReply?: string;
@@ -108,12 +108,12 @@ export function buildAgentToAgentAnnounceContext(params: {
params.requesterSessionKey
? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
: undefined,
params.requesterProvider
? `Agent 1 (requester) provider: ${params.requesterProvider}.`
params.requesterChannel
? `Agent 1 (requester) channel: ${params.requesterChannel}.`
: undefined,
`Agent 2 (target) session: ${params.targetSessionKey}.`,
params.targetProvider
? `Agent 2 (target) provider: ${params.targetProvider}.`
params.targetChannel
? `Agent 2 (target) channel: ${params.targetChannel}.`
: undefined,
`Original request: ${params.originalMessage}`,
params.roundOneReply
@@ -123,7 +123,7 @@ export function buildAgentToAgentAnnounceContext(params: {
? `Latest reply: ${params.latestReply}`
: "Latest reply: (not available).",
`If you want to remain silent, reply exactly "${ANNOUNCE_SKIP_TOKEN}".`,
"Any other reply will be posted to the target provider.",
"Any other reply will be posted to the target channel.",
"After this reply, the agent-to-agent conversation is over.",
].filter(Boolean);
return lines.join("\n");

View File

@@ -28,7 +28,7 @@ describe("sessions_send gating", () => {
it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => {
const tool = createSessionsSendTool({
agentSessionKey: "agent:main:main",
agentProvider: "whatsapp",
agentChannel: "whatsapp",
});
const result = await tool.execute("call1", {

View File

@@ -13,9 +13,9 @@ import {
} from "../../routing/session-key.js";
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
import {
type GatewayMessageProvider,
INTERNAL_MESSAGE_PROVIDER,
} from "../../utils/message-provider.js";
type GatewayMessageChannel,
INTERNAL_MESSAGE_CHANNEL,
} from "../../utils/message-channel.js";
import { AGENT_LANE_NESTED } from "../lanes.js";
import { readLatestAssistantReply, runAgentStep } from "./agent-step.js";
import type { AnyAgentTool } from "./common.js";
@@ -51,7 +51,7 @@ const SessionsSendToolSchema = Type.Object({
export function createSessionsSendTool(opts?: {
agentSessionKey?: string;
agentProvider?: GatewayMessageProvider;
agentChannel?: GatewayMessageChannel;
sandboxed?: boolean;
}): AnyAgentTool {
return {
@@ -297,7 +297,7 @@ export function createSessionsSendTool(opts?: {
const agentMessageContext = buildAgentToAgentMessageContext({
requesterSessionKey: opts?.agentSessionKey,
requesterProvider: opts?.agentProvider,
requesterChannel: opts?.agentChannel,
targetSessionKey: displayKey,
});
const sendParams = {
@@ -305,12 +305,12 @@ export function createSessionsSendTool(opts?: {
sessionKey: resolvedKey,
idempotencyKey,
deliver: false,
provider: INTERNAL_MESSAGE_PROVIDER,
channel: INTERNAL_MESSAGE_CHANNEL,
lane: AGENT_LANE_NESTED,
extraSystemPrompt: agentMessageContext,
};
const requesterSessionKey = opts?.agentSessionKey;
const requesterProvider = opts?.agentProvider;
const requesterChannel = opts?.agentChannel;
const maxPingPongTurns = resolvePingPongTurns(cfg);
const delivery = { status: "pending", mode: "announce" as const };
@@ -344,7 +344,7 @@ export function createSessionsSendTool(opts?: {
sessionKey: resolvedKey,
displayKey,
});
const targetProvider = announceTarget?.provider ?? "unknown";
const targetChannel = announceTarget?.channel ?? "unknown";
if (
maxPingPongTurns > 0 &&
requesterSessionKey &&
@@ -360,9 +360,9 @@ export function createSessionsSendTool(opts?: {
: "target";
const replyPrompt = buildAgentToAgentReplyContext({
requesterSessionKey,
requesterProvider,
requesterChannel,
targetSessionKey: displayKey,
targetProvider,
targetChannel,
currentRole,
turn,
maxTurns: maxPingPongTurns,
@@ -386,9 +386,9 @@ export function createSessionsSendTool(opts?: {
}
const announcePrompt = buildAgentToAgentAnnounceContext({
requesterSessionKey,
requesterProvider,
requesterChannel,
targetSessionKey: displayKey,
targetProvider,
targetChannel,
originalMessage: message,
roundOneReply: primaryReply,
latestReply,
@@ -412,7 +412,7 @@ export function createSessionsSendTool(opts?: {
params: {
to: announceTarget.to,
message: announceReply.trim(),
provider: announceTarget.provider,
channel: announceTarget.channel,
accountId: announceTarget.accountId,
idempotencyKey: crypto.randomUUID(),
},
@@ -421,7 +421,7 @@ export function createSessionsSendTool(opts?: {
} catch (err) {
log.warn("sessions_send announce delivery failed", {
runId: runContextId,
provider: announceTarget.provider,
channel: announceTarget.channel,
to: announceTarget.to,
error: formatErrorMessage(err),
});

View File

@@ -9,7 +9,7 @@ import {
normalizeAgentId,
parseAgentSessionKey,
} from "../../routing/session-key.js";
import type { GatewayMessageProvider } from "../../utils/message-provider.js";
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
import { resolveAgentConfig } from "../agent-scope.js";
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
import { optionalStringEnum } from "../schema/typebox.js";
@@ -47,7 +47,7 @@ function normalizeModelSelection(value: unknown): string | undefined {
export function createSessionsSpawnTool(opts?: {
agentSessionKey?: string;
agentProvider?: GatewayMessageProvider;
agentChannel?: GatewayMessageChannel;
sandboxed?: boolean;
}): AnyAgentTool {
return {
@@ -174,7 +174,7 @@ export function createSessionsSpawnTool(opts?: {
}
const childSystemPrompt = buildSubagentSystemPrompt({
requesterSessionKey,
requesterProvider: opts?.agentProvider,
requesterChannel: opts?.agentChannel,
childSessionKey,
label: label || undefined,
task,
@@ -188,7 +188,7 @@ export function createSessionsSpawnTool(opts?: {
params: {
message: task,
sessionKey: childSessionKey,
provider: opts?.agentProvider,
channel: opts?.agentChannel,
idempotencyKey: childIdem,
deliver: false,
lane: AGENT_LANE_SUBAGENT,
@@ -221,7 +221,7 @@ export function createSessionsSpawnTool(opts?: {
runId: childRunId,
childSessionKey,
requesterSessionKey: requesterInternalKey,
requesterProvider: opts?.agentProvider,
requesterChannel: opts?.agentChannel,
requesterDisplayKey,
task,
cleanup,

View File

@@ -36,7 +36,7 @@ vi.mock("../../slack/actions.js", () => ({
describe("handleSlackAction", () => {
it("adds reactions", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction(
{
action: "react",
@@ -50,7 +50,7 @@ describe("handleSlackAction", () => {
});
it("removes reactions on empty emoji", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction(
{
action: "react",
@@ -64,7 +64,7 @@ describe("handleSlackAction", () => {
});
it("removes reactions when remove flag set", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction(
{
action: "react",
@@ -79,7 +79,7 @@ describe("handleSlackAction", () => {
});
it("rejects removes without emoji", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await expect(
handleSlackAction(
{
@@ -96,7 +96,7 @@ describe("handleSlackAction", () => {
it("respects reaction gating", async () => {
const cfg = {
slack: { botToken: "tok", actions: { reactions: false } },
channels: { slack: { botToken: "tok", actions: { reactions: false } } },
} as ClawdbotConfig;
await expect(
handleSlackAction(
@@ -112,7 +112,7 @@ describe("handleSlackAction", () => {
});
it("passes threadTs to sendSlackMessage for thread replies", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction(
{
action: "sendMessage",
@@ -133,7 +133,7 @@ describe("handleSlackAction", () => {
});
it("auto-injects threadTs from context when replyToMode=all", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{
@@ -159,7 +159,7 @@ describe("handleSlackAction", () => {
});
it("replyToMode=first threads first message then stops", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
const hasRepliedRef = { value: false };
const context = {
@@ -198,7 +198,7 @@ describe("handleSlackAction", () => {
});
it("replyToMode=first marks hasRepliedRef even when threadTs is explicit", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
const hasRepliedRef = { value: false };
const context = {
@@ -244,7 +244,7 @@ describe("handleSlackAction", () => {
});
it("replyToMode=first without hasRepliedRef does not thread", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{ action: "sendMessage", to: "channel:C123", content: "No ref" },
@@ -263,7 +263,7 @@ describe("handleSlackAction", () => {
});
it("does not auto-inject threadTs when replyToMode=off", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{
@@ -285,7 +285,7 @@ describe("handleSlackAction", () => {
});
it("does not auto-inject threadTs when sending to different channel", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{
@@ -311,7 +311,7 @@ describe("handleSlackAction", () => {
});
it("explicit threadTs overrides context threadTs", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{
@@ -338,7 +338,7 @@ describe("handleSlackAction", () => {
});
it("handles channel target without prefix when replyToMode=all", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{

View File

@@ -93,7 +93,7 @@ export async function handleSlackAction(
const accountId = readStringParam(params, "accountId");
const accountOpts = accountId ? { accountId } : undefined;
const account = resolveSlackAccount({ cfg, accountId });
const actionConfig = account.actions ?? cfg.slack?.actions;
const actionConfig = account.actions ?? cfg.channels?.slack?.actions;
const isActionEnabled = createActionGate(actionConfig);
if (reactionsActions.has(action)) {

View File

@@ -34,7 +34,9 @@ describe("handleTelegramAction", () => {
});
it("adds reactions", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
action: "react",
@@ -53,7 +55,9 @@ describe("handleTelegramAction", () => {
});
it("removes reactions on empty emoji", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
action: "react",
@@ -72,7 +76,9 @@ describe("handleTelegramAction", () => {
});
it("removes reactions when remove flag set", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
action: "react",
@@ -93,7 +99,9 @@ describe("handleTelegramAction", () => {
it("respects reaction gating", async () => {
const cfg = {
telegram: { botToken: "tok", actions: { reactions: false } },
channels: {
telegram: { botToken: "tok", actions: { reactions: false } },
},
} as ClawdbotConfig;
await expect(
handleTelegramAction(
@@ -109,7 +117,9 @@ describe("handleTelegramAction", () => {
});
it("sends a text message", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
const result = await handleTelegramAction(
{
action: "sendMessage",
@@ -130,7 +140,9 @@ describe("handleTelegramAction", () => {
});
it("sends a message with media", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
action: "sendMessage",
@@ -152,7 +164,9 @@ describe("handleTelegramAction", () => {
it("respects sendMessage gating", async () => {
const cfg = {
telegram: { botToken: "tok", actions: { sendMessage: false } },
channels: {
telegram: { botToken: "tok", actions: { sendMessage: false } },
},
} as ClawdbotConfig;
await expect(
handleTelegramAction(
@@ -182,7 +196,9 @@ describe("handleTelegramAction", () => {
});
it("requires inlineButtons capability when buttons are provided", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await expect(
handleTelegramAction(
{
@@ -198,7 +214,9 @@ describe("handleTelegramAction", () => {
it("sends messages with inline keyboard buttons when enabled", async () => {
const cfg = {
telegram: { botToken: "tok", capabilities: ["inlineButtons"] },
channels: {
telegram: { botToken: "tok", capabilities: ["inlineButtons"] },
},
} as ClawdbotConfig;
await handleTelegramAction(
{

View File

@@ -1,7 +1,6 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { resolveProviderCapabilities } from "../../config/provider-capabilities.js";
import {
reactMessageTelegram,
sendMessageTelegram,
@@ -26,9 +25,9 @@ function hasInlineButtonsCapability(params: {
accountId?: string | undefined;
}): boolean {
const caps =
resolveProviderCapabilities({
resolveChannelCapabilities({
cfg: params.cfg,
provider: "telegram",
channel: "telegram",
accountId: params.accountId,
}) ?? [];
return caps.some((cap) => cap.toLowerCase() === "inlinebuttons");
@@ -84,7 +83,7 @@ export async function handleTelegramAction(
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const accountId = readStringParam(params, "accountId");
const isActionEnabled = createActionGate(cfg.telegram?.actions);
const isActionEnabled = createActionGate(cfg.channels?.telegram?.actions);
if (action === "react") {
if (!isActionEnabled("reactions")) {
@@ -103,7 +102,7 @@ export async function handleTelegramAction(
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
throw new Error(
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or telegram.botToken.",
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
);
}
await reactMessageTelegram(chatId ?? "", messageId ?? 0, emoji ?? "", {
@@ -130,7 +129,7 @@ export async function handleTelegramAction(
!hasInlineButtonsCapability({ cfg, accountId: accountId ?? undefined })
) {
throw new Error(
'Telegram inline buttons requested but not enabled. Add "inlineButtons" to telegram.capabilities (or telegram.accounts.<id>.capabilities).',
'Telegram inline buttons requested but not enabled. Add "inlineButtons" to channels.telegram.capabilities (or channels.telegram.accounts.<id>.capabilities).',
);
}
// Optional threading parameters for forum topics and reply chains
@@ -143,7 +142,7 @@ export async function handleTelegramAction(
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
throw new Error(
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or telegram.botToken.",
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
);
}
const result = await sendMessageTelegram(to, content, {

View File

@@ -10,7 +10,7 @@ vi.mock("../../web/outbound.js", () => ({
}));
const enabledConfig = {
whatsapp: { actions: { reactions: true } },
channels: { whatsapp: { actions: { reactions: true } } },
} as ClawdbotConfig;
describe("handleWhatsAppAction", () => {
@@ -112,7 +112,7 @@ describe("handleWhatsAppAction", () => {
it("respects reaction gating", async () => {
const cfg = {
whatsapp: { actions: { reactions: false } },
channels: { whatsapp: { actions: { reactions: false } } },
} as ClawdbotConfig;
await expect(
handleWhatsAppAction(

View File

@@ -14,7 +14,7 @@ export async function handleWhatsAppAction(
cfg: ClawdbotConfig,
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const isActionEnabled = createActionGate(cfg.whatsapp?.actions);
const isActionEnabled = createActionGate(cfg.channels?.whatsapp?.actions);
if (action === "react") {
if (!isActionEnabled("reactions")) {