Skip to content

Commit e632eab

Browse files
committed
refactor(agents): share cron subagent model selection
1 parent 0dcc93f commit e632eab

6 files changed

Lines changed: 170 additions & 16 deletions

File tree

src/agents/agent-scope.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
resolveAgentModelFallbacksOverride,
1717
resolveAgentModelPrimary,
1818
resolveRunModelFallbacksOverride,
19+
resolveSubagentModelConfigSelection,
1920
resolveSubagentModelFallbacksOverride,
2021
resolveAgentWorkspaceDir,
2122
resolveAgentIdByWorkspacePath,
@@ -547,6 +548,16 @@ describe("resolveAgentConfig", () => {
547548
fallbacks: ["google/gemini-3-pro"],
548549
},
549550
},
551+
{
552+
id: "metadata-only-subagent",
553+
model: {
554+
primary: "anthropic/claude-sonnet-4-6",
555+
fallbacks: ["google/gemini-3-pro"],
556+
},
557+
subagents: {
558+
model: { timeoutMs: 1_000 },
559+
},
560+
},
550561
{
551562
id: "default-subagent",
552563
},
@@ -567,13 +578,67 @@ describe("resolveAgentConfig", () => {
567578
expect(resolveSubagentModelFallbacksOverride(cfg, "agent-model")).toEqual([
568579
"google/gemini-3-pro",
569580
]);
581+
expect(resolveSubagentModelFallbacksOverride(cfg, "metadata-only-subagent")).toEqual([
582+
"google/gemini-3-pro",
583+
]);
570584
expect(resolveSubagentModelFallbacksOverride(cfg, "default-subagent")).toEqual([
571585
"openai-codex/gpt-5.4",
572586
"zai/glm-5",
573587
]);
574588
expect(resolveSubagentModelFallbacksOverride(cfg, "strict")).toStrictEqual([]);
575589
});
576590

