@oh-my-pi/pi-coding-agent 15.13.3 → 16.0.1

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 (93) hide show
  1. package/CHANGELOG.md +155 -133
  2. package/dist/cli.js +621 -530
  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 +66 -5
  10. package/dist/types/discovery/helpers.d.ts +7 -0
  11. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
  12. package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
  13. package/dist/types/modes/components/advisor-message.d.ts +9 -0
  14. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  15. package/dist/types/modes/controllers/command-controller.d.ts +3 -1
  16. package/dist/types/modes/interactive-mode.d.ts +3 -1
  17. package/dist/types/modes/types.d.ts +8 -1
  18. package/dist/types/sdk.d.ts +3 -3
  19. package/dist/types/session/agent-session.d.ts +81 -2
  20. package/dist/types/session/session-history-format.d.ts +4 -0
  21. package/dist/types/session/session-manager.d.ts +4 -1
  22. package/dist/types/session/yield-queue.d.ts +2 -0
  23. package/dist/types/task/index.d.ts +21 -0
  24. package/dist/types/tools/github-cache.d.ts +5 -4
  25. package/dist/types/tools/job.d.ts +1 -0
  26. package/dist/types/tools/path-utils.d.ts +1 -0
  27. package/dist/types/tools/report-tool-issue.d.ts +0 -1
  28. package/dist/types/web/search/index.d.ts +2 -2
  29. package/dist/types/web/search/provider.d.ts +2 -0
  30. package/package.json +13 -13
  31. package/src/advisor/__tests__/advisor.test.ts +586 -0
  32. package/src/advisor/advise-tool.ts +87 -0
  33. package/src/advisor/index.ts +3 -0
  34. package/src/advisor/runtime.ts +248 -0
  35. package/src/advisor/watchdog.ts +83 -0
  36. package/src/cli/args.ts +1 -0
  37. package/src/collab/host.ts +1 -1
  38. package/src/config/model-roles.ts +13 -1
  39. package/src/config/settings-schema.ts +65 -6
  40. package/src/discovery/claude-plugins.ts +3 -42
  41. package/src/discovery/github.ts +101 -6
  42. package/src/discovery/helpers.ts +11 -0
  43. package/src/eval/__tests__/prelude-agent.test.ts +73 -0
  44. package/src/eval/js/shared/prelude.txt +12 -3
  45. package/src/eval/py/prelude.py +26 -2
  46. package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
  47. package/src/extensibility/plugins/loader.ts +3 -2
  48. package/src/extensibility/plugins/manager.ts +4 -3
  49. package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
  50. package/src/extensibility/plugins/runtime-config.ts +9 -0
  51. package/src/internal-urls/docs-index.generated.ts +10 -9
  52. package/src/internal-urls/issue-pr-protocol.ts +8 -4
  53. package/src/main.ts +9 -1
  54. package/src/modes/acp/acp-agent.ts +3 -3
  55. package/src/modes/components/advisor-message.ts +99 -0
  56. package/src/modes/components/agent-hub.ts +7 -0
  57. package/src/modes/components/assistant-message.ts +86 -0
  58. package/src/modes/components/settings-defs.ts +7 -0
  59. package/src/modes/components/status-line/segments.ts +20 -7
  60. package/src/modes/components/tips.txt +1 -1
  61. package/src/modes/controllers/command-controller.ts +69 -2
  62. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  63. package/src/modes/controllers/input-controller.ts +1 -0
  64. package/src/modes/controllers/selector-controller.ts +7 -0
  65. package/src/modes/interactive-mode.ts +59 -2
  66. package/src/modes/rpc/rpc-mode.ts +3 -3
  67. package/src/modes/runtime-init.ts +2 -1
  68. package/src/modes/types.ts +8 -1
  69. package/src/modes/utils/ui-helpers.ts +9 -0
  70. package/src/prompts/advisor/advise-tool.md +1 -0
  71. package/src/prompts/advisor/system.md +31 -0
  72. package/src/prompts/agents/designer.md +8 -0
  73. package/src/prompts/review-request.md +1 -1
  74. package/src/prompts/system/subagent-system-prompt.md +4 -1
  75. package/src/prompts/tools/eval.md +13 -3
  76. package/src/prompts/tools/irc.md +1 -1
  77. package/src/sdk.ts +61 -14
  78. package/src/session/agent-session.ts +667 -13
  79. package/src/session/session-dump-format.ts +15 -131
  80. package/src/session/session-history-format.ts +30 -11
  81. package/src/session/session-manager.ts +3 -1
  82. package/src/session/yield-queue.ts +5 -1
  83. package/src/slash-commands/builtin-registry.ts +105 -4
  84. package/src/system-prompt.ts +1 -1
  85. package/src/task/executor.ts +5 -4
  86. package/src/task/index.ts +70 -9
  87. package/src/tools/github-cache.ts +32 -7
  88. package/src/tools/job.ts +14 -1
  89. package/src/tools/path-utils.ts +33 -2
  90. package/src/tools/report-tool-issue.ts +2 -7
  91. package/src/web/scrapers/docs-rs.ts +2 -3
  92. package/src/web/search/index.ts +2 -2
  93. package/src/web/search/provider.ts +14 -2
@@ -92,6 +92,7 @@ import { STTController, type SttState } from "../stt";
92
92
  import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
93
93
  import { formatTaskId } from "../task/render";
94
94
  import type { LspStartupServerInfo } from "../tools";
95
+ import { isImageProviderPreference, setPreferredImageProvider } from "../tools/image-gen";
95
96
  import { normalizeLocalScheme } from "../tools/path-utils";
96
97
  import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../tools/render-utils";
97
98
  import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
@@ -103,6 +104,12 @@ import type { EventBus } from "../utils/event-bus";
103
104
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
104
105
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
105
106
  import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
107
+ import {
108
+ isSearchProviderId,
109
+ isSearchProviderPreference,
110
+ setExcludedSearchProviders,
111
+ setPreferredSearchProvider,
112
+ } from "../web/search";
106
113
  import type { AssistantMessageComponent } from "./components/assistant-message";
107
114
  import type { BashExecutionComponent } from "./components/bash-execution";
108
115
  import { ChatBlock, type ChatBlockHost } from "./components/chat-block";
@@ -542,6 +549,7 @@ export class InteractiveMode implements InteractiveModeContext {
542
549
  if (eventBus) {
543
550
  this.#eventBusUnsubscribers.push(
544
551
  eventBus.on(LSP_STARTUP_EVENT_CHANNEL, data => {
552
+ if (this.settings.get("startup.quiet")) return;
545
553
  this.#handleLspStartupEvent(data as LspStartupEvent);
546
554
  }),
547
555
  );
@@ -551,6 +559,7 @@ export class InteractiveMode implements InteractiveModeContext {
551
559
  logger.warn("Ignoring malformed mcp:connecting event", { data });
552
560
  return;
553
561
  }
562
+ if (this.settings.get("startup.quiet")) return;
554
563
  this.showStatus(formatMCPConnectingMessage(data.serverNames));
555
564
  }),
556
565
  );
@@ -788,6 +797,30 @@ export class InteractiveMode implements InteractiveModeContext {
788
797
  this.session.setSessionSwitchReconciler?.(() => this.#reconcileModeFromSession());
789
798
  await this.#reconcileModeFromSession();
790
799
 
800
+ // Brand-new sessions optionally start in plan mode when the user has made it
801
+ // the startup default. "Brand-new" means the resolved branch carries no
802
+ // conversation context (buildSessionContext().messages — covers messages,
803
+ // custom messages, branch summaries, and compaction summaries) and the user
804
+ // set no explicit `mode_change` (which #reconcileModeFromSession just
805
+ // restored). SDK startup metadata and extension `custom` state entries are
806
+ // ignored. This way `omp --continue` (or auto-resume) that finds no recent
807
+ // session and creates a fresh one still honors the default, while a session
808
+ // with restored context or an explicit mode keeps its reconciled mode. Scoped
809
+ // to launch (not the switch reconciler above) so /new and the plan-approval →
810
+ // execution handoff clear never get dragged back into plan mode. #enterPlanMode
811
+ // is idempotent and self-guards against an already-active plan/goal mode; it
812
+ // does not check plan.enabled itself.
813
+ const hasConversationContext = this.sessionManager.buildSessionContext().messages.length > 0;
814
+ const hasExplicitMode = this.sessionManager.getEntries().some(entry => entry.type === "mode_change");
815
+ const isFreshSession = !hasConversationContext && !hasExplicitMode;
816
+ if (
817
+ isFreshSession &&
818
+ this.session.settings.get("plan.defaultOnStartup") &&
819
+ this.session.settings.get("plan.enabled")
820
+ ) {
821
+ await this.#enterPlanMode();
822
+ }
823
+
791
824
  // Restore unsent editor draft from previous session shutdown (Ctrl+D).
792
825
  // One-shot: consumeDraft removes the sidecar after read so the next
793
826
  // resume does not re-restore the same text.
@@ -902,6 +935,22 @@ export class InteractiveMode implements InteractiveModeContext {
902
935
  // up the destination project's configuration.
903
936
  if (isSettingsInitialized()) {
904
937
  await settings.reloadForCwd(newCwd);
938
+ // Reapply provider preferences from the newly-loaded settings so the
939
+ // module-level search/image provider state reflects the destination
940
+ // project's configuration. Without this, the previous project's
941
+ // exclusions leak and newly-excluded providers are still used.
942
+ const excludedWebSearchProviders = settings.get("providers.webSearchExclude");
943
+ if (Array.isArray(excludedWebSearchProviders)) {
944
+ setExcludedSearchProviders(excludedWebSearchProviders.filter(isSearchProviderId));
945
+ }
946
+ const webSearchProvider = settings.get("providers.webSearch");
947
+ if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
948
+ setPreferredSearchProvider(webSearchProvider);
949
+ }
950
+ const imageProvider = settings.get("providers.image");
951
+ if (isImageProviderPreference(imageProvider)) {
952
+ setPreferredImageProvider(imageProvider);
953
+ }
905
954
  }
906
955
  // Re-warm plugin roots, capabilities, slash commands, and the ssh tool so
907
956
  // the next prompt sees everything scoped to the new project directory.
@@ -3333,8 +3382,12 @@ export class InteractiveMode implements InteractiveModeContext {
3333
3382
  return this.#commandController.handleExportCommand(text);
3334
3383
  }
3335
3384
 
3336
- handleDumpCommand() {
3337
- return this.#commandController.handleDumpCommand();
3385
+ handleDumpCommand(isRaw?: boolean) {
3386
+ return this.#commandController.handleDumpCommand(isRaw);
3387
+ }
3388
+
3389
+ handleAdvisorDumpCommand(isRaw?: boolean) {
3390
+ return this.#commandController.handleAdvisorDumpCommand(isRaw);
3338
3391
  }
3339
3392
 
3340
3393
  handleDebugTranscriptCommand(): Promise<void> {
@@ -3353,6 +3406,10 @@ export class InteractiveMode implements InteractiveModeContext {
3353
3406
  return this.#commandController.handleSessionCommand();
3354
3407
  }
3355
3408
 
3409
+ handleAdvisorStatusCommand(): Promise<void> {
3410
+ return this.#commandController.handleAdvisorStatusCommand();
3411
+ }
3412
+
3356
3413
  handleJobsCommand(): Promise<void> {
3357
3414
  return this.#commandController.handleJobsCommand();
3358
3415
  }
@@ -26,7 +26,7 @@ import { buildSkillPromptMessage } from "../../extensibility/skills";
26
26
  import { loadSlashCommands } from "../../extensibility/slash-commands";
27
27
  import { type Theme, theme } from "../../modes/theme/theme";
28
28
  import type { AgentSession } from "../../session/agent-session";
29
- import { SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
29
+ import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../../session/messages";
30
30
  import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
31
31
  import { buildAvailableSlashCommands } from "../../slash-commands/available-commands";
32
32
  import type { EventBus } from "../../utils/event-bus";
@@ -751,12 +751,12 @@ export async function runRpcMode(
751
751
  }
752
752
 
753
753
  case "abort": {
754
- await session.abort();
754
+ await session.abort({ reason: USER_INTERRUPT_LABEL });
755
755
  return success(id, "abort");
756
756
  }
757
757
 
758
758
  case "abort_and_prompt": {
759
- await session.abort();
759
+ await session.abort({ reason: USER_INTERRUPT_LABEL });
760
760
  session
761
761
  .prompt(command.message, { images: command.images })
762
762
  .catch(e => output(error(id, "abort_and_prompt", e.message)));
@@ -10,6 +10,7 @@ import { runExtensionCompact, runExtensionSetModel } from "../extensibility/exte
10
10
  import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
11
11
  import type { ExtensionError, ExtensionUIContext } from "../extensibility/extensions/types";
12
12
  import type { AgentSession } from "../session/agent-session";
13
+ import { USER_INTERRUPT_LABEL } from "../session/messages";
13
14
 
14
15
  /** Action name for an extension-originated send failure. */
15
16
  export type ExtensionSendAction = "extension_send" | "extension_send_user";
@@ -98,7 +99,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
98
99
  {
99
100
  getModel: () => session.model,
100
101
  isIdle: () => !session.isStreaming,
101
- abort: () => session.abort(),
102
+ abort: () => session.abort({ reason: USER_INTERRUPT_LABEL }),
102
103
  hasPendingMessages: () => session.queuedMessageCount > 0,
103
104
  shutdown,
104
105
  getContextUsage: () => session.getContextUsage(),
@@ -53,6 +53,11 @@ export type SubmittedUserInput = {
53
53
  * as a hidden agent-authored `developer` message rather than a visible user
54
54
  * turn. Used by the `c`/`.` continue shortcut. */
55
55
  synthetic?: boolean;
56
+ /** Marks this submission as a deliberate user resume (set by the `.`/`c`
57
+ * continue shortcut, which is also `synthetic`). Forwarded to
58
+ * `session.prompt({ userInitiated })` so it clears advisor auto-resume
59
+ * suppression even though it is synthetic. */
60
+ userInitiated?: boolean;
56
61
  display?: boolean;
57
62
  /** Queue intent if the session is (or becomes) busy when this submission is
58
63
  * dispatched: "steer" (interrupt the active turn) or "followUp" (process after
@@ -270,13 +275,15 @@ export interface InteractiveModeContext {
270
275
  handleShareCommand(): Promise<void>;
271
276
  handleTodoCommand(args: string): Promise<void>;
272
277
  handleSessionCommand(): Promise<void>;
278
+ handleAdvisorStatusCommand(): Promise<void>;
273
279
  handleJobsCommand(): Promise<void>;
274
280
  handleUsageCommand(reports?: UsageReport[] | null): Promise<void>;
275
281
  handleChangelogCommand(showFull?: boolean): Promise<void>;
276
282
  handleHotkeysCommand(): void;
277
283
  handleToolsCommand(): void;
278
284
  handleContextCommand(): void;
279
- handleDumpCommand(): void;
285
+ handleDumpCommand(isRaw?: boolean): void;
286
+ handleAdvisorDumpCommand(isRaw?: boolean): void;
280
287
  handleDebugTranscriptCommand(): Promise<void>;
281
288
  handleClearCommand(): Promise<void>;
282
289
  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";
@@ -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;
@@ -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.
@@ -14,6 +14,14 @@ Implement and review UI designs. Edit files, create components, run commands whe
14
14
  - Responsive design, layout structure
15
15
  </strengths>
16
16
 
17
+ <design-system>
18
+ Treat the design system as the foundation — UI built without one collapses into inconsistency. Work four phases in order:
19
+ 1. **Token-first analysis (before any CSS/JSX/Svelte).** `search`/`read` for the design tokens (colors, spacing, typography, shadows, radii), theme files (CSS variables, Tailwind config, `theme.ts`), and shared primitives (Button, Card, Input, Layout). Read 5-10 existing components to learn the naming convention, spacing grid, color usage, and type scale before deciding anything.
20
+ 2. **No coherent system? Build the minimal one first.** Extract what exists, then define a palette, type scale, spacing scale (4px/8px base), radii/shadows/transitions, and primitive components — THEN implement the request against it.
21
+ 3. **Compose with the system, never around it.** Colors → tokens/CSS variables, never hardcoded hex; spacing → scale values, never arbitrary px; type → scale steps; components → extend/compose existing primitives, not one-off div soup. Need something outside the system? Add the new token to the system first, then use it — never a one-off override.
22
+ 4. **Verify before done.** Every color a token, every spacing on the scale, every component on the existing composition pattern, zero magic numbers — a designer would see consistency across old and new. Any "no" → not done.
23
+ </design-system>
24
+
17
25
  <procedure>
18
26
  ## Implementation
19
27
  1. Read existing components, tokens, patterns—reuse before inventing
@@ -37,7 +37,7 @@ Group files by locality, e.g.:
37
37
  Reviewer MUST:
38
38
  1. Focus ONLY on assigned files
39
39
  2. {{#if skipDiff}}{{diffInstruction}}{{else}}MUST use diff hunks below (NEVER re-run git diff){{/if}}
40
- 3. MAY read full file context as needed via `read`
40
+ 3. {{contextInstruction}}
41
41
  4. Call `report_finding` per issue
42
42
  5. Call `yield` with verdict when done
43
43
 
@@ -41,7 +41,10 @@ You NEVER modify files outside this tree or in the original repository.
41
41
  You can reach other live agents via the `irc` tool. Your id is `{{ircSelfId}}`. Currently visible peers:
42
42
  {{ircPeers}}
43
43
 
44
- Use `irc` only when you need a quick answer from a peer; NEVER use it for long-form content. Address peers by id or use `"all"` to broadcast.
44
+ Use `irc` only for quick coordination, never long-form content. Address peers by id or use `"all"` to broadcast.
45
+ - Discovery: the roster above shows each peer's role and what it is doing now; `irc` op:"list" refreshes it.
46
+ - Coordination: before you edit a file or start work a sibling may already own, message that peer first — overlapping edits collide.
47
+ - Follow-up: answer a peer's question with a short reply (set `replyTo`); use `await` only when you genuinely cannot proceed without the answer.
45
48
  {{/if}}
46
49
 
47
50
  COMPLETION
@@ -41,9 +41,9 @@ tool.<name>(args) → unknown
41
41
  Invoke any session tool; `args` is its parameter object.
42
42
  completion(prompt, model?="default", system?=None, schema?=None) → str | dict
43
43
  Oneshot stateless completion (no history, no tools). `model` tier: "smol" (fast) | "default" (session model) | "slow" (most capable). JSON-Schema `schema` forces structured output, returns parsed object.
44
- {{#if spawns}}agent(prompt, agent_type?="task", model?=None, label?=None, schema?=None) → str | dict
45
- Run a subagent, return its final output. `agent_type`/`agentType` picks another discovered agent; `schema` as in completion(). Share background via `local://` files referenced in the prompt.
46
- {{#if js}} JS: options are ONE trailing object — agent(prompt, { agentType, schema }).
44
+ {{#if spawns}}agent(prompt, agent_type?="task", model?=None, label?=None, schema?=None, return_handle?=False) → str | dict
45
+ Run a subagent, return its final output. `agent_type`/`agentType` picks another discovered agent; `schema` as in completion(). Share background via `local://` files referenced in the prompt. `return_handle`/`returnHandle` → a DAG node dict { text, output, handle: "agent://<id>", id, agent } (parsed object under `data` when `schema` set) so a downstream stage references the transcript by handle instead of re-inlining it.
46
+ {{#if js}} JS: options are ONE trailing object — agent(prompt, { agentType, schema, returnHandle }).
47
47
  {{/if}}
48
48
  {{/if}}
49
49
  parallel(thunks) → list
@@ -58,3 +58,13 @@ budget → per-turn token budget
58
58
  {{#if py}}`budget.total` (ceiling or None), `budget.spent()`, `budget.remaining()` (math.inf when no ceiling), `budget.hard` (bool).{{/if}}{{#if js}}`await budget.total()` (ceiling or null), `await budget.spent()`, `await budget.remaining()` (Infinity when no ceiling), `await budget.hard()`.{{/if}} Ceiling comes from a `+Nk` directive (advisory) or `+Nk!`/Goal Mode (hard — `agent()` refuses to spawn past it); otherwise None/null, spend still tracked across the turn.
59
59
  ```
60
60
  </prelude>
61
+ {{#if spawns}}
62
+ <dag>
63
+ Build a dependency graph by piping handles through the stage helpers — ephemeral, in-session, acyclic waves:
64
+ - **Name nodes.** Capture each `agent(…, {{#if py}}return_handle=True{{/if}}{{#if js}}{ returnHandle: true }{{/if}})` result; it carries `handle` (`agent://<id>`) + `output`.
65
+ - **Wire edges by reference.** Embed an upstream node's `handle` or `output` in the dependent stage's prompt so a large transcript flows by reference, never re-inlined. For bulk artifacts, `write("local://<name>.md", …)` and pass the URI.
66
+ - **`pipeline(items, *stages)` = staged waves** with a barrier between stages (every item clears stage N before any enters stage N+1) — the linear spine of a DAG. **`parallel(thunks)` = one wave** of independent nodes.
67
+ - **Isolate failure.** A raising node re-raises the lowest-index error and aborts its wave; wrap each risky node in try/except so a failed node degrades only its dependent subtree while independent branches still finish.
68
+ - **Acyclic only.** A node never waits on its own descendant; cycles are an authoring bug, not a supported pattern.
69
+ </dag>
70
+ {{/if}}
@@ -17,7 +17,7 @@ Reach for `irc` proactively when continuing alone is wasteful or wrong; when in
17
17
  - **Unexpected state** — missing file, config contradicting the assignment, API/tool behaving differently than told. DM `Main` (or your spawner) instead of guessing.
18
18
  - **Blocked by another agent** — a peer holds the file/branch/resource or decision you need, or started the change you're about to make. DM them (or broadcast to discover who) before duplicating work.
19
19
  - **Decision outside your scope** — a genuine fork the assignment didn't pre-decide. Ask the requester rather than picking unilaterally.
20
- - **Coordination** — a peer's in-flight work would benefit from yours, or vice-versa.
20
+ - **Coordination** — a peer's in-flight work overlaps yours (the roster shows each peer's role and current activity); message before editing a shared file or duplicating a sibling's change.
21
21
 
22
22
  NEVER for: routine progress updates, things a tool call can verify, questions your assignment/repo/docs already answer.
23
23
  </when_to_use>
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";
@@ -128,6 +129,7 @@ import {
128
129
  type CustomMessage,
129
130
  convertToLlm,
130
131
  LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE,
132
+ USER_INTERRUPT_LABEL,
131
133
  wrapSteeringForModel,
132
134
  } from "./session/messages";
133
135
  import { getRestorableSessionModels } from "./session/session-context";
@@ -176,6 +178,7 @@ import {
176
178
  getSearchTools,
177
179
  HIDDEN_TOOLS,
178
180
  isImageProviderPreference,
181
+ isSearchProviderId,
179
182
  isSearchProviderPreference,
180
183
  type LspStartupServerInfo,
181
184
  loadSshTool,
@@ -184,6 +187,7 @@ import {
184
187
  renderSearchToolBm25Description,
185
188
  SearchTool,
186
189
  SearchToolBm25Tool,
190
+ setExcludedSearchProviders,
187
191
  setPreferredImageProvider,
188
192
  setPreferredSearchProvider,
189
193
  type Tool,
@@ -551,12 +555,12 @@ export interface CreateAgentSessionResult {
551
555
  eventBus: EventBus;
552
556
  }
553
557
 
554
- export type ToolCallFormat = "auto" | "native" | ToolCallSyntax;
558
+ export type DialectFormat = "auto" | "native" | Dialect;
555
559
 
556
- export function resolveToolCallSyntax(
557
- format: ToolCallFormat,
560
+ export function resolveDialect(
561
+ format: DialectFormat,
558
562
  model: Pick<Model, "supportsTools"> | undefined,
559
- ): ToolCallSyntax | undefined {
563
+ ): Dialect | undefined {
560
564
  if (format === "native") return undefined;
561
565
  if (format === "auto") return model?.supportsTools === false ? "glm" : undefined;
562
566
  return format;
@@ -1141,6 +1145,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1141
1145
  ? Promise.resolve(options.contextFiles)
1142
1146
  : logger.time("discoverContextFiles", discoverContextFiles, cwd, agentDir);
1143
1147
  contextFilesPromise.catch(() => {});
1148
+ const watchdogFilesPromise = logger.time("discoverWatchdogFiles", () => discoverWatchdogFiles(cwd, agentDir));
1149
+ watchdogFilesPromise.catch(() => {});
1144
1150
  const promptTemplatesPromise = options.promptTemplates
1145
1151
  ? Promise.resolve(options.promptTemplates)
1146
1152
  : logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir);
@@ -1161,6 +1167,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1161
1167
  discoveredSkillsPromise?.catch(() => {});
1162
1168
 
1163
1169
  // Initialize provider preferences from settings
1170
+ const excludedWebSearchProviders = settings.get("providers.webSearchExclude");
1171
+ if (Array.isArray(excludedWebSearchProviders)) {
1172
+ setExcludedSearchProviders(excludedWebSearchProviders.filter(isSearchProviderId));
1173
+ }
1174
+
1164
1175
  const webSearchProvider = settings.get("providers.webSearch");
1165
1176
  if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
1166
1177
  setPreferredSearchProvider(webSearchProvider);
@@ -1370,9 +1381,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1370
1381
  }
1371
1382
  return result;
1372
1383
  };
1373
- const [contextFiles, resolvedWorkspaceTree] = await Promise.all([
1384
+ const [contextFiles, resolvedWorkspaceTree, watchdogFiles] = await Promise.all([
1374
1385
  contextFilesPromise,
1375
1386
  raceWithDeadline("buildWorkspaceTree", workspaceTreePromise),
1387
+ watchdogFilesPromise,
1376
1388
  ]);
1377
1389
 
1378
1390
  let agent: Agent;
@@ -1608,8 +1620,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1608
1620
  let startDeferredMCPDiscovery:
1609
1621
  | ((liveSession: AgentSession, activation: DeferredMCPActivation) => void)
1610
1622
  | undefined;
1623
+ const startupQuiet = settings.get("startup.quiet");
1611
1624
  const onMCPConnecting = (serverNames: string[]) => {
1612
- if (!options.hasUI || serverNames.length === 0) return;
1625
+ if (!options.hasUI || startupQuiet || serverNames.length === 0) return;
1613
1626
  eventBus.emit(MCP_CONNECTING_EVENT_CHANNEL, { serverNames } satisfies McpConnectingEvent);
1614
1627
  };
1615
1628
  const mcpDiscoverOptions = {
@@ -1992,7 +2005,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1992
2005
  isIdle: () => !session.isStreaming,
1993
2006
  hasQueuedMessages: () => session.queuedMessageCount > 0,
1994
2007
  abort: () => {
1995
- session.abort();
2008
+ session.abort({ reason: USER_INTERRUPT_LABEL });
1996
2009
  },
1997
2010
  settings,
1998
2011
  autoApprove: options.autoApprove ?? false,
@@ -2161,10 +2174,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2161
2174
  }
2162
2175
  appendPrompt = parts.join("\n\n");
2163
2176
  }
2164
- // Owned/in-band tool syntax (non-native) repeats the catalog as `# Tool:`
2177
+ // Owned/in-band tool dialect (non-native) repeats the catalog as `# Tool:`
2165
2178
  // sections; native tool calling lets the compact name list suffice.
2166
- const nativeTools =
2167
- resolveToolCallSyntax(settings.get("tools.format"), agent?.state.model ?? model) === undefined;
2179
+ const nativeTools = resolveDialect(settings.get("tools.format"), agent?.state.model ?? model) === undefined;
2168
2180
  const defaultPrompt = await buildSystemPromptInternal({
2169
2181
  cwd,
2170
2182
  skills,
@@ -2506,7 +2518,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2506
2518
  return result;
2507
2519
  },
2508
2520
  intentTracing: !!intentField,
2509
- toolCallSyntax: resolveToolCallSyntax(settings.get("tools.format"), model),
2521
+ dialect: resolveDialect(settings.get("tools.format"), model),
2510
2522
  abortOnFabricatedToolResult: settings.get("tools.abortOnFabricatedResult"),
2511
2523
  getToolChoice: () => session?.nextToolChoice(),
2512
2524
  telemetry: options.telemetry,
@@ -2537,7 +2549,41 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2537
2549
  }
2538
2550
  }
2539
2551
 
2552
+ // Hard-isolated read-only toolset for the advisor (built unconditionally so
2553
+ // it can be toggled at runtime). Fresh ReadTool/SearchTool/FindTool bound to a
2554
+ // DISTINCT ToolSession so the advisor's investigative reads never touch the
2555
+ // primary's snapshot, seen-lines, conflict, or summary caches (all keyed on
2556
+ // session identity). `cwd` stays dynamic; edit/yield capabilities are off.
2557
+ const advisorToolSession: ToolSession = {
2558
+ ...toolSession,
2559
+ get cwd() {
2560
+ return sessionManager.getCwd();
2561
+ },
2562
+ hasEditTool: false,
2563
+ requireYieldTool: false,
2564
+ conflictHistory: undefined,
2565
+ fileSnapshotStore: undefined,
2566
+ getSessionId: () => {
2567
+ const id = sessionManager.getSessionId?.();
2568
+ return id ? `${id}-advisor` : null;
2569
+ },
2570
+ getAgentId: () => "advisor",
2571
+ };
2572
+ const built = await Promise.all(
2573
+ [...ADVISOR_READONLY_TOOL_NAMES].map(name =>
2574
+ BUILTIN_TOOLS[name as keyof typeof BUILTIN_TOOLS](advisorToolSession),
2575
+ ),
2576
+ );
2577
+ const advisorReadOnlyTools: Tool[] = built
2578
+ .filter((tool): tool is Tool => tool != null)
2579
+ .map(wrapToolWithMetaNotice);
2580
+
2581
+ let advisorWatchdogPrompt: string | undefined;
2582
+ if (watchdogFiles && watchdogFiles.length > 0) {
2583
+ advisorWatchdogPrompt = watchdogFiles.join("\n\n");
2584
+ }
2540
2585
  session = new AgentSession({
2586
+ advisorWatchdogPrompt,
2541
2587
  agent,
2542
2588
  thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
2543
2589
  sessionManager,
@@ -2592,6 +2638,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2592
2638
  agentKind,
2593
2639
  providerSessionId: options.providerSessionId,
2594
2640
  parentEvalSessionId: options.parentEvalSessionId,
2641
+ advisorReadOnlyTools,
2595
2642
  });
2596
2643
  hasSession = true;
2597
2644
  if (asyncJobManager) {
@@ -2696,7 +2743,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2696
2743
  type: "completed",
2697
2744
  servers: result.servers,
2698
2745
  };
2699
- eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
2746
+ if (!startupQuiet) eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
2700
2747
  } catch (error) {
2701
2748
  const errorMessage = error instanceof Error ? error.message : String(error);
2702
2749
  logger.warn("LSP server warmup failed", { cwd, error: errorMessage });
@@ -2708,7 +2755,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2708
2755
  type: "failed",
2709
2756
  error: errorMessage,
2710
2757
  };
2711
- eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
2758
+ if (!startupQuiet) eventBus.emit(LSP_STARTUP_EVENT_CHANNEL, event);
2712
2759
  }
2713
2760
  })();
2714
2761
  }