@oh-my-pi/pi-coding-agent 8.4.5 → 8.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [8.5.0] - 2026-01-27
6
+
7
+ ### Added
8
+ - Added subagent support for preloading skill contents into the system prompt instead of listing available skills
9
+ - Added session init entries to capture system prompt, task, tools, and output schema for subagent session logs
10
+
11
+ ### Fixed
12
+ - Reduced Task tool progress update overhead to keep the UI responsive during high-volume streaming output
13
+ - Fixed subagent session logs dropping pre-assistant entries (user/task metadata) before the first assistant response
14
+
15
+ ### Removed
16
+ - Removed enter-plan-mode tool
5
17
  ## [8.4.5] - 2026-01-26
6
18
 
7
19
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "8.4.5",
3
+ "version": "8.5.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -83,11 +83,11 @@
83
83
  "test": "bun test"
84
84
  },
85
85
  "dependencies": {
86
- "@oh-my-pi/omp-stats": "8.4.5",
87
- "@oh-my-pi/pi-agent-core": "8.4.5",
88
- "@oh-my-pi/pi-ai": "8.4.5",
89
- "@oh-my-pi/pi-tui": "8.4.5",
90
- "@oh-my-pi/pi-utils": "8.4.5",
86
+ "@oh-my-pi/omp-stats": "8.5.0",
87
+ "@oh-my-pi/pi-agent-core": "8.5.0",
88
+ "@oh-my-pi/pi-ai": "8.5.0",
89
+ "@oh-my-pi/pi-tui": "8.5.0",
90
+ "@oh-my-pi/pi-utils": "8.5.0",
91
91
  "@openai/agents": "^0.4.3",
92
92
  "@sinclair/typebox": "^0.34.46",
93
93
  "ajv": "^8.17.1",
@@ -247,12 +247,6 @@ export class EventController {
247
247
  this.ctx.setTodos(details.todos);
248
248
  }
249
249
  }
