Skip to content

Commit 38669fe

Browse files
committed
feat(skills): cap skill and spawn descriptions to one line
1 parent 4a03fb5 commit 38669fe

5 files changed

Lines changed: 73 additions & 7 deletions

File tree

‎src/agent/skill-search.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ToolDefinition } from "@intx/types/runtime";
44
import { type } from "arktype";
55

66
import type { SkillSummary } from "../extensions/skills.js";
7+
import { firstLine } from "../extensions/skills.js";
78
import {
89
lexicalFields,
910
rankAndCut,
@@ -31,14 +32,14 @@ const SKILL_SEARCH_INPUT_SCHEMA = {
3132
export const skillSearchDefinition: ToolDefinition = {
3233
name: "skill_search",
3334
description:
34-
"Look up skill details by capability. Skill names are listed in the system prompt; call this for descriptions, then use_skill to load a body. Directly callable — do not tool_search for this.",
35+
"Look up skill details by capability, then use_skill to load a body. Directly callable. See Guidelines: Tool choice.",
3536
inputSchema: SKILL_SEARCH_INPUT_SCHEMA,
3637
};
3738

3839
export const workerSkillSearchDefinition: ToolDefinition = {
3940
name: "skill_search",
4041
description:
41-
"Find a skill during prep when attached skills are not enough. Do not search on a tiny one-file fix. Directly callable — do not tool_search for this. Returns name + description; load a body with use_skill.",
42+
"Prep lookup for a skill when attached skills are not enough; skip tiny one-file fixes. Directly callable; load via use_skill. See Guidelines: Tool choice.",
4243
inputSchema: SKILL_SEARCH_INPUT_SCHEMA,
4344
};
4445

@@ -105,7 +106,7 @@ export function createSkillSearchTool(
105106
return `No skills matched "${query}". Try different keywords describing the capability.`;
106107
}
107108
return matches
108-
.map((skill) => `- ${skill.name}: ${skill.description}`)
109+
.map((skill) => `- ${skill.name}: ${firstLine(skill.description)}`)
109110
.join("\n");
110111
},
111112
});

‎src/agent/use-skill.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ const USE_SKILL_INPUT_SCHEMA = {
2929
export const useSkillDefinition: ToolDefinition = {
3030
name: "use_skill",
3131
description:
32-
"Load the full instructions for a skill. Names are listed under 'Skills' in the system prompt; call skill_search for descriptions, then this tool with the skill's name to load the body. The returned instructions stay in effect for the rest of the task.",
32+
"Load the full instructions for a skill; locate details first with skill_search. See Guidelines: Tool choice.",
3333
inputSchema: USE_SKILL_INPUT_SCHEMA,
3434
};
3535

3636
export const workerUseSkillDefinition: ToolDefinition = {
3737
name: "use_skill",
3838
description:
39-
"Load a skill you already know by name (brief or search). Do not reload skills listed as attached or already in context. The returned instructions stay in effect for the rest of the task.",
39+
"Load a skill by name; do not reload attached skills or bodies already in context. See Guidelines: Tool choice.",
4040
inputSchema: USE_SKILL_INPUT_SCHEMA,
4141
};
4242

‎src/extensions/skills.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ export async function resolveSkillBody(
174174
return undefined;
175175
}
176176

177+
// Registration strings and result lines stay one line: keep the first line so
178+
// a multi-line frontmatter description cannot leak extra lines onto the wire.
179+
export function firstLine(value: string): string {
180+
return value.split("\n", 1)[0]?.trim() ?? "";
181+
}
182+
177183
// Discover every available skill (name + one-line description). Deduped by name:
178184
// the first base dir that provides a skill wins, so a higher-precedence dir
179185
// shadows a lower one. Descriptions feed skill_search and the slash picker; the
@@ -200,7 +206,10 @@ export async function discoverSkills(
200206
// First-wins: claim the name even when skipping the listing.
201207
seen.add(entry.name);
202208
if (fm.disableModelInvocation) continue;
203-
skills.push({ name: entry.name, description: fm.description ?? "" });
209+
skills.push({
210+
name: entry.name,
211+
description: firstLine(fm.description ?? ""),
212+
});
204213
}
205214
}
206215
return skills;

