@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.3

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 (179) hide show
  1. package/CHANGELOG.md +162 -40
  2. package/dist/cli.js +4559 -4543
  3. package/dist/types/advisor/config.d.ts +14 -6
  4. package/dist/types/advisor/runtime.d.ts +20 -11
  5. package/dist/types/autolearn/controller.d.ts +1 -0
  6. package/dist/types/config/model-discovery.d.ts +17 -0
  7. package/dist/types/config/model-resolver.d.ts +6 -2
  8. package/dist/types/config/settings-schema.d.ts +39 -10
  9. package/dist/types/config/settings.d.ts +50 -0
  10. package/dist/types/cursor.d.ts +11 -0
  11. package/dist/types/discovery/helpers.d.ts +5 -2
  12. package/dist/types/eval/agent-bridge.d.ts +7 -19
  13. package/dist/types/exec/bash-executor.d.ts +2 -0
  14. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  15. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  17. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +55 -1
  18. package/dist/types/extensibility/shared-events.d.ts +6 -0
  19. package/dist/types/extensibility/utils.d.ts +2 -2
  20. package/dist/types/lsp/client.d.ts +2 -0
  21. package/dist/types/lsp/types.d.ts +3 -0
  22. package/dist/types/mcp/transports/stdio.d.ts +42 -1
  23. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  24. package/dist/types/modes/components/agent-hub.d.ts +15 -0
  25. package/dist/types/modes/components/hook-editor.d.ts +7 -0
  26. package/dist/types/modes/components/model-hub.d.ts +5 -2
  27. package/dist/types/modes/components/session-selector.d.ts +2 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  30. package/dist/types/modes/print-mode.d.ts +1 -1
  31. package/dist/types/modes/warp-events.d.ts +24 -0
  32. package/dist/types/modes/warp-events.test.d.ts +1 -0
  33. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  34. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  35. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  36. package/dist/types/registry/persisted-agents.d.ts +3 -0
  37. package/dist/types/sdk.d.ts +27 -5
  38. package/dist/types/session/agent-session.d.ts +34 -10
  39. package/dist/types/session/session-entries.d.ts +6 -1
  40. package/dist/types/session/session-history-format.d.ts +10 -0
  41. package/dist/types/session/session-manager.d.ts +19 -0
  42. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  43. package/dist/types/task/executor.d.ts +26 -1
  44. package/dist/types/task/index.d.ts +1 -1
  45. package/dist/types/task/label.d.ts +1 -1
  46. package/dist/types/task/parallel.d.ts +14 -0
  47. package/dist/types/task/structured-subagent.d.ts +111 -0
  48. package/dist/types/task/types.d.ts +51 -0
  49. package/dist/types/telemetry-export.d.ts +34 -9
  50. package/dist/types/tools/approval.d.ts +8 -0
  51. package/dist/types/tools/bash.d.ts +2 -0
  52. package/dist/types/tools/essential-tools.d.ts +29 -0
  53. package/dist/types/tools/fetch.d.ts +4 -5
  54. package/dist/types/tools/hub/messaging.d.ts +5 -1
  55. package/dist/types/tools/image-gen.d.ts +2 -1
  56. package/dist/types/tools/index.d.ts +17 -1
  57. package/dist/types/tools/todo.d.ts +31 -0
  58. package/dist/types/tools/xdev.d.ts +11 -2
  59. package/dist/types/tui/tree-list.d.ts +7 -0
  60. package/dist/types/utils/markit.d.ts +11 -0
  61. package/dist/types/utils/title-generator.d.ts +2 -1
  62. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  63. package/dist/types/web/search/types.d.ts +1 -1
  64. package/package.json +21 -16
  65. package/src/advisor/__tests__/advisor.test.ts +1304 -42
  66. package/src/advisor/__tests__/config.test.ts +58 -2
  67. package/src/advisor/config.ts +76 -24
  68. package/src/advisor/runtime.ts +445 -92
  69. package/src/autolearn/controller.ts +23 -28
  70. package/src/cli/file-processor.ts +1 -2
  71. package/src/cli.ts +5 -1
  72. package/src/config/model-discovery.ts +81 -21
  73. package/src/config/model-registry.ts +25 -6
  74. package/src/config/model-resolver.ts +14 -7
  75. package/src/config/settings-schema.ts +46 -9
  76. package/src/config/settings.ts +405 -25
  77. package/src/cursor.ts +20 -3
  78. package/src/debug/report-bundle.ts +40 -4
  79. package/src/discovery/helpers.ts +28 -5
  80. package/src/eval/__tests__/agent-bridge.test.ts +133 -47
  81. package/src/eval/__tests__/prelude-agent.test.ts +29 -0
  82. package/src/eval/agent-bridge.ts +104 -477
  83. package/src/eval/jl/prelude.jl +7 -6
  84. package/src/eval/js/shared/prelude.txt +5 -4
  85. package/src/eval/py/prelude.py +11 -39
  86. package/src/eval/rb/prelude.rb +5 -6
  87. package/src/exec/bash-executor.ts +14 -5
  88. package/src/extensibility/custom-tools/loader.ts +3 -3
  89. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  90. package/src/extensibility/extensions/loader.ts +3 -3
  91. package/src/extensibility/extensions/managed-timers.ts +83 -0
  92. package/src/extensibility/extensions/runner.ts +26 -0
  93. package/src/extensibility/extensions/types.ts +18 -0
  94. package/src/extensibility/extensions/wrapper.ts +2 -1
  95. package/src/extensibility/hooks/loader.ts +3 -3
  96. package/src/extensibility/legacy-pi-coding-agent-shim.ts +149 -8
  97. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  98. package/src/extensibility/plugins/manager.ts +2 -2
  99. package/src/extensibility/shared-events.ts +6 -0
  100. package/src/extensibility/utils.ts +91 -25
  101. package/src/irc/bus.ts +22 -3
  102. package/src/launch/broker.ts +3 -2
  103. package/src/launch/client.ts +2 -2
  104. package/src/launch/presence.ts +2 -2
  105. package/src/lsp/client.ts +58 -1
  106. package/src/lsp/index.ts +62 -6
  107. package/src/lsp/types.ts +3 -0
  108. package/src/main.ts +11 -8
  109. package/src/mcp/manager.ts +9 -3
  110. package/src/mcp/oauth-flow.ts +20 -0
  111. package/src/mcp/transports/stdio.test.ts +269 -1
  112. package/src/mcp/transports/stdio.ts +255 -24
  113. package/src/modes/components/advisor-config.ts +65 -3
  114. package/src/modes/components/agent-hub.ts +1 -72
  115. package/src/modes/components/ask-dialog.ts +1 -1
  116. package/src/modes/components/bash-execution.ts +7 -3
  117. package/src/modes/components/eval-execution.ts +3 -1
  118. package/src/modes/components/hook-editor.ts +18 -3
  119. package/src/modes/components/model-hub.ts +138 -42
  120. package/src/modes/components/session-selector.ts +4 -0
  121. package/src/modes/components/status-line/component.test.ts +1 -0
  122. package/src/modes/components/status-line/segments.ts +21 -6
  123. package/src/modes/controllers/command-controller.ts +167 -47
  124. package/src/modes/controllers/event-controller.ts +5 -0
  125. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  126. package/src/modes/controllers/input-controller.ts +12 -12
  127. package/src/modes/controllers/selector-controller.ts +191 -31
  128. package/src/modes/interactive-mode.ts +169 -62
  129. package/src/modes/print-mode.ts +3 -3
  130. package/src/modes/rpc/host-tools.ts +2 -1
  131. package/src/modes/rpc/rpc-mode.ts +19 -4
  132. package/src/modes/warp-events.test.ts +794 -0
  133. package/src/modes/warp-events.ts +232 -0
  134. package/src/plan-mode/model-transition.test.ts +60 -0
  135. package/src/plan-mode/model-transition.ts +51 -0
  136. package/src/prompts/system/system-prompt.md +1 -1
  137. package/src/prompts/tools/eval.md +5 -2
  138. package/src/prompts/tools/read.md +1 -1
  139. package/src/prompts/tools/task.md +12 -8
  140. package/src/prompts/tools/write.md +1 -1
  141. package/src/registry/agent-lifecycle.ts +133 -18
  142. package/src/registry/persisted-agents.ts +74 -0
  143. package/src/sdk.ts +343 -122
  144. package/src/session/agent-session.ts +1359 -368
  145. package/src/session/session-entries.ts +6 -1
  146. package/src/session/session-history-format.ts +20 -5
  147. package/src/session/session-manager.ts +57 -0
  148. package/src/session/streaming-output.ts +41 -1
  149. package/src/slash-commands/builtin-registry.ts +7 -0
  150. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  151. package/src/system-prompt.ts +7 -2
  152. package/src/task/executor.ts +100 -22
  153. package/src/task/index.ts +258 -429
  154. package/src/task/label.ts +2 -0
  155. package/src/task/parallel.ts +43 -0
  156. package/src/task/persisted-revive.ts +19 -7
  157. package/src/task/structured-subagent.ts +642 -0
  158. package/src/task/types.ts +58 -0
  159. package/src/telemetry-export.ts +453 -97
  160. package/src/tools/__tests__/eval-description.test.ts +1 -1
  161. package/src/tools/approval.ts +11 -0
  162. package/src/tools/bash.ts +71 -38
  163. package/src/tools/essential-tools.ts +45 -0
  164. package/src/tools/fetch.ts +28 -105
  165. package/src/tools/gh.ts +169 -2
  166. package/src/tools/hub/messaging.ts +16 -3
  167. package/src/tools/image-gen.ts +69 -7
  168. package/src/tools/index.ts +50 -15
  169. package/src/tools/path-utils.ts +1 -0
  170. package/src/tools/read.ts +62 -29
  171. package/src/tools/todo.ts +126 -13
  172. package/src/tools/write.ts +22 -4
  173. package/src/tools/xdev.ts +14 -3
  174. package/src/tui/tree-list.ts +39 -0
  175. package/src/utils/markit.ts +12 -0
  176. package/src/utils/title-generator.ts +15 -4
  177. package/src/utils/zip.ts +16 -1
  178. package/src/web/search/providers/kimi.ts +18 -12
  179. package/src/web/search/types.ts +6 -1
