diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 34230fa70..4b6c20343 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/prompts/system/custom-system-prompt.md b/packages/coding-agent/src/prompts/system/custom-system-prompt.md index 72ab8250f..6f7a13efb 100644 --- a/packages/coding-agent/src/prompts/system/custom-system-prompt.md +++ b/packages/coding-agent/src/prompts/system/custom-system-prompt.md @@ -43,6 +43,20 @@ Use the read tool to load a skill's file when the task matches its description. {{/list}} {{/if}} +{{#if preloadedSkills.length}} +The following skills are preloaded in full. Apply their instructions directly. + + +{{#list preloadedSkills join="\n"}} + +skill://{{escapeXml name}} + +{{content}} + + +{{/list}} + +{{/if}} {{#if rules.length}} The following rules define project-specific guidelines and constraints: diff --git a/packages/coding-agent/src/prompts/system/system-prompt.md b/packages/coding-agent/src/prompts/system/system-prompt.md index 9e468a832..da877e145 100644 --- a/packages/coding-agent/src/prompts/system/system-prompt.md +++ b/packages/coding-agent/src/prompts/system/system-prompt.md @@ -264,6 +264,18 @@ If a skill covers what you're producing, read it before proceeding. {{/list}} {{/if}} +{{#if preloadedSkills.length}} + +The following skills are preloaded in full. Apply their instructions directly. + +{{#list preloadedSkills join="\n"}} + +skill://{{escapeXml name}} +{{content}} + +{{/list}} + +{{/if}} {{#if rules.length}} Rules are local constraints. diff --git a/packages/coding-agent/src/prompts/tools/task.md b/packages/coding-agent/src/prompts/tools/task.md index 3036b92b0..35046b541 100644 --- a/packages/coding-agent/src/prompts/tools/task.md +++ b/packages/coding-agent/src/prompts/tools/task.md @@ -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) diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 728eb0e6f..4653ef8ee 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -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, diff --git a/packages/coding-agent/src/session/session-manager.ts b/packages/coding-agent/src/session/session-manager.ts index d92d0c5e5..dfc72ef79 100644 --- a/packages/coding-agent/src/session/session-manager.ts +++ b/packages/coding-agent/src/session/session-manager.ts @@ -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( summary: string, diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts index 0c1e26717..7e7ec2a0b 100644 --- a/packages/coding-agent/src/system-prompt.ts +++ b/packages/coding-agent/src/system-prompt.ts @@ -23,6 +23,24 @@ interface GitContext { commits: string; } +type PreloadedSkill = { name: string; content: string }; + +async function loadPreloadedSkillContents(preloadedSkills: Skill[]): Promise { + 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, diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts index fe63a550d..3fd93c8ff 100644 --- a/packages/coding-agent/src/task/executor.ts +++ b/packages/coding-agent/src/task/executor.ts @@ -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 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(); + 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 { + 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 { progressMap.set(index, { - ...progress, - args: tasksWithContext[index]?.args, + ...structuredClone(progress), + args: tasksWithSkills[index]?.args, }); emitProgress(); }, @@ -447,7 +492,8 @@ export class TaskTool implements AgentTool { progressMap.set(index, { - ...progress, - args: tasksWithContext[index]?.args, + ...structuredClone(progress), + args: tasksWithSkills[index]?.args, }); emitProgress(); }, @@ -490,7 +536,8 @@ export class TaskTool implements AgentTool; 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, }; } diff --git a/packages/coding-agent/src/task/types.ts b/packages/coding-agent/src/task/types.ts index a1cf6a14f..7e727ef3d 100644 --- a/packages/coding-agent/src/task/types.ts +++ b/packages/coding-agent/src/task/types.ts @@ -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; diff --git a/packages/coding-agent/src/task/worker-protocol.ts b/packages/coding-agent/src/task/worker-protocol.ts index 11e586179..289141998 100644 --- a/packages/coding-agent/src/task/worker-protocol.ts +++ b/packages/coding-agent/src/task/worker-protocol.ts @@ -107,6 +107,7 @@ export interface SubagentWorkerStartPayload { pythonPreludeDocs?: PreludeHelper[]; contextFiles?: ContextFileEntry[]; skills?: Skill[]; + preloadedSkills?: Skill[]; promptTemplates?: PromptTemplate[]; mcpTools?: MCPToolMetadata[]; pythonToolProxy?: boolean; diff --git a/packages/coding-agent/src/task/worker.ts b/packages/coding-agent/src/task/worker.ts index 85774c1b6..431b96709 100644 --- a/packages/coding-agent/src/task/worker.ts +++ b/packages/coding-agent/src/task/worker.ts @@ -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", () => {