feat: wire multi-agent config and routing
Co-authored-by: Mark Pors <1078320+pors@users.noreply.github.com>
This commit is contained in:
@@ -14,10 +14,10 @@ describe("diffConfigPaths", () => {
|
||||
});
|
||||
|
||||
it("captures array changes", () => {
|
||||
const prev = { routing: { groupChat: { mentionPatterns: ["a"] } } };
|
||||
const next = { routing: { groupChat: { mentionPatterns: ["b"] } } };
|
||||
const prev = { messages: { groupChat: { mentionPatterns: ["a"] } } };
|
||||
const next = { messages: { groupChat: { mentionPatterns: ["b"] } } };
|
||||
const paths = diffConfigPaths(prev, next);
|
||||
expect(paths).toContain("routing.groupChat.mentionPatterns");
|
||||
expect(paths).toContain("messages.groupChat.mentionPatterns");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -64,7 +64,11 @@ const RELOAD_RULES: ReloadRule[] = [
|
||||
{ prefix: "gateway.reload", kind: "none" },
|
||||
{ prefix: "hooks.gmail", kind: "hot", actions: ["restart-gmail-watcher"] },
|
||||
{ prefix: "hooks", kind: "hot", actions: ["reload-hooks"] },
|
||||
{ prefix: "agent.heartbeat", kind: "hot", actions: ["restart-heartbeat"] },
|
||||
{
|
||||
prefix: "agents.defaults.heartbeat",
|
||||
kind: "hot",
|
||||
actions: ["restart-heartbeat"],
|
||||
},
|
||||
{ prefix: "cron", kind: "hot", actions: ["restart-cron"] },
|
||||
{
|
||||
prefix: "browser",
|
||||
@@ -78,12 +82,13 @@ const RELOAD_RULES: ReloadRule[] = [
|
||||
{ prefix: "signal", kind: "hot", actions: ["restart-provider:signal"] },
|
||||
{ prefix: "imessage", kind: "hot", actions: ["restart-provider:imessage"] },
|
||||
{ prefix: "msteams", kind: "hot", actions: ["restart-provider:msteams"] },
|
||||
{ prefix: "identity", kind: "none" },
|
||||
{ prefix: "agents", kind: "none" },
|
||||
{ prefix: "tools", kind: "none" },
|
||||
{ prefix: "bindings", kind: "none" },
|
||||
{ prefix: "audio", kind: "none" },
|
||||
{ prefix: "wizard", kind: "none" },
|
||||
{ prefix: "logging", kind: "none" },
|
||||
{ prefix: "models", kind: "none" },
|
||||
{ prefix: "agent", kind: "none" },
|
||||
{ prefix: "routing", kind: "none" },
|
||||
{ prefix: "messages", kind: "none" },
|
||||
{ prefix: "session", kind: "none" },
|
||||
{ prefix: "whatsapp", kind: "none" },
|
||||
|
||||
@@ -857,7 +857,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
).items;
|
||||
let thinkingLevel = entry?.thinkingLevel;
|
||||
if (!thinkingLevel) {
|
||||
const configured = cfg.agent?.thinkingDefault;
|
||||
const configured = cfg.agents?.defaults?.thinkingDefault;
|
||||
if (configured) {
|
||||
thinkingLevel = configured;
|
||||
} else {
|
||||
|
||||
@@ -61,7 +61,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
).items;
|
||||
let thinkingLevel = entry?.thinkingLevel;
|
||||
if (!thinkingLevel) {
|
||||
const configured = cfg.agent?.thinkingDefault;
|
||||
const configured = cfg.agents?.defaults?.thinkingDefault;
|
||||
if (configured) {
|
||||
thinkingLevel = configured;
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { installSkill } from "../../agents/skills-install.js";
|
||||
import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
|
||||
import { DEFAULT_AGENT_WORKSPACE_DIR } from "../../agents/workspace.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
@@ -28,8 +30,10 @@ export const skillsHandlers: GatewayRequestHandlers = {
|
||||
return;
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
const workspaceDirRaw = cfg.agent?.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
const workspaceDir = resolveUserPath(workspaceDirRaw);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(
|
||||
cfg,
|
||||
resolveDefaultAgentId(cfg),
|
||||
);
|
||||
const report = buildWorkspaceSkillStatus(workspaceDir, {
|
||||
config: cfg,
|
||||
});
|
||||
@@ -53,7 +57,10 @@ export const skillsHandlers: GatewayRequestHandlers = {
|
||||
timeoutMs?: number;
|
||||
};
|
||||
const cfg = loadConfig();
|
||||
const workspaceDirRaw = cfg.agent?.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
|
||||
const workspaceDirRaw = resolveAgentWorkspaceDir(
|
||||
cfg,
|
||||
resolveDefaultAgentId(cfg),
|
||||
);
|
||||
const result = await installSkill({
|
||||
workspaceDir: workspaceDirRaw,
|
||||
skillName: p.name,
|
||||
|
||||
@@ -11,12 +11,11 @@ installGatewayTestHooks();
|
||||
|
||||
describe("gateway server agents", () => {
|
||||
test("lists configured agents via agents.list RPC", async () => {
|
||||
testState.routingConfig = {
|
||||
defaultAgentId: "work",
|
||||
agents: {
|
||||
work: { name: "Work" },
|
||||
home: { name: "Home" },
|
||||
},
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{ id: "work", name: "Work", default: true },
|
||||
{ id: "home", name: "Home" },
|
||||
],
|
||||
};
|
||||
|
||||
const { ws } = await startServerWithClient();
|
||||
|
||||
@@ -210,7 +210,7 @@ describe("gateway hot reload", () => {
|
||||
gmail: { account: "me@example.com" },
|
||||
},
|
||||
cron: { enabled: true, store: "/tmp/cron.json" },
|
||||
agent: { heartbeat: { every: "1m" }, maxConcurrent: 2 },
|
||||
agents: { defaults: { heartbeat: { every: "1m" }, maxConcurrent: 2 } },
|
||||
browser: { enabled: true, controlUrl: "http://127.0.0.1:18791" },
|
||||
web: { enabled: true },
|
||||
telegram: { botToken: "token" },
|
||||
@@ -224,7 +224,7 @@ describe("gateway hot reload", () => {
|
||||
changedPaths: [
|
||||
"hooks.gmail.account",
|
||||
"cron.enabled",
|
||||
"agent.heartbeat.every",
|
||||
"agents.defaults.heartbeat.every",
|
||||
"browser.enabled",
|
||||
"web.enabled",
|
||||
"telegram.botToken",
|
||||
|
||||
@@ -328,12 +328,8 @@ describe("gateway server sessions", () => {
|
||||
testState.sessionConfig = {
|
||||
store: path.join(dir, "{agentId}", "sessions.json"),
|
||||
};
|
||||
testState.routingConfig = {
|
||||
defaultAgentId: "home",
|
||||
agents: {
|
||||
home: {},
|
||||
work: {},
|
||||
},
|
||||
testState.agentsConfig = {
|
||||
list: [{ id: "home", default: true }, { id: "work" }],
|
||||
};
|
||||
const homeDir = path.join(dir, "home");
|
||||
const workDir = path.join(dir, "work");
|
||||
|
||||
@@ -687,10 +687,13 @@ export async function startGatewayServer(
|
||||
{ controller: AbortController; sessionId: string; sessionKey: string }
|
||||
>();
|
||||
setCommandLaneConcurrency("cron", cfgAtStart.cron?.maxConcurrentRuns ?? 1);
|
||||
setCommandLaneConcurrency("main", cfgAtStart.agent?.maxConcurrent ?? 1);
|
||||
setCommandLaneConcurrency(
|
||||
"main",
|
||||
cfgAtStart.agents?.defaults?.maxConcurrent ?? 1,
|
||||
);
|
||||
setCommandLaneConcurrency(
|
||||
"subagent",
|
||||
cfgAtStart.agent?.subagents?.maxConcurrent ?? 1,
|
||||
cfgAtStart.agents?.defaults?.subagents?.maxConcurrent ?? 1,
|
||||
);
|
||||
|
||||
const cronLogger = getChildLogger({
|
||||
@@ -1975,10 +1978,13 @@ export async function startGatewayServer(
|
||||
}
|
||||
|
||||
setCommandLaneConcurrency("cron", nextConfig.cron?.maxConcurrentRuns ?? 1);
|
||||
setCommandLaneConcurrency("main", nextConfig.agent?.maxConcurrent ?? 1);
|
||||
setCommandLaneConcurrency(
|
||||
"main",
|
||||
nextConfig.agents?.defaults?.maxConcurrent ?? 1,
|
||||
);
|
||||
setCommandLaneConcurrency(
|
||||
"subagent",
|
||||
nextConfig.agent?.subagents?.maxConcurrent ?? 1,
|
||||
nextConfig.agents?.defaults?.subagents?.maxConcurrent ?? 1,
|
||||
);
|
||||
|
||||
if (plan.hotReasons.length > 0) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { lookupContextTokens } from "../agents/context.js";
|
||||
import {
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
@@ -231,11 +232,11 @@ function listExistingAgentIdsFromDisk(): string[] {
|
||||
|
||||
function listConfiguredAgentIds(cfg: ClawdbotConfig): string[] {
|
||||
const ids = new Set<string>();
|
||||
const defaultId = normalizeAgentId(cfg.routing?.defaultAgentId);
|
||||
const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg));
|
||||
ids.add(defaultId);
|
||||
const agents = cfg.routing?.agents;
|
||||
if (agents && typeof agents === "object") {
|
||||
for (const id of Object.keys(agents)) ids.add(normalizeAgentId(id));
|
||||
const agents = cfg.agents?.list ?? [];
|
||||
for (const entry of agents) {
|
||||
if (entry?.id) ids.add(normalizeAgentId(entry.id));
|
||||
}
|
||||
for (const id of listExistingAgentIdsFromDisk()) ids.add(id);
|
||||
const sorted = Array.from(ids).filter(Boolean);
|
||||
@@ -252,22 +253,19 @@ export function listAgentsForGateway(cfg: ClawdbotConfig): {
|
||||
scope: SessionScope;
|
||||
agents: GatewayAgentRow[];
|
||||
} {
|
||||
const defaultId = normalizeAgentId(cfg.routing?.defaultAgentId);
|
||||
const defaultId = normalizeAgentId(resolveDefaultAgentId(cfg));
|
||||
const mainKey =
|
||||
(cfg.session?.mainKey ?? DEFAULT_MAIN_KEY).trim() || DEFAULT_MAIN_KEY;
|
||||
const scope = cfg.session?.scope ?? "per-sender";
|
||||
const configured = cfg.routing?.agents;
|
||||
const configuredById = new Map<string, { name?: string }>();
|
||||
if (configured && typeof configured === "object") {
|
||||
for (const [key, value] of Object.entries(configured)) {
|
||||
if (!value || typeof value !== "object") continue;
|
||||
configuredById.set(normalizeAgentId(key), {
|
||||
name:
|
||||
typeof value.name === "string" && value.name.trim()
|
||||
? value.name.trim()
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
for (const entry of cfg.agents?.list ?? []) {
|
||||
if (!entry?.id) continue;
|
||||
configuredById.set(normalizeAgentId(entry.id), {
|
||||
name:
|
||||
typeof entry.name === "string" && entry.name.trim()
|
||||
? entry.name.trim()
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
const agents = listConfiguredAgentIds(cfg).map((id) => {
|
||||
const meta = configuredById.get(id);
|
||||
@@ -350,7 +348,7 @@ export function loadCombinedSessionStoreForGateway(cfg: ClawdbotConfig): {
|
||||
const storeConfig = cfg.session?.store;
|
||||
if (storeConfig && !isStorePathTemplate(storeConfig)) {
|
||||
const storePath = resolveStorePath(storeConfig);
|
||||
const defaultAgentId = normalizeAgentId(cfg.routing?.defaultAgentId);
|
||||
const defaultAgentId = normalizeAgentId(resolveDefaultAgentId(cfg));
|
||||
const store = loadSessionStore(storePath);
|
||||
const combined: Record<string, SessionEntry> = {};
|
||||
for (const [key, entry] of Object.entries(store)) {
|
||||
@@ -396,7 +394,7 @@ export function getSessionDefaults(
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
const contextTokens =
|
||||
cfg.agent?.contextTokens ??
|
||||
cfg.agents?.defaults?.contextTokens ??
|
||||
lookupContextTokens(resolved.model) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
return {
|
||||
|
||||
@@ -85,7 +85,8 @@ export const agentCommand = hoisted.agentCommand;
|
||||
|
||||
export const testState = {
|
||||
agentConfig: undefined as Record<string, unknown> | undefined,
|
||||
routingConfig: undefined as Record<string, unknown> | undefined,
|
||||
agentsConfig: undefined as Record<string, unknown> | undefined,
|
||||
bindingsConfig: undefined as Array<Record<string, unknown>> | undefined,
|
||||
sessionStorePath: undefined as string | undefined,
|
||||
sessionConfig: undefined as Record<string, unknown> | undefined,
|
||||
allowFrom: undefined as string[] | undefined,
|
||||
@@ -242,12 +243,18 @@ vi.mock("../config/config.js", async () => {
|
||||
changes: testState.migrationChanges,
|
||||
}),
|
||||
loadConfig: () => ({
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(os.tmpdir(), "clawd-gateway-test"),
|
||||
...testState.agentConfig,
|
||||
},
|
||||
routing: testState.routingConfig,
|
||||
agents: (() => {
|
||||
const defaults = {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(os.tmpdir(), "clawd-gateway-test"),
|
||||
...testState.agentConfig,
|
||||
};
|
||||
if (testState.agentsConfig) {
|
||||
return { ...testState.agentsConfig, defaults };
|
||||
}
|
||||
return { defaults };
|
||||
})(),
|
||||
bindings: testState.bindingsConfig,
|
||||
whatsapp: {
|
||||
allowFrom: testState.allowFrom,
|
||||
},
|
||||
@@ -356,7 +363,8 @@ export function installGatewayTestHooks() {
|
||||
testState.sessionConfig = undefined;
|
||||
testState.sessionStorePath = undefined;
|
||||
testState.agentConfig = undefined;
|
||||
testState.routingConfig = undefined;
|
||||
testState.agentsConfig = undefined;
|
||||
testState.bindingsConfig = undefined;
|
||||
testState.allowFrom = undefined;
|
||||
testIsNixMode.value = false;
|
||||
cronIsolatedRun.mockClear();
|
||||
|
||||
Reference in New Issue
Block a user