Skip to content

Commit e0abe4a

Browse files
committed
fix(agent): project worker wires from the profile table
requires_tools and worker advertisement resolve wire names through the profile projection, so a later family uses the same path as gpt. apply_patch, shell, and update_plan are rows in that table.
1 parent f1e838e commit e0abe4a

5 files changed

Lines changed: 208 additions & 64 deletions

File tree

‎src/agent/tool-aliases.ts‎

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,40 +42,114 @@ const ENGINE_TO_WIRE_BY_PROFILE: Record<
4242
gpt: { ...DEFAULT_ENGINE_TO_WIRE, ...GPT_OVERRIDES },
4343
};
4444

45-
/** Registry engines the gpt profile folds into apply_patch, in advertise order. */
46-
export const GPT_PATCH_ENGINES = [
47-
"write_file",
48-
"edit_file",
49-
"delete_file",
50-
] as const;
45+
/**
46+
* Engines one advertised wire replaces. A profile without an entry keeps
47+
* the 1:1 wire table. Adding a family is a new ToolProfile plus a row here
48+
* and in ENGINE_TO_WIRE_BY_PROFILE — not a new director package.
49+
*/
50+
const PROFILE_FOLDS: Record<
51+
ToolProfile,
52+
{ wire: string; engines: readonly string[] } | undefined
53+
> = {
54+
default: undefined,
55+
gpt: {
56+
wire: "apply_patch",
57+
engines: ["write_file", "edit_file", "delete_file"],
58+
},
59+
};
60+
61+
function buildAdvertisedWireEngines(
62+
profile: ToolProfile,
63+
): ReadonlyMap<string, readonly string[]> {
64+
const fold = PROFILE_FOLDS[profile];
65+
const folded = new Set(fold?.engines ?? []);
66+
const map = new Map<string, string[]>();
67+
for (const [engine, wire] of Object.entries(
68+
ENGINE_TO_WIRE_BY_PROFILE[profile],
69+
)) {
70+
if (folded.has(engine)) continue;
71+
const existing = map.get(wire);
72+
if (existing === undefined) map.set(wire, [engine]);
73+
else existing.push(engine);
74+
}
75+
if (fold !== undefined) map.set(fold.wire, [...fold.engines]);
76+
return map;
77+
}
78+
79+
const ADVERTISED_WIRE_ENGINES = Object.fromEntries(
80+
(Object.keys(ENGINE_TO_WIRE_BY_PROFILE) as ToolProfile[]).map((profile) => [
81+
profile,
82+
buildAdvertisedWireEngines(profile),
83+
]),
84+
) as Record<ToolProfile, ReadonlyMap<string, readonly string[]>>;
85+
86+
export function toolProfiles(): readonly ToolProfile[] {
87+
return Object.keys(ENGINE_TO_WIRE_BY_PROFILE) as ToolProfile[];
88+
}
89+
90+
/** Wire name → engine ids this profile advertises. Folded wires map to every engine they replace. */
91+
export function advertisedWireEngines(
92+
profile: ToolProfile,
93+
): ReadonlyMap<string, readonly string[]> {
94+
return ADVERTISED_WIRE_ENGINES[profile];
95+
}
96+
97+
/**
98+
* Engines `wire` stands for on `profile`. Undefined when this profile does
99+
* not advertise that name (a later family follows the same table).
100+
*/
101+
export function enginesForAdvertisedWire(
102+
wire: string,
103+
profile: ToolProfile,
104+
): readonly string[] | undefined {
105+
const table = ADVERTISED_WIRE_ENGINES[profile];
106+
return table.get(wire) ?? table.get(wire.toLowerCase());
107+
}
51108

52-
/** Tools the gpt profile folds into the single apply_patch envelope tool. */
53-
const PATCH_FOLDED_ENGINES: ReadonlySet<string> = new Set(GPT_PATCH_ENGINES);
109+
/** Every wire any profile advertises for `engine`, including fold wires. */
110+
export function advertisedNamesForEngine(engine: string): readonly string[] {
111+
const names: string[] = [];
112+
const seen = new Set<string>();
113+
for (const profile of toolProfiles()) {
114+
const fold = PROFILE_FOLDS[profile];
115+
const wire =
116+
fold !== undefined && fold.engines.includes(engine)
117+
? fold.wire
118+
: advertisedToolName(engine, profile);
119+
if (seen.has(wire)) continue;
120+
seen.add(wire);
121+
names.push(wire);
122+
}
123+
return names;
124+
}
54125

