Skip to content

Commit 4931d74

Browse files
Merge pull request #1133 from corbitsdev/cl-8297-0330-add-promptresidual-and-append-grok-tool-budget-once
feat(prompts): promptResidual with grok tool budget, appended once
2 parents 1528e33 + eaf7ca9 commit 4931d74

7 files changed

Lines changed: 110 additions & 1 deletion

File tree

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,33 @@ describe("resolveModelFamilyPolicy", () => {
102102
expect(muse.toolDisciplineRules).toContain("Never re-read a file");
103103
expect(base.toolDisciplineRules).toBeUndefined();
104104
});
105+
106+
describe("promptResidual (CL-8297)", () => {
107+
test("grok leaf carries the generic 4-line tool-budget residual", () => {
108+
const leaf = resolveModelFamilyPolicy({
109+
providerName: "xai/default",
110+
model: "grok-4.6",
111+
});
112+
expect(leaf.family).toBe("grok");
113+
expect(leaf.promptResidual).toBeDefined();
114+
if (!leaf.promptResidual)
115+
throw new Error("expected promptResidual to be defined");
116+
expect(leaf.promptResidual.split("\n")).toHaveLength(4);
117+
expect(leaf.promptResidual).toContain("Tool budget:");
118+
});
119+
120+
test("grok orchestrators and default family carry no residual", () => {
121+
const orchestrator = resolveModelFamilyPolicy({
122+
providerName: "xai/default",
123+
model: "grok-4.6",
124+
orchestrator: true,
125+
});
126+
expect(orchestrator.promptResidual).toBeUndefined();
127+
const base = resolveModelFamilyPolicy({
128+
providerName: "anthropic",
129+
model: "claude-sonnet-4",
130+
});
131+
expect(base.promptResidual).toBeUndefined();
132+
});
133+
});
105134
});

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ export interface ModelFamilyPolicy {
3838
* at the tail so it cannot disturb the cached prompt prefix.
3939
*/
4040
toolDisciplineRules?: string;
41+
/**
42+
* Provider-family residual appended once to the assembled leaf system
43+
* prompt (CL-8297). Generic tool-budget text today (grok only); the
44+
* ceremony / Claude / GPT seams stay unfilled in sibling lanes. Withheld
45+
* from orchestrators and appended at the tail so it cannot disturb the
46+
* cached prompt prefix. Undefined for families that need none.
47+
*/
48+
promptResidual?: string | undefined;
4149
}
4250

4351
const DEFAULT_WRAP_UP_NUDGE_TEXT =
@@ -66,6 +74,16 @@ const DEFAULT_POLICY: Omit<ModelFamilyPolicy, "family"> = {
6674
advertisedToolDeny: [],
6775
};
6876

77+
// Generic 4-line tool-budget residual (CL-8297). Grok leaves get this via
78+
// promptResidual today; other families leave the seam unfilled until their
79+
// own lanes land. Deliberately free of ceremony lines and family-specific
80+
// routing — pure tool-loop budget.
81+
export const GROK_TOOL_BUDGET_RESIDUAL =
82+
"Tool budget:\n" +
83+
"- Batch independent tool calls into a single turn.\n" +
84+
"- Never re-issue a tool call whose result you already have.\n" +
85+
"- When the next call would only repeat prior work, write the report instead.";
86+
6987
// A directly observed 14-turn pure-tool-call session for this family
7088
// previously motivated a tightened nudge/pause pair here (6/10). That pair
7189
// was miscalibrated: it fired on a session that was making real progress
@@ -83,6 +101,8 @@ const GROK_POLICY: Omit<ModelFamilyPolicy, "family"> = {
83101
applyGrokFinishBias: true,
84102
// Leaf value; the resolver clears it for orchestrators below.
85103
advertisedToolDeny: ["skill_search"],
104+
// Leaf value; the resolver clears it for orchestrators below.
105+
promptResidual: GROK_TOOL_BUDGET_RESIDUAL,
86106
};
87107

88108
// Kimi (Moonshot) detection ships now so callers can branch on family, but
@@ -126,6 +146,7 @@ export function resolveModelFamilyPolicy(input: {
126146
...policy,
127147
applyGrokFinishBias: policy.applyGrokFinishBias && !orchestrator,
128148
advertisedToolDeny: orchestrator ? [] : policy.advertisedToolDeny,
149+
promptResidual: orchestrator ? undefined : policy.promptResidual,
129150
};
130151
}
131152
case "kimi":

‎src/agent/prompt-sizes.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,3 +332,22 @@ describe("skywalker grok prefix (infer envelope vs trimmed director)", () => {
332332
}
333333
});
334334
});
335+
336+
describe("grok tool-budget residual (CL-8297)", () => {
337+
const countOccurrences = (haystack: string, needle: string): number =>
338+
haystack.split(needle).length - 1;
339+
340+
test("a grok leaf director prompt contains the tool budget exactly once", () => {
341+
const prompt = assembleDirectorPrompt("builder", "grok");
342+
expect(countOccurrences(prompt, "Tool budget:")).toBe(1);
343+
});
344+
345+
test("default-family and orchestrator prompts carry no tool budget", () => {
346+
expect(assembleDirectorPrompt("builder", "default")).not.toContain(
347+
"Tool budget:",
348+
);
349+
expect(assembleDirectorPrompt("skywalker", "grok")).not.toContain(
350+
"Tool budget:",
351+
);
352+
});
353+
});

