feat(coding-agent): simplified skill management by removing per-task pinning

- Removed per-task skill pinning feature; subagents now inherit session skill set instead of per-task selection.
- Removed `preloadedSkills` option from `CreateAgentSessionOptions` interface and related system prompt plumbing.
- Removed `skills` field from Task schema; task execution now passes available session skills directly to subagents.
- Simplified task execution pipeline by removing skill resolution logic and preloaded skills handling from system prompt templates.
This commit is contained in:
can1357
2026-02-26 20:47:54 +01:00
parent e4bd646255
commit 2e20d87151
13 changed files with 29 additions and 143 deletions
+2 -9
View File
@@ -122,19 +122,12 @@ For providers other than codex/claude/native (for example `agents`, `claude-plug
System prompt construction (`src/system-prompt.ts`) uses discovered skills as follows:
- if `read` tool is available **and** no explicit preloaded skills are supplied:
- if `read` tool is available:
- include discovered skills list in prompt
- otherwise:
- omit discovered list
- if preloaded skills are provided (for example from Task tool skill pinning):
- inline full preloaded skill contents in `<preloaded_skills>`
### Task tool skill pinning
When a Task call specifies `skills`, runtime resolves names against session skills:
- unknown names cause an immediate error with available skill names
- resolved skills are passed as preloaded skills to subagents
Task tool subagents receive the session's discovered/provided skills list via normal session creation; there is no per-task skill pinning override.
### Interactive `/skill:<name>` commands
+14
View File
@@ -1,6 +1,20 @@
# Changelog
## [Unreleased]
### Breaking Changes
- Removed `preloadedSkills` option from `CreateAgentSessionOptions`; skills are no longer inlined into system prompts
- Removed `skills` field from Task schema; subagents now always inherit the session skill set instead of per-task skill selection
- Removed Task tool per-task `tasks[].skills` support; subagents now always inherit the session skill set
- Removed `preloadedSkills` system prompt plumbing and template sections; skills are no longer inlined as a separate preloaded block
### Changed
- Simplified task execution to always pass available session skills to subagents instead of resolving per-task skill lists
### Removed
- Removed preloaded skills section from system prompt templates; skills are now referenced only as available resources
## [13.3.6] - 2026-02-26
### Breaking Changes
@@ -40,16 +40,6 @@ If a skill covers your output, you **MUST** read `skill://<name>` before proceed
{{/list}}
</skills>
{{/if}}
{{#if preloadedSkills.length}}
Following skills are preloaded in full; you **MUST** apply instructions directly.
<preloaded-skills>
{{#list preloadedSkills join="\n"}}
<skill name="{{name}}">
{{content}}
</skill>
{{/list}}
</preloaded-skills>
{{/if}}
{{#if rules.length}}
Rules are local constraints.
You **MUST** read `rule://<name>` when working in that domain.
@@ -93,14 +93,6 @@ You **MUST** use the following skills, to save you time, when working in their d
{{/each}}
{{/if}}
{{#if preloadedSkills.length}}
Preloaded skills:
{{#each preloadedSkills}}
## {{name}}
{{content}}
{{/each}}
{{/if}}
{{#if rules.length}}
# Rules
Domain-specific rules from past experience. **MUST** read `rule://<name>` when working in their territory.
@@ -12,7 +12,6 @@ Subagents lack your conversation history. Every decision, file content, and user
- `.id`: CamelCase, max 32 chars
- `.description`: UI display only — subagent never sees it
- `.assignment`: Complete self-contained instructions. One-liners PROHIBITED; missing acceptance criteria = too vague.
- `.skills`: Skill names to preload
- `context`: Shared background prepended to every assignment. Session-specific info only.
- `schema`: JTD schema for expected output. Format lives here — **MUST NOT** be duplicated in assignments.
- `tasks`: Tasks to execute in parallel.
-4
View File
@@ -144,8 +144,6 @@ export interface CreateAgentSessionOptions {
/** Skills. Default: discovered from multiple locations */
skills?: Skill[];
/** Skills to inline into the system prompt instead of listing available skills. */
preloadedSkills?: Skill[];
/** Rules. Default: discovered from multiple locations */
rules?: Rule[];
/** Context files (AGENTS.md content). Default: discovered walking up from cwd */
@@ -1128,7 +1126,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
const defaultPrompt = await buildSystemPromptInternal({
cwd,
skills,
preloadedSkills: options.preloadedSkills,
contextFiles,
tools,
toolNames,
@@ -1147,7 +1144,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
return await buildSystemPromptInternal({
cwd,
skills,
preloadedSkills: options.preloadedSkills,
contextFiles,
tools,
toolNames,
+3 -34
View File
@@ -16,24 +16,6 @@ import { loadSkills, type Skill } from "./extensibility/skills";
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
type PreloadedSkill = { name: string; content: string };
async function loadPreloadedSkillContents(preloadedSkills: Skill[]): Promise<PreloadedSkill[]> {
const contents = await Promise.all(
preloadedSkills.map(async skill => {
try {
const content = await Bun.file(skill.filePath).text();
return { name: skill.name, content };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to load skill "${skill.name}" from ${skill.filePath}: ${message}`);
}
}),
);
return contents;
}
function firstNonEmpty(...values: (string | undefined | null)[]): string | null {
for (const value of values) {
const trimmed = value?.trim();
@@ -350,11 +332,9 @@ export interface BuildSystemPromptOptions {
cwd?: string;
/** Pre-loaded context files (skips discovery if provided). */
contextFiles?: Array<{ path: string; content: string; depth?: number }>;
/** Pre-loaded skills (skips discovery if provided). */
/** Skills provided directly to system prompt construction. */
skills?: Skill[];
/** Skills to inline into the system prompt instead of listing available skills. */
preloadedSkills?: Skill[];
/** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
/** Pre-loaded rulebook rules (descriptions, excluding TTSR and always-apply). */
rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
/** Intent field name injected into every tool schema. If set, explains the field in the prompt. */
intentField?: string;
@@ -378,13 +358,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
cwd,
contextFiles: providedContextFiles,
skills: providedSkills,
preloadedSkills: providedPreloadedSkills,
rules,
intentField,
eagerTasks = false,
} = options;
const resolvedCwd = cwd ?? getProjectDir();
const preloadedSkills = providedPreloadedSkills;
const prepPromise = (() => {
const systemPromptCustomizationPromise = logger.timeAsync("loadSystemPromptFiles", loadSystemPromptFiles, {
@@ -400,9 +378,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
: skillsSettings?.enabled !== false
? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).then(result => result.skills)
: Promise.resolve([]);
const preloadedSkillContentsPromise = preloadedSkills
? logger.timeAsync("loadPreloadedSkills", loadPreloadedSkillContents, preloadedSkills)
: [];
return Promise.all([
resolvePromptInput(customPrompt, "system prompt"),
@@ -411,7 +386,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
contextFilesPromise,
agentsMdSearchPromise,
skillsPromise,
preloadedSkillContentsPromise,
]).then(
([
resolvedCustomPrompt,
@@ -420,7 +394,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
contextFiles,
agentsMdSearch,
skills,
preloadedSkillContents,
]) => ({
resolvedCustomPrompt,
resolvedAppendPrompt,
@@ -428,7 +401,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
contextFiles,
agentsMdSearch,
skills,
preloadedSkillContents,
}),
);
})();
@@ -451,7 +423,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
files: [],
};
let skills: Skill[] = providedSkills ?? [];
let preloadedSkillContents: PreloadedSkill[] = [];
if (prepResult.type === "timeout") {
logger.warn("System prompt preparation timed out; using minimal startup context", {
@@ -474,7 +445,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
contextFiles = prepResult.value.contextFiles;
agentsMdSearch = prepResult.value.agentsMdSearch;
skills = prepResult.value.skills;
preloadedSkillContents = prepResult.value.preloadedSkillContents;
}
const now = new Date();
@@ -517,7 +487,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
// Filter skills to only include those with read tool
const hasRead = tools?.has("read");
const filteredSkills = preloadedSkills === undefined && hasRead ? skills : [];
const filteredSkills = hasRead ? skills : [];
const environment = await logger.timeAsync("getEnvironmentInfo", getEnvironmentInfo);
const data = {
@@ -531,7 +501,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
contextFiles,
agentsMdSearch,
skills: filteredSkills,
preloadedSkills: preloadedSkillContents,
rules: rules ?? [],
date,
dateTime,
@@ -158,7 +158,6 @@ export interface ExecutorOptions {
eventBus?: EventBus;
contextFiles?: ContextFileEntry[];
skills?: Skill[];
preloadedSkills?: Skill[];
promptTemplates?: PromptTemplate[];
mcpManager?: MCPManager;
authStorage?: AuthStorage;
@@ -950,7 +949,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
requireSubmitResultTool: true,
contextFiles: options.contextFiles,
skills: options.skills,
preloadedSkills: options.preloadedSkills,
promptTemplates: options.promptTemplates,
systemPrompt: defaultPrompt =>
renderPromptTemplate(subagentSystemPromptTemplate, {
+8 -55
View File
@@ -690,58 +690,13 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
// Build full prompts with context prepended
const tasksWithContext = tasksWithUniqueIds.map(t => renderTemplate(context, t));
const availableSkills = [...(this.session.skills ?? [])];
const contextFiles = this.session.contextFiles;
const availableSkills = this.session.skills;
const availableSkillList = availableSkills ?? [];
const promptTemplates = this.session.promptTemplates;
const skillLookup = new Map(availableSkillList.map(skill => [skill.name, skill]));
const missingSkillsByTask: Array<{ id: string; missing: string[] }> = [];
const tasksWithSkills = tasksWithContext.map(task => {
if (task.skills === undefined) {
return { ...task, resolvedSkills: availableSkills, preloadedSkills: undefined };
}
const requested = task.skills;
const resolved = [] as typeof availableSkillList;
const missing: string[] = [];
const seen = new Set<string>();
for (const name of requested) {
const trimmed = name.trim();
if (!trimmed || seen.has(trimmed)) continue;
seen.add(trimmed);
const skill = skillLookup.get(trimmed);
if (skill) {
resolved.push(skill);
} else {
missing.push(trimmed);
}
}
if (missing.length > 0) {
missingSkillsByTask.push({ id: task.id, missing });
}
return { ...task, resolvedSkills: resolved, preloadedSkills: resolved };
});
if (missingSkillsByTask.length > 0) {
const available = availableSkillList.map(skill => skill.name).join(", ") || "none";
const details = missingSkillsByTask.map(entry => `${entry.id}: ${entry.missing.join(", ")}`).join("; ");
return {
content: [
{
type: "text",
text: `Unknown skills requested: ${details}. Available skills: ${available}`,
},
],
details: {
projectAgentsDir,
results: [],
totalDurationMs: Date.now() - startTime,
},
};
}
// Initialize progress for all tasks
for (let i = 0; i < tasksWithSkills.length; i++) {
const t = tasksWithSkills[i];
for (let i = 0; i < tasksWithContext.length; i++) {
const t = tasksWithContext[i];
progressMap.set(i, {
index: i,
id: t.id,
@@ -760,7 +715,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
}
emitProgress();
const runTask = async (task: (typeof tasksWithSkills)[number], index: number) => {
const runTask = async (task: (typeof tasksWithContext)[number], index: number) => {
if (!isIsolated) {
return runSubprocess({
cwd: this.session.cwd,
@@ -791,8 +746,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
settings: this.session.settings,
mcpManager: this.session.mcpManager,
contextFiles,
skills: task.resolvedSkills,
preloadedSkills: task.preloadedSkills,
skills: availableSkills,
promptTemplates,
});
}
@@ -842,8 +796,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
settings: this.session.settings,
mcpManager: this.session.mcpManager,
contextFiles,
skills: task.resolvedSkills,
preloadedSkills: task.preloadedSkills,
skills: availableSkills,
promptTemplates,
});
if (mergeMode === "branch" && result.exitCode === 0) {
@@ -927,7 +880,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
// Execute in parallel with concurrency limit
const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
tasksWithSkills,
tasksWithContext,
maxConcurrency,
runTask,
signal,
@@ -938,7 +891,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
if (result !== undefined) {
return result;
}
const task = tasksWithSkills[index];
const task = tasksWithContext[index];
return {
index,
id: task.id,
+2 -4
View File
@@ -7,7 +7,6 @@ interface RenderResult {
task: string;
id: string;
description: string;
skills?: string[];
}
/**
@@ -16,17 +15,16 @@ interface RenderResult {
* If context is provided, it is prepended with a separator.
*/
export function renderTemplate(context: string | undefined, task: TaskItem): RenderResult {
let { id, description, assignment, skills } = task;
let { id, description, assignment } = task;
assignment = assignment.trim();
context = context?.trim();
if (!context || !assignment) {
return { task: assignment || context!, id, description, skills };
return { task: assignment || context!, id, description };
}
return {
task: renderPromptTemplate(subagentUserPromptTemplate, { context, assignment }),
id,
description,
skills,
};
}
-5
View File
@@ -44,11 +44,6 @@ export const taskItemSchema = Type.Object({
description:
"Complete per-task instructions the subagent executes. Must follow the Target/Change/Edge Cases/Acceptance structure. Only include per-task deltas — shared background belongs in `context`.",
}),
skills: Type.Optional(
Type.Array(Type.String(), {
description: "Skill names to preload into the subagent. Use only where it changes correctness.",
}),
),
});
export type TaskItem = Static<typeof taskItemSchema>;
@@ -41,7 +41,6 @@ const baseRenderContext: TemplateContext = {
planContent: "1. Read code\n2. Add tests",
planExists: true,
planFilePath: "local://PLAN.md",
preloadedSkills: [{ name: "semantic-compression", content: "preloaded skill content" }],
readFiles: ["packages/coding-agent/src/prompts/system/custom-system-prompt.md"],
repeatToolDescriptions: true,
reentry: false,
@@ -43,14 +43,4 @@ describe("renderTemplate", () => {
});
expect(result.task).toBe("just the assignment");
});
test("passes through skills", () => {
const result = renderTemplate(undefined, {
id: "X",
description: "label",
assignment: "do stuff",
skills: ["react", "postgres"],
});
expect(result.skills).toEqual(["react", "postgres"]);
});
});