@eddyskywalker/dsh-chatgpt-subscription 0.58.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -1
  3. package/lib/client.js +1714 -255
  4. package/lib/client.js.map +1 -1
  5. package/lib/codemode.js +1952 -0
  6. package/lib/index.js +587 -243
  7. package/lib/llm-compat-u9mor0uu.js +110 -0
  8. package/lib/types/client/CodexImageToolView.d.ts.map +1 -1
  9. package/lib/types/client/codemode/CodeRunToolView.d.ts +68 -0
  10. package/lib/types/client/codemode/CodeRunToolView.d.ts.map +1 -0
  11. package/lib/types/client/codemode/highlight.d.ts +25 -0
  12. package/lib/types/client/codemode/highlight.d.ts.map +1 -0
  13. package/lib/types/client/codemode/styles.d.ts +11 -0
  14. package/lib/types/client/codemode/styles.d.ts.map +1 -0
  15. package/lib/types/client/codemode/tool-name.d.ts +10 -0
  16. package/lib/types/client/codemode/tool-name.d.ts.map +1 -0
  17. package/lib/types/client/index.d.ts.map +1 -1
  18. package/lib/types/client/locales-ru/hub.d.ts.map +1 -1
  19. package/lib/types/client/locales.d.ts +23 -1
  20. package/lib/types/client/locales.d.ts.map +1 -1
  21. package/lib/types/client/progress-updates/progress-row.d.ts.map +1 -1
  22. package/lib/types/client/styles.d.ts.map +1 -1
  23. package/lib/types/compat.d.ts +10 -1
  24. package/lib/types/compat.d.ts.map +1 -1
  25. package/lib/types/entry-codemode.d.ts +37 -0
  26. package/lib/types/entry-codemode.d.ts.map +1 -0
  27. package/lib/types/host/agent-preset.d.ts +24 -2
  28. package/lib/types/host/agent-preset.d.ts.map +1 -1
  29. package/lib/types/host/antigravity/image-tool.d.ts +10 -2
  30. package/lib/types/host/antigravity/image-tool.d.ts.map +1 -1
  31. package/lib/types/host/cache-warmup/install.d.ts +10 -4
  32. package/lib/types/host/cache-warmup/install.d.ts.map +1 -1
  33. package/lib/types/host/cache-warmup/projection.d.ts +17 -0
  34. package/lib/types/host/cache-warmup/projection.d.ts.map +1 -1
  35. package/lib/types/host/codemode/discovery.d.ts +33 -0
  36. package/lib/types/host/codemode/discovery.d.ts.map +1 -0
  37. package/lib/types/host/codemode/executor.d.ts +140 -0
  38. package/lib/types/host/codemode/executor.d.ts.map +1 -0
  39. package/lib/types/host/codemode/retention.d.ts +34 -0
  40. package/lib/types/host/codemode/retention.d.ts.map +1 -0
  41. package/lib/types/host/codemode/rules.d.ts +193 -0
  42. package/lib/types/host/codemode/rules.d.ts.map +1 -0
  43. package/lib/types/host/codemode/scope.d.ts +59 -0
  44. package/lib/types/host/codemode/scope.d.ts.map +1 -0
  45. package/lib/types/host/codemode/transport.d.ts +299 -0
  46. package/lib/types/host/codemode/transport.d.ts.map +1 -0
  47. package/lib/types/host/codex-images.d.ts +12 -0
  48. package/lib/types/host/codex-images.d.ts.map +1 -1
  49. package/lib/types/host/command-code/model-catalog.d.ts +14 -14
  50. package/lib/types/host/command-code/model-catalog.d.ts.map +1 -1
  51. package/lib/types/host/command-code/routes.d.ts.map +1 -1
  52. package/lib/types/host/preset-sync.d.ts +19 -4
  53. package/lib/types/host/preset-sync.d.ts.map +1 -1
  54. package/package.json +6 -1
  55. package/presets/dispatch/README.md +13 -6
  56. package/presets/dispatch/agent.cordis.yml +19 -16
  57. package/presets/dispatch/preset.yml +1 -1
package/lib/index.js CHANGED
@@ -1,15 +1,16 @@
1
1
  import { a as resolveProgressUpdatesSettings, n as attachProgressSettings } from "./entry-progress-updates-CxQewio9.js";
2
2
  import { attachPlanModePipelineSettings, resolvePlanModePipelineEnabled } from "./plan-mode-pipeline.js";
3
+ import { n as PLUGIN_MESSAGE_SOURCE_KIND, r as normalizeGenerateOptions, t as LlmError$1 } from "./llm-compat-u9mor0uu.js";
3
4
  import { DEFAULT_SKIP_PROVIDERS, PRUNER_SERVICE_NAME, resolveSkipProviders, shouldSkipPruningForSession } from "./tool-result-pruner.js";
4
5
  import { PLAN_PROJECTION_KEY, createPlanNotice } from "./plan-mode.js";
5
6
  import { createRequire } from "node:module";
6
7
  import * as LlmModule from "@deepseek-ai/dsh-llm";
