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

@@ -55,19 +55,17 @@ export function createAgentsListTool(opts?: {
.map((value) => normalizeAgentId(value)),
);
const configuredAgents = cfg.routing?.agents ?? {};
const configuredIds = Object.keys(configuredAgents).map((key) =>
normalizeAgentId(key),
const configuredAgents = Array.isArray(cfg.agents?.list)
? cfg.agents?.list
: [];
const configuredIds = configuredAgents.map((entry) =>
normalizeAgentId(entry.id),
);
const configuredNameMap = new Map<string, string>();
for (const [key, value] of Object.entries(configuredAgents)) {
if (!value || typeof value !== "object") continue;
const name =
typeof (value as { name?: unknown }).name === "string"
? ((value as { name?: string }).name?.trim() ?? "")
: "";
for (const entry of configuredAgents) {
const name = entry?.name?.trim() ?? "";
if (!name) continue;
configuredNameMap.set(normalizeAgentId(key), name);
configuredNameMap.set(normalizeAgentId(entry.id), name);
}
const allowed = new Set<string>();

View File

@@ -23,7 +23,7 @@ import type { AnyAgentTool } from "./common.js";
const DEFAULT_PROMPT = "Describe the image.";
function ensureImageToolConfigured(cfg?: ClawdbotConfig): boolean {
const imageModel = cfg?.agent?.imageModel as
const imageModel = cfg?.agents?.defaults?.imageModel as
| { primary?: string; fallbacks?: string[] }
| string
| undefined;
@@ -45,7 +45,7 @@ function pickMaxBytes(
) {
return Math.floor(maxBytesMb * 1024 * 1024);
}
const configured = cfg?.agent?.mediaMaxMb;
const configured = cfg?.agents?.defaults?.mediaMaxMb;
if (
typeof configured === "number" &&
Number.isFinite(configured) &&
@@ -141,7 +141,7 @@ export function createImageTool(options?: {
label: "Image",
name: "image",
description:
"Analyze an image with the configured image model (agent.imageModel). Provide a prompt and image path or URL.",
"Analyze an image with the configured image model (agents.defaults.imageModel). Provide a prompt and image path or URL.",
parameters: Type.Object({
prompt: Type.Optional(Type.String()),
image: Type.String(),

View File

@@ -25,7 +25,7 @@ const SessionsHistoryToolSchema = Type.Object({
function resolveSandboxSessionToolsVisibility(
cfg: ReturnType<typeof loadConfig>,
) {
return cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
}
async function isSpawnedSessionAllowed(params: {
@@ -97,7 +97,7 @@ export function createSessionsHistoryTool(opts?: {
}
}
const routingA2A = cfg.routing?.agentToAgent;
const routingA2A = cfg.tools?.agentToAgent;
const a2aEnabled = routingA2A?.enabled === true;
const allowPatterns = Array.isArray(routingA2A?.allow)
? routingA2A.allow
@@ -126,14 +126,13 @@ export function createSessionsHistoryTool(opts?: {
return jsonResult({
status: "forbidden",
error:
"Agent-to-agent history is disabled. Set routing.agentToAgent.enabled=true to allow cross-agent access.",
"Agent-to-agent history is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent access.",
});
}
if (!matchesAllow(requesterAgentId) || !matchesAllow(targetAgentId)) {
return jsonResult({
status: "forbidden",
error:
"Agent-to-agent history denied by routing.agentToAgent.allow.",
error: "Agent-to-agent history denied by tools.agentToAgent.allow.",
});
}
}

View File

@@ -13,7 +13,7 @@ vi.mock("../../config/config.js", async (importOriginal) => {
loadConfig: () =>
({
session: { scope: "per-sender", mainKey: "main" },
routing: { agentToAgent: { enabled: false } },
tools: { agentToAgent: { enabled: false } },
}) as never,
};
});
@@ -32,7 +32,7 @@ describe("sessions_list gating", () => {
});
});
it("filters out other agents when routing.agentToAgent.enabled is false", async () => {
it("filters out other agents when tools.agentToAgent.enabled is false", async () => {
const tool = createSessionsListTool({ agentSessionKey: "agent:main:main" });
const result = await tool.execute("call1", {});
expect(result.details).toMatchObject({

View File

@@ -53,7 +53,7 @@ const SessionsListToolSchema = Type.Object({
function resolveSandboxSessionToolsVisibility(
cfg: ReturnType<typeof loadConfig>,
) {
return cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
}
export function createSessionsListTool(opts?: {
@@ -126,7 +126,7 @@ export function createSessionsListTool(opts?: {
const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
const storePath = typeof list?.path === "string" ? list.path : undefined;
const routingA2A = cfg.routing?.agentToAgent;
const routingA2A = cfg.tools?.agentToAgent;
const a2aEnabled = routingA2A?.enabled === true;
const allowPatterns = Array.isArray(routingA2A?.allow)
? routingA2A.allow

View File

@@ -13,7 +13,7 @@ vi.mock("../../config/config.js", async (importOriginal) => {
loadConfig: () =>
({
session: { scope: "per-sender", mainKey: "main" },
routing: { agentToAgent: { enabled: false } },
tools: { agentToAgent: { enabled: false } },
}) as never,
};
});
@@ -25,7 +25,7 @@ describe("sessions_send gating", () => {
callGatewayMock.mockReset();
});
it("blocks cross-agent sends when routing.agentToAgent.enabled is false", async () => {
it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => {
const tool = createSessionsSendTool({
agentSessionKey: "agent:main:main",
agentProvider: "whatsapp",

View File

@@ -54,7 +54,7 @@ export function createSessionsSendTool(opts?: {
const cfg = loadConfig();
const { mainKey, alias } = resolveMainSessionAlias(cfg);
const visibility =
cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
const requesterInternalKey =
typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
? resolveInternalSessionKey({
@@ -126,7 +126,7 @@ export function createSessionsSendTool(opts?: {
mainKey,
});
const routingA2A = cfg.routing?.agentToAgent;
const routingA2A = cfg.tools?.agentToAgent;
const a2aEnabled = routingA2A?.enabled === true;
const allowPatterns = Array.isArray(routingA2A?.allow)
? routingA2A.allow
@@ -156,7 +156,7 @@ export function createSessionsSendTool(opts?: {
runId: crypto.randomUUID(),
status: "forbidden",
error:
"Agent-to-agent messaging is disabled. Set routing.agentToAgent.enabled=true to allow cross-agent sends.",
"Agent-to-agent messaging is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent sends.",
sessionKey: displayKey,
});
}
@@ -165,7 +165,7 @@ export function createSessionsSendTool(opts?: {
runId: crypto.randomUUID(),
status: "forbidden",
error:
"Agent-to-agent messaging denied by routing.agentToAgent.allow.",
"Agent-to-agent messaging denied by tools.agentToAgent.allow.",
sessionKey: displayKey,
});
}