Skip to content

Commit cb36b4d

Browse files
committed
fix(agent): match muse-wonder models and scope muse rules to leaves
1 parent afa244c commit cb36b4d

8 files changed

Lines changed: 139 additions & 41 deletions

‎src/agent/director.test.ts‎

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2656,7 +2656,43 @@ describe("chatDirector spacer echo", () => {
26562656
// them AFTER super(), so the base director kept sending the original and
26572657
// the feature was a silent no-op.
26582658
describe("tool-discipline rules on the wire", () => {
2659-
async function promptSentFor(model: string): Promise<string | undefined> {
2659+
async function subAgentPromptSentFor(
2660+
model: string,
2661+
orchestrator = false,
2662+
): Promise<string | undefined> {
2663+
const { SubAgentDirector } = await import("../subagent/nudge-director.js");
2664+
const { resolveModelFamilyPolicy } =
2665+
await import("./model-family-policy.js");
2666+
const policy = resolveModelFamilyPolicy({
2667+
providerName: "opencode-go",
2668+
model,
2669+
orchestrator,
2670+
});
2671+
const director = new SubAgentDirector(
2672+
"BASE PROMPT",
2673+
[],
2674+
undefined,
2675+
undefined,
2676+
Date.now,
2677+
false,
2678+
false,
2679+
undefined,
2680+
policy.toolDisciplineRules,
2681+
);
2682+
const event = {
2683+
type: "message.received",
2684+
message: { role: "user", content: "hi" },
2685+
} as unknown as ReactorInboundEvent;
2686+
const actions = actionsArray(
2687+
await director.decide(event, mockState, stubReactorCapabilities),
2688+
);
2689+
const infer = actions.find((a) => a.type === "infer") as
2690+
| { options?: ExtendedInferenceOptions }
2691+
| undefined;
2692+
return infer?.options?.systemPrompt;
2693+
}
2694+
2695+
async function chatPromptSentFor(model: string): Promise<string | undefined> {
26602696
const director = createChatDirector("BASE PROMPT", [], {
26612697
provider: { providerName: "opencode-go", model },
26622698
});
@@ -2665,21 +2701,31 @@ describe("tool-discipline rules on the wire", () => {
26652701
message: { role: "user", content: "hi" },
26662702
} as unknown as ReactorInboundEvent;
26672703
const actions = actionsArray(
2668-
await director.decide(event, stubReactorState, stubReactorCapabilities),
2704+
await director.decide(event, mockState, stubReactorCapabilities),
26692705
);
26702706
const infer = actions.find((a) => a.type === "infer") as
26712707
| { options?: ExtendedInferenceOptions }
26722708
| undefined;
26732709
return infer?.options?.systemPrompt;
26742710
}
26752711

2676-
test("a Muse Spark session appends rules at the tail, leaving the base prefix intact", async () => {
2677-
const prompt = await promptSentFor("muse-spark-1.3-contributor");
2678-
expect(prompt?.startsWith("BASE PROMPT")).toBe(true);
2679-
expect(prompt?.length ?? 0).toBeGreaterThan("BASE PROMPT".length);
2712+
test("a Muse leaf appends rules at the tail, leaving the base prefix intact", async () => {
2713+
for (const model of ["muse-spark-1.3-contributor", "muse-wonder-1.0"]) {
2714+
const prompt = await subAgentPromptSentFor(model, false);
2715+
expect(prompt?.startsWith("BASE PROMPT")).toBe(true);
2716+
expect(prompt?.length ?? 0).toBeGreaterThan("BASE PROMPT".length);
2717+
}
2718+
});
2719+
2720+
test("a Muse orchestrator and primary ChatDirector withhold leaf tool-discipline rules", async () => {
2721+
for (const model of ["muse-spark-1.3-contributor", "muse-wonder-1.0"]) {
2722+
expect(await subAgentPromptSentFor(model, true)).toBe("BASE PROMPT");
2723+
expect(await chatPromptSentFor(model)).toBe("BASE PROMPT");
2724+
}
26802725
});
26812726

26822727
test("a family with no rules sends the prompt untouched", async () => {
2683-
expect(await promptSentFor("claude-sonnet-4")).toBe("BASE PROMPT");
2728+
expect(await subAgentPromptSentFor("claude-sonnet-4")).toBe("BASE PROMPT");
2729+
expect(await chatPromptSentFor("claude-sonnet-4")).toBe("BASE PROMPT");
26842730
});
26852731
});

‎src/agent/director.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,9 +1290,12 @@ export function createChatDirector(
12901290
return new ChatDirectorImpl(systemPrompt, toolDefinitions, {
12911291
...rest,
12921292
// `provider` is raw {providerName, model} input; the constructor wants
1293-
// the resolved ModelFamilyPolicy.
1293+
// the resolved ModelFamilyPolicy. ChatDirector is always the primary
1294+
// orchestrator, so leaf-only residuals are withheld.
12941295
modelFamilyPolicy:
1295-
provider !== undefined ? resolveModelFamilyPolicy(provider) : undefined,
1296+
provider !== undefined
1297+
? resolveModelFamilyPolicy({ ...provider, orchestrator: true })
1298+
: undefined,
12961299
// Seed the live source id (replaces getProviderId), refreshed on every
12971300
// inference completion so /model switches remap retry stamping.
12981301
sessionProviderName: provider?.providerName,

‎src/agent/model-family-policy.test.ts‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,30 @@ describe("resolveModelFamilyPolicy", () => {
9090
}
9191
});
9292

93-
test("muse spark carries tool-discipline rules; other families do not", () => {
94-
const muse = resolveModelFamilyPolicy({
95-
providerName: "opencode-go/acme",
96-
model: "muse-spark-1.3-contributor",
97-
});
93+
test("muse leaves carry tool-discipline rules; muse orchestrators and other families do not", () => {
94+
for (const model of [
95+
"muse-spark-1.3-contributor",
96+
"muse-wonder-1.0",
97+
] as const) {
98+
const leaf = resolveModelFamilyPolicy({
99+
providerName: "meta-api",
100+
model,
101+
orchestrator: false,
102+
});
103+
const orchestrator = resolveModelFamilyPolicy({
104+
providerName: "meta-api",
105+
model,
106+
orchestrator: true,
107+
});
108+
expect(leaf.family).toBe("muse");
109+
expect(leaf.toolDisciplineRules?.length).toBeGreaterThan(0);
110+
expect(orchestrator.family).toBe("muse");
111+
expect(orchestrator.toolDisciplineRules).toBeUndefined();
112+
}
98113
const base = resolveModelFamilyPolicy({
99114
providerName: "anthropic",
100115
model: "claude-sonnet-4",
101116
});
102-
expect(muse.family).toBe("muse");
103-
expect(muse.toolDisciplineRules?.length).toBeGreaterThan(0);
104117
expect(base.toolDisciplineRules).toBeUndefined();
105118
});
106119

‎src/agent/model-family-policy.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export interface ModelFamilyPolicy {
3838
* not self-terminate a tool loop. Appended at the tail to keep the cached
3939
* prompt prefix intact.
4040
*/
41-
toolDisciplineRules?: string;
41+
toolDisciplineRules?: string | undefined;
4242
/**
4343
* Provider-family residual appended once to the leaf system prompt
4444
* (tool-budget text for grok, XML task_guidance for claude, narrate
@@ -224,7 +224,13 @@ export function resolveModelFamilyPolicy(input: {
224224
...KIMI_POLICY,
225225
};
226226
case "muse":
227-
return { family, ...MUSE_POLICY };
227+
return {
228+
family,
229+
...MUSE_POLICY,
230+
toolDisciplineRules: orchestrator
231+
? undefined
232+
: MUSE_POLICY.toolDisciplineRules,
233+
};
228234
case "claude":
229235
// Like grok's finish-bias, task_guidance only makes sense on leaf
230236
// workers — orchestrators get the permissive default.

‎src/provider/reasoning-effort.test.ts‎

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -127,27 +127,33 @@ describe("supportedEfforts", () => {
127127
expect(supportedEfforts("deepseek-v3")).toEqual(["low", "medium", "high"]);
128128
});
129129

130-
// Every catalog Muse Spark id reaches supportedEfforts unnormalized; two
131-
// ids pin the family rule.
132-
test.each(["muse-spark-1.3-contributor", "muse-spark-1.3-contributor-free"])(
133-
"Muse Spark id %s supports minimal through high",
134-
(model) => {
135-
expect(supportedEfforts(model)).toEqual([
136-
"minimal",
137-
"low",
138-
"medium",
139-
"high",
140-
]);
141-
expect(defaultEffortForModel(model)).toBe("low");
142-
},
143-
);
130+
// Every catalog Muse id reaches supportedEfforts unnormalized; spark and
131+
// wonder ids pin the family rule.
132+
test.each([
133+
"muse-spark-1.3-contributor",
134+
"muse-spark-1.3-contributor-free",
135+
"muse-wonder-1.0",
136+
])("Muse id %s supports minimal through high", (model) => {
137+
expect(supportedEfforts(model)).toEqual([
138+
"minimal",
139+
"low",
140+
"medium",
141+
"high",
142+
]);
143+
expect(defaultEffortForModel(model)).toBe("low");
144+
});
144145

145-
test("a model merely containing muse-spark is not matched", () => {
146+
test("a model merely containing muse- is not matched", () => {
146147
expect(supportedEfforts("not-muse-spark-1.3")).toEqual([
147148
"low",
148149
"medium",
149150
"high",
150151
]);
152+
expect(supportedEfforts("not-muse-wonder-1.0")).toEqual([
153+
"low",
154+
"medium",
155+
"high",
156+
]);
151157
});
152158
});
153159

@@ -175,11 +181,13 @@ describe("validateEffort", () => {
175181
expect(validateEffort("grok-4.5", "xhigh").ok).toBe(false);
176182
});
177183

178-
test("accepts minimal on Muse Spark and rejects none", () => {
179-
expect(validateEffort("muse-spark-1.3-contributor", "minimal")).toEqual({
180-
ok: true,
181-
});
182-
expect(validateEffort("muse-spark-1.3-contributor", "none").ok).toBe(false);
184+
test("accepts minimal on Muse models and rejects none", () => {
185+
for (const model of ["muse-spark-1.3-contributor", "muse-wonder-1.0"]) {
186+
expect(validateEffort(model, "minimal")).toEqual({
187+
ok: true,
188+
});
189+
expect(validateEffort(model, "none").ok).toBe(false);
190+
}
183191
});
184192

185193
test("rejects medium on glm-5.3 family", () => {

‎src/provider/reasoning-effort.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ const MUSE_SPARK_EFFORTS: readonly ReasoningEffort[] = DEFAULT_EFFORTS;
8888
// and nothing normalizes the model string first. Mirrors the grok/kimi checks
8989
// in src/subagent/provider-family.ts.
9090
function isMuseSparkModel(model: string): boolean {
91-
return /^muse-spark/i.test(model.trim());
91+
return /^muse-/i.test(model.trim());
9292
}
9393

9494
// grok-4.6/4.7 accept xhigh (CODEX_EFFORTS); grok-4.5 and composer stay on

‎src/subagent/provider-family.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
isDeepSeekLeafProvider,
77
isGptProvider,
88
isKimiLeafProvider,
9+
isMuseSparkLeafProvider,
910
isXaiGrokLeafProvider,
1011
shouldApplyGrokAntiThrash,
1112
} from "./provider-family.js";
@@ -67,13 +68,33 @@ describe("isKimiLeafProvider", () => {
6768
);
6869
});
6970

71+
describe("isMuseSparkLeafProvider", () => {
72+
test.each<[ProviderRow, boolean]>([
73+
[
74+
{ providerName: "opencode-go", model: "muse-spark-1.3-contributor" },
75+
true,
76+
],
77+
[{ providerName: "meta-api", model: "muse-wonder-1.0" }, true],
78+
[{ providerName: "meta/default", model: "MUSE-WONDER-1.0" }, true],
79+
[{ providerName: "meta-api", model: "not-muse-wonder-1.0" }, false],
80+
[{ providerName: "meta-api" }, false],
81+
])("matches %j -> %s", (row, expected) => {
82+
expect(isMuseSparkLeafProvider(row)).toBe(expected);
83+
});
84+
});
85+
7086
describe("detectModelFamily", () => {
7187
test.each<[ProviderRow, ReturnType<typeof detectModelFamily>]>([
7288
[{ providerName: "xai/default", model: "grok-4.5" }, "grok"],
7389
[{ providerName: "xai/default", model: "grok-4.6" }, "grok"],
7490
[{ providerName: "xai/default", model: "grok-4.7" }, "grok"],
7591
[{ providerName: "moonshot", model: "kimi-k2" }, "kimi"],
7692
[{ providerName: "opencode-go", model: "kimi-k3" }, "kimi"],
93+
[
94+
{ providerName: "opencode-go", model: "muse-spark-1.3-contributor" },
95+
"muse",
96+
],
97+
[{ providerName: "meta-api", model: "muse-wonder-1.0" }, "muse"],
7798
[{ providerName: "anthropic", model: "claude-sonnet-4" }, "claude"],
7899
[{ providerName: "openai-compat", model: "claude-opus-4-6" }, "claude"],
79100
[{ providerName: "openai", model: "gpt-5.6" }, "gpt"],

‎src/subagent/provider-family.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@ export function isKimiLeafProvider(input: {
4343
return false;
4444
}
4545

46-
/** True when the provider/model is the OpenCode Go Muse Spark family. */
46+
// Model-id prefix match: the family ships across OpenCode Go, Zen, and Meta
47+
// catalogs under muse-spark and muse-wonder ids without a shared provider name.
4748
export function isMuseSparkLeafProvider(input: {
4849
providerName: string;
4950
model?: string;
5051
}): boolean {
51-
return input.model !== undefined && /^muse-spark/i.test(input.model.trim());
52+
return input.model !== undefined && /^muse-/i.test(input.model.trim());
5253
}
5354

5455
/** True when the provider/model is Anthropic's Claude family. */

0 commit comments

Comments
 (0)