Skip to content

Commit d633c05

Browse files
committed
Deny skill_search on grok and kimi leaf workers
Search is the noisy surface on small models; use_skill stays available so brief-named skills still load.
1 parent 2e8e804 commit d633c05

3 files changed

Lines changed: 80 additions & 7 deletions

File tree

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,39 @@ describe("resolveModelFamilyPolicy", () => {
5252
expect(kimi.toolOnlyTurnNudgeAt).toBe(base.toolOnlyTurnNudgeAt);
5353
expect(kimi.subAgentStallTimeoutMs).toBe(base.subAgentStallTimeoutMs);
5454
});
55+
56+
test("advertisedToolDeny is empty by default and never contains use_skill", () => {
57+
const leaf = resolveModelFamilyPolicy({
58+
providerName: "anthropic",
59+
model: "claude-opus-4-6",
60+
orchestrator: false,
61+
});
62+
expect(leaf.advertisedToolDeny).toEqual([]);
63+
expect(leaf.advertisedToolDeny).not.toContain("use_skill");
64+
});
65+
66+
test("grok and kimi leaves deny skill_search only", () => {
67+
for (const input of [
68+
{ providerName: "xai", model: "grok-4-1-fast-non-reasoning" },
69+
{ providerName: "moonshot", model: "kimi-k2-0711" },
70+
] as const) {
71+
const leaf = resolveModelFamilyPolicy({
72+
...input,
73+
orchestrator: false,
74+
});
75+
expect(leaf.advertisedToolDeny).toEqual(["skill_search"]);
76+
expect(leaf.advertisedToolDeny).not.toContain("use_skill");
77+
}
78+
});
79+
80+
test("orchestrators keep the full surface on every family", () => {
81+
for (const input of [
82+
{ providerName: "xai", model: "grok-4-1-fast-non-reasoning" },
83+
{ providerName: "moonshot", model: "kimi-k2-0711" },
84+
{ providerName: "anthropic", model: "claude-opus-4-6" },
85+
] as const) {
86+
const policy = resolveModelFamilyPolicy({ ...input, orchestrator: true });
87+
expect(policy.advertisedToolDeny).toEqual([]);
88+
}
89+
});
5590
});

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export interface ModelFamilyPolicy {
2525
subAgentStallTimeoutMs: number;
2626
/** Grok's finish-bias residual (withhold from orchestrators; see provider-family.ts). */
2727
applyGrokFinishBias: boolean;
28+
/**
29+
* Tool names to drop from the advertised wire prefix and the dispatch gate
30+
* (CL-7668). Empty by default; grok/kimi leaves deny `skill_search` only and
31+
* load brief-named skills straight through `use_skill`, which is never
32+
* denied. Orchestrators keep the full surface.
33+
*/
34+
advertisedToolDeny: readonly string[];
2835
}
2936

3037
const DEFAULT_WRAP_UP_NUDGE_TEXT =
@@ -50,6 +57,7 @@ const DEFAULT_POLICY: Omit<ModelFamilyPolicy, "family"> = {
5057
wrapUpNudgeText: DEFAULT_WRAP_UP_NUDGE_TEXT,
5158
subAgentStallTimeoutMs: 5 * 60_000,
5259
applyGrokFinishBias: false,
60+
advertisedToolDeny: [],
5361
};
5462

5563
// A directly observed 14-turn pure-tool-call session for this family
@@ -67,6 +75,8 @@ const GROK_POLICY: Omit<ModelFamilyPolicy, "family"> = {
6775
wrapUpNudgeText: GROK_WRAP_UP_NUDGE_TEXT,
6876
subAgentStallTimeoutMs: DEFAULT_POLICY.subAgentStallTimeoutMs,
6977
applyGrokFinishBias: true,
78+
// Leaf value; the resolver clears it for orchestrators below.
79+
advertisedToolDeny: ["skill_search"],
7080
};
7181

7282
// Kimi (Moonshot) detection ships now so callers can branch on family, but
@@ -92,10 +102,15 @@ export function resolveModelFamilyPolicy(input: {
92102
return {
93103
...policy,
94104
applyGrokFinishBias: policy.applyGrokFinishBias && !orchestrator,
105+
advertisedToolDeny: orchestrator ? [] : policy.advertisedToolDeny,
95106
};
96107
}
97108
case "kimi":
98-
return { family, ...KIMI_POLICY };
109+
return {
110+
family,
111+
...KIMI_POLICY,
112+
advertisedToolDeny: orchestrator ? [] : ["skill_search"],
113+
};
99114
default:
100115
return { family: "default", ...DEFAULT_POLICY };
101116
}

‎src/session/assemble-runtime.ts‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
type ToolAvailability,
4949
} from "../agent/tool-search.js";
5050
import { normalizeToolDefinitionsForProvider } from "../agent/tool-schema-normalize.js";
51+
import { resolveModelFamilyPolicy } from "../agent/model-family-policy.js";
5152
import { createChatDirector, type ChatDirector } from "../agent/director.js";
5253
import { createDoomLoopCorrectiveNote } from "../agent/doom-loop-note.js";
5354
import type { Task } from "../agent/tasks.js";
@@ -369,17 +370,39 @@ export function createAdvertisedToolset(args: {
369370
];
370371
const activated = createActivatedToolTracker();
371372
// Advertise then family-gate wire schemas (kimi gets a non-recursive present).
373+
// advertisedToolDeny (CL-7668) drops grok/kimi-leaf skill_search from the
374+
// wire prefix and the dispatch gate; orchestrators keep the full surface.
375+
// Resolved per call so a live model switch re-gates without a rebuild.
376+
// use_skill is never denied — leaves load brief-named skills by exact name.
377+
const deniedFor = (provider: {
378+
providerName: string;
379+
model: string;
380+
}): readonly string[] =>
381+
resolveModelFamilyPolicy({
382+
providerName: provider.providerName,
383+
model: provider.model,
384+
orchestrator: args.sessionMode === "orchestrator",
385+
}).advertisedToolDeny;
372386
const computeAdvertised = (
373387
all: readonly ToolDefinition[],
374-
): ToolDefinition[] =>
375-
normalizeToolDefinitionsForProvider(
376-
advertisedTools(all, activated.list(), prefix),
388+
): ToolDefinition[] => {
389+
const provider = args.getProvider();
390+
const denied = deniedFor(provider);
391+
const gatedPrefix =
392+
denied.length === 0
393+
? prefix
394+
: prefix.filter((name) => !denied.includes(name));
395+
return normalizeToolDefinitionsForProvider(
396+
advertisedTools(all, activated.list(), gatedPrefix),
377397
{
378-
...args.getProvider(),
398+
...provider,
379399
},
380400
);
381-
const isAdvertised = (name: string): boolean =>
382-
prefix.includes(name) || activated.has(name);
401+
};
402+
const isAdvertised = (name: string): boolean => {
403+
if (deniedFor(args.getProvider()).includes(name)) return false;
404+
return prefix.includes(name) || activated.has(name);
405+
};
383406
return { activated, computeAdvertised, isAdvertised };
384407
}
385408

0 commit comments

Comments
 (0)