@oh-my-pi/pi-coding-agent 16.2.13 → 16.3.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.
Files changed (166) hide show
  1. package/CHANGELOG.md +108 -7
  2. package/dist/cli.js +6033 -5982
  3. package/dist/types/advisor/config.d.ts +4 -2
  4. package/dist/types/collab/host.d.ts +16 -0
  5. package/dist/types/collab/protocol.d.ts +9 -3
  6. package/dist/types/commit/model-selection.d.ts +5 -0
  7. package/dist/types/config/model-resolver.d.ts +12 -10
  8. package/dist/types/config/settings-schema.d.ts +28 -5
  9. package/dist/types/edit/modes/patch.d.ts +11 -0
  10. package/dist/types/eval/agent-bridge.d.ts +5 -1
  11. package/dist/types/exec/bash-executor.d.ts +1 -0
  12. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +86 -2
  13. package/dist/types/extensibility/skills.d.ts +2 -1
  14. package/dist/types/mnemopi/state.d.ts +12 -0
  15. package/dist/types/modes/components/agent-hub.d.ts +9 -5
  16. package/dist/types/modes/components/status-line/component.test.d.ts +1 -0
  17. package/dist/types/modes/components/usage-row.d.ts +1 -1
  18. package/dist/types/modes/controllers/command-controller.d.ts +0 -1
  19. package/dist/types/modes/controllers/extension-ui-controller.d.ts +6 -0
  20. package/dist/types/modes/controllers/streaming-reveal.d.ts +17 -1
  21. package/dist/types/modes/controllers/tool-args-reveal.d.ts +30 -3
  22. package/dist/types/modes/interactive-mode.d.ts +12 -7
  23. package/dist/types/modes/rpc/rpc-client.d.ts +5 -0
  24. package/dist/types/modes/rpc/rpc-mode.d.ts +64 -1
  25. package/dist/types/modes/session-teardown.d.ts +63 -0
  26. package/dist/types/modes/session-teardown.test.d.ts +1 -0
  27. package/dist/types/modes/theme/theme.d.ts +9 -4
  28. package/dist/types/modes/types.d.ts +2 -3
  29. package/dist/types/modes/utils/copy-targets.d.ts +2 -0
  30. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +1 -0
  31. package/dist/types/sdk.d.ts +9 -0
  32. package/dist/types/session/agent-session.d.ts +46 -13
  33. package/dist/types/session/exit-diagnostics.d.ts +48 -0
  34. package/dist/types/session/session-loader.d.ts +5 -0
  35. package/dist/types/task/executor.d.ts +12 -5
  36. package/dist/types/task/parallel.d.ts +1 -0
  37. package/dist/types/task/render.test.d.ts +1 -0
  38. package/dist/types/thinking.d.ts +2 -0
  39. package/dist/types/tools/checkpoint.d.ts +8 -0
  40. package/dist/types/tools/eval-render.d.ts +1 -0
  41. package/dist/types/tools/fetch.d.ts +11 -1
  42. package/dist/types/tools/index.d.ts +3 -1
  43. package/dist/types/tools/irc.d.ts +1 -0
  44. package/dist/types/tools/output-meta.d.ts +7 -0
  45. package/dist/types/tools/output-schema-validator.d.ts +7 -4
  46. package/dist/types/tools/path-utils.d.ts +16 -0
  47. package/dist/types/tools/render-utils.d.ts +4 -1
  48. package/dist/types/utils/git.d.ts +47 -2
  49. package/dist/types/web/search/provider.d.ts +7 -0
  50. package/dist/types/web/search/types.d.ts +1 -1
  51. package/package.json +12 -12
  52. package/src/advisor/config.ts +4 -2
  53. package/src/cli/bench-cli.ts +7 -2
  54. package/src/collab/guest.ts +94 -5
  55. package/src/collab/host.ts +80 -3
  56. package/src/collab/protocol.ts +9 -2
  57. package/src/commit/model-selection.ts +8 -2
  58. package/src/config/keybindings.ts +3 -1
  59. package/src/config/model-discovery.ts +66 -2
  60. package/src/config/model-registry.ts +7 -3
  61. package/src/config/model-resolver.ts +51 -23
  62. package/src/config/settings-schema.ts +44 -14
  63. package/src/edit/hashline/diff.ts +13 -2
  64. package/src/edit/index.ts +58 -8
  65. package/src/edit/modes/patch.ts +53 -18
  66. package/src/edit/streaming.ts +7 -6
  67. package/src/eval/__tests__/agent-bridge.test.ts +57 -1
  68. package/src/eval/agent-bridge.ts +15 -5
  69. package/src/exec/bash-executor.ts +7 -12
  70. package/src/extensibility/legacy-pi-coding-agent-shim.ts +534 -16
  71. package/src/extensibility/skills.ts +29 -6
  72. package/src/goals/guided-setup.ts +4 -3
  73. package/src/internal-urls/docs-index.generated.txt +1 -1
  74. package/src/lsp/client.ts +11 -14
  75. package/src/main.ts +38 -10
  76. package/src/mnemopi/state.ts +43 -7
  77. package/src/modes/acp/acp-agent.ts +1 -1
  78. package/src/modes/components/agent-hub.ts +10 -2
  79. package/src/modes/components/agent-transcript-viewer.ts +39 -7
  80. package/src/modes/components/assistant-message.ts +16 -10
  81. package/src/modes/components/chat-transcript-builder.ts +11 -1
  82. package/src/modes/components/custom-editor.test.ts +20 -0
  83. package/src/modes/components/hook-selector.ts +44 -25
  84. package/src/modes/components/model-selector.ts +1 -1
  85. package/src/modes/components/status-line/component.test.ts +44 -0
  86. package/src/modes/components/status-line/component.ts +9 -1
  87. package/src/modes/components/status-line/segments.ts +3 -3
  88. package/src/modes/components/usage-row.ts +16 -2
  89. package/src/modes/controllers/command-controller.ts +6 -21
  90. package/src/modes/controllers/event-controller.ts +11 -9
  91. package/src/modes/controllers/extension-ui-controller.ts +84 -3
  92. package/src/modes/controllers/input-controller.ts +16 -13
  93. package/src/modes/controllers/selector-controller.ts +3 -8
  94. package/src/modes/controllers/streaming-reveal.ts +75 -53
  95. package/src/modes/controllers/tool-args-reveal.ts +340 -10
  96. package/src/modes/interactive-mode.ts +122 -79
  97. package/src/modes/rpc/rpc-client.ts +21 -0
  98. package/src/modes/rpc/rpc-mode.ts +197 -46
  99. package/src/modes/session-teardown.test.ts +219 -0
  100. package/src/modes/session-teardown.ts +82 -0
  101. package/src/modes/setup-wizard/scenes/theme.ts +2 -2
  102. package/src/modes/skill-command.ts +7 -20
  103. package/src/modes/theme/theme.ts +29 -15
  104. package/src/modes/types.ts +2 -3
  105. package/src/modes/utils/copy-targets.ts +12 -0
  106. package/src/modes/utils/ui-helpers.ts +19 -2
  107. package/src/plan-mode/approved-plan-prompt.test.ts +36 -0
  108. package/src/prompts/advisor/system.md +1 -1
  109. package/src/prompts/agents/tester.md +6 -2
  110. package/src/prompts/skills/autoload.md +8 -0
  111. package/src/prompts/skills/user-invocation.md +11 -0
  112. package/src/prompts/steering/parent-irc.md +5 -0
  113. package/src/prompts/system/irc-incoming.md +2 -0
  114. package/src/prompts/system/mid-run-todo-nudge.md +3 -0
  115. package/src/prompts/system/plan-mode-approved.md +7 -10
  116. package/src/prompts/system/plan-mode-compact-instructions.md +2 -1
  117. package/src/prompts/system/plan-mode-reference.md +3 -4
  118. package/src/prompts/system/rewind-report.md +6 -0
  119. package/src/prompts/system/subagent-system-prompt.md +3 -0
  120. package/src/prompts/tools/irc.md +2 -1
  121. package/src/prompts/tools/job.md +2 -2
  122. package/src/prompts/tools/rewind.md +3 -2
  123. package/src/prompts/tools/task.md +4 -0
  124. package/src/sdk.ts +18 -4
  125. package/src/session/agent-session.ts +660 -114
  126. package/src/session/exit-diagnostics.ts +202 -0
  127. package/src/session/session-context.ts +25 -13
  128. package/src/session/session-loader.ts +58 -25
  129. package/src/session/session-manager.ts +0 -1
  130. package/src/session/session-persistence.ts +30 -4
  131. package/src/session/settings-stream-fn.ts +14 -0
  132. package/src/slash-commands/builtin-registry.ts +35 -3
  133. package/src/system-prompt.ts +15 -1
  134. package/src/task/executor.ts +31 -8
  135. package/src/task/index.ts +31 -9
  136. package/src/task/isolation-runner.ts +18 -2
  137. package/src/task/parallel.ts +7 -2
  138. package/src/task/render.test.ts +121 -0
  139. package/src/task/render.ts +48 -2
  140. package/src/task/worktree.ts +12 -13
  141. package/src/task/yield-assembly.ts +8 -5
  142. package/src/thinking.ts +5 -0
  143. package/src/tools/ask.ts +188 -9
  144. package/src/tools/ast-edit.ts +7 -0
  145. package/src/tools/ast-grep.ts +11 -0
  146. package/src/tools/bash.ts +6 -30
  147. package/src/tools/checkpoint.ts +15 -1
  148. package/src/tools/eval-render.ts +15 -0
  149. package/src/tools/fetch.ts +82 -18
  150. package/src/tools/gh.ts +1 -1
  151. package/src/tools/grep.ts +45 -27
  152. package/src/tools/index.ts +3 -1
  153. package/src/tools/irc.ts +24 -9
  154. package/src/tools/output-meta.ts +50 -0
  155. package/src/tools/output-schema-validator.ts +152 -15
  156. package/src/tools/path-utils.ts +55 -10
  157. package/src/tools/read.ts +1 -1
  158. package/src/tools/render-utils.ts +5 -3
  159. package/src/tools/yield.ts +41 -1
  160. package/src/utils/commit-message-generator.ts +2 -2
  161. package/src/utils/git.ts +271 -29
  162. package/src/utils/thinking-display.ts +13 -0
  163. package/src/web/search/index.ts +9 -28
  164. package/src/web/search/provider.ts +26 -1
  165. package/src/web/search/providers/duckduckgo.ts +1 -1
  166. package/src/web/search/types.ts +5 -1