7
- import { EMPTY_RESPONSE_CODE, HarnessError, LlmAdapter, LlmError, LlmError as LlmError$1, ProviderRequestId, ReasoningEffortId, ToolCallId, attributionHeaders, createUserMessage, errorChain, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
8
+ import { EMPTY_RESPONSE_CODE, HarnessError, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, ToolCallId, attributionHeaders, createUserMessage, errorChain, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
8
9
  import { defineTool } from "@deepseek-ai/dsh-tools";
9
10
  import { z } from "zod";
10
11
  import z$1 from "@deepseek-ai/schemastery";
11
12
  import fs, { chmod, lstat, mkdir, open, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
12
- import path, { basename, dirname, join, relative, resolve } from "node:path";
13
+ import path, { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
14
  import os, { homedir, platform, userInfo } from "node:os";
14
15
  import { constants, createCipheriv, createDecipheriv, createHash, createHmac, publicEncrypt, randomBytes, randomUUID } from "node:crypto";
15
16
  import { isDeepStrictEqual } from "node:util";
@@ -807,11 +808,20 @@ const ROUTE_PREFIX$5 = "/api/dsh-chatgpt-subscription";
807
808
  * `test/responses-client.test.ts` asserts the two agree, so a release that
808
809
  * forgets this constant fails the suite instead of shipping quietly.
809
810
  */
810
- const PLUGIN_VERSION = "0.58.0";
811
+ const PLUGIN_VERSION = "0.60.0";
811
812
  const CODEX_CHATGPT_PROVIDER_ID = "codex-chatgpt";
812
813
  const CODEX_API_BASE = "https://chatgpt.com/backend-api/codex";
813
814
  const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
814
815
  const CODEX_IMAGE_GENERATION_URL = `${CODEX_API_BASE}/images/generations`;
816
+ /**
817
+ * The edit route: the same image surface, addressed for reference-image input.
818
+ *
819
+ * A generation has nothing to modify and goes to `/images/generations`; a
820
+ * request that names reference images is an EDIT and goes here, with those
821
+ * images carried inline in a JSON `images` array. It is a separate route, not a
822
+ * flag on the generation body, so the two are stated as two constants.
823
+ */
824
+ const CODEX_IMAGE_EDIT_URL = `${CODEX_API_BASE}/images/edits`;
815
825
  const CODEX_SEARCH_URL = `${CODEX_API_BASE}/alpha/search`;
816
826
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
817
827
  /**
@@ -4425,7 +4435,7 @@ var OAuthService = class {
4425
4435
  throw new OAuthServiceError("oauth-token-exchange-failed", `The ChatGPT device code could not be issued (${response.status}${detail === null ? "" : `, ${detail}`}).`, response.status);
4426
4436
  }
4427
4437
  const payload = await response.json().catch(() => null);
4428
- const record = isRecord$68(payload) ? payload : {};
4438
+ const record = isRecord$67(payload) ? payload : {};
4429
4439
  const authId = nonEmptyString$11(record.device_auth_id);
4430
4440
  const userCode = nonEmptyString$11(record.user_code) ?? nonEmptyString$11(record.usercode);
4431
4441
  if (authId === void 0 || userCode === void 0) throw new OAuthServiceError("oauth-token-exchange-failed", "ChatGPT returned no device code.");
@@ -4492,7 +4502,7 @@ var OAuthService = class {
4492
4502
  const detail = deviceErrorIdentifier(payload);
4493
4503
  throw new OAuthServiceError("oauth-token-exchange-failed", `The ChatGPT device confirmation was refused (${response.status}${detail === void 0 ? "" : `, ${detail}`}).`, response.status);
4494
4504
  }
4495
- const record = isRecord$68(payload) ? payload : {};
4505
+ const record = isRecord$67(payload) ? payload : {};
4496
4506
  const authorizationCode = nonEmptyString$11(record.authorization_code);
4497
4507
  const codeVerifier = nonEmptyString$11(record.code_verifier);
4498
4508
  if (authorizationCode === void 0 || codeVerifier === void 0) throw new OAuthServiceError("oauth-token-exchange-failed", "ChatGPT confirmed the device code but returned no authorization code.");
@@ -4825,11 +4835,11 @@ const DEVICE_PENDING_STATUSES = /* @__PURE__ */ new Set([
4825
4835
  function isPendingDeviceAnswer(response, payload) {
4826
4836
  if (response.status === 403 || response.status === 404) return true;
4827
4837
  if (!response.ok && response.status !== 400) return false;
4828
- if (!isRecord$68(payload)) return false;
4838
+ if (!isRecord$67(payload)) return false;
4829
4839
  const status = payload.status;
4830
4840
  if (typeof status === "string" && DEVICE_PENDING_STATUSES.has(status.toLowerCase())) return true;
4831
4841
  const error = payload.error;
4832
- if (isRecord$68(error)) {
4842
+ if (isRecord$67(error)) {
4833
4843
  const code = error.code;
4834
4844
  if (typeof code === "string" && DEVICE_PENDING_STATUSES.has(code.toLowerCase())) return true;
4835
4845
  }
@@ -4843,18 +4853,18 @@ function isPendingDeviceAnswer(response, payload) {
4843
4853
  * wrong.
4844
4854
  */
4845
4855
  function deviceErrorIdentifier(payload) {
4846
- if (!isRecord$68(payload)) return void 0;
4856
+ if (!isRecord$67(payload)) return void 0;
4847
4857
  const candidates = [payload.code];
4848
4858
  const error = payload.error;
4849
4859
  if (typeof error === "string") candidates.push(error);
4850
- else if (isRecord$68(error)) candidates.push(error.code, error.type);
4860
+ else if (isRecord$67(error)) candidates.push(error.code, error.type);
4851
4861
  candidates.push(payload.status);
4852
4862
  for (const candidate of candidates) if (typeof candidate === "string" && /^[a-z0-9_.-]{1,64}$/i.test(candidate)) return candidate;
4853
4863
  }
4854
4864
  function nonEmptyString$11(value) {
4855
4865
  return typeof value === "string" && value !== "" ? value : void 0;
4856
4866
  }
4857
- function isRecord$68(value) {
4867
+ function isRecord$67(value) {
4858
4868
  return typeof value === "object" && value !== null && !Array.isArray(value);
4859
4869
  }
4860
4870
  async function oauthErrorIdentifier(response) {
@@ -5035,7 +5045,7 @@ const CODEX_CLI_CREDENTIAL_SOURCE = "codex";
5035
5045
  * here, and the import has to be repeated afterwards.
5036
5046
  */
5037
5047
  const ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT = "The sign-in imported from the Codex CLI has expired. Sign in again in the Codex CLI, then import it again from this settings card.";
5038
- function isRecord$67(value) {
5048
+ function isRecord$66(value) {
5039
5049
  return typeof value === "object" && value !== null && !Array.isArray(value);
5040
5050
  }
5041
5051
  /** A trimmed, non-empty string, or nothing. */
@@ -5137,7 +5147,7 @@ function decodeJwtClaims$1(token) {
5137
5147
  if (parts.length !== 3) return void 0;
5138
5148
  try {
5139
5149
  const value = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
5140
- return isRecord$67(value) ? value : void 0;
5150
+ return isRecord$66(value) ? value : void 0;
5141
5151
  } catch {
5142
5152
  return;
5143
5153
  }
@@ -5182,10 +5192,10 @@ function expiryFromAccessToken(accessToken) {
5182
5192
  * the file carries cannot reach the result.
5183
5193
  */
5184
5194
  function parseAdoptedCodexCredential(value, sourcePath) {
5185
- if (!isRecord$67(value)) return void 0;
5195
+ if (!isRecord$66(value)) return void 0;
5186
5196
  if (value.auth_mode !== "chatgpt") return void 0;
5187
5197
  const tokens = value[CODEX_CLI_TOKENS_KEY];
5188
- if (!isRecord$67(tokens)) return void 0;
5198
+ if (!isRecord$66(tokens)) return void 0;
5189
5199
  const accessToken = nonEmptyString$10(tokens.access_token);
5190
5200
  if (accessToken === void 0) return void 0;
5191
5201
  const refreshToken = nonEmptyString$10(tokens.refresh_token);
@@ -5204,7 +5214,7 @@ function parseAdoptedCodexCredential(value, sourcePath) {
5204
5214
  const email = claims === void 0 ? void 0 : nonEmptyString$10(claims.email);
5205
5215
  if (email !== void 0) credentials.email = email;
5206
5216
  const authClaims = claims === void 0 ? void 0 : claims[CODEX_AUTH_CLAIMS_KEY];
5207
- const planType = isRecord$67(authClaims) ? nonEmptyString$10(authClaims.chatgpt_plan_type) : void 0;
5217
+ const planType = isRecord$66(authClaims) ? nonEmptyString$10(authClaims.chatgpt_plan_type) : void 0;
5208
5218
  if (planType !== void 0) credentials.planType = planType;
5209
5219
  }
5210
5220
  const accountId = nonEmptyString$10(tokens.account_id);
@@ -5990,115 +6000,34 @@ function webServerOrigin(server) {
5990
6000
  return `http://${server.host === "0.0.0.0" ? "127.0.0.1" : server.host}:${server.port}`;
5991
6001
  }
5992
6002
  //#endregion
5993
- //#region src/host/common/llm-compat.ts
5994
- /** This package's kind in the harness message-source map. */
5995
- const PLUGIN_MESSAGE_SOURCE_KIND = "dsh-chatgpt-subscription";
5996
- function isRecord$66(value) {
5997
- return typeof value === "object" && value !== null && !Array.isArray(value);
5998
- }
5999
- /** Read a message body's blocks, keeping every entry that carries a block tag. */
6000
- function readBlocks(raw) {
6001
- if (!Array.isArray(raw)) return [];
6002
- return raw.filter((block) => isRecord$66(block) && typeof block.type === "string");
6003
- }
6003
+ //#region src/host/codex-images.ts
6004
6004
  /**
6005
- * The first value that is a non-empty string.
6005
+ * Output canvases the Codex image endpoint accepts in `size`.
6006
6006
  *
6007
- * Used for fields the two generations spell in different places: a value that
6008
- * is present but empty must not shadow a real one further down the chain, which
6009
- * is what `??` alone does.
6010
- */
6011
- function firstNonEmptyString(...values) {
6012
- for (const value of values) if (typeof value === "string" && value !== "") return value;
6013
- return "";
6014
- }
6015
- /**
6016
- * The result block a `role: 'tool'` message already carries inside its body.
6017
- *
6018
- * Some harness generations deliver the call id inside the result block rather
6019
- * than on the message, and one shape carries both: `role: 'tool'` with a
6020
- * `tool-result` block holding the id. Reading only the message-level fields
6021
- * leaves {@link toolCallIdOf} nothing to find.
6022
- */
6023
- function embeddedToolResult(content) {
6024
- if (content.length !== 1) return void 0;
6025
- const [block] = content;
6026
- return block?.type === "tool-result" ? block : void 0;
6027
- }
6028
- /**
6029
- * The call id one `role: 'tool'` message associates with its `tool-call`.
6030
- *
6031
- * The id is the only thing tying a result to the call it answers: a mapper that
6032
- * sends it empty makes every result an orphan and tells the repair pass that no
6033
- * call was answered, which some gateways reject outright (WorkBuddy answers
6034
- * `11148`, "tool calls and tool results do not match"). It therefore has to be
6035
- * read from every place the harness generations put it — the message, the
6036
- * message's source, and the result block inside the body — and an empty string
6037
- * must not shadow a later, real one.
6038
- */
6039
- function toolCallIdOf(raw, source, embedded) {
6040
- return firstNonEmptyString(raw.toolCallId, raw.callId, source?.callId, embedded?.toolCallId);
6041
- }
6042
- /**
6043
- * Normalize one harness message list into {@link Message}.
6044
- *
6045
- * A 0.1.7 `role: 'tool'` message is re-wrapped as the user-role message holding
6046
- * one {@link ToolResultBlock} that the mappers expect, keeping the tool
6047
- * provenance (`kind: 'tool'`) they use to tell a real tool result from a user
6048
- * turn. A message that already carries that block keeps its own body instead of
6049
- * nesting a second `tool-result` around it. Everything a newer harness adds
6050
- * beside that keeps its own shape.
6051
- * @param messages - Messages straight off the request options, in either generation's shape.
6052
- * @returns The same conversation in the pre-0.1.7 vocabulary.
6053
- */
6054
- function normalizeMessages(messages) {
6055
- const normalized = [];
6056
- for (const raw of messages ?? []) {
6057
- if (!isRecord$66(raw)) continue;
6058
- const content = readBlocks(raw.content);
6059
- if (raw.role === "tool") {
6060
- const source = isRecord$66(raw.source) ? raw.source : void 0;
6061
- const embedded = embeddedToolResult(content);
6062
- const callId = toolCallIdOf(raw, source, embedded);
6063
- const isError = raw.isError === true || embedded?.isError === true;
6064
- normalized.push({
6065
- id: raw.id,
6066
- role: "user",
6067
- content: [{
6068
- type: "tool-result",
6069
- toolCallId: callId,
6070
- content: embedded === void 0 ? content : embedded.content,
6071
- ...isError ? { isError: true } : {}
6072
- }],
6073
- source: {
6074
- kind: "tool",
6075
- callId
6076
- }
6077
- });
6078
- continue;
6079
- }
6080
- normalized.push({
6081
- id: raw.id,
6082
- role: typeof raw.role === "string" ? raw.role : "user",
6083
- content,
6084
- ...isRecord$66(raw.source) ? { source: raw.source } : {}
6085
- });
6086
- }
6087
- return normalized;
6088
- }
6089
- /**
6090
- * Normalize request options for the mappers.
6091
- * @param options - Request options the harness handed the adapter.
6092
- * @returns The same options with {@link Message} history.
6007
+ * These are the official client's own three canvases plus the default `auto`,
6008
+ * which lets the service pick. A size outside this set is not negotiated: the
6009
+ * backend answers it with a 400, so the tool refuses it before the request.
6093
6010
  */
6094
- function normalizeGenerateOptions(options) {
6095
- return {
6096
- ...options,
6097
- messages: normalizeMessages(options.messages)
6098
- };
6099
- }
6100
- //#endregion
6101
- //#region src/host/codex-images.ts
6011
+ const CODEX_IMAGE_SIZES = [
6012
+ "auto",
6013
+ "1024x1024",
6014
+ "1536x1024",
6015
+ "1024x1536"
6016
+ ];
6017
+ /** Render effort the endpoint accepts in `quality`; `auto` is the service's own default. */
6018
+ const CODEX_IMAGE_QUALITIES = [
6019
+ "auto",
6020
+ "low",
6021
+ "medium",
6022
+ "high"
6023
+ ];
6024
+ /** Frame budget of an animated GIF the endpoint accepts. */
6025
+ const GIF_MIN_FRAMES = 2;
6026
+ const GIF_MAX_FRAMES = 16;
6027
+ const GIF_MIN_FRAME_MS = 20;
6028
+ const GIF_MAX_FRAME_MS = 2e3;
6029
+ /** How much of an upstream error body is quoted back to the caller. */
6030
+ const MAX_UPSTREAM_EXCERPT_CHARS = 300;
6102
6031
  const PNG_SIGNATURE = [
6103
6032
  137,
6104
6033
  80,
@@ -6109,16 +6038,78 @@ const PNG_SIGNATURE = [
6109
6038
  26,
6110
6039
  10
6111
6040
  ];
6041
+ /**
6042
+ * Magic-byte checks per media type.
6043
+ *
6044
+ * An extension is a claim about a file's name; the bytes are the claim the file
6045
+ * itself makes. Both are checked, because sending a file whose declared type the
6046
+ * service cannot decode returns a plain upstream error rather than an image.
6047
+ */
6048
+ const SIGNATURES$1 = {
6049
+ "image/png": (bytes) => bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71,
6050
+ "image/jpeg": (bytes) => bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255,
6051
+ "image/webp": (bytes) => bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80,
6052
+ "image/gif": (bytes) => bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70
6053
+ };
6054
+ /**
6055
+ * Extensions a reference image may carry.
6056
+ *
6057
+ * Deliberately narrower than {@link SIGNATURES}: an edit accepts stills only.
6058
+ * Animated GIF input is not part of the edit wire contract, so it is refused
6059
+ * here instead of being sent to an endpoint that does not take it.
6060
+ */
6061
+ const REFERENCE_EXTENSIONS = {
6062
+ ".png": "image/png",
6063
+ ".jpg": "image/jpeg",
6064
+ ".jpeg": "image/jpeg",
6065
+ ".webp": "image/webp"
6066
+ };
6112
6067
  function createCodexImageTool(oauth, attachments, options = {}) {
6113
6068
  const fetchFn = options.fetchFn ?? fetch;
6114
6069
  return defineTool({
6115
6070
  name: CODEX_IMAGE_TOOL_NAME,
6116
- description: "Generate a PNG image using the signed-in ChatGPT subscription-backed Codex image endpoint.",
6117
- parameters: { prompt: {
6118
- type: "string",
6119
- required: true,
6120
- description: "A detailed image generation prompt."
6121
- } },
6071
+ description: "Generate or edit a PNG or GIF image with the signed-in ChatGPT subscription-backed Codex image endpoint. Describe the subject, style, composition and any text to render in the prompt; pass referenceImagePaths to edit local PNG/JPEG/WebP images, and gif to animate the result. Scheduling: call this tool yourself for a single generation, an edit, or a retry - never create a subagent just to generate one image. Delegate only when the creative task needs several candidate images or is large enough to be a multi-step task of its own.",
6072
+ parameters: {
6073
+ prompt: {
6074
+ type: "string",
6075
+ required: true,
6076
+ description: "A detailed image generation prompt."
6077
+ },
6078
+ size: {
6079
+ type: "string",
6080
+ enum: CODEX_IMAGE_SIZES,
6081
+ description: "Output canvas. Omit or pass auto to let the service choose. 1024x1024 is square, 1536x1024 landscape and 1024x1536 portrait."
6082
+ },
6083
+ quality: {
6084
+ type: "string",
6085
+ enum: CODEX_IMAGE_QUALITIES,
6086
+ description: "Render effort. Omit or pass auto for the service default; low is fastest and high is most detailed."
6087
+ },
6088
+ transparentBackground: {
6089
+ type: "boolean",
6090
+ description: "true for a transparent background (PNG or GIF alpha), false for an explicitly opaque one. Omit it to leave the background decision to the service."
6091
+ },
6092
+ referenceImagePaths: {
6093
+ type: "array",
6094
+ items: { type: "string" },
6095
+ description: "Up to 5 local PNG, JPEG or WebP files to edit. Absolute paths recommended. The request becomes an edit against those images; nothing else on disk is read."
6096
+ },
6097
+ gif: {
6098
+ type: "object",
6099
+ additionalProperties: false,
6100
+ description: "Ask for an animated GIF. An empty object is allowed and lets the service choose the frames.",
6101
+ properties: {
6102
+ frameCount: {
6103
+ type: "integer",
6104
+ description: "Number of frames, 2 to 16."
6105
+ },
6106
+ frameDurationMs: {
6107
+ type: "integer",
6108
+ description: "Milliseconds each frame is shown, 20 to 2000."
6109
+ }
6110
+ }
6111
+ }
6112
+ },
6122
6113
  output: {
6123
6114
  schema: {
6124
6115
  type: "object",
@@ -6143,7 +6134,7 @@ function createCodexImageTool(oauth, attachments, options = {}) {
6143
6134
  },
6144
6135
  mediaType: {
6145
6136
  type: "string",
6146
- enum: ["image/png"],
6137
+ enum: ["image/png", "image/gif"],
6147
6138
  required: true
6148
6139
  },
6149
6140
  bytes: {
@@ -6190,34 +6181,52 @@ function createCodexImageTool(oauth, attachments, options = {}) {
6190
6181
  async execute(args, exec) {
6191
6182
  const prompt = args.prompt.trim();
6192
6183
  if (prompt === "") throw new HarnessError("Image prompt cannot be empty.", "CODEX_IMAGE_INVALID_PROMPT");
6193
- if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new HarnessError("PNG image attachments are not enabled in this DSH environment.", "CODEX_IMAGE_ATTACHMENT_UNSUPPORTED");
6184
+ const gif = readGifOptions(args.gif);
6185
+ const references = await readReferenceImages(args.referenceImagePaths ?? [], attachments);
6186
+ assertOutputTypeSupported(attachments, gif === void 0 ? "image/png" : "image/gif");
6187
+ const body = buildImageBody({
6188
+ prompt,
6189
+ size: args.size ?? "auto",
6190
+ quality: args.quality ?? "auto",
6191
+ ...args.transparentBackground !== void 0 ? { transparentBackground: args.transparentBackground } : {},
6192
+ ...gif !== void 0 ? { gif } : {},
6193
+ references
6194
+ });
6195
+ const url = references.length > 0 ? CODEX_IMAGE_EDIT_URL : CODEX_IMAGE_GENERATION_URL;
6196
+ const turnId = String(exec.callId);
6194
6197
  let credentials = await imageCredentials(oauth);
6195
- let response = await requestImage$1(fetchFn, credentials, prompt, String(exec.callId), exec.signal);
6198
+ let response = await requestImage$1(fetchFn, credentials, url, body, turnId, exec.signal);
6196
6199
  if (response.status === 401) {
6197
6200
  await response.body?.cancel().catch(() => void 0);
6198
6201
  credentials = await imageCredentials(oauth, true);
6199
- response = await requestImage$1(fetchFn, credentials, prompt, String(exec.callId), exec.signal);
6202
+ response = await requestImage$1(fetchFn, credentials, url, body, turnId, exec.signal);
6200
6203
  }
6201
6204
  if (response.status === 429) {
6202
- await response.body?.cancel().catch(() => void 0);
6203
- throw new HarnessError("Codex image generation was rate limited.", "CODEX_IMAGE_RATE_LIMITED");
6205
+ const detail = upstreamExcerpt(await response.text().catch(() => ""));
6206
+ throw new HarnessError(`Codex image generation was rate limited (429).${detail === "" ? "" : ` ${detail}`}`, "CODEX_IMAGE_RATE_LIMITED");
6207
+ }
6208
+ if (response.status === 400) {
6209
+ const detail = upstreamExcerpt(await response.text().catch(() => ""));
6210
+ throw new HarnessError(`Codex rejected the image request (400). The parameters this call stated were: ${statedParameterSummary(args, gif, references.length)}. One of them is not accepted as given, and no field was dropped to retry.` + (detail === "" ? "" : ` Upstream said: ${detail}`), "CODEX_IMAGE_REQUEST_REJECTED");
6204
6211
  }
6205
6212
  if (!response.ok) {
6206
- await response.body?.cancel().catch(() => void 0);
6207
- throw new HarnessError(`Codex image generation failed (${response.status}).`, "CODEX_IMAGE_FAILED");
6213
+ const detail = upstreamExcerpt(await response.text().catch(() => ""));
6214
+ throw new HarnessError(`Codex image generation failed (${response.status}).${detail === "" ? "" : ` ${detail}`}`, "CODEX_IMAGE_FAILED");
6208
6215
  }
6209
- const bytes = decodeImageBytes(readBase64Image(await response.json()), maxGeneratedImageBytes(attachments));
6216
+ const decoded = decodeImageBytes(readBase64Image(await response.json()), maxGeneratedImageBytes(attachments));
6217
+ if (gif !== void 0 && decoded.mediaType !== "image/gif") throw new HarnessError(`Codex was asked for an animated GIF but returned ${decoded.mediaType}. Nothing was saved: the answer is not the animation that was requested. Omit the gif parameter to accept a still image.`, "CODEX_IMAGE_RESPONSE_INVALID");
6218
+ if (!attachments.imageLimits.mediaTypes.includes(decoded.mediaType)) throw new HarnessError(`Codex returned ${decoded.mediaType} but this DSH environment does not accept that attachment type.`, "CODEX_IMAGE_ATTACHMENT_UNSUPPORTED");
6210
6219
  const image = await attachments.saveImage({
6211
- data: bytes,
6212
- mediaType: "image/png",
6213
- name: "codex-generated-image.png"
6220
+ data: decoded.data,
6221
+ mediaType: decoded.mediaType,
6222
+ name: `codex-generated-image.${decoded.mediaType === "image/gif" ? "gif" : "png"}`
6214
6223
  });
6215
6224
  const output = {
6216
6225
  prompt,
6217
6226
  model: CODEX_IMAGE_MODEL,
6218
6227
  image: {
6219
6228
  attachmentId: image.attachmentId,
6220
- mediaType: "image/png",
6229
+ mediaType: decoded.mediaType,
6221
6230
  bytes: image.bytes,
6222
6231
  width: image.width,
6223
6232
  height: image.height,
@@ -6239,6 +6248,124 @@ function createCodexImageTool(oauth, attachments, options = {}) {
6239
6248
  }
6240
6249
  });
6241
6250
  }
6251
+ /**
6252
+ * Validate the frame options of a GIF request.
6253
+ *
6254
+ * An empty object is legal: it asks for an animation and leaves both numbers to
6255
+ * the service. A stated number is checked here rather than by the parameter
6256
+ * schema because the schema DSL carries no numeric bounds, and because
6257
+ * `execute` is reachable directly.
6258
+ */
6259
+ function readGifOptions(gif) {
6260
+ if (gif === void 0) return void 0;
6261
+ const result = {};
6262
+ const frameCount = gif.frameCount;
6263
+ if (frameCount !== void 0) {
6264
+ if (!Number.isInteger(frameCount) || frameCount < GIF_MIN_FRAMES || frameCount > GIF_MAX_FRAMES) throw new HarnessError(`gif.frameCount must be a whole number from ${GIF_MIN_FRAMES} to ${GIF_MAX_FRAMES} (got ${String(frameCount)}).`, "CODEX_IMAGE_GIF_INVALID");
6265
+ result.frameCount = frameCount;
6266
+ }
6267
+ const frameDurationMs = gif.frameDurationMs;
6268
+ if (frameDurationMs !== void 0) {
6269
+ if (!Number.isInteger(frameDurationMs) || frameDurationMs < GIF_MIN_FRAME_MS || frameDurationMs > GIF_MAX_FRAME_MS) throw new HarnessError(`gif.frameDurationMs must be a whole number of milliseconds from ${GIF_MIN_FRAME_MS} to ${GIF_MAX_FRAME_MS} (got ${String(frameDurationMs)}).`, "CODEX_IMAGE_GIF_INVALID");
6270
+ result.frameDurationMs = frameDurationMs;
6271
+ }
6272
+ return result;
6273
+ }
6274
+ /** Refuse an output type this deployment cannot store, before anything is requested. */
6275
+ function assertOutputTypeSupported(attachments, outputType) {
6276
+ if (attachments.imageLimits.mediaTypes.includes(outputType)) return;
6277
+ throw new HarnessError(outputType === "image/gif" ? "GIF output was requested, but this DSH environment does not accept GIF attachments, so nothing was requested. Omit the gif parameter to generate a PNG instead." : "PNG image attachments are not enabled in this DSH environment.", "CODEX_IMAGE_ATTACHMENT_UNSUPPORTED");
6278
+ }
6279
+ /**
6280
+ * Build the wire body of one request.
6281
+ *
6282
+ * Field order and the defaults are deliberate: a call with no new options
6283
+ * produces exactly `prompt/background auto/model/quality auto/size auto`, which
6284
+ * is the body this tool has always sent. Every new field appears only when the
6285
+ * caller stated it, so an old call site keeps its old bytes on the wire.
6286
+ */
6287
+ function buildImageBody(request) {
6288
+ const body = {
6289
+ prompt: request.prompt,
6290
+ background: request.transparentBackground === true ? "transparent" : request.transparentBackground === false ? "opaque" : "auto",
6291
+ model: CODEX_IMAGE_MODEL,
6292
+ quality: request.quality,
6293
+ size: request.size
6294
+ };
6295
+ if (request.gif !== void 0) {
6296
+ body.output_format = "gif";
6297
+ if (request.gif.frameCount !== void 0) body.frame_count = request.gif.frameCount;
6298
+ if (request.gif.frameDurationMs !== void 0) body.frame_duration_ms = request.gif.frameDurationMs;
6299
+ }
6300
+ if (request.references.length > 0) body.images = request.references.map((image) => ({ image_url: `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}` }));
6301
+ return body;
6302
+ }
6303
+ /**
6304
+ * The optional parameters one request actually stated, in words.
6305
+ *
6306
+ * A 400 says only that the request was invalid, which is useless on a tool with
6307
+ * seven parameters: the caller has to be told WHICH ones it sent, or it has no
6308
+ * way to know what to change. Only stated options are named - a default that the
6309
+ * tool has always sent cannot be the field to fix - and each GIF sub-field is
6310
+ * named separately, because `gif: {}` is a legal call that stated neither number.
6311
+ */
6312
+ function statedParameterSummary(args, gif, referenceCount) {
6313
+ const stated = [];
6314
+ if (args.size !== void 0) stated.push(`size=${args.size}`);
6315
+ if (args.quality !== void 0) stated.push(`quality=${args.quality}`);
6316
+ if (args.transparentBackground !== void 0) stated.push(`transparentBackground=${String(args.transparentBackground)}`);
6317
+ if (referenceCount > 0) stated.push(`referenceImagePaths (${referenceCount} image${referenceCount === 1 ? "" : "s"})`);
6318
+ if (gif !== void 0) {
6319
+ const frames = [];
6320
+ if (gif.frameCount !== void 0) frames.push(`frameCount=${String(gif.frameCount)}`);
6321
+ if (gif.frameDurationMs !== void 0) frames.push(`frameDurationMs=${String(gif.frameDurationMs)}`);
6322
+ stated.push(frames.length === 0 ? "gif (frame numbers left to the service)" : `gif (${frames.join(", ")})`);
6323
+ }
6324
+ return stated.length === 0 ? "no optional parameter was stated" : stated.join(", ");
6325
+ }
6326
+ /**
6327
+ * Read and validate the reference images one edit request names.
6328
+ *
6329
+ * Every named file is checked as a local still image: the path must not be a
6330
+ * URL, the extension must be PNG/JPEG/WebP, the file must be a regular non-empty
6331
+ * file inside the attachment byte limits, and its leading bytes must match the
6332
+ * declared type. Nothing else on disk is read, and the returned bytes are the
6333
+ * ONLY image data that reaches the request.
6334
+ */
6335
+ async function readReferenceImages(files, attachments) {
6336
+ if (files.length === 0) return [];
6337
+ const limit = Math.min(5, attachments.imageLimits.maxImagesPerMessage);
6338
+ if (files.length > limit) throw new HarnessError(`At most ${limit} reference images are accepted; ${files.length} were given.`, "CODEX_IMAGE_REFERENCE_LIMIT");
6339
+ const perImageBytes = attachments.imageLimits.maxImageBytes;
6340
+ const totalBytes = attachments.imageLimits.maxMessageImageBytes;
6341
+ const images = [];
6342
+ let sentBytes = 0;
6343
+ for (const [index, raw] of files.entries()) {
6344
+ const label = `referenceImagePaths[${index}]`;
6345
+ const value = raw.trim();
6346
+ if (value === "") throw new HarnessError(`${label} is empty; every reference image must name a local file.`, "CODEX_IMAGE_REFERENCE_INVALID_PATH");
6347
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value) || /^data:/i.test(value)) throw new HarnessError(`${label} is a URL (${value}). Reference images must be local PNG, JPEG or WebP files; nothing is fetched.`, "CODEX_IMAGE_REFERENCE_INVALID_PATH");
6348
+ const extension = extname(value).toLowerCase();
6349
+ const mediaType = REFERENCE_EXTENSIONS[extension];
6350
+ if (mediaType === void 0) throw new HarnessError(extension === ".gif" ? `${label} (${value}) is a GIF. Reference images must be still PNG, JPEG or WebP files; animated input is not part of the edit request.` : `${label} (${value}) is not a PNG, JPEG or WebP file (extension ${extension === "" ? "(none)" : `"${extension}"`}).`, "CODEX_IMAGE_REFERENCE_UNSUPPORTED");
6351
+ if (!attachments.imageLimits.mediaTypes.includes(mediaType)) throw new HarnessError(`${label} is ${mediaType}, which this DSH environment does not accept as an attachment.`, "CODEX_IMAGE_ATTACHMENT_UNSUPPORTED");
6352
+ const absolute = resolve(value);
6353
+ const stats = await stat(absolute).catch(() => void 0);
6354
+ if (stats === void 0) throw new HarnessError(`${label} (${value}) does not exist or cannot be read.`, "CODEX_IMAGE_REFERENCE_NOT_FOUND");
6355
+ if (!stats.isFile()) throw new HarnessError(`${label} (${value}) is not a regular file; pass a path to one PNG, JPEG or WebP file.`, "CODEX_IMAGE_REFERENCE_NOT_A_FILE");
6356
+ if (stats.size === 0) throw new HarnessError(`${label} (${value}) is empty.`, "CODEX_IMAGE_REFERENCE_EMPTY");
6357
+ if (stats.size > perImageBytes) throw new HarnessError(`${label} (${value}) is ${stats.size} bytes, above the ${perImageBytes} byte limit for one attachment.`, "CODEX_IMAGE_REFERENCE_TOO_LARGE");
6358
+ const data = new Uint8Array(await readFile(absolute));
6359
+ if (!SIGNATURES$1[mediaType](data)) throw new HarnessError(`${label} (${value}) does not start like a ${mediaType} file, so its bytes contradict its extension.`, "CODEX_IMAGE_REFERENCE_INVALID_CONTENT");
6360
+ sentBytes += data.byteLength;
6361
+ if (sentBytes > totalBytes) throw new HarnessError(`The reference images total ${sentBytes} bytes, above the ${totalBytes} byte limit for one message.`, "CODEX_IMAGE_REFERENCE_TOO_LARGE");
6362
+ images.push({
6363
+ mediaType,
6364
+ data
6365
+ });
6366
+ }
6367
+ return images;
6368
+ }
6242
6369
  async function imageCredentials(oauth, force = false) {
6243
6370
  try {
6244
6371
  return await oauth.credentials(force, { purpose: "tool" });
@@ -6246,8 +6373,8 @@ async function imageCredentials(oauth, force = false) {
6246
6373
  throw new HarnessError(`ChatGPT subscription credentials are required for Codex image generation.${error instanceof Error && error.message !== "" ? ` (${error.message})` : ""}`, "CODEX_IMAGE_CREDENTIAL_MISSING", { cause: error });
6247
6374
  }
6248
6375
  }
6249
- function requestImage$1(fetchFn, credentials, prompt, turnId, signal) {
6250
- return fetchFn(CODEX_IMAGE_GENERATION_URL, {
6376
+ function requestImage$1(fetchFn, credentials, url, body, turnId, signal) {
6377
+ return fetchFn(url, {
6251
6378
  method: "POST",
6252
6379
  headers: {
6253
6380
  ...codexHeaders(credentials),
@@ -6255,16 +6382,19 @@ function requestImage$1(fetchFn, credentials, prompt, turnId, signal) {
6255
6382
  "content-type": "application/json",
6256
6383
  "x-codex-image-turn-id": turnId
6257
6384
  },
6258
- body: JSON.stringify({
6259
- prompt,
6260
- background: "auto",
6261
- model: CODEX_IMAGE_MODEL,
6262
- quality: "auto",
6263
- size: "auto"
6264
- }),
6385
+ body: JSON.stringify(body),
6265
6386
  signal
6266
6387
  });
6267
6388
  }
6389
+ /**
6390
+ * The upstream error body, short enough to read in a message and stripped of
6391
+ * anything that must never be echoed: bearer credentials, base64 image payloads
6392
+ * and any long base64-shaped run.
6393
+ */
6394
+ function upstreamExcerpt(text) {
6395
+ const collapsed = text.replace(/data:[a-z0-9.+-]+\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]*/gi, "data:<redacted>").replace(/Bearer\s+[A-Za-z0-9._~+/=-]{6,}/gi, "Bearer <redacted>").replace(/[A-Za-z0-9+/]{200,}={0,2}/g, "<redacted>").replace(/\s+/g, " ").trim();
6396
+ return collapsed.length > MAX_UPSTREAM_EXCERPT_CHARS ? `${collapsed.slice(0, MAX_UPSTREAM_EXCERPT_CHARS)}…` : collapsed;
6397
+ }
6268
6398
  function readBase64Image(value) {
6269
6399
  const root = record$7(value);
6270
6400
  const data = Array.isArray(root?.data) ? root.data[0] : null;
@@ -6272,12 +6402,27 @@ function readBase64Image(value) {
6272
6402
  if (image === null) throw new HarnessError("Codex image response did not include image data.", "CODEX_IMAGE_RESPONSE_INVALID");
6273
6403
  return image;
6274
6404
  }
6405
+ /**
6406
+ * Decode one returned image and identify it by its own magic bytes.
6407
+ *
6408
+ * The endpoint answers a requested GIF with GIF bytes in the same `b64_json`
6409
+ * field it uses for PNG, and states nothing about which one it produced, so the
6410
+ * bytes are the only reliable source for the media type – and storing a GIF
6411
+ * under `image/png` would fail at save time or, worse, mislabel the file.
6412
+ */
6275
6413
  function decodeImageBytes(base64, maxBytes) {
6276
6414
  if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64) || base64.length % 4 !== 0) throw new HarnessError("Codex image response was not valid base64.", "CODEX_IMAGE_RESPONSE_INVALID");
6277
6415
  const buffer = Buffer.from(base64, "base64");
6278
6416
  if (buffer.length > maxBytes) throw new HarnessError("Generated image exceeds the configured attachment size limit.", "CODEX_IMAGE_TOO_LARGE");
6279
- for (const [index, byte] of PNG_SIGNATURE.entries()) if (buffer[index] !== byte) throw new HarnessError("Codex image response was not a PNG image.", "CODEX_IMAGE_RESPONSE_INVALID");
6280
- return buffer;
6417
+ if (SIGNATURES$1["image/gif"](buffer)) return {
6418
+ mediaType: "image/gif",
6419
+ data: buffer
6420
+ };
6421
+ for (const [index, byte] of PNG_SIGNATURE.entries()) if (buffer[index] !== byte) throw new HarnessError("Codex image response was neither PNG nor GIF image data.", "CODEX_IMAGE_RESPONSE_INVALID");
6422
+ return {
6423
+ mediaType: "image/png",
6424
+ data: buffer
6425
+ };
6281
6426
  }
6282
6427
  function maxGeneratedImageBytes(attachments) {
6283
6428
  return Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
@@ -10451,6 +10596,9 @@ var AntigravityAdapter = class extends LlmAdapter {
10451
10596
  /**
10452
10597
  * Aspect ratios Gemini image models accept in `imageConfig.aspectRatio`.
10453
10598
  * Omitted, the model picks one itself, which is what the request then does.
10599
+ *
10600
+ * The last four are the extreme strips: a wider or taller frame than the square
10601
+ * family covers, useful for banners, headers and phone wallpapers.
10454
10602
  */
10455
10603
  const ANTIGRAVITY_IMAGE_ASPECT_RATIOS = [
10456
10604
  "1:1",
@@ -10462,7 +10610,21 @@ const ANTIGRAVITY_IMAGE_ASPECT_RATIOS = [
10462
10610
  "5:4",
10463
10611
  "9:16",
10464
10612
  "16:9",
10465
- "21:9"
10613
+ "21:9",
10614
+ "1:4",
10615
+ "4:1",
10616
+ "1:8",
10617
+ "8:1"
10618
+ ];
10619
+ /**
10620
+ * Image sizes Gemini image models accept in `imageConfig.imageSize`.
10621
+ * Omitted, the model picks one itself, which is what the request then does.
10622
+ */
10623
+ const ANTIGRAVITY_IMAGE_SIZES = [
10624
+ "0.5K",
10625
+ "1K",
10626
+ "2K",
10627
+ "4K"
10466
10628
  ];
10467
10629
  /** Images one call keeps; the model normally returns one. */
10468
10630
  const MAX_IMAGES_PER_CALL = 4;
@@ -10476,7 +10638,11 @@ const MAX_IMAGES_PER_CALL = 4;
10476
10638
  * Antigravity clients send for image runtimes; it has not been captured from
10477
10639
  * the official client.
10478
10640
  */
10479
- function buildImageRequest(prompt, projectId, aspectRatio) {
10641
+ function buildImageRequest(prompt, projectId, aspectRatio, imageSize) {
10642
+ const imageConfig = {
10643
+ ...aspectRatio ? { aspectRatio } : {},
10644
+ ...imageSize ? { imageSize } : {}
10645
+ };
10480
10646
  return {
10481
10647
  project: projectId,
10482
10648
  model: ANTIGRAVITY_IMAGE_MODEL,
@@ -10487,7 +10653,7 @@ function buildImageRequest(prompt, projectId, aspectRatio) {
10487
10653
  }],
10488
10654
  generationConfig: {
10489
10655
  responseModalities: ["TEXT", "IMAGE"],
10490
- ...aspectRatio ? { imageConfig: { aspectRatio } } : {}
10656
+ ...Object.keys(imageConfig).length > 0 ? { imageConfig } : {}
10491
10657
  }
10492
10658
  },
10493
10659
  requestType: "image_gen",
@@ -10599,7 +10765,7 @@ function createAntigravityImageTool(accountPool, store, attachments, options = {
10599
10765
  const fetchFn = options.fetchFn ?? fetch;
10600
10766
  return defineTool({
10601
10767
  name: ANTIGRAVITY_IMAGE_TOOL_NAME,
10602
- description: "Generate an image with Gemini image generation through the signed-in Antigravity account. Describe the subject, style, composition and any text to render in the prompt.",
10768
+ description: "Generate an image with Gemini image generation through the signed-in Antigravity account. Describe the subject, style, composition and any text to render in the prompt. Scheduling: a single generation, a change to an image already generated, and a retry of a failed attempt are direct calls from the agent that needs the image — do not create a subagent just to generate one image. Delegate only a task that genuinely needs several alternative directions or complex creative planning, and pass the prompt that comes back to this tool.",
10603
10769
  parameters: {
10604
10770
  prompt: {
10605
10771
  type: "string",
@@ -10609,7 +10775,12 @@ function createAntigravityImageTool(accountPool, store, attachments, options = {
10609
10775
  aspectRatio: {
10610
10776
  type: "string",
10611
10777
  enum: ANTIGRAVITY_IMAGE_ASPECT_RATIOS,
10612
- description: "Optional aspect ratio. Omit it to let the model choose."
10778
+ description: "Optional aspect ratio, one of the listed values. Omit it to let the model choose. If the upstream rejects the value, re-issue the call with another value from the list rather than the same one."
10779
+ },
10780
+ imageSize: {
10781
+ type: "string",
10782
+ enum: ANTIGRAVITY_IMAGE_SIZES,
10783
+ description: "Optional output size, one of 0.5K, 1K, 2K or 4K. Omit it to let the model choose. It is sent only when given, so it never overrides an aspect ratio the caller did not set."
10613
10784
  }
10614
10785
  },
10615
10786
  output: {
@@ -10693,7 +10864,7 @@ function createAntigravityImageTool(accountPool, store, attachments, options = {
10693
10864
  const prompt = args.prompt.trim();
10694
10865
  if (prompt === "") throw new HarnessError("Image prompt cannot be empty.", "ANTIGRAVITY_IMAGE_INVALID_PROMPT");
10695
10866
  if (options.isEnabled?.() === false) throw new HarnessError("The Antigravity line is switched off; enable it in Settings → Antigravity to generate images.", "ANTIGRAVITY_IMAGE_DISABLED");
10696
- const response = await requestImage(accountPool, store, fetchFn, prompt, args.aspectRatio, exec.signal);
10867
+ const response = await requestImage(accountPool, store, fetchFn, prompt, args.aspectRatio, args.imageSize, exec.signal);
10697
10868
  if (response.images.length === 0) throw new HarnessError(noImageMessage(response), "ANTIGRAVITY_IMAGE_EMPTY");
10698
10869
  const keep = Math.min(MAX_IMAGES_PER_CALL, attachments.imageLimits.maxImagesPerMessage, response.images.length);
10699
10870
  const saved = [];
@@ -10752,12 +10923,37 @@ async function acquireAccount(accountPool, store, tried, fetchFn) {
10752
10923
  }
10753
10924
  }
10754
10925
  }
10755
- async function requestImage(accountPool, store, fetchFn, prompt, aspectRatio, signal) {
10926
+ /**
10927
+ * Whether a status means the upstream refused the SHAPE of the request rather
10928
+ * than the account, the allowance or the transient state of the service.
10929
+ */
10930
+ const PARAMETER_REJECTION_STATUSES = /* @__PURE__ */ new Set([400, 422]);
10931
+ /**
10932
+ * What a failed image request reports: the status, a bounded excerpt of what
10933
+ * the upstream said, and — when the refusal was about the request — which
10934
+ * optional fields were sent.
10935
+ *
10936
+ * A parameter rejection is never retried without the field it names. Dropping
10937
+ * `aspectRatio` or `imageSize` would answer with an image that silently has a
10938
+ * shape nobody asked for, so the caller is told which fields to change and
10939
+ * makes that call itself.
10940
+ */
10941
+ function imageFailureMessage(status, errorText, sent) {
10942
+ const parts = [`Antigravity image generation failed (${status ?? "no response"}).`];
10943
+ if (status !== void 0 && PARAMETER_REJECTION_STATUSES.has(status)) {
10944
+ const fields = [sent.aspectRatio === void 0 ? void 0 : `aspectRatio "${sent.aspectRatio}"`, sent.imageSize === void 0 ? void 0 : `imageSize "${sent.imageSize}"`].filter((field) => field !== void 0);
10945
+ parts.push(fields.length > 0 ? `The request itself was refused, and these optional fields were what it carried: ${fields.join(", ")}. It is not retried without them: re-issue the call with a different candidate value, or omit the field, because the upstream refused this request shape.` : "The request itself was refused while no aspectRatio or imageSize was set, so a different value for either cannot fix it; change the prompt or check the account before retrying.");
10946
+ }
10947
+ const excerpt = upstreamBodyExcerpt(errorText);
10948
+ if (excerpt !== "") parts.push(`Upstream said: ${excerpt}`);
10949
+ return parts.join(" ");
10950
+ }
10951
+ async function requestImage(accountPool, store, fetchFn, prompt, aspectRatio, imageSize, signal) {
10756
10952
  const tried = /* @__PURE__ */ new Set();
10757
10953
  while (true) {
10758
10954
  const { account, token, projectId } = await acquireAccount(accountPool, store, tried, fetchFn);
10759
10955
  tried.add(account.id);
10760
- const body = JSON.stringify(buildImageRequest(prompt, projectId || "antigravity-default", aspectRatio));
10956
+ const body = JSON.stringify(buildImageRequest(prompt, projectId || "antigravity-default", aspectRatio, imageSize));
10761
10957
  let response;
10762
10958
  let firstResponse;
10763
10959
  for (const endpoint of endpointCandidates()) try {
@@ -10789,7 +10985,10 @@ async function requestImage(accountPool, store, fetchFn, prompt, aspectRatio, si
10789
10985
  }
10790
10986
  throw new HarnessError(`Antigravity image generation was rate limited (429) without a stated reset time, so it is handled as a transient rate limit rather than a spent quota. ${upstreamBodyExcerpt(errorText)}`.trim(), "ANTIGRAVITY_IMAGE_RATE_LIMITED");
10791
10987
  }
10792
- throw new HarnessError(`Antigravity image generation failed (${status ?? "no response"}).${errorText ? ` ${errorText}` : ""}`, "ANTIGRAVITY_IMAGE_FAILED");
10988
+ throw new HarnessError(imageFailureMessage(status, errorText, {
10989
+ aspectRatio,
10990
+ imageSize
10991
+ }), "ANTIGRAVITY_IMAGE_FAILED");
10793
10992
  }
10794
10993
  }
10795
10994
  //#endregion
@@ -19773,6 +19972,21 @@ const COMMAND_CODE_MODELS = [
19773
19972
  maxTokens: 256e3,
19774
19973
  hidden: false
19775
19974
  },
19975
+ {
19976
+ id: "stealth/crouton-alpha",
19977
+ name: "Crouton Alpha",
19978
+ inputModalities: ["text", "image"],
19979
+ reasoningEfforts: [
19980
+ "low",
19981
+ "medium",
19982
+ "high",
19983
+ "xhigh",
19984
+ "max"
19985
+ ],
19986
+ contextWindow: 1048576,
19987
+ maxTokens: 943718,
19988
+ hidden: false
19989
+ },
19776
19990
  {
19777
19991
  id: "reflection/beam:free",
19778
19992
  name: "Beam 1.0",
@@ -19790,7 +20004,7 @@ const COMMAND_CODE_MODELS = [
19790
20004
  },
19791
20005
  {
19792
20006
  id: "poolside/laguna-s-2.1-free",
19793
- name: "Laguna S 2.1",
20007
+ name: "Laguna S 2.1 (Free)",
19794
20008
  inputModalities: ["text"],
19795
20009
  reasoningEfforts: [],
19796
20010
  contextWindow: 256e3,
@@ -19819,6 +20033,32 @@ const COMMAND_CODE_MODELS = [
19819
20033
  maxTokens: 32768,
19820
20034
  hidden: false
19821
20035
  },
20036
+ {
20037
+ id: "inclusionai/ling-3.1-flash-1019",
20038
+ name: "Ling 3.1 Flash 1019",
20039
+ inputModalities: ["text"],
20040
+ reasoningEfforts: [
20041
+ "low",
20042
+ "medium",
20043
+ "high"
20044
+ ],
20045
+ contextWindow: 1e6,
20046
+ maxTokens: 262144,
20047
+ hidden: false
20048
+ },
20049
+ {
20050
+ id: "inclusionai/ling-3.1-flash-1019:free",
20051
+ name: "Ling 3.1 Flash 1019",
20052
+ inputModalities: ["text"],
20053
+ reasoningEfforts: [
20054
+ "low",
20055
+ "medium",
20056
+ "high"
20057
+ ],
20058
+ contextWindow: 1e6,
20059
+ maxTokens: 262144,
20060
+ hidden: true
20061
+ },
19822
20062
  {
19823
20063
  id: "meta/muse-spark-1.1",
19824
20064
  name: "Muse Spark 1.1",
@@ -19980,42 +20220,37 @@ const COMMAND_CODE_MODELS = [
19980
20220
  function commandCodeModelDef(modelId) {
19981
20221
  return COMMAND_CODE_MODELS.find((model) => model.id === modelId);
19982
20222
  }
19983
- /**
19984
- * Model ids the 1.88.2 catalog refresh added to the shipped default selection.
19985
- *
19986
- * The shipped default is the set of models the settings card offers before the
19987
- * user edits it, and a stored selection that still equals it counts as "never
19988
- * edited" (see `resolveEnabledModelIds` in `./routes.ts`). A refresh that
19989
- * changed the default without saying so would make every untouched selection
19990
- * from the previous release look edited, and the models this release adds
19991
- * would stay hidden behind a list nobody touched. These two lists carry that
19992
- * change: the retired ids are the ones the previous default had and this one
19993
- * does not, and the added ids are the ones it gained.
19994
- */
19995
- const SHIPPED_DEFAULT_ADDED_MODEL_IDS = [
19996
- "claude-haiku-5-5",
19997
- "gpt-6.1-terra",
19998
- "moonshotai/Kimi-K3.1-Pro",
19999
- "minimax/minimax-m3.1-flash",
20000
- "ornith/ornith-1.5-122b",
20001
- "Qwen/Qwen3.8-Max-Prime",
20002
- "google/gemini-4-argon",
20003
- "nvidia/nemotron-3.5-super-vl",
20004
- "stealth/glyph-cluster:free",
20005
- "reflection/beam:free",
20006
- "inclusionai/ling-3.1-flash",
20007
- "meta/muse-spark-open",
20008
- "meta/muse-wonder-1.0",
20009
- "mistral/mistral-large-4"
20010
- ];
20011
- /** Model ids the 1.88.2 catalog refresh dropped from the shipped default selection. */
20012
- const SHIPPED_DEFAULT_RETIRED_MODEL_IDS = [
20013
- "xiaomi/mimo-v2.5-pro",
20014
- "xiaomi/mimo-v2.5",
20015
- "stealth/space-bunny-alpha",
20016
- "inclusionai/ling-3.0-flash-sante:free",
20017
- "inclusionai/ling-3.1-flash:free"
20018
- ];
20223
+ /** Default-selection deltas recognized by `resolveEnabledModelIds` in `./routes.ts`. */
20224
+ const SHIPPED_DEFAULT_MIGRATIONS = [{
20225
+ version: "1.88.4",
20226
+ added: ["stealth/crouton-alpha", "inclusionai/ling-3.1-flash-1019"],
20227
+ retired: []
20228
+ }, {
20229
+ version: "1.88.2",
20230
+ added: [
20231
+ "claude-haiku-5-5",
20232
+ "gpt-6.1-terra",
20233
+ "moonshotai/Kimi-K3.1-Pro",
20234
+ "minimax/minimax-m3.1-flash",
20235
+ "ornith/ornith-1.5-122b",
20236
+ "Qwen/Qwen3.8-Max-Prime",
20237
+ "google/gemini-4-argon",
20238
+ "nvidia/nemotron-3.5-super-vl",
20239
+ "stealth/glyph-cluster:free",
20240
+ "reflection/beam:free",
20241
+ "inclusionai/ling-3.1-flash",
20242
+ "meta/muse-spark-open",
20243
+ "meta/muse-wonder-1.0",
20244
+ "mistral/mistral-large-4"
20245
+ ],
20246
+ retired: [
20247
+ "xiaomi/mimo-v2.5-pro",
20248
+ "xiaomi/mimo-v2.5",
20249
+ "stealth/space-bunny-alpha",
20250
+ "inclusionai/ling-3.0-flash-sante:free",
20251
+ "inclusionai/ling-3.1-flash:free"
20252
+ ]
20253
+ }];
20019
20254
  //#endregion
20020
20255
  //#region src/host/command-code/types.ts
20021
20256
  const PROVIDER_ID$11 = "command-code";
@@ -24386,16 +24621,20 @@ function resolveEnabledModelIds$5(stored, catalog, enabled = true) {
24386
24621
  /**
24387
24622
  * Whether a stored selection still is a shipped default.
24388
24623
  *
24389
- * The current one, and the one the previous release shipped: the refresh moved
24390
- * a handful of models in and out of the default, and the difference between the
24391
- * two defaults is exactly the pair of id lists the catalog records.
24624
+ * Walk the recorded catalog refreshes newest-first. Each step reconstructs one
24625
+ * earlier default, so users who never edited either the immediate previous
24626
+ * release or an older recorded release still receive the current catalog.
24392
24627
  */
24393
24628
  function isUneditedSelection(stored, shipped) {
24394
24629
  if (stored.length === 0) return false;
24395
24630
  if (sameSelection(stored, shipped)) return true;
24396
- const previous = new Set([...shipped].filter((id) => !SHIPPED_DEFAULT_ADDED_MODEL_IDS.includes(id)));
24397
- for (const id of SHIPPED_DEFAULT_RETIRED_MODEL_IDS) previous.add(id);
24398
- return sameSelection(stored, previous);
24631
+ let previous = new Set(shipped);
24632
+ for (const migration of SHIPPED_DEFAULT_MIGRATIONS) {
24633
+ previous = new Set([...previous].filter((id) => !migration.added.includes(id)));
24634
+ for (const id of migration.retired) previous.add(id);
24635
+ if (sameSelection(stored, previous)) return true;
24636
+ }
24637
+ return false;
24399
24638
  }
24400
24639
  /** Whether two id lists name exactly the same models, in any order. */
24401
24640
  function sameSelection(stored, other) {
@@ -27971,6 +28210,25 @@ const TOOL_SOURCE = /(?:^|[-_/])(?:read|view|cat|open|edit|write|patch|apply_pat
27971
28210
  /** 独立开关覆盖的检索工具;只有明确导航任务才允许进入新投影。 */
27972
28211
  const TOOL_RETRIEVAL = /^(?:read|web_fetch|grep|glob)$/;
27973
28212
  /**
28213
+ * PTC 家族的**程序传输**工具名(一个集合,两个名字)。
28214
+ *
28215
+ * 「模型写一段程序、工具调用发生在程序内部」是同一种传输,只是不同预设给主工具起
28216
+ * 了不同名字:PTC 预设叫 `run_code`,Code Mode 预设叫 `code_run`。凡是按工具名判断
28217
+ * 「这条结果来自程序传输」的地方都必须认这**整个集合**——名字只写一个,预设一换,
28218
+ * 大结果的 `ref://` 预览与精确补读就会静默失效。
28219
+ */
28220
+ const CACHE_WARMUP_PTC_TRANSPORT_TOOLS = /* @__PURE__ */ new Set(["run_code", "code_run"]);
28221
+ /**
28222
+ * 这个名字是否属于 PTC 家族的程序传输工具。
28223
+ *
28224
+ * 只做**精确**匹配:`code_runner` 之类的近似名不算,宁可少投影也不能误伤。
28225
+ * @param name - 工具名。
28226
+ * @returns 属于 {@link CACHE_WARMUP_PTC_TRANSPORT_TOOLS} 时为 true。
28227
+ */
28228
+ function isPtcTransportName(name) {
28229
+ return CACHE_WARMUP_PTC_TRANSPORT_TOOLS.has(name);
28230
+ }
28231
+ /**
27974
28232
  * 多节/多读输出进入新投影的最小长度门槛(字符)。
27975
28233
  *
27976
28234
  * 短于这个长度、又没有 spill 的普通源码与契约读一律逐字保留:
@@ -29266,7 +29524,7 @@ function planProjection(text, toolName, navigation, retrievalOnly, spilled = fal
29266
29524
  kind: "verbatim",
29267
29525
  strategy: "verbatim-contract"
29268
29526
  };
29269
- if (toolName === "run_code" && navigation === true) {
29527
+ if (isPtcTransportName(toolName) && navigation === true) {
29270
29528
  const outline = planRetrievalOutline(text, "read");
29271
29529
  if (outline !== void 0) return outline;
29272
29530
  if (retrievalOnly === true) return {
@@ -92083,6 +92341,7 @@ const RENAMED_PACKAGES = /* @__PURE__ */ new Map([["@deepseek-ai/dsh-workflow-wo
92083
92341
  */
92084
92342
  const OPTIONALLY_ABSENT_PACKAGES = [
92085
92343
  "@deepseek-ai/dsh-tool-present",
92344
+ "@deepseek-ai/dsh-tool-subagent-control/list-agents",
92086
92345
  "@eddyskywalker/dsh-chatgpt-subscription/plan-mode-pipeline",
92087
92346
  "@eddyskywalker/dsh-chatgpt-subscription/plan-mode",
92088
92347
  "@eddyskywalker/dsh-chatgpt-subscription/tool-result-pruner"
@@ -92145,12 +92404,20 @@ function candidatePackageRoots(entryScript = process.argv[1]) {
92145
92404
  } catch {}
92146
92405
  return [...roots];
92147
92406
  }
92407
+ /** The package-name portion of a bare package or package-subpath specifier. */
92408
+ function packageNameOf(specifier) {
92409
+ return specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
92410
+ }
92148
92411
  /**
92149
92412
  * Whether a package or exported subpath is installed in any candidate node_modules tree.
92150
92413
  * A subpath needs its own exact `exports` key; the package root alone is not enough.
92414
+ *
92415
+ * The nearest manifest on each Node resolution chain is authoritative. Looking
92416
+ * farther out after that manifest rejects a subpath would let an unrelated old
92417
+ * installation resurrect an export the running package deliberately removed.
92151
92418
  */
92152
92419
  function packageInstalled(specifier, roots = candidatePackageRoots()) {
92153
- const pkg = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
92420
+ const pkg = packageNameOf(specifier);
92154
92421
  const subpath = specifier.slice(pkg.length);
92155
92422
  for (const root of roots) {
92156
92423
  let dir = root;
@@ -92160,9 +92427,9 @@ function packageInstalled(specifier, roots = candidatePackageRoots()) {
92160
92427
  if (subpath === "") return true;
92161
92428
  try {
92162
92429
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
92163
- if (manifest.exports !== void 0 && Object.prototype.hasOwnProperty.call(manifest.exports, `.${subpath}`)) return true;
92430
+ return manifest.exports !== void 0 && Object.prototype.hasOwnProperty.call(manifest.exports, `.${subpath}`);
92164
92431
  } catch {
92165
- continue;
92432
+ return false;
92166
92433
  }
92167
92434
  }
92168
92435
  const parent = dirname(dir);
@@ -92173,20 +92440,72 @@ function packageInstalled(specifier, roots = candidatePackageRoots()) {
92173
92440
  return false;
92174
92441
  }
92175
92442
  /**
92176
- * Whether a bare specifier resolves from this module in the running
92177
- * installation. A harness whose Node build exposes no `import.meta.resolve`
92178
- * or whose plugin is executed from outside the profile tree falls through to
92179
- * checking candidate `node_modules` trees across the running harness home.
92443
+ * Packages the preset intentionally loads from the plugin's own dependency
92444
+ * closure rather than the harness installation. Every other harness row must
92445
+ * resolve where the registry mounts it, not merely beside this plugin.
92446
+ */
92447
+ const PLUGIN_RESOLVED_PACKAGES = ["@eddyskywalker/dsh-chatgpt-subscription", "@deepseek-ai/dsh-tool-executor-native"];
92448
+ /** Whether `candidate` is `root` itself or one of its descendants. */
92449
+ function pathInside(root, candidate) {
92450
+ const rel = relative(root, candidate);
92451
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
92452
+ }
92453
+ /**
92454
+ * Whether a plugin-side resolution points into the package the harness's own
92455
+ * entry script reaches. Walking ancestors mirrors Node's node_modules lookup;
92456
+ * the CLI commonly sits in `bin/`, beside rather than inside node_modules.
92457
+ */
92458
+ function resolvedFromHarness(resolved, entryDir, specifier) {
92459
+ if (typeof resolved !== "string") return false;
92460
+ let path;
92461
+ try {
92462
+ path = fileURLToPath(resolved);
92463
+ } catch {
92464
+ return false;
92465
+ }
92466
+ const pkg = packageNameOf(specifier);
92467
+ let dir = entryDir;
92468
+ for (;;) {
92469
+ if (pathInside(join(dir, "node_modules", pkg), path)) return true;
92470
+ const parent = dirname(dir);
92471
+ if (parent === dir) return false;
92472
+ dir = parent;
92473
+ }
92474
+ }
92475
+ /**
92476
+ * Resolve against the installation that will actually mount the preset.
92477
+ *
92478
+ * `import.meta.resolve` is based at this plugin, while the harness mounts
92479
+ * runtime-declared preset rows from its own context. A successful plugin-side
92480
+ * resolution is therefore not sufficient for a harness package: an old copy in
92481
+ * the profile tree must not override the running harness's missing export.
92482
+ * Plugin-owned rows and the plugin-borne native executor are the deliberate
92483
+ * exceptions. With no harness entry, retain the legacy local probe behavior.
92484
+ */
92485
+ function resolvesFromInstallation(specifier, entryScript, resolveFromPlugin) {
92486
+ const entryDir = harnessEntryDir(entryScript);
92487
+ if (entryDir !== void 0 && packageInstalled(specifier, [entryDir])) return true;
92488
+ let resolved;
92489
+ let didResolve = false;
92490
+ if (resolveFromPlugin !== void 0) try {
92491
+ resolved = resolveFromPlugin(specifier);
92492
+ didResolve = true;
92493
+ } catch {}
92494
+ if (entryDir === void 0) {
92495
+ if (didResolve) return true;
92496
+ return packageInstalled(specifier, candidatePackageRoots(entryScript));
92497
+ }
92498
+ if (didResolve && PLUGIN_RESOLVED_PACKAGES.includes(packageNameOf(specifier))) return true;
92499
+ return didResolve && resolvedFromHarness(resolved, entryDir, specifier);
92500
+ }
92501
+ /**
92502
+ * Whether a bare specifier resolves in the installation that mounts presets.
92180
92503
  * @param specifier - bare package specifier.
92181
92504
  * @returns whether the specifier resolves.
92182
92505
  */
92183
92506
  function resolvesFromHere(specifier) {
92184
- const resolve = import.meta.resolve;
92185
- if (resolve !== void 0) try {
92186
- resolve(specifier);
92187
- return true;
92188
- } catch {}
92189
- return packageInstalled(specifier);
92507
+ const resolveFromPlugin = import.meta.resolve;
92508
+ return resolvesFromInstallation(specifier, process.argv[1], resolveFromPlugin);
92190
92509
  }
92191
92510
  /**
92192
92511
  * Point every renamed package name at the spelling this installation ships.
@@ -92527,6 +92846,20 @@ const PRESET_REGISTRY_PACKAGE = "@deepseek-ai/dsh-agent-preset";
92527
92846
  const WORKING_DIRECTORY_PACKAGE = "@deepseek-ai/dsh-working-directory";
92528
92847
  /** The standalone PTC executor introduced when tool presentation split into executor plugins. */
92529
92848
  const PTC_EXECUTOR_PACKAGE = "@deepseek-ai/dsh-tool-executor-ptc";
92849
+ /**
92850
+ * The native executor this package delegates Code Mode execution to.
92851
+ *
92852
+ * A program sub-call must run through the harness's own policy, approval, and
92853
+ * post-execute pipeline; re-implementing that pipeline is how a plugin silently
92854
+ * drops policy. A profile plugin cannot resolve the host's kernel packages, so
92855
+ * this package carries the executor as a real dependency and drives the host
92856
+ * registry with it.
92857
+ */
92858
+ const NATIVE_EXECUTOR_PACKAGE = "@deepseek-ai/dsh-tool-executor-native";
92859
+ /** This package's Code Mode row: the preset's own tools stay native, the rest is program-only. */
92860
+ const CODEMODE_PACKAGE = "@eddyskywalker/dsh-chatgpt-subscription/codemode";
92861
+ /** The deprecated presentation row generations without a native executor keep. */
92862
+ const AGENT_TOOL_PRESENTATION_PACKAGE = "@deepseek-ai/dsh-agent-tool-presentation";
92530
92863
  /** The id the `dispatch` preset is selectable under. */
92531
92864
  const DISPATCH_PRESET_ID = "dispatch";
92532
92865
  /**
@@ -92542,7 +92875,7 @@ const DISPATCH_PRESET_ID = "dispatch";
92542
92875
  const DISPATCH_PRESET = {
92543
92876
  id: DISPATCH_PRESET_ID,
92544
92877
  name: "调度模式",
92545
- description: "基于 PTC 模式的调度 Agent:先分诊任务复杂度,L2 任务在规划前先做澄清访谈,再由主代理规划、派发子代理(显式选择子代理模型)、审查与验收。",
92878
+ description: "基于 Code Mode 的调度 Agent:先分诊任务复杂度,L2 任务在规划前先做澄清访谈,再由主代理规划、派发子代理(显式选择子代理模型)、审查与验收。",
92546
92879
  order: 5,
92547
92880
  plugins: [
92548
92881
  {
@@ -92561,8 +92894,8 @@ const DISPATCH_PRESET = {
92561
92894
 
92562
92895
  ## R0.5 · Compatibility Gate (Once per New Session, Before R0)
92563
92896
  A legacy Agent Teams runtime (pre-#7519) registers spawn_teammate and replaces send_message / list_agents / interrupt_agent with team-scoped variants, leaving spawned subagents unreachable. The current runtime only adds a shared task board (team_task_create and its list/get/update siblings) and reuses this mode's tools.
92564
- Criterion: whether spawn_teammate exists (PTC: tools.spawn_teammate). Never gate on wait_agent, send_message, list_agents, or interrupt_agent: this mode mounts those itself.
92565
- - Absent -> Write "Orchestration ready: subagent" and proceed to R0. The board tools stay visible in the PTC SDK whether or not the board is on - frozen declaration, not availability - so confirm with team_task_list before relying on it. When the board is on it is bookkeeping, not a second dispatcher: the dispatch ledger stays the dispatch authority and R5 briefs stay the task contract; publish cross-agent dependencies (blocked_by) and write scopes on one team task per brief, claimed and completed by the child that owns it; never start work because a task is ready, and never reassign a task to skip R6.
92897
+ Criterion: whether spawn_teammate exists (check via code_run). Never gate on wait_agent, send_message, list_agents, or interrupt_agent: this mode mounts those itself.
92898
+ - Absent -> Write "Orchestration ready: subagent" and proceed to R0. The board tools stay declared whether or not the board is on - frozen declaration, not availability - so confirm with team_task_list before relying on it. When the board is on it is bookkeeping, not a second dispatcher: the dispatch ledger stays the dispatch authority and R5 briefs stay the task contract; publish cross-agent dependencies (blocked_by) and write scopes on one team task per brief, claimed and completed by the child that owns it; never start work because a task is ready, and never reassign a task to skip R6.
92566
92899
  - Present -> This session runs the legacy runtime and is **not orchestratable**. In order, and nothing else:
92567
92900
  1. State in one sentence that the legacy runtime shadows the coordination tools and this mode has no degraded path.
92568
92901
  2. Call ask_user_question once (see R-W) offering: disable the legacy runtime, restart Host, **start a new session**; switch preset; or execute this task directly.
@@ -92589,9 +92922,10 @@ L0/L1: execute directly (or 1 subagent for L1). L2: R-G, R9, and R6 are mandator
92589
92922
  - Lead-Core: tightly coupled cross-module/contract changes or sequential chains where splitting duplicates reads or causes hand_back loops. -> Implement directly in continuous steps (track in todo_write, no self-briefs) and run R6 verification.
92590
92923
  - Hybrid: unsettled shared contracts/seams plus orthogonal downstream work. -> Implement & freeze core seams yourself first, then delegate orthogonal adapters/tests/noisy exploration per R2/R5 (or subagent_fork for warm-context isolation).
92591
92924
  - Fan-out: >=3 orthogonal subtasks with disjoint files, settled interfaces, and clear parallel/routing gain. -> Coordinate (plan, R2/R5 dispatch, R6 review/acceptance, docs, non-delegable or disjoint parallel modules); never edit an active child's Boundary.
92925
+ - [Media generation] Generate/revise/retry images directly; delegate complex creative work, never a bare call.
92592
92926
 
92593
92927
  ## R2 · Model Routing (Check Availability First, Then Decide How to Dispatch)
92594
- Check whether list_subagent_models exists (in PTC mode: tools.list_subagent_models). The tool row and provider/model/reasoning_effort parameters are mounted statically, so they are present even in a session that authorized no route at all. Read that fact instead of inferring it from the tool's output:
92928
+ Check whether list_subagent_models exists. The tool row and provider/model/reasoning_effort parameters are mounted statically, so they are present even in a session that authorized no route at all. Read that fact instead of inferring it from the tool's output:
92595
92929
  - If the output says neither—no authorized-routes block and no "No allowlist is in force" sentence—the annotation is absent and you cannot read the posture. Treat it as [State: Unlisted] for behaviour, but say "unconfirmed" rather than claiming either posture, and never report a route as authorized.
92596
92930
  - [State: Listed] Output ends with "Authorized child routes for this Session": enforcement is active.
92597
92931
  - The sole source of truth for the allowlist is list_subagent_models: call list_subagent_models({}) to list authorized providers, and call list_subagent_models({ provider }) to list authorized models under that provider; trust its output directly without cross-checking local config files or workspace code (even if upstream tool text says "Catalog membership is advisory", the runtime output is already filtered to this session's allowlist).
@@ -92607,7 +92941,7 @@ A subagent has full authority to plan, execute, and self-verify within its assig
92607
92941
  Every task brief covers six elements: Goal / Boundary (which files or modules may be touched) / Constraints / Limits (see below) / Acceptance Criteria / Required Evidence to Report.
92608
92942
  - [Compact Delta Brief] Standing default limits and the report schema already live in [Role Identification], so use a Compact Delta Brief by default: state Goal, original file/line Source Pointers (do not paraphrase code into long prose), Module-Interface Boundary, Constraints, Acceptance Criteria, and Required Evidence, and omit repeating standing limits unless overriding them.
92609
92943
  - [Limits] When overriding any standing limit, write one line per limit spelling out four things: **what it bounds, in what unit, the cap, and whether it accumulates**. Write explicit caps ("at most 3 attempts", "edits at most 5 files", "test runs: unlimited"), never ranges ("2–5 files"); where an override dimension is left unstated, silence is read as a limit of zero, and a quota that resets per cycle is lost, not banked.
92610
- - Do not ask the child to restate your rules back as a correctness check; if a limit is genuinely unclear, instruct the child to spend one minimal tool call to verify it. Dispatch independent tasks concurrently in one message (via Promise.all in PTC mode) only after the interview has converged.
92944
+ - Do not ask the child to restate your rules back as a correctness check; if a limit is genuinely unclear, instruct the child to spend one minimal tool call to verify it. Dispatch independent tasks concurrently in one message (native calls; use code_run Promise.all for batch filter/MCP) only after the interview has converged.
92611
92945
 
92612
92946
  ## R6 · Review & Acceptance
92613
92947
  Two layers: subagent self-check -> your independent review (reading diffs, running tests, checking each criterion). The completion notification is a post-hoc signal: it tells you a child stopped, not that the work passed.
@@ -92833,9 +93167,8 @@ Fallback is a task-scoped, non-session-wide exception for delegated briefs: it c
92833
93167
  }
92834
93168
  },
92835
93169
  {
92836
- id: "tool-presentation",
92837
- name: "@deepseek-ai/dsh-agent-tool-presentation",
92838
- config: { mode: "ptc" }
93170
+ id: "code-mode",
93171
+ name: CODEMODE_PACKAGE
92839
93172
  },
92840
93173
  {
92841
93174
  id: "present",
@@ -92859,8 +93192,16 @@ Fallback is a task-scoped, non-session-wide exception for delegated briefs: it c
92859
93192
  * the export is absent; older harnesses keep it because their main entry does not
92860
93193
  * register the tool.
92861
93194
  *
92862
- * When the standalone PTC executor exists, it replaces the deprecated
92863
- * agent-tool-presentation compatibility row and its `{ mode: 'ptc' }` config.
93195
+ * Code Mode needs the native tool executor (it is the pipeline the program's
93196
+ * sub-calls are delegated to), so that package's presence decides the
93197
+ * presentation rows:
93198
+ *
93199
+ * - resolvable: the `code-mode` row mounts as-is and no PTC executor row exists
93200
+ * beside it (one executor per scope);
93201
+ * - absent: the row falls back to the deprecated agent-tool-presentation row and,
93202
+ * when the standalone PTC executor exists, to that executor instead — the
93203
+ * behaviour of every generation before Code Mode.
93204
+ *
92864
93205
  * @param resolves - whether a bare specifier resolves in this installation.
92865
93206
  * @returns the preset definition for the running harness generation.
92866
93207
  */
@@ -92868,7 +93209,8 @@ function resolveDispatchPreset(resolves = resolvesFromHere) {
92868
93209
  const hasWorkingDirectory = resolves(WORKING_DIRECTORY_PACKAGE);
92869
93210
  const hasLegacyListAgents = resolves("@deepseek-ai/dsh-tool-subagent-control/list-agents");
92870
93211
  const hasPtcExecutor = resolves(PTC_EXECUTOR_PACKAGE);
92871
- if (!hasWorkingDirectory && hasLegacyListAgents && !hasPtcExecutor) return DISPATCH_PRESET;
93212
+ const hasNativeExecutor = resolves(NATIVE_EXECUTOR_PACKAGE);
93213
+ if (!hasWorkingDirectory && hasLegacyListAgents && !hasPtcExecutor && hasNativeExecutor) return DISPATCH_PRESET;
92872
93214
  return {
92873
93215
  ...DISPATCH_PRESET,
92874
93216
  plugins: DISPATCH_PRESET.plugins.map((row) => {
@@ -92876,14 +93218,14 @@ function resolveDispatchPreset(resolves = resolvesFromHere) {
92876
93218
  ...row,
92877
93219
  config: row.config.filter((nested) => nested.id !== "tool-subagent-list-agents")
92878
93220
  };
92879
- if (row.id === "tool-presentation" && hasPtcExecutor) {
92880
- const { config: _omitted, ...rest } = row;
92881
- return {
92882
- ...rest,
92883
- id: "tool-executor-ptc",
92884
- name: PTC_EXECUTOR_PACKAGE
92885
- };
92886
- }
93221
+ if (row.id === "code-mode" && !hasNativeExecutor) return hasPtcExecutor ? {
93222
+ id: "tool-executor-ptc",
93223
+ name: PTC_EXECUTOR_PACKAGE
93224
+ } : {
93225
+ id: "tool-presentation",
93226
+ name: AGENT_TOOL_PRESENTATION_PACKAGE,
93227
+ config: { mode: "ptc" }
93228
+ };
92887
93229
  if (row.id !== "persona" || !hasWorkingDirectory || typeof row.config !== "object" || row.config === null) return row;
92888
93230
  const { suffix: _omitted, ...rest } = row.config;
92889
93231
  return {
@@ -94760,12 +95102,13 @@ const CACHE_WARMUP_PROVIDER = "kimi-code";
94760
95102
  /**
94761
95103
  * 允许进入结果投影的工具集合。
94762
95104
  *
94763
- * 除了 PTC 的 `run_code`,宿主的独立 Shell 与后台任务输出工具(`win_bash`、
94764
- * `bash`、`pwsh`、`job_output`)同样经常吐出整屏测试/构建日志。源码读取/编辑类
94765
- * 工具(`read`、`edit`、`grep`、`glob`、CodeGraph 等)不在此列,保持 100% 原样。
95105
+ * 除了 PTC 家族的程序传输(`run_code` 与 Code Mode 的 `code_run`),宿主的独立
95106
+ * Shell 与后台任务输出工具(`win_bash`、`bash`、`pwsh`、`job_output`)同样经常
95107
+ * 吐出整屏测试/构建日志。源码读取/编辑类工具(`read`、`edit`、`grep`、`glob`、
95108
+ * CodeGraph 等)不在此列,保持 100% 原样。
94766
95109
  */
94767
95110
  const CACHE_WARMUP_SOURCE_TOOLS = /* @__PURE__ */ new Set([
94768
- "run_code",
95111
+ ...CACHE_WARMUP_PTC_TRANSPORT_TOOLS,
94769
95112
  "win_bash",
94770
95113
  "bash",
94771
95114
  "pwsh",
@@ -94782,7 +95125,7 @@ const CACHE_WARMUP_SECTION_NAME = "plugin:cache-warmup";
94782
95125
  const CACHE_WARMUP_GUIDANCE = [
94783
95126
  "## Result previews",
94784
95127
  "",
94785
- "On large `run_code` results this line may replace the output with a preview: a short description plus one or more `ref://<session>/<sha256>` handles.",
95128
+ "On large `run_code`/`code_run` results this line may replace the output with a preview: a short description plus one or more `ref://<session>/<sha256>` handles.",
94786
95129
  "",
94787
95130
  "- A preview is **not** the full result. It states `complete`, `omitted` and `nextOffset` explicitly; treat an incomplete preview as incomplete data, not as the whole output.",
94788
95131
  "- To read the original, call `cache_warmup_result` with `action: \"page\"` and that `refId`, then follow `nextOffset` until `complete` is true.",
@@ -95001,7 +95344,8 @@ function installCacheWarmup(ctx, options) {
95001
95344
  *
95002
95345
  * 决策取舍写在代码里,因为它全是「什么时候**不动**」:
95003
95346
  * - 不是 `accept`(`block` 的 feedback 是别人的东西);
95004
- * - 工具名不是 `run_code`、线路不是本线、会话拿不到;
95347
+ * - 工具名不在源工具集合里(`run_code`/`code_run`/Shell/`job_output`)、线路不是
95348
+ * 本线、会话拿不到;
95005
95349
  * - 没有存储、内容为空。
95006
95350
  * 上面任何一条成立就原样返回同一个决策对象。
95007
95351
  *
@@ -95019,7 +95363,7 @@ function installCacheWarmup(ctx, options) {
95019
95363
  const retrievalTool = CACHE_WARMUP_RETRIEVAL_TOOLS.has(exec.name);
95020
95364
  if (!sourceTool && !retrievalTool) return decision;
95021
95365
  const settings = options.settings();
95022
- const ptcRetrieval = exec.name === "run_code" && settings.retrievalEnabled === true;
95366
+ const ptcRetrieval = isPtcTransportName(exec.name) && settings.retrievalEnabled === true;
95023
95367
  if (sourceTool && !settings.enabled && !ptcRetrieval) return decision;
95024
95368
  if (retrievalTool && settings.retrievalEnabled !== true) return decision;
95025
95369
  const navigation = retrievalTool || ptcRetrieval ? cacheWarmupNavigationIntentOf(exec.agent) : void 0;