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

@@ -85,9 +85,11 @@ describe("block streaming", () => {
onBlockReply,
},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: path.join(home, "sessions.json") },
@@ -140,9 +142,11 @@ describe("block streaming", () => {
onBlockReply,
},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
telegram: { allowFrom: ["*"] },
session: { store: path.join(home, "sessions.json") },
@@ -185,9 +189,11 @@ describe("block streaming", () => {
onBlockReply,
},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: path.join(home, "sessions.json") },
@@ -239,9 +245,11 @@ describe("block streaming", () => {
blockReplyTimeoutMs: 10,
},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
telegram: { allowFrom: ["*"] },
session: { store: path.join(home, "sessions.json") },

View File

@@ -78,11 +78,13 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": { alias: " help " },
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": { alias: " help " },
},
},
},
whatsapp: { allowFrom: ["*"] },
@@ -108,9 +110,11 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: path.join(home, "sessions.json") },
@@ -138,11 +142,13 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
routing: {
messages: {
queue: {
mode: "collect",
debounceMs: 1500,
@@ -174,10 +180,12 @@ describe("directive behavior", () => {
{ Body: "/think", From: "+1222", To: "+1222" },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
thinkingDefault: "high",
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
thinkingDefault: "high",
},
},
session: { store: path.join(home, "sessions.json") },
},
@@ -198,9 +206,11 @@ describe("directive behavior", () => {
{ Body: "/think", From: "+1222", To: "+1222" },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
session: { store: path.join(home, "sessions.json") },
},
@@ -232,9 +242,11 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: path.join(home, "sessions.json") },
@@ -270,9 +282,11 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: path.join(home, "sessions.json") },
@@ -303,9 +317,11 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["*"],
@@ -330,9 +346,11 @@ describe("directive behavior", () => {
{ Body: "/verbose on", From: "+1222", To: "+1222" },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
session: { store: path.join(home, "sessions.json") },
},
@@ -352,10 +370,12 @@ describe("directive behavior", () => {
{ Body: "/think", From: "+1222", To: "+1222" },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
thinkingDefault: "high",
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
thinkingDefault: "high",
},
},
session: { store: path.join(home, "sessions.json") },
},
@@ -376,9 +396,11 @@ describe("directive behavior", () => {
{ Body: "/think", From: "+1222", To: "+1222" },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
session: { store: path.join(home, "sessions.json") },
},
@@ -399,10 +421,12 @@ describe("directive behavior", () => {
{ Body: "/verbose", From: "+1222", To: "+1222" },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
verboseDefault: "on",
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
verboseDefault: "on",
},
},
session: { store: path.join(home, "sessions.json") },
},
@@ -423,9 +447,11 @@ describe("directive behavior", () => {
{ Body: "/reasoning", From: "+1222", To: "+1222" },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
session: { store: path.join(home, "sessions.json") },
},
@@ -452,10 +478,14 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
elevatedDefault: "on",
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
elevatedDefault: "on",
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1222"] },
},
@@ -486,13 +516,17 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
sandbox: { mode: "off" },
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1222"] },
},
sandbox: { mode: "off" },
},
whatsapp: { allowFrom: ["+1222"] },
session: { store: path.join(home, "sessions.json") },
@@ -520,9 +554,13 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1222"] },
},
@@ -552,9 +590,13 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1222"] },
},
@@ -585,9 +627,13 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1222"] },
},
@@ -613,9 +659,11 @@ describe("directive behavior", () => {
{ Body: "/queue interrupt", From: "+1222", To: "+1222" },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
@@ -644,9 +692,11 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
@@ -677,9 +727,11 @@ describe("directive behavior", () => {
{ Body: "/queue interrupt", From: "+1222", To: "+1222" },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
@@ -690,9 +742,11 @@ describe("directive behavior", () => {
{ Body: "/queue reset", From: "+1222", To: "+1222" },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: storePath },
@@ -749,9 +803,11 @@ describe("directive behavior", () => {
ctx,
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["*"],
@@ -810,9 +866,11 @@ describe("directive behavior", () => {
{ Body: "/verbose on", From: ctx.From, To: ctx.To },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["*"],
@@ -825,9 +883,11 @@ describe("directive behavior", () => {
ctx,
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["*"],
@@ -853,12 +913,14 @@ describe("directive behavior", () => {
{ Body: "/model", From: "+1222", To: "+1222" },
{},
{
agent: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
},
},
},
session: { store: storePath },
@@ -883,12 +945,14 @@ describe("directive behavior", () => {
{ Body: "/model status", From: "+1222", To: "+1222" },
{},
{
agent: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
},
},
},
session: { store: storePath },
@@ -913,12 +977,14 @@ describe("directive behavior", () => {
{ Body: "/model list", From: "+1222", To: "+1222" },
{},
{
agent: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
},
},
},
session: { store: storePath },
@@ -943,12 +1009,14 @@ describe("directive behavior", () => {
{ Body: "/model", From: "+1222", To: "+1222" },
{},
{
agent: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
},
},
},
session: { store: storePath },
@@ -972,11 +1040,13 @@ describe("directive behavior", () => {
{ Body: "/model list", From: "+1222", To: "+1222" },
{},
{
agent: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
},
},
},
session: { store: storePath },
@@ -999,12 +1069,14 @@ describe("directive behavior", () => {
{ Body: "/model openai/gpt-4.1-mini", From: "+1222", To: "+1222" },
{},
{
agent: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
},
},
},
session: { store: storePath },
@@ -1030,12 +1102,14 @@ describe("directive behavior", () => {
{ Body: "/model Opus", From: "+1222", To: "+1222" },
{},
{
agent: {
model: { primary: "openai/gpt-4.1-mini" },
workspace: path.join(home, "clawd"),
models: {
"openai/gpt-4.1-mini": {},
"anthropic/claude-opus-4-5": { alias: "Opus" },
agents: {
defaults: {
model: { primary: "openai/gpt-4.1-mini" },
workspace: path.join(home, "clawd"),
models: {
"openai/gpt-4.1-mini": {},
"anthropic/claude-opus-4-5": { alias: "Opus" },
},
},
},
session: { store: storePath },
@@ -1081,12 +1155,14 @@ describe("directive behavior", () => {
{ Body: "/model Opus@anthropic:work", From: "+1222", To: "+1222" },
{},
{
agent: {
model: { primary: "openai/gpt-4.1-mini" },
workspace: path.join(home, "clawd"),
models: {
"openai/gpt-4.1-mini": {},
"anthropic/claude-opus-4-5": { alias: "Opus" },
agents: {
defaults: {
model: { primary: "openai/gpt-4.1-mini" },
workspace: path.join(home, "clawd"),
models: {
"openai/gpt-4.1-mini": {},
"anthropic/claude-opus-4-5": { alias: "Opus" },
},
},
},
session: { store: storePath },
@@ -1112,12 +1188,14 @@ describe("directive behavior", () => {
{ Body: "/model Opus", From: "+1222", To: "+1222" },
{},
{
agent: {
model: { primary: "openai/gpt-4.1-mini" },
workspace: path.join(home, "clawd"),
models: {
"openai/gpt-4.1-mini": {},
"anthropic/claude-opus-4-5": { alias: "Opus" },
agents: {
defaults: {
model: { primary: "openai/gpt-4.1-mini" },
workspace: path.join(home, "clawd"),
models: {
"openai/gpt-4.1-mini": {},
"anthropic/claude-opus-4-5": { alias: "Opus" },
},
},
},
session: { store: storePath },
@@ -1151,12 +1229,14 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
},
},
},
whatsapp: {
@@ -1204,9 +1284,11 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["*"],
@@ -1242,9 +1324,13 @@ describe("directive behavior", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1004"] },
},

View File

@@ -57,9 +57,11 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
function makeCfg(home: string) {
return {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["*"],

View File

@@ -53,9 +53,11 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
function makeCfg(home: string) {
return {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: path.join(home, "sessions.json") },

View File

@@ -50,13 +50,15 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
function makeCfg(home: string, queue?: Record<string, unknown>) {
return {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
session: { store: path.join(home, "sessions.json") },
routing: queue ? { queue } : undefined,
messages: queue ? { queue } : undefined,
};
}

View File

@@ -25,13 +25,18 @@ const usageMocks = vi.hoisted(() => ({
vi.mock("../infra/provider-usage.js", () => usageMocks);
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
import {
abortEmbeddedPiRun,
compactEmbeddedPiSession,
runEmbeddedPiAgent,
} from "../agents/pi-embedded.js";
import { ensureSandboxWorkspaceForSession } from "../agents/sandbox.js";
import { loadSessionStore, resolveSessionKey } from "../config/sessions.js";
import {
loadSessionStore,
resolveAgentIdFromSessionKey,
resolveSessionKey,
} from "../config/sessions.js";
import { getReplyFromConfig } from "./reply.js";
import { HEARTBEAT_TOKEN } from "./tokens.js";
@@ -61,9 +66,11 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
function makeCfg(home: string) {
return {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["*"],
@@ -345,9 +352,11 @@ describe("trigger handling", () => {
it("allows owner to set send policy", async () => {
await withTempHome(async (home) => {
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["+1000"],
@@ -381,9 +390,13 @@ describe("trigger handling", () => {
it("allows approved sender to toggle elevated mode", async () => {
await withTempHome(async (home) => {
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1000"] },
},
@@ -420,9 +433,13 @@ describe("trigger handling", () => {
it("rejects elevated toggles when disabled", async () => {
await withTempHome(async (home) => {
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
tools: {
elevated: {
enabled: false,
allowFrom: { whatsapp: ["+1000"] },
@@ -467,9 +484,13 @@ describe("trigger handling", () => {
},
});
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1000"] },
},
@@ -510,9 +531,13 @@ describe("trigger handling", () => {
},
});
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1000"] },
},
@@ -545,9 +570,13 @@ describe("trigger handling", () => {
it("allows elevated directive in groups when mentioned", async () => {
await withTempHome(async (home) => {
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1000"] },
},
@@ -589,9 +618,13 @@ describe("trigger handling", () => {
it("allows elevated directive in direct chats without mentions", async () => {
await withTempHome(async (home) => {
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1000"] },
},
@@ -635,9 +668,13 @@ describe("trigger handling", () => {
},
});
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
tools: {
elevated: {
allowFrom: { whatsapp: ["+1000"] },
},
@@ -668,9 +705,11 @@ describe("trigger handling", () => {
it("falls back to discord dm allowFrom for elevated approval", async () => {
await withTempHome(async (home) => {
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
discord: {
dm: {
@@ -708,9 +747,13 @@ describe("trigger handling", () => {
it("treats explicit discord elevated allowlist as override", async () => {
await withTempHome(async (home) => {
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
tools: {
elevated: {
allowFrom: { discord: [] },
},
@@ -799,9 +842,12 @@ describe("trigger handling", () => {
});
const cfg = makeCfg(home);
cfg.agent = {
...cfg.agent,
heartbeat: { model: "anthropic/claude-haiku-4-5-20251001" },
cfg.agents = {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
heartbeat: { model: "anthropic/claude-haiku-4-5-20251001" },
},
};
await getReplyFromConfig(
@@ -941,15 +987,17 @@ describe("trigger handling", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["*"],
groups: { "*": { requireMention: false } },
},
routing: {
messages: {
groupChat: {},
},
session: { store: join(home, "sessions.json") },
@@ -985,9 +1033,11 @@ describe("trigger handling", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["*"],
@@ -1024,9 +1074,11 @@ describe("trigger handling", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["*"],
@@ -1056,9 +1108,11 @@ describe("trigger handling", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["+1999"],
@@ -1083,9 +1137,11 @@ describe("trigger handling", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["+1999"],
@@ -1124,9 +1180,11 @@ describe("trigger handling", () => {
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
},
},
whatsapp: {
allowFrom: ["*"],
@@ -1229,12 +1287,14 @@ describe("trigger handling", () => {
});
const cfg = {
agent: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
sandbox: {
mode: "non-main" as const,
workspaceRoot: join(home, "sandboxes"),
agents: {
defaults: {
model: "anthropic/claude-opus-4-5",
workspace: join(home, "clawd"),
sandbox: {
mode: "non-main" as const,
workspaceRoot: join(home, "sandboxes"),
},
},
},
whatsapp: {
@@ -1272,10 +1332,11 @@ describe("trigger handling", () => {
ctx,
cfg.session?.mainKey,
);
const agentId = resolveAgentIdFromSessionKey(sessionKey);
const sandbox = await ensureSandboxWorkspaceForSession({
config: cfg,
sessionKey,
workspaceDir: cfg.agent.workspace,
workspaceDir: resolveAgentWorkspaceDir(cfg, agentId),
});
expect(sandbox).not.toBeNull();
if (!sandbox) {

View File

@@ -212,7 +212,7 @@ export async function getReplyFromConfig(
): Promise<ReplyPayload | ReplyPayload[] | undefined> {
const cfg = configOverride ?? loadConfig();
const agentId = resolveAgentIdFromSessionKey(ctx.SessionKey);
const agentCfg = cfg.agent;
const agentCfg = cfg.agents?.defaults;
const sessionCfg = cfg.session;
const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({
cfg,
@@ -239,7 +239,7 @@ export async function getReplyFromConfig(
resolveAgentWorkspaceDir(cfg, agentId) ?? DEFAULT_AGENT_WORKSPACE_DIR;
const workspace = await ensureAgentWorkspace({
dir: workspaceDirRaw,
ensureBootstrapFiles: !cfg.agent?.skipBootstrap,
ensureBootstrapFiles: !agentCfg?.skipBootstrap,
});
const workspaceDir = workspace.dir;
const agentDir = resolveAgentDir(cfg, agentId);
@@ -257,7 +257,7 @@ export async function getReplyFromConfig(
opts?.onTypingController?.(typing);
let transcribedText: string | undefined;
if (cfg.routing?.transcribeAudio && isAudio(ctx.MediaType)) {
if (cfg.audio?.transcription && isAudio(ctx.MediaType)) {
const transcribed = await transcribeInboundAudio(cfg, ctx, defaultRuntime);
if (transcribed?.text) {
transcribedText = transcribed.text;
@@ -329,7 +329,7 @@ export async function getReplyFromConfig(
cmd.textAliases.map((a) => a.replace(/^\//, "").toLowerCase()),
),
);
const configuredAliases = Object.values(cfg.agent?.models ?? {})
const configuredAliases = Object.values(cfg.agents?.defaults?.models ?? {})
.map((entry) => entry.alias?.trim())
.filter((alias): alias is string => Boolean(alias))
.filter((alias) => !reservedCommands.has(alias.toLowerCase()));
@@ -391,7 +391,7 @@ export async function getReplyFromConfig(
sessionCtx.Provider?.trim().toLowerCase() ??
ctx.Provider?.trim().toLowerCase() ??
"";
const elevatedConfig = agentCfg?.elevated;
const elevatedConfig = cfg.tools?.elevated;
const discordElevatedFallback =
messageProviderKey === "discord" ? cfg.discord?.dm?.allowFrom : undefined;
const elevatedEnabled = elevatedConfig?.enabled !== false;

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]

View File

@@ -35,7 +35,9 @@ import type {
VerboseLevel,
} from "./thinking.js";
type AgentConfig = NonNullable<ClawdbotConfig["agent"]>;
type AgentConfig = Partial<
NonNullable<NonNullable<ClawdbotConfig["agents"]>["defaults"]>
>;
export const formatTokenCount = formatTokenCountShared;
@@ -188,7 +190,11 @@ export function buildStatusMessage(args: StatusArgs): string {
const now = args.now ?? Date.now();
const entry = args.sessionEntry;
const resolved = resolveConfiguredModelRef({
cfg: { agent: args.agent ?? {} },
cfg: {
agents: {
defaults: args.agent ?? {},
},
} as ClawdbotConfig,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});

View File

@@ -37,8 +37,8 @@ describe("transcribeInboundAudio", () => {
vi.stubGlobal("fetch", fetchMock);
const cfg = {
routing: {
transcribeAudio: {
audio: {
transcription: {
command: ["echo", "{{MediaPath}}"],
timeoutSeconds: 5,
},
@@ -64,7 +64,7 @@ describe("transcribeInboundAudio", () => {
it("returns undefined when no transcription command", async () => {
const { transcribeInboundAudio } = await import("./transcription.js");
const res = await transcribeInboundAudio(
{ routing: {} } as never,
{ audio: {} } as never,
{} as never,
runtime as never,
);

View File

@@ -18,7 +18,7 @@ export async function transcribeInboundAudio(
ctx: MsgContext,
runtime: RuntimeEnv,
): Promise<{ text: string } | undefined> {
const transcriber = cfg.routing?.transcribeAudio;
const transcriber = cfg.audio?.transcription;
if (!transcriber?.command?.length) return undefined;
const timeoutMs = Math.max((transcriber.timeoutSeconds ?? 45) * 1000, 1_000);