import assert from "node:assert/strict"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { describe, it } from "node:test"; import type { AgentConfig } from "../../src/agents/agents.ts"; import { createChildSafeState } from "../../src/extension/fanout-child.ts"; import { createSubagentExecutor } from "../../src/runs/foreground/subagent-executor.ts"; import { describeSubagentRouting, emitSubagentRoutingTelemetry, readSubagentRoutingSettings, routeSubagentLaunch, type SubagentRoutingSettings, } from "../../src/runs/shared/sai-subagent-routing.ts"; import { makeMinimalCtx } from "../support/helpers.ts"; function agent(name: string, extra: Partial = {}): AgentConfig { return { name, description: `${name} agent`, systemPromptMode: "append", inheritProjectContext: true, inheritGlobalContext: true, inheritSkills: true, systemPrompt: "", source: "builtin", filePath: `${name}.md`, ...extra } as AgentConfig; } const agents = [ agent("delegate"), agent("reviewer", { tools: ["read", "grep"] }), agent("worker", { tools: ["read", "bash", "edit", "write"], model: "anthropic/opus", modelProvider: "anthropic" }), agent("secret", { disabled: true }), agent("external", { runner: { type: "external-cli" } as AgentConfig["runner"] }), ]; const settings: SubagentRoutingSettings = { connection: { provider: "tokenin", model: "cloudflare/clef", timeoutMs: 1000, minConfidence: 0.6 }, maxBytes: 16_384, contextChars: 4000, genericAgents: ["delegate"], tiers: { simple: "p/small", reasoning: "p/big" }, }; type Asked = { allowed: Record }; /** A fake Sai: answers each asked question from `answers`, records what was offered. */ function fakeAsk(answers: Record, asked: Asked[] = []) { return async (_ctx: unknown, _connection: unknown, request: Asked) => { asked.push({ allowed: request.allowed }); const choices = Object.fromEntries(Object.keys(request.allowed).filter((q) => answers[q]).map((q) => [q, answers[q]!])); return { choices, rejected: {}, elapsedMs: 5 }; }; } const ctx = {} as never; type Emitted = { channel: string; data: Record }; function recordingEvents(seen: Emitted[]) { return { emit: (channel: string, data: unknown) => seen.push({ channel, data: data as Record }) }; } describe("Sai subagent routing", () => { it("routes an agentless task, offering only enabled native agents inside the ceiling", async () => { const asked: Asked[] = []; const result = await routeSubagentLaunch( { task: "fix the bug", agents, ceiling: { allowedAgents: ["delegate", "worker", "secret"], sources: ["test"] } as never }, ctx, settings, fakeAsk({ agent: { choice: "worker", confidence: 0.9 } }, asked) as never, ); assert.equal(result.agent, "worker"); assert.deepEqual(asked[0]!.allowed.agent, ["delegate", "worker"]); }); it("offers keep for a generic agent and never re-routes an explicit specialist", async () => { const asked: Asked[] = []; const kept = await routeSubagentLaunch({ task: "t", agent: "delegate", agents }, ctx, settings, fakeAsk({ agent: { choice: "keep", confidence: 0.9 } }, asked) as never); assert.equal(kept.agent, "delegate"); assert.ok(asked[0]!.allowed.agent!.includes("keep")); assert.ok(!asked[0]!.allowed.agent!.includes("delegate")); const explicitAsked: Asked[] = []; const explicit = await routeSubagentLaunch({ task: "t", agent: "reviewer", model: "x/y", agents }, ctx, settings, fakeAsk({}, explicitAsked) as never); assert.equal(explicit.agent, "reviewer"); assert.ok(explicitAsked.every((ask) => !("agent" in ask.allowed))); }); it("abstention leaves the launch as requested", async () => { const result = await routeSubagentLaunch({ task: "t", agents }, ctx, settings, (async () => ({ choices: {}, rejected: { agent: "no-credential" }, failure: "no-credential", elapsedMs: 0 })) as never); assert.equal(result.agent, undefined); assert.equal(result.agents, agents); assert.equal(result.abstained, "no-credential"); }); it("only drops declared, unprotected tools on a confident unneeded, and applies the tier model", async () => { const asked: Asked[] = []; const result = await routeSubagentLaunch( { task: "summarize the README", agent: "worker", agents }, ctx, settings, fakeAsk({ tier: { choice: "simple", confidence: 0.8 }, tool_0: { choice: "unneeded", confidence: 0.9 }, tool_1: { choice: "unneeded" }, tool_2: { choice: "needed", confidence: 0.9 } }, asked) as never, ); // read is never offered: tool_0..2 are bash, edit, write. assert.deepEqual(Object.keys(asked[0]!.allowed).sort(), ["tier", "tool_0", "tool_1", "tool_2"]); assert.deepEqual(asked[0]!.allowed.tier, ["simple", "reasoning"]); assert.deepEqual(result.droppedTools, ["bash"]); const worker = result.agents.find((a) => a.name === "worker")!; assert.deepEqual(worker.excludeTools, ["bash"]); assert.equal(worker.model, "p/small"); assert.equal(worker.modelProvider, undefined); assert.deepEqual(worker.tools, ["read", "bash", "edit", "write"]); // The discovered list itself is untouched. assert.equal(agents.find((a) => a.name === "worker")!.model, "anthropic/opus"); }); it("skips the tier question when the parent chose a model", async () => { const asked: Asked[] = []; const result = await routeSubagentLaunch({ task: "t", agent: "worker", model: "x/y", agents }, ctx, settings, fakeAsk({ tier: { choice: "simple", confidence: 0.9 } }, asked) as never); assert.ok(!("tier" in asked[0]!.allowed)); assert.equal(result.tier, undefined); }); it("reads settings: saiAdvisory-only opt-in, tiers, generic agents, and the default Clef connection", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sai-subagent-")); try { const file = path.join(dir, "settings.json"); fs.writeFileSync(file, JSON.stringify({ saiAdvisory: { routes: {} } })); assert.equal(readSubagentRoutingSettings(file), undefined); // The removed key is not a fallback: old-key-only settings stay off. fs.writeFileSync(file, JSON.stringify({ jevAdvisory: { routes: { subagent: { enabled: true, tiers: { simple: "p/old" }, genericAgents: ["oldgen"] } } } })); assert.equal(readSubagentRoutingSettings(file), undefined); // Mixed input ignores the removed key's tiers and generic agents. fs.writeFileSync(file, JSON.stringify({ jevAdvisory: { routes: { subagent: { enabled: true, tiers: { complex: "p/old" }, genericAgents: ["oldgen"] } } }, saiAdvisory: { routes: { subagent: { enabled: true } } }, })); const mixed = readSubagentRoutingSettings(file)!; assert.deepEqual(mixed.tiers, {}); assert.deepEqual(mixed.genericAgents, ["delegate"]); // Modern opt-in fields parse from saiAdvisory only. fs.writeFileSync(file, JSON.stringify({ saiAdvisory: { routes: { subagent: { enabled: true, tiers: { simple: " p/s ", bogus: "x" }, genericAgents: ["delegate", "general"] } } } })); const read = readSubagentRoutingSettings(file)!; assert.deepEqual(read.tiers, { simple: "p/s" }); assert.deepEqual(read.genericAgents, ["delegate", "general"]); assert.equal(read.connection.provider, "tokenin"); assert.equal(read.connection.model, "cloudflare/clef"); assert.equal(read.connection.timeoutMs, 5000); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); it("emits content-free sai-routing telemetry with outcome sai only when it changed the launch", async () => { const routedAgents = [agent("delegate"), agent("worker", { description: "DESCRIPTION-CANARY" })]; const seen: Emitted[] = []; const changed = await routeSubagentLaunch( { task: "TASK-CANARY rewrite the frobnicator", agents: routedAgents }, ctx, settings, fakeAsk({ agent: { choice: "worker", confidence: 0.9 } }) as never, ); emitSubagentRoutingTelemetry(recordingEvents(seen), changed); assert.equal(seen.length, 1); assert.equal(seen[0]!.channel, "sai-routing"); assert.equal(seen[0]!.data.event, "decision"); assert.equal(seen[0]!.data.route, "subagent"); assert.equal(seen[0]!.data.outcome, "sai"); assert.equal(seen[0]!.data.agent, "worker"); const serialized = JSON.stringify(seen); assert.doesNotMatch(serialized, /TASK-CANARY|DESCRIPTION-CANARY|Which subagent/); const kept: Emitted[] = []; const unchanged = await routeSubagentLaunch({ task: "t", agent: "delegate", agents }, ctx, settings, fakeAsk({ agent: { choice: "keep", confidence: 0.9 } }) as never); emitSubagentRoutingTelemetry(recordingEvents(kept), unchanged); assert.equal(kept[0]!.data.outcome, "fallback"); const abstained: Emitted[] = []; const failed = await routeSubagentLaunch({ task: "t", agents }, ctx, settings, (async () => ({ choices: {}, rejected: { agent: "no-credential" }, failure: "no-credential", elapsedMs: 0 })) as never); emitSubagentRoutingTelemetry(recordingEvents(abstained), failed); assert.equal(abstained[0]!.data.outcome, "fallback"); assert.equal(abstained[0]!.data.reason, "no-credential"); }); it("describes what Sai set up and stays silent when nothing changed", () => { assert.equal( describeSubagentRouting({ agents: [], droppedTools: ["bash"], elapsedMs: 5, questions: 3, routedAgent: "reviewer", tier: "simple" }, { simple: "p/small" }), "Sai set up this subagent: agent reviewer; simple tier (p/small); without bash.", ); assert.equal(describeSubagentRouting({ agents: [], droppedTools: [], elapsedMs: 5, questions: 3 }, {}), undefined); }); it("names Sai in the missing-agent guidance when routing is off", async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sai-subagent-")); const previous = process.env.SELESAI_CODING_AGENT_DIR; process.env.SELESAI_CODING_AGENT_DIR = dir; try { const executor = createSubagentExecutor({ pi: { events: { emit() {}, on() { return () => {}; } }, getSessionName() { return "parent"; } } as never, state: createChildSafeState(), config: { maxSubagentDepth: 2, control: {}, intercomBridge: {} } as never, asyncByDefault: false, tempArtifactsDir: os.tmpdir(), getSubagentSessionRoot: () => os.tmpdir(), expandTilde: (value) => value, discoverAgents: () => ({ agents: [] as never[] }), }); const result = await executor.executePublic("agentless", { task: "work" }, new AbortController().signal, undefined, makeMinimalCtx(os.tmpdir()) as never); const text = result.content.map((part) => part.text ?? "").join("\n"); assert.equal(result.isError, true); assert.match(text, /Sai subagent routing \(saiAdvisory\.routes\.subagent\.enabled\) is off/); assert.doesNotMatch(text, /Jev|jevAdvisory/); } finally { if (previous === undefined) delete process.env.SELESAI_CODING_AGENT_DIR; else process.env.SELESAI_CODING_AGENT_DIR = previous; fs.rmSync(dir, { recursive: true, force: true }); } }); });