55126
/** gpt models are trained on apply_patch, not write/edit/delete. Other
56-
* profiles and lists without a file-mutation tool pass through unchanged. */
127+
* profiles and lists without a folded tool pass through unchanged. */
57128
export function foldFileToolNames(
58129
names: readonly string[],
59130
profile: ToolProfile,
60131
): readonly string[] {
61-
if (profile !== "gpt") return names;
132+
const fold = PROFILE_FOLDS[profile];
133+
if (fold === undefined) return names;
62134
const folded = (name: string): boolean =>
63-
PATCH_FOLDED_ENGINES.has(engineToolName(name));
135+
fold.engines.includes(engineToolName(name));
64136
if (!names.some(folded)) return names;
65137
const first = names.findIndex(folded);
66138
return names.flatMap((name, i) =>
67-
i === first ? ["apply_patch"] : folded(name) ? [] : [name],
139+
i === first ? [fold.wire] : folded(name) ? [] : [name],
68140
);
69141
}
70142

71143
export function foldFileToolDefinitions(
72144
defs: readonly ToolDefinition[],
73145
profile: ToolProfile,
74146
): ToolDefinition[] {
75-
if (profile !== "gpt" || !defs.some((d) => d.name === "apply_patch")) {
147+
const fold = PROFILE_FOLDS[profile];
148+
if (fold === undefined || !defs.some((d) => d.name === fold.wire)) {
76149
return [...defs];
77150
}
78-
return defs.filter((d) => !PATCH_FOLDED_ENGINES.has(engineToolName(d.name)));
151+
const folded = new Set(fold.engines);
152+
return defs.filter((d) => !folded.has(engineToolName(d.name)));
79153
}
80154

81155
export function toolProfileForFamily(family: ModelFamily): ToolProfile {

‎src/agent/tool-search.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
ORCHESTRATOR_TOOLS,
3030
READ_TOOLS,
3131
} from "./directors/tool-sets.js";
32+
import { advertisedWireEngines, toolProfiles } from "./tool-aliases.js";
3233

3334
const FULL_AVAILABILITY: ToolAvailability = {
3435
languageServerAvailable: true,
@@ -1050,6 +1051,25 @@ describe("advertisedToolNamesForWorker", () => {
10501051
expect(build).not.toContain("write_file");
10511052
});
10521053

1054+
test("every non-default profile projects allowlisted engines through its wire table", () => {
1055+
const allow = BUILD_TOOLS as readonly string[];
1056+
for (const profile of toolProfiles()) {
1057+
const names = advertisedToolNamesForWorker({
1058+
allow: BUILD_TOOLS,
1059+
profile,
1060+
});
1061+
if (profile === "default") {
1062+
expect(names).toContain("write_file");
1063+
expect(names).not.toContain("apply_patch");
1064+
continue;
1065+
}
1066+
for (const [wire, engines] of advertisedWireEngines(profile)) {
1067+
if (!engines.some((engine) => allow.includes(engine))) continue;
1068+
expect(names, `${profile} ${wire}`).toContain(wire);
1069+
}
1070+
}
1071+
});
1072+
10531073
test("nested orchestrator allowlist adds fleet verbs but not search_agents", () => {
10541074
const orch = advertisedToolNamesForWorker({ allow: ORCHESTRATOR_TOOLS });
10551075
expect(orch).toContain("spawn_agent");

‎src/agent/tool-search.ts‎

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import {
1212
import type { SessionMode } from "../config/session-mode.js";
1313
import { sessionModeEnablesSubAgents } from "../config/session-mode.js";
1414
import {
15+
advertisedNamesForEngine,
1516
advertisedToolName,
1617
foldFileToolNames,
17-
GPT_PATCH_ENGINES,
1818
projectToolDefinition,
1919
type ToolProfile,
2020
} from "./tool-aliases.js";
@@ -139,10 +139,10 @@ export function advertisedToolNamesForWorker(opts: {
139139
seen.add(name);
140140
out.push(name);
141141
}
142-
if (opts.profile !== "gpt") return out;
142+
if (opts.profile === undefined || opts.profile === "default") return out;
143143
return foldFileToolNames(
144-
out.map((name) => advertisedToolName(name, "gpt")),
145-
"gpt",
144+
out.map((name) => advertisedToolName(name, opts.profile)),
145+
opts.profile,
146146
);
147147
}
148148

@@ -177,19 +177,11 @@ function isAlreadyAdvertised(
177177
advertisedNames: readonly string[],
178178
): boolean {
179179
const engine = canonicalToolName(defName);
180-
if (
181-
advertisedNames.includes(defName) ||
182-
advertisedNames.includes(engine) ||
183-
advertisedNames.includes(advertisedToolName(engine)) ||
184-
advertisedNames.includes(advertisedToolName(engine, "gpt"))
185-
) {
180+
if (advertisedNames.includes(defName) || advertisedNames.includes(engine)) {
186181
return true;
187182
}
188-
// apply_patch stands in for the file engines; a gpt prefix lists the wire
189-
// name only, so search must not offer write/edit/delete beside it.
190-
return (
191-
advertisedNames.includes("apply_patch") &&
192-
(GPT_PATCH_ENGINES as readonly string[]).includes(engine)
183+
return advertisedNamesForEngine(engine).some((name) =>
184+
advertisedNames.includes(name),
193185
);
194186
}
195187

‎src/subagent/capability-preflight.test.ts‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import {
1010
type CapabilityUnavailable,
1111
} from "./capability-preflight.js";
1212
import type { CapabilityFilter } from "../agent/profile-types.js";
13-
import { DOCS_TOOLS, READ_TOOLS } from "../agent/directors/tool-sets.js";
13+
import {
14+
BUILD_TOOLS,
15+
DOCS_TOOLS,
16+
READ_TOOLS,
17+
} from "../agent/directors/tool-sets.js";
18+
import { advertisedWireEngines, toolProfiles } from "../agent/tool-aliases.js";
1419

1520
const ALLOW_SHELL: CapabilityFilter = {
1621
mode: "allow",
@@ -255,6 +260,55 @@ describe("preflightCapabilities", () => {
255260
expect(result.unavailable.tool).toBe("apply_patch");
256261
});
257262

263+
test("gpt wire names pass when the worker has the underlying engines", () => {
264+
const resolvedFilter: CapabilityFilter = {
265+
mode: "allow",
266+
tools: [...BUILD_TOOLS],
267+
};
268+
const cases = [
269+
{
270+
wire: "apply_patch",
271+
canonical: ["write_file", "edit_file", "delete_file"],
272+
},
273+
{ wire: "shell", canonical: ["run_shell"] },
274+
{ wire: "update_plan", canonical: ["manage_tasks"] },
275+
];
276+
for (const entry of cases) {
277+
expect(
278+
preflightCapabilities({
279+
required: [entry.wire],
280+
resolvedFilter,
281+
knownEngines: DEFAULT_KNOWN_ENGINES,
282+
agentLabel: "coder",
283+
toolProfile: "gpt",
284+
}),
285+
entry.wire,
286+
).toEqual({ ok: true, canonical: entry.canonical });
287+
}
288+
});
289+
290+
test("every profile accepts its advertised wires when those engines are allowed", () => {
291+
const known = new Set(DEFAULT_KNOWN_ENGINES);
292+
const tools = [...BUILD_TOOLS, "manage_tasks", "wait_agents"];
293+
for (const profile of toolProfiles()) {
294+
for (const [wire, mapped] of advertisedWireEngines(profile)) {
295+
if (!mapped.every((engine) => known.has(engine))) continue;
296+
const result = preflightCapabilities({
297+
required: [wire],
298+
resolvedFilter: { mode: "allow", tools },
299+
knownEngines: DEFAULT_KNOWN_ENGINES,
300+
agentLabel: "coder",
301+
toolProfile: profile,
302+
});
303+
expect(result.ok, `${profile} ${wire}`).toBe(true);
304+
if (!result.ok) continue;
305+
for (const engine of result.canonical) {
306+
expect(mapped, `${profile} ${wire}`).toContain(engine);
307+
}
308+
}
309+
}
310+
});
311+
258312
test("gpt apply_patch maps onto only the file engines the allowlist has", () => {
259313
const result = preflightCapabilities({
260314
required: ["apply_patch"],

0 commit comments

Comments
 (0)