‎src/subagent/agent-fleet.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ const SpawnAgentArgs = type({
548548
export const spawnAgentToolDefinition: ToolDefinition = {
549549
name: SPAWN_AGENT_TOOL_NAME,
550550
description:
551-
"Start a worker agent and return IMMEDIATELY with its agent_id — this never blocks on the worker's completion. Pass agent= a director/profile id returned by search_agents, or intent= (one of explore|implement|review|plan|general). The child starts blank. One focused task per worker. success_criteria is required for implement/review (and their default directors). Fire several spawn_agent calls in one turn to start independent lanes in parallel, then reply and end the turn — workers keep running while you are idle. Reports arrive as mailbox mail where mailbox delivery is mounted; where wait_agents is mounted (exec primary), collect with it instead. Do not poll. Excess fan-out is queued rather than refused.",
551+
"Start a worker agent and return immediately with its agent_id; one focused task per worker. See Guidelines: Orchestration.",
552552
inputSchema: {
553553
type: "object",
554554
properties: {

‎tests/unit/skills.test.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,19 @@ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
55

66
import {
77
discoverSkills,
8+
firstLine,
89
resolveSkillBody,
910
} from "../../src/extensions/skills.js";
11+
import {
12+
createSkillSearchTool,
13+
skillSearchDefinition,
14+
workerSkillSearchDefinition,
15+
} from "../../src/agent/skill-search.js";
16+
import {
17+
useSkillDefinition,
18+
workerUseSkillDefinition,
19+
} from "../../src/agent/use-skill.js";
20+
import { spawnAgentToolDefinition } from "../../src/subagent/agent-fleet.js";
1021
import { defined } from "../helpers/defined.js";
1122

1223
const fixtureCwd = join(import.meta.dirname, "../fixtures/skill-workspace");
@@ -261,3 +272,48 @@ describe("path-like skill refs", () => {
261272
}
262273
});
263274
});
275+
276+
describe("description caps (CL-8854)", () => {
277+
const MAX_DESCRIPTION_CHARS = 160;
278+
279+
test("all five registration descriptions are one line within the cap", () => {
280+
const skillDescriptions = [
281+
skillSearchDefinition.description,
282+
workerSkillSearchDefinition.description,
283+
useSkillDefinition.description,
284+
workerUseSkillDefinition.description,
285+
];
286+
for (const description of skillDescriptions) {
287+
expect(description).not.toContain("\n");
288+
expect(description.length).toBeLessThanOrEqual(MAX_DESCRIPTION_CHARS);
289+
expect(description.endsWith("See Guidelines: Tool choice.")).toBe(true);
290+
}
291+
expect(spawnAgentToolDefinition.description).not.toContain("\n");
292+
expect(spawnAgentToolDefinition.description.length).toBeLessThanOrEqual(
293+
MAX_DESCRIPTION_CHARS,
294+
);
295+
expect(
296+
spawnAgentToolDefinition.description.endsWith(
297+
"See Guidelines: Orchestration.",
298+
),
299+
).toBe(true);
300+
});
301+
302+
test("firstLine keeps only the first line", () => {
303+
expect(firstLine("one\ntwo\nthree")).toBe("one");
304+
expect(firstLine(" padded \nrest")).toBe("padded");
305+
expect(firstLine("single")).toBe("single");
306+
});
307+
308+
test("a multi-line catalog description renders as a single result line", async () => {
309+
const tool = createSkillSearchTool({
310+
skills: [{ name: "scribe", description: "write docs\nsecond line" }],
311+
});
312+
if (tool.kind !== "string") throw new Error("expected string tool");
313+
const out = await tool.handler(
314+
{ query: "scribe" },
315+
new AbortController().signal,
316+
);
317+
expect(out).toBe("- scribe: write docs");
318+
});
319+
});

0 commit comments

Comments
 (0)