@oh-my-pi/pi-coding-agent 15.13.2 → 16.0.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 (84) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/cli.js +587 -499
  3. package/dist/types/advisor/__tests__/advisor.test.d.ts +1 -0
  4. package/dist/types/advisor/advise-tool.d.ts +58 -0
  5. package/dist/types/advisor/index.d.ts +3 -0
  6. package/dist/types/advisor/runtime.d.ts +52 -0
  7. package/dist/types/advisor/watchdog.d.ts +5 -0
  8. package/dist/types/config/model-roles.d.ts +1 -1
  9. package/dist/types/config/settings-schema.d.ts +75 -5
  10. package/dist/types/eval/js/context-manager.d.ts +15 -0
  11. package/dist/types/modes/components/advisor-message.d.ts +9 -0
  12. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  13. package/dist/types/modes/controllers/command-controller.d.ts +3 -1
  14. package/dist/types/modes/interactive-mode.d.ts +4 -1
  15. package/dist/types/modes/types.d.ts +9 -1
  16. package/dist/types/sdk.d.ts +3 -3
  17. package/dist/types/session/agent-session.d.ts +71 -2
  18. package/dist/types/session/session-history-format.d.ts +4 -0
  19. package/dist/types/session/unexpected-stop-classifier.d.ts +13 -0
  20. package/dist/types/session/yield-queue.d.ts +2 -0
  21. package/dist/types/stt/asr-client.d.ts +1 -1
  22. package/dist/types/tiny/title-client.d.ts +1 -1
  23. package/dist/types/tools/job.d.ts +1 -0
  24. package/dist/types/tools/path-utils.d.ts +1 -0
  25. package/dist/types/tools/report-tool-issue.d.ts +0 -1
  26. package/dist/types/tts/tts-client.d.ts +1 -1
  27. package/dist/types/utils/thinking-display.d.ts +1 -17
  28. package/package.json +13 -13
  29. package/src/advisor/__tests__/advisor.test.ts +586 -0
  30. package/src/advisor/advise-tool.ts +87 -0
  31. package/src/advisor/index.ts +3 -0
  32. package/src/advisor/runtime.ts +248 -0
  33. package/src/advisor/watchdog.ts +83 -0
  34. package/src/cli.ts +25 -12
  35. package/src/config/model-registry.ts +6 -2
  36. package/src/config/model-roles.ts +13 -1
  37. package/src/config/settings-schema.ts +67 -5
  38. package/src/eval/__tests__/agent-bridge.test.ts +106 -46
  39. package/src/eval/__tests__/js-context-manager.test.ts +12 -2
  40. package/src/eval/js/context-manager.ts +40 -3
  41. package/src/eval/js/worker-entry.ts +7 -0
  42. package/src/export/html/template.js +18 -22
  43. package/src/internal-urls/docs-index.generated.ts +8 -5
  44. package/src/main.ts +19 -5
  45. package/src/modes/acp/acp-agent.ts +2 -2
  46. package/src/modes/acp/acp-event-mapper.ts +2 -2
  47. package/src/modes/components/advisor-message.ts +99 -0
  48. package/src/modes/components/agent-hub.ts +38 -7
  49. package/src/modes/components/assistant-message.ts +110 -15
  50. package/src/modes/components/snapcompact-shape-preview-doc.md +2 -2
  51. package/src/modes/components/snapcompact-shape-preview.ts +2 -2
  52. package/src/modes/components/status-line/segments.ts +20 -7
  53. package/src/modes/components/tree-selector.ts +3 -2
  54. package/src/modes/controllers/command-controller.ts +69 -2
  55. package/src/modes/controllers/event-controller.ts +3 -3
  56. package/src/modes/controllers/input-controller.ts +7 -1
  57. package/src/modes/controllers/streaming-reveal.ts +4 -4
  58. package/src/modes/interactive-mode.ts +14 -2
  59. package/src/modes/types.ts +9 -1
  60. package/src/modes/utils/ui-helpers.ts +12 -3
  61. package/src/prompts/advisor/advise-tool.md +1 -0
  62. package/src/prompts/advisor/system.md +31 -0
  63. package/src/prompts/agents/oracle.md +0 -1
  64. package/src/prompts/agents/reviewer.md +0 -1
  65. package/src/prompts/system/unexpected-stop-classifier.md +17 -0
  66. package/src/prompts/system/unexpected-stop-retry.md +4 -0
  67. package/src/sdk.ts +52 -13
  68. package/src/session/agent-session.ts +722 -21
  69. package/src/session/session-dump-format.ts +15 -142
  70. package/src/session/session-history-format.ts +30 -11
  71. package/src/session/unexpected-stop-classifier.ts +129 -0
  72. package/src/session/yield-queue.ts +5 -1
  73. package/src/slash-commands/builtin-registry.ts +102 -4
  74. package/src/stt/asr-client.ts +1 -1
  75. package/src/system-prompt.ts +1 -1
  76. package/src/tiny/title-client.ts +1 -1
  77. package/src/tools/browser/tab-supervisor.ts +1 -1
  78. package/src/tools/browser/tab-worker-entry.ts +12 -4
  79. package/src/tools/job.ts +1 -0
  80. package/src/tools/path-utils.ts +33 -2
  81. package/src/tools/report-tool-issue.ts +2 -7
  82. package/src/tts/tts-client.ts +1 -1
  83. package/src/utils/thinking-display.ts +8 -34
  84. package/src/web/scrapers/docs-rs.ts +2 -3
