@oh-my-pi/pi-coding-agent 8.4.3 → 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.
@@ -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
  () => {
package/src/tools/bash.ts CHANGED
@@ -85,10 +85,8 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
85
85
  throw new ToolError(`Working directory is not a directory: ${commandCwd}`);
86
86
  }
87
87
 
88
- // Auto-convert milliseconds to seconds if value > 1000 (16+ min is unreasonable)
89
- let timeoutSec = rawTimeout > 1000 ? rawTimeout / 1000 : rawTimeout;
90
88
  // Clamp to reasonable range: 1s - 3600s (1 hour)
91
- timeoutSec = Math.max(1, Math.min(3600, timeoutSec));
89
+ const timeoutSec = Math.max(1, Math.min(3600, rawTimeout));
92
90
  const timeoutMs = timeoutSec * 1000;
93
91
 
94
92
  // Track output for streaming updates (tail only)