feat: multi-agent routing + multi-account providers

This commit is contained in:
Peter Steinberger
2026-01-06 18:25:37 +00:00
parent 50d4b17417
commit dbfa316d19
129 changed files with 3760 additions and 1126 deletions

View File

@@ -29,6 +29,8 @@ import type { ClawdbotConfig } from "../config/config.js";
import {
DEFAULT_IDLE_MINUTES,
loadSessionStore,
resolveAgentIdFromSessionKey,
resolveMainSessionKey,
resolveSessionTranscriptPath,
resolveStorePath,
type SessionEntry,
@@ -87,7 +89,7 @@ function isHeartbeatOnlyResponse(
function resolveDeliveryTarget(
cfg: ClawdbotConfig,
jobPayload: {
channel?:
provider?:
| "last"
| "whatsapp"
| "telegram"
@@ -98,36 +100,37 @@ function resolveDeliveryTarget(
to?: string;
},
) {
const requestedChannel =
typeof jobPayload.channel === "string" ? jobPayload.channel : "last";
const requestedProvider =
typeof jobPayload.provider === "string" ? jobPayload.provider : "last";
const explicitTo =
typeof jobPayload.to === "string" && jobPayload.to.trim()
? jobPayload.to.trim()
: undefined;
const sessionCfg = cfg.session;
const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
const storePath = resolveStorePath(sessionCfg?.store);
const mainSessionKey = resolveMainSessionKey(cfg);
const agentId = resolveAgentIdFromSessionKey(mainSessionKey);
const storePath = resolveStorePath(sessionCfg?.store, { agentId });
const store = loadSessionStore(storePath);
const main = store[mainKey];
const lastChannel =
main?.lastChannel && main.lastChannel !== "webchat"
? main.lastChannel
const main = store[mainSessionKey];
const lastProvider =
main?.lastProvider && main.lastProvider !== "webchat"
? main.lastProvider
: undefined;
const lastTo = typeof main?.lastTo === "string" ? main.lastTo.trim() : "";
const channel = (() => {
const provider = (() => {
if (
requestedChannel === "whatsapp" ||
requestedChannel === "telegram" ||
requestedChannel === "discord" ||
requestedChannel === "slack" ||
requestedChannel === "signal" ||
requestedChannel === "imessage"
requestedProvider === "whatsapp" ||
requestedProvider === "telegram" ||
requestedProvider === "discord" ||
requestedProvider === "slack" ||
requestedProvider === "signal" ||
requestedProvider === "imessage"
) {
return requestedChannel;
return requestedProvider;
}
return lastChannel ?? "whatsapp";
return lastProvider ?? "whatsapp";
})();
const to = (() => {
@@ -136,7 +139,7 @@ function resolveDeliveryTarget(
})();
const sanitizedWhatsappTo = (() => {
if (channel !== "whatsapp") return to;
if (provider !== "whatsapp") return to;
const rawAllow = cfg.whatsapp?.allowFrom ?? [];
if (rawAllow.includes("*")) return to;
const allowFrom = rawAllow
@@ -150,8 +153,8 @@ function resolveDeliveryTarget(
})();
return {
channel,
to: channel === "whatsapp" ? sanitizedWhatsappTo : to,
provider,
to: provider === "whatsapp" ? sanitizedWhatsappTo : to,
};
}
@@ -181,7 +184,7 @@ function resolveCronSession(params: {
model: entry?.model,
contextTokens: entry?.contextTokens,
sendPolicy: entry?.sendPolicy,
lastChannel: entry?.lastChannel,
lastProvider: entry?.lastProvider,
lastTo: entry?.lastTo,
};
return { storePath, store, sessionEntry, systemSent, isNewSession: !fresh };
@@ -251,9 +254,9 @@ export async function runCronIsolatedAgentTurn(params: {
params.job.payload.bestEffortDeliver === true;
const resolvedDelivery = resolveDeliveryTarget(params.cfg, {
channel:
provider:
params.job.payload.kind === "agentTurn"
? params.job.payload.channel
? params.job.payload.provider
: "last",
to:
params.job.payload.kind === "agentTurn"
@@ -302,7 +305,7 @@ export async function runCronIsolatedAgentTurn(params: {
registerAgentRunContext(cronSession.sessionEntry.sessionId, {
sessionKey: params.sessionKey,
});
const surface = resolvedDelivery.channel;
const messageProvider = resolvedDelivery.provider;
const fallbackResult = await runWithModelFallback({
cfg: params.cfg,
provider,
@@ -311,7 +314,7 @@ export async function runCronIsolatedAgentTurn(params: {
runEmbeddedPiAgent({
sessionId: cronSession.sessionEntry.sessionId,
sessionKey: params.sessionKey,
surface,
messageProvider,
sessionFile,
workspaceDir,
config: params.cfg,
@@ -380,7 +383,7 @@ export async function runCronIsolatedAgentTurn(params: {
delivery && isHeartbeatOnlyResponse(payloads, Math.max(0, ackMaxChars));
if (delivery && !skipHeartbeatDelivery) {
if (resolvedDelivery.channel === "whatsapp") {
if (resolvedDelivery.provider === "whatsapp") {
if (!resolvedDelivery.to) {
if (!bestEffortDeliver)
return {
@@ -415,7 +418,7 @@ export async function runCronIsolatedAgentTurn(params: {
return { status: "error", summary, error: String(err) };
return { status: "ok", summary };
}
} else if (resolvedDelivery.channel === "telegram") {
} else if (resolvedDelivery.provider === "telegram") {
if (!resolvedDelivery.to) {
if (!bestEffortDeliver)
return {
@@ -459,14 +462,14 @@ export async function runCronIsolatedAgentTurn(params: {
return { status: "error", summary, error: String(err) };
return { status: "ok", summary };
}
} else if (resolvedDelivery.channel === "discord") {
} else if (resolvedDelivery.provider === "discord") {
if (!resolvedDelivery.to) {
if (!bestEffortDeliver)
return {
status: "error",
summary,
error:
"Cron delivery to Discord requires --channel discord and --to <channelId|user:ID>",
"Cron delivery to Discord requires --provider discord and --to <channelId|user:ID>",
};
return {
status: "skipped",
@@ -503,14 +506,14 @@ export async function runCronIsolatedAgentTurn(params: {
return { status: "error", summary, error: String(err) };
return { status: "ok", summary };
}
} else if (resolvedDelivery.channel === "slack") {
} else if (resolvedDelivery.provider === "slack") {
if (!resolvedDelivery.to) {
if (!bestEffortDeliver)
return {
status: "error",
summary,
error:
"Cron delivery to Slack requires --channel slack and --to <channelId|user:ID>",
"Cron delivery to Slack requires --provider slack and --to <channelId|user:ID>",
};
return {
status: "skipped",
@@ -543,7 +546,7 @@ export async function runCronIsolatedAgentTurn(params: {
return { status: "error", summary, error: String(err) };
return { status: "ok", summary };
}
} else if (resolvedDelivery.channel === "signal") {
} else if (resolvedDelivery.provider === "signal") {
if (!resolvedDelivery.to) {
if (!bestEffortDeliver)
return {
@@ -582,7 +585,7 @@ export async function runCronIsolatedAgentTurn(params: {
return { status: "error", summary, error: String(err) };
return { status: "ok", summary };
}
} else if (resolvedDelivery.channel === "imessage") {
} else if (resolvedDelivery.provider === "imessage") {
if (!resolvedDelivery.to) {
if (!bestEffortDeliver)
return {