@@ -115,6 +115,7 @@ export type SymbolKey =
115
115
  | "icon.cacheMiss"
116
116
  | "icon.input"
117
117
  | "icon.output"
118
+ | "icon.throughput"
118
119
  | "icon.host"
119
120
  | "icon.session"
120
121
  | "icon.package"
@@ -319,6 +320,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
319
320
  "icon.cacheMiss": "⊘",
320
321
  "icon.input": "⤵",
321
322
  "icon.output": "⤴",
323
+ "icon.throughput": "⚡",
322
324
  "icon.host": "🖥",
323
325
  "icon.session": "🆔",
324
326
  "icon.package": "📦",
@@ -597,6 +599,8 @@ const NERD_SYMBOLS: SymbolMap = {
597
599
  "icon.input": "\uf090",
598
600
  // pick:  | alt:  →
599
601
  "icon.output": "\uf08b",
602
+ // pick: (nf-fa-tachometer) | alt: ⚡ ↬
603
+ "icon.throughput": "\uf0e4",
600
604
  // pick:  | alt:  
601
605
  "icon.host": "\uf109",
602
606
  // pick:  | alt:  
@@ -827,10 +831,11 @@ const ASCII_SYMBOLS: SymbolMap = {
827
831
  "icon.ghost": "@",
828
832
  "icon.agents": "AG",
829
833
  "icon.job": "bg",
834
+ "icon.output": "out:",
835
+ "icon.throughput": "tok/s:",
830
836
  "icon.cache": "cache",
831
837
  "icon.cacheMiss": "!",
832
838
  "icon.input": "in:",
833
- "icon.output": "out:",
834
839
  "icon.host": "host",
835
840
  "icon.session": "id",
836
841
  "icon.package": "[P]",
@@ -1820,6 +1825,7 @@ export class Theme {
1820
1825
  cacheMiss: this.#symbols["icon.cacheMiss"],
1821
1826
  input: this.#symbols["icon.input"],
1822
1827
  output: this.#symbols["icon.output"],
1828
+ throughput: this.#symbols["icon.throughput"],
1823
1829
  host: this.#symbols["icon.host"],
1824
1830
  session: this.#symbols["icon.session"],
1825
1831
  package: this.#symbols["icon.package"],
@@ -2151,6 +2157,11 @@ export function getCurrentThemeName(): string | undefined {
2151
2157
  export function fgOrPlain(color: ThemeColor, text: string, styledText: string = text): string {
2152
2158
  return typeof theme === "undefined" ? text : theme.fg(color, styledText);
2153
2159
  }
2160
+ export interface ThemeChangeEvent {
2161
+ /** Preview/presentation-only changes should repaint live UI without replacing native scrollback. */
2162
+ ephemeral?: boolean;
2163
+ }
2164
+
2154
2165
  var currentSymbolPresetOverride: SymbolPreset | undefined;
2155
2166
  var currentColorBlindMode: boolean = false;
2156
2167
  var themeWatcher: fs.FSWatcher | undefined;
@@ -2159,7 +2170,7 @@ var sigwinchHandler: (() => void) | undefined;
2159
2170
  var autoDetectedTheme: boolean = false;
2160
2171
  var autoDarkTheme: string = "dark";
2161
2172
  var autoLightTheme: string = "light";
2162
- var onThemeChangeCallback: (() => void) | undefined;
2173
+ var onThemeChangeCallback: ((event: ThemeChangeEvent) => void) | undefined;
2163
2174
  var themeLoadRequestId: number = 0;
2164
2175
  let themeEpoch = 0;
2165
2176
 
@@ -2235,7 +2246,10 @@ export async function setTheme(
2235
2246
  }
2236
2247
  }
2237
2248
 
2238
- export async function previewTheme(name: string): Promise<{ success: boolean; error?: string }> {
2249
+ export async function previewTheme(
2250
+ name: string,
2251
+ event: ThemeChangeEvent = { ephemeral: true },
2252
+ ): Promise<{ success: boolean; error?: string }> {
2239
2253
  const requestId = ++themeLoadRequestId;
2240
2254
  try {
2241
2255
  const loadedTheme = await loadTheme(name, getCurrentThemeOptions());
@@ -2243,7 +2257,7 @@ export async function previewTheme(name: string): Promise<{ success: boolean; er
2243
2257
  return { success: false, error: "Theme preview superseded by a newer request" };
2244
2258
  }
2245
2259
  theme = loadedTheme;
2246
- notifyThemeChange();
2260
+ notifyThemeChange(event);
2247
2261
  return { success: true };
2248
2262
  } catch (error) {
2249
2263
  if (requestId !== themeLoadRequestId) {
@@ -2259,9 +2273,9 @@ export async function previewTheme(name: string): Promise<{ success: boolean; er
2259
2273
  /**
2260
2274
  * Enable auto-detection mode, switching to the appropriate dark/light theme.
2261
2275
  */
2262
- export function enableAutoTheme(): void {
2276
+ export function enableAutoTheme(event: ThemeChangeEvent = {}): void {
2263
2277
  autoDetectedTheme = true;
2264
- reevaluateAutoTheme("enableAutoTheme");
2278
+ reevaluateAutoTheme("enableAutoTheme", event);
2265
2279
  }
2266
2280
 
2267
2281
  /**
@@ -2290,7 +2304,7 @@ export function setThemeInstance(themeInstance: Theme): void {
2290
2304
  theme = themeInstance;
2291
2305
  currentThemeName = "<in-memory>";
2292
2306
  stopThemeWatcher();
2293
- notifyThemeChange();
2307
+ notifyThemeChange({ ephemeral: true });
2294
2308
  }
2295
2309
 
2296
2310
  /**
@@ -2311,7 +2325,7 @@ export async function setSymbolPreset(preset: SymbolPreset): Promise<void> {
2311
2325
  theme = await loadTheme("dark", getCurrentThemeOptions());
2312
2326
  if (requestId !== themeLoadRequestId) return;
2313
2327
  }
2314
- notifyThemeChange();
2328
+ notifyThemeChange({ ephemeral: true });
2315
2329
  }
2316
2330
 
2317
2331
  /**
@@ -2340,7 +2354,7 @@ export async function setColorBlindMode(enabled: boolean): Promise<void> {
2340
2354
  theme = await loadTheme("dark", getCurrentThemeOptions());
2341
2355
  if (requestId !== themeLoadRequestId) return;
2342
2356
  }
2343
- notifyThemeChange();
2357
+ notifyThemeChange({ ephemeral: true });
2344
2358
  }
2345
2359
 
2346
2360
  /**
@@ -2350,7 +2364,7 @@ export function getColorBlindMode(): boolean {
2350
2364
  return currentColorBlindMode;
2351
2365
  }
2352
2366
 
2353
- export function onThemeChange(callback: () => void): () => void {
2367
+ export function onThemeChange(callback: (event: ThemeChangeEvent) => void): () => void {
2354
2368
  onThemeChangeCallback = callback;
2355
2369
  return () => {
2356
2370
  if (onThemeChangeCallback === callback) {
@@ -2371,9 +2385,9 @@ export function getThemeEpoch(): number {
2371
2385
  }
2372
2386
 
2373
2387
  /** Bump the theme epoch and notify the registered theme-change listener. */
2374
- function notifyThemeChange(): void {
2388
+ function notifyThemeChange(event: ThemeChangeEvent = {}): void {
2375
2389
  themeEpoch++;
2376
- onThemeChangeCallback?.();
2390
+ onThemeChangeCallback?.(event);
2377
2391
  }
2378
2392
 
2379
2393
  /**
@@ -2428,7 +2442,7 @@ async function startThemeWatcher(): Promise<void> {
2428
2442
  loadTheme(watchedThemeName, getCurrentThemeOptions())
2429
2443
  .then(loadedTheme => {
2430
2444
  theme = loadedTheme;
2431
- notifyThemeChange();
2445
+ notifyThemeChange({ ephemeral: true });
2432
2446
  })
2433
2447
  .catch(() => {
2434
2448
  // Ignore errors (file might be in invalid state while being edited)
@@ -2460,7 +2474,7 @@ async function startThemeWatcher(): Promise<void> {
2460
2474
  * Shared logic for re-evaluating the auto-detected theme.
2461
2475
  * Called from SIGWINCH, terminal appearance change handler, and macOS fallback observer.
2462
2476
  */
2463
- function reevaluateAutoTheme(debugLabel: string): void {
2477
+ function reevaluateAutoTheme(debugLabel: string, event: ThemeChangeEvent = {}): void {
2464
2478
  if (!autoDetectedTheme) return;
2465
2479
  const resolved = getDefaultTheme();
2466
2480
  if (resolved === currentThemeName) return;
@@ -2468,7 +2482,7 @@ function reevaluateAutoTheme(debugLabel: string): void {
2468
2482
  loadTheme(resolved, getCurrentThemeOptions())
2469
2483
  .then(loadedTheme => {
2470
2484
  theme = loadedTheme;
2471
- notifyThemeChange();
2485
+ notifyThemeChange(event);
2472
2486
  })
2473
2487
  .catch(err => {
2474
2488
  logger.debug(`Theme switch on ${debugLabel} failed`, { error: String(err) });
@@ -14,6 +14,7 @@ import type {
14
14
  ExtensionWidgetOptions,
15
15
  } from "../extensibility/extensions";
16
16
  import type { CompactOptions } from "../extensibility/extensions/types";
17
+ import type { Skill } from "../extensibility/skills";
17
18
  import type { MCPManager } from "../mcp";
18
19
  import type { PlanApprovalDetails } from "../plan-mode/approved-plan";
19
20
  import type { AgentSession } from "../session/agent-session";
@@ -129,7 +130,6 @@ export interface InteractiveModeContext {
129
130
  historyStorage?: HistoryStorage;
130
131
  mcpManager?: MCPManager;
131
132
  lspServers?: LspStartupServerInfo[];
132
- titleSystemPrompt?: string;
133
133
  collabHost?: CollabHost;
134
134
  collabGuest?: CollabGuestLink;
135
135
  eventController: EventController;
@@ -205,7 +205,7 @@ export interface InteractiveModeContext {
205
205
  lastStatusSpacer: Spacer | undefined;
206
206
  lastStatusText: Text | undefined;
207
207
  fileSlashCommands: Set<string>;
208
- skillCommands: Map<string, string>;
208
+ skillCommands: Map<string, Skill>;
209
209
  oauthManualInput: OAuthManualInputManager;
210
210
  todoPhases: TodoPhase[];
211
211
 
@@ -297,7 +297,6 @@ export interface InteractiveModeContext {
297
297
  getUserMessageText(message: Message): string;
298
298
  findLastAssistantMessage(): AssistantMessage | undefined;
299
299
  extractAssistantText(message: AssistantMessage): string;
300
- updateEditorTopBorder(): void;
301
300
  /** Refresh the running-subagents status badge from the active local or collab registry. */
302
301
  syncRunningSubagentBadge(): void;
303
302
  updateEditorBorderColor(): void;
@@ -118,6 +118,18 @@ export function extractCodeBlocks(text: string): CodeBlock[] {
118
118
  .map(b => ({ lang: b.lang, code: b.code }));
119
119
  }
120
120
 
121
+ /** Walk the transcript backwards for the most recent fenced assistant code block. */
122
+ export function extractLastCodeBlock(messages: readonly AgentMessage[]): CodeBlock | undefined {
123
+ for (let i = messages.length - 1; i >= 0; i--) {
124
+ const msg = messages[i];
125
+ const text = assistantText(msg);
126
+ if (!text) continue;
127
+ const blocks = extractCodeBlocks(text);
128
+ if (blocks.length > 0) return blocks[blocks.length - 1];
129
+ }
130
+ return undefined;
131
+ }
132
+
121
133
  /** Extract `>`-quoted blocks from assistant markdown, in document order. */
122
134
  export function extractQuoteBlocks(text: string): QuoteBlock[] {
123
135
  return extractBlocks(text)
@@ -34,6 +34,7 @@ import { ToolExecutionComponent } from "../../modes/components/tool-execution";
34
34
  import { TranscriptBlock } from "../../modes/components/transcript-container";
35
35
  import { createUsageRowBlock } from "../../modes/components/usage-row";
36
36
  import { UserMessageComponent } from "../../modes/components/user-message";
37
+ import { decodeStreamedToolArgs, streamingStringKeysForTool } from "../../modes/controllers/tool-args-reveal";
37
38
  import { materializeImageReferenceLinksSync } from "../../modes/image-references";
38
39
  import { theme } from "../../modes/theme/theme";
39
40
  import type { CompactionQueuedMessage, InteractiveModeContext } from "../../modes/types";
@@ -296,12 +297,16 @@ export class UiHelpers {
296
297
  // read run so the row sits under it. Mirrors the live path, where the read
297
298
  // group is created during streaming and the row is appended below it.
298
299
  let pendingUsage: Usage | undefined;
300
+ let pendingUsageDuration: number | undefined;
301
+ let pendingUsageTtft: number | undefined;
299
302
  const flushPendingUsage = () => {
300
303
  if (!pendingUsage) return;
301
304
  readGroup?.seal();
302
305
  readGroup = null;
303
- this.ctx.chatContainer.addChild(createUsageRowBlock(pendingUsage));
306
+ this.ctx.chatContainer.addChild(createUsageRowBlock(pendingUsage, pendingUsageDuration, pendingUsageTtft));
304
307
  pendingUsage = undefined;
308
+ pendingUsageDuration = undefined;
309
+ pendingUsageTtft = undefined;
305
310
  };
306
311
  // Rebuild-time mirror of the event controller's displaceable-poll
307
312
  // bookkeeping: a `job` poll that found every watched job still running is
@@ -412,8 +417,18 @@ export class UiHelpers {
412
417
  readGroup = null;
413
418
  const tool = this.ctx.viewSession.getToolByName(content.name);
414
419
  const partialJson = getStreamingPartialJson(content);
420
+ // Mid-stream rebuild (theme change, settings, focus replay): decode
421
+ // display args from the raw stream exactly like the live reveal path.
422
+ // The provider-parsed `arguments` lag the stream by up to a throttled
423
+ // parse window, so spreading them alone would freeze a long write/edit
424
+ // preview at its last full parse.
425
+ const rawInput = content.customWireName !== undefined;
415
426
  const renderArgs = partialJson
416
- ? { ...content.arguments, __partialJson: partialJson }
427
+ ? decodeStreamedToolArgs(partialJson, {
428
+ rawInput,
429
+ fullArgs: content.arguments,
430
+ streamingStringKeys: streamingStringKeysForTool(content.name, rawInput),
431
+ })
417
432
  : content.arguments;
418
433
  const component = new ToolExecutionComponent(
419
434
  content.name,
@@ -444,6 +459,8 @@ export class UiHelpers {
444
459
  }
445
460
  }
446
461
  pendingUsage = this.ctx.settings.get("display.showTokenUsage") ? message.usage : undefined;
462
+ pendingUsageDuration = message.duration;
463
+ pendingUsageTtft = message.ttft;
447
464
  } else if (message.role === "toolResult") {
448
465
  const pendingReadComponent = this.ctx.pendingTools.get(message.toolCallId);
449
466
  const isReadGroupResult =
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { prompt } from "@oh-my-pi/pi-utils";
3
+ import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
4
+ import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
5
+ type: "text",
6
+ };
7
+ import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
8
+
9
+ const PLAN_FILE_PATH = "local://durable-plan.md";
10
+ const PLAN_SENTINEL = "SENTINEL_HEADROOM_COMPRESSED_PLAN_CONTENT";
11
+
12
+ describe("approved plan execution prompts", () => {
13
+ it("requires reading the durable plan file without inlining plan content", () => {
14
+ const approved = prompt.render(planModeApprovedPrompt, {
15
+ planContent: PLAN_SENTINEL,
16
+ planFilePath: PLAN_FILE_PATH,
17
+ contextPreserved: false,
18
+ });
19
+ const reference = prompt.render(planModeReferencePrompt, {
20
+ planContent: PLAN_SENTINEL,
21
+ planFilePath: PLAN_FILE_PATH,
22
+ });
23
+ const compact = prompt.render(planModeCompactInstructionsPrompt, {
24
+ planFilePath: PLAN_FILE_PATH,
25
+ });
26
+
27
+ for (const rendered of [approved, reference, compact]) {
28
+ expect(rendered).toContain(PLAN_FILE_PATH);
29
+ }
30
+ for (const rendered of [approved, reference, compact]) {
31
+ expect(rendered).not.toContain(PLAN_SENTINEL);
32
+ }
33
+ expect(approved).toContain("MUST read `local://durable-plan.md`");
34
+ expect(reference).toContain("MUST read `local://durable-plan.md`");
35
+ });
36
+ });
@@ -14,7 +14,7 @@ Offer that view before they sink work into the wrong direction.
14
14
 
15
15
  <workflow>
16
16
  You receive the agent's transcript incrementally, including their thoughts.
17
- You have read-only access through `read`, `grep`, `glob` to verify your suspicions.
17
+ Use the tools this session grants you to verify suspicions — by default read-only lookup (`read`, `grep`, `glob`); operators may extend the grant via `WATCHDOG.yml`. Advising is your primary channel; touch mutating tools (when granted) only when a verify step genuinely needs them.
18
18
  Keep exploration lean:
19
19
  - 2–3 tool calls per advise.
20
20
  - Exception: critical bugs may need deeper verification before raising a blocker.
@@ -22,6 +22,8 @@ A test suite is a liability until it pays for itself. Every worthless test is ne
22
22
  - Mutation test in your head: if a plausible bug — a flipped condition, an off-by-one, a wrong return value, a dropped case — would still let the test PASS, the test is worthless. Discard it.
23
23
  - You NEVER write tests that assert plumbing or restate the implementation. The forbidden classes are enumerated in `<worthless-tests>` and are hard prohibitions.
24
24
  - You MUST match the repo's existing test conventions — framework, file layout, naming, assertion style. A second convention beside an existing one is PROHIBITED.
25
+ - NEVER test defaults (configurations, fallback values, or default environment values). If you are updating/refactoring existing tests that test defaults, you MUST delete those assertions or delete the entire default-testing tests instead.
26
+ - You are explicitly ALLOWED to write **no tests at all** if you were spawned for a stupid reason (meaning: the change is trivial—such as docs, comments, types, exports, or simple config; the behavior is already fully covered; or any tests you would write would be worthless, restate plumbing, or test defaults). If so, state this clearly and exit.
25
27
  </critical>
26
28
 
27
29
  <anti-patterns name="worthless-tests">
@@ -33,7 +35,7 @@ NEVER write any of these. Each is a green check that survives real bugs:
33
35
  - **Construction smoke.** "Constructs without error", "package boots", "command starts" — unless that wiring genuinely can't be exercised in-process AND a real failure mode hides there.
34
36
  - **Mock round-trips.** Asserting a mock was called with the args you just passed it. You tested the mock, not the system.
35
37
  - **Existence/shape-only.** Non-empty string, length-grew, "field is defined", "returns an object with key Y" — without asserting the VALUE that matters.
36
- - **Default snapshots.** Asserting every field of a default config equals its current default. A harmless default change shouldn't redden a test. Assert logical behavior, not the current state.
38
+ - **Default values.** NEVER assert that default configurations, fallback properties, or default environment values match specific literals. A harmless change to a default setting must never break the tests. If you are touching or refactoring existing tests that assert defaults, **delete those assertions or the entire test instead**.
37
39
  - **Field-wiring.** Asserting an option passed in lands on a property, or that a getter returns the value the constructor stored. Test the downstream BEHAVIOR that depends on it, not the assignment.
38
40
  - **Duplicate-layer coverage.** Re-proving through mocks what an integration test already proves. Drop the narrower restatement.
39
41
 
@@ -103,5 +105,7 @@ Tests MUST be full-suite safe and order-independent, not merely file-local safe.
103
105
  - A test exists to FAIL on a real bug. No nameable contract, or no plausible bug would redden it → NEVER write it.
104
106
  - NEVER assert plumbing, restate the implementation, or grep the source. Test observable behavior through the public surface.
105
107
  - No timing races, no environment pollution, deterministic and order-independent — full-suite safe.
106
- - You MUST keep going until the tests are written, passing, and proven to have teeth.
108
+ - NEVER test defaults. If updating tests that do, delete them instead.
109
+ - You are explicitly ALLOWED to write **no tests at all** if you were spawned for a stupid reason (trivial changes, already covered, or if any possible test would be worthless/test defaults).
110
+ - You MUST keep going until the tests are written, passing, and proven to have teeth (unless skipped per above).
107
111
  </critical>
@@ -0,0 +1,8 @@
1
+ {{body}}
2
+
3
+ ---
4
+
5
+ Skill: {{filePath}}
6
+ {{#if userArgs}}
7
+ User: {{userArgs}}
8
+ {{/if}}
@@ -0,0 +1,11 @@
1
+ [IMPORTANT: The user has invoked the "{{name}}" skill, indicating they want you to follow its instructions. The full skill content is loaded below.]
2
+
3
+ {{body}}
4
+
5
+ ---
6
+
7
+ [Skill directory: {{baseDir}}]
8
+ Resolve any relative paths in this skill (e.g. `scripts/foo.js`, `templates/config.yaml`) against that directory using its absolute path: read referenced assets and templates, and run scripts with the terminal tool when the skill's instructions call for it.
9
+ {{#if userArgs}}
10
+ User: {{userArgs}}
11
+ {{/if}}
@@ -0,0 +1,5 @@
1
+ Your current interruptible wait was interrupted because an IRC message arrived from your parent agent `{{from}}`.
2
+
3
+ Parent IRC message:
4
+
5
+ {{message}}
@@ -3,5 +3,7 @@ Incoming IRC message from agent `{{from}}`{{#if replyTo}} (replying to {{replyTo
3
3
 
4
4
  {{message}}
5
5
 
6
+ {{#if interrupting}}An agent sent this while you were waiting or working. Any active interruptible wait was stopped early so you can read it now.{{/if}}
7
+
6
8
  {{#if autoReplied}}You are mid-task, so a side-channel auto-reply was generated from your context and delivered to `{{from}}` on your behalf (recorded after this message). Follow up with the `irc` tool (`op: "send"`, `to: "{{from}}"`) only if that auto-reply needs correcting.{{else}}If a response is expected, reply with the `irc` tool (`op: "send"`, `to: "{{from}}"`) — you may finish your current step first. Nobody replies on your behalf.{{/if}}
7
9
  </irc>
@@ -0,0 +1,3 @@
1
+ <system-reminder>
2
+ Gentle reminder: {{incompleteCount}} todo item{{#if plural}}s are{{else}} is{{/if}} still open. If you finished a task since the last `{{toolRefs.todo}}` update, mark it done now so progress stays visible; otherwise just keep working.
3
+ </system-reminder>
@@ -1,25 +1,22 @@
1
1
  Plan approved.
2
2
  {{#if contextPreserved}}
3
- - Context preserved. Use conversation history when useful; this plan is the source of truth if it conflicts with earlier exploration.
3
+ - Context preserved. Use conversation history when useful; the plan file is the source of truth if it conflicts with earlier exploration.
4
4
  {{/if}}
5
5
 
6
6
  <instruction>
7
- You MUST execute this plan step by step. You have full tool access.
7
+ You MUST read `{{planFilePath}}` before executing.
8
+ The file content is the authoritative plan; visible/compressed context is secondary.
9
+ Read failure? Report the exact path and error instead of guessing.
10
+ After reading, you MUST execute the plan step by step with full tool access.
8
11
  You MUST verify each step before proceeding to the next.
9
12
  {{#has tools "todo"}}
10
- Before execution, initialize todo tracking with `todo`.
13
+ After reading the plan, initialize todo tracking with `todo`.
11
14
  After each completed step, immediately update `todo`.
12
15
  If `todo` fails, fix the payload and retry before continuing.
13
16
  {{/has}}
14
- The plan path is for subagent handoff only. You already have the plan; NEVER read it.
15
17
  </instruction>
16
18
 
17
- The full plan is injected below. You MUST execute it now:
18
-
19
- <plan path="{{planFilePath}}">
20
- {{planContent}}
21
- </plan>
22
-
23
19
  <critical>
20
+ NEVER stop because inline plan content is compressed, expired, or unrecoverable. Read `{{planFilePath}}`.
24
21
  You MUST keep going until complete. This matters.
25
22
  </critical>
@@ -12,5 +12,6 @@ You MUST drop:
12
12
  - Restated context already present in the plan file.
13
13
 
14
14
  {{#if planFilePath}}
15
- The approved plan file is at `{{planFilePath}}`; it is the authoritative source of truth and need not be re-summarized in detail.
15
+ The approved plan file is at `{{planFilePath}}`; it is the authoritative source of truth.
16
+ You MUST preserve this durable path and the fact that the executor must read it directly after compaction.
16
17
  {{/if}}
@@ -1,11 +1,10 @@
1
1
  ## Existing Plan
2
2
 
3
- <plan path="{{planFilePath}}">
4
- {{planContent}}
5
- </plan>
3
+ The approved plan file is at `{{planFilePath}}`.
6
4
 
7
5
  <instruction>
8
6
  If this plan is relevant to current work and not complete, you MUST continue executing it.
7
+ If you do not have the current plan content in visible context, you MUST read `{{planFilePath}}`.
9
8
  If the plan is stale or unrelated, you MUST ignore it.
10
- The plan path is for subagent handoff only. You already have the plan; NEVER read it.
9
+ NEVER stop because inline plan content is compressed, expired, or unrecoverable. Read the file.
11
10
  </instruction>
@@ -0,0 +1,6 @@
1
+ Checkpoint completed. The checkpoint's exploratory branch was rewound; the branch summary and retained report below are now the context.
2
+
3
+ Do not call `rewind` again for this checkpoint. Continue from this retained report.
4
+
5
+ Report:
6
+ {{report}}
@@ -61,6 +61,9 @@ Yield protocol:
61
61
 
62
62
  This is your only way to return a final result. For structured results, you NEVER put JSON in plain text or substitute a text summary for `result.data`.
63
63
 
64
+ {{#if outputSchemaOverridesAgent}}
65
+ Caller schema overrides agent-native output instructions. Ignore ROLE-provided output/yield labels, field names, examples, and procedures that conflict with the interface below. Use ONLY labels/fields from the caller schema; safest path: omit `type` and terminal-yield the full `result.data` object.
66
+ {{/if}}
64
67
  {{#if outputSchema}}
65
68
  Your terminal `yield` MUST use exactly this shape — the schema fields go inside `result.data`, NEVER at the top level and NEVER as a stringified summary:
66
69
  ```ts
@@ -12,7 +12,8 @@ Use `op: "send"` to deliver a message to a specific peer or broadcast to `"all"`
12
12
 
13
13
  # Waiting and Inboxes
14
14
  Messages only arrive when the peer actively sends one—do not interrogate a peer for status.
15
- - If you are completely blocked and MUST wait for an answer, use `op: "wait"` (or `await: true` on a send). This blocks your turn until a message arrives. If it times out, that just means "no message arrived", not a failure.
15
+ - If you are completely blocked and MUST wait for an answer, use `op: "wait"` (or `await: true` on a send). The wait returns when a matching message arrives, the timeout elapses, or any IRC / steering message interrupts the wait. Parent-agent IRC interrupts with steering-level priority.
16
+ - No need to alternate `irc wait`, `irc inbox`, and `job poll`: waits surface cross-channel interrupts promptly. The next turn includes the interrupt reason and message.
16
17
  - To check for messages without blocking, use `op: "inbox"` to drain your queue.
17
18
 
18
19
  # When to Coordinate
@@ -4,8 +4,8 @@ Background tasks deliver their results automatically the moment they finish. You
4
4
 
5
5
  # Interventions
6
6
 
7
- - **Block and wait:** Pass `poll` with specific job IDs when you are completely blocked and cannot do any other work. The call returns as soon as one watched job finishes or the wait window elapses — NOT when all of them finish; re-issue to keep waiting.
7
+ - **Block and wait:** Pass `poll` with specific job IDs when you are completely blocked and cannot do any other work. The call returns as soon as one watched job finishes, the wait window elapses, or an IRC / steering message interrupts the wait — NOT when all jobs finish; re-issue to keep waiting.
8
8
  - To watch EVERY running job, issue a call with NO fields at all (no `poll`, no `cancel`, no `list`). NEVER pass an array of every running ID.
9
- - A finished job's output is included in the wait result.
9
+ - A finished job's output, or the interrupting message and reason, is included in the next turn.
10
10
  - **Stop execution:** Pass `cancel` with job IDs to kill jobs that have hung, stalled, or are no longer needed. A cancel-only call returns immediately.
11
11
  - **Snapshot:** Pass `list: true` to get the current status of all jobs without waiting.
@@ -9,5 +9,6 @@ Requirements:
9
9
  - You MUST call this before yielding if a checkpoint is active.
10
10
 
11
11
  Behavior:
12
- - If no checkpoint is active, this tool errors.
13
- - On success, the session rewinds and keeps your report as retained context.
12
+ - If no checkpoint is active, this tool errors. If the checkpoint already rewound, continue from the retained report instead of retrying.
13
+ - On success, the session rewinds, keeps your report as retained context, and closes the checkpoint.
14
+ - A successful rewind is final for that checkpoint; repeat calls error.
@@ -4,7 +4,11 @@ Execution blocks your turn: the call only returns once the work is completely fi
4
4
 
5
5
  # Delegation Strategy
6
6
  - **Maximize parallelism:** Break work into the widest possible {{#if batchEnabled}}array of `tasks[]`{{else}}set of parallel `task` calls{{/if}}. NEVER serialize work that can run concurrently. Tasks touching different files or independent refactors should run in parallel; agents resolve their own file collisions live.
7
+ {{#when MAX_CONCURRENCY ">" 0}}
8
+ - **Concurrency cap:** At most {{pluralize MAX_CONCURRENCY "subagent" "subagents"}} run at once in this session — anything beyond that just queues, so a {{#if batchEnabled}}`tasks[]` batch{{else}}set of parallel `task` calls{{/if}} larger than {{MAX_CONCURRENCY}} only delays results. Keep the fan-out at or under the cap.
9
+ {{/when}}
7
10
  - **Sequence only when necessary:** The only reason to run A before B is if B strictly requires A's output to function (e.g., a core API contract or schema migration). {{#if ircEnabled}}If the missing piece is small, run them in parallel and have B ask A via `irc`!{{/if}}
11
+ {{#if ircEnabled}}- **Steering delivery:** Parent-to-subagent IRC is delivered immediately as steering; subagents blocked in `job poll` / `irc wait` do not need to poll separately for it.{{/if}}
8
12
  - **Role matching:** Assign each subagent a specific `role` (e.g. "Security Reviewer", "DB Migrator"). Do not spawn generic workers.
9
13
  - **No overhead:** Each assignment MUST instruct its agent to skip formatters, linters, and project-wide test suites. You will run those once at the end.
10
14
  - **One-pass agents:** Prefer agents that investigate **and** edit in a single pass; only spin a read-only discovery step (e.g. `explore`) when the affected files are genuinely unknown.
package/src/sdk.ts CHANGED
@@ -135,6 +135,7 @@ import { wrapStreamFnWithProviderConcurrency } from "./task/provider-concurrency
135
135
  import {
136
136
  AUTO_THINKING,
137
137
  type ConfiguredThinkingLevel,
138
+ concreteThinkingLevel,
138
139
  parseConfiguredThinkingLevel,
139
140
  parseThinkingLevel,
140
141
  resolveProvisionalAutoLevel,
@@ -404,6 +405,15 @@ export interface CreateAgentSessionOptions {
404
405
  customSystemPrompt?: string;
405
406
  /** Already-loaded text appended through the bundled system prompt templates. */
406
407
  appendSystemPrompt?: string;
408
+ /**
409
+ * Already-loaded title-generation system prompt override (typically
410
+ * {@link discoverTitleSystemPromptFile} → {@link resolvePromptInput}). When
411
+ * set, every automatic session-title generation path on this session — the
412
+ * first-input title and the replan-driven refresh — uses this prompt
413
+ * instead of the bundled default. Refresh on cwd change via
414
+ * {@link AgentSession.setTitleSystemPrompt}.
415
+ */
416
+ titleSystemPrompt?: string;
407
417
  /** Optional provider-facing session identifier for prompt caches and sticky auth selection.
408
418
  * Keeps persisted session files isolated while reusing provider-side caches. */
409
419
  providerSessionId?: string;
@@ -1258,7 +1268,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1258
1268
  ? getRestorableSessionModels(existingSession.models, sessionManager.getLastModelChangeRole())
1259
1269
  : [];
1260
1270
  let restoredSessionModelIndex = -1;
1261
- let restoredSessionThinkingLevel: ThinkingLevel | undefined;
1271
+ let restoredSessionThinkingLevel: ConfiguredThinkingLevel | undefined;
1262
1272
  if (!hasExplicitModel && !model && sessionModelStrings.length > 0) {
1263
1273
  logger.time("restoreSessionModel", () => {
1264
1274
  let failedSessionModel: string | undefined;
@@ -1266,6 +1276,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1266
1276
  const sessionModelStr = sessionModelStrings[i];
1267
1277
  const parsedModel = parseModelString(sessionModelStr, {
1268
1278
  allowMaxAlias: true,
1279
+ allowAutoAlias: true,
1269
1280
  isLiteralModelId: (provider, id) => modelRegistry.find(provider, id) !== undefined,
1270
1281
  });
1271
1282
  if (!parsedModel) {
@@ -1333,7 +1344,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1333
1344
  // Concrete level the agent/session start with. With `auto` this is the
1334
1345
  // provisional level shown until the first per-turn classification resolves;
1335
1346
  // `auto` itself stays a session-only concept handled by AgentSession.
1336
- let effectiveThinkingLevel: ThinkingLevel | undefined = thinkingLevel === AUTO_THINKING ? undefined : thinkingLevel;
1347
+ let effectiveThinkingLevel: ThinkingLevel | undefined = concreteThinkingLevel(thinkingLevel);
1337
1348
  if (model) {
1338
1349
  const resolvedModel = model;
1339
1350
  effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
@@ -1567,6 +1578,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1567
1578
  activateDiscoveredTools: toolNames => session.activateDiscoveredTools(toolNames),
1568
1579
  getCheckpointState: () => session.getCheckpointState(),
1569
1580
  setCheckpointState: state => session.setCheckpointState(state ?? undefined),
1581
+ getLastCompletedRewind: () => session.getLastCompletedRewind(),
1570
1582
  getToolChoiceQueue: () => session.toolChoiceQueue,
1571
1583
  buildToolChoice: name => {
1572
1584
  const m = session.model;
@@ -1916,6 +1928,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1916
1928
  const sessionModelStr = sessionModelStrings[i];
1917
1929
  const parsedModel = parseModelString(sessionModelStr, {
1918
1930
  allowMaxAlias: true,
1931
+ allowAutoAlias: true,
1919
1932
  isLiteralModelId: (provider, id) => modelRegistry.find(provider, id) !== undefined,
1920
1933
  });
1921
1934
  if (!parsedModel) continue;
@@ -1930,7 +1943,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1930
1943
  // `thinking.defaultLevel` must not become sticky.
1931
1944
  thinkingLevel = pickInitialThinkingLevel(restoredModel);
1932
1945
  autoThinking = thinkingLevel === AUTO_THINKING;
1933
- effectiveThinkingLevel = thinkingLevel === AUTO_THINKING ? undefined : thinkingLevel;
1946
+ effectiveThinkingLevel = concreteThinkingLevel(thinkingLevel);
1934
1947
  effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
1935
1948
  autoThinking
1936
1949
  ? resolveProvisionalAutoLevel(restoredModel)
@@ -1986,7 +1999,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1986
1999
  // so the role's explicit selector (e.g. `:max`) now applies.
1987
2000
  thinkingLevel = pickInitialThinkingLevel(resolvedDefaultModel);
1988
2001
  autoThinking = thinkingLevel === AUTO_THINKING;
1989
- effectiveThinkingLevel = thinkingLevel === AUTO_THINKING ? undefined : thinkingLevel;
2002
+ effectiveThinkingLevel = concreteThinkingLevel(thinkingLevel);
1990
2003
  effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
1991
2004
  autoThinking
1992
2005
  ? resolveProvisionalAutoLevel(resolvedDefaultModel)
@@ -2744,6 +2757,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2744
2757
  providerSessionId: options.providerSessionId,
2745
2758
  parentEvalSessionId: options.parentEvalSessionId,
2746
2759
  advisorTools,
2760
+ titleSystemPrompt: options.titleSystemPrompt,
2747
2761
  });
2748
2762
  hasSession = true;
2749
2763
  if (asyncJobManager) {