feat: wire multi-agent config and routing

Co-authored-by: Mark Pors <1078320+pors@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-09 12:44:23 +00:00
parent 81beda0772
commit 7b81d97ec2
189 changed files with 4340 additions and 2903 deletions

View File

@@ -24,22 +24,32 @@ describe("resolveHeartbeatIntervalMs", () => {
it("returns null when invalid or zero", () => {
expect(
resolveHeartbeatIntervalMs({ agent: { heartbeat: { every: "0m" } } }),
resolveHeartbeatIntervalMs({
agents: { defaults: { heartbeat: { every: "0m" } } },
}),
).toBeNull();
expect(
resolveHeartbeatIntervalMs({ agent: { heartbeat: { every: "oops" } } }),
resolveHeartbeatIntervalMs({
agents: { defaults: { heartbeat: { every: "oops" } } },
}),
).toBeNull();
});
it("parses duration strings with minute defaults", () => {
expect(
resolveHeartbeatIntervalMs({ agent: { heartbeat: { every: "5m" } } }),
resolveHeartbeatIntervalMs({
agents: { defaults: { heartbeat: { every: "5m" } } },
}),
).toBe(5 * 60_000);
expect(
resolveHeartbeatIntervalMs({ agent: { heartbeat: { every: "5" } } }),
resolveHeartbeatIntervalMs({
agents: { defaults: { heartbeat: { every: "5" } } },
}),
).toBe(5 * 60_000);
expect(
resolveHeartbeatIntervalMs({ agent: { heartbeat: { every: "2h" } } }),
resolveHeartbeatIntervalMs({
agents: { defaults: { heartbeat: { every: "2h" } } },
}),
).toBe(2 * 60 * 60_000);
});
});
@@ -51,7 +61,7 @@ describe("resolveHeartbeatPrompt", () => {
it("uses a trimmed override when configured", () => {
const cfg: ClawdbotConfig = {
agent: { heartbeat: { prompt: " ping " } },
agents: { defaults: { heartbeat: { prompt: " ping " } } },
};
expect(resolveHeartbeatPrompt(cfg)).toBe("ping");
});
@@ -65,7 +75,7 @@ describe("resolveHeartbeatDeliveryTarget", () => {
it("respects target none", () => {
const cfg: ClawdbotConfig = {
agent: { heartbeat: { target: "none" } },
agents: { defaults: { heartbeat: { target: "none" } } },
};
expect(resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry })).toEqual({
provider: "none",
@@ -101,7 +111,7 @@ describe("resolveHeartbeatDeliveryTarget", () => {
it("applies allowFrom fallback for WhatsApp targets", () => {
const cfg: ClawdbotConfig = {
agent: { heartbeat: { target: "whatsapp", to: "+1999" } },
agents: { defaults: { heartbeat: { target: "whatsapp", to: "+1999" } } },
whatsapp: { allowFrom: ["+1555", "+1666"] },
};
const entry = {
@@ -118,7 +128,7 @@ describe("resolveHeartbeatDeliveryTarget", () => {
it("keeps explicit telegram targets", () => {
const cfg: ClawdbotConfig = {
agent: { heartbeat: { target: "telegram", to: "123" } },
agents: { defaults: { heartbeat: { target: "telegram", to: "123" } } },
};
expect(resolveHeartbeatDeliveryTarget({ cfg, entry: baseEntry })).toEqual({
provider: "telegram",
@@ -150,8 +160,10 @@ describe("runHeartbeatOnce", () => {
);
const cfg: ClawdbotConfig = {
agent: {
heartbeat: { every: "5m", target: "whatsapp", to: "+1555" },
agents: {
defaults: {
heartbeat: { every: "5m", target: "whatsapp", to: "+1555" },
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
@@ -200,8 +212,10 @@ describe("runHeartbeatOnce", () => {
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
try {
const cfg: ClawdbotConfig = {
routing: { defaultAgentId: "work" },
agent: { heartbeat: { every: "5m" } },
agents: {
defaults: { heartbeat: { every: "5m" } },
list: [{ id: "work", default: true }],
},
whatsapp: { allowFrom: ["*"] },
session: { store: storeTemplate },
};
@@ -277,12 +291,14 @@ describe("runHeartbeatOnce", () => {
);
const cfg: ClawdbotConfig = {
agent: {
heartbeat: {
every: "5m",
target: "whatsapp",
to: "+1555",
ackMaxChars: 0,
agents: {
defaults: {
heartbeat: {
every: "5m",
target: "whatsapp",
to: "+1555",
ackMaxChars: 0,
},
},
},
whatsapp: { allowFrom: ["*"] },
@@ -335,8 +351,10 @@ describe("runHeartbeatOnce", () => {
);
const cfg: ClawdbotConfig = {
agent: {
heartbeat: { every: "5m", target: "whatsapp", to: "+1555" },
agents: {
defaults: {
heartbeat: { every: "5m", target: "whatsapp", to: "+1555" },
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
@@ -392,8 +410,10 @@ describe("runHeartbeatOnce", () => {
);
const cfg: ClawdbotConfig = {
agent: {
heartbeat: { every: "5m", target: "telegram", to: "123456" },
agents: {
defaults: {
heartbeat: { every: "5m", target: "telegram", to: "123456" },
},
},
telegram: { botToken: "test-bot-token-123" },
session: { store: storePath },
@@ -455,8 +475,10 @@ describe("runHeartbeatOnce", () => {
);
const cfg: ClawdbotConfig = {
agent: {
heartbeat: { every: "5m", target: "telegram", to: "123456" },
agents: {
defaults: {
heartbeat: { every: "5m", target: "telegram", to: "123456" },
},
},
telegram: {
accounts: {

View File

@@ -53,7 +53,9 @@ export function resolveHeartbeatIntervalMs(
overrideEvery?: string,
) {
const raw =
overrideEvery ?? cfg.agent?.heartbeat?.every ?? DEFAULT_HEARTBEAT_EVERY;
overrideEvery ??
cfg.agents?.defaults?.heartbeat?.every ??
DEFAULT_HEARTBEAT_EVERY;
if (!raw) return null;
const trimmed = String(raw).trim();
if (!trimmed) return null;
@@ -68,13 +70,14 @@ export function resolveHeartbeatIntervalMs(
}
export function resolveHeartbeatPrompt(cfg: ClawdbotConfig) {
return resolveHeartbeatPromptText(cfg.agent?.heartbeat?.prompt);
return resolveHeartbeatPromptText(cfg.agents?.defaults?.heartbeat?.prompt);
}
function resolveHeartbeatAckMaxChars(cfg: ClawdbotConfig) {
return Math.max(
0,
cfg.agent?.heartbeat?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
cfg.agents?.defaults?.heartbeat?.ackMaxChars ??
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
);
}

View File

@@ -114,7 +114,9 @@ describe("deliverOutboundPayloads", () => {
it("uses iMessage media maxBytes from agent fallback", async () => {
const sendIMessage = vi.fn().mockResolvedValue({ messageId: "i1" });
const cfg: ClawdbotConfig = { agent: { mediaMaxMb: 3 } };
const cfg: ClawdbotConfig = {
agents: { defaults: { mediaMaxMb: 3 } },
};
await deliverOutboundPayloads({
cfg,

View File

@@ -82,7 +82,9 @@ function resolveMediaMaxBytes(
: (cfg.imessage?.accounts?.[normalizedAccountId]?.mediaMaxMb ??
cfg.imessage?.mediaMaxMb);
if (providerLimit) return providerLimit * MB;
if (cfg.agent?.mediaMaxMb) return cfg.agent.mediaMaxMb * MB;
if (cfg.agents?.defaults?.mediaMaxMb) {
return cfg.agents.defaults.mediaMaxMb * MB;
}
return undefined;
}

View File

@@ -130,7 +130,7 @@ export function resolveHeartbeatDeliveryTarget(params: {
entry?: SessionEntry;
}): OutboundTarget {
const { cfg, entry } = params;
const rawTarget = cfg.agent?.heartbeat?.target;
const rawTarget = cfg.agents?.defaults?.heartbeat?.target;
const target: HeartbeatTarget =
rawTarget === "whatsapp" ||
rawTarget === "telegram" ||
@@ -148,9 +148,9 @@ export function resolveHeartbeatDeliveryTarget(params: {
}
const explicitTo =
typeof cfg.agent?.heartbeat?.to === "string" &&
cfg.agent.heartbeat.to.trim()
? cfg.agent.heartbeat.to.trim()
typeof cfg.agents?.defaults?.heartbeat?.to === "string" &&
cfg.agents.defaults.heartbeat.to.trim()
? cfg.agents.defaults.heartbeat.to.trim()
: undefined;
const lastProvider =

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import JSON5 from "json5";
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { ClawdbotConfig } from "../config/config.js";
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
import type { SessionEntry } from "../config/sessions.js";
@@ -12,7 +13,6 @@ import { createSubsystemLogger } from "../logging.js";
import {
buildAgentMainSessionKey,
DEFAULT_ACCOUNT_ID,
DEFAULT_AGENT_ID,
DEFAULT_MAIN_KEY,
normalizeAgentId,
} from "../routing/session-key.js";
@@ -192,9 +192,7 @@ export async function detectLegacyStateMigrations(params: {
const stateDir = resolveStateDir(env, homedir);
const oauthDir = resolveOAuthDir(env, stateDir);
const targetAgentId = normalizeAgentId(
params.cfg.routing?.defaultAgentId ?? DEFAULT_AGENT_ID,
);
const targetAgentId = normalizeAgentId(resolveDefaultAgentId(params.cfg));
const rawMainKey = params.cfg.session?.mainKey;
const targetMainKey =
typeof rawMainKey === "string" && rawMainKey.trim().length > 0