‎src/agent/prompt-sizes.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
MAX_AGENTS_MD_BYTES,
1818
} from "./context-extensions.js";
1919
import { shouldApplyGrokAntiThrash } from "../subagent/provider-family.js";
20+
import { resolveModelFamilyPolicy } from "./model-family-policy.js";
2021
import { isCodexProviderName } from "../config/codex-providers.js";
2122
import { shellCollectDefinition } from "./background-shell-tool.js";
2223
import {
@@ -35,7 +36,9 @@ import { webSearchDefinition } from "../tools/web-search.js";
3536
* Assembles each director prompt exactly as src/subagent/run.ts does:
3637
* extensions=[director systemPromptRole] + environment + tools +
3738
* appendix, with the Grok finish-bias note gated by
38-
* shouldApplyGrokAntiThrash (leaves on Grok-family providers only).
39+
* shouldApplyGrokAntiThrash (leaves on Grok-family providers only) and the
40+
* family promptResidual (CL-8297 tool budget, grok leaves only) resolved
41+
* from the model family policy.
3942
*
4043
* The env and provider inputs are pinned here so sizes never drift with the
4144
* machine, date, or checkout — only real prompt changes move the numbers.
@@ -156,6 +159,7 @@ export function assembleDirectorPrompt(
156159
const pkg = DIRECTOR_REGISTRY[directorId];
157160
const orchestrator = pkg.spawn.maySpawn;
158161
const provider = family === "grok" ? GROK_PROVIDER : DEFAULT_PROVIDER;
162+
const policy = resolveModelFamilyPolicy({ ...provider, orchestrator });
159163
return buildSubAgentSystemPrompt(
160164
[formatDirectorSystemPrompt(pkg)],
161165
CANONICAL_PROMPT_ENV,
@@ -164,6 +168,7 @@ export function assembleDirectorPrompt(
164168
orchestrator,
165169
toolNames: canonicalToolNamesForDirector(pkg, family),
166170
grokAntiThrash: shouldApplyGrokAntiThrash({ ...provider, orchestrator }),
171+
promptResidual: policy.promptResidual,
167172
},
168173
);
169174
}

‎src/agent/prompts.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,3 +370,29 @@ describe("grok finish-bias residual gating (extends existing provider-family tes
370370
expect(prompt.toLowerCase()).not.toContain("kimi");
371371
});
372372
});
373+
374+
describe("promptResidual assembly (CL-8297)", () => {
375+
const TOOL_BUDGET =
376+
"Tool budget:\n" +
377+
"- Batch independent tool calls into a single turn.\n" +
378+
"- Never re-issue a tool call whose result you already have.\n" +
379+
"- When the next call would only repeat prior work, write the report instead.";
380+
381+
it("appends promptResidual exactly once at the tail for a grok leaf", () => {
382+
const prompt = buildSubAgentSystemPrompt(undefined, undefined, undefined, {
383+
orchestrator: false,
384+
grokAntiThrash: true,
385+
promptResidual: TOOL_BUDGET,
386+
});
387+
expect(countOccurrences(prompt, TOOL_BUDGET)).toBe(1);
388+
expect(prompt.trimEnd().endsWith(TOOL_BUDGET)).toBe(true);
389+
});
390+
391+
it("omits the tool budget when promptResidual is unset", () => {
392+
const prompt = buildSubAgentSystemPrompt(undefined, undefined, undefined, {
393+
orchestrator: false,
394+
grokAntiThrash: true,
395+
});
396+
expect(prompt).not.toContain("Tool budget:");
397+
});
398+
});

‎src/agent/prompts.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,11 @@ export function buildSubAgentSystemPrompt(
530530
toolNames?: readonly string[];
531531
/** When true, append the tiny Grok/xAI finish-bias note (provider residual). */
532532
grokAntiThrash?: boolean;
533+
/**
534+
* Family policy residual (CL-8297) appended once at the tail so it
535+
* cannot disturb the cached prompt prefix. Unset for families with none.
536+
*/
537+
promptResidual?: string | undefined;
533538
} = {},
534539
): string {
535540
const toolListForPrompt =
@@ -556,5 +561,8 @@ export function buildSubAgentSystemPrompt(
556561
if (opts.grokAntiThrash === true) {
557562
sections.push(buildGrokLeafAntiThrashNote());
558563
}
564+
if (opts.promptResidual !== undefined && opts.promptResidual.length > 0) {
565+
sections.push(opts.promptResidual);
566+
}
559567
return joinSections(sections);
560568
}

‎src/subagent/run.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,7 @@ async function runSubAgentInner(
10551055
model: params.provider.model,
10561056
orchestrator: params.orchestrator === true,
10571057
}),
1058+
promptResidual: modelFamilyPolicy.promptResidual,
10581059
},
10591060
);
10601061

0 commit comments

Comments
 (0)