Skip to content

Commit ba54c5a

Browse files
committed
refactor(subagents): share effective model selection resolver
1 parent 122bc59 commit ba54c5a

File tree

4 files changed

+73
-21
lines changed

4 files changed

+73
-21
lines changed

src/agents/model-selection.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { OpenClawConfig } from "../config/config.js";
2-
import { resolveAgentModelPrimary } from "./agent-scope.js";
2+
import { resolveAgentConfig, resolveAgentModelPrimary } from "./agent-scope.js";
33
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
44
import type { ModelCatalogEntry } from "./model-catalog.js";
55
import { normalizeGoogleModelId } from "./models-config.providers.js";
@@ -316,6 +316,38 @@ export function resolveDefaultModelForAgent(params: {
316316
});
317317
}
318318

319+
export function resolveSubagentConfiguredModelSelection(params: {
320+
cfg: OpenClawConfig;
321+
agentId: string;
322+
}): string | undefined {
323+
const agentConfig = resolveAgentConfig(params.cfg, params.agentId);
324+
return (
325+
normalizeModelSelection(agentConfig?.subagents?.model) ??
326+
normalizeModelSelection(params.cfg.agents?.defaults?.subagents?.model) ??
327+
normalizeModelSelection(agentConfig?.model)
328+
);
329+
}
330+
331+
export function resolveSubagentSpawnModelSelection(params: {
332+
cfg: OpenClawConfig;
333+
agentId: string;
334+
modelOverride?: unknown;
335+
}): string {
336+
const runtimeDefault = resolveDefaultModelForAgent({
337+
cfg: params.cfg,
338+
agentId: params.agentId,
339+
});
340+
return (
341+
normalizeModelSelection(params.modelOverride) ??
342+
resolveSubagentConfiguredModelSelection({
343+
cfg: params.cfg,
344+
agentId: params.agentId,
345+
}) ??
346+
normalizeModelSelection(params.cfg.agents?.defaults?.model?.primary) ??
347+
`${runtimeDefault.provider}/${runtimeDefault.model}`
348+
);
349+
}
350+
319351
export function buildAllowedModelSet(params: {
320352
cfg: OpenClawConfig;
321353
catalog: ModelCatalogEntry[];

src/agents/openclaw-tools.subagents.sessions-spawn.model.e2e.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,40 @@ describe("openclaw-tools: subagents (sessions_spawn model + thinking)", () => {
255255
});
256256
});
257257

258+
it("sessions_spawn prefers target agent primary model over global default", async () => {
259+
resetSubagentRegistryForTests();
260+
callGatewayMock.mockReset();
261+
setSessionsSpawnConfigOverride({
262+
session: { mainKey: "main", scope: "per-sender" },
263+
agents: {
264+
defaults: { model: { primary: "minimax/MiniMax-M2.1" } },
265+
list: [{ id: "research", model: { primary: "opencode/claude" } }],
266+
},
267+
});
268+
const calls: GatewayCall[] = [];
269+
mockPatchAndSingleAgentRun({ calls, runId: "run-agent-primary-model" });
270+
271+
const tool = await getSessionsSpawnTool({
272+
agentSessionKey: "agent:research:main",
273+
agentChannel: "discord",
274+
});
275+
276+
const result = await tool.execute("call-agent-primary-model", {
277+
task: "do thing",
278+
});
279+
expect(result.details).toMatchObject({
280+
status: "accepted",
281+
modelApplied: true,
282+
});
283+
284+
const patchCall = calls.find(
285+
(call) => call.method === "sessions.patch" && (call.params as { model?: string })?.model,
286+
);
287+
expect(patchCall?.params).toMatchObject({
288+
model: "opencode/claude",
289+
});
290+
});
291+
258292
it("sessions_spawn fails when model patch is rejected", async () => {
259293
resetSubagentRegistryForTests();
260294
callGatewayMock.mockReset();

src/agents/subagent-spawn.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.j
66
import { normalizeDeliveryContext } from "../utils/delivery-context.js";
77
import { resolveAgentConfig } from "./agent-scope.js";
88
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
9-
import { normalizeModelSelection, resolveDefaultModelForAgent } from "./model-selection.js";
9+
import { resolveSubagentSpawnModelSelection } from "./model-selection.js";
1010
import { buildSubagentSystemPrompt } from "./subagent-announce.js";
1111
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
1212
import { countActiveRunsForSession, registerSubagentRun } from "./subagent-registry.js";
@@ -149,16 +149,11 @@ export async function spawnSubagentDirect(
149149
const childDepth = callerDepth + 1;
150150
const spawnedByKey = requesterInternalKey;
151151
const targetAgentConfig = resolveAgentConfig(cfg, targetAgentId);
152-
const runtimeDefaultModel = resolveDefaultModelForAgent({
152+
const resolvedModel = resolveSubagentSpawnModelSelection({
153153
cfg,
154154
agentId: targetAgentId,
155+
modelOverride,
155156
});
156-
const resolvedModel =
157-
normalizeModelSelection(modelOverride) ??
158-
normalizeModelSelection(targetAgentConfig?.subagents?.model) ??
159-
normalizeModelSelection(cfg.agents?.defaults?.subagents?.model) ??
160-
normalizeModelSelection(cfg.agents?.defaults?.model?.primary) ??
161-
normalizeModelSelection(`${runtimeDefaultModel.provider}/${runtimeDefaultModel.model}`);
162157

163158
const resolvedThinkingDefaultRaw =
164159
readStringParam(targetAgentConfig?.subagents ?? {}, "thinking") ??

src/gateway/sessions-patch.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { randomUUID } from "node:crypto";
2-
import { resolveAgentConfig, resolveDefaultAgentId } from "../agents/agent-scope.js";
2+
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
33
import type { ModelCatalogEntry } from "../agents/model-catalog.js";
44
import {
5-
normalizeModelSelection,
65
resolveAllowedModelRef,
76
resolveDefaultModelForAgent,
7+
resolveSubagentConfiguredModelSelection,
88
} from "../agents/model-selection.js";
99
import { normalizeGroupActivation } from "../auto-reply/group-activation.js";
1010
import {
@@ -62,15 +62,6 @@ function normalizeExecAsk(raw: string): "off" | "on-miss" | "always" | undefined
6262
return undefined;
6363
}
6464

65-
function resolveSubagentModelHint(cfg: OpenClawConfig, agentId: string): string | undefined {
66-
const agentConfig = resolveAgentConfig(cfg, agentId);
67-
return (
68-
normalizeModelSelection(agentConfig?.subagents?.model) ??
69-
normalizeModelSelection(cfg.agents?.defaults?.subagents?.model) ??
70-
normalizeModelSelection(agentConfig?.model)
71-
);
72-
}
73-
7465
export async function applySessionsPatchToStore(params: {
7566
cfg: OpenClawConfig;
7667
store: Record<string, SessionEntry>;
@@ -84,7 +75,7 @@ export async function applySessionsPatchToStore(params: {
8475
const sessionAgentId = normalizeAgentId(parsedAgent?.agentId ?? resolveDefaultAgentId(cfg));
8576
const resolvedDefault = resolveDefaultModelForAgent({ cfg, agentId: sessionAgentId });
8677
const subagentModelHint = isSubagentSessionKey(storeKey)
87-
? resolveSubagentModelHint(cfg, sessionAgentId)
78+
? resolveSubagentConfiguredModelSelection({ cfg, agentId: sessionAgentId })
8879
: undefined;
8980

9081
const existing = store[storeKey];

0 commit comments

Comments
 (0)