package/src/tools/gh.ts CHANGED
@@ -10,7 +10,7 @@ import type {
10
10
  ToolApprovalDecision,
11
11
  } from "@oh-my-pi/pi-agent-core";
12
12
 
13
- import { getWorktreeDir, hashPath, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
13
+ import { getWorktreeDir, hashPath, isEnoent, logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
14
14
  import { type } from "arktype";
15
15
  import type { Settings } from "../config/settings";
16
16
  import githubDescription from "../prompts/tools/github.md" with { type: "text" };
@@ -239,6 +239,8 @@ const RUN_WATCH_TAIL_DEFAULT = 15;
239
239
  const RUN_WATCH_TAIL_MAX = 200;
240
240
  const REVIEW_COMMENTS_PAGE_SIZE = 100;
241
241
  const RUN_JOBS_PAGE_SIZE = 100;
242
+ const PR_DIFF_FILES_PAGE_SIZE = 100;
243
+ const PR_DIFF_FILES_MAX = 3000;
242
244
  const PR_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)(?:\/.*)?$/;
243
245
  const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)(?:\/.*)?$/;
244
246
  const RUN_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/actions\/runs\/(\d+)(?:\/.*)?$/;
@@ -2931,6 +2933,160 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
2931
2933
  return file;
2932
2934
  }
2933
2935
 
2936
+ /**
2937
+ * A single entry from `GET /repos/{owner}/{repo}/pulls/{n}/files`. `patch` is
2938
+ * absent for binary files and for individual file diffs GitHub deems too large
2939
+ * to render.
2940
+ */
2941
+ interface GhPrFileApi {
2942
+ filename?: string;
2943
+ previous_filename?: string;
2944
+ status?: string;
2945
+ additions?: number;
2946
+ deletions?: number;
2947
+ patch?: string;
2948
+ }
2949
+
2950
+ interface GhPrApi {
2951
+ changed_files?: number;
2952
+ }
2953
+
2954
+ /**
2955
+ * GitHub rejects the aggregate PR diff endpoint with HTTP 406 once the diff
2956
+ * exceeds 20,000 lines. Detect that specific failure so the caller can fall
2957
+ * back to the per-file endpoint instead of aborting the whole review.
2958
+ */
2959
+ function isPrDiffTooLargeError(err: unknown): boolean {
2960
+ const message = err instanceof Error ? err.message : String(err);
2961
+ return (
2962
+ /\bHTTP 406\b/.test(message) ||
2963
+ /exceeded the maximum number of lines/i.test(message) ||
2964
+ /\btoo_large\b/.test(message)
2965
+ );
2966
+ }
2967
+
2968
+ function formatSyntheticDiffPath(prefix: "a/" | "b/", path: string): string {
2969
+ const prefixedPath = `${prefix}${path}`;
2970
+ if (!/[\u0000-\u001F\s"\\]/.test(prefixedPath)) return prefixedPath;
2971
+
2972
+ let escaped = "";
2973
+ for (const char of prefixedPath) {
2974
+ switch (char) {
2975
+ case "\\":
2976
+ escaped += "\\\\";
2977
+ break;
2978
+ case '"':
2979
+ escaped += '\\"';
2980
+ break;
2981
+ case "\n":
2982
+ escaped += "\\n";
2983
+ break;
2984
+ case "\r":
2985
+ escaped += "\\r";
2986
+ break;
2987
+ case "\t":
2988
+ escaped += "\\t";
2989
+ break;
2990
+ default: {
2991
+ const code = char.charCodeAt(0);
2992
+ escaped += code < 32 ? `\\${code.toString(8).padStart(3, "0")}` : char;
2993
+ }
2994
+ }
2995
+ }
2996
+ return `"${escaped}"`;
2997
+ }
2998
+
2999
+ /**
3000
+ * Reconstruct a `diff --git` section from a single files-API entry. The API's
3001
+ * `patch` field carries only the hunk body, so the `diff --git`/`---`/`+++`
3002
+ * headers are synthesized to match `gh pr diff` output — this keeps
3003
+ * {@link parsePrUnifiedDiff} and the review parser producing identical section
3004
+ * boundaries and byte offsets. Files whose `patch` is omitted (binary or
3005
+ * too-large) stay visible with an explicit marker rather than being dropped.
3006
+ */
3007
+ function buildSyntheticDiffSection(file: GhPrFileApi): string | undefined {
3008
+ const newPath = file.filename;
3009
+ if (!newPath) return undefined;
3010
+ const status = file.status ?? "modified";
3011
+ const oldPath = file.previous_filename ?? newPath;
3012
+ const oldDiffPath = formatSyntheticDiffPath("a/", oldPath);
3013
+ const newDiffPath = formatSyntheticDiffPath("b/", newPath);
3014
+ const lines: string[] = [`diff --git ${oldDiffPath} ${newDiffPath}`];
3015
+ if (status === "added") {
3016
+ lines.push("new file mode 100644");
3017
+ } else if (status === "removed") {
3018
+ lines.push("deleted file mode 100644");
3019
+ } else if (status === "renamed" || file.previous_filename) {
3020
+ lines.push(`rename from ${oldPath}`, `rename to ${newPath}`);
3021
+ }
3022
+ if (typeof file.patch === "string" && file.patch.length > 0) {
3023
+ lines.push(status === "added" ? "--- /dev/null" : `--- ${oldDiffPath}`);
3024
+ lines.push(status === "removed" ? "+++ /dev/null" : `+++ ${newDiffPath}`);
3025
+ lines.push(file.patch);
3026
+ } else {
3027
+ lines.push(
3028
+ `* patch unavailable (binary or too large); additions ${file.additions ?? 0}, deletions ${file.deletions ?? 0}`,
3029
+ );
3030
+ }
3031
+ return lines.join("\n");
3032
+ }
3033
+
3034
+ /**
3035
+ * Fallback PR diff retrieval via the paginated per-file endpoint, used when the
3036
+ * aggregate `gh pr diff` is rejected for exceeding GitHub's 20,000-line limit.
3037
+ * The per-file patches are not subject to that aggregate cap, so even very
3038
+ * large PRs can be reassembled into a synthetic unified diff.
3039
+ */
3040
+ async function fetchPrDiffViaFilesApi(
3041
+ cwd: string,
3042
+ repo: string,
3043
+ number: number,
3044
+ signal: AbortSignal | undefined,
3045
+ ): Promise<string> {
3046
+ const pull = await git.github.json<GhPrApi>(
3047
+ cwd,
3048
+ ["api", "--method", "GET", `/repos/${repo}/pulls/${number}`],
3049
+ signal,
3050
+ { repoProvided: true },
3051
+ );
3052
+ if ((pull.changed_files ?? 0) > PR_DIFF_FILES_MAX) {
3053
+ throw new ToolError(
3054
+ `Pull request changes ${pull.changed_files} files, exceeding GitHub's ${PR_DIFF_FILES_MAX}-file limit for the per-file diff API.`,
3055
+ );
3056
+ }
3057
+
3058
+ const sections: string[] = [];
3059
+ let page = 1;
3060
+ while (true) {
3061
+ const response = await git.github.json<GhPrFileApi[]>(
3062
+ cwd,
3063
+ [
3064
+ "api",
3065
+ "--method",
3066
+ "GET",
3067
+ `/repos/${repo}/pulls/${number}/files`,
3068
+ "-F",
3069
+ `per_page=${PR_DIFF_FILES_PAGE_SIZE}`,
3070
+ "-F",
3071
+ `page=${page}`,
3072
+ ],
3073
+ signal,
3074
+ { repoProvided: true },
3075
+ );
3076
+ for (const file of response) {
3077
+ const section = buildSyntheticDiffSection(file);
3078
+ if (section) sections.push(section);
3079
+ }
3080
+ if (response.length < PR_DIFF_FILES_PAGE_SIZE) {
3081
+ break;
3082
+ }
3083
+ page += 1;
3084
+ }
3085
+ // Trailing newline mirrors `gh pr diff` so downstream parsers splitting on
3086
+ // `^diff --git ` see identical boundaries.
3087
+ return sections.length > 0 ? `${sections.join("\n")}\n` : "";
3088
+ }
3089
+
2934
3090
  async function fetchPrDiffFresh(
2935
3091
  cwd: string,
2936
3092
  repo: string,
@@ -2939,7 +3095,18 @@ async function fetchPrDiffFresh(
2939
3095
  ): Promise<{ rendered: string; sourceUrl: string | undefined; payload: PrDiffPayload }> {
2940
3096
  const args = ["pr", "diff", String(number), "--color", "never"];
2941
3097
  appendRepoFlag(args, repo, String(number));
2942
- const text = await git.github.text(cwd, args, signal, { repoProvided: true, trimOutput: false });
3098
+ let text: string;
3099
+ try {
3100
+ text = await git.github.text(cwd, args, signal, { repoProvided: true, trimOutput: false });
3101
+ } catch (err) {
3102
+ if (!isPrDiffTooLargeError(err)) throw err;
3103
+ logger.debug("gh pr diff exceeded GitHub's aggregate line limit; falling back to per-file API", {
3104
+ repo,
3105
+ number,
3106
+ err: String(err),
3107
+ });
3108
+ text = await fetchPrDiffViaFilesApi(cwd, repo, number, signal);
3109
+ }
2943
3110
  const payload = parsePrUnifiedDiff(text);
2944
3111
  // `rendered` already carries the verbatim diff; blank the payload copy so
2945
3112
  // the cache row stores a potentially huge diff once instead of twice.
@@ -17,6 +17,7 @@ import type { RenderResultOptions } from "../../extensibility/custom-tools/types
17
17
  import { IrcBus, type IrcDeliveryReceipt, type IrcMessage } from "../../irc/bus";
18
18
  import type { Theme } from "../../modes/theme/theme";
19
19
  import { type AgentRegistry, MAIN_AGENT_ID } from "../../registry/agent-registry";
20
+ import { registerPersistedSubagents } from "../../registry/persisted-agents";
20
21
  import { canSpawnAtDepth } from "../../task/types";
21
22
  import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../../tui";
22
23
  import {
@@ -81,10 +82,22 @@ export function messageResult(senderId: string, waited: IrcMessage): AgentToolRe
81
82
  };
82
83
  }
83
84
 
84
- export function executeList(registry: AgentRegistry, senderId: string): AgentToolResult<CoordinationDetails> {
85
+ /**
86
+ * List every addressable peer, restoring parked refs from disk when a resumed
87
+ * session has no in-memory roster.
88
+ */
89
+ export async function executeList(
90
+ registry: AgentRegistry,
91
+ senderId: string,
92
+ ): Promise<AgentToolResult<CoordinationDetails>> {
93
+ let refs = registry.list();
94
+ if (!refs.some(ref => ref.id !== senderId && ref.status !== "aborted" && ref.kind !== "advisor")) {
95
+ await registerPersistedSubagents(registry, registry.get(senderId)?.sessionFile);
96
+ refs = registry.list();
97
+ }
98
+
85
99
  const bus = IrcBus.global();
86
- const peers = registry
87
- .list()
100
+ const peers = refs
88
101
  .filter(ref => ref.id !== senderId && ref.status !== "aborted" && ref.kind !== "advisor")
89
102
  .map(ref => ({
90
103
  id: ref.id,
@@ -45,7 +45,7 @@ const IMAGE_SYSTEM_INSTRUCTION =
45
45
  "You are an AI image generator. Generate images based on user descriptions. Focus on creating high-quality, visually appealing images that match the user's request.";
46
46
 
47
47
  export type ImageProvider = "antigravity" | "gemini" | "openai" | "openai-codex" | "openrouter" | "xai";
48
- export type ImageProviderPreference = Exclude<ImageProvider, "openai-codex"> | "auto";
48
+ export type ImageProviderPreference = ImageProvider | "auto";
49
49
 
50
50
  interface ImageApiKey {
51
51
  provider: ImageProvider;
@@ -57,8 +57,17 @@ interface ImageApiKey {
57
57
  const COMMON_IMAGE_ASPECT_RATIOS = ["1:1", "3:4", "4:3", "9:16", "16:9"] as const;
58
58
  const XAI_IMAGE_ASPECT_RATIOS = [...COMMON_IMAGE_ASPECT_RATIOS, "3:2", "2:3"] as const;
59
59
  const COMMON_IMAGE_ASPECT_RATIO_SET = new Set<string>(COMMON_IMAGE_ASPECT_RATIOS);
60
- const IMAGE_PROVIDER_PREFERENCES = new Set<string>(["auto", "antigravity", "gemini", "openai", "openrouter", "xai"]);
61
- const AUTO_IMAGE_PROVIDER_ORDER = ["openai", "antigravity", "xai", "openrouter", "gemini"] as const;
60
+ const IMAGE_PROVIDER_CHOICES = [
61
+ "auto",
62
+ "antigravity",
63
+ "gemini",
64
+ "openai",
65
+ "openai-codex",
66
+ "openrouter",
67
+ "xai",
68
+ ] as const;
69
+ const IMAGE_PROVIDER_PREFERENCES = new Set<string>(IMAGE_PROVIDER_CHOICES);
70
+ const AUTO_IMAGE_PROVIDER_ORDER = ["openai", "openai-codex", "antigravity", "xai", "openrouter", "gemini"] as const;
62
71
 
63
72
  const responseModalitySchema = type('"IMAGE" | "TEXT"');
64
73
 
@@ -71,6 +80,10 @@ const inputImageSchema = type({
71
80
  "mime_type?": type("string").describe("mime type"),
72
81
  });
73
82
 
83
+ const imageProviderSchema = type
84
+ .enumerated(...IMAGE_PROVIDER_CHOICES)
85
+ .describe("image provider for this request; overrides the providers.image setting (default: use the setting)");
86
+
74
87
  export const imageGenSchema = type({
75
88
  subject: type("string").describe("main subject"),
76
89
  "action?": type("string").describe("what subject is doing"),
@@ -83,6 +96,7 @@ export const imageGenSchema = type({
83
96
  "aspect_ratio?": aspectRatioSchema,
84
97
  "image_size?": imageSizeSchema,
85
98
  "input?": inputImageSchema.array().describe("input images"),
99
+ "provider?": imageProviderSchema,
86
100
  });
87
101
  export type ImageGenParams = typeof imageGenSchema.infer;
88
102
  export type GeminiResponseModality = typeof responseModalitySchema.infer;
@@ -548,6 +562,49 @@ async function findOpenAIHostedImageCredentials(
548
562
  };
549
563
  }
550
564
 
565
+ // Codex (ChatGPT subscription) chat models that carry OpenAI's hosted
566
+ // `image_generation` tool. Priority: newest general model first, then Codex
567
+ // variants; any available openai-codex hosted-image model is the last resort.
568
+ const CODEX_IMAGE_MODEL_PRIORITY = ["gpt-5.5", "gpt-5.4", "gpt-5.1", "gpt-5", "gpt-5-codex"] as const;
569
+
570
+ function resolveDefaultCodexImageModel(modelRegistry: ModelRegistry): Model | undefined {
571
+ for (const id of CODEX_IMAGE_MODEL_PRIORITY) {
572
+ const model = modelRegistry.find("openai-codex", id);
573
+ if (model && isOpenAIHostedImageModel(model)) return model;
574
+ }
575
+ return modelRegistry.getAll().find(model => model.provider === "openai-codex" && isOpenAIHostedImageModel(model));
576
+ }
577
+
578
+ /**
579
+ * Codex subscription (ChatGPT OAuth) image credentials — engages OpenAI's hosted
580
+ * `image_generation` tool through a CONNECTED Codex account, independent of the
581
+ * active chat model. This is what lets image generation run on a ChatGPT
582
+ * subscription (no metered OPENAI_API_KEY) even when the active model is, e.g.,
583
+ * Claude. The active-model-is-codex case is already served by
584
+ * {@link findOpenAIHostedImageCredentials}, so it is skipped here to avoid a
585
+ * duplicate resolution.
586
+ */
587
+ async function findCodexSubscriptionImageCredentials(
588
+ modelRegistry: ModelRegistry | undefined,
589
+ activeModel: Model | undefined,
590
+ sessionId?: string,
591
+ ): Promise<ImageApiKey | null> {
592
+ if (!modelRegistry) return null;
593
+ if (isOpenAIHostedImageModel(activeModel) && getOpenAIHostedImageProvider(activeModel) === "openai-codex") {
594
+ return null;
595
+ }
596
+ // A Codex subscription credential is an OAuth JWT with an account claim. API
597
+ // keys stored under this provider cannot use the ChatGPT backend and must not
598
+ // prevent fallback providers from being selected.
599
+ const token = await modelRegistry.getApiKeyForProvider("openai-codex", sessionId);
600
+ if (!token || !getCodexAccountId(token)) return null;
601
+ const model = resolveDefaultCodexImageModel(modelRegistry);
602
+ if (!model) return null;
603
+ const apiKey = await modelRegistry.getApiKey(model, sessionId);
604
+ if (!isAuthenticated(apiKey) || !getCodexAccountId(apiKey)) return null;
605
+ return { provider: "openai-codex", apiKey, model };
606
+ }
607
+
551
608
  function activeImageProvider(model: Model | undefined): Exclude<ImageProviderPreference, "auto"> | null {
552
609
  switch (model?.provider) {
553
610
  case "openai":
@@ -567,7 +624,10 @@ function activeImageProvider(model: Model | undefined): Exclude<ImageProviderPre
567
624
  }
568
625
  }
569
626
 
570
- function imageProviderOrder(activeModel: Model | undefined): Array<Exclude<ImageProviderPreference, "auto">> {
627
+ function imageProviderOrder(
628
+ activeModel: Model | undefined,
629
+ preference: ImageProviderPreference = preferredImageProvider,
630
+ ): Array<Exclude<ImageProviderPreference, "auto">> {
571
631
  const providers: Array<Exclude<ImageProviderPreference, "auto">> = [];
572
632
  const added = new Set<Exclude<ImageProviderPreference, "auto">>();
573
633
  const add = (provider: Exclude<ImageProviderPreference, "auto"> | null): void => {
@@ -576,7 +636,7 @@ function imageProviderOrder(activeModel: Model | undefined): Array<Exclude<Image
576
636
  providers.push(provider);
577
637
  };
578
638
 
579
- if (preferredImageProvider !== "auto") add(preferredImageProvider);
639
+ if (preference !== "auto") add(preference);
580
640
  add(activeImageProvider(activeModel));
581
641
  for (const provider of AUTO_IMAGE_PROVIDER_ORDER) add(provider);
582
642
  return providers;
@@ -591,6 +651,8 @@ async function findImageApiKey(
591
651
  switch (provider) {
592
652
  case "openai":
593
653
  return findOpenAIHostedImageCredentials(modelRegistry, activeModel, sessionId);
654
+ case "openai-codex":
655
+ return findCodexSubscriptionImageCredentials(modelRegistry, activeModel, sessionId);
594
656
  case "antigravity":
595
657
  return modelRegistry ? findAntigravityCredentials(modelRegistry, sessionId) : null;
596
658
  case "xai":
@@ -1047,7 +1109,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1047
1109
  async execute(_toolCallId, params, _onUpdate, ctx, signal) {
1048
1110
  return untilAborted(signal, async () => {
1049
1111
  const sessionId = ctx.sessionManager.getSessionId();
1050
- const providerOrder = imageProviderOrder(ctx.model);
1112
+ const providerOrder = imageProviderOrder(ctx.model, params.provider ?? preferredImageProvider);
1051
1113
  const cwd = ctx.sessionManager.getCwd();
1052
1114
  const requestSignal = ptree.combineSignals(signal, IMAGE_TIMEOUT);
1053
1115
  const fetchImpl = ctx.fetch ?? fetch;
@@ -1604,7 +1666,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1604
1666
 
1605
1667
  if (!foundCredentials) {
1606
1668
  throw new Error(
1607
- "No image API credentials found. Use a GPT Responses/Codex model with OpenAI credentials, login with google-antigravity or xAI Grok OAuth, or set XAI_API_KEY, OPENROUTER_API_KEY, GEMINI_API_KEY, or GOOGLE_API_KEY.",
1669
+ "No image API credentials found. Connect a Codex (ChatGPT) subscription, use a GPT Responses/Codex model with OpenAI credentials, log in with google-antigravity or xAI Grok OAuth, or set OPENAI_API_KEY, XAI_API_KEY, OPENROUTER_API_KEY, GEMINI_API_KEY, or GOOGLE_API_KEY.",
1608
1670
  );
1609
1671
  }
1610
1672
 
@@ -28,7 +28,7 @@ import type { UsageStatistics } from "../session/session-entries";
28
28
  import type { ToolChoiceQueue } from "../session/tool-choice-queue";
29
29
  import { TaskTool } from "../task";
30
30
  import type { AgentOutputManager } from "../task/output-manager";
31
- import { canSpawnAtDepth } from "../task/types";
31
+ import { canSpawnAtDepth, type StructuredSubagentSchemaMode } from "../task/types";
32
32
  import type { EventBus } from "../utils/event-bus";
33
33
  import { WebSearchTool } from "../web/search";
34
34
  import type { WorkspaceTree } from "../workspace-tree";
@@ -45,7 +45,7 @@ import { resolveEvalBackends } from "./eval-backends";
45
45
  import { GithubTool } from "./gh";
46
46
  import { GlobTool } from "./glob";
47
47
  import { GrepTool } from "./grep";
48
- import { HubTool } from "./hub";
48
+ import { HubTool, isIrcEnabled } from "./hub";
49
49
  import { InspectImageTool } from "./inspect-image";
50
50
  import { LearnTool } from "./learn";
51
51
  import { ManageSkillTool } from "./manage-skill";
@@ -74,6 +74,7 @@ export * from "./bash";
74
74
  export * from "./browser";
75
75
  export * from "./checkpoint";
76
76
  export * from "./debug";
77
+ export * from "./essential-tools";
77
78
  export * from "./eval";
78
79
  export * from "./eval-backends";
79
80
  export * from "./gh";
@@ -182,17 +183,31 @@ export interface ToolSession {
182
183
  customToolPaths?: ToolPathWithSource[];
183
184
  /** Whether LSP integrations are enabled */
184
185
  enableLsp?: boolean;
186
+ /** Whether this invocation may expose IRC. `false` removes it even for subagents. */
187
+ enableIrc?: boolean;
188
+ /**
189
+ * Whether MCP capabilities may be forwarded to child sessions. `false`
190
+ * prohibits inherited-manager and process-global MCP fallback.
191
+ */
192
+ enableMCP?: boolean;
185
193
  /** Whether an edit-capable tool is available in this session (controls hashline output) */
186
194
  hasEditTool?: boolean;
187
195
  /** Event bus for tool/extension communication */
188
196
  eventBus?: EventBus;
189
- /** Output schema for structured completion (subagents) */
197
+ /** Output schema for structured completion (subagents). */
190
198
  outputSchema?: unknown;
199
+ /** Enforcement policy for {@link outputSchema}; defaults to legacy permissive behavior. */
200
+ outputSchemaMode?: StructuredSubagentSchemaMode;
191
201
  /** Whether to include the yield tool by default */
192
202
  requireYieldTool?: boolean;
193
203
  /** Session starts with a prewalk hand-off armed. Keeps `todo` in yield-gated
194
204
  * (subagent) registries: the prewalk plan nudge + todo gate need it. */
195
205
  prewalkArmed?: boolean;
206
+ /**
207
+ * Constrain the active set to the caller's explicit built-in names (plus a
208
+ * required yield tool). Suppresses automatic tool-set expansion.
209
+ */
210
+ restrictToolNames?: boolean;
196
211
  /** Task recursion depth (0 = top-level, 1 = first child, etc.) */
197
212
  taskDepth?: number;
198
213
  /** Get shared eval executor session ID. Subagents inherit this to share JS/Python/Ruby/Julia state. */
@@ -402,13 +417,18 @@ export type ToolName = BuiltinToolName;
402
417
  * Create tools from BUILTIN_TOOLS registry.
403
418
  */
404
419
  export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
420
+ const restrictToolNames = session.restrictToolNames === true;
405
421
  const includeYield = session.requireYieldTool === true;
406
422
  const enableLsp = session.enableLsp ?? true;
407
- let requestedTools = toolNames && toolNames.length > 0 ? normalizeToolNames(toolNames) : undefined;
423
+ const requestedTools = restrictToolNames
424
+ ? normalizeToolNames(toolNames ?? [])
425
+ : toolNames && toolNames.length > 0
426
+ ? normalizeToolNames(toolNames)
427
+ : undefined;
408
428
  const goalEnabled = session.settings.get("goal.enabled");
409
- const goalModeActive = goalEnabled && session.getGoalModeState?.()?.enabled === true;
429
+ const goalModeActive = !restrictToolNames && goalEnabled && session.getGoalModeState?.()?.enabled === true;
410
430
  if (goalModeActive && requestedTools && !requestedTools.includes("goal")) {
411
- requestedTools = [...requestedTools, "goal"];
431
+ requestedTools.push("goal");
412
432
  }
413
433
  const backends = resolveEvalBackends(session);
414
434
  const allowPython = backends.python;
@@ -465,8 +485,12 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
465
485
  // unreachable, in which case eval dispatches exclusively to the others.
466
486
  const allowEval = effectivePythonAllowed || allowJs || effectiveRubyAllowed || effectiveJuliaAllowed;
467
487
 
468
- // Auto-include AST counterparts when their text-based sibling is present
469
- if (requestedTools) {
488
+ // Auto-include AST counterparts when their text-based sibling is present.
489
+ // Restricted callers own the active list and must not have it widened.
490
+ if (requestedTools && !restrictToolNames) {
491
+ if (goalModeActive && !requestedTools.includes("goal")) {
492
+ requestedTools.push("goal");
493
+ }
470
494
  if (
471
495
  requestedTools.includes("grep") &&
472
496
  !requestedTools.includes("ast_grep") &&
@@ -521,6 +545,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
521
545
  if (name === "ask") return session.settings.get("ask.enabled");
522
546
  if (name === "browser") return session.settings.get("browser.enabled");
523
547
  if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
548
+ if (name === "hub") {
549
+ return (
550
+ !restrictToolNames && session.enableIrc !== false && isIrcEnabled(session.settings, session.taskDepth ?? 0)
551
+ );
552
+ }
524
553
  if (name === "retain" || name === "recall" || name === "reflect") {
525
554
  return ["hindsight", "mnemopi"].includes(session.settings.get("memory.backend") ?? "");
526
555
  }
@@ -568,15 +597,17 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
568
597
  );
569
598
  let tools = baseResults.filter((r): r is Tool => r !== null);
570
599
 
571
- // xd:// mounting: unmount discoverable built-ins from the tools array and
572
- // expose them as virtual device URLs driven through read/write. Active for
573
- // default tool sets when `tools.xdev` is enabled.
574
- const xdevEnabled = requestedTools === undefined && session.settings.get("tools.xdev");
600
+ // Ordinary sessions use xd:// for discoverable built-ins, custom tools, and
601
+ // MCP tools. Structured children must expose only their host-provided names,
602
+ // so never allocate a registry that later SDK assembly could populate.
603
+ // Explicitly requested built-ins retain their top-level presentation.
604
+ const xdevEnabled = !restrictToolNames && session.settings.get("tools.xdev");
605
+ const mountBuiltinTools = requestedTools === undefined;
575
606
  if (xdevEnabled) {
576
607
  const mounted: Tool[] = [];
577
608
  const kept: Tool[] = [];
578
609
  for (const tool of tools) {
579
- const mountable = isMountableUnderXdev(tool) && tool.name in BUILTIN_TOOLS;
610
+ const mountable = mountBuiltinTools && isMountableUnderXdev(tool) && tool.name in BUILTIN_TOOLS;
580
611
  (mountable ? mounted : kept).push(tool);
581
612
  }
582
613
  session.xdevRegistry = new XdevRegistry(mounted);
@@ -593,13 +624,17 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
593
624
  // (e.g. ast_edit) also resolve through a `write` to xd://resolve/reject. Retain
594
625
  // both whenever any device is mounted or a deferrable tool can stage one.
595
626
  const xdevMounted = (session.xdevRegistry?.size ?? 0) > 0;
596
- if ((tools.some(tool => tool.deferrable === true) || xdevMounted) && !tools.some(tool => tool.name === "write")) {
627
+ if (
628
+ !restrictToolNames &&
629
+ (tools.some(tool => tool.deferrable === true) || xdevMounted) &&
630
+ !tools.some(tool => tool.name === "write")
631
+ ) {
597
632
  const writeTool = await logger.time("createTools:write", BUILTIN_TOOLS.write, session);
598
633
  if (writeTool) {
599
634
  tools.push(wrapToolWithMetaNotice(writeTool));
600
635
  }
601
636
  }
602
- if (xdevMounted && !tools.some(tool => tool.name === "read")) {
637
+ if (!restrictToolNames && xdevMounted && !tools.some(tool => tool.name === "read")) {
603
638
  const readTool = await logger.time("createTools:read", BUILTIN_TOOLS.read, session);
604
639
  if (readTool) {
605
640
  tools.push(wrapToolWithMetaNotice(readTool));
@@ -35,6 +35,7 @@ const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
35
35
  agent: true,
36
36
  artifact: true,
37
37
  issue: true,
38
+ history: true,
38
39
  local: true,
39
40
  memory: true,
40
41
  omp: true,