591+
it("resolves the subagent model config selected for isolated runs", () => {
592+
const cfg: OpenClawConfig = {
593+
agents: {
594+
defaults: {
595+
subagents: { model: "openai/gpt-5.4" },
596+
},
597+
list: [
598+
{
599+
id: "agent-model",
600+
model: {
601+
primary: "anthropic/claude-sonnet-4-6",
602+
fallbacks: ["google/gemini-3-pro"],
603+
},
604+
},
605+
{
606+
id: "subagent-model",
607+
model: "anthropic/claude-sonnet-4-6",
608+
subagents: {
609+
model: {
610+
primary: "kimi/kimi-code",
611+
fallbacks: ["openai-codex/gpt-5.4"],
612+
},
613+
},
614+
},
615+
{
616+
id: "metadata-only-subagent",
617+
model: "anthropic/claude-sonnet-4-6",
618+
subagents: {
619+
model: { timeoutMs: 1_000 },
620+
},
621+
},
622+
],
623+
},
624+
};
625+
626+
expect(resolveSubagentModelConfigSelection({ cfg, agentId: "agent-model" })).toEqual({
627+
primary: "anthropic/claude-sonnet-4-6",
628+
fallbacks: ["google/gemini-3-pro"],
629+
});
630+
expect(resolveSubagentModelConfigSelection({ cfg, agentId: "subagent-model" })).toEqual({
631+
primary: "kimi/kimi-code",
632+
fallbacks: ["openai-codex/gpt-5.4"],
633+
});
634+
expect(resolveSubagentModelConfigSelection({ cfg, agentId: "metadata-only-subagent" })).toBe(
635+
"anthropic/claude-sonnet-4-6",
636+
);
637+
expect(resolveSubagentModelConfigSelection({ cfg, agentId: "default-subagent" })).toBe(
638+
"openai/gpt-5.4",
639+
);
640+
});
641+
577642
it("should return agent-specific sandbox config", () => {
578643
const cfg = {
579644
agents: {

src/agents/agent-scope.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,48 @@ function resolveSelectedModelFallbacksOverride(
172172
return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined;
173173
}
174174

175+
export function resolveSubagentModelConfigSelection(params: {
176+
cfg: OpenClawConfig;
177+
agentId?: string;
178+
agentConfigOverride?: Pick<AgentConfig, "model" | "subagents">;
179+
}): AgentModelConfig | undefined {
180+
return resolveSubagentModelConfigValue({
181+
...params,
182+
select: (raw) => (resolvePrimaryStringValue(raw) ? raw : undefined),
183+
});
184+
}
185+
186+
function resolveSubagentModelConfigValue<T>(params: {
187+
cfg: OpenClawConfig;
188+
agentId?: string;
189+
agentConfigOverride?: Pick<AgentConfig, "model" | "subagents">;
190+
select: (raw: AgentModelConfig | undefined) => T | undefined;
191+
}): T | undefined {
192+
const agentConfig =
193+
params.agentConfigOverride ??
194+
(params.agentId ? resolveAgentConfig(params.cfg, params.agentId) : undefined);
195+
for (const raw of [
196+
agentConfig?.subagents?.model,
197+
agentConfig?.model,
198+
params.cfg.agents?.defaults?.subagents?.model,
199+
]) {
200+
const selected = params.select(raw);
201+
if (selected !== undefined) {
202+
return selected;
203+
}
204+
}
205+
return undefined;
206+
}
207+
175208
export function resolveSubagentModelFallbacksOverride(
176209
cfg: OpenClawConfig,
177210
agentId: string,
178211
): string[] | undefined {
179-
const agentConfig = resolveAgentConfig(cfg, agentId);
180-
return (
181-
resolveSelectedModelFallbacksOverride(agentConfig?.subagents?.model) ??
182-
resolveAgentModelFallbacksOverride(cfg, agentId) ??
183-
resolveSelectedModelFallbacksOverride(cfg.agents?.defaults?.subagents?.model)
184-
);
212+
return resolveSubagentModelConfigValue({
213+
cfg,
214+
agentId,
215+
select: resolveSelectedModelFallbacksOverride,
216+
});
185217
}
186218

187219
export function resolveFallbackAgentId(params: {

src/cron/isolated-agent.model-formatting.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ vi.mock("./isolated-agent/run-model-selection.runtime.js", () => ({
3939
resolveAllowedModelRef: resolveAllowedModelRefMock,
4040
resolveConfiguredModelRef: resolveConfiguredModelRefMock,
4141
resolveHooksGmailModel: resolveHooksGmailModelMock,
42+
resolveSubagentModelConfigSelection: ({
43+
cfg,
44+
agentConfigOverride,
45+
}: {
46+
cfg?: { agents?: { defaults?: { subagents?: { model?: unknown } } } };
47+
agentConfigOverride?: { model?: unknown; subagents?: { model?: unknown } };
48+
}) => {
49+
for (const raw of [
50+
agentConfigOverride?.subagents?.model,
51+
agentConfigOverride?.model,
52+
cfg?.agents?.defaults?.subagents?.model,
53+
]) {
54+
if (normalizeModelSelectionMock(raw)) {
55+
return raw;
56+
}
57+
}
58+
return undefined;
59+
},
4260
}));
4361

4462
import { resolveCronModelSelection } from "./isolated-agent/model-selection.js";
@@ -615,6 +633,26 @@ describe("cron model formatting and precedence edge cases", () => {
615633
);
616634
});
617635

636+
it("falls through metadata-only subagents.model to the agent model", async () => {
637+
await expectSelectedModel(
638+
{
639+
cfg: {
640+
agents: {
641+
defaults: {
642+
model: "anthropic/claude-sonnet-4-6",
643+
subagents: { model: "ollama/llama3.2:3b" },
644+
},
645+
},
646+
},
647+
agentConfigOverride: {
648+
model: { primary: "anthropic/claude-opus-4-6" },
649+
subagents: { model: { timeoutMs: 1_000 } },
650+
},
651+
},
652+
{ provider: "anthropic", model: "claude-opus-4-6" },
653+
);
654+
});
655+
618656
it("job payload model override takes precedence over subagents.model", async () => {
619657
await expectSelectedModel(
620658
{

src/cron/isolated-agent/model-selection.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { AgentConfig } from "../../config/types.agents.js";
12
import type { OpenClawConfig } from "../../config/types.openclaw.js";
23
import type { CronJob } from "../types.js";
34
import {
@@ -9,6 +10,7 @@ import {
910
resolveAllowedModelRef,
1011
resolveConfiguredModelRef,
1112
resolveHooksGmailModel,
13+
resolveSubagentModelConfigSelection,
1214
} from "./run-model-selection.runtime.js";
1315

1416
type CronSessionModelOverrides = {
@@ -19,12 +21,7 @@ type CronSessionModelOverrides = {
1921
export type ResolveCronModelSelectionParams = {
2022
cfg: OpenClawConfig;
2123
cfgWithAgentDefaults: OpenClawConfig;
22-
agentConfigOverride?: {
23-
model?: unknown;
24-
subagents?: {
25-
model?: unknown;
26-
};
27-
};
24+
agentConfigOverride?: Pick<AgentConfig, "model" | "subagents">;
2825
sessionEntry: CronSessionModelOverrides;
2926
payload: CronJob["payload"];
3027
isGmailHook: boolean;
@@ -82,10 +79,13 @@ export async function resolveCronModelSelection(
8279
return catalog;
8380
};
8481

85-
const subagentModelRaw =
86-
normalizeModelSelection(params.agentConfigOverride?.subagents?.model) ??
87-
normalizeModelSelection(params.agentConfigOverride?.model) ??
88-
normalizeModelSelection(params.cfg.agents?.defaults?.subagents?.model);
82+
const subagentModelRaw = normalizeModelSelection(
83+
resolveSubagentModelConfigSelection({
84+
cfg: params.cfg,
85+
agentId: params.agentId,
86+
agentConfigOverride: params.agentConfigOverride,
87+
}),
88+
);
8989
if (subagentModelRaw) {
9090
const resolvedSubagent = resolveAllowedModelRef({
9191
cfg: params.cfgWithAgentDefaults,

src/cron/isolated-agent/run-model-selection.runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
2+
export { resolveSubagentModelConfigSelection } from "../../agents/agent-scope.js";
23
export { loadModelCatalog } from "../../agents/model-catalog.js";
34
export {
45
getModelRefStatus,

src/cron/isolated-agent/run.test-harness.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,24 @@ vi.mock("./run-model-selection.runtime.js", () => ({
164164
resolveAllowedModelRef: resolveAllowedModelRefMock,
165165
resolveConfiguredModelRef: resolveConfiguredModelRefMock,
166166
resolveHooksGmailModel: resolveHooksGmailModelMock,
167+
resolveSubagentModelConfigSelection: ({
168+
cfg,
169+
agentConfigOverride,
170+
}: {
171+
cfg?: { agents?: { defaults?: { subagents?: { model?: unknown } } } };
172+
agentConfigOverride?: { model?: unknown; subagents?: { model?: unknown } };
173+
}) => {
174+
for (const raw of [
175+
agentConfigOverride?.subagents?.model,
176+
agentConfigOverride?.model,
177+
cfg?.agents?.defaults?.subagents?.model,
178+
]) {
179+
if (normalizeModelSelectionForTest(raw)) {
180+
return raw;
181+
}
182+
}
183+
return undefined;
184+
},
167185
}));
168186

169187
vi.mock("./run-execution.runtime.js", () => ({

0 commit comments

Comments
 (0)