@@ -84,9 +84,9 @@ export class CommandController {
84
84
  }
85
85
  }
86
86
 
87
- handleDumpCommand() {
87
+ handleDumpCommand(isRaw = false) {
88
88
  try {
89
- const formatted = this.ctx.session.formatSessionAsText();
89
+ const formatted = this.ctx.session.formatSessionAsText({ compact: !isRaw });
90
90
  if (!formatted) {
91
91
  this.ctx.showError("No messages to dump yet.");
92
92
  return;
@@ -98,6 +98,26 @@ export class CommandController {
98
98
  }
99
99
  }
100
100
 
101
+ handleAdvisorDumpCommand(isRaw = false) {
102
+ try {
103
+ const advisorHistory = this.ctx.session.formatAdvisorHistoryAsText({ compact: !isRaw });
104
+ if (advisorHistory === null) {
105
+ this.ctx.showError("Advisor is not active for this session.");
106
+ return;
107
+ }
108
+ if (!advisorHistory) {
109
+ this.ctx.showError("Advisor has no history yet.");
110
+ return;
111
+ }
112
+ copyToClipboard(advisorHistory);
113
+ this.ctx.showStatus("Advisor history copied to clipboard");
114
+ } catch (error: unknown) {
115
+ this.ctx.showError(
116
+ `Failed to copy advisor history: ${error instanceof Error ? error.message : "Unknown error"}`,
117
+ );
118
+ }
119
+ }
120
+
101
121
  async handleDebugTranscriptCommand(): Promise<void> {
102
122
  try {
103
123
  const width = Math.max(1, this.ctx.ui.terminal.columns);
@@ -305,6 +325,53 @@ export class CommandController {
305
325
  this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
306
326
  }
307
327
 
328
+ async handleAdvisorStatusCommand(): Promise<void> {
329
+ const stats = this.ctx.session.getAdvisorStats();
330
+ if (!stats.active) {
331
+ this.ctx.present([
332
+ new Spacer(1),
333
+ new Text(
334
+ stats.configured
335
+ ? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
336
+ : "Advisor is disabled.",
337
+ 1,
338
+ 0,
339
+ ),
340
+ ]);
341
+ return;
342
+ }
343
+ const model = stats.model!;
344
+ let info = `${theme.bold("Advisor Status")}\n\n`;
345
+ info += `${theme.bold("Provider")}\n`;
346
+ info += `${theme.fg("dim", "Model:")} ${model.provider}/${model.id}\n`;
347
+ info += `\n${theme.bold("Messages")}\n`;
348
+ info += `${theme.fg("dim", "User:")} ${stats.messages.user.toLocaleString()}\n`;
349
+ info += `${theme.fg("dim", "Assistant:")} ${stats.messages.assistant.toLocaleString()}\n`;
350
+ info += `${theme.fg("dim", "Total:")} ${stats.messages.total.toLocaleString()}\n`;
351
+ info += `\n${theme.bold("Context")}\n`;
352
+ if (stats.contextWindow > 0) {
353
+ const percent = Math.round((stats.contextTokens / stats.contextWindow) * 100);
354
+ info += `${theme.fg("dim", "Tokens:")} ${stats.contextTokens.toLocaleString()} / ${stats.contextWindow.toLocaleString()} (${percent}%)\n`;
355
+ } else {
356
+ info += `${theme.fg("dim", "Tokens:")} ${stats.contextTokens.toLocaleString()}\n`;
357
+ }
358
+ info += `\n${theme.bold("Spend")}\n`;
359
+ info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
360
+ info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
361
+ if (stats.tokens.cacheRead > 0) {
362
+ info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
363
+ }
364
+ if (stats.tokens.cacheWrite > 0) {
365
+ info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
366
+ }
367
+ info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
368
+ if (stats.cost > 0) {
369
+ info += `\n${theme.bold("Cost")}\n`;
370
+ info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(4)}\n`;
371
+ }
372
+ this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
373
+ }
374
+
308
375
  async handleJobsCommand(): Promise<void> {
309
376
  const snapshot = this.ctx.session.getAsyncJobSnapshot({ recentLimit: 5 });
310
377
  if (!snapshot) {
@@ -22,7 +22,7 @@ import type { AgentSessionEvent } from "../../session/agent-session";
22
22
  import { isSilentAbort, readQueueChipText, resolveAbortLabel } from "../../session/messages";
23
23
  import type { ResolveToolDetails } from "../../tools/resolve";
24
24
  import { vocalizer } from "../../tts/vocalizer";
25
- import { hasVisibleThinking } from "../../utils/thinking-display";
25
+ import { canonicalizeMessage } from "../../utils/thinking-display";
26
26
  import { interruptHint } from "../shared";
27
27
  import { StreamingRevealController } from "./streaming-reveal";
28
28
  import { ToolArgsRevealController } from "./tool-args-reveal";
@@ -480,8 +480,8 @@ export class EventController {
480
480
 
481
481
  const visibleBlockCount = this.ctx.streamingMessage.content.filter(
482
482
  content =>
483
- (content.type === "text" && content.text.trim().length > 0) ||
484
- (content.type === "thinking" && hasVisibleThinking(content)),
483
+ (content.type === "text" && canonicalizeMessage(content.text)) ||
484
+ (content.type === "thinking" && canonicalizeMessage(content.thinking)),
485
485
  ).length;
486
486
  if (visibleBlockCount > this.#lastVisibleBlockCount) {
487
487
  this.#resetReadGroup();
@@ -689,11 +689,17 @@ export class InputController {
689
689
  this.ctx.pendingImages = [];
690
690
  this.ctx.pendingImageLinks = [];
691
691
 
692
- // Render user message immediately, then let session events catch up
692
+ // Render user message immediately, then let session events catch up.
693
+ // Tag the submission as "steer": this is a normal Enter the controller
694
+ // believed was idle, but a background turn can start in the gap before
695
+ // `submitInteractiveInput` dispatches it. Steering matches the
696
+ // streaming-branch Enter (above) and keeps the message from throwing
697
+ // AgentBusyError on that race.
693
698
  const submission = this.ctx.startPendingSubmission({
694
699
  text,
695
700
  images,
696
701
  imageLinks: inputImageLinks,
702
+ streamingBehavior: "steer",
697
703
  });
698
704
 
699
705
  this.ctx.onInputCallback(submission);
@@ -1,7 +1,7 @@
1
1
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
2
  import { getSegmenter } from "@oh-my-pi/pi-tui";
3
3
  import { LRUCache } from "lru-cache/raw";
4
- import { hasVisibleThinking } from "../../utils/thinking-display";
4
+ import { canonicalizeMessage } from "../../utils/thinking-display";
5
5
  import type { AssistantMessageComponent } from "../components/assistant-message";
6
6
 
7
7
  export const STREAMING_REVEAL_FRAME_MS = 1000 / 30;
@@ -88,7 +88,7 @@ export function visibleUnits(message: AssistantMessage, hideThinking: boolean):
88
88
  for (const block of message.content) {
89
89
  if (block.type === "text") {
90
90
  total += countGraphemes(block.text);
91
- } else if (block.type === "thinking" && !hideThinking && hasVisibleThinking(block)) {
91
+ } else if (block.type === "thinking" && !hideThinking && canonicalizeMessage(block.thinking)) {
92
92
  total += countGraphemes(block.thinking);
93
93
  }
94
94
  }
@@ -129,7 +129,7 @@ export function buildDisplayMessage(
129
129
  const units = countOf(i, block.text);
130
130
  content.push(revealTextBlock(block, remaining, units));
131
131
  remaining = Math.max(0, remaining - units);
132
- } else if (block.type === "thinking" && !hideThinking && hasVisibleThinking(block)) {
132
+ } else if (block.type === "thinking" && !hideThinking && canonicalizeMessage(block.thinking)) {
133
133
  const units = countOf(i, block.thinking);
134
134
  content.push(revealThinkingBlock(block, remaining, units));
135
135
  remaining = Math.max(0, remaining - units);
@@ -231,7 +231,7 @@ export class StreamingRevealController {
231
231
  const block = message.content[i]!;
232
232
  if (block.type === "text") {
233
233
  total += this.#unitCounter.count(i, block.text);
234
- } else if (block.type === "thinking" && !this.#hideThinkingBlock && hasVisibleThinking(block)) {
234
+ } else if (block.type === "thinking" && !this.#hideThinkingBlock && canonicalizeMessage(block.thinking)) {
235
235
  total += this.#unitCounter.count(i, block.thinking);
236
236
  }
237
237
  }
@@ -542,6 +542,7 @@ export class InteractiveMode implements InteractiveModeContext {
542
542
  if (eventBus) {
543
543
  this.#eventBusUnsubscribers.push(
544
544
  eventBus.on(LSP_STARTUP_EVENT_CHANNEL, data => {
545
+ if (this.settings.get("startup.quiet")) return;
545
546
  this.#handleLspStartupEvent(data as LspStartupEvent);
546
547
  }),
547
548
  );
@@ -551,6 +552,7 @@ export class InteractiveMode implements InteractiveModeContext {
551
552
  logger.warn("Ignoring malformed mcp:connecting event", { data });
552
553
  return;
553
554
  }
555
+ if (this.settings.get("startup.quiet")) return;
554
556
  this.showStatus(formatMCPConnectingMessage(data.serverNames));
555
557
  }),
556
558
  );
@@ -1123,6 +1125,7 @@ export class InteractiveMode implements InteractiveModeContext {
1123
1125
  imageLinks?: (string | undefined)[];
1124
1126
  customType?: string;
1125
1127
  display?: boolean;
1128
+ streamingBehavior?: "steer" | "followUp";
1126
1129
  }): SubmittedUserInput {
1127
1130
  const submission: SubmittedUserInput = {
1128
1131
  text: input.text,
@@ -1130,6 +1133,7 @@ export class InteractiveMode implements InteractiveModeContext {
1130
1133
  imageLinks: input.imageLinks,
1131
1134
  customType: input.customType,
1132
1135
  display: input.display,
1136
+ streamingBehavior: input.streamingBehavior,
1133
1137
  cancelled: false,
1134
1138
  started: false,
1135
1139
  };
@@ -3331,8 +3335,12 @@ export class InteractiveMode implements InteractiveModeContext {
3331
3335
  return this.#commandController.handleExportCommand(text);
3332
3336
  }
3333
3337
 
3334
- handleDumpCommand() {
3335
- return this.#commandController.handleDumpCommand();
3338
+ handleDumpCommand(isRaw?: boolean) {
3339
+ return this.#commandController.handleDumpCommand(isRaw);
3340
+ }
3341
+
3342
+ handleAdvisorDumpCommand(isRaw?: boolean) {
3343
+ return this.#commandController.handleAdvisorDumpCommand(isRaw);
3336
3344
  }
3337
3345
 
3338
3346
  handleDebugTranscriptCommand(): Promise<void> {
@@ -3351,6 +3359,10 @@ export class InteractiveMode implements InteractiveModeContext {
3351
3359
  return this.#commandController.handleSessionCommand();
3352
3360
  }
3353
3361
 
3362
+ handleAdvisorStatusCommand(): Promise<void> {
3363
+ return this.#commandController.handleAdvisorStatusCommand();
3364
+ }
3365
+
3354
3366
  handleJobsCommand(): Promise<void> {
3355
3367
  return this.#commandController.handleJobsCommand();
3356
3368
  }
@@ -54,6 +54,11 @@ export type SubmittedUserInput = {
54
54
  * turn. Used by the `c`/`.` continue shortcut. */
55
55
  synthetic?: boolean;
56
56
  display?: boolean;
57
+ /** Queue intent if the session is (or becomes) busy when this submission is
58
+ * dispatched: "steer" (interrupt the active turn) or "followUp" (process after
59
+ * it). Normal user Enter carries "steer" to match the streaming-branch Enter;
60
+ * background/continuation submits omit it and default to "followUp". */
61
+ streamingBehavior?: "steer" | "followUp";
57
62
  cancelled: boolean;
58
63
  started: boolean;
59
64
  };
@@ -222,6 +227,7 @@ export interface InteractiveModeContext {
222
227
  imageLinks?: (string | undefined)[];
223
228
  customType?: string;
224
229
  display?: boolean;
230
+ streamingBehavior?: "steer" | "followUp";
225
231
  }): SubmittedUserInput;
226
232
  cancelPendingSubmission(): boolean;
227
233
  markPendingSubmissionStarted(input: SubmittedUserInput): boolean;
@@ -264,13 +270,15 @@ export interface InteractiveModeContext {
264
270
  handleShareCommand(): Promise<void>;
265
271
  handleTodoCommand(args: string): Promise<void>;
266
272
  handleSessionCommand(): Promise<void>;
273
+ handleAdvisorStatusCommand(): Promise<void>;
267
274
  handleJobsCommand(): Promise<void>;
268
275
  handleUsageCommand(reports?: UsageReport[] | null): Promise<void>;
269
276
  handleChangelogCommand(showFull?: boolean): Promise<void>;
270
277
  handleHotkeysCommand(): void;
271
278
  handleToolsCommand(): void;
272
279
  handleContextCommand(): void;
273
- handleDumpCommand(): void;
280
+ handleDumpCommand(isRaw?: boolean): void;
281
+ handleAdvisorDumpCommand(isRaw?: boolean): void;
274
282
  handleDebugTranscriptCommand(): Promise<void>;
275
283
  handleClearCommand(): Promise<void>;
276
284
  handleFreshCommand(): Promise<void>;
@@ -1,9 +1,11 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import type { AssistantMessage, ImageContent, Message, Usage } from "@oh-my-pi/pi-ai";
3
3
  import { type Component, Spacer, Text, TruncatedText } from "@oh-my-pi/pi-tui";
4
+ import type { AdvisorMessageDetails } from "../../advisor";
4
5
  import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol";
5
6
  import { settings } from "../../config/settings";
6
7
  import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
8
+ import { createAdvisorMessageCard } from "../../modes/components/advisor-message";
7
9
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
8
10
  import { createBackgroundTanDispatchBlock } from "../../modes/components/background-tan-message";
9
11
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
@@ -45,7 +47,7 @@ import {
45
47
  import type { SessionContext } from "../../session/session-context";
46
48
  import { createIrcMessageCard } from "../../tools/irc";
47
49
  import { formatBytes, formatDuration } from "../../tools/render-utils";
48
- import { hasVisibleThinking } from "../../utils/thinking-display";
50
+ import { canonicalizeMessage } from "../../utils/thinking-display";
49
51
 
50
52
  type TextBlock = { type: "text"; text: string };
51
53
  interface RenderInitialMessagesOptions {
@@ -240,6 +242,13 @@ export class UiHelpers {
240
242
  this.ctx.chatContainer.addChild(card);
241
243
  return [card];
242
244
  }
245
+ if (message.customType === "advisor") {
246
+ const details = (message as CustomMessage<AdvisorMessageDetails>).details;
247
+ this.ctx.chatContainer.addChild(
248
+ createAdvisorMessageCard(details, () => this.ctx.toolOutputExpanded, theme),
249
+ );
250
+ break;
251
+ }
243
252
  if (message.customType === BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE) {
244
253
  this.ctx.chatContainer.addChild(createBackgroundTanDispatchBlock(message as CustomMessage<unknown>));
245
254
  break;
@@ -399,8 +408,8 @@ export class UiHelpers {
399
408
  const assistantComponent = lastChild instanceof AssistantMessageComponent ? lastChild : undefined;
400
409
  const hasVisibleAssistantContent = message.content.some(
401
410
  content =>
402
- (content.type === "text" && content.text.trim().length > 0) ||
403
- (content.type === "thinking" && hasVisibleThinking(content)),
411
+ (content.type === "text" && canonicalizeMessage(content.text)) ||
412
+ (content.type === "thinking" && canonicalizeMessage(content.thinking)),
404
413
  );
405
414
  if (hasVisibleAssistantContent) {
406
415
  // Rebuild reconstructs immutable history; seal (not finalize) so the
@@ -0,0 +1 @@
1
+ Send one concrete, terse piece of advice to the agent you are watching. Use sparingly; stay silent when nothing matters.
@@ -0,0 +1,31 @@
1
+ <system-conventions>
2
+ RFC 2119 applies to MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` and `AVOID` are aliases for `MUST NOT` and `SHOULD NOT`.
3
+ You can explore the workspace; budget is 2–3 tool calls per advise (exception: critical bugs warrant deeper verification before raising a blocker).
4
+ </system-conventions>
5
+
6
+ You bring a different angle.
7
+ The agent might not have thought about an edge case, spotted a hallucinated API, or realized a simpler approach exists.
8
+ Your job is to offer that view before they sink work into the wrong direction.
9
+
10
+ <workflow>
11
+ You receive the agent's transcript incrementally, including private thinking.
12
+ You have read-only access through `read`, `search`, `find` to verify your suspicions.
13
+ Keep exploration lean — 2–3 calls per advise unless you've spotted a critical bug and need to be absolutely certain before raising a blocker.
14
+ </workflow>
15
+
16
+ <communication>
17
+ At most one `advise` per update. Prefer silence when the agent is on track. Address the agent directly. Offer alternatives, not lectures. Never restate what they know; never explain how to use the advisor.
18
+ </communication>
19
+
20
+ <critical>
21
+ You SHOULD call `advise` when: agent might be heading the wrong way, missed an edge case, about to call a hallucinated API, going in circles, picking brittle approach over better one. Low confidence bar — "this might be wrong" is worth noting if they didn't think about it.
22
+ NEVER advise just to second-guess decisions the agent understands and is committed to, if you are not certain.
23
+ </critical>
24
+
25
+ <completeness>
26
+ **`nit`** — Non-urgent cleanup, refactor, style, missed opportunity. Folded at next step boundary; agent keeps working. Examples: edge cases that don't break correctness, simplifications, better approach the agent can consider.
27
+ **`concern`** — Agent might be heading wrong or missed something material. Offers your view; agent decides. Use when: exploring wrong code path, picking fragile approach when better exists, missing constraint, hallucinated API, going in circles, edge case about to be baked in.
28
+ **`blocker`** — Stop and reconsider. Use ONLY when: continuing will clearly waste the turn, produce broken output, or the path is fundamentally unsound. Verify thoroughly before raising.
29
+ </completeness>
30
+
31
+ You MAY suggest an approach or fix if you've explored enough to be confident. Your job is pair programming, not just bugs — offer the better designs, not just the warning.
@@ -4,7 +4,6 @@ description: Wise senior engineer to consult or delegate work to — debugging,
4
4
  spawns: explore
5
5
  model: pi/slow
6
6
  thinking-level: xhigh
7
- blocking: true
8
7
  ---
9
8
 
10
9
  You are the wise guy on the team — a senior engineer with deep judgment that other agents consult when they are stuck, uncertain, or need a second opinion. You also take direct delegation: if the caller hands you work, you do it, including reads, writes, edits, and running commands.
@@ -5,7 +5,6 @@ tools: read, search, find, bash, lsp, web_search, ast_grep, report_finding
5
5
  spawns: explore
6
6
  model: pi/slow
7
7
  thinking-level: high
8
- blocking: true
9
8
  output:
10
9
  properties:
11
10
  overall_correctness:
@@ -0,0 +1,17 @@
1
+ You are checking whether an assistant message is an unexpected stop. A message is an unexpected stop if the assistant says it will take an action, continue working, or call a tool, but then ends without actually doing so.
2
+
3
+ Examples of unexpected stops:
4
+ - "I should do the same for the JS eval worker. Doing that now."
5
+ - "Let me run the tests next."
6
+ - "I'll fix that now."
7
+ - "Should I do that for you?"
8
+
9
+ Not an unexpected stop:
10
+ - "I've completed the task."
11
+ - "Is there anything else I can help with?"
12
+ - "The fix is done and tests pass."
13
+
14
+ Message:
15
+ {{message}}
16
+
17
+ Answer with a single word: YES if this is an unexpected stop, NO otherwise.
@@ -0,0 +1,4 @@
1
+ <system-injection>
2
+ You said you would continue with a tool call or action but stopped. Continue now.
3
+ Attempt #{{retryCount}}/{{maxRetries}}
4
+ </system-injection>
package/src/sdk.ts CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  type SimpleStreamOptions,
17
17
  streamSimple,
18
18
  } from "@oh-my-pi/pi-ai";
19
- import type { ToolCallSyntax } from "@oh-my-pi/pi-ai/grammar";
19
+ import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
20
20
  import {
21
21
  getOpenAICodexTransportDetails,
22
22
  prewarmOpenAICodexResponses,
@@ -35,6 +35,7 @@ import {
35
35
  prompt,
36
36
  Snowflake,
37
37
  } from "@oh-my-pi/pi-utils";
38
+ import { ADVISOR_READONLY_TOOL_NAMES, discoverWatchdogFiles } from "./advisor";
38
39
  import { type AsyncJob, AsyncJobManager } from "./async";
39
40
  import { AutoLearnController, buildAutoLearnInstructions } from "./autolearn/controller";
40
41
  import { loadCapability } from "./capability";
@@ -551,12 +552,12 @@ export interface CreateAgentSessionResult {
551
552
  eventBus: EventBus;
552
553
  }
553
554
 
554
- export type ToolCallFormat = "auto" | "native" | ToolCallSyntax;
555
+ export type DialectFormat = "auto" | "native" | Dialect;
555
556
 
556
- export function resolveToolCallSyntax(
557
- format: ToolCallFormat,
557
+ export function resolveDialect(
558
+ format: DialectFormat,
558
559
  model: Pick<Model, "supportsTools"> | undefined,
559
- ): ToolCallSyntax | undefined {
560
+ ): Dialect | undefined {
560
561
  if (format === "native") return undefined;
561
562
  if (format === "auto") return model?.supportsTools === false ? "glm" : undefined;
562
563
  return format;
@@ -1141,6 +1142,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1141
1142
  ? Promise.resolve(options.contextFiles)
1142
1143
  : logger.time("discoverContextFiles", discoverContextFiles, cwd, agentDir);
1143
1144
  contextFilesPromise.catch(() => {});
1145
+ const watchdogFilesPromise = logger.time("discoverWatchdogFiles", () => discoverWatchdogFiles(cwd, agentDir));
1146
+ watchdogFilesPromise.catch(() => {});
1144
1147
  const promptTemplatesPromise = options.promptTemplates
1145
1148
  ? Promise.resolve(options.promptTemplates)
1146
1149
  : logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir);
@@ -1370,9 +1373,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1370
1373
  }
1371
1374
  return result;
1372
1375
  };
1373
- const [contextFiles, resolvedWorkspaceTree] = await Promise.all([
1376
+ const [contextFiles, resolvedWorkspaceTree, watchdogFiles] = await Promise.all([
1374
1377
  contextFilesPromise,
1375
1378
  raceWithDeadline("buildWorkspaceTree", workspaceTreePromise),
1379
+ watchdogFilesPromise,
1376
1380
  ]);
1377
1381
 
1378
1382
  let agent: Agent;
@@ -1608,8 +1612,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1608
1612
  let startDeferredMCPDiscovery:
1609
1613
  | ((liveSession: AgentSession, activation: DeferredMCPActivation) => void)
1610
1614
  | undefined;
1615
+ const startupQuiet = settings.get("startup.quiet");
1611
1616
  const onMCPConnecting = (serverNames: string[]) => {
1612
- if (!options.hasUI || serverNames.length === 0) return;
1617
+ if (!options.hasUI || startupQuiet || serverNames.length === 0) return;
1613
1618
  eventBus.emit(MCP_CONNECTING_EVENT_CHANNEL, { serverNames } satisfies McpConnectingEvent);
1614
1619
  };
1615
1620
  const mcpDiscoverOptions = {
@@ -2161,10 +2166,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2161
2166
  }
2162
2167
  appendPrompt = parts.join("\n\n");
2163
2168
  }
2164
- // Owned/in-band tool syntax (non-native) repeats the catalog as `# Tool:`
2169
+ // Owned/in-band tool dialect (non-native) repeats the catalog as `# Tool:`
2165
2170
  // sections; native tool calling lets the compact name list suffice.
2166
- const nativeTools =
2167
- resolveToolCallSyntax(settings.get("tools.format"), agent?.state.model ?? model) === undefined;
2171
+ const nativeTools = resolveDialect(settings.get("tools.format"), agent?.state.model ?? model) === undefined;
2168
2172
  const defaultPrompt = await buildSystemPromptInternal({
2169
2173
  cwd,
2170
2174
  skills,
@@ -2506,7 +2510,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2506
2510
  return result;
2507
2511
  },
2508
2512
  intentTracing: !!intentField,
2509
- toolCallSyntax: resolveToolCallSyntax(settings.get("tools.format"), model),
2513
+ dialect: resolveDialect(settings.get("tools.format"), model),
2510
2514
  abortOnFabricatedToolResult: settings.get("tools.abortOnFabricatedResult"),
2511
2515
  getToolChoice: () => session?.nextToolChoice(),
2512
2516
  telemetry: options.telemetry,
@@ -2537,7 +2541,41 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2537
2541
  }
2538
2542
  }
2539
2543
 
2544
+ // Hard-isolated read-only toolset for the advisor (built unconditionally so
2545
+ // it can be toggled at runtime). Fresh ReadTool/SearchTool/FindTool bound to a
2546
+ // DISTINCT ToolSession so the advisor's investigative reads never touch the
2547
+ // primary's snapshot, seen-lines, conflict, or summary caches (all keyed on
2548
+ // session identity). `cwd` stays dynamic; edit/yield capabilities are off.
2549
+ const advisorToolSession: ToolSession = {
2550
+ ...toolSession,
2551
+ get cwd() {
2552
+ return sessionManager.getCwd();
2553
+ },
2554
+ hasEditTool: false,
2555
+ requireYieldTool: false,
2556
+ conflictHistory: undefined,
2557
+ fileSnapshotStore: undefined,
2558
+ getSessionId: () => {
2559
+ const id = sessionManager.getSessionId?.();
2560
+ return id ? `${id}-advisor` : null;
2561
+ },
2562
+ getAgentId: () => "advisor",
2563
+ };
2564
+ const built = await Promise.all(
2565
+ [...ADVISOR_READONLY_TOOL_NAMES].map(name =>
2566
+ BUILTIN_TOOLS[name as keyof typeof BUILTIN_TOOLS](advisorToolSession),
2567
+ ),
2568
+ );
2569
+ const advisorReadOnlyTools: Tool[] = built
2570
+ .filter((tool): tool is Tool => tool != null)
2571
+ .map(wrapToolWithMetaNotice);
2572
+
2573
+ let advisorWatchdogPrompt: string | undefined;
2574
+ if (watchdogFiles && watchdogFiles.length > 0) {
2575
+ advisorWatchdogPrompt = watchdogFiles.join("\n\n");
2576
+ }
2540
2577
  session = new AgentSession({
2578
+ advisorWatchdogPrompt,
2541
2579
  agent,
2542
2580
  thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
2543
2581
  sessionManager,
@@ -2592,6 +2630,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2592
2630
  agentKind,
2593
2631
  providerSessionId: options.providerSessionId,
2594
2632
  parentEvalSessionId: options.parentEvalSessionId,
2633
+ advisorReadOnlyTools,
2595
2634
  });
2596
2635
  hasSession = true;
2597
2636
  if (asyncJobManager) {
@@ -2696,7 +2735,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2696
2735
  type: "completed",
2697
2736
  servers: result.servers,
2698
2737
  };
2699
- eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
2738
+ if (!startupQuiet) eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
2700
2739
  } catch (error) {
2701
2740
  const errorMessage = error instanceof Error ? error.message : String(error);
2702
2741
  logger.warn("LSP server warmup failed", { cwd, error: errorMessage });
@@ -2708,7 +2747,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2708
2747
  type: "failed",
2709
2748
  error: errorMessage,
2710
2749
  };
2711
- eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
2750
+ if (!startupQuiet) eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
2712
2751
  }
2713
2752
  })();
2714
2753
  }