250
- if (event.toolName === "enter_plan_mode" && !event.isError) {
251
- const details = event.result.details as import("../../tools").EnterPlanModeDetails | undefined;
252
- if (details) {
253
- await this.ctx.handleEnterPlanModeTool(details);
254
- }
255
- }
256
250
  if (event.toolName === "exit_plan_mode" && !event.isError) {
257
251
  const details = event.result.details as ExitPlanModeDetails | undefined;
258
252
  if (details) {
@@ -29,7 +29,7 @@ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
29
29
  import { HistoryStorage } from "../session/history-storage";
30
30
  import type { SessionContext, SessionManager } from "../session/session-manager";
31
31
  import { getRecentSessions } from "../session/session-manager";
32
- import type { EnterPlanModeDetails, ExitPlanModeDetails } from "../tools";
32
+ import type { ExitPlanModeDetails } from "../tools";
33
33
  import { setTerminalTitle } from "../utils/title-generator";
34
34
  import type { AssistantMessageComponent } from "./components/assistant-message";
35
35
  import type { BashExecutionComponent } from "./components/bash-execution";
@@ -631,25 +631,6 @@ export class InteractiveMode implements InteractiveModeContext {
631
631
  await this.enterPlanMode();
632
632
  }
633
633
 
634
- async handleEnterPlanModeTool(details: EnterPlanModeDetails): Promise<void> {
635
- if (this.planModeEnabled) {
636
- this.showWarning("Plan mode is already active.");
637
- return;
638
- }
639
-
640
- const confirmed = await this.showHookConfirm(
641
- "Enter plan mode?",
642
- "This enables read-only planning and creates a plan file for approval.",
643
- );
644
- if (!confirmed) {
645
- return;
646
- }
647
-
648
- const planFilePath = details.planFilePath || this.getPlanFilePath();
649
- this.planModePlanFilePath = planFilePath;
650
- await this.enterPlanMode({ planFilePath, workflow: details.workflow });
651
- }
652
-
653
634
  async handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void> {
654
635
  if (!this.planModeEnabled) {
655
636
  this.showWarning("Plan mode is not active.");
@@ -178,7 +178,6 @@ export interface InteractiveModeContext {
178
178
  openExternalEditor(): void;
179
179
  registerExtensionShortcuts(): void;
180
180
  handlePlanModeCommand(): Promise<void>;
181
- handleEnterPlanModeTool(details: import("../tools").EnterPlanModeDetails): Promise<void>;
182
181
  handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void>;
183
182
 
184
183
  // Hook UI methods
@@ -43,6 +43,20 @@ Use the read tool to load a skill's file when the task matches its description.
43
43
  {{/list}}
44
44
  </available_skills>
45
45
  {{/if}}
46
+ {{#if preloadedSkills.length}}
47
+ The following skills are preloaded in full. Apply their instructions directly.
48
+
49
+ <preloaded_skills>
50
+ {{#list preloadedSkills join="\n"}}
51
+ <skill name="{{name}}">
52
+ <location>skill://{{escapeXml name}}</location>
53
+ <content>
54
+ {{content}}
55
+ </content>
56
+ </skill>
57
+ {{/list}}
58
+ </preloaded_skills>
59
+ {{/if}}
46
60
  {{#if rules.length}}
47
61
  The following rules define project-specific guidelines and constraints:
48
62
 
@@ -264,6 +264,18 @@ If a skill covers what you're producing, read it before proceeding.
264
264
  {{/list}}
265
265
  </skills>
266
266
  {{/if}}
267
+ {{#if preloadedSkills.length}}
268
+ <preloaded_skills>
269
+ The following skills are preloaded in full. Apply their instructions directly.
270
+
271
+ {{#list preloadedSkills join="\n"}}
272
+ <skill name="{{name}}">
273
+ <location>skill://{{escapeXml name}}</location>
274
+ {{content}}
275
+ </skill>
276
+ {{/list}}
277
+ </preloaded_skills>
278
+ {{/if}}
267
279
  {{#if rules.length}}
268
280
  <rules>
269
281
  Rules are local constraints.
@@ -38,6 +38,7 @@ Agents with `output="structured"` have a fixed schema enforced via frontmatter;
38
38
  - `id`: Short CamelCase identifier (max 32 chars, e.g., "SessionStore", "LspRefactor")
39
39
  - `description`: Short human-readable description of what the task does
40
40
  - `args`: Object with keys matching `\{{placeholders}}` in context (always include this, even if empty)
41
+ - `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.
41
42
  - `output`: (optional) JTD schema for structured subagent output (used by the complete tool)
42
43
  </parameters>
43
44
 
package/src/sdk.ts CHANGED
@@ -156,6 +156,8 @@ export interface CreateAgentSessionOptions {
156
156
 
157
157
  /** Skills. Default: discovered from multiple locations */
158
158
  skills?: Skill[];
159
+ /** Skills to inline into the system prompt instead of listing available skills. */
160
+ preloadedSkills?: Skill[];
159
161
  /** Context files (AGENTS.md content). Default: discovered walking up from cwd */
160
162
  contextFiles?: Array<{ path: string; content: string }>;
161
163
  /** Prompt templates. Default: discovered from cwd/.omp/prompts/ + agentDir/prompts/ */
@@ -993,6 +995,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
993
995
  const defaultPrompt = await buildSystemPromptInternal({
994
996
  cwd,
995
997
  skills,
998
+ preloadedSkills: options.preloadedSkills,
996
999
  contextFiles,
997
1000
  tools,
998
1001
  toolNames,
@@ -1007,6 +1010,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1007
1010
  return await buildSystemPromptInternal({
1008
1011
  cwd,
1009
1012
  skills,
1013
+ preloadedSkills: options.preloadedSkills,
1010
1014
  contextFiles,
1011
1015
  tools,
1012
1016
  toolNames,
@@ -110,6 +110,19 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
110
110
  injectedRules: string[];
111
111
  }
112
112
 
113
+ /** Session init entry - captures initial context for subagent sessions (debugging/replay). */
114
+ export interface SessionInitEntry extends SessionEntryBase {
115
+ type: "session_init";
116
+ /** Full system prompt sent to the model */
117
+ systemPrompt: string;
118
+ /** Initial task/user message */
119
+ task: string;
120
+ /** Tools available to the agent */
121
+ tools: string[];
122
+ /** Output schema if structured output was requested */
123
+ outputSchema?: unknown;
124
+ }
125
+
113
126
  /**
114
127
  * Custom message entry for extensions to inject messages into LLM context.
115
128
  * Use customType to identify your extension's entries.
@@ -140,7 +153,8 @@ export type SessionEntry =
140
153
  | CustomEntry
141
154
  | CustomMessageEntry
142
155
  | LabelEntry
143
- | TtsrInjectionEntry;
156
+ | TtsrInjectionEntry
157
+ | SessionInitEntry;
144
158
 
145
159
  /** Raw file entry (includes header) */
146
160
  export type FileEntry = SessionHeader | SessionEntry;
@@ -1263,7 +1277,7 @@ export class SessionManager {
1263
1277
  if (this.persistError) throw this.persistError;
1264
1278
 
1265
1279
  const hasAssistant = this.fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
1266
- if (!hasAssistant) return;
1280
+ if (!hasAssistant && !this.flushed) return;
1267
1281
 
1268
1282
  if (!this.flushed) {
1269
1283
  this.flushed = true;
@@ -1368,6 +1382,19 @@ export class SessionManager {
1368
1382
  return entry.id;
1369
1383
  }
1370
1384
 
1385
+ /** Append session init metadata (for subagent debugging/replay). Returns entry id. */
1386
+ appendSessionInit(init: { systemPrompt: string; task: string; tools: string[]; outputSchema?: unknown }): string {
1387
+ const entry: SessionInitEntry = {
1388
+ type: "session_init",
1389
+ id: generateId(this.byId),
1390
+ parentId: this.leafId,
1391
+ timestamp: new Date().toISOString(),
1392
+ ...init,
1393
+ };
1394
+ this._appendEntry(entry);
1395
+ return entry.id;
1396
+ }
1397
+
1371
1398
  /** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
1372
1399
  appendCompaction<T = unknown>(
1373
1400
  summary: string,
@@ -23,6 +23,24 @@ interface GitContext {
23
23
  commits: string;
24
24
  }
25
25
 
26
+ type PreloadedSkill = { name: string; content: string };
27
+
28
+ async function loadPreloadedSkillContents(preloadedSkills: Skill[]): Promise<PreloadedSkill[]> {
29
+ const contents = await Promise.all(
30
+ preloadedSkills.map(async skill => {
31
+ try {
32
+ const content = await Bun.file(skill.filePath).text();
33
+ return { name: skill.name, content };
34
+ } catch (err) {
35
+ const message = err instanceof Error ? err.message : String(err);
36
+ throw new Error(`Failed to load skill "${skill.name}" from ${skill.filePath}: ${message}`);
37
+ }
38
+ }),
39
+ );
40
+
41
+ return contents;
42
+ }
43
+
26
44
  /**
27
45
  * Load git context for the system prompt.
28
46
  * Returns structured git data or null if not in a git repo.
@@ -643,6 +661,8 @@ export interface BuildSystemPromptOptions {
643
661
  contextFiles?: Array<{ path: string; content: string; depth?: number }>;
644
662
  /** Pre-loaded skills (skips discovery if provided). */
645
663
  skills?: Skill[];
664
+ /** Skills to inline into the system prompt instead of listing available skills. */
665
+ preloadedSkills?: Skill[];
646
666
  /** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
647
667
  rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
648
668
  }
@@ -662,6 +682,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
662
682
  cwd,
663
683
  contextFiles: providedContextFiles,
664
684
  skills: providedSkills,
685
+ preloadedSkills: providedPreloadedSkills,
665
686
  rules,
666
687
  } = options;
667
688
  const resolvedCwd = cwd ?? process.cwd();
@@ -707,13 +728,15 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
707
728
  const skills =
708
729
  providedSkills ??
709
730
  (skillsSettings?.enabled !== false ? (await loadSkills({ ...skillsSettings, cwd: resolvedCwd })).skills : []);
731
+ const preloadedSkills = providedPreloadedSkills;
732
+ const preloadedSkillContents = preloadedSkills ? await loadPreloadedSkillContents(preloadedSkills) : [];
710
733
 
711
734
  // Get git context
712
735
  const git = await loadGitContext(resolvedCwd);
713
736
 
714
737
  // Filter skills to only include those with read tool
715
738
  const hasRead = tools?.has("read");
716
- const filteredSkills = hasRead ? skills : [];
739
+ const filteredSkills = preloadedSkills === undefined && hasRead ? skills : [];
717
740
 
718
741
  if (resolvedCustomPrompt) {
719
742
  return renderPromptTemplate(customSystemPromptTemplate, {
@@ -724,6 +747,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
724
747
  agentsMdSearch,
725
748
  git,
726
749
  skills: filteredSkills,
750
+ preloadedSkills: preloadedSkillContents,
727
751
  rules: rules ?? [],
728
752
  dateTime,
729
753
  cwd: resolvedCwd,
@@ -738,6 +762,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
738
762
  agentsMdSearch,
739
763
  git,
740
764
  skills: filteredSkills,
765
+ preloadedSkills: preloadedSkillContents,
741
766
  rules: rules ?? [],
742
767
  dateTime,
743
768
  cwd: resolvedCwd,
@@ -75,6 +75,7 @@ export interface ExecutorOptions {
75
75
  eventBus?: EventBus;
76
76
  contextFiles?: ContextFileEntry[];
77
77
  skills?: Skill[];
78
+ preloadedSkills?: Skill[];
78
79
  promptTemplates?: PromptTemplate[];
79
80
  mcpManager?: MCPManager;
80
81
  authStorage?: AuthStorage;
@@ -361,6 +362,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
361
362
 
362
363
  const outputChunks: string[] = [];
363
364
  const finalOutputChunks: string[] = [];
365
+ const RECENT_OUTPUT_TAIL_BYTES = 8 * 1024;
366
+ let recentOutputTail = "";
364
367
  let stderr = "";
365
368
  let resolved = false;
366
369
  type AbortReason = "signal" | "terminate";
@@ -513,7 +516,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
513
516
  signal.addEventListener("abort", onAbort, { once: true, signal: listenerSignal });
514
517
  }
515
518
 
516
- const emitProgress = () => {
519
+ const PROGRESS_COALESCE_MS = 150;
520
+ let lastProgressEmitMs = 0;
521
+ let progressTimeoutId: ReturnType<typeof setTimeout> | null = null;
522
+
523
+ const emitProgressNow = () => {
517
524
  progress.durationMs = Date.now() - startTime;
518
525
  onProgress?.({ ...progress });
519
526
  if (options.eventBus) {
@@ -525,6 +532,33 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
525
532
  progress: { ...progress },
526
533
  });
527
534
  }
535
+ lastProgressEmitMs = Date.now();
536
+ };
537
+
538
+ const scheduleProgress = (flush = false) => {
539
+ if (flush) {
540
+ if (progressTimeoutId) {
541
+ clearTimeout(progressTimeoutId);
542
+ progressTimeoutId = null;
543
+ }
544
+ emitProgressNow();
545
+ return;
546
+ }
547
+ const now = Date.now();
548
+ const elapsed = now - lastProgressEmitMs;
549
+ if (lastProgressEmitMs === 0 || elapsed >= PROGRESS_COALESCE_MS) {
550
+ if (progressTimeoutId) {
551
+ clearTimeout(progressTimeoutId);
552
+ progressTimeoutId = null;
553
+ }
554
+ emitProgressNow();
555
+ return;
556
+ }
557
+ if (progressTimeoutId) return;
558
+ progressTimeoutId = setTimeout(() => {
559
+ progressTimeoutId = null;
560
+ emitProgressNow();
561
+ }, PROGRESS_COALESCE_MS - elapsed);
528
562
  };
529
563
 
530
564
  const getMessageContent = (message: unknown): unknown => {
@@ -541,6 +575,40 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
541
575
  return undefined;
542
576
  };
543
577
 
578
+ const updateRecentOutputLines = () => {
579
+ const lines = recentOutputTail.split("\n").filter(line => line.trim());
580
+ progress.recentOutput = lines.slice(-8).reverse();
581
+ };
582
+
583
+ const appendRecentOutputTail = (text: string) => {
584
+ if (!text) return;
585
+ recentOutputTail += text;
586
+ if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
587
+ recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
588
+ }
589
+ updateRecentOutputLines();
590
+ };
591
+
592
+ const replaceRecentOutputFromContent = (content: unknown[]) => {
593
+ recentOutputTail = "";
594
+ for (const block of content) {
595
+ if (!block || typeof block !== "object") continue;
596
+ const record = block as { type?: unknown; text?: unknown };
597
+ if (record.type !== "text" || typeof record.text !== "string") continue;
598
+ if (!record.text) continue;
599
+ recentOutputTail += record.text;
600
+ if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
601
+ recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
602
+ }
603
+ }
604
+ updateRecentOutputLines();
605
+ };
606
+
607
+ const resetRecentOutput = () => {
608
+ recentOutputTail = "";
609
+ progress.recentOutput = [];
610
+ };
611
+
544
612
  const processEvent = (event: AgentEvent) => {
545
613
  if (resolved) return;
546
614
 
@@ -555,8 +623,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
555
623
  }
556
624
 
557
625
  const now = Date.now();
626
+ let flushProgress = false;
558
627
 
559
628
  switch (event.type) {
629
+ case "message_start":
630
+ if (event.message?.role === "assistant") {
631
+ resetRecentOutput();
632
+ }
633
+ break;
634
+
560
635
  case "tool_execution_start":
561
636
  progress.toolCount++;
562
637
  progress.currentTool = event.toolName;
@@ -616,23 +691,28 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
616
691
  schedulePendingTermination();
617
692
  }
618
693
  }
694
+ flushProgress = true;
619
695
  break;
620
696
  }
621
697
 
622
698
  case "message_update": {
623
- // Extract text for progress display only (replace, don't accumulate)
699
+ if (event.message?.role !== "assistant") break;
700
+ const assistantEvent = (
701
+ event as AgentEvent & {
702
+ assistantMessageEvent?: { type?: string; delta?: string };
703
+ }
704
+ ).assistantMessageEvent;
705
+ if (assistantEvent?.type === "text_delta" && typeof assistantEvent.delta === "string") {
706
+ appendRecentOutputTail(assistantEvent.delta);
707
+ break;
708
+ }
709
+ if (assistantEvent && assistantEvent.type !== "text_delta") {
710
+ break;
711
+ }
624
712
  const updateContent =
625
713
  getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
626
714
  if (updateContent && Array.isArray(updateContent)) {
627
- const allText: string[] = [];
628
- for (const block of updateContent) {
629
- if (block.type === "text" && block.text) {
630
- const lines = block.text.split("\n").filter((l: string) => l.trim());
631
- allText.push(...lines);
632
- }
633
- }
634
- // Show last 8 lines from current state (not accumulated)
635
- progress.recentOutput = allText.slice(-8).reverse();
715
+ replaceRecentOutputFromContent(updateContent);
636
716
  }
637
717
  break;
638
718
  }
@@ -698,10 +778,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
698
778
  }
699
779
  }
700
780
  }
781
+ flushProgress = true;
701
782
  break;
702
783
  }
703
784
 
704
- emitProgress();
785
+ scheduleProgress(flushProgress);
705
786
  };
706
787
 
707
788
  const startMessage: SubagentWorkerRequest = {
@@ -724,6 +805,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
724
805
  pythonPreludeDocs: pythonPreludeDocsPayload,
725
806
  contextFiles: options.contextFiles,
726
807
  skills: options.skills,
808
+ preloadedSkills: options.preloadedSkills,
727
809
  promptTemplates: options.promptTemplates,
728
810
  mcpTools: options.mcpManager ? extractMCPToolMetadata(options.mcpManager) : undefined,
729
811
  pythonToolProxy: pythonProxyEnabled,
@@ -972,6 +1054,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
972
1054
  clearTimeout(terminationTimeoutId);
973
1055
  terminationTimeoutId = null;
974
1056
  }
1057
+ if (progressTimeoutId) {
1058
+ clearTimeout(progressTimeoutId);
1059
+ progressTimeoutId = null;
1060
+ }
975
1061
  cancelPendingTermination();
976
1062
  if (!terminated) {
977
1063
  terminated = true;
@@ -1066,7 +1152,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1066
1152
  // Update final progress
1067
1153
  const wasAborted = abortedViaComplete || (!hasComplete && (done.aborted || signal?.aborted || false));
1068
1154
  progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
1069
- emitProgress();
1155
+ scheduleProgress(true);
1070
1156
 
1071
1157
  return {
1072
1158
  index,
package/src/task/index.ts CHANGED
@@ -391,12 +391,57 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
391
391
  // Build full prompts with context prepended
392
392
  const tasksWithContext = tasksWithUniqueIds.map(t => renderTemplate(context, t));
393
393
  const contextFiles = this.session.contextFiles;
394
- const skills = this.session.skills;
394
+ const availableSkills = this.session.skills;
395
+ const availableSkillList = availableSkills ?? [];
395
396
  const promptTemplates = this.session.promptTemplates;
397
+ const skillLookup = new Map(availableSkillList.map(skill => [skill.name, skill]));
398
+ const missingSkillsByTask: Array<{ id: string; missing: string[] }> = [];
399
+ const tasksWithSkills = tasksWithContext.map(task => {
400
+ if (task.skills === undefined) {
401
+ return { ...task, resolvedSkills: availableSkills, preloadedSkills: undefined };
402
+ }
403
+ const requested = task.skills;
404
+ const resolved = [] as typeof availableSkillList;
405
+ const missing: string[] = [];
406
+ const seen = new Set<string>();
407
+ for (const name of requested) {
408
+ const trimmed = name.trim();
409
+ if (!trimmed || seen.has(trimmed)) continue;
410
+ seen.add(trimmed);
411
+ const skill = skillLookup.get(trimmed);
412
+ if (skill) {
413
+ resolved.push(skill);
414
+ } else {
415
+ missing.push(trimmed);
416
+ }
417
+ }
418
+ if (missing.length > 0) {
419
+ missingSkillsByTask.push({ id: task.id, missing });
420
+ }
421
+ return { ...task, resolvedSkills: resolved, preloadedSkills: resolved };
422
+ });
423
+
424
+ if (missingSkillsByTask.length > 0) {
425
+ const available = availableSkillList.map(skill => skill.name).join(", ") || "none";
426
+ const details = missingSkillsByTask.map(entry => `${entry.id}: ${entry.missing.join(", ")}`).join("; ");
427
+ return {
428
+ content: [
429
+ {
430
+ type: "text",
431
+ text: `Unknown skills requested: ${details}. Available skills: ${available}`,
432
+ },
433
+ ],
434
+ details: {
435
+ projectAgentsDir,
436
+ results: [],
437
+ totalDurationMs: Date.now() - startTime,
438
+ },
439
+ };
440
+ }
396
441
 
397
442
  // Initialize progress for all tasks
398
- for (let i = 0; i < tasksWithContext.length; i++) {
399
- const t = tasksWithContext[i];
443
+ for (let i = 0; i < tasksWithSkills.length; i++) {
444
+ const t = tasksWithSkills[i];
400
445
  progressMap.set(i, {
401
446
  index: i,
402
447
  id: t.id,
@@ -416,7 +461,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
416
461
  }
417
462
  emitProgress();
418
463
 
419
- const runTask = async (task: (typeof tasksWithContext)[number], index: number) => {
464
+ const runTask = async (task: (typeof tasksWithSkills)[number], index: number) => {
420
465
  if (!isIsolated) {
421
466
  return runSubprocess({
422
467
  cwd: this.session.cwd,
@@ -438,7 +483,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
438
483
  onProgress: progress => {
439
484
  progressMap.set(index, {
440
485
  ...structuredClone(progress),
441
- args: tasksWithContext[index]?.args,
486
+ args: tasksWithSkills[index]?.args,
442
487
  });
443
488
  emitProgress();
444
489
  },
@@ -447,7 +492,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
447
492
  settingsManager: this.session.settingsManager,
448
493
  mcpManager: this.session.mcpManager,
449
494
  contextFiles,
450
- skills,
495
+ skills: task.resolvedSkills,
496
+ preloadedSkills: task.preloadedSkills,
451
497
  promptTemplates,
452
498
  });
453
499
  }
@@ -481,7 +527,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
481
527
  onProgress: progress => {
482
528
  progressMap.set(index, {
483
529
  ...structuredClone(progress),
484
- args: tasksWithContext[index]?.args,
530
+ args: tasksWithSkills[index]?.args,
485
531
  });
486
532
  emitProgress();
487
533
  },
@@ -490,7 +536,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
490
536
  settingsManager: this.session.settingsManager,
491
537
  mcpManager: this.session.mcpManager,
492
538
  contextFiles,
493
- skills,
539
+ skills: task.resolvedSkills,
540
+ preloadedSkills: task.preloadedSkills,
494
541
  promptTemplates,
495
542
  });
496
543
  const patch = await captureDeltaPatch(worktreeDir, baseline);
@@ -527,7 +574,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
527
574
 
528
575
  // Execute in parallel with concurrency limit
529
576
  const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
530
- tasksWithContext,
577
+ tasksWithSkills,
531
578
  MAX_CONCURRENCY,
532
579
  runTask,
533
580
  signal,
@@ -538,10 +585,10 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
538
585
  if (result !== undefined) {
539
586
  return {
540
587
  ...result,
541
- args: tasksWithContext[index]?.args,
588
+ args: tasksWithSkills[index]?.args,
542
589
  };
543
590
  }
544
- const task = tasksWithContext[index];
591
+ const task = tasksWithSkills[index];
545
592
  return {
546
593
  index,
547
594
  id: task.id,
@@ -5,10 +5,11 @@ type RenderResult = {
5
5
  args: Record<string, string>;
6
6
  id: string;
7
7
  description: string;
8
+ skills?: string[];
8
9
  };
9
10
 
10
11
  export function renderTemplate(template: string, task: TaskItem): RenderResult {
11
- const { id, description, args } = task;
12
+ const { id, description, args, skills } = task;
12
13
 
13
14
  let usedPlaceholder = false;
14
15
  const unknownArguments: string[] = [];
@@ -43,5 +44,6 @@ export function renderTemplate(template: string, task: TaskItem): RenderResult {
43
44
  args: { id, description, ...args },
44
45
  id,
45
46
  description,
47
+ skills,
46
48
  };
47
49
  }
package/src/task/types.ts CHANGED
@@ -49,6 +49,11 @@ export const taskItemSchema = Type.Object({
49
49
  description: "Arguments to fill {{placeholders}} in context",
50
50
  }),
51
51
  ),
52
+ skills: Type.Optional(
53
+ Type.Array(Type.String(), {
54
+ description: "Skill names to preload into the subagent system prompt",
55
+ }),
56
+ ),
52
57
  });
53
58
 
54
59
  export type TaskItem = Static<typeof taskItemSchema>;
@@ -107,6 +107,7 @@ export interface SubagentWorkerStartPayload {
107
107
  pythonPreludeDocs?: PreludeHelper[];
108
108
  contextFiles?: ContextFileEntry[];
109
109
  skills?: Skill[];
110
+ preloadedSkills?: Skill[];
110
111
  promptTemplates?: PromptTemplate[];
111
112
  mcpTools?: MCPToolMetadata[];
112
113
  pythonToolProxy?: boolean;
@@ -608,6 +608,7 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
608
608
  requireCompleteTool: true,
609
609
  contextFiles: payload.contextFiles,
610
610
  skills: payload.skills,
611
+ preloadedSkills: payload.preloadedSkills,
611
612
  promptTemplates: payload.promptTemplates,
612
613
  // Append system prompt (equivalent to CLI's --append-system-prompt)
613
614
  systemPrompt: defaultPrompt =>
@@ -627,6 +628,14 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
627
628
  runState.session = session;
628
629
  checkAbort();
629
630
 
631
+ // Write session init metadata for debugging/replay
632
+ session.sessionManager.appendSessionInit({
633
+ systemPrompt: session.agent.state.systemPrompt,
634
+ task: payload.task,
635
+ tools: session.getAllToolNames(),
636
+ outputSchema: payload.outputSchema,
637
+ });
638
+
630
639
  signal.addEventListener(
631
640
  "abort",
632
641
  () => {
@@ -19,7 +19,6 @@ import { AskTool } from "./ask";
19
19
  import { BashTool } from "./bash";
20
20
  import { CalculatorTool } from "./calculator";
21
21
  import { CompleteTool } from "./complete";
22
- import { EnterPlanModeTool } from "./enter-plan-mode";
23
22
  import { ExitPlanModeTool } from "./exit-plan-mode";
24
23
  import { FetchTool } from "./fetch";
25
24
  import { FindTool } from "./find";
@@ -73,7 +72,6 @@ export { AskTool, type AskToolDetails } from "./ask";
73
72
  export { BashTool, type BashToolDetails, type BashToolOptions } from "./bash";
74
73
  export { CalculatorTool, type CalculatorToolDetails } from "./calculator";
75
74
  export { CompleteTool } from "./complete";
76
- export { type EnterPlanModeDetails, EnterPlanModeTool } from "./enter-plan-mode";
77
75
  export { type ExitPlanModeDetails, ExitPlanModeTool } from "./exit-plan-mode";
78
76
  export { FetchTool, type FetchToolDetails } from "./fetch";
79
77
  export { type FindOperations, FindTool, type FindToolDetails, type FindToolOptions } from "./find";
@@ -199,7 +197,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
199
197
  fetch: s => new FetchTool(s),
200
198
  web_search: s => new WebSearchTool(s),
201
199
  write: s => new WriteTool(s),
202
- enter_plan_mode: s => new EnterPlanModeTool(s),
203
200
  };
204
201
 
205
202
  export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
@@ -1,98 +0,0 @@
1
- Transitions to plan mode for designing implementation approaches before writing code.
2
-
3
- <conditions>
4
- Prefer using EnterPlanMode for implementation tasks unless they're simple. Use it when ANY of these conditions apply:
5
- 1. **New Feature Implementation**: Adding meaningful new functionality
6
- - Example: "Add a logout button" — where should it go? What should happen on click?
7
- - Example: "Add form validation" — what rules? What error messages?
8
- 2. **Multiple Valid Approaches**: The task can be solved in several different ways
9
- - Example: "Add caching to the API" — could use Redis, in-memory, file-based, etc.
10
- - Example: "Improve performance" — many optimization strategies possible
11
- 3. **Code Modifications**: Changes that affect existing behavior or structure
12
- - Example: "Update the login flow" — what exactly should change?
13
- - Example: "Refactor this component" — what's the target architecture?
14
- 4. **Architectural Decisions**: The task requires choosing between patterns or technologies
15
- - Example: "Add real-time updates" — WebSockets vs SSE vs polling
16
- - Example: "Implement state management" — Redux vs Context vs custom solution
17
- 5. **Multi-File Changes**: The task will likely touch more than 2-3 files
18
- - Example: "Refactor the authentication system"
19
- - Example: "Add a new API endpoint with tests"
20
- 6. **Unclear Requirements**: You need to explore before understanding the full scope
21
- - Example: "Make the app faster" — need to profile and identify bottlenecks
22
- - Example: "Fix the bug in checkout" — need to investigate root cause
23
- 7. **User Preferences Matter**: The implementation could reasonably go multiple ways
24
- - If you would use `ask` to clarify the approach, use EnterPlanMode instead
25
- - Plan mode lets you explore first, then present options with context
26
- </conditions>
27
-
28
- <instruction>
29
- In plan mode:
30
- 1. Explore codebase with `find`, `grep`, `read`, `ls`
31
- 2. Understand existing patterns and architecture
32
- 3. Design implementation approach
33
- 4. Use `ask` if clarification needed
34
- 5. Call `exit_plan_mode` when ready
35
- </instruction>
36
-
37
- <output>
38
- Requires user approval to enter. Once approved, you enter read-only exploration mode with restricted tool access.
39
- </output>
40
-
41
- <parameters>
42
- Optional parameters:
43
- - `parallel`: Explore independent threads in parallel before synthesizing.
44
- - `iterative`: One thread at a time with checkpoints between steps.
45
- </parameters>
46
-
47
- <example name="auth">
48
- User: "Add user authentication to the app"
49
- → Use plan mode: architectural decisions (session vs JWT, where to store tokens, middleware structure)
50
- </example>
51
-
52
- <example name="optimization">
53
- User: "Optimize the database queries"
54
- → Use plan mode: multiple approaches possible, need to profile first, significant impact
55
- </example>
56
-
57
- <example name="dark-mode">
58
- User: "Implement dark mode"
59
- → Use plan mode: architectural decision on theme system, affects many components
60
- </example>
61
-
62
- <example name="delete-button">
63
- User: "Add a delete button to the user profile"
64
- → Use plan mode: seems simple but involves placement, confirmation dialog, API call, error handling, state updates
65
- </example>
66
-
67
- <example name="error-handling">
68
- User: "Update the error handling in the API"
69
- → Use plan mode: affects multiple files, user should approve the approach
70
- </example>
71
-
72
- <example name="typo-skip">
73
- User: "Fix the typo in the README"
74
- → Skip plan mode: straightforward, no planning needed
75
- </example>
76
-
77
- <example name="debug-skip">
78
- User: "Add a console.log to debug this function"
79
- → Skip plan mode: simple, obvious implementation
80
- </example>
81
-
82
- <example name="research-skip">
83
- User: "What files handle routing?"
84
- → Skip plan mode: research task, not implementation planning
85
- </example>
86
-
87
- <avoid>
88
- - Single-line or few-line fixes (typos, obvious bugs)
89
- - Adding a single function with clear requirements
90
- - Tasks with very specific, detailed instructions
91
- - Pure research/exploration tasks
92
- </avoid>
93
-
94
- <critical>
95
- - This tool REQUIRES user approval — they must consent to entering plan mode
96
- - If unsure whether to use it, err on the side of planning — alignment upfront beats rework
97
- - Users appreciate being consulted before significant changes are made to their codebase
98
- </critical>
@@ -1,81 +0,0 @@
1
- import * as fs from "node:fs/promises";
2
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
3
- import { StringEnum } from "@oh-my-pi/pi-ai";
4
- import { isEnoent } from "@oh-my-pi/pi-utils";
5
- import { Type } from "@sinclair/typebox";
6
- import { renderPromptTemplate } from "../config/prompt-templates";
7
- import { resolvePlanUrlToPath } from "../internal-urls";
8
- import enterPlanModeDescription from "../prompts/tools/enter-plan-mode.md" with { type: "text" };
9
- import type { ToolSession } from ".";
10
- import { ToolError } from "./tool-errors";
11
-
12
- const enterPlanModeSchema = Type.Object({
13
- workflow: Type.Optional(
14
- StringEnum(["parallel", "iterative"], {
15
- description: "Planning workflow to use",
16
- }),
17
- ),
18
- });
19
-
20
- export interface EnterPlanModeDetails {
21
- planFilePath: string;
22
- planExists: boolean;
23
- workflow?: "parallel" | "iterative";
24
- }
25
-
26
- export class EnterPlanModeTool implements AgentTool<typeof enterPlanModeSchema, EnterPlanModeDetails> {
27
- public readonly name = "enter_plan_mode";
28
- public readonly label = "EnterPlanMode";
29
- public readonly description: string;
30
- public readonly parameters = enterPlanModeSchema;
31
-
32
- private readonly session: ToolSession;
33
-
34
- constructor(session: ToolSession) {
35
- this.session = session;
36
- this.description = renderPromptTemplate(enterPlanModeDescription);
37
- }
38
-
39
- public async execute(
40
- _toolCallId: string,
41
- params: { workflow?: "parallel" | "iterative" },
42
- _signal?: AbortSignal,
43
- _onUpdate?: AgentToolUpdateCallback<EnterPlanModeDetails>,
44
- _context?: AgentToolContext,
45
- ): Promise<AgentToolResult<EnterPlanModeDetails>> {
46
- const state = this.session.getPlanModeState?.();
47
- if (state?.enabled) {
48
- throw new ToolError("Plan mode is already active.");
49
- }
50
-
51
- const sessionId = this.session.getSessionId?.();
52
- if (!sessionId) {
53
- throw new ToolError("Plan mode requires an active session.");
54
- }
55
-
56
- const settingsManager = this.session.settingsManager;
57
- if (!settingsManager) {
58
- throw new ToolError("Settings manager unavailable for plan mode.");
59
- }
60
-
61
- const planFilePath = `plan://${sessionId}/plan.md`;
62
- const resolvedPlanPath = resolvePlanUrlToPath(planFilePath, {
63
- getPlansDirectory: settingsManager.getPlansDirectory.bind(settingsManager),
64
- cwd: this.session.cwd,
65
- });
66
- let planExists = false;
67
- try {
68
- const stat = await fs.stat(resolvedPlanPath);
69
- planExists = stat.isFile();
70
- } catch (error) {
71
- if (!isEnoent(error)) {
72
- throw error;
73
- }
74
- }
75
-
76
- return {
77
- content: [{ type: "text", text: "Plan mode requested." }],
78
- details: { planFilePath, planExists, workflow: params.workflow },
79
- };
80
- }
81
- }