feat(coding-agent): added subagent skill preloads
- Inline preloaded skill contents in system prompts and allow task-level skill selection. - Record session init entries with system prompt, task, tools, and output schema. - Fix session persistence to keep pre-assistant entries in subagent logs.
This commit is contained in:
@@ -2,8 +2,13 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Added subagent support for preloading skill contents into the system prompt instead of listing available skills
|
||||
- Added session init entries to capture system prompt, task, tools, and output schema for subagent session logs
|
||||
|
||||
### Fixed
|
||||
- Reduced Task tool progress update overhead to keep the UI responsive during high-volume streaming output
|
||||
- Fixed subagent session logs dropping pre-assistant entries (user/task metadata) before the first assistant response
|
||||
|
||||
### Removed
|
||||
- Removed enter-plan-mode tool
|
||||
|
||||
@@ -43,6 +43,20 @@ Use the read tool to load a skill's file when the task matches its description.
|
||||
{{/list}}
|
||||
</available_skills>
|
||||
{{/if}}
|
||||
{{#if preloadedSkills.length}}
|
||||
The following skills are preloaded in full. Apply their instructions directly.
|
||||
|
||||
<preloaded_skills>
|
||||
{{#list preloadedSkills join="\n"}}
|
||||
<skill name="{{name}}">
|
||||
<location>skill://{{escapeXml name}}</location>
|
||||
<content>
|
||||
{{content}}
|
||||
</content>
|
||||
</skill>
|
||||
{{/list}}
|
||||
</preloaded_skills>
|
||||
{{/if}}
|
||||
{{#if rules.length}}
|
||||
The following rules define project-specific guidelines and constraints:
|
||||
|
||||
|
||||
@@ -264,6 +264,18 @@ If a skill covers what you're producing, read it before proceeding.
|
||||
{{/list}}
|
||||
</skills>
|
||||
{{/if}}
|
||||
{{#if preloadedSkills.length}}
|
||||
<preloaded_skills>
|
||||
The following skills are preloaded in full. Apply their instructions directly.
|
||||
|
||||
{{#list preloadedSkills join="\n"}}
|
||||
<skill name="{{name}}">
|
||||
<location>skill://{{escapeXml name}}</location>
|
||||
{{content}}
|
||||
</skill>
|
||||
{{/list}}
|
||||
</preloaded_skills>
|
||||
{{/if}}
|
||||
{{#if rules.length}}
|
||||
<rules>
|
||||
Rules are local constraints.
|
||||
|
||||
@@ -38,6 +38,7 @@ Agents with `output="structured"` have a fixed schema enforced via frontmatter;
|
||||
- `id`: Short CamelCase identifier (max 32 chars, e.g., "SessionStore", "LspRefactor")
|
||||
- `description`: Short human-readable description of what the task does
|
||||
- `args`: Object with keys matching `\{{placeholders}}` in context (always include this, even if empty)
|
||||
- `skills`: (optional) Array of skill names to preload into this task's system prompt. When set, the skills index section is omitted and the full SKILL.md contents are embedded.
|
||||
- `output`: (optional) JTD schema for structured subagent output (used by the complete tool)
|
||||
</parameters>
|
||||
|
||||
|
||||
@@ -156,6 +156,8 @@ 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[];
|
||||
/** Context files (AGENTS.md content). Default: discovered walking up from cwd */
|
||||
contextFiles?: Array<{ path: string; content: string }>;
|
||||
/** Prompt templates. Default: discovered from cwd/.omp/prompts/ + agentDir/prompts/ */
|
||||
@@ -993,6 +995,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
const defaultPrompt = await buildSystemPromptInternal({
|
||||
cwd,
|
||||
skills,
|
||||
preloadedSkills: options.preloadedSkills,
|
||||
contextFiles,
|
||||
tools,
|
||||
toolNames,
|
||||
@@ -1007,6 +1010,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
return await buildSystemPromptInternal({
|
||||
cwd,
|
||||
skills,
|
||||
preloadedSkills: options.preloadedSkills,
|
||||
contextFiles,
|
||||
tools,
|
||||
toolNames,
|
||||
|
||||
@@ -110,6 +110,19 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
|
||||
injectedRules: string[];
|
||||
}
|
||||
|
||||
/** Session init entry - captures initial context for subagent sessions (debugging/replay). */
|
||||
export interface SessionInitEntry extends SessionEntryBase {
|
||||
type: "session_init";
|
||||
/** Full system prompt sent to the model */
|
||||
systemPrompt: string;
|
||||
/** Initial task/user message */
|
||||
task: string;
|
||||
/** Tools available to the agent */
|
||||
tools: string[];
|
||||
/** Output schema if structured output was requested */
|
||||
outputSchema?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom message entry for extensions to inject messages into LLM context.
|
||||
* Use customType to identify your extension's entries.
|
||||
@@ -140,7 +153,8 @@ export type SessionEntry =
|
||||
| CustomEntry
|
||||
| CustomMessageEntry
|
||||
| LabelEntry
|
||||
| TtsrInjectionEntry;
|
||||
| TtsrInjectionEntry
|
||||
| SessionInitEntry;
|
||||
|
||||
/** Raw file entry (includes header) */
|
||||
export type FileEntry = SessionHeader | SessionEntry;
|
||||
@@ -1263,7 +1277,7 @@ export class SessionManager {
|
||||
if (this.persistError) throw this.persistError;
|
||||
|
||||
const hasAssistant = this.fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
|
||||
if (!hasAssistant) return;
|
||||
if (!hasAssistant && !this.flushed) return;
|
||||
|
||||
if (!this.flushed) {
|
||||
this.flushed = true;
|
||||
@@ -1368,6 +1382,19 @@ export class SessionManager {
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
/** Append session init metadata (for subagent debugging/replay). Returns entry id. */
|
||||
appendSessionInit(init: { systemPrompt: string; task: string; tools: string[]; outputSchema?: unknown }): string {
|
||||
const entry: SessionInitEntry = {
|
||||
type: "session_init",
|
||||
id: generateId(this.byId),
|
||||
parentId: this.leafId,
|
||||
timestamp: new Date().toISOString(),
|
||||
...init,
|
||||
};
|
||||
this._appendEntry(entry);
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
/** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
|
||||
appendCompaction<T = unknown>(
|
||||
summary: string,
|
||||
|
||||
@@ -23,6 +23,24 @@ interface GitContext {
|
||||
commits: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load git context for the system prompt.
|
||||
* Returns structured git data or null if not in a git repo.
|
||||
@@ -643,6 +661,8 @@ export interface BuildSystemPromptOptions {
|
||||
contextFiles?: Array<{ path: string; content: string; depth?: number }>;
|
||||
/** Pre-loaded skills (skips discovery if provided). */
|
||||
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). */
|
||||
rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
|
||||
}
|
||||
@@ -662,6 +682,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
||||
cwd,
|
||||
contextFiles: providedContextFiles,
|
||||
skills: providedSkills,
|
||||
preloadedSkills: providedPreloadedSkills,
|
||||
rules,
|
||||
} = options;
|
||||
const resolvedCwd = cwd ?? process.cwd();
|
||||
@@ -707,13 +728,15 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
||||
const skills =
|
||||
providedSkills ??
|
||||
(skillsSettings?.enabled !== false ? (await loadSkills({ ...skillsSettings, cwd: resolvedCwd })).skills : []);
|
||||
const preloadedSkills = providedPreloadedSkills;
|
||||
const preloadedSkillContents = preloadedSkills ? await loadPreloadedSkillContents(preloadedSkills) : [];
|
||||
|
||||
// Get git context
|
||||
const git = await loadGitContext(resolvedCwd);
|
||||
|
||||
// Filter skills to only include those with read tool
|
||||
const hasRead = tools?.has("read");
|
||||
const filteredSkills = hasRead ? skills : [];
|
||||
const filteredSkills = preloadedSkills === undefined && hasRead ? skills : [];
|
||||
|
||||
if (resolvedCustomPrompt) {
|
||||
return renderPromptTemplate(customSystemPromptTemplate, {
|
||||
@@ -724,6 +747,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
||||
agentsMdSearch,
|
||||
git,
|
||||
skills: filteredSkills,
|
||||
preloadedSkills: preloadedSkillContents,
|
||||
rules: rules ?? [],
|
||||
dateTime,
|
||||
cwd: resolvedCwd,
|
||||
@@ -738,6 +762,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
||||
agentsMdSearch,
|
||||
git,
|
||||
skills: filteredSkills,
|
||||
preloadedSkills: preloadedSkillContents,
|
||||
rules: rules ?? [],
|
||||
dateTime,
|
||||
cwd: resolvedCwd,
|
||||
|
||||
@@ -75,6 +75,7 @@ export interface ExecutorOptions {
|
||||
eventBus?: EventBus;
|
||||
contextFiles?: ContextFileEntry[];
|
||||
skills?: Skill[];
|
||||
preloadedSkills?: Skill[];
|
||||
promptTemplates?: PromptTemplate[];
|
||||
mcpManager?: MCPManager;
|
||||
authStorage?: AuthStorage;
|
||||
@@ -804,6 +805,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
pythonPreludeDocs: pythonPreludeDocsPayload,
|
||||
contextFiles: options.contextFiles,
|
||||
skills: options.skills,
|
||||
preloadedSkills: options.preloadedSkills,
|
||||
promptTemplates: options.promptTemplates,
|
||||
mcpTools: options.mcpManager ? extractMCPToolMetadata(options.mcpManager) : undefined,
|
||||
pythonToolProxy: pythonProxyEnabled,
|
||||
|
||||
@@ -391,12 +391,57 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
||||
// Build full prompts with context prepended
|
||||
const tasksWithContext = tasksWithUniqueIds.map(t => renderTemplate(context, t));
|
||||
const contextFiles = this.session.contextFiles;
|
||||
const skills = this.session.skills;
|
||||
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 < tasksWithContext.length; i++) {
|
||||
const t = tasksWithContext[i];
|
||||
for (let i = 0; i < tasksWithSkills.length; i++) {
|
||||
const t = tasksWithSkills[i];
|
||||
progressMap.set(i, {
|
||||
index: i,
|
||||
id: t.id,
|
||||
@@ -416,7 +461,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
||||
}
|
||||
emitProgress();
|
||||
|
||||
const runTask = async (task: (typeof tasksWithContext)[number], index: number) => {
|
||||
const runTask = async (task: (typeof tasksWithSkills)[number], index: number) => {
|
||||
if (!isIsolated) {
|
||||
return runSubprocess({
|
||||
cwd: this.session.cwd,
|
||||
@@ -437,8 +482,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
||||
eventBus: undefined,
|
||||
onProgress: progress => {
|
||||
progressMap.set(index, {
|
||||
...progress,
|
||||
args: tasksWithContext[index]?.args,
|
||||
...structuredClone(progress),
|
||||
args: tasksWithSkills[index]?.args,
|
||||
});
|
||||
emitProgress();
|
||||
},
|
||||
@@ -447,7 +492,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
||||
settingsManager: this.session.settingsManager,
|
||||
mcpManager: this.session.mcpManager,
|
||||
contextFiles,
|
||||
skills,
|
||||
skills: task.resolvedSkills,
|
||||
preloadedSkills: task.preloadedSkills,
|
||||
promptTemplates,
|
||||
});
|
||||
}
|
||||
@@ -480,8 +526,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
||||
eventBus: undefined,
|
||||
onProgress: progress => {
|
||||
progressMap.set(index, {
|
||||
...progress,
|
||||
args: tasksWithContext[index]?.args,
|
||||
...structuredClone(progress),
|
||||
args: tasksWithSkills[index]?.args,
|
||||
});
|
||||
emitProgress();
|
||||
},
|
||||
@@ -490,7 +536,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
||||
settingsManager: this.session.settingsManager,
|
||||
mcpManager: this.session.mcpManager,
|
||||
contextFiles,
|
||||
skills,
|
||||
skills: task.resolvedSkills,
|
||||
preloadedSkills: task.preloadedSkills,
|
||||
promptTemplates,
|
||||
});
|
||||
const patch = await captureDeltaPatch(worktreeDir, baseline);
|
||||
@@ -527,7 +574,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
||||
|
||||
// Execute in parallel with concurrency limit
|
||||
const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
|
||||
tasksWithContext,
|
||||
tasksWithSkills,
|
||||
MAX_CONCURRENCY,
|
||||
runTask,
|
||||
signal,
|
||||
@@ -538,10 +585,10 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
||||
if (result !== undefined) {
|
||||
return {
|
||||
...result,
|
||||
args: tasksWithContext[index]?.args,
|
||||
args: tasksWithSkills[index]?.args,
|
||||
};
|
||||
}
|
||||
const task = tasksWithContext[index];
|
||||
const task = tasksWithSkills[index];
|
||||
return {
|
||||
index,
|
||||
id: task.id,
|
||||
|
||||
@@ -5,10 +5,11 @@ type RenderResult = {
|
||||
args: Record<string, string>;
|
||||
id: string;
|
||||
description: string;
|
||||
skills?: string[];
|
||||
};
|
||||
|
||||
export function renderTemplate(template: string, task: TaskItem): RenderResult {
|
||||
const { id, description, args } = task;
|
||||
const { id, description, args, skills } = task;
|
||||
|
||||
let usedPlaceholder = false;
|
||||
const unknownArguments: string[] = [];
|
||||
@@ -43,5 +44,6 @@ export function renderTemplate(template: string, task: TaskItem): RenderResult {
|
||||
args: { id, description, ...args },
|
||||
id,
|
||||
description,
|
||||
skills,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,6 +49,11 @@ export const taskItemSchema = Type.Object({
|
||||
description: "Arguments to fill {{placeholders}} in context",
|
||||
}),
|
||||
),
|
||||
skills: Type.Optional(
|
||||
Type.Array(Type.String(), {
|
||||
description: "Skill names to preload into the subagent system prompt",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type TaskItem = Static<typeof taskItemSchema>;
|
||||
|
||||
@@ -107,6 +107,7 @@ export interface SubagentWorkerStartPayload {
|
||||
pythonPreludeDocs?: PreludeHelper[];
|
||||
contextFiles?: ContextFileEntry[];
|
||||
skills?: Skill[];
|
||||
preloadedSkills?: Skill[];
|
||||
promptTemplates?: PromptTemplate[];
|
||||
mcpTools?: MCPToolMetadata[];
|
||||
pythonToolProxy?: boolean;
|
||||
|
||||
@@ -608,6 +608,7 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
|
||||
requireCompleteTool: true,
|
||||
contextFiles: payload.contextFiles,
|
||||
skills: payload.skills,
|
||||
preloadedSkills: payload.preloadedSkills,
|
||||
promptTemplates: payload.promptTemplates,
|
||||
// Append system prompt (equivalent to CLI's --append-system-prompt)
|
||||
systemPrompt: defaultPrompt =>
|
||||
@@ -627,6 +628,14 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
|
||||
runState.session = session;
|
||||
checkAbort();
|
||||
|
||||
// Write session init metadata for debugging/replay
|
||||
session.sessionManager.appendSessionInit({
|
||||
systemPrompt: session.agent.state.systemPrompt,
|
||||
task: payload.task,
|
||||
tools: session.getAllToolNames(),
|
||||
outputSchema: payload.outputSchema,
|
||||
});
|
||||
|
||||
signal.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
|
||||
Reference in New Issue
Block a user