feat: subagent model defaults

This commit is contained in:
Peter Steinberger
2026-01-12 18:08:16 +00:00
parent 17ff25bd20
commit 7b93356fb7
7 changed files with 141 additions and 4 deletions

View File

@@ -687,6 +687,91 @@ describe("subagents", () => {
});
});
it("sessions_spawn applies default subagent model from defaults config", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
configOverride = {
session: { mainKey: "main", scope: "per-sender" },
agents: { defaults: { subagents: { model: "minimax/MiniMax-M2.1" } } },
};
const calls: Array<{ method?: string; params?: unknown }> = [];
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: unknown };
calls.push(request);
if (request.method === "sessions.patch") {
return { ok: true };
}
if (request.method === "agent") {
return { runId: "run-default-model", status: "accepted" };
}
return {};
});
const tool = createClawdbotTools({
agentSessionKey: "agent:main:main",
agentProvider: "discord",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
const result = await tool.execute("call-default-model", {
task: "do thing",
});
expect(result.details).toMatchObject({
status: "accepted",
modelApplied: true,
});
const patchCall = calls.find((call) => call.method === "sessions.patch");
expect(patchCall?.params).toMatchObject({
model: "minimax/MiniMax-M2.1",
});
});
it("sessions_spawn prefers per-agent subagent model over defaults", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
configOverride = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: { subagents: { model: "minimax/MiniMax-M2.1" } },
list: [{ id: "research", subagents: { model: "opencode/claude" } }],
},
};
const calls: Array<{ method?: string; params?: unknown }> = [];
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: unknown };
calls.push(request);
if (request.method === "sessions.patch") {
return { ok: true };
}
if (request.method === "agent") {
return { runId: "run-agent-model", status: "accepted" };
}
return {};
});
const tool = createClawdbotTools({
agentSessionKey: "agent:research:main",
agentProvider: "discord",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
const result = await tool.execute("call-agent-model", {
task: "do thing",
});
expect(result.details).toMatchObject({
status: "accepted",
modelApplied: true,
});
const patchCall = calls.find((call) => call.method === "sessions.patch");
expect(patchCall?.params).toMatchObject({
model: "opencode/claude",
});
});
it("sessions_spawn skips invalid model overrides and continues", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();

View File

@@ -35,6 +35,17 @@ const SessionsSpawnToolSchema = Type.Object({
),
});
function normalizeModelSelection(value: unknown): string | undefined {
if (typeof value === "string") {
const trimmed = value.trim();
return trimmed || undefined;
}
if (!value || typeof value !== "object") return undefined;
const primary = (value as { primary?: unknown }).primary;
if (typeof primary === "string" && primary.trim()) return primary.trim();
return undefined;
}
export function createSessionsSpawnTool(opts?: {
agentSessionKey?: string;
agentProvider?: GatewayMessageProvider;
@@ -51,7 +62,7 @@ export function createSessionsSpawnTool(opts?: {
const task = readStringParam(params, "task", { required: true });
const label = typeof params.label === "string" ? params.label.trim() : "";
const requestedAgentId = readStringParam(params, "agentId");
const model = readStringParam(params, "model");
const modelOverride = readStringParam(params, "model");
const cleanup =
params.cleanup === "keep" || params.cleanup === "delete"
? (params.cleanup as "keep" | "delete")
@@ -129,11 +140,16 @@ export function createSessionsSpawnTool(opts?: {
}
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
const shouldPatchSpawnedBy = opts?.sandboxed === true;
if (model) {
const targetAgentConfig = resolveAgentConfig(cfg, targetAgentId);
const resolvedModel =
normalizeModelSelection(modelOverride) ??
normalizeModelSelection(targetAgentConfig?.subagents?.model) ??
normalizeModelSelection(cfg.agents?.defaults?.subagents?.model);
if (resolvedModel) {
try {
await callGateway({
method: "sessions.patch",
params: { key: childSessionKey, model },
params: { key: childSessionKey, model: resolvedModel },
timeoutMs: 10_000,
});
modelApplied = true;
@@ -218,7 +234,7 @@ export function createSessionsSpawnTool(opts?: {
status: "accepted",
childSessionKey,
runId: childRunId,
modelApplied: model ? modelApplied : undefined,
modelApplied: resolvedModel ? modelApplied : undefined,
warning: modelWarning,
});
},