@oh-my-pi/pi-coding-agent 16.0.0 → 16.0.2

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 (70) hide show
  1. package/CHANGELOG.md +140 -133
  2. package/dist/cli.js +250 -218
  3. package/dist/types/config/model-resolver.d.ts +14 -0
  4. package/dist/types/config/settings-schema.d.ts +22 -0
  5. package/dist/types/discovery/helpers.d.ts +7 -0
  6. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
  7. package/dist/types/exec/non-interactive-env.d.ts +2 -0
  8. package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
  9. package/dist/types/modes/types.d.ts +5 -0
  10. package/dist/types/session/agent-session.d.ts +11 -1
  11. package/dist/types/session/messages.d.ts +3 -0
  12. package/dist/types/session/session-manager.d.ts +4 -1
  13. package/dist/types/task/index.d.ts +21 -0
  14. package/dist/types/tools/github-cache.d.ts +5 -4
  15. package/dist/types/tools/job.d.ts +1 -0
  16. package/dist/types/utils/markit.d.ts +8 -0
  17. package/dist/types/web/search/index.d.ts +2 -2
  18. package/dist/types/web/search/provider.d.ts +2 -0
  19. package/package.json +12 -12
  20. package/src/advisor/__tests__/advisor.test.ts +44 -0
  21. package/src/cli/args.ts +2 -0
  22. package/src/collab/host.ts +1 -1
  23. package/src/config/model-resolver.ts +35 -1
  24. package/src/config/settings-schema.ts +23 -1
  25. package/src/discovery/claude-plugins.ts +3 -42
  26. package/src/discovery/github.ts +189 -6
  27. package/src/discovery/helpers.ts +11 -0
  28. package/src/eval/__tests__/prelude-agent.test.ts +73 -0
  29. package/src/eval/js/shared/prelude.txt +12 -3
  30. package/src/eval/py/prelude.py +26 -2
  31. package/src/exec/bash-executor.ts +2 -2
  32. package/src/exec/non-interactive-env.ts +71 -0
  33. package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
  34. package/src/extensibility/extensions/runner.ts +17 -1
  35. package/src/extensibility/plugins/loader.ts +157 -23
  36. package/src/extensibility/plugins/manager.ts +44 -36
  37. package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
  38. package/src/extensibility/plugins/runtime-config.ts +9 -0
  39. package/src/internal-urls/docs-index.generated.ts +9 -9
  40. package/src/internal-urls/issue-pr-protocol.ts +8 -4
  41. package/src/main.ts +5 -1
  42. package/src/modes/acp/acp-agent.ts +3 -3
  43. package/src/modes/components/settings-defs.ts +7 -0
  44. package/src/modes/components/tips.txt +1 -1
  45. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  46. package/src/modes/controllers/input-controller.ts +1 -0
  47. package/src/modes/controllers/selector-controller.ts +7 -0
  48. package/src/modes/interactive-mode.ts +47 -0
  49. package/src/modes/rpc/rpc-mode.ts +3 -3
  50. package/src/modes/runtime-init.ts +2 -1
  51. package/src/modes/types.ts +5 -0
  52. package/src/prompts/agents/designer.md +8 -0
  53. package/src/prompts/review-request.md +1 -1
  54. package/src/prompts/system/subagent-system-prompt.md +4 -1
  55. package/src/prompts/tools/eval.md +13 -3
  56. package/src/prompts/tools/irc.md +1 -1
  57. package/src/sdk.ts +9 -1
  58. package/src/session/agent-session.ts +260 -50
  59. package/src/session/messages.ts +1 -1
  60. package/src/session/session-manager.ts +3 -1
  61. package/src/slash-commands/builtin-registry.ts +5 -2
  62. package/src/system-prompt.ts +7 -1
  63. package/src/task/executor.ts +105 -8
  64. package/src/task/index.ts +70 -9
  65. package/src/tools/github-cache.ts +32 -7
  66. package/src/tools/job.ts +14 -1
  67. package/src/utils/lang-from-path.ts +5 -0
  68. package/src/utils/markit.ts +24 -1
  69. package/src/web/search/index.ts +2 -2
  70. package/src/web/search/provider.ts +14 -2
@@ -28,7 +28,7 @@ import {
28
28
  parsePositiveDecimalInt,
29
29
  resolveDefaultRepoMemoized,
30
30
  } from "../tools/gh";
31
- import { formatFreshnessNote } from "../tools/github-cache";
31
+ import { type CacheStatus, formatFreshnessNote } from "../tools/github-cache";
32
32
  import * as git from "../utils/git";
33
33
  import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext } from "./types";
34
34
 
@@ -355,7 +355,7 @@ interface BuildSingleArgs {
355
355
  scheme: Scheme;
356
356
  parsed: ParsedSingle;
357
357
  rendered: string;
358
- status: "miss" | "fresh" | "stale" | "disabled";
358
+ status: CacheStatus;
359
359
  fetchedAt: number;
360
360
  /** Resolved repo (post short-form expansion) — used for the PR-only diff hint. */
361
361
  repo?: string;
@@ -377,11 +377,15 @@ function buildSingleResource({
377
377
  const diffUrl = repoSegment ? `pr://${repoSegment}/${parsed.number}/diff` : `pr://${parsed.number}/diff`;
378
378
  notes.push(`Diff: ${diffUrl}`);
379
379
  }
380
+ const content =
381
+ status === "stale"
382
+ ? `> WARNING: Live GitHub refresh failed; this ${scheme} content is cached and may be stale.\n\n${rendered}`
383
+ : rendered;
380
384
  return {
381
385
  url: url.href,
382
- content: rendered,
386
+ content,
383
387
  contentType: "text/markdown",
384
- size: Buffer.byteLength(rendered, "utf-8"),
388
+ size: Buffer.byteLength(content, "utf-8"),
385
389
  notes,
386
390
  };
387
391
  }
package/src/main.ts CHANGED
@@ -311,7 +311,11 @@ export async function submitInteractiveInput(
311
311
  // developer directive to a visible user message. A synthetic submit while
312
312
  // streaming keeps its prior behavior (rejected as busy) rather than changing
313
313
  // its role.
314
- await session.prompt(input.text, { synthetic: true, expandPromptTemplates: false });
314
+ await session.prompt(input.text, {
315
+ synthetic: true,
316
+ expandPromptTemplates: false,
317
+ userInitiated: input.userInitiated,
318
+ });
315
319
  } else {
316
320
  await session.prompt(input.text, { images: input.images, streamingBehavior });
317
321
  }
@@ -62,7 +62,7 @@ import { loadAllExtensions } from "../../modes/components/extensions/state-manag
62
62
  import { theme } from "../../modes/theme/theme";
63
63
  import { type PlanApprovalDetails, resolveApprovedPlan } from "../../plan-mode/approved-plan";
64
64
  import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
65
- import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
65
+ import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../../session/messages";
66
66
  import type { UsageStatistics } from "../../session/session-entries";
67
67
  import type { SessionInfo as StoredSessionInfo } from "../../session/session-listing";
68
68
  import { SessionManager } from "../../session/session-manager";
@@ -836,7 +836,7 @@ export class AcpAgent implements Agent {
836
836
  timer = setTimeout(() => reject(new Error("ACP cancel cleanup timed out")), this.#cancelCleanupTimeoutMs);
837
837
  });
838
838
  try {
839
- await Promise.race([record.session.abort(), timeout]);
839
+ await Promise.race([record.session.abort({ reason: USER_INTERRUPT_LABEL }), timeout]);
840
840
  } finally {
841
841
  if (timer) clearTimeout(timer);
842
842
  // Order matters: clear `cleanup` before evicting the slot so the slot-eviction
@@ -2098,7 +2098,7 @@ export class AcpAgent implements Agent {
2098
2098
  getModel: () => record.session.model,
2099
2099
  isIdle: () => !record.session.isStreaming,
2100
2100
  abort: () => {
2101
- void record.session.abort();
2101
+ void record.session.abort({ reason: USER_INTERRUPT_LABEL });
2102
2102
  },
2103
2103
  hasPendingMessages: () => record.session.queuedMessageCount > 0,
2104
2104
  shutdown: () => {},
@@ -104,6 +104,13 @@ const CONDITIONS: Record<string, () => boolean> = {
104
104
  return false;
105
105
  }
106
106
  },
107
+ planModeEnabled: () => {
108
+ try {
109
+ return Settings.instance.get("plan.enabled");
110
+ } catch {
111
+ return false;
112
+ }
113
+ },
107
114
  };
108
115
 
109
116
  // ═══════════════════════════════════════════════════════════════════════════
@@ -6,7 +6,7 @@ Find out which model you emotionally abuse the most with `omp stats`
6
6
  Try task isolation to create CoW worktrees
7
7
  Need a cheap nested model call? Use `completion(x...)`. Have a big batch of tasks? Ask clanker to use it!
8
8
  Spaghetti code? Try complaining with /omfg
9
- Did you know? Each kitty/tmux/cmux split keeps its own session — `omp -c` resumes the right one
9
+ Did you know? Each kitty/tmux/cmux/zellij/wezterm split keeps its own session — `omp -c` resumes the right one
10
10
  Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
11
11
  Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
12
12
  Say `workflowz` in your message to drive the task with parallel subagents in eval — watch it glow as you type
@@ -23,6 +23,7 @@ import { HookInputComponent } from "../../modes/components/hook-input";
23
23
  import { HookSelectorComponent, type HookSelectorSlider } from "../../modes/components/hook-selector";
24
24
  import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
25
25
  import type { InteractiveModeContext, InteractiveSelectorDialogOptions } from "../../modes/types";
26
+ import { USER_INTERRUPT_LABEL } from "../../session/messages";
26
27
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
27
28
 
28
29
  const MAX_WIDGET_LINES = 10;
@@ -123,7 +124,7 @@ export class ExtensionUiController {
123
124
  const contextActions: ExtensionContextActions = {
124
125
  getModel: () => this.ctx.session.model,
125
126
  isIdle: () => !this.ctx.session.isStreaming,
126
- abort: () => this.ctx.session.abort(),
127
+ abort: () => this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL }),
127
128
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
128
129
  shutdown: () => {
129
130
  // Defer the actual teardown to the main loop, which calls
@@ -359,7 +360,7 @@ export class ExtensionUiController {
359
360
  const contextActions: ExtensionContextActions = {
360
361
  getModel: () => this.ctx.session.model,
361
362
  isIdle: () => !this.ctx.session.isStreaming,
362
- abort: () => this.ctx.session.abort(),
363
+ abort: () => this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL }),
363
364
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
364
365
  shutdown: () => {
365
366
  // Defer the actual teardown to the main loop, which calls
@@ -500,7 +501,7 @@ export class ExtensionUiController {
500
501
  isIdle: () => !this.ctx.session.isStreaming,
501
502
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
502
503
  abort: () => {
503
- this.ctx.session.abort();
504
+ this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
504
505
  },
505
506
  shutdown: () => {
506
507
  // Signal shutdown request
@@ -483,6 +483,7 @@ export class InputController {
483
483
  cancelled: false,
484
484
  started: true,
485
485
  synthetic: true,
486
+ userInitiated: true,
486
487
  });
487
488
  }
488
489
  return;
@@ -40,7 +40,9 @@ import {
40
40
  import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
41
41
  import {
42
42
  isImageProviderPreference,
43
+ isSearchProviderId,
43
44
  isSearchProviderPreference,
45
+ setExcludedSearchProviders,
44
46
  setPreferredImageProvider,
45
47
  setPreferredSearchProvider,
46
48
  } from "../../tools";
@@ -419,6 +421,11 @@ export class SelectorController {
419
421
  setPreferredSearchProvider(value);
420
422
  }
421
423
  break;
424
+ case "providers.webSearchExclude":
425
+ if (Array.isArray(value)) {
426
+ setExcludedSearchProviders(value.filter(isSearchProviderId));
427
+ }
428
+ break;
422
429
  case "providers.image":
423
430
  if (isImageProviderPreference(value)) {
424
431
  setPreferredImageProvider(value);
@@ -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";
@@ -790,6 +797,30 @@ export class InteractiveMode implements InteractiveModeContext {
790
797
  this.session.setSessionSwitchReconciler?.(() => this.#reconcileModeFromSession());
791
798
  await this.#reconcileModeFromSession();
792
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
+
793
824
  // Restore unsent editor draft from previous session shutdown (Ctrl+D).
794
825
  // One-shot: consumeDraft removes the sidecar after read so the next
795
826
  // resume does not re-restore the same text.
@@ -904,6 +935,22 @@ export class InteractiveMode implements InteractiveModeContext {
904
935
  // up the destination project's configuration.
905
936
  if (isSettingsInitialized()) {
906
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
+ }
907
954
  }
908
955
  // Re-warm plugin roots, capabilities, slash commands, and the ssh tool so
909
956
  // the next prompt sees everything scoped to the new project directory.
@@ -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
@@ -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
@@ -129,6 +129,7 @@ import {
129
129
  type CustomMessage,
130
130
  convertToLlm,
131
131
  LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE,
132
+ USER_INTERRUPT_LABEL,
132
133
  wrapSteeringForModel,
133
134
  } from "./session/messages";
134
135
  import { getRestorableSessionModels } from "./session/session-context";
@@ -177,6 +178,7 @@ import {
177
178
  getSearchTools,
178
179
  HIDDEN_TOOLS,
179
180
  isImageProviderPreference,
181
+ isSearchProviderId,
180
182
  isSearchProviderPreference,
181
183
  type LspStartupServerInfo,
182
184
  loadSshTool,
@@ -185,6 +187,7 @@ import {
185
187
  renderSearchToolBm25Description,
186
188
  SearchTool,
187
189
  SearchToolBm25Tool,
190
+ setExcludedSearchProviders,
188
191
  setPreferredImageProvider,
189
192
  setPreferredSearchProvider,
190
193
  type Tool,
@@ -1164,6 +1167,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1164
1167
  discoveredSkillsPromise?.catch(() => {});
1165
1168
 
1166
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
+
1167
1175
  const webSearchProvider = settings.get("providers.webSearch");
1168
1176
  if (typeof webSearchProvider === "string" && isSearchProviderPreference(webSearchProvider)) {
1169
1177
  setPreferredSearchProvider(webSearchProvider);
@@ -1997,7 +2005,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1997
2005
  isIdle: () => !session.isStreaming,
1998
2006
  hasQueuedMessages: () => session.queuedMessageCount > 0,
1999
2007
  abort: () => {
2000
- session.abort();
2008
+ session.abort({ reason: USER_INTERRUPT_LABEL });
2001
2009
  },
2002
2010
  settings,
2003
2011
  autoApprove: options.autoApprove ?? false,