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:
+2
-9
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user