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

@@ -34,7 +34,7 @@ export function resolveBlockStreamingChunking(
} {
const providerKey = normalizeChunkProvider(provider);
const textLimit = resolveTextChunkLimit(cfg, providerKey);
const chunkCfg = cfg?.agent?.blockStreamingChunk;
const chunkCfg = cfg?.agents?.defaults?.blockStreamingChunk;
const maxRequested = Math.max(
1,
Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX),

View File

@@ -163,18 +163,19 @@ export async function buildStatusReply(params: {
? (normalizeGroupActivation(sessionEntry?.groupActivation) ??
defaultGroupActivation())
: undefined;
const agentDefaults = cfg.agents?.defaults ?? {};
const statusText = buildStatusMessage({
config: cfg,
agent: {
...cfg.agent,
...agentDefaults,
model: {
...cfg.agent?.model,
...agentDefaults.model,
primary: `${provider}/${model}`,
},
contextTokens,
thinkingDefault: cfg.agent?.thinkingDefault,
verboseDefault: cfg.agent?.verboseDefault,
elevatedDefault: cfg.agent?.elevatedDefault,
thinkingDefault: agentDefaults.thinkingDefault,
verboseDefault: agentDefaults.verboseDefault,
elevatedDefault: agentDefaults.elevatedDefault,
},
sessionEntry,
sessionKey,

View File

@@ -23,6 +23,7 @@ import {
resolveConfiguredModelRef,
resolveModelRefFromString,
} from "../../agents/model-selection.js";
import { resolveSandboxConfigForAgent } from "../../agents/sandbox.js";
import type { ClawdbotConfig } from "../../config/config.js";
import {
resolveAgentIdFromSessionKey,
@@ -363,16 +364,16 @@ export async function handleDirectiveOnly(params: {
currentElevatedLevel,
} = params;
const runtimeIsSandboxed = (() => {
const sandboxMode = params.cfg.agent?.sandbox?.mode ?? "off";
if (sandboxMode === "off") return false;
const sessionKey = params.sessionKey?.trim();
if (!sessionKey) return false;
const agentId = resolveAgentIdFromSessionKey(sessionKey);
const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, agentId);
if (sandboxCfg.mode === "off") return false;
const mainKey = resolveAgentMainSessionKey({
cfg: params.cfg,
agentId,
});
if (sandboxMode === "all") return true;
if (sandboxCfg.mode === "all") return true;
return sessionKey !== mainKey;
})();
const shouldHintDirectRuntime =
@@ -394,7 +395,9 @@ export async function handleDirectiveOnly(params: {
provider: string;
id: string;
}> = [];
for (const raw of Object.keys(params.cfg.agent?.models ?? {})) {
for (const raw of Object.keys(
params.cfg.agents?.defaults?.models ?? {},
)) {
const resolved = resolveModelRefFromString({
raw: String(raw),
defaultProvider,
@@ -851,7 +854,7 @@ export async function persistInlineDirectives(params: {
model: string;
initialModelLabel: string;
formatModelSwitchEvent: (label: string, alias?: string) => string;
agentCfg: ClawdbotConfig["agent"] | undefined;
agentCfg: NonNullable<ClawdbotConfig["agents"]>["defaults"] | undefined;
}): Promise<{ provider: string; model: string; contextTokens: number }> {
const {
directives,
@@ -1007,13 +1010,16 @@ export function resolveDefaultModel(params: {
agentModelOverride && agentModelOverride.length > 0
? {
...params.cfg,
agent: {
...params.cfg.agent,
model: {
...(typeof params.cfg.agent?.model === "object"
? params.cfg.agent.model
: undefined),
primary: agentModelOverride,
agents: {
...params.cfg.agents,
defaults: {
...params.cfg.agents?.defaults,
model: {
...(typeof params.cfg.agents?.defaults?.model === "object"
? params.cfg.agents.defaults.model
: undefined),
primary: agentModelOverride,
},
},
},
}

View File

@@ -9,7 +9,7 @@ import {
describe("mention helpers", () => {
it("builds regexes and skips invalid patterns", () => {
const regexes = buildMentionRegexes({
routing: {
messages: {
groupChat: { mentionPatterns: ["\\bclawd\\b", "(invalid"] },
},
});
@@ -23,7 +23,7 @@ describe("mention helpers", () => {
it("matches patterns case-insensitively", () => {
const regexes = buildMentionRegexes({
routing: { groupChat: { mentionPatterns: ["\\bclawd\\b"] } },
messages: { groupChat: { mentionPatterns: ["\\bclawd\\b"] } },
});
expect(matchesMentionPatterns("CLAWD: hi", regexes)).toBe(true);
});
@@ -31,11 +31,16 @@ describe("mention helpers", () => {
it("uses per-agent mention patterns when configured", () => {
const regexes = buildMentionRegexes(
{
routing: {
messages: {
groupChat: { mentionPatterns: ["\\bglobal\\b"] },
agents: {
work: { mentionPatterns: ["\\bworkbot\\b"] },
},
},
agents: {
list: [
{
id: "work",
groupChat: { mentionPatterns: ["\\bworkbot\\b"] },
},
],
},
},
"work",

View File

@@ -1,23 +1,62 @@
import { resolveAgentConfig } from "../../agents/agent-scope.js";
import type { ClawdbotConfig } from "../../config/config.js";
import type { MsgContext } from "../templating.js";
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) {
const patterns: string[] = [];
const name = identity?.name?.trim();
if (name) {
const parts = name.split(/\s+/).filter(Boolean).map(escapeRegExp);
const re = parts.length ? parts.join(String.raw`\s+`) : escapeRegExp(name);
patterns.push(String.raw`\b@?${re}\b`);
}
const emoji = identity?.emoji?.trim();
if (emoji) {
patterns.push(escapeRegExp(emoji));
}
return patterns;
}
const BACKSPACE_CHAR = "\u0008";
function normalizeMentionPattern(pattern: string): string {
if (!pattern.includes(BACKSPACE_CHAR)) return pattern;
return pattern.split(BACKSPACE_CHAR).join("\\b");
}
function normalizeMentionPatterns(patterns: string[]): string[] {
return patterns.map(normalizeMentionPattern);
}
function resolveMentionPatterns(
cfg: ClawdbotConfig | undefined,
agentId?: string,
): string[] {
if (!cfg) return [];
const agentConfig = agentId ? cfg.routing?.agents?.[agentId] : undefined;
if (agentConfig && Object.hasOwn(agentConfig, "mentionPatterns")) {
return agentConfig.mentionPatterns ?? [];
const agentConfig = agentId ? resolveAgentConfig(cfg, agentId) : undefined;
const agentGroupChat = agentConfig?.groupChat;
if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
return agentGroupChat.mentionPatterns ?? [];
}
return cfg.routing?.groupChat?.mentionPatterns ?? [];
const globalGroupChat = cfg.messages?.groupChat;
if (globalGroupChat && Object.hasOwn(globalGroupChat, "mentionPatterns")) {
return globalGroupChat.mentionPatterns ?? [];
}
const derived = deriveMentionPatterns(agentConfig?.identity);
return derived.length > 0 ? derived : [];
}
export function buildMentionRegexes(
cfg: ClawdbotConfig | undefined,
agentId?: string,
): RegExp[] {
const patterns = resolveMentionPatterns(cfg, agentId);
const patterns = normalizeMentionPatterns(
resolveMentionPatterns(cfg, agentId),
);
return patterns
.map((pattern) => {
try {
@@ -66,7 +105,9 @@ export function stripMentions(
agentId?: string,
): string {
let result = text;
const patterns = resolveMentionPatterns(cfg, agentId);
const patterns = normalizeMentionPatterns(
resolveMentionPatterns(cfg, agentId),
);
for (const p of patterns) {
try {
const re = new RegExp(p, "gi");

View File

@@ -33,7 +33,9 @@ type ModelSelectionState = {
export async function createModelSelectionState(params: {
cfg: ClawdbotConfig;
agentCfg: ClawdbotConfig["agent"] | undefined;
agentCfg:
| NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
| undefined;
sessionEntry?: SessionEntry;
sessionStore?: Record<string, SessionEntry>;
sessionKey?: string;
@@ -201,7 +203,9 @@ export function resolveModelDirectiveSelection(params: {
}
export function resolveContextTokens(params: {
agentCfg: ClawdbotConfig["agent"] | undefined;
agentCfg:
| NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
| undefined;
model: string;
}): number {
return (

View File

@@ -553,7 +553,7 @@ export function resolveQueueSettings(params: {
inlineOptions?: Partial<QueueSettings>;
}): QueueSettings {
const providerKey = params.provider?.trim().toLowerCase();
const queueCfg = params.cfg.routing?.queue;
const queueCfg = params.cfg.messages?.queue;
const providerModeRaw =
providerKey && queueCfg?.byProvider
? (queueCfg.byProvider as Record<string, string | undefined>)[providerKey]