dsh-image-gen 0.5.0 → 0.6.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 (42) hide show
  1. package/README.en.md +345 -344
  2. package/README.md +345 -344
  3. package/README.zh-CN.md +3 -3
  4. package/lib/client.js +115006 -228
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2270 -601
  7. package/lib/types/canvas-state-route.d.ts +37 -0
  8. package/lib/types/canvas-state.d.ts +77 -0
  9. package/lib/types/canvas-tools.d.ts +44 -0
  10. package/lib/types/client/browser-image-utils.d.ts +4 -0
  11. package/lib/types/client/canvas/canvas-bridge.d.ts +10 -0
  12. package/lib/types/client/canvas/canvas-model.d.ts +129 -0
  13. package/lib/types/client/canvas/canvas-nodes.d.ts +23 -0
  14. package/lib/types/client/canvas/canvas-store.d.ts +29 -0
  15. package/lib/types/client/canvas/canvas-style.d.ts +1 -0
  16. package/lib/types/client/canvas/canvas-view.d.ts +8 -0
  17. package/lib/types/client/canvas/rf-base-css.d.ts +2 -0
  18. package/lib/types/client/gallery-store.d.ts +2 -0
  19. package/lib/types/client/gallery-view.d.ts +13 -2
  20. package/lib/types/client/image-result-node.d.ts +2 -0
  21. package/lib/types/client/index.d.ts +4 -2
  22. package/lib/types/client/studio-chat-panel.d.ts +23 -0
  23. package/lib/types/client/studio-style.d.ts +1 -1
  24. package/lib/types/client/studio-view.d.ts +8 -6
  25. package/lib/types/client/tl/canvas-sync.d.ts +22 -0
  26. package/lib/types/client/tl/studio-tl-canvas.d.ts +2 -0
  27. package/lib/types/client/tl/tl-canvas-bridge.d.ts +53 -0
  28. package/lib/types/client/tl/tl-css.d.ts +2 -0
  29. package/lib/types/index.d.ts +1 -0
  30. package/lib/types/reference-image.d.ts +1 -0
  31. package/lib/types/shared.d.ts +86 -0
  32. package/lib/types/studio-chat.d.ts +48 -0
  33. package/package.json +22 -15
  34. package/skills/install-dsh-image-gen/SKILL.md +22 -22
  35. package/lib/types/client/inspiration-api.d.ts +0 -10
  36. package/lib/types/client/inspiration-detail.d.ts +0 -14
  37. package/lib/types/inspiration/cache.d.ts +0 -20
  38. package/lib/types/inspiration/downloader.d.ts +0 -27
  39. package/lib/types/inspiration/routes.d.ts +0 -13
  40. package/lib/types/inspiration/sources.d.ts +0 -33
  41. package/lib/types/inspiration/types.d.ts +0 -65
  42. package/lib/types/inspiration/updater.d.ts +0 -7
package/lib/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
3
  import z from "@deepseek-ai/schemastery";
4
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
4
  import { lstat, mkdir, readFile, readdir, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  //#region \0rolldown/runtime.js
8
8
  var __defProp = Object.defineProperty;
@@ -16,7 +16,175 @@ var __exportAll = (all, no_symbols) => {
16
16
  return target;
17
17
  };
18
18
  //#endregion
19
- //#region node_modules/.pnpm/@deepseek-ai+dsh-util-value_e176751d487506fbf1ea91463d745d9b/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
19
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-value_005f00f2ec004257b269f27fd5e936b4/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
20
+ /** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
21
+ /**
22
+ * Mark an unreachable closed-union branch.
23
+ * @param value - impossible value; an unhandled typed variant fails at the call site.
24
+ * @param context - optional switch-site label included in the failure message.
25
+ * @returns never; a runtime value that escaped its type always throws.
26
+ */
27
+ function assertNever(value, context) {
28
+ const rendered = JSON.stringify(value) ?? String(value);
29
+ throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
30
+ }
31
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
32
+ function hasIntrinsicConstructor$1(prototype, name) {
33
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
34
+ if (typeof constructor !== "function") return false;
35
+ try {
36
+ return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
42
+ function isIntrinsicObjectPrototype$1(value) {
43
+ return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor$1(value, "Object");
44
+ }
45
+ /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
46
+ function hasPlainArrayPrototype$1(value) {
47
+ const prototype = Object.getPrototypeOf(value);
48
+ if (!Array.isArray(prototype) || !hasIntrinsicConstructor$1(prototype, "Array")) return false;
49
+ const objectPrototype = Object.getPrototypeOf(prototype);
50
+ return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype$1(objectPrototype);
51
+ }
52
+ /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
53
+ function hasPlainObjectPrototype(value) {
54
+ const prototype = Object.getPrototypeOf(value);
55
+ return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype$1(prototype);
56
+ }
57
+ /** Return every JSON-visible object key, or reject own data JSON would discard. */
58
+ function enumerableStringKeys(value) {
59
+ const keys = Reflect.ownKeys(value);
60
+ if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
61
+ return keys;
62
+ }
63
+ /** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
64
+ function walkJsonValue(value, detach) {
65
+ const ancestors = /* @__PURE__ */ new Set();
66
+ let root;
67
+ const assign = (destination, item) => {
68
+ if (destination === void 0) return;
69
+ if (destination.kind === "root") root = item;
70
+ else if (destination.kind === "array") destination.target[destination.index] = item;
71
+ else Object.defineProperty(destination.target, destination.key, {
72
+ value: item,
73
+ enumerable: true,
74
+ configurable: true,
75
+ writable: true
76
+ });
77
+ };
78
+ const tasks = [{
79
+ kind: "visit",
80
+ value,
81
+ ...detach ? { destination: { kind: "root" } } : {}
82
+ }];
83
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
84
+ if (task.kind === "leave") {
85
+ ancestors.delete(task.source);
86
+ continue;
87
+ }
88
+ if (task.kind === "array-item") {
89
+ if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
90
+ tasks.push({
91
+ kind: "visit",
92
+ value: task.source[task.index],
93
+ ...task.target === void 0 ? {} : { destination: {
94
+ kind: "array",
95
+ target: task.target,
96
+ index: task.index
97
+ } }
98
+ });
99
+ continue;
100
+ }
101
+ if (task.kind === "object-property") {
102
+ tasks.push({
103
+ kind: "visit",
104
+ value: task.source[task.key],
105
+ ...task.target === void 0 ? {} : { destination: {
106
+ kind: "object",
107
+ target: task.target,
108
+ key: task.key
109
+ } }
110
+ });
111
+ continue;
112
+ }
113
+ const current = task.value;
114
+ if (current === null) {
115
+ assign(task.destination, null);
116
+ continue;
117
+ }
118
+ if (typeof current === "boolean" || typeof current === "string") {
119
+ assign(task.destination, current);
120
+ continue;
121
+ }
122
+ if (typeof current === "number") {
123
+ if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
124
+ assign(task.destination, current);
125
+ continue;
126
+ }
127
+ if (typeof current !== "object") return void 0;
128
+ if (ancestors.has(current)) return void 0;
129
+ if (Array.isArray(current)) {
130
+ if (!hasPlainArrayPrototype$1(current)) return void 0;
131
+ const length = current.length;
132
+ if (Reflect.ownKeys(current).length !== length + 1) return void 0;
133
+ const target = detach ? [] : void 0;
134
+ if (target !== void 0) assign(task.destination, target);
135
+ ancestors.add(current);
136
+ tasks.push({
137
+ kind: "leave",
138
+ source: current
139
+ });
140
+ for (let index = length - 1; index >= 0; index--) tasks.push({
141
+ kind: "array-item",
142
+ source: current,
143
+ index,
144
+ ...target === void 0 ? {} : { target }
145
+ });
146
+ continue;
147
+ }
148
+ if (!hasPlainObjectPrototype(current)) return void 0;
149
+ const keys = enumerableStringKeys(current);
150
+ if (keys === void 0) return void 0;
151
+ const target = detach ? {} : void 0;
152
+ if (target !== void 0) assign(task.destination, target);
153
+ ancestors.add(current);
154
+ tasks.push({
155
+ kind: "leave",
156
+ source: current
157
+ });
158
+ for (let index = keys.length - 1; index >= 0; index--) {
159
+ const key = keys[index];
160
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
161
+ if (key === void 0) return void 0;
162
+ tasks.push({
163
+ kind: "object-property",
164
+ source: current,
165
+ key,
166
+ ...target === void 0 ? {} : { target }
167
+ });
168
+ }
169
+ }
170
+ return detach ? root : true;
171
+ }
172
+ /**
173
+ * Validate and detach lossless JSON in one read per property.
174
+ * @param value - candidate value to validate and detach.
175
+ * @returns the detached snapshot, or `undefined` when the value is not losslessly JSON-serializable.
176
+ */
177
+ function snapshotJsonValue(value) {
178
+ return walkJsonValue(value, true);
179
+ }
180
+ /**
181
+ * Test the same lossless JSON rules as {@link snapshotJsonValue} without detaching the value.
182
+ * @param value - candidate value to test.
183
+ * @returns whether the value survives a JSON round trip without loss.
184
+ */
185
+ function isJsonValue(value) {
186
+ return walkJsonValue(value, false) === true;
187
+ }
20
188
  /**
21
189
  * Compare JSON-compatible values structurally.
22
190
  * @param a - one JSON-compatible value.
@@ -41,7 +209,7 @@ function deepEqualJson(a, b) {
41
209
  * @param value - value to freeze.
42
210
  * @returns the same value after every reachable enumerable child is frozen.
43
211
  */
44
- function deepFreeze$1(value) {
212
+ function deepFreeze(value) {
45
213
  const seen = /* @__PURE__ */ new WeakSet();
46
214
  const pending = [{
47
215
  kind: "visit",
@@ -79,7 +247,7 @@ function deepFreeze$1(value) {
79
247
  return value;
80
248
  }
81
249
  //#endregion
82
- //#region node_modules/.pnpm/@deepseek-ai+dsh-settings@0_ee3e2f9fab981d3a5a4a6c717af48d35/node_modules/@deepseek-ai/dsh-settings/lib/index.js
250
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-settings@0_935f3a2ac52a1f8e83ba61ff4169507a/node_modules/@deepseek-ai/dsh-settings/lib/index.js
83
251
  var lib_exports = /* @__PURE__ */ __exportAll({
84
252
  SettingsConflictError: () => SettingsConflictError,
85
253
  SettingsProvider: () => SettingsProvider,
@@ -370,7 +538,7 @@ var SettingsProvider = class extends Service {
370
538
  base: options?.base,
371
539
  applies: options?.applies ?? "live",
372
540
  ...options?.validate === void 0 ? {} : { validate: options.validate },
373
- resolved: deepFreeze$1(this.resolve(schema, options?.base, this.section(parsedNs), options?.validate)),
541
+ resolved: deepFreeze(this.resolve(schema, options?.base, this.section(parsedNs), options?.validate)),
374
542
  revision: 0,
375
543
  watchers: /* @__PURE__ */ new Set()
376
544
  };
@@ -542,7 +710,7 @@ var SettingsProvider = class extends Service {
542
710
  const current = this.section(ns) ?? {};
543
711
  if (expectedRevision !== void 0 && expectedRevision !== registration.revision) throw new SettingsConflictError(ns, expectedRevision, registration.revision);
544
712
  const section = mode === "merge" ? mergeLayers(current, snapshot) : mode === "replace" ? snapshot : snapshot["ops"].reduce(applyPathOp, current);
545
- const next = deepFreeze$1(this.resolve(registration.schema, registration.base, section, registration.validate));
713
+ const next = deepFreeze(this.resolve(registration.schema, registration.base, section, registration.validate));
546
714
  await this.persist(ns, section);
547
715
  this.document[ns] = section;
548
716
  if (this.registrations.get(ns) === registration && !this.isStopped()) {
@@ -571,7 +739,7 @@ var SettingsProvider = class extends Service {
571
739
  for (const registration of this.registrations.values()) {
572
740
  let next;
573
741
  try {
574
- next = deepFreeze$1(this.resolve(registration.schema, registration.base, this.section(registration.ns), registration.validate));
742
+ next = deepFreeze(this.resolve(registration.schema, registration.base, this.section(registration.ns), registration.validate));
575
743
  } catch (error) {
576
744
  this.ctx.logger.warn("settings: keeping last good \"%s\" after invalid stored section", registration.ns);
577
745
  this.ctx.logger.warn(error);
@@ -969,83 +1137,192 @@ function scopeTarget(base, key) {
969
1137
  return carrier;
970
1138
  }
971
1139
  //#endregion
972
- //#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0._13401da53fdd229a45877adbff64345d/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
973
- /** Largest delay Node schedules without clamping it to one millisecond. */
974
- const MAX_TIMER_DELAY_MS = 2147483647;
975
- //#endregion
976
- //#region node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.1-_de8559ed89b7370843bac1bad71a6196/node_modules/@deepseek-ai/dsh-llm/lib/index.js
1140
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-typert-pro_32eafa0ce53608e9afb5dafcec019560/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
1141
+ /** The one Remote failure class shared by owners, the Gateway, and consumers. */
977
1142
  /**
978
- * dsh-llm's owned branded ids: tool-call correlation and provider request
979
- * diagnostics.
980
- *
981
- * The `Branded<B>` primitive itself lives in `@deepseek-ai/dsh-brand` (a
982
- * zero-dependency type-only package) so every owner of a cross-boundary id can
983
- * brand it without depending on dsh-llm; see that package's README for the
984
- * nominal-typing policy.
985
- *
986
- * @module @deepseek-ai/dsh-llm/brand
1143
+ * One Remote call failure: a real Error carrying its stable code and typed
1144
+ * details. Owners throw it at the failure point; the Host Gateway encodes it
1145
+ * onto the wire unchanged; the Client face rebuilds an instance for the
1146
+ * `RemoteResult` error branch, so `throw result.error` keeps throw semantics.
1147
+ * Discrimination is always by `code`, never by instanceof.
987
1148
  */
1149
+ var RemoteError = class extends Error {
1150
+ code;
1151
+ details;
1152
+ /** Structural marker: cross-realm/bundle identification never uses instanceof. */
1153
+ isDSHRemoteError = true;
1154
+ /**
1155
+ * @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
1156
+ * @param message - human diagnostic carried across the wire.
1157
+ * @param details - structured payload typed by the code.
1158
+ * @param options - standard Error options (`cause` survives in-process only).
1159
+ */
1160
+ constructor(code, message, details, options) {
1161
+ super(message, options);
1162
+ this.code = code;
1163
+ this.details = details;
1164
+ this.name = "RemoteError";
1165
+ }
1166
+ };
988
1167
  /**
989
- * Brand a message identifier.
990
- * @param id - the opaque message identifier.
991
- * @returns the same string, branded; no validation is performed.
1168
+ * Remote decorators and explicit Gateway bindings backed by versioned
1169
+ * descriptors carried on decorated class prototypes. Strict reflection
1170
+ * remains a Typert compiler responsibility.
1171
+ * @module @deepseek-ai/dsh-typert-protocol
992
1172
  */
993
- function MessageId(id) {
994
- return id;
995
- }
1173
+ const TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
996
1174
  /**
997
- * Brand a string as a {@link CallId}.
998
- * @param id - the provider-issued (or synthesized) call id.
999
- * @returns the same string, branded; no validation is performed.
1175
+ * Test one generated Remote name against the Connection endpoint grammar.
1176
+ * @param value - namespace, method, lookup, or Context segment.
1177
+ * @returns whether the value can cross the shared RPC carrier unchanged.
1000
1178
  */
1001
- function CallId(id) {
1002
- return id;
1179
+ function isTypertRemoteSegment(value) {
1180
+ return value !== "." && value !== ".." && TYPERT_REMOTE_SEGMENT_PATTERN.test(value);
1003
1181
  }
1182
+ const REMOTE_METHOD_DESCRIPTOR = "@deepseek-ai/dsh-typert-protocol/remote-methods";
1004
1183
  /**
1005
- * Deep-freeze a value in place with an iterative traversal, guarding cycles,
1006
- * so later mutation throws without imposing a JavaScript call-stack depth cap.
1007
- * {@link AbortSignal} objects are deliberately skipped because they are the
1008
- * request's live cancellation channel and freezing them breaks abort.
1009
- * @param value - the value to freeze in place.
1010
- * @returns the same value, frozen.
1184
+ * Bind one visible Service field to a Cordis key and Remote namespace.
1185
+ * @param service - owning Service instance, normally `this`.
1186
+ * @param serviceKey - exact Cordis service key.
1187
+ * @param options - optional distinct wire namespace.
1188
+ * @returns a frozen, inspectable binding with no compiler-injected metadata.
1011
1189
  */
1012
- function deepFreeze(value) {
1013
- const seen = /* @__PURE__ */ new WeakSet();
1014
- const pending = [{
1015
- kind: "visit",
1016
- node: value
1017
- }];
1018
- while (pending.length > 0) {
1019
- const task = pending.pop();
1020
- /* v8 ignore next -- the loop condition guarantees one pending task. */
1021
- if (task === void 0) continue;
1022
- if (task.kind === "property") {
1023
- pending.push({
1024
- kind: "visit",
1025
- node: task.source[task.key]
1026
- });
1027
- continue;
1028
- }
1029
- const node = task.node;
1030
- if (node === null || typeof node !== "object") continue;
1031
- if (node instanceof AbortSignal) continue;
1032
- if (seen.has(node)) continue;
1033
- seen.add(node);
1034
- Object.freeze(node);
1035
- const keys = Object.keys(node);
1036
- for (let index = keys.length - 1; index >= 0; index--) {
1037
- const key = keys[index];
1038
- /* v8 ignore next -- the loop is bounded by the captured key count. */
1039
- if (key === void 0) continue;
1040
- pending.push({
1041
- kind: "property",
1042
- source: node,
1043
- key
1044
- });
1045
- }
1190
+ function bindTypertRemote(service, serviceKey, options = {}) {
1191
+ validateName("service key", serviceKey);
1192
+ const namespace = options.namespace ?? serviceKey;
1193
+ validateName("namespace", namespace);
1194
+ return Object.freeze({
1195
+ service,
1196
+ serviceKey,
1197
+ namespace
1198
+ });
1199
+ }
1200
+ /** Cordis Service base that exposes its registered name through Typert Gateway. */
1201
+ var TypertRemoteService = class extends Service {
1202
+ /** Visible binding consumed by the Gateway's source-mode discovery. */
1203
+ typertRemote;
1204
+ /**
1205
+ * Register the Service and bind the same key to Typert Gateway.
1206
+ * @param ctx - owning Cordis Context.
1207
+ * @param serviceKey - exact Cordis service key and default wire namespace.
1208
+ * @param options - optional distinct wire namespace.
1209
+ */
1210
+ constructor(ctx, serviceKey, options = {}) {
1211
+ super(ctx, serviceKey);
1212
+ this.typertRemote = bindTypertRemote(this, this.name, options);
1213
+ }
1214
+ };
1215
+ function Remote(methodExportOrOptions, context) {
1216
+ if (typeof methodExportOrOptions === "string") {
1217
+ validateName("Remote export name", methodExportOrOptions);
1218
+ return remoteDecorator({ kind: "direct" }, void 0, methodExportOrOptions);
1219
+ }
1220
+ if (typeof methodExportOrOptions === "object") {
1221
+ if (remoteOptionMode(methodExportOrOptions) !== "stream" || Reflect.ownKeys(methodExportOrOptions).length !== 1) throw new TypeError("typert-protocol: Remote options must contain exactly mode: \"stream\"");
1222
+ return remoteDecorator({ kind: "direct" }, "stream");
1223
+ }
1224
+ if (context === void 0) throw new TypeError("typert-protocol: Remote decorator context is missing");
1225
+ addMarkerInitializer(context, { kind: "direct" });
1226
+ }
1227
+ function remoteOptionMode(options) {
1228
+ return Reflect.get(options, "mode");
1229
+ }
1230
+ function remoteDecorator(invocation, mode, exportName) {
1231
+ return function(_method, context) {
1232
+ addMarkerInitializer(context, invocation, mode, exportName);
1233
+ };
1234
+ }
1235
+ function readRemoteMethodDescriptor(prototype) {
1236
+ const property = Object.getOwnPropertyDescriptor(prototype, REMOTE_METHOD_DESCRIPTOR);
1237
+ if (property === void 0) return void 0;
1238
+ const descriptor = property.value;
1239
+ if (descriptor === null || typeof descriptor !== "object") throw new TypeError("typert-protocol: Remote method descriptor must be an object");
1240
+ const version = Reflect.get(descriptor, "version");
1241
+ if (version !== 1) throw new TypeError(`typert-protocol: unsupported Remote method descriptor version ${String(version)}`);
1242
+ const methods = Reflect.get(descriptor, "methods");
1243
+ if (!Array.isArray(methods)) throw new TypeError("typert-protocol: Remote method descriptor methods must be an array");
1244
+ return descriptor;
1245
+ }
1246
+ function addMarkerInitializer(context, invocation, mode, exportName) {
1247
+ if (context.private || context.static || typeof context.name !== "string") throw new TypeError("typert-protocol: Remote decorators require a public instance method with a string name");
1248
+ const method = context.name;
1249
+ context.addInitializer(function() {
1250
+ const prototype = Object.getPrototypeOf(this);
1251
+ if (prototype === null) throw new TypeError(`typert-protocol: cannot mark Remote method "${method}" on an object without a prototype`);
1252
+ mark(prototype, method, invocation, mode, exportName);
1253
+ });
1254
+ }
1255
+ function mark(prototype, method, invocation, mode, exportName) {
1256
+ const descriptor = readRemoteMethodDescriptor(prototype);
1257
+ const marker = Object.freeze({
1258
+ method,
1259
+ ...exportName === void 0 || exportName === method ? {} : { exportName },
1260
+ ...mode === void 0 ? {} : { mode },
1261
+ invocation: Object.freeze(invocation)
1262
+ });
1263
+ const current = descriptor?.methods.find((candidate) => candidate.method === method);
1264
+ if (current !== void 0) {
1265
+ if (current.exportName === marker.exportName && current.mode === marker.mode && sameInvocation(current.invocation, invocation)) return;
1266
+ throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`);
1046
1267
  }
1268
+ Object.defineProperty(prototype, REMOTE_METHOD_DESCRIPTOR, {
1269
+ configurable: true,
1270
+ value: Object.freeze({
1271
+ version: 1,
1272
+ methods: Object.freeze([...descriptor?.methods ?? [], marker])
1273
+ })
1274
+ });
1275
+ }
1276
+ function sameInvocation(left, right) {
1277
+ if (left.kind === "direct") return right.kind === "direct";
1278
+ if (right.kind === "direct") return false;
1279
+ return left.context === right.context;
1280
+ }
1281
+ function validateName(subject, value) {
1282
+ if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
1283
+ }
1284
+ //#endregion
1285
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-crypt_66ec5f64228d8b6b846c0e0318a734f3/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
1286
+ /**
1287
+ * Random v4 UUID, minted from `crypto.getRandomValues`.
1288
+ * @returns the UUID string.
1289
+ */
1290
+ function randomUUID$1() {
1291
+ const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
1292
+ const hex = Array.from(bytes, (byte, index) => {
1293
+ return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
1294
+ }).join("");
1295
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1296
+ }
1297
+ //#endregion
1298
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.5-rc.1_@deepseek-ai+cordis@4.0.1/node_modules/@deepseek-ai/dsh-brand/lib/index.js
1299
+ /**
1300
+ * Duplicate-install-safe nominal primitive helpers.
1301
+ *
1302
+ * A brand makes structurally identical strings or numbers non-interchangeable
1303
+ * at the type level: a `SessionId` cannot be passed where a `ToolCallId` is
1304
+ * expected, and an event sequence cannot be passed as a log offset. Comparison,
1305
+ * logging, and serialization retain the underlying primitive behavior.
1306
+ *
1307
+ * This package owns no concrete domain value and keeps no runtime identity or mutable
1308
+ * state, so independently installed copies produce interchangeable values.
1309
+ *
1310
+ * @module @deepseek-ai/dsh-brand
1311
+ */
1312
+ /**
1313
+ * Apply a compile-time string brand without changing the value.
1314
+ * @param value - string admitted by the domain that owns the target brand.
1315
+ * @returns the same string with the requested compile-time brand.
1316
+ */
1317
+ function brandString(value) {
1047
1318
  return value;
1048
1319
  }
1320
+ //#endregion
1321
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0._5b95bb48758e6032066aa142c5fbb450/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
1322
+ /** Largest delay Node schedules without clamping it to one millisecond. */
1323
+ const MAX_TIMER_DELAY_MS = 2147483647;
1324
+ //#endregion
1325
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.5-rc.1_@deepseek-ai+cordis@4.0.1/node_modules/@deepseek-ai/dsh-llm/lib/index.js
1049
1326
  /**
1050
1327
  * Detach and deep-freeze a message whose identity already exists.
1051
1328
  * @param message - complete message, including its stable identity.
@@ -1062,7 +1339,7 @@ function freezeMessage(message) {
1062
1339
  function createMessage(input) {
1063
1340
  return freezeMessage({
1064
1341
  ...input,
1065
- id: MessageId(crypto.randomUUID())
1342
+ id: brandString(randomUUID$1())
1066
1343
  });
1067
1344
  }
1068
1345
  /**
@@ -1144,255 +1421,1076 @@ const alwaysPolicySchema = z.object({
1144
1421
  backoff: backoffSchema
1145
1422
  });
1146
1423
  z.union([normalPolicySchema, alwaysPolicySchema]);
1424
+ const NORMAL_POLICY_KEYS = /* @__PURE__ */ new Set([
1425
+ "mode",
1426
+ "maxRetries",
1427
+ "retryableCodes",
1428
+ "backoff"
1429
+ ]);
1430
+ const ALWAYS_POLICY_KEYS = /* @__PURE__ */ new Set([
1431
+ "mode",
1432
+ "maxRetries",
1433
+ "retryableCodes",
1434
+ "backoff"
1435
+ ]);
1436
+ const BACKOFF_KEYS = /* @__PURE__ */ new Set([
1437
+ "initialDelayMs",
1438
+ "maxDelayMs",
1439
+ "jitterRatio"
1440
+ ]);
1441
+ function validateKeys(value, allowed, path) {
1442
+ for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${path}: unknown key "${key}"`);
1443
+ }
1444
+ function resolveBackoff(config, path) {
1445
+ if (config !== void 0) validateKeys(config, BACKOFF_KEYS, path);
1446
+ const initialDelayMs = config?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
1447
+ const maxDelayMs = config?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
1448
+ const jitterRatio = config?.jitterRatio ?? DEFAULT_JITTER_RATIO;
1449
+ if (!Number.isFinite(initialDelayMs) || initialDelayMs <= 0 || initialDelayMs > 2147483647) throw new Error(`${path}.initialDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
1450
+ if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0 || maxDelayMs > 2147483647) throw new Error(`${path}.maxDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
1451
+ if (initialDelayMs > maxDelayMs) throw new Error(`${path}.initialDelayMs must be less than or equal to maxDelayMs`);
1452
+ if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) throw new Error(`${path}.jitterRatio must be between 0 and 1`);
1453
+ return Object.freeze({
1454
+ initialDelayMs,
1455
+ maxDelayMs,
1456
+ jitterRatio
1457
+ });
1458
+ }
1147
1459
  /**
1148
- * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
1149
- * adapters from drifting. See
1150
- * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
1151
- *
1152
- * App-attribution vocabulary for provider requests.
1153
- * @module @deepseek-ai/dsh-llm/attribution
1460
+ * Validate, default, and detach one provider-owned retry policy.
1461
+ * @param config - optional provider configuration; omission selects normal defaults.
1462
+ * @param path - diagnostic path naming the provider config that owns the value.
1463
+ * @returns an immutable policy safe to capture in provider registration state.
1154
1464
  */
1155
- const { version } = createRequire(import.meta.url)("../package.json");
1156
- /**
1157
- * Exhaustiveness helper for closed core unions. Use {@link assertNever} at the default branch so a
1158
- * new variant fails compilation at every required handler. Do not use it for declaration-merged
1159
- * unions such as session events or content blocks: handle known variants and explicitly fall
1160
- * through because plugins may add valid unknown cases.
1161
- * @module @deepseek-ai/dsh-llm/never
1465
+ function resolveRetryPolicy(config, path) {
1466
+ if (config === void 0) return Object.freeze({
1467
+ mode: "normal",
1468
+ maxRetries: DEFAULT_MAX_RETRIES,
1469
+ retryableCodes: DEFAULT_RETRYABLE_CODES,
1470
+ ...resolveBackoff(void 0, `${path}.backoff`)
1471
+ });
1472
+ switch (config.mode) {
1473
+ case "normal": {
1474
+ validateKeys(config, NORMAL_POLICY_KEYS, path);
1475
+ const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
1476
+ const retryableCodes = config.retryableCodes ?? [...DEFAULT_RETRYABLE_CODES];
1477
+ if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new Error(`${path}.maxRetries must be a non-negative safe integer`);
1478
+ if (retryableCodes.length === 0) throw new Error(`${path}.retryableCodes must not be empty`);
1479
+ if (retryableCodes.some((code) => typeof code !== "string" || code.length === 0)) throw new Error(`${path}.retryableCodes must contain only non-empty strings`);
1480
+ if (new Set(retryableCodes).size !== retryableCodes.length) throw new Error(`${path}.retryableCodes must not contain duplicates`);
1481
+ return Object.freeze({
1482
+ mode: "normal",
1483
+ maxRetries,
1484
+ retryableCodes: Object.freeze([...retryableCodes]),
1485
+ ...resolveBackoff(config.backoff, `${path}.backoff`)
1486
+ });
1487
+ }
1488
+ case "always":
1489
+ validateKeys(config, ALWAYS_POLICY_KEYS, path);
1490
+ return Object.freeze({
1491
+ mode: "always",
1492
+ ...resolveBackoff(config.backoff, `${path}.backoff`)
1493
+ });
1494
+ default: throw new Error(`${path}.mode must be "normal" or "always"`);
1495
+ }
1496
+ }
1497
+ /**
1498
+ * Field-wise equality over {@link LlmCallConfig} — the comparison a caller
1499
+ * runs to decide whether a proposed configuration is a real change (worth a
1500
+ * logged header snapshot) or the held one restated.
1501
+ * @param a - one configuration.
1502
+ * @param b - the other.
1503
+ * @returns whether every field (including the `stop` list, element-wise) matches.
1504
+ */
1505
+ function callConfigEquals(a, b) {
1506
+ if (a.provider !== b.provider || a.model !== b.model || a.reasoningEffort !== b.reasoningEffort || a.temperature !== b.temperature || a.maxTokens !== b.maxTokens) return false;
1507
+ if (a.stop === void 0 || b.stop === void 0) return a.stop === b.stop;
1508
+ return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i]);
1509
+ }
1510
+ /**
1511
+ * Normalization for values thrown by a final LLM adapter boundary.
1512
+ *
1513
+ * @module @deepseek-ai/dsh-llm/adapter-failure
1162
1514
  */
1163
1515
  /**
1164
- * Mark an unreachable closed-union branch. A newly unhandled typed variant fails at the call site;
1165
- * a value that escaped its type throws with diagnostics at runtime.
1166
- * @param value - the impossible value; typed `never` so an unhandled variant fails compilation at the call site.
1167
- * @param context - optional label (e.g. the switch site) prefixed into the throw message.
1168
- * @returns never — it always throws, with the offending value JSON-rendered in the message.
1516
+ * Detach serializable provider facts from a value thrown by an adapter.
1517
+ * @param value - arbitrary value thrown during adapter dispatch or iteration.
1518
+ * @returns immutable provider-neutral facts suitable for a terminal finish chunk.
1519
+ * @internal
1169
1520
  */
1170
- function assertNever(value, context) {
1171
- const rendered = JSON.stringify(value) ?? String(value);
1172
- throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
1521
+ function normalizeLlmFailure(value) {
1522
+ const error = value instanceof Error ? value : new HarnessError(thrownMessage(value), "UNKNOWN", { cause: value });
1523
+ const carried = ownFailureSnapshot(error);
1524
+ if (carried !== void 0 && carried.code === ownErrorCode(error)) return carried;
1525
+ return Object.freeze({
1526
+ message: errorMessage$2(error),
1527
+ code: harnessErrorCode(error)
1528
+ });
1173
1529
  }
1174
- //#endregion
1175
- //#region node_modules/.pnpm/@deepseek-ai+dsh-session@0._f3475d2e0abd209e639ed06267209bdd/node_modules/@deepseek-ai/dsh-session/lib/index.js
1176
- /** Lossless-JSON validation and detached snapshots for durable session data. @module @deepseek-ai/dsh-session/json */
1177
- /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
1178
- function hasIntrinsicConstructor$1(prototype, name) {
1179
- const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
1180
- if (typeof constructor !== "function") return false;
1530
+ /** Render a non-Error throw without letting hostile coercion escape normalization. */
1531
+ function thrownMessage(value) {
1181
1532
  try {
1182
- return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
1183
- } catch {
1184
- return false;
1533
+ const message = String(value);
1534
+ return message.length > 0 ? message : "LLM adapter failed";
1535
+ } catch (_hostileThrownValue) {
1536
+ return "LLM adapter failed";
1185
1537
  }
1186
1538
  }
1187
- /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
1188
- function isIntrinsicObjectPrototype$1(value) {
1189
- return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor$1(value, "Object");
1539
+ /** Read a foreign error's own data-backed `code` without invoking accessors. */
1540
+ function ownErrorCode(error) {
1541
+ try {
1542
+ const descriptor = Object.getOwnPropertyDescriptor(error, "code");
1543
+ return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
1544
+ } catch (_sdkPropertyTrap) {
1545
+ return;
1546
+ }
1190
1547
  }
1191
- /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
1192
- function hasPlainArrayPrototype$1(value) {
1193
- const prototype = Object.getPrototypeOf(value);
1194
- if (!Array.isArray(prototype) || !hasIntrinsicConstructor$1(prototype, "Array")) return false;
1195
- const objectPrototype = Object.getPrototypeOf(prototype);
1196
- return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype$1(objectPrototype);
1548
+ /** Snapshot an own data property without invoking an SDK-defined accessor. */
1549
+ function ownFailureSnapshot(error) {
1550
+ try {
1551
+ const descriptor = Object.getOwnPropertyDescriptor(error, "failure");
1552
+ return descriptor !== void 0 && "value" in descriptor ? failureSnapshot(descriptor.value) : void 0;
1553
+ } catch (_sdkPropertyTrap) {
1554
+ return;
1555
+ }
1197
1556
  }
1198
- /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
1199
- function hasPlainObjectPrototype(value) {
1200
- const prototype = Object.getPrototypeOf(value);
1201
- return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype$1(prototype);
1557
+ /** Validate and detach an arbitrary serializable failure payload. */
1558
+ function failureSnapshot(value) {
1559
+ if (typeof value !== "object" || value === null) return void 0;
1560
+ try {
1561
+ const candidate = value;
1562
+ const message = candidate.message;
1563
+ const code = candidate.code;
1564
+ const status = candidate.status;
1565
+ const providerRetryAfterMs = candidate.providerRetryAfterMs;
1566
+ const requestId = candidate.requestId;
1567
+ if (typeof message !== "string" || message.length === 0 || typeof code !== "string" || code.length === 0 || status !== void 0 && (!Number.isInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && (typeof requestId !== "string" || requestId.length === 0)) return void 0;
1568
+ return Object.freeze({
1569
+ message,
1570
+ code,
1571
+ ...status === void 0 ? {} : { status },
1572
+ ...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
1573
+ ...requestId === void 0 ? {} : { requestId }
1574
+ });
1575
+ } catch (_sdkFailureGetter) {
1576
+ return;
1577
+ }
1202
1578
  }
1203
- /** Return every JSON-visible object key, or reject own data JSON would discard. */
1204
- function enumerableStringKeys(value) {
1205
- const keys = Reflect.ownKeys(value);
1206
- if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
1207
- return keys;
1579
+ /** Read an SDK error message without letting an accessor replace the primary failure. */
1580
+ function errorMessage$2(error) {
1581
+ try {
1582
+ const message = error.message;
1583
+ if (typeof message === "string" && message.length > 0) return message;
1584
+ } catch (_sdkMessageGetter) {}
1585
+ return "LLM adapter failed";
1208
1586
  }
1209
- /** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
1210
- function walkJsonValue(value, detach) {
1211
- const ancestors = /* @__PURE__ */ new Set();
1212
- let root;
1213
- const assign = (destination, item) => {
1214
- if (destination === void 0) return;
1215
- if (destination.kind === "root") root = item;
1216
- else if (destination.kind === "array") destination.target[destination.index] = item;
1217
- else Object.defineProperty(destination.target, destination.key, {
1218
- value: item,
1219
- enumerable: true,
1220
- configurable: true,
1221
- writable: true
1222
- });
1223
- };
1224
- const tasks = [{
1225
- kind: "visit",
1226
- value,
1227
- ...detach ? { destination: { kind: "root" } } : {}
1228
- }];
1229
- for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
1230
- if (task.kind === "leave") {
1231
- ancestors.delete(task.source);
1232
- continue;
1233
- }
1234
- if (task.kind === "array-item") {
1235
- if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
1236
- tasks.push({
1237
- kind: "visit",
1238
- value: task.source[task.index],
1239
- ...task.target === void 0 ? {} : { destination: {
1240
- kind: "array",
1241
- target: task.target,
1242
- index: task.index
1243
- } }
1244
- });
1245
- continue;
1246
- }
1247
- if (task.kind === "object-property") {
1248
- tasks.push({
1249
- kind: "visit",
1250
- value: task.source[task.key],
1251
- ...task.target === void 0 ? {} : { destination: {
1252
- kind: "object",
1253
- target: task.target,
1254
- key: task.key
1255
- } }
1587
+ /** Trust only Harness-owned codes; third-party SDK codes are not our taxonomy. */
1588
+ function harnessErrorCode(error) {
1589
+ return error instanceof HarnessError ? error.code : "UNKNOWN";
1590
+ }
1591
+ function quoted(value) {
1592
+ return JSON.stringify(value);
1593
+ }
1594
+ /**
1595
+ * Stable text shown to a model that cannot accept one durable image reference.
1596
+ * @param ref - durable normalized attachment omitted from the request.
1597
+ * @returns deterministic text-only placeholder.
1598
+ */
1599
+ function textOnlyImageText(ref) {
1600
+ return `[image omitted because this model accepts text only; attachment sha256:${String(ref.attachmentId).slice(7, 15)}]`;
1601
+ }
1602
+ /**
1603
+ * True when typed model content contains an image block, walking nested
1604
+ * tool-result content. This is the one recursive image walk shared by every
1605
+ * image policy (capability gating, text-only serialization, compaction
1606
+ * survey), so a consumer cannot silently diverge on nesting depth.
1607
+ * @param content - typed model content blocks.
1608
+ * @returns whether any nested block is an image.
1609
+ */
1610
+ function contentHasImage(content) {
1611
+ return content.some((block) => block.type === "image" || block.type === "tool-result" && contentHasImage(block.content));
1612
+ }
1613
+ /**
1614
+ * True when typed model content contains a file block, walking nested
1615
+ * tool-result content on the same recursion every file policy shares.
1616
+ * Reads current content on every call without retaining scan results.
1617
+ * @param content - typed model content blocks.
1618
+ * @returns whether any nested block is a file.
1619
+ */
1620
+ function contentHasFile(content) {
1621
+ for (const block of content) if (block.type === "file" || block.type === "tool-result" && contentHasFile(block.content)) return true;
1622
+ return false;
1623
+ }
1624
+ /**
1625
+ * Stable model-facing handle for one durable file reference: the address of
1626
+ * the verbatim stored copy and the instruction to read it on demand. This is
1627
+ * the only representation a provider ever receives for a file.
1628
+ * @param ref - durable verbatim file reference.
1629
+ * @param readonlyPath - execution-world path of the stored copy, when resolvable.
1630
+ * @returns deterministic handle text naming the file, its size, and its address.
1631
+ */
1632
+ function fileHandleText(ref, readonlyPath) {
1633
+ const digest = String(ref.attachmentId).slice(7, 15);
1634
+ const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
1635
+ if (readonlyPath === void 0) return `[${identity} was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
1636
+ return `[${identity}: verbatim read-only copy saved at ${quoted(readonlyPath)}. Read that path with your file tools when its contents are needed; copy it to a writable location before modifying it. When delegating file work, include this saved path in the delegation prompt; only subagents sharing this execution environment can read it.]`;
1637
+ }
1638
+ /** Replace every file occurrence, including nested tool results, with handle text. */
1639
+ function replaceFilesWithHandles(blocks, resolvePath) {
1640
+ let next;
1641
+ for (const [index, block] of blocks.entries()) {
1642
+ if (block.type === "file") {
1643
+ next ??= blocks.slice(0, index);
1644
+ next.push({
1645
+ type: "text",
1646
+ text: fileHandleText(block.attachment, resolvePath(block.attachment))
1256
1647
  });
1257
1648
  continue;
1258
1649
  }
1259
- const current = task.value;
1260
- if (current === null) {
1261
- assign(task.destination, null);
1262
- continue;
1263
- }
1264
- if (typeof current === "boolean" || typeof current === "string") {
1265
- assign(task.destination, current);
1266
- continue;
1267
- }
1268
- if (typeof current === "number") {
1269
- if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
1270
- assign(task.destination, current);
1271
- continue;
1650
+ if (block.type === "tool-result") {
1651
+ const content = replaceFilesWithHandles(block.content, resolvePath);
1652
+ if (content !== block.content) {
1653
+ next ??= blocks.slice(0, index);
1654
+ next.push({
1655
+ ...block,
1656
+ content
1657
+ });
1658
+ continue;
1659
+ }
1272
1660
  }
1273
- if (typeof current !== "object") return void 0;
1274
- if (ancestors.has(current)) return void 0;
1275
- if (Array.isArray(current)) {
1276
- if (!hasPlainArrayPrototype$1(current)) return void 0;
1277
- const length = current.length;
1278
- if (Reflect.ownKeys(current).length !== length + 1) return void 0;
1279
- const target = detach ? [] : void 0;
1280
- if (target !== void 0) assign(task.destination, target);
1281
- ancestors.add(current);
1282
- tasks.push({
1283
- kind: "leave",
1284
- source: current
1285
- });
1286
- for (let index = length - 1; index >= 0; index--) tasks.push({
1287
- kind: "array-item",
1288
- source: current,
1289
- index,
1290
- ...target === void 0 ? {} : { target }
1661
+ next?.push(block);
1662
+ }
1663
+ return next ?? blocks;
1664
+ }
1665
+ /**
1666
+ * Project durable file history into deterministic handle text for every model
1667
+ * route. Unlike images, no provider receives file blocks natively, so this
1668
+ * projection is unconditional in request assembly.
1669
+ * @param messages - complete request history.
1670
+ * @param resolvePath - resolve one reference's current execution-world read path.
1671
+ * @returns the original list without files, otherwise shallow message copies with handle text.
1672
+ */
1673
+ function projectFilesToText(messages, resolvePath) {
1674
+ if (!messages.some((message) => contentHasFile(message.content))) return messages;
1675
+ return messages.map((message) => {
1676
+ const content = replaceFilesWithHandles(message.content, resolvePath);
1677
+ return content === message.content ? message : {
1678
+ ...message,
1679
+ content
1680
+ };
1681
+ });
1682
+ }
1683
+ /** Replace every image occurrence, including nested tool results, for a text-only model. */
1684
+ function replaceImagesForTextModel(blocks) {
1685
+ let next;
1686
+ for (const [index, block] of blocks.entries()) {
1687
+ if (block.type === "image") {
1688
+ next ??= blocks.slice(0, index);
1689
+ next.push({
1690
+ type: "text",
1691
+ text: textOnlyImageText(block.attachment)
1291
1692
  });
1292
1693
  continue;
1293
1694
  }
1294
- if (!hasPlainObjectPrototype(current)) return void 0;
1295
- const keys = enumerableStringKeys(current);
1296
- if (keys === void 0) return void 0;
1297
- const target = detach ? {} : void 0;
1298
- if (target !== void 0) assign(task.destination, target);
1299
- ancestors.add(current);
1300
- tasks.push({
1301
- kind: "leave",
1302
- source: current
1303
- });
1304
- for (let index = keys.length - 1; index >= 0; index--) {
1305
- const key = keys[index];
1306
- /* v8 ignore next -- the loop is bounded by the captured key count. */
1307
- if (key === void 0) return void 0;
1308
- tasks.push({
1309
- kind: "object-property",
1310
- source: current,
1311
- key,
1312
- ...target === void 0 ? {} : { target }
1313
- });
1695
+ if (block.type === "tool-result") {
1696
+ const content = replaceImagesForTextModel(block.content);
1697
+ if (content !== block.content) {
1698
+ next ??= blocks.slice(0, index);
1699
+ next.push({
1700
+ ...block,
1701
+ content
1702
+ });
1703
+ continue;
1704
+ }
1314
1705
  }
1706
+ next?.push(block);
1315
1707
  }
1316
- return detach ? root : true;
1708
+ return next ?? blocks;
1317
1709
  }
1318
1710
  /**
1319
- * Validate and detach lossless JSON in one read per property, so a stateful
1320
- * getter cannot change between validation and copying. Traversal is iterative,
1321
- * so valid nesting is bounded by available memory rather than the JavaScript
1322
- * call stack. Accepts ordinary arrays, plain or null-prototype objects, and JSON
1323
- * scalars; rejects sparse, cyclic, exotic, negative-zero, and non-finite values.
1324
- * Getter throws propagate.
1325
- *
1326
- * @param value - the candidate value to validate and detach.
1327
- * @returns the detached snapshot, or `undefined` when the value is not
1328
- * losslessly JSON-serializable.
1711
+ * Project durable image history into deterministic text for an exact text-only model.
1712
+ * @param messages - complete request history.
1713
+ * @returns the original list without images, otherwise shallow message copies with stable placeholders.
1329
1714
  */
1330
- function snapshotJsonValue(value) {
1331
- return walkJsonValue(value, true);
1715
+ function projectImagesForTextModel(messages) {
1716
+ if (!messages.some((message) => contentHasImage(message.content))) return messages;
1717
+ return messages.map((message) => {
1718
+ const content = replaceImagesForTextModel(message.content);
1719
+ return content === message.content ? message : {
1720
+ ...message,
1721
+ content
1722
+ };
1723
+ });
1332
1724
  }
1333
1725
  /**
1334
- * Test the same lossless JSON boundary as {@link snapshotJsonValue} without
1335
- * detaching it. Only own enumerable string properties participate; `toJSON`
1336
- * is ignored and getters run, so persistence boundaries use the snapshotter.
1337
- * @param value - the candidate event data to test.
1338
- * @returns whether `value` survives JSON round-trip losslessly.
1726
+ * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
1727
+ * adapters from drifting. See
1728
+ * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
1729
+ *
1730
+ * App-attribution vocabulary for provider requests.
1731
+ * @module @deepseek-ai/dsh-llm/attribution
1339
1732
  */
1340
- function isJsonValue(value) {
1341
- return walkJsonValue(value, false) === true;
1342
- }
1343
- //#endregion
1344
- //#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1._7117760508de0db46d4e3da5e4a645e6/node_modules/@deepseek-ai/dsh-tools/lib/index.js
1733
+ const { version } = createRequire(import.meta.url)("../package.json");
1345
1734
  /**
1346
- * Enforced JSON Schema subset shared by tool outputs, generated Code Mode
1347
- * types, subagents, and workflows. The subset accepts any JSON root, an
1348
- * annotation-only schema for unconstrained JSON, one scalar `type`, object
1349
- * `properties`/`required`/boolean `additionalProperties`, array `items`,
1350
- * type-correct scalar `enum`/`const`, and exact-one `oneOf`.
1735
+ * LLM service: adapter registry with a waterfall-interceptable streaming call
1736
+ * API. Exports the `LlmRuntime` default, the abstract `LlmAdapter` for
1737
+ * provider backends, and `BlockAssembler` for chunk assembly.
1351
1738
  *
1352
- * Unsupported or misplaced keywords reject rather than being accepted without
1353
- * enforcement. Consumers that require an object root apply
1354
- * {@link assertObjectJsonSchema} before accepting input.
1355
- * @module dsh-tools/json-schema
1739
+ * @module @deepseek-ai/dsh-llm
1356
1740
  */
1741
+ var __runInitializers = function(thisArg, initializers, value) {
1742
+ var useValue = arguments.length > 2;
1743
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
1744
+ return useValue ? value : void 0;
1745
+ };
1746
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
1747
+ function accept(f) {
1748
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
1749
+ return f;
1750
+ }
1751
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
1752
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
1753
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
1754
+ var _, done = false;
1755
+ for (var i = decorators.length - 1; i >= 0; i--) {
1756
+ var context = {};
1757
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
1758
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
1759
+ context.addInitializer = function(f) {
1760
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
1761
+ extraInitializers.push(accept(f || null));
1762
+ };
1763
+ var result = (0, decorators[i])(kind === "accessor" ? {
1764
+ get: descriptor.get,
1765
+ set: descriptor.set
1766
+ } : descriptor[key], context);
1767
+ if (kind === "accessor") {
1768
+ if (result === void 0) continue;
1769
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
1770
+ if (_ = accept(result.get)) descriptor.get = _;
1771
+ if (_ = accept(result.set)) descriptor.set = _;
1772
+ if (_ = accept(result.init)) initializers.unshift(_);
1773
+ } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
1774
+ else descriptor[key] = _;
1775
+ }
1776
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
1777
+ done = true;
1778
+ };
1357
1779
  /**
1358
- * Thrown when a raw schema falls outside the enforced subset. `violations`
1359
- * lists every offending path instead of stopping at the first author error.
1780
+ * Typed error for LLM-related failures. Extends {@link HarnessError}, so the
1781
+ * `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy.
1360
1782
  */
1361
- var JsonSchemaError = class extends HarnessError {
1362
- /** Individual schema violations in walk order. */
1363
- violations;
1364
- constructor(violations) {
1365
- super(`unsupported JSON schema: ${violations.join("; ")}`, "UNSUPPORTED_SCHEMA");
1366
- this.name = "JsonSchemaError";
1367
- this.violations = violations;
1783
+ var LlmError = class extends HarnessError {
1784
+ /** Serializable facts retained beside this live Error. */
1785
+ failure;
1786
+ /**
1787
+ * @param message - non-empty human-readable failure summary.
1788
+ * @param code - non-empty stable provider-neutral machine code.
1789
+ * @param options - optional cause and validated serializable provider facts.
1790
+ */
1791
+ constructor(message, code, options) {
1792
+ if (typeof message !== "string" || message.length === 0) throw new Error("LlmError message must be a non-empty string");
1793
+ if (typeof code !== "string" || code.length === 0) throw new Error("LlmError code must be a non-empty string");
1794
+ if (options?.status !== void 0 && (!Number.isInteger(options.status) || options.status < 100 || options.status > 599)) throw new Error("LlmError status must be an integer from 100 through 599");
1795
+ if (options?.providerRetryAfterMs !== void 0 && (!Number.isFinite(options.providerRetryAfterMs) || options.providerRetryAfterMs <= 0)) throw new Error("LlmError providerRetryAfterMs must be a positive finite number");
1796
+ if (options?.requestId !== void 0 && (typeof options.requestId !== "string" || options.requestId.length === 0)) throw new Error("LlmError requestId must be a non-empty string");
1797
+ super(message, code, options);
1798
+ this.name = "LlmError";
1799
+ this.failure = Object.freeze({
1800
+ message,
1801
+ code,
1802
+ ...options?.status === void 0 ? {} : { status: options.status },
1803
+ ...options?.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
1804
+ ...options?.requestId === void 0 ? {} : { requestId: options.requestId }
1805
+ });
1368
1806
  }
1369
1807
  };
1370
- const CONSTRAINT_KEYWORDS = /* @__PURE__ */ new Set([
1371
- "type",
1372
- "oneOf",
1373
- "properties",
1374
- "required",
1375
- "additionalProperties",
1376
- "items",
1377
- "enum",
1378
- "const"
1379
- ]);
1380
- const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
1381
- "description",
1382
- "title",
1383
- "default",
1384
- "examples"
1385
- ]);
1386
- const SCHEMA_TYPES = [
1387
- "object",
1388
- "array",
1389
- "string",
1390
- "number",
1391
- "integer",
1392
- "boolean",
1393
- "null"
1394
- ];
1395
- /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
1808
+ (() => {
1809
+ let _classSuper = TypertRemoteService;
1810
+ let _instanceExtraInitializers = [];
1811
+ let _listProviders_decorators;
1812
+ let _listConfigurableProviders_decorators;
1813
+ let _remoteDiscoverModels_decorators;
1814
+ return class LlmRuntime extends _classSuper {
1815
+ static {
1816
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
1817
+ _listProviders_decorators = [Remote];
1818
+ _listConfigurableProviders_decorators = [Remote];
1819
+ _remoteDiscoverModels_decorators = [Remote("discoverModels")];
1820
+ __esDecorate(this, null, _listProviders_decorators, {
1821
+ kind: "method",
1822
+ name: "listProviders",
1823
+ static: false,
1824
+ private: false,
1825
+ access: {
1826
+ has: (obj) => "listProviders" in obj,
1827
+ get: (obj) => obj.listProviders
1828
+ },
1829
+ metadata: _metadata
1830
+ }, null, _instanceExtraInitializers);
1831
+ __esDecorate(this, null, _listConfigurableProviders_decorators, {
1832
+ kind: "method",
1833
+ name: "listConfigurableProviders",
1834
+ static: false,
1835
+ private: false,
1836
+ access: {
1837
+ has: (obj) => "listConfigurableProviders" in obj,
1838
+ get: (obj) => obj.listConfigurableProviders
1839
+ },
1840
+ metadata: _metadata
1841
+ }, null, _instanceExtraInitializers);
1842
+ __esDecorate(this, null, _remoteDiscoverModels_decorators, {
1843
+ kind: "method",
1844
+ name: "remoteDiscoverModels",
1845
+ static: false,
1846
+ private: false,
1847
+ access: {
1848
+ has: (obj) => "remoteDiscoverModels" in obj,
1849
+ get: (obj) => obj.remoteDiscoverModels
1850
+ },
1851
+ metadata: _metadata
1852
+ }, null, _instanceExtraInitializers);
1853
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
1854
+ enumerable: true,
1855
+ configurable: true,
1856
+ writable: true,
1857
+ value: _metadata
1858
+ });
1859
+ }
1860
+ adapters = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new Map());
1861
+ directory = /* @__PURE__ */ new Map();
1862
+ discoveries = /* @__PURE__ */ new Map();
1863
+ constructor(ctx) {
1864
+ super(ctx, "llm");
1865
+ }
1866
+ /** Notify topology observers without letting one broken listener veto the commit. */
1867
+ emitAdaptersUpdated() {
1868
+ let invariantFailure;
1869
+ for (const listener of this.ctx.events.dispatch("emit", ["llm/adapters-updated"])) try {
1870
+ const returned = listener();
1871
+ if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
1872
+ this.warnAdaptersListenerFailure(error);
1873
+ });
1874
+ } catch (error) {
1875
+ if (error?.code === "INVARIANT") {
1876
+ invariantFailure ??= error;
1877
+ continue;
1878
+ }
1879
+ this.warnAdaptersListenerFailure(error);
1880
+ }
1881
+ if (invariantFailure !== void 0) throw invariantFailure;
1882
+ }
1883
+ /** Contained-listener diagnostic shared by the sync and async failure paths. */
1884
+ warnAdaptersListenerFailure(error) {
1885
+ this.ctx.logger.warn("llm: an llm/adapters-updated listener failed");
1886
+ this.ctx.logger.warn(error);
1887
+ }
1888
+ /**
1889
+ * Register an adapter for the given provider routes. Throws `LlmError` with code
1890
+ * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
1891
+ * Disposed with the fiber.
1892
+ * @param providers - every provider route this adapter should serve.
1893
+ * @param adapter - the adapter that streams calls for those providers.
1894
+ * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
1895
+ */
1896
+ registerAdapter(providers, adapter) {
1897
+ const owned = /* @__PURE__ */ new Set();
1898
+ let released = false;
1899
+ const dispose = this.ctx.effect(function* () {
1900
+ if (providers.length === 0) throw new LlmError("an adapter must register at least one provider", "INVALID_ADAPTER");
1901
+ this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned));
1902
+ yield () => {
1903
+ released = true;
1904
+ for (const provider of owned) this.adapters.delete(provider);
1905
+ owned.clear();
1906
+ this.emitAdaptersUpdated();
1907
+ };
1908
+ }.bind(this), "llm.registerAdapter()");
1909
+ const handle = (() => void dispose());
1910
+ handle.replace = (next) => {
1911
+ if (released) throw new LlmError("a disposed adapter registration cannot replace its routes", "REGISTRATION_DISPOSED");
1912
+ this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned));
1913
+ };
1914
+ return handle;
1915
+ }
1916
+ /**
1917
+ * Validate one candidate route set for `adapter`, treating routes this
1918
+ * registration already holds as available. Nothing is mutated: a rejected
1919
+ * candidate leaves the registry exactly as it was.
1920
+ */
1921
+ prepareRoutes(providers, adapter, owned) {
1922
+ const unique = /* @__PURE__ */ new Set();
1923
+ const registrations = [];
1924
+ for (const provider of providers) {
1925
+ if (provider.length === 0) throw new LlmError("adapter provider names must be non-empty", "INVALID_ADAPTER");
1926
+ if (unique.has(provider) || this.adapters.has(provider) && !owned.has(provider)) throw new LlmError(`an adapter for provider "${provider}" is already registered`, "DUPLICATE_ADAPTER");
1927
+ const info = adapter.providerInfo(provider);
1928
+ if (typeof info.id !== "string" || info.id !== provider || typeof info.name !== "string" || info.name.length === 0) throw new LlmError(`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`, "INVALID_ADAPTER");
1929
+ unique.add(provider);
1930
+ const retryPolicy = adapter.providerRetryPolicy(provider) ?? resolveRetryPolicy(void 0, `llm: provider "${provider}" retryPolicy`);
1931
+ registrations.push({
1932
+ adapter,
1933
+ provider: {
1934
+ id: info.id,
1935
+ name: info.name
1936
+ },
1937
+ retryPolicy
1938
+ });
1939
+ }
1940
+ return registrations;
1941
+ }
1942
+ /**
1943
+ * Swap this registration's routes for the prepared ones in one synchronous
1944
+ * section, so no observer can see the registry between the release and the
1945
+ * re-registration. The route set's one mutation point is also where
1946
+ * `llm/adapters-updated` is published, so a `replace` announces itself
1947
+ * exactly like a first registration.
1948
+ */
1949
+ commitRoutes(owned, registrations) {
1950
+ for (const provider of owned) this.adapters.delete(provider);
1951
+ owned.clear();
1952
+ for (const registration of registrations) {
1953
+ this.adapters.set(registration.provider.id, registration);
1954
+ owned.add(registration.provider.id);
1955
+ }
1956
+ this.emitAdaptersUpdated();
1957
+ }
1958
+ /**
1959
+ * Describe provider routes with a registered adapter.
1960
+ * @returns detached provider metadata in registration order.
1961
+ */
1962
+ listProviders() {
1963
+ return [...this.adapters.values()].map(({ provider }) => ({ ...provider }));
1964
+ }
1965
+ /**
1966
+ * Declare provider routes an adapter plugin can activate through
1967
+ * configuration. Registration is all-or-nothing: an empty list, invalid
1968
+ * entry, or a provider already declared by any registration throws
1969
+ * `LlmError` without registering the rest. Disposed with the fiber.
1970
+ * @param entries - every configurable provider this plugin owns.
1971
+ * @returns a handle that withdraws all of them, and can atomically replace them.
1972
+ */
1973
+ registerConfigurableProviders(entries) {
1974
+ let held = [];
1975
+ let disposed = false;
1976
+ /**
1977
+ * Validate a candidate set in full against everything this registration
1978
+ * does not already hold, then publish it. Nothing is written until the
1979
+ * whole set passes, so a refused candidate leaves the current entries in
1980
+ * place — the property that makes `replace` a swap rather than a
1981
+ * delete-then-add that can strand the directory empty.
1982
+ */
1983
+ const commit = (candidates) => {
1984
+ const detached = [];
1985
+ const own = new Set(held.map((entry) => entry.provider));
1986
+ for (const entry of candidates) {
1987
+ if (entry.provider.length === 0 || entry.displayName.length === 0 || entry.settingsNs.length === 0) throw new LlmError("configurable providers need a non-empty provider, displayName, and settingsNs", "INVALID_DIRECTORY");
1988
+ if (entry.settingsPath.some((segment) => segment.length === 0)) throw new LlmError(`configurable provider "${entry.provider}" has an empty settingsPath segment`, "INVALID_DIRECTORY");
1989
+ if (this.directory.has(entry.provider) && !own.has(entry.provider) || detached.some((seen) => seen.provider === entry.provider)) throw new LlmError(`configurable provider "${entry.provider}" is already declared`, "DUPLICATE_DIRECTORY");
1990
+ detached.push({
1991
+ ...entry,
1992
+ settingsPath: [...entry.settingsPath]
1993
+ });
1994
+ }
1995
+ for (const entry of held) this.directory.delete(entry.provider);
1996
+ for (const entry of detached) this.directory.set(entry.provider, entry);
1997
+ held = detached;
1998
+ this.emitAdaptersUpdated();
1999
+ };
2000
+ const dispose = this.ctx.effect(function* () {
2001
+ if (entries.length === 0) throw new LlmError("a configurable-provider registration must declare at least one provider", "INVALID_DIRECTORY");
2002
+ commit(entries);
2003
+ yield () => {
2004
+ disposed = true;
2005
+ for (const entry of held) this.directory.delete(entry.provider);
2006
+ held = [];
2007
+ this.emitAdaptersUpdated();
2008
+ };
2009
+ }.bind(this), "llm.registerConfigurableProviders()");
2010
+ const handle = (() => void dispose());
2011
+ handle.replace = (next) => {
2012
+ if (disposed) throw new LlmError("this configurable-provider registration was disposed", "REGISTRATION_DISPOSED");
2013
+ commit(next);
2014
+ };
2015
+ return handle;
2016
+ }
2017
+ /**
2018
+ * List every declared configurable provider, registered or dormant.
2019
+ * @returns detached directory entries in declaration order.
2020
+ */
2021
+ listConfigurableProviders() {
2022
+ return [...this.directory.values()].map((entry) => ({
2023
+ ...entry,
2024
+ settingsPath: [...entry.settingsPath]
2025
+ }));
2026
+ }
2027
+ /**
2028
+ * Offer to interrogate provider endpoints on behalf of the settings
2029
+ * namespace this plugin owns. The namespace is the key because that is what
2030
+ * a configuration surface already holds from the configurable-provider
2031
+ * directory, and because a provider being *added* has no route to name yet.
2032
+ * Disposed with the fiber.
2033
+ * @param settingsNs - the namespace whose profiles this discovery serves.
2034
+ * @param discover - interrogates one endpoint and must honor the supplied signal.
2035
+ * @returns the disposer that withdraws the offer.
2036
+ */
2037
+ registerModelDiscovery(settingsNs, discover) {
2038
+ const dispose = this.ctx.effect(function* () {
2039
+ if (settingsNs.length === 0) throw new LlmError("model discovery needs a non-empty settings namespace", "INVALID_DISCOVERY");
2040
+ if (this.discoveries.has(settingsNs)) throw new LlmError(`model discovery for "${settingsNs}" is already registered`, "DUPLICATE_DISCOVERY");
2041
+ this.discoveries.set(settingsNs, discover);
2042
+ yield () => {
2043
+ this.discoveries.delete(settingsNs);
2044
+ };
2045
+ }.bind(this), "llm.registerModelDiscovery()");
2046
+ return () => void dispose();
2047
+ }
2048
+ /**
2049
+ * Interrogate one provider endpoint for the models it advertises. The
2050
+ * request describes a draft, not a stored route, so nothing here reads or
2051
+ * writes settings or credentials — the caller owns both, and the reply is
2052
+ * candidate metadata a surface may offer for adoption.
2053
+ * @param settingsNs - namespace whose registered discovery serves this draft.
2054
+ * @param request - the endpoint, protocol, and one-shot credential to use.
2055
+ * @param signal - caller cancellation.
2056
+ * @returns the advertised models, deduplicated in endpoint order.
2057
+ */
2058
+ async discoverModels(settingsNs, request, signal) {
2059
+ const discover = this.discoveries.get(settingsNs);
2060
+ if (discover === void 0) throw new LlmError(`no model discovery is registered for "${settingsNs}"`, "NO_DISCOVERY");
2061
+ if ((request.provider ?? "").length === 0 && (request.baseURL ?? "").length === 0) throw new LlmError("model discovery needs a provider route or a baseURL", "INVALID_DISCOVERY");
2062
+ const discovered = signal === void 0 ? await discover(request) : await discover(request, signal);
2063
+ const seen = /* @__PURE__ */ new Set();
2064
+ const models = [];
2065
+ for (const model of discovered) {
2066
+ if (typeof model.id !== "string" || model.id.length === 0 || seen.has(model.id)) continue;
2067
+ seen.add(model.id);
2068
+ models.push({
2069
+ id: model.id,
2070
+ ...model.name === void 0 ? {} : { name: model.name },
2071
+ ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
2072
+ ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
2073
+ });
2074
+ }
2075
+ return models;
2076
+ }
2077
+ /**
2078
+ * Remote adapter for one draft provider interrogation.
2079
+ * @param settingsNs - namespace whose registered discovery serves this draft.
2080
+ * @param request - endpoint, protocol, and one-shot credential to use.
2081
+ * @param signal - caller cancellation supplied by the Remote carrier.
2082
+ * @returns advertised models in endpoint order.
2083
+ * @throws RemoteError with `llm/model-discovery-rejected` when discovery refuses or fails.
2084
+ */
2085
+ async remoteDiscoverModels(settingsNs, request, signal) {
2086
+ try {
2087
+ return await this.discoverModels(settingsNs, request, signal);
2088
+ } catch (error) {
2089
+ throw new RemoteError("llm/model-discovery-rejected", error instanceof Error ? error.message : String(error), {
2090
+ settingsNs,
2091
+ ...request.baseURL === void 0 ? {} : { baseURL: request.baseURL }
2092
+ }, { cause: error });
2093
+ }
2094
+ }
2095
+ /**
2096
+ * Resolve the retry policy captured when one provider route was registered.
2097
+ * @param provider - registered provider route to inspect.
2098
+ * @returns the provider-owned policy, with normal defaults already resolved.
2099
+ */
2100
+ providerRetryPolicy(provider) {
2101
+ return this.registration(provider).retryPolicy;
2102
+ }
2103
+ /**
2104
+ * Resolve provider-side request-image pricing for one exact route, or
2105
+ * `undefined` when the provider is unregistered or declares none. Unknown
2106
+ * providers degrade to `undefined` rather than throwing because callers
2107
+ * price durable history whose route may no longer be mounted.
2108
+ * @param provider - provider route named by a request header.
2109
+ * @param model - exact model id named by the same header.
2110
+ * @returns the owning adapter's image pricing for the route, when declared.
2111
+ */
2112
+ imageRequestPricing(provider, model) {
2113
+ return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
2114
+ }
2115
+ /**
2116
+ * Resolve the exact text one durable file occurrence contributes to every
2117
+ * provider request in the current execution environment.
2118
+ * @param ref - durable verbatim file reference from model history.
2119
+ * @returns the same deterministic handle text used at adapter dispatch.
2120
+ */
2121
+ fileRequestText(ref) {
2122
+ return fileHandleText(ref, this.fileReadPath(ref));
2123
+ }
2124
+ /** Detach typed adapter-owned modality metadata. */
2125
+ detachedModalities(modalities) {
2126
+ return modalities === void 0 ? void 0 : [...modalities];
2127
+ }
2128
+ /**
2129
+ * Discover models advertised by one registered provider. Catalog membership
2130
+ * is advisory and never changes routing or request validation.
2131
+ * @param provider - registered provider route to inspect.
2132
+ * @returns detached model metadata in adapter-preferred order.
2133
+ */
2134
+ async listModels(provider) {
2135
+ const models = await this.registration(provider).adapter.listModels(provider);
2136
+ const seen = /* @__PURE__ */ new Set();
2137
+ return models.map((model) => {
2138
+ if (typeof model.provider !== "string" || model.provider !== provider || typeof model.id !== "string" || model.id.length === 0 || typeof model.name !== "string" || model.name.length === 0 || model.description !== void 0 && typeof model.description !== "string" || seen.has(model.id)) throw new LlmError(`adapter returned invalid or duplicate model metadata for provider "${provider}"`, "INVALID_CATALOG");
2139
+ seen.add(model.id);
2140
+ const inputModalities = this.detachedModalities(model.inputModalities);
2141
+ return {
2142
+ provider: model.provider,
2143
+ id: model.id,
2144
+ name: model.name,
2145
+ ...model.description === void 0 ? {} : { description: model.description },
2146
+ ...inputModalities === void 0 ? {} : { inputModalities }
2147
+ };
2148
+ });
2149
+ }
2150
+ /**
2151
+ * Resolve and validate all metadata from the adapter that owns one exact
2152
+ * route. The result is detached from adapter-owned objects; catalog
2153
+ * membership remains advisory and does not control request routing.
2154
+ * @param provider - registered provider route to inspect.
2155
+ * @param model - exact model id passed to the adapter.
2156
+ * @param signal - optional cancellation for adapter-owned asynchronous lookup.
2157
+ * @returns exact model identity plus available context and reasoning metadata.
2158
+ */
2159
+ async resolveModelInfo(provider, model, signal) {
2160
+ return this.resolveModelInfoFor(this.registration(provider), model, signal);
2161
+ }
2162
+ async resolveModelInfoFor(registration, model, signal) {
2163
+ const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
2164
+ return this.normalizeModelInfo(registration, model, resolved);
2165
+ }
2166
+ /** Validate and detach one adapter-returned exact model result. */
2167
+ normalizeModelInfo(registration, model, resolved) {
2168
+ const provider = registration.provider.id;
2169
+ if (typeof resolved.provider !== "string" || resolved.provider !== provider || typeof resolved.id !== "string" || resolved.id !== model || typeof resolved.name !== "string" || resolved.name.length === 0 || resolved.description !== void 0 && typeof resolved.description !== "string") throw new LlmError(`adapter returned invalid exact model metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
2170
+ const context = resolved.context;
2171
+ if (context !== void 0 && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_CONTEXT");
2172
+ const inputModalities = this.detachedModalities(resolved.inputModalities);
2173
+ const systemPromptUpdate = resolved.systemPromptUpdate;
2174
+ if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
2175
+ const defaultMaxTokens = resolved.defaultMaxTokens;
2176
+ if (defaultMaxTokens !== void 0 && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, "INVALID_MODEL_MAX_TOKENS");
2177
+ const info = {
2178
+ provider,
2179
+ id: model,
2180
+ name: resolved.name,
2181
+ ...resolved.description === void 0 ? {} : { description: resolved.description },
2182
+ ...inputModalities === void 0 ? {} : { inputModalities },
2183
+ ...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
2184
+ ...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
2185
+ ...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate }
2186
+ };
2187
+ const reasoning = resolved.reasoning;
2188
+ if (reasoning === void 0) return info;
2189
+ if (reasoning.efforts.length === 0) throw new LlmError(`adapter returned invalid reasoning metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
2190
+ const seen = /* @__PURE__ */ new Set();
2191
+ const efforts = reasoning.efforts.map((effort) => {
2192
+ if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) throw new LlmError(`adapter returned invalid or duplicate reasoning effort metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
2193
+ seen.add(effort.id);
2194
+ return {
2195
+ id: effort.id,
2196
+ name: effort.name,
2197
+ ...effort.description === void 0 ? {} : { description: effort.description }
2198
+ };
2199
+ });
2200
+ if (reasoning.defaultEffort !== void 0 && !seen.has(reasoning.defaultEffort)) throw new LlmError(`adapter returned an unknown default reasoning effort for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
2201
+ return {
2202
+ ...info,
2203
+ reasoning: {
2204
+ efforts,
2205
+ ...reasoning.defaultEffort === void 0 ? {} : { defaultEffort: reasoning.defaultEffort }
2206
+ }
2207
+ };
2208
+ }
2209
+ /**
2210
+ * Validate a conversation call config against its exact model capability and
2211
+ * materialize adapter-configured defaults. Unsupported explicit efforts
2212
+ * reject before provider I/O; no clamping or aliasing is performed. This
2213
+ * standalone query does not bind a later dispatch; use {@link prepareCall}
2214
+ * when logging and streaming must share one adapter registration.
2215
+ * @param config - provider/model route and optional request controls.
2216
+ * @param signal - optional cancellation for adapter-owned capability lookup.
2217
+ * @returns a detached config only when a default must be materialized.
2218
+ */
2219
+ async resolveCallConfig(config, signal) {
2220
+ return (await this.resolveCallFor(this.registration(config.provider), config, signal)).config;
2221
+ }
2222
+ async resolveCallFor(registration, config, signal) {
2223
+ const info = await this.resolveModelInfoFor(registration, config.model, signal);
2224
+ return this.resolveCallWithInfo(config, info);
2225
+ }
2226
+ /** Validate request controls against one already-bound exact model result. */
2227
+ resolveCallWithInfo(config, info) {
2228
+ const defaulted = config.maxTokens === void 0 && info.defaultMaxTokens !== void 0 ? {
2229
+ ...config,
2230
+ maxTokens: info.defaultMaxTokens
2231
+ } : config;
2232
+ const reasoning = info.reasoning;
2233
+ const requested = defaulted.reasoningEffort;
2234
+ let resolvedConfig = defaulted;
2235
+ if (reasoning === void 0) {
2236
+ if (requested !== void 0) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`, "UNSUPPORTED_REASONING_EFFORT");
2237
+ } else {
2238
+ const effective = requested ?? reasoning.defaultEffort;
2239
+ if (effective !== void 0) {
2240
+ if (!reasoning.efforts.some((effort) => effort.id === effective)) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`, "UNSUPPORTED_REASONING_EFFORT");
2241
+ if (requested !== effective) resolvedConfig = {
2242
+ ...defaulted,
2243
+ reasoningEffort: effective
2244
+ };
2245
+ }
2246
+ }
2247
+ return {
2248
+ config: resolvedConfig,
2249
+ ...info.context === void 0 ? {} : { context: info.context },
2250
+ modelInfo: info
2251
+ };
2252
+ }
2253
+ /**
2254
+ * Resolve one call under its current adapter registration. The returned
2255
+ * one-shot handle keeps that registration across header logging and dispatch,
2256
+ * so HMR cannot combine one adapter's capability result with another adapter.
2257
+ * @param config - provider/model route and optional request controls.
2258
+ * @param signal - optional cancellation for adapter-owned capability lookup.
2259
+ * @returns a prepared config and its registration-bound stream entry point.
2260
+ */
2261
+ async prepareCall(config, signal) {
2262
+ const registration = this.registration(config.provider);
2263
+ const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
2264
+ const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
2265
+ const resolved = this.resolveCallWithInfo(config, modelInfo);
2266
+ const resolvedConfig = deepFreeze(structuredClone(resolved.config));
2267
+ const context = resolved.context === void 0 ? void 0 : deepFreeze(structuredClone(resolved.context));
2268
+ const adapterDefaults = deepFreeze({
2269
+ ...config.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
2270
+ ...config.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
2271
+ });
2272
+ let dispatched = false;
2273
+ return Object.freeze({
2274
+ config: resolvedConfig,
2275
+ retryPolicy: registration.retryPolicy,
2276
+ adapterDefaults,
2277
+ ...context === void 0 ? {} : { context },
2278
+ ...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
2279
+ ...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
2280
+ stream: (options) => {
2281
+ if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
2282
+ if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
2283
+ dispatched = true;
2284
+ return this.streamWithRegistration(options, {
2285
+ registration,
2286
+ config: resolvedConfig,
2287
+ modelInfo,
2288
+ dispatch: (options) => adapterCall.stream(options)
2289
+ });
2290
+ }
2291
+ });
2292
+ }
2293
+ registration(provider) {
2294
+ const registration = this.adapters.get(provider);
2295
+ if (!registration) throw new LlmError(`no adapter registered for provider "${provider}"`, "NO_ADAPTER");
2296
+ return registration;
2297
+ }
2298
+ /** Remove replay state whose historical route is owned by another adapter. */
2299
+ forAdapter(options, adapter) {
2300
+ const messages = options.messages.map((message) => {
2301
+ const source = message.source;
2302
+ if (message.role !== "assistant" || source.kind !== "model" || source.replayState === void 0) return message;
2303
+ if (this.adapters.get(source.provider)?.adapter === adapter) return message;
2304
+ return freezeMessage({
2305
+ ...message,
2306
+ source: {
2307
+ kind: "model",
2308
+ provider: source.provider,
2309
+ model: source.model
2310
+ }
2311
+ });
2312
+ });
2313
+ if (messages.every((message, index) => message === options.messages[index])) return options;
2314
+ const filtered = {
2315
+ ...options,
2316
+ messages
2317
+ };
2318
+ return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
2319
+ }
2320
+ /**
2321
+ * Resolve the current execution-world read path of one durable file
2322
+ * reference through the mounted attachment and filesystem providers.
2323
+ */
2324
+ fileReadPath(ref) {
2325
+ let hostPath;
2326
+ try {
2327
+ hostPath = this.ctx.get("attachments")?.fileHostPath(ref);
2328
+ } catch {
2329
+ return;
2330
+ }
2331
+ if (hostPath === void 0) return void 0;
2332
+ return this.ctx.get("fs")?.processPathFromHostPath(hostPath);
2333
+ }
2334
+ /**
2335
+ * Final adapter boundary. Adapter selection, dispatch, iterator construction,
2336
+ * and iteration failures become one terminal failure chunk. Middleware and
2337
+ * downstream consumer failures remain thrown plugin or consumer errors.
2338
+ */
2339
+ async *adapterStream(options, prepared) {
2340
+ let iterator;
2341
+ try {
2342
+ const registration = prepared?.registration ?? this.registration(options.provider);
2343
+ const adapter = registration.adapter;
2344
+ let modelInfo;
2345
+ let resolvedConfig;
2346
+ let dispatch;
2347
+ if (prepared === void 0) {
2348
+ const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
2349
+ modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
2350
+ resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
2351
+ dispatch = (options) => adapterCall.stream(options);
2352
+ } else {
2353
+ modelInfo = prepared.modelInfo;
2354
+ resolvedConfig = prepared.config;
2355
+ dispatch = prepared.dispatch;
2356
+ }
2357
+ if (prepared !== void 0 && !callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
2358
+ const resolvedOptions = callConfigEquals(options, resolvedConfig) ? options : Object.isFrozen(options) ? deepFreeze({
2359
+ ...options,
2360
+ ...resolvedConfig
2361
+ }) : {
2362
+ ...options,
2363
+ ...resolvedConfig
2364
+ };
2365
+ let projectedMessages = resolvedOptions.messages;
2366
+ if (projectedMessages.some((message) => contentHasFile(message.content))) projectedMessages = projectFilesToText(projectedMessages, (ref) => this.fileReadPath(ref));
2367
+ if (modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && projectedMessages.some((message) => contentHasImage(message.content))) projectedMessages = projectImagesForTextModel(projectedMessages);
2368
+ const projectedOptions = projectedMessages === resolvedOptions.messages ? resolvedOptions : Object.isFrozen(resolvedOptions) ? deepFreeze({
2369
+ ...resolvedOptions,
2370
+ messages: projectedMessages
2371
+ }) : {
2372
+ ...resolvedOptions,
2373
+ messages: projectedMessages
2374
+ };
2375
+ iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
2376
+ } catch (error) {
2377
+ yield adapterFailureChunk(error, options.signal);
2378
+ return;
2379
+ }
2380
+ let completed = false;
2381
+ try {
2382
+ while (true) {
2383
+ let item;
2384
+ try {
2385
+ const next = await iterator.next();
2386
+ item = next.done ? { done: true } : {
2387
+ done: false,
2388
+ value: next.value
2389
+ };
2390
+ } catch (error) {
2391
+ completed = true;
2392
+ yield adapterFailureChunk(error, options.signal);
2393
+ return;
2394
+ }
2395
+ if (item.done) {
2396
+ completed = true;
2397
+ return;
2398
+ }
2399
+ yield item.value;
2400
+ }
2401
+ } finally {
2402
+ if (!completed) {
2403
+ const close = iterator.return?.bind(iterator);
2404
+ if (close) await close();
2405
+ }
2406
+ }
2407
+ }
2408
+ /**
2409
+ * Stream one model call as raw chunks (token-level deltas). Replay state is
2410
+ * retained only when the same adapter instance owns its historical provider
2411
+ * and the target provider. Final adapter selection remains fixed through
2412
+ * asynchronous exact-model resolution and dispatch. Adapter selection,
2413
+ * dispatch, and iteration failures become terminal `error` or `aborted`
2414
+ * finish chunks; middleware, nested-call, cleanup, and consumer failures
2415
+ * remain thrown.
2416
+ * @param options - the full request; `options.provider` selects the adapter.
2417
+ * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
2418
+ */
2419
+ stream(options) {
2420
+ return this.streamWithRegistration(options);
2421
+ }
2422
+ streamWithRegistration(options, prepared) {
2423
+ return this.ctx.waterfall(this, "llm/stream", options, () => this.adapterStream(options, prepared));
2424
+ }
2425
+ };
2426
+ })();
2427
+ /** Convert one adapter throw into the stream protocol's terminal outcome. */
2428
+ function adapterFailureChunk(error, signal) {
2429
+ const failure = normalizeLlmFailure(error);
2430
+ return {
2431
+ type: "finish",
2432
+ reason: signal?.aborted || failure.code === "ABORTED" ? {
2433
+ kind: "aborted",
2434
+ failure
2435
+ } : {
2436
+ kind: "error",
2437
+ failure
2438
+ }
2439
+ };
2440
+ }
2441
+ //#endregion
2442
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1._aaa8ad560a1b4ea9b6e9490b78b8a32d/node_modules/@deepseek-ai/dsh-tools/lib/index.js
2443
+ /**
2444
+ * Enforced JSON Schema subset shared by tool outputs, generated PTC mode
2445
+ * types, subagents, and workflows. The subset accepts any JSON root, an
2446
+ * annotation-only schema for unconstrained JSON, one scalar `type`, object
2447
+ * `properties`/`required`/boolean `additionalProperties`, array `items`,
2448
+ * type-correct scalar `enum`/`const`, and exact-one `oneOf`.
2449
+ *
2450
+ * Unsupported or misplaced keywords reject rather than being accepted without
2451
+ * enforcement. Consumers that require an object root apply
2452
+ * {@link assertObjectJsonSchema} before accepting input.
2453
+ * @module dsh-tools/json-schema
2454
+ */
2455
+ /**
2456
+ * Thrown when a raw schema falls outside the enforced subset. `violations`
2457
+ * lists every offending path instead of stopping at the first author error.
2458
+ */
2459
+ var JsonSchemaError = class extends HarnessError {
2460
+ /** Individual schema violations in walk order. */
2461
+ violations;
2462
+ constructor(violations) {
2463
+ super(`unsupported JSON schema: ${violations.join("; ")}`, "UNSUPPORTED_SCHEMA");
2464
+ this.name = "JsonSchemaError";
2465
+ this.violations = violations;
2466
+ }
2467
+ };
2468
+ const CONSTRAINT_KEYWORDS = /* @__PURE__ */ new Set([
2469
+ "type",
2470
+ "oneOf",
2471
+ "properties",
2472
+ "required",
2473
+ "additionalProperties",
2474
+ "items",
2475
+ "enum",
2476
+ "const"
2477
+ ]);
2478
+ const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
2479
+ "description",
2480
+ "title",
2481
+ "default",
2482
+ "examples"
2483
+ ]);
2484
+ const SCHEMA_TYPES = [
2485
+ "object",
2486
+ "array",
2487
+ "string",
2488
+ "number",
2489
+ "integer",
2490
+ "boolean",
2491
+ "null"
2492
+ ];
2493
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
1396
2494
  function hasIntrinsicConstructor(prototype, name) {
1397
2495
  const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
1398
2496
  if (typeof constructor !== "function") return false;
@@ -2194,13 +3292,13 @@ function defineTool(options) {
2194
3292
  return tool;
2195
3293
  }
2196
3294
  /**
2197
- * Code Mode `run_code` transport. Programs call the registry's agent-visible
3295
+ * PTC mode `run_code` transport. Programs call the registry's agent-visible
2198
3296
  * tools through nested executions scheduled under the native concurrency
2199
3297
  * contract; each sub-dispatch is logged for reconstruction, while only the
2200
3298
  * outer curated result enters model history.
2201
- * @module @deepseek-ai/dsh-tools/src/code-mode
3299
+ * @module @deepseek-ai/dsh-tools/src/ptc
2202
3300
  */
2203
- /** The model-facing name of the Code Mode tool. */
3301
+ /** The model-facing name of the PTC mode tool. */
2204
3302
  const RUN_CODE_NAME = "run_code";
2205
3303
  /**
2206
3304
  * The TypeScript flavor: the fallback for a schema read with no runtime
@@ -2518,7 +3616,7 @@ function createRunCodeTool(registry, options) {
2518
3616
  if (runOver()) throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} not dispatched`);
2519
3617
  const normalized = jsonNormalizeArgs(rawArgs);
2520
3618
  const n = ++dispatches;
2521
- const subCallId = CallId(`${String(exec.callId)}:code:${n}`);
3619
+ const subCallId = brandString(`${String(exec.callId)}:ptc:${n}`);
2522
3620
  const input = {
2523
3621
  callId: subCallId,
2524
3622
  rootCallId: exec.rootCallId,
@@ -2550,7 +3648,7 @@ function createRunCodeTool(registry, options) {
2550
3648
  isError: result.isError,
2551
3649
  content: result.content
2552
3650
  });
2553
- agent.session.append("tool/code-dispatch", {
3651
+ agent.session.append("tool/ptc-dispatch", {
2554
3652
  rootCallId: exec.rootCallId,
2555
3653
  parentCallId: exec.callId,
2556
3654
  subCallId,
@@ -2572,7 +3670,7 @@ function createRunCodeTool(registry, options) {
2572
3670
  reject(/* @__PURE__ */ new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`));
2573
3671
  },
2574
3672
  async start() {
2575
- exec.agent?.session.append("tool/code-dispatch-start", {
3673
+ exec.agent?.session.append("tool/ptc-dispatch-start", {
2576
3674
  rootCallId: exec.rootCallId,
2577
3675
  parentCallId: exec.callId,
2578
3676
  subCallId,
@@ -2606,7 +3704,7 @@ function createRunCodeTool(registry, options) {
2606
3704
  content: result.content,
2607
3705
  source: {
2608
3706
  kind: "plugin",
2609
- plugin: "tools-code-mode"
3707
+ plugin: "tools-ptc"
2610
3708
  }
2611
3709
  }));
2612
3710
  for (const context of result.additionalContexts ?? []) exec.deferContext(context);
@@ -2690,7 +3788,7 @@ function createRunCodeTool(registry, options) {
2690
3788
  return definition;
2691
3789
  }
2692
3790
  /**
2693
- * Code Mode codegen: the pure projection from registered tool schemas to the TypeScript SDK
3791
+ * PTC mode codegen: the pure projection from registered tool schemas to the TypeScript SDK
2694
3792
  * text the model programs against (the `tools:sdk` prompt section). Sibling of
2695
3793
  * `json-schema.ts` — `schemas()` (native function calling) and this module (the generated
2696
3794
  * `declare const tools` API) are two projections of the same store.
@@ -2897,17 +3995,35 @@ function jsonSchemaToTs(schema, indent = 0) {
2897
3995
  return "unknown";
2898
3996
  }
2899
3997
  }
2900
- /** The fixed model-facing usage contract rendered above the declarations (see the Code Mode Agent Note's "What the model sees"). */
3998
+ /** The fixed model-facing usage contract rendered above the declarations (see the PTC mode Agent Note's "What the model sees"). */
2901
3999
  const SDK_INSTRUCTIONS$1 = `## Writing code for run_code
2902
4000
 
2903
- \`run_code\` takes two required arguments: \`code\` — the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped) — and \`description\`, a short summary of what the program does. Inside the program:
4001
+ \`run_code\` takes two required arguments: \`code\` — the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped) — and \`description\`, a short summary of what the program does. The declarations below are SDK bindings for this program. A declaration does not make its name a directly callable tool; only names supplied as separate tool schemas may be called directly.`;
4002
+ const SDK_PROGRAM_INSTRUCTIONS = `Inside the program:
2904
4003
 
2905
4004
  - Call tools as \`await tools.name(args)\` — quoted access for exotic names: \`tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
2906
4005
  - A FAILED tool call rejects with \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose \`message\` is human-readable — \`try/catch\` it to handle and continue.
2907
4006
  - Independent read-only calls MAY overlap under \`Promise.all\` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with \`await\`.
2908
4007
  - Emit results with \`return\` and/or \`console.log(...)\`. Only what you print or return is program output. A successful tool result containing an image is attached after the run so you can inspect it on the next step; every other intermediate result stays out of the conversation, so extract just what you need.
2909
4008
 
2910
- The available tools:`;
4009
+ Program-only SDK bindings:`;
4010
+ /** Whether one string schema accepts the literal used by the bash example. */
4011
+ function acceptsExampleString(schema, value) {
4012
+ return schema?.type === "string" && (schema.const === void 0 || schema.const === value) && (schema.enum === void 0 || schema.enum.includes(value));
4013
+ }
4014
+ /** Render the bash example only when its literal arguments satisfy the current parameter schema. */
4015
+ function renderBashExample(schemas) {
4016
+ const bash = schemas.find((schema) => schema.name === "bash");
4017
+ if (bash === void 0) return "";
4018
+ const parameters = bash.parameters;
4019
+ if (parameters.type !== "object") return "";
4020
+ const required = parameters.required ?? [];
4021
+ if (required.some((name) => name !== "command" && name !== "description")) return "";
4022
+ if (!acceptsExampleString(parameters.properties?.command, "pwd")) return "";
4023
+ const needsDescription = required.includes("description");
4024
+ if (needsDescription && !acceptsExampleString(parameters.properties?.description, "Show current directory")) return "";
4025
+ return ` When no separate \`bash\` schema is supplied, invoke a declared \`bash\` binding inside \`run_code\`:\n\n\`run_code({ code: "return await tools.bash({ command: 'pwd'${needsDescription ? ", description: 'Show current directory'" : ""} })", description: "Show current directory" })\``;
4026
+ }
2911
4027
  /**
2912
4028
  * Render the full `tools:sdk` prompt section: the fixed usage instructions
2913
4029
  * plus one `declare const tools` interface covering every given tool.
@@ -2929,7 +4045,7 @@ function renderToolsSdk(schemas) {
2929
4045
  argsMembers.push(`${pad$1(1)}${renderKey(schema.name)}: ${jsonSchemaToTs(schema.parameters, 1)};`);
2930
4046
  outputMembers.push(`${pad$1(1)}${renderKey(schema.name)}: ${jsonSchemaToTs(schema.output, 1)};`);
2931
4047
  }
2932
- return `${SDK_INSTRUCTIONS$1}\n\n\`\`\`ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\n${[
4048
+ const declaration = [
2933
4049
  `interface ToolArgsMap {${argsMembers.length > 0 ? `\n${argsMembers.join("\n")}\n` : ""}}`,
2934
4050
  `interface ToolOutputMap {${outputMembers.length > 0 ? `\n${outputMembers.join("\n")}\n` : ""}}`,
2935
4051
  "type ToolName = keyof ToolOutputMap",
@@ -2944,16 +4060,17 @@ function renderToolsSdk(schemas) {
2944
4060
  " [K in ToolName]: (args: ToolArgsMap[K]) => Promise<ToolOutputMap[K]>;",
2945
4061
  "}"
2946
4062
  ].join("\n")
2947
- ].join("\n\n")}\n\`\`\``;
4063
+ ].join("\n\n");
4064
+ return `${SDK_INSTRUCTIONS$1}${renderBashExample(sorted)}\n\n${SDK_PROGRAM_INSTRUCTIONS}\n\n\`\`\`ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\n${declaration}\n\`\`\``;
2948
4065
  }
2949
4066
  /**
2950
- * Code Mode codegen — Python flavor. The pure projection from registered tool schemas to the
4067
+ * PTC mode codegen — Python flavor. The pure projection from registered tool schemas to the
2951
4068
  * Python SDK text the model programs against under `runtime.language === 'python'`. Sibling of
2952
4069
  * {@link ./ts-types.ts | ts-types.ts}; the two files are two projections of the same registry
2953
4070
  * store, keyed by the loaded {@link @deepseek-ai/dsh-code-runtime#CodeRuntime.language | code
2954
4071
  * runtime's language}.
2955
4072
  *
2956
- * Under `mode: 'code'` the native tool schemas are omitted from the request, so this generated
4073
+ * Under `mode: 'ptc'` the native tool schemas are omitted from the request, so this generated
2957
4074
  * SDK is the model's ONLY source for each tool's argument names, required fields, types,
2958
4075
  * descriptions, and canonical output shapes; under `mode: 'both'` the native schemas ship
2959
4076
  * alongside it and it is one of two. Object-shaped arguments and outputs therefore render as one
@@ -2974,7 +4091,7 @@ const IDENTIFIER = /^[\p{XID_Start}_]\p{XID_Continue}*$/u;
2974
4091
  * Python identifiers are not ASCII: `路径` is as legal a field name as `path`,
2975
4092
  * and rejecting it would degrade the whole enclosing object, dropping every
2976
4093
  * field's name, requiredness, and type — information whose only source under
2977
- * `mode: 'code'` is this generated text.
4094
+ * `mode: 'ptc'` is this generated text.
2978
4095
  *
2979
4096
  * NFKC stability is a second and separate condition, because CPython
2980
4097
  * normalizes identifiers at compile time while JSON keys are compared as
@@ -3121,7 +4238,7 @@ function pad(indent) {
3121
4238
  * (`SyntaxError: source code string cannot contain null bytes`), whether it
3122
4239
  * sits in a docstring or in a comment, so one such byte anywhere in a schema
3123
4240
  * description would make the whole generated SDK unparseable — under
3124
- * `mode: 'code'`, the model's only declaration of the tools. The rest are
4241
+ * `mode: 'ptc'`, the model's only declaration of the tools. The rest are
3125
4242
  * legal but invisible; escaping them with the same rule keeps the emitted text
3126
4243
  * readable and the treatment uniform.
3127
4244
  *
@@ -3187,7 +4304,7 @@ function describe(schema) {
3187
4304
  * Backslashes are doubled first, every quote is escaped, and a trailing
3188
4305
  * backslash cannot survive: a description ending in `"` or an odd backslash
3189
4306
  * would otherwise merge with (or escape) the closing triple quote and make
3190
- * the generated block — Code Mode's only SDK — syntactically invalid Python.
4307
+ * the generated block — PTC mode's only SDK — syntactically invalid Python.
3191
4308
  */
3192
4309
  function docLines(description, indent) {
3193
4310
  const collapsed = describe({ description });
@@ -3641,7 +4758,7 @@ function renderToolsSdkPy(schemas) {
3641
4758
  * section under a non-native mode; a runtime whose language is not a key
3642
4759
  * fails the assembly loudly (same idiom as `toolOrder` violations). Adding a
3643
4760
  * new backend language is three parallel edits — a {@link CodeSdkLanguage}
3644
- * member, an entry here, and a `RUN_CODE_FLAVORS` entry in `code-mode.ts` for
4761
+ * member, an entry here, and a `RUN_CODE_FLAVORS` entry in `ptc.ts` for
3645
4762
  * its `run_code` schema strings — plus the renderer function this table points
3646
4763
  * at. The `satisfies` clause pins this table's key set to that union, which
3647
4764
  * the flavor table is checked against too, so any of the three left out is a
@@ -3652,17 +4769,11 @@ function renderToolsSdkPy(schemas) {
3652
4769
  * {@link Config.mode} JSDoc.
3653
4770
  */
3654
4771
  /**
3655
- * Prompt order of the `code` collapse statement: after the persona and before
3656
- * the 100-199 per-tool guidance band, so the model reads which tools it may
3657
- * call before it reads what each one is for.
3658
- */
3659
- const COLLAPSE_SECTION_ORDER = 99;
3660
- /**
3661
- * The model-facing statement of the `code` collapse. Names the consequence
4772
+ * The model-facing statement of the `ptc` collapse. Names the consequence
3662
4773
  * (the call fails) and the route (inside the program), because a rule the
3663
4774
  * model can only discover by being denied is one it corrects too late.
3664
4775
  */
3665
- const CODE_ONLY_INSTRUCTION = `\`${RUN_CODE_NAME}\` is the only tool you can call directly — a tool call naming any other tool fails. Reach every tool the SDK declares below from inside the program.`;
4776
+ const PTC_ONLY_INSTRUCTION = `\`${RUN_CODE_NAME}\` is the only tool you can call directly — a tool call naming any other tool fails. Reach every tool the SDK declares below from inside the program.`;
3666
4777
  const SDK_RENDERERS = {
3667
4778
  typescript: renderToolsSdk,
3668
4779
  python: renderToolsSdkPy
@@ -3809,7 +4920,7 @@ function resolveMaxParallelSubCalls(value) {
3809
4920
  static Config = z.object({
3810
4921
  mode: z.union([
3811
4922
  "native",
3812
- "code",
4923
+ "ptc",
3813
4924
  "both"
3814
4925
  ]).default("native"),
3815
4926
  maxParallelSubCalls: z.natural().min(1).default(10)
@@ -3838,10 +4949,10 @@ function resolveMaxParallelSubCalls(value) {
3838
4949
  /**
3839
4950
  * Reserved presentation transport, kept outside the filterable registration
3840
4951
  * layers. Built on first need rather than at construction: which agents run
3841
- * a code mode is no longer known when the service is constructed, and the
4952
+ * a PTC mode is no longer known when the service is constructed, and the
3842
4953
  * transport is stateless beyond its closures over `this`.
3843
4954
  */
3844
- codeTransport;
4955
+ ptcTransport;
3845
4956
  constructor(ctx, config = {}) {
3846
4957
  super(ctx, "tools");
3847
4958
  this.defaultMode = config.mode ?? "native";
@@ -3853,34 +4964,33 @@ function resolveMaxParallelSubCalls(value) {
3853
4964
  }
3854
4965
  }
3855
4966
  /**
3856
- * The prompt statement of the `code` executor collapse, registered wherever
3857
- * {@link sdkSection} is and rendering empty outside an effective `code`.
4967
+ * The prompt statement of the `ptc` executor collapse, registered wherever
4968
+ * {@link sdkSection} is and rendering empty outside an effective `ptc`.
3858
4969
  *
3859
4970
  * Every tool contributes its own guidance section naming its tool, none of
3860
- * them qualify how that tool is reached, and they all render before the SDK
3861
- * (orders 100-199 against {@link SDK_SECTION_ORDER}). Without this the model
3862
- * reads a catalog of tools it is told to use and no statement that only
3863
- * `run_code` may be called, so it emits a native call, receives
3864
- * `UNKNOWN_TOOL` for a tool the prompt just declared, and concludes the
3865
- * deployment is inconsistent. {@link COLLAPSE_SECTION_ORDER} places the rule
3866
- * before that guidance rather than after it.
4971
+ * them qualify how that tool is reached, and they all render before the SDK.
4972
+ * Without this the model reads a catalog of tools it is told to use and no
4973
+ * statement that only `run_code` may be called, so it emits a native call,
4974
+ * receives `UNKNOWN_TOOL` for a tool the prompt just declared, and concludes
4975
+ * the deployment is inconsistent. Its order places the rule before that
4976
+ * guidance rather than after it.
3867
4977
  *
3868
4978
  * `both` renders empty: native calls do execute there, so the rule is false.
3869
4979
  * @returns the section registration.
3870
4980
  */
3871
4981
  collapseSection() {
3872
4982
  return {
3873
- name: "tools:code-only",
3874
- order: COLLAPSE_SECTION_ORDER,
3875
- text: (context) => this.modeFor(context.scope) === "code" ? CODE_ONLY_INSTRUCTION : ""
4983
+ name: "tools:ptc-only",
4984
+ order: this.ctx.systemPrompt.getSectionOrder("PTC_ONLY"),
4985
+ text: (context) => this.modeFor(context.scope) === "ptc" ? PTC_ONLY_INSTRUCTION : ""
3876
4986
  };
3877
4987
  }
3878
4988
  /**
3879
- * The generated-SDK prompt section, registered globally by a code-mode
4989
+ * The generated-SDK prompt section, registered globally by a PTC mode
3880
4990
  * deployment and per scope by {@link presentAs}.
3881
4991
  *
3882
4992
  * The body regenerates from the CALLING scope, and renders empty for an
3883
- * agent presenting natively — an agent that opted out under a code-mode
4993
+ * agent presenting natively — an agent that opted out under a PTC mode
3884
4994
  * deployment still sees the global registration, and an empty section is
3885
4995
  * dropped from the rendered prompt.
3886
4996
  * @returns the section registration.
@@ -3888,7 +4998,7 @@ function resolveMaxParallelSubCalls(value) {
3888
4998
  sdkSection() {
3889
4999
  return {
3890
5000
  name: "tools:sdk",
3891
- order: 150,
5001
+ order: this.ctx.systemPrompt.getSectionOrder("TOOLS_SDK"),
3892
5002
  text: (context) => {
3893
5003
  const mode = this.modeFor(context.scope);
3894
5004
  if (mode === "native") return "";
@@ -3924,13 +5034,13 @@ function resolveMaxParallelSubCalls(value) {
3924
5034
  * @returns the shared transport definition.
3925
5035
  */
3926
5036
  requireCodeTransport() {
3927
- this.codeTransport ??= createRunCodeTool(this, {
5037
+ this.ptcTransport ??= createRunCodeTool(this, {
3928
5038
  requireRuntime: () => this.requireCodeRuntime(this.defaultMode),
3929
5039
  peekRuntime: () => this.ctx.get("codeRuntime"),
3930
5040
  maxParallel: this.maxParallelSubCalls,
3931
5041
  shapeDispatchLog: (dispatch) => this.shapeDispatchLog(dispatch)
3932
5042
  });
3933
- return this.codeTransport;
5043
+ return this.ptcTransport;
3934
5044
  }
3935
5045
  /**
3936
5046
  * Present the calling scope's tools in `mode` instead of the deployment
@@ -3938,7 +5048,7 @@ function resolveMaxParallelSubCalls(value) {
3938
5048
  * declaration covers every agent joined under it.
3939
5049
  *
3940
5050
  * Scoped only, and one declaration per scope: this is how an agent preset
3941
- * composes Code Mode agents beside native ones in the same process, and a
5051
+ * composes PTC mode agents beside native ones in the same process, and a
3942
5052
  * process-global override would be the `mode` config field instead.
3943
5053
  * @param mode - the presentation the covered agents' models see.
3944
5054
  * @returns the exact disposer that restores the deployment default.
@@ -3973,7 +5083,7 @@ function resolveMaxParallelSubCalls(value) {
3973
5083
  };
3974
5084
  this.requireCodeRuntime(mode);
3975
5085
  const schemas = [...view.visible.values()].map((definition) => this.schemaOf(definition, false));
3976
- if (mode === "code") return {
5086
+ if (mode === "ptc") return {
3977
5087
  schemas: schemas.filter((schema) => schema.name === RUN_CODE_NAME),
3978
5088
  knownNames: [RUN_CODE_NAME]
3979
5089
  };
@@ -3987,16 +5097,14 @@ function resolveMaxParallelSubCalls(value) {
3987
5097
  * Read at use time (assembly / run_code execution), NOT via static
3988
5098
  * `inject`: an inject entry would hold `ctx.tools` — and every tool plugin
3989
5099
  * behind it — hostage to a code runtime existing even under `mode:
3990
- * 'native'` (the loop's optional-backend idiom, same as
3991
- * `sessionPersistence`).
5100
+ * 'native'`.
3992
5101
  *
3993
5102
  * Assembly and `run_code` execution read separately, so the language is not
3994
5103
  * bound to a request. Harmless while one published backend exists — both
3995
5104
  * reads return the same flavor — but a reload that swapped in a second
3996
5105
  * language between them would hand a program written against one SDK to the
3997
5106
  * other. Binding it is deferred until a second backend ships (the first
3998
- * point it is testable); rationale in the
3999
- * [language-dispatch note](../../../../.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md).
5107
+ * point it is testable).
4000
5108
  */
4001
5109
  requireCodeRuntime(mode) {
4002
5110
  const runtime = this.ctx.get("codeRuntime");
@@ -4020,7 +5128,7 @@ function resolveMaxParallelSubCalls(value) {
4020
5128
  assertSupportedJsonSchema(output.schema);
4021
5129
  const timeoutMs = definition.timeoutMs;
4022
5130
  if (timeoutMs !== void 0 && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) throw new TypeError(`tool "${name}" timeoutMs must be a positive finite number`);
4023
- if (name === "run_code") throw new Error(`tool name "${RUN_CODE_NAME}" is reserved for the Code Mode presentation transport and cannot be registered or shadowed`);
5131
+ if (name === "run_code") throw new Error(`tool name "${RUN_CODE_NAME}" is reserved for the PTC mode presentation transport and cannot be registered or shadowed`);
4024
5132
  return this.layers.effect(this.ctx, (layer) => layer.tools.insert(name, definition), { label: "tools.register()" });
4025
5133
  }
4026
5134
  /**
@@ -4040,7 +5148,7 @@ function resolveMaxParallelSubCalls(value) {
4040
5148
  ...allow !== void 0 ? { allow: new Set(allow) } : {},
4041
5149
  ...deny !== void 0 ? { deny: new Set(deny) } : {}
4042
5150
  };
4043
- if ([...allow ?? [], ...deny ?? []].includes("run_code")) throw new Error(`tools.restrict() cannot name reserved Code Mode presentation transport "${RUN_CODE_NAME}"; restrict end-capability tools instead`);
5151
+ if ([...allow ?? [], ...deny ?? []].includes("run_code")) throw new Error(`tools.restrict() cannot name reserved PTC mode presentation transport "${RUN_CODE_NAME}"; restrict end-capability tools instead`);
4044
5152
  const known = this.view(scope).restrictableNames;
4045
5153
  const unknown = [...allow ?? [], ...deny ?? []].filter((name) => !known.has(name));
4046
5154
  if (unknown.length > 0) throw new Error(`tools.restrict() names unknown global tool${unknown.length > 1 ? "s" : ""} ${unknown.map((n) => `"${n}"`).join(", ")}; known global tools: ${[...known].sort().join(", ") || "(none)"}`);
@@ -4082,9 +5190,9 @@ function resolveMaxParallelSubCalls(value) {
4082
5190
  * A restriction filters what a scope inherits — the global layer and every
4083
5191
  * ancestor layer on its chain — and never what its OWN layer registers.
4084
5192
  * That exemption is what a per-child capability filter has to keep intact:
4085
- * the delegation runtime registers a child's reporting and structured-output
4086
- * tools into the child's own layer, and a filter naming the capabilities the
4087
- * child may use must not strip the machinery it answers through.
5193
+ * the delegation runtime registers a child's structured-output tool into the
5194
+ * child's own layer, and a filter naming the capabilities the child may use
5195
+ * must not strip the machinery it answers through.
4088
5196
  *
4089
5197
  * Reading the exempt set as "the global layer" instead of "not mine" held
4090
5198
  * only while every model-facing tool sat in the host composition. Once
@@ -4136,7 +5244,7 @@ function resolveMaxParallelSubCalls(value) {
4136
5244
  /**
4137
5245
  * Resolve the definition that MAY EXECUTE for a call, applying the mode
4138
5246
  * collapse at the operation boundary that owns it. The registry view
4139
- * (`get`) is presentation-agnostic; here a MODEL-DIRECT call under `code`
5247
+ * (`get`) is presentation-agnostic; here a MODEL-DIRECT call under `ptc`
4140
5248
  * may only name the reserved `run_code` transport, while a nested
4141
5249
  * sub-dispatch (a `parent` token set — the `run_code` SDK calling a tool
4142
5250
  * it bound) may call any visible tool. Denial surfaces as `UNKNOWN_TOOL`
@@ -4161,7 +5269,7 @@ function resolveMaxParallelSubCalls(value) {
4161
5269
  schemas(scope) {
4162
5270
  return [...this.view(scope).visible.values()].map((definition) => this.schemaOf(definition, true));
4163
5271
  }
4164
- /** Project visible callable tools onto the generated Code Mode SDK contract. */
5272
+ /** Project visible callable tools onto the generated PTC mode SDK contract. */
4165
5273
  sdkSchemas(scope) {
4166
5274
  return [...this.view(scope).visible.values()].filter((definition) => definition.name !== RUN_CODE_NAME).map((definition) => {
4167
5275
  const output = snapshotJsonValue(definition.output.schema);
@@ -4201,8 +5309,8 @@ function resolveMaxParallelSubCalls(value) {
4201
5309
  }
4202
5310
  }
4203
5311
  /**
4204
- * Run the `tools/code-dispatch-log` waterfall over one settled sub-dispatch
4205
- * and return the content the bridge should log on `tool/code-dispatch`.
5312
+ * Run the `tools/ptc-dispatch-log` waterfall over one settled sub-dispatch
5313
+ * and return the content the bridge should log on `tool/ptc-dispatch`.
4206
5314
  * Contained: when a listener throws, the method logs the original settled
4207
5315
  * content; that failure must not fail the dispatch or omit the settle event. Private:
4208
5316
  * the ONE consumer is the `run_code` bridge this registry constructs, which
@@ -4211,20 +5319,20 @@ function resolveMaxParallelSubCalls(value) {
4211
5319
  */
4212
5320
  async shapeDispatchLog(dispatch) {
4213
5321
  try {
4214
- return await this.ctx.waterfall(scopeTarget(this, dispatch.agent), "tools/code-dispatch-log", dispatch, () => Promise.resolve(dispatch.content));
5322
+ return await this.ctx.waterfall(scopeTarget(this, dispatch.agent), "tools/ptc-dispatch-log", dispatch, () => Promise.resolve(dispatch.content));
4215
5323
  } catch (error) {
4216
- this.ctx.logger.warn(`tools: code-dispatch-log listener failed for ${dispatch.name}: ${errorMessage$1(error)}; logging the original settled content`);
5324
+ this.ctx.logger.warn(`tools: ptc-dispatch-log listener failed for ${dispatch.name}: ${errorMessage$1(error)}; logging the original settled content`);
4217
5325
  return dispatch.content;
4218
5326
  }
4219
5327
  }
4220
5328
  /**
4221
- * Whether the `code` mode collapse denies a model-direct call: only the
5329
+ * Whether the `ptc` mode collapse denies a model-direct call: only the
4222
5330
  * reserved `run_code` transport may be named. Nested sub-dispatches (a
4223
5331
  * `parent` token set) bypass the collapse. One home for the
4224
5332
  * security-relevant predicate, shared by {@link resolveExecution} and
4225
5333
  * {@link createExecution} so the two can never drift apart.
4226
5334
  *
4227
- * Resolved through {@link modeFor}, NOT `defaultMode`: an agent given `code`
5335
+ * Resolved through {@link modeFor}, NOT `defaultMode`: an agent given `ptc`
4228
5336
  * by an agent preset under a native deployment is the composition
4229
5337
  * `dsh-agent-tool-presentation` exists for, and reading the deployment default would
4230
5338
  * leave exactly that agent uncollapsed — announcing one surface while
@@ -4234,7 +5342,7 @@ function resolveMaxParallelSubCalls(value) {
4234
5342
  * @param nested - whether the call is a transport sub-dispatch, not a model-direct call.
4235
5343
  */
4236
5344
  collapses(name, scope, nested) {
4237
- return !nested && this.modeFor(scope) === "code" && name !== "run_code";
5345
+ return !nested && this.modeFor(scope) === "ptc" && name !== "run_code";
4238
5346
  }
4239
5347
  /**
4240
5348
  * Execute through pre-policy, guards, around-dispatch, post-policy,
@@ -4841,6 +5949,19 @@ const INSPIRATION_ROUTE = "/plugins/dsh-image-gen/inspiration";
4841
5949
  const SAVE_WORKSPACE_ROUTE = "/plugins/dsh-image-gen/save-workspace";
4842
5950
  /** Browser route the settings card probes provider connectivity through. */
4843
5951
  const TEST_CONNECTION_ROUTE = "/plugins/dsh-image-gen/test";
5952
+ /** Browser route the workbench infinite canvas pushes live state through. */
5953
+ const CANVAS_STATE_ROUTE = "/plugins/dsh-image-gen/canvas-state";
5954
+ /** Every accepted canvas node kind, for validating untrusted pushes. */
5955
+ const CANVAS_NODE_KINDS = [
5956
+ "image",
5957
+ "draw",
5958
+ "text",
5959
+ "note",
5960
+ "geo",
5961
+ "arrow",
5962
+ "frame",
5963
+ "other"
5964
+ ];
4844
5965
  /** Namespace persisted through DSH Settings. */
4845
5966
  const IMAGE_GENERATION_NAMESPACE = "image-generation";
4846
5967
  /** Supported providers. */
@@ -5175,7 +6296,7 @@ function selectComfyUIWorkflow(active, requested) {
5175
6296
  return workflow;
5176
6297
  }
5177
6298
  //#endregion
5178
- //#region node_modules/.pnpm/@deepseek-ai+dsh-credential_07859f6f9ce036fd17600077e6a695b1/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
6299
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-credential_bc2d3e5c9a179df7ac53b40c354d894e/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
5179
6300
  /**
5180
6301
  * Service Definition for the credential-reference capability seam (`ctx.credentials`). Settings and composition files carry
5181
6302
  * *references* to secrets — environment-variable names — while providers own
@@ -5193,7 +6314,7 @@ const REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
5193
6314
  */
5194
6315
  function credentialRef(value) {
5195
6316
  if (!isCredentialRefName(value)) throw new TypeError(`credential ref "${value}" must match ${String(REF_PATTERN)}`);
5196
- return value;
6317
+ return brandString(value);
5197
6318
  }
5198
6319
  /**
5199
6320
  * Whether a raw string could name a reference at all. Consumers that receive
@@ -5220,15 +6341,738 @@ async function resolveApiKey(ctx, provider) {
5220
6341
  return value.length > 0 ? value : void 0;
5221
6342
  }
5222
6343
  /**
5223
- * Resolve one provider's API key or fail with a user-facing message that says
5224
- * where to configure it. Passing `tool` selects the Agent-facing English
5225
- * wording; without it the message is the browser-facing Chinese one used by
5226
- * the Studio route.
6344
+ * Resolve one provider's API key or fail with a user-facing message that says
6345
+ * where to configure it. Passing `tool` selects the Agent-facing English
6346
+ * wording; without it the message is the browser-facing Chinese one used by
6347
+ * the Studio route.
6348
+ */
6349
+ async function requireApiKey(ctx, provider, tool) {
6350
+ const value = await resolveApiKey(ctx, provider);
6351
+ if (value !== void 0) return value;
6352
+ throw new Error(tool === void 0 ? `${PROVIDER_DISPLAY_NAMES[provider]} 尚未配置 API Key,请先到 设置 > 插件 > 图像生成 配置` : `${tool} requires the ${PROVIDER_DISPLAY_NAMES[provider]} API key; configure it in Settings > Plugins > Image generation.`);
6353
+ }
6354
+ //#endregion
6355
+ //#region lib/types/canvas-state.js
6356
+ /** How many node lines the digest lists before collapsing to "+N more". */
6357
+ const DIGEST_NODE_LIMIT = 24;
6358
+ /** How many selected shapes the digest itemizes before collapsing. */
6359
+ const DIGEST_SELECTION_LIMIT = 12;
6360
+ /**
6361
+ * Identity-bearing selection signature. Count and kinds alone would make
6362
+ * "1 selected image" of the lion and of the monster indistinguishable; the
6363
+ * item summaries (name, attachment id, dimensions, text) keep them apart.
6364
+ */
6365
+ function signatureOf(count, kinds, items) {
6366
+ return JSON.stringify([
6367
+ count,
6368
+ kinds,
6369
+ items
6370
+ ]);
6371
+ }
6372
+ function describeNode(node) {
6373
+ switch (node.kind) {
6374
+ case "image": return [
6375
+ "image",
6376
+ typeof node.width === "number" && typeof node.height === "number" ? `${node.width}x${node.height}` : void 0,
6377
+ typeof node.name === "string" && node.name.length > 0 ? `"${node.name}"` : void 0,
6378
+ typeof node.attachmentId === "string" ? `conversation attachment ${node.attachmentId}` : void 0,
6379
+ typeof node.provider === "string" && node.provider.length > 0 ? `generated by ${node.provider}${typeof node.model === "string" && node.model.length > 0 ? `/${node.model}` : ""}` : void 0,
6380
+ typeof node.prompt === "string" && node.prompt.length > 0 ? `prompt "${node.prompt}"` : void 0
6381
+ ].filter((part) => part !== void 0).join(" ");
6382
+ case "draw": return "draw (freehand stroke)";
6383
+ case "text":
6384
+ case "note": {
6385
+ const preview = typeof node.text === "string" && node.text.length > 0 ? `"${node.text}"` : "(empty)";
6386
+ return `${node.kind} ${preview}`;
6387
+ }
6388
+ case "geo": return typeof node.name === "string" && node.name.length > 0 ? `shape ${node.name}` : "shape";
6389
+ default: return node.kind;
6390
+ }
6391
+ }
6392
+ /** The most recently applied connected entry, or undefined when none is connected. */
6393
+ function freshestEntry(entries) {
6394
+ let latest;
6395
+ for (const entry of entries) if (latest === void 0 || entry.seq > latest.seq) latest = entry;
6396
+ return latest;
6397
+ }
6398
+ /**
6399
+ * Host-side mirror of every currently connected workbench canvas.
6400
+ *
6401
+ * One instance is shared by the canvas-state route, the canvas tools, and the
6402
+ * system-prompt context registration. More than one canvas can be mounted at
6403
+ * once (right-sidebar studio tab, conversation gallery tab); each instance
6404
+ * pushes under its own `clientInstance` id and `latest()` reads the freshest.
6405
+ */
6406
+ var CanvasMirror = class {
6407
+ entries = /* @__PURE__ */ new Map();
6408
+ nextSeq = 0;
6409
+ /**
6410
+ * Apply one validated push. A disconnect removes the instance; an unchanged
6411
+ * selection keeps the previously received screenshot bytes so the model can
6412
+ * still view them while unrelated canvas edits keep flowing in.
6413
+ */
6414
+ apply(push, selectionImage) {
6415
+ if (!push.connected) {
6416
+ this.entries.delete(push.clientInstance);
6417
+ return;
6418
+ }
6419
+ const previous = this.entries.get(push.clientInstance);
6420
+ const selectionCount = push.selection?.count ?? 0;
6421
+ const selectionKinds = [...push.selection?.kinds ?? []];
6422
+ const selectionItems = [...push.selection?.items ?? []];
6423
+ const selectionSignature = signatureOf(selectionCount, selectionKinds, selectionItems);
6424
+ const keepImage = previous !== void 0 && previous.selectionSignature === selectionSignature ? previous.selectionImage : void 0;
6425
+ this.entries.set(push.clientInstance, {
6426
+ clientInstance: push.clientInstance,
6427
+ connected: true,
6428
+ nodeCount: push.nodeCount,
6429
+ nodes: [...push.nodes ?? []],
6430
+ selectionCount,
6431
+ selectionKinds,
6432
+ selectionItems,
6433
+ selectionSignature,
6434
+ ...selectionImage !== void 0 ? { selectionImage } : keepImage !== void 0 ? { selectionImage: keepImage } : {},
6435
+ receivedAt: Date.now(),
6436
+ seq: ++this.nextSeq,
6437
+ updatedAt: push.updatedAt
6438
+ });
6439
+ }
6440
+ /** The freshest connected canvas entry, or undefined. */
6441
+ latest() {
6442
+ return freshestEntry(this.entries.values());
6443
+ }
6444
+ /** The in-memory selection screenshot of the freshest canvas, if present. */
6445
+ latestSelectionImage() {
6446
+ return this.latest()?.selectionImage;
6447
+ }
6448
+ /** Whether any canvas instance is currently connected. */
6449
+ connected() {
6450
+ return this.latest() !== void 0;
6451
+ }
6452
+ /**
6453
+ * Compact one-screen digest of the canvas for the system prompt and tools.
6454
+ * Evaluated per call so the "updated N ago" line stays current between pushes.
6455
+ */
6456
+ digest(now = Date.now()) {
6457
+ const entry = this.latest();
6458
+ if (entry === void 0) return ["Image-gen workbench canvas: not connected.", "No image-gen workbench infinite canvas is open right now. If the user refers to a canvas, drawings, or a sketch, ask them to open the image-gen workbench (gallery tab or right-sidebar studio tab) and switch it to the infinite canvas view."].join(" ");
6459
+ const lines = [];
6460
+ const kindCounts = /* @__PURE__ */ new Map();
6461
+ for (const node of entry.nodes) kindCounts.set(node.kind, (kindCounts.get(node.kind) ?? 0) + 1);
6462
+ const unlisted = Math.max(0, entry.nodeCount - entry.nodes.length);
6463
+ const shapeTotal = `${entry.nodeCount} shape${entry.nodeCount === 1 ? "" : "s"}`;
6464
+ const breakdown = [...kindCounts.entries()].map(([kind, count]) => `${count} ${kind}${count === 1 ? "" : "s"}`).join(", ");
6465
+ const age = Math.max(0, Math.round((now - entry.receivedAt) / 1e3));
6466
+ const ageText = age < 5 ? "just now" : age < 90 ? `${age}s ago` : `${Math.round(age / 60)}min ago`;
6467
+ lines.push(`Image-gen workbench canvas: ${shapeTotal}${breakdown.length > 0 ? ` (${breakdown})` : ""}, updated ${ageText}.`);
6468
+ for (const node of entry.nodes.slice(0, DIGEST_NODE_LIMIT)) lines.push(`- ${describeNode(node)}`);
6469
+ if (entry.nodes.length > DIGEST_NODE_LIMIT) lines.push(`- ... ${entry.nodes.length - DIGEST_NODE_LIMIT} more shapes omitted`);
6470
+ if (unlisted > 0) lines.push(`- (${unlisted} further shapes not itemized)`);
6471
+ if (entry.selectionCount === 0) lines.push("Selection: none. Ask the user to select shapes on the canvas when they want you to work on something specific.");
6472
+ else {
6473
+ const kinds = entry.selectionKinds.length > 0 ? ` (${entry.selectionKinds.join(", ")})` : "";
6474
+ lines.push(`Selection: ${entry.selectionCount} shape${entry.selectionCount === 1 ? "" : "s"}${kinds}.`);
6475
+ for (const item of entry.selectionItems.slice(0, DIGEST_SELECTION_LIMIT)) lines.push(`- selected: ${describeNode(item)}`);
6476
+ if (entry.selectionItems.length > DIGEST_SELECTION_LIMIT) lines.push(`- ... ${entry.selectionItems.length - DIGEST_SELECTION_LIMIT} more selected shapes omitted`);
6477
+ const unlistedSelection = Math.max(0, entry.selectionCount - entry.selectionItems.length);
6478
+ if (unlistedSelection > 0) lines.push(`- (${unlistedSelection} further selected shapes not itemized)`);
6479
+ const screenshot = entry.selectionImage !== void 0 ? "A screenshot of the selection is available; call view_canvas to see it. edit_image with source=canvas_selection sends the full-resolution originals of selected conversation images, plus the screenshot when the selection also contains other content (hand-drawn strokes, pasted images)." : "No selection screenshot yet — the canvas pushes one about a second after the selection settles. edit_image with source=canvas_selection can still use the full-resolution originals of selected conversation images; for anything else ask the user to re-select the shapes.";
6480
+ lines.push(screenshot);
6481
+ }
6482
+ return lines.join("\n");
6483
+ }
6484
+ };
6485
+ //#endregion
6486
+ //#region lib/types/canvas-state-route.js
6487
+ const MAX_CLIENT_INSTANCE_CHARS = 64;
6488
+ const MAX_NODE_TEXT_CHARS = 200;
6489
+ const MAX_ID_CHARS = 256;
6490
+ /** Provider ids come from the shared enum (longest is "openai-compat"). */
6491
+ const MAX_PROVIDER_CHARS = 32;
6492
+ /** Model ids and ComfyUI workflow labels are user-facing strings. */
6493
+ const MAX_MODEL_CHARS = 128;
6494
+ const MAX_NODE_COUNT = 1e5;
6495
+ const MAX_SELECTION_COUNT = 1e3;
6496
+ /** Serve one canvas-state push. */
6497
+ async function serveCanvasState(req, res, deps) {
6498
+ if (req.method !== "POST") return jsonError$4(res, 405, "method-not-allowed");
6499
+ if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$4(res, 415, "json-required");
6500
+ const origin = req.headers.origin;
6501
+ const host = req.headers.host;
6502
+ if (origin !== void 0 && host !== void 0 && origin !== `http://${host}` && origin !== `https://${host}`) return jsonError$4(res, 403, "origin-rejected");
6503
+ let body;
6504
+ try {
6505
+ body = JSON.parse(await readBody$3(req, deps.maxBodyBytes));
6506
+ } catch (error) {
6507
+ return jsonError$4(res, error instanceof Error && error.message === "request-too-large" ? 413 : 400, "invalid-request");
6508
+ }
6509
+ const push = parseCanvasStatePush(body);
6510
+ if (push === void 0) return jsonError$4(res, 400, "invalid-canvas-state");
6511
+ let selectionImage;
6512
+ if (push.selectionImage !== void 0) {
6513
+ const decoded = decodeSelectionImage(push.selectionImage);
6514
+ if (decoded === void 0) return jsonError$4(res, 400, "invalid-selection-image");
6515
+ if (decoded.data.byteLength > deps.maxImageBytes) return jsonError$4(res, 413, "selection-image-too-large");
6516
+ selectionImage = decoded;
6517
+ }
6518
+ deps.mirror.apply(push, selectionImage);
6519
+ res.writeHead(200, {
6520
+ "content-type": "application/json",
6521
+ "cache-control": "no-store"
6522
+ });
6523
+ res.end(JSON.stringify({ ok: true }));
6524
+ }
6525
+ /**
6526
+ * Validate one untrusted canvas-state push at the HTTP boundary.
6527
+ * Returns a normalized copy, or undefined when any field is malformed.
6528
+ * Present-but-malformed known fields reject the whole push: they signal a
6529
+ * client/server version skew rather than a legitimately absent value.
6530
+ */
6531
+ function parseCanvasStatePush(value) {
6532
+ try {
6533
+ return parsePush(value);
6534
+ } catch (error) {
6535
+ if (error instanceof InvalidPush) return void 0;
6536
+ throw error;
6537
+ }
6538
+ }
6539
+ /** Sentinel thrown by field validators; caught at the public parse boundary. */
6540
+ var InvalidPush = class extends Error {};
6541
+ function parsePush(value) {
6542
+ const record = plainRecord(value);
6543
+ if (record === void 0) throw new InvalidPush();
6544
+ const clientInstance = boundedString(record.clientInstance, 1, MAX_CLIENT_INSTANCE_CHARS);
6545
+ if (clientInstance === void 0) throw new InvalidPush();
6546
+ if (typeof record.connected !== "boolean") throw new InvalidPush();
6547
+ if (!nonNegativeInteger$1(record.nodeCount) || record.nodeCount > MAX_NODE_COUNT) throw new InvalidPush();
6548
+ if (!nonNegativeInteger$1(record.updatedAt)) throw new InvalidPush();
6549
+ if (Math.abs(record.updatedAt - Date.now()) > 3e5) throw new InvalidPush();
6550
+ let nodes;
6551
+ if (record.nodes !== void 0) {
6552
+ if (!Array.isArray(record.nodes) || record.nodes.length > 48) throw new InvalidPush();
6553
+ const parsed = [];
6554
+ for (const raw of record.nodes) {
6555
+ const node = parseNodeSummary(raw);
6556
+ if (node === void 0) throw new InvalidPush();
6557
+ parsed.push(node);
6558
+ }
6559
+ nodes = parsed;
6560
+ }
6561
+ let selection;
6562
+ if (record.selection !== void 0) {
6563
+ const raw = plainRecord(record.selection);
6564
+ if (raw === void 0) throw new InvalidPush();
6565
+ if (!nonNegativeInteger$1(raw.count) || raw.count > MAX_SELECTION_COUNT) throw new InvalidPush();
6566
+ if (!Array.isArray(raw.kinds) || raw.kinds.length > 8) throw new InvalidPush();
6567
+ const kinds = [];
6568
+ for (const kind of raw.kinds) {
6569
+ if (typeof kind !== "string" || !CANVAS_NODE_KINDS.includes(kind)) throw new InvalidPush();
6570
+ kinds.push(kind);
6571
+ }
6572
+ let items;
6573
+ if (raw.items !== void 0) {
6574
+ if (!Array.isArray(raw.items) || raw.items.length > 16) throw new InvalidPush();
6575
+ if (raw.items.length > raw.count) throw new InvalidPush();
6576
+ const parsed = [];
6577
+ for (const item of raw.items) {
6578
+ const node = parseNodeSummary(item);
6579
+ if (node === void 0) throw new InvalidPush();
6580
+ parsed.push(node);
6581
+ }
6582
+ items = parsed;
6583
+ }
6584
+ selection = items === void 0 ? {
6585
+ count: raw.count,
6586
+ kinds
6587
+ } : {
6588
+ count: raw.count,
6589
+ kinds,
6590
+ items
6591
+ };
6592
+ }
6593
+ if (record.selectionImage !== void 0 && typeof record.selectionImage !== "string") throw new InvalidPush();
6594
+ return {
6595
+ clientInstance,
6596
+ connected: record.connected,
6597
+ nodeCount: record.nodeCount,
6598
+ ...nodes === void 0 ? {} : { nodes },
6599
+ ...selection === void 0 ? {} : { selection },
6600
+ ...typeof record.selectionImage === "string" ? { selectionImage: record.selectionImage } : {},
6601
+ updatedAt: record.updatedAt
6602
+ };
6603
+ }
6604
+ /** Decode and sanity-check an embedded selection screenshot data URL. */
6605
+ function decodeSelectionImage(dataUrl) {
6606
+ const match = /^data:(image\/(?:png|jpeg|webp));base64,([A-Za-z0-9+/]*={0,2})$/.exec(dataUrl);
6607
+ if (match === null) return void 0;
6608
+ const mediaType = match[1];
6609
+ const base64 = match[2];
6610
+ if (mediaType === void 0 || base64 === void 0) return void 0;
6611
+ try {
6612
+ const buffer = Buffer.from(base64, "base64");
6613
+ if (buffer.byteLength === 0) return void 0;
6614
+ return {
6615
+ data: new Uint8Array(buffer),
6616
+ mediaType
6617
+ };
6618
+ } catch {
6619
+ return;
6620
+ }
6621
+ }
6622
+ function parseNodeSummary(value) {
6623
+ const record = plainRecord(value);
6624
+ if (record === void 0) return void 0;
6625
+ if (typeof record.kind !== "string" || !CANVAS_NODE_KINDS.includes(record.kind)) return void 0;
6626
+ const galleryId = optionalBoundedString(record.galleryId, MAX_ID_CHARS);
6627
+ const attachmentId = optionalBoundedString(record.attachmentId, MAX_ID_CHARS);
6628
+ const name = optionalBoundedString(record.name, MAX_ID_CHARS);
6629
+ const text = optionalBoundedString(record.text, MAX_NODE_TEXT_CHARS);
6630
+ const prompt = optionalBoundedString(record.prompt, 120);
6631
+ const provider = optionalBoundedString(record.provider, MAX_PROVIDER_CHARS);
6632
+ const model = optionalBoundedString(record.model, MAX_MODEL_CHARS);
6633
+ const width = optionalDimension(record.width);
6634
+ const height = optionalDimension(record.height);
6635
+ return {
6636
+ kind: record.kind,
6637
+ ...galleryId === void 0 ? {} : { galleryId },
6638
+ ...attachmentId === void 0 ? {} : { attachmentId },
6639
+ ...name === void 0 ? {} : { name },
6640
+ ...width === void 0 ? {} : { width },
6641
+ ...height === void 0 ? {} : { height },
6642
+ ...text === void 0 ? {} : { text },
6643
+ ...prompt === void 0 ? {} : { prompt },
6644
+ ...provider === void 0 ? {} : { provider },
6645
+ ...model === void 0 ? {} : { model }
6646
+ };
6647
+ }
6648
+ function plainRecord(value) {
6649
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
6650
+ }
6651
+ function boundedString(value, min, max) {
6652
+ if (typeof value !== "string" || value.length < min || value.length > max) return void 0;
6653
+ return value;
6654
+ }
6655
+ /** Absent is fine; present-but-not-a-bounded-string rejects the whole push. */
6656
+ function optionalBoundedString(value, max) {
6657
+ if (value === void 0) return void 0;
6658
+ const parsed = boundedString(value, 0, max);
6659
+ if (parsed === void 0) throw new InvalidPush();
6660
+ return parsed;
6661
+ }
6662
+ /** Absent is fine; present-but-not-a-finite-dimension rejects the whole push. */
6663
+ function optionalDimension(value) {
6664
+ if (value === void 0) return void 0;
6665
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1e6) throw new InvalidPush();
6666
+ return Math.round(value);
6667
+ }
6668
+ function nonNegativeInteger$1(value) {
6669
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
6670
+ }
6671
+ async function readBody$3(req, maxBytes) {
6672
+ const chunks = [];
6673
+ let bytes = 0;
6674
+ for await (const chunk of req) {
6675
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
6676
+ bytes += buffer.byteLength;
6677
+ if (bytes > maxBytes) throw new Error("request-too-large");
6678
+ chunks.push(buffer);
6679
+ }
6680
+ return Buffer.concat(chunks).toString("utf8");
6681
+ }
6682
+ function jsonError$4(res, status, code) {
6683
+ res.writeHead(status, {
6684
+ "content-type": "application/json",
6685
+ "cache-control": "no-store"
6686
+ });
6687
+ res.end(JSON.stringify({ error: code }));
6688
+ }
6689
+ //#endregion
6690
+ //#region lib/types/reference-image.js
6691
+ /** Latest-DSH compatibility boundary for resolving conversation image references. */
6692
+ /**
6693
+ * Resolve one or more edit references while keeping every DSH-specific detail
6694
+ * behind this compatibility boundary. Explicit selectors preserve caller order;
6695
+ * without selectors, all images in the newest image-bearing message are used.
6696
+ */
6697
+ async function resolveReferenceImages(input) {
6698
+ const sourceAttachmentIds = mergeSelectors({
6699
+ single: input.sourceAttachmentId,
6700
+ multiple: input.sourceAttachmentIds,
6701
+ singleName: "source_attachment_id",
6702
+ multipleName: "source_attachment_ids",
6703
+ equal: attachmentIdsEqual
6704
+ });
6705
+ const sourcePaths = mergeSelectors({
6706
+ single: input.sourcePath,
6707
+ multiple: input.sourcePaths,
6708
+ singleName: "source_path",
6709
+ multipleName: "source_paths",
6710
+ equal: (left, right) => left.trim() === right.trim()
6711
+ });
6712
+ if (sourceAttachmentIds !== void 0 && sourcePaths !== void 0) throw new Error("edit_image accepts only one of source_attachment_id, source_attachment_ids, source_path, or source_paths");
6713
+ if (sourcePaths !== void 0) return Promise.all(sourcePaths.map((sourcePath) => readWorkspaceReferenceImage({
6714
+ sourcePath,
6715
+ ...input.agent?.session.header?.cwd === void 0 ? {} : { workspaceRoot: input.agent.session.header.cwd },
6716
+ ...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes },
6717
+ signal: input.signal
6718
+ })));
6719
+ if (input.agent === void 0) throw new Error("edit_image requires an active DSH agent session to resolve a reference image");
6720
+ const refs = findReferenceImages(input.agent.session.deriveMessages(), sourceAttachmentIds);
6721
+ if (refs.length === 0) {
6722
+ if (sourceAttachmentIds !== void 0) throw new Error(`edit_image could not find image attachment ${sourceAttachmentIds[0]} in the current conversation`);
6723
+ throw new Error("edit_image requires an image in the current conversation; upload or generate an image first");
6724
+ }
6725
+ if (sourceAttachmentIds !== void 0 && refs.length !== sourceAttachmentIds.length) {
6726
+ const missing = sourceAttachmentIds.find((id) => !refs.some((ref) => attachmentIdsEqual(String(ref.attachmentId), id)));
6727
+ throw new Error(`edit_image could not find image attachment ${missing ?? "unknown"} in the current conversation`);
6728
+ }
6729
+ return Promise.all(refs.map(async (ref) => {
6730
+ const stored = await input.attachments.readImage(ref, input.signal);
6731
+ if (input.maxBytes !== void 0 && stored.data.byteLength > input.maxBytes) throw new Error(`edit_image source image is too large (${stored.data.byteLength} bytes; maximum ${input.maxBytes})`);
6732
+ return {
6733
+ data: stored.data,
6734
+ mediaType: stored.ref.mediaType
6735
+ };
6736
+ }));
6737
+ }
6738
+ /** Find images in caller order, or every image in the newest image-bearing message. */
6739
+ function findReferenceImages(messages, sourceAttachmentIds) {
6740
+ if (sourceAttachmentIds !== void 0) return sourceAttachmentIds.flatMap((id) => {
6741
+ const ref = findReferenceImage(messages, id);
6742
+ return ref === void 0 ? [] : [ref];
6743
+ });
6744
+ const latestHumanMessage = [...messages].reverse().find((message) => message.source?.kind === "user");
6745
+ if (latestHumanMessage !== void 0) {
6746
+ const refs = collectInBlocks(latestHumanMessage.content);
6747
+ if (refs.length > 0) return refs;
6748
+ }
6749
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
6750
+ const refs = collectInBlocks(messages[index]?.content ?? []);
6751
+ if (refs.length > 0) return refs;
6752
+ }
6753
+ return [];
6754
+ }
6755
+ /**
6756
+ * Read an explicitly named workspace image without exposing filesystem or DSH
6757
+ * details to provider adapters. Both lexical and real-path containment are
6758
+ * enforced so absolute paths, parent traversal, and symlink escapes fail.
6759
+ */
6760
+ async function readWorkspaceReferenceImage(input) {
6761
+ const requested = input.sourcePath.trim();
6762
+ if (requested.length === 0) throw new Error("edit_image source_path must not be empty");
6763
+ if (input.workspaceRoot === void 0) throw new Error("edit_image source_path requires an active DSH session workspace");
6764
+ const root = resolve(input.workspaceRoot);
6765
+ const candidate = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);
6766
+ if (!containsPath$1(root, candidate)) throw new Error("edit_image source_path must stay inside the session workspace: " + requested);
6767
+ let realRoot;
6768
+ let realCandidate;
6769
+ try {
6770
+ [realRoot, realCandidate] = await Promise.all([realpath(root), realpath(candidate)]);
6771
+ } catch (error) {
6772
+ if (error.code === "ENOENT") throw new Error("edit_image could not find workspace image: " + requested);
6773
+ throw error;
6774
+ }
6775
+ if (!containsPath$1(realRoot, realCandidate)) throw new Error("edit_image source_path resolves outside the session workspace: " + requested);
6776
+ const file = await stat(realCandidate);
6777
+ if (!file.isFile()) throw new Error("edit_image source_path is not a file: " + requested);
6778
+ if (input.maxBytes !== void 0 && file.size > input.maxBytes) throw new Error("edit_image source image is too large (" + file.size + " bytes; maximum " + input.maxBytes + ")");
6779
+ const data = await readFile(realCandidate, { signal: input.signal });
6780
+ const mediaType = detectImageMediaType(data);
6781
+ if (mediaType === void 0) throw new Error("edit_image source_path is not a supported PNG, JPEG, WebP, or GIF image: " + requested);
6782
+ return {
6783
+ data: new Uint8Array(data),
6784
+ mediaType
6785
+ };
6786
+ }
6787
+ /** Find the newest matching image in the effective, replacement-aware history. */
6788
+ function findReferenceImage(messages, sourceAttachmentId) {
6789
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
6790
+ const found = findInBlocks(messages[index]?.content ?? [], sourceAttachmentId);
6791
+ if (found !== void 0) return found;
6792
+ }
6793
+ }
6794
+ /** Validate an untrusted serialized image reference at an HTTP/UI boundary. */
6795
+ function parseImageAttachmentRef(value) {
6796
+ const ref = record$7(value);
6797
+ if (ref === void 0) return void 0;
6798
+ if (typeof ref.attachmentId !== "string" || !imageMediaType$4(ref.mediaType)) return void 0;
6799
+ if (!nonNegativeInteger(ref.bytes) || !positiveInteger(ref.width) || !positiveInteger(ref.height)) return void 0;
6800
+ if (ref.name !== void 0 && typeof ref.name !== "string") return void 0;
6801
+ const originalDimensions = ref.originalDimensions === void 0 ? void 0 : parseDimensions(ref.originalDimensions);
6802
+ if (ref.originalDimensions !== void 0 && originalDimensions === void 0) return void 0;
6803
+ return {
6804
+ attachmentId: ref.attachmentId,
6805
+ mediaType: ref.mediaType,
6806
+ bytes: ref.bytes,
6807
+ width: ref.width,
6808
+ height: ref.height,
6809
+ ...typeof ref.name === "string" ? { name: ref.name } : {},
6810
+ ...originalDimensions === void 0 ? {} : { originalDimensions }
6811
+ };
6812
+ }
6813
+ function findInBlocks(blocks, sourceAttachmentId) {
6814
+ for (let index = blocks.length - 1; index >= 0; index -= 1) {
6815
+ const block = blocks[index];
6816
+ if (block === void 0) continue;
6817
+ if (block.type === "image") {
6818
+ if (sourceAttachmentId === void 0 || attachmentIdsEqual(String(block.attachment.attachmentId), sourceAttachmentId)) return block.attachment;
6819
+ continue;
6820
+ }
6821
+ if (block.type === "tool-result") {
6822
+ const nested = findInBlocks(block.content, sourceAttachmentId);
6823
+ if (nested !== void 0) return nested;
6824
+ }
6825
+ }
6826
+ }
6827
+ function parseDimensions(value) {
6828
+ const dimensions = record$7(value);
6829
+ if (dimensions === void 0 || !positiveInteger(dimensions.width) || !positiveInteger(dimensions.height)) return void 0;
6830
+ return {
6831
+ width: dimensions.width,
6832
+ height: dimensions.height
6833
+ };
6834
+ }
6835
+ function imageMediaType$4(value) {
6836
+ return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
6837
+ }
6838
+ function collectInBlocks(blocks) {
6839
+ const refs = [];
6840
+ for (const block of blocks) {
6841
+ if (block.type === "image") refs.push(block.attachment);
6842
+ if (block.type === "tool-result") refs.push(...collectInBlocks(block.content));
6843
+ }
6844
+ return refs;
6845
+ }
6846
+ function attachmentIdsEqual(actual, requested) {
6847
+ if (actual === requested) return true;
6848
+ const actualDigest = sha256Digest(actual);
6849
+ const requestedDigest = sha256Digest(requested);
6850
+ return actualDigest !== void 0 && actualDigest === requestedDigest;
6851
+ }
6852
+ function sha256Digest(value) {
6853
+ return /^(?:sha256:)?([0-9a-f]{64})$/i.exec(value.trim())?.[1]?.toLowerCase();
6854
+ }
6855
+ function mergeSelectors(input) {
6856
+ if (input.multiple === void 0) return input.single === void 0 ? void 0 : [input.single];
6857
+ if (input.multiple.length === 0) {
6858
+ if (input.single !== void 0) return [input.single];
6859
+ throw new Error(`edit_image ${input.multipleName} must not be empty`);
6860
+ }
6861
+ const single = input.single;
6862
+ if (single !== void 0 && !input.multiple.some((value) => input.equal(single, value))) throw new Error(`edit_image ${input.singleName} must also appear in ${input.multipleName} when both are provided`);
6863
+ return input.multiple;
6864
+ }
6865
+ function detectImageMediaType(data) {
6866
+ if (startsWith(data, [
6867
+ 137,
6868
+ 80,
6869
+ 78,
6870
+ 71,
6871
+ 13,
6872
+ 10,
6873
+ 26,
6874
+ 10
6875
+ ])) return "image/png";
6876
+ if (startsWith(data, [
6877
+ 255,
6878
+ 216,
6879
+ 255
6880
+ ])) return "image/jpeg";
6881
+ if (ascii(data, 0, 6) === "GIF87a" || ascii(data, 0, 6) === "GIF89a") return "image/gif";
6882
+ if (ascii(data, 0, 4) === "RIFF" && ascii(data, 8, 4) === "WEBP") return "image/webp";
6883
+ }
6884
+ function startsWith(data, signature) {
6885
+ return signature.every((byte, index) => data[index] === byte);
6886
+ }
6887
+ function ascii(data, offset, length) {
6888
+ return String.fromCharCode(...data.subarray(offset, offset + length));
6889
+ }
6890
+ function containsPath$1(parent, child) {
6891
+ const rel = relative(parent, child);
6892
+ return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute(rel);
6893
+ }
6894
+ function positiveInteger(value) {
6895
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
6896
+ }
6897
+ function nonNegativeInteger(value) {
6898
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
6899
+ }
6900
+ function record$7(value) {
6901
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
6902
+ }
6903
+ //#endregion
6904
+ //#region lib/types/canvas-tools.js
6905
+ /** Register the canvas tools on a context that owns a canvas mirror. */
6906
+ function registerCanvasTools(ctx, mirror, deps) {
6907
+ ctx.tools.register(defineTool({
6908
+ name: "canvas_state",
6909
+ description: "Inspect the current state of the image-gen workbench infinite canvas: what shapes are on it (generated images, hand-drawn strokes, text notes) and what is currently selected. Use whenever the user refers to \"the canvas\", something they drew or placed there, or a selection, and before view_canvas or edit_image with source=canvas_selection.",
6910
+ parameters: {},
6911
+ output: {
6912
+ schema: {
6913
+ type: "object",
6914
+ additionalProperties: false,
6915
+ properties: {
6916
+ digest: {
6917
+ type: "string",
6918
+ required: true
6919
+ },
6920
+ connected: {
6921
+ type: "boolean",
6922
+ required: true
6923
+ },
6924
+ nodeCount: {
6925
+ type: "integer",
6926
+ required: true
6927
+ },
6928
+ selectionCount: {
6929
+ type: "integer",
6930
+ required: true
6931
+ },
6932
+ hasSelectionImage: {
6933
+ type: "boolean",
6934
+ required: true
6935
+ }
6936
+ }
6937
+ },
6938
+ render: (_args, value) => [{
6939
+ type: "text",
6940
+ text: value.digest
6941
+ }]
6942
+ },
6943
+ async execute() {
6944
+ const entry = mirror.latest();
6945
+ return {
6946
+ digest: mirror.digest(),
6947
+ connected: entry !== void 0,
6948
+ nodeCount: entry?.nodeCount ?? 0,
6949
+ selectionCount: entry?.selectionCount ?? 0,
6950
+ hasSelectionImage: mirror.latestSelectionImage() !== void 0
6951
+ };
6952
+ }
6953
+ }));
6954
+ ctx.tools.register(defineTool({
6955
+ name: "view_canvas",
6956
+ description: "View the current selection on the image-gen workbench infinite canvas as an image (a screenshot the canvas pushed for its selection: hand-drawn sketches, annotated images, or any selected shapes). Call it after canvas_state confirms a selection exists. When nothing is selected, ask the user to select the shapes first. The image becomes part of the conversation, so you can reason about it directly.",
6957
+ parameters: {},
6958
+ output: {
6959
+ schema: {
6960
+ type: "object",
6961
+ additionalProperties: false,
6962
+ properties: {
6963
+ digest: {
6964
+ type: "string",
6965
+ required: true
6966
+ },
6967
+ attachment: {
6968
+ type: "object",
6969
+ additionalProperties: false,
6970
+ properties: {
6971
+ attachmentId: {
6972
+ type: "string",
6973
+ required: true
6974
+ },
6975
+ mediaType: {
6976
+ type: "string",
6977
+ required: true
6978
+ },
6979
+ bytes: {
6980
+ type: "integer",
6981
+ required: true
6982
+ },
6983
+ width: {
6984
+ type: "integer",
6985
+ required: true
6986
+ },
6987
+ height: {
6988
+ type: "integer",
6989
+ required: true
6990
+ },
6991
+ name: { type: "string" }
6992
+ }
6993
+ }
6994
+ }
6995
+ },
6996
+ render: (_args, value) => {
6997
+ if (value.attachment === void 0) return [{
6998
+ type: "text",
6999
+ text: `${value.digest}\nNo selection screenshot is available. Ask the user to open the image-gen workbench infinite canvas and select the shapes, then try again.`
7000
+ }];
7001
+ return [{
7002
+ type: "text",
7003
+ text: `${value.digest}\nThe screenshot of the current canvas selection is attached below. Respond to the user without calling read or other tools to locate it.`
7004
+ }, {
7005
+ type: "image",
7006
+ attachment: value.attachment
7007
+ }];
7008
+ }
7009
+ },
7010
+ async execute() {
7011
+ const image = mirror.latestSelectionImage();
7012
+ const attachment = image === void 0 ? void 0 : await deps.persistSelectionImage(image);
7013
+ return {
7014
+ digest: mirror.digest(),
7015
+ ...attachment === void 0 ? {} : { attachment: attachmentMeta$1(attachment) }
7016
+ };
7017
+ }
7018
+ }));
7019
+ }
7020
+ function attachmentMeta$1(ref) {
7021
+ return {
7022
+ attachmentId: ref.attachmentId,
7023
+ mediaType: ref.mediaType,
7024
+ bytes: ref.bytes,
7025
+ width: ref.width,
7026
+ height: ref.height,
7027
+ ...ref.name === void 0 ? {} : { name: ref.name }
7028
+ };
7029
+ }
7030
+ /**
7031
+ * Resolve the mirrored canvas selection as an edit reference image for
7032
+ * edit_image's `canvas_selection` source. The screenshot comes straight from
7033
+ * the mirror's in-memory bytes — identical to what view_canvas shows the
7034
+ * model — and is never persisted here: reference images go to the image
7035
+ * provider, not the conversation.
5227
7036
  */
5228
- async function requireApiKey(ctx, provider, tool) {
5229
- const value = await resolveApiKey(ctx, provider);
5230
- if (value !== void 0) return value;
5231
- throw new Error(tool === void 0 ? `${PROVIDER_DISPLAY_NAMES[provider]} 尚未配置 API Key,请先到 设置 > 插件 > 图像生成 配置` : `${tool} requires the ${PROVIDER_DISPLAY_NAMES[provider]} API key; configure it in Settings > Plugins > Image generation.`);
7037
+ async function resolveCanvasSelectionReferences(input) {
7038
+ const entry = input.mirror.latest();
7039
+ if (entry === void 0) throw new Error("The image-gen workbench canvas is not connected. Ask the user to open the image-gen workbench (gallery tab or right-sidebar studio tab), switch to the infinite canvas view, and select the shapes to edit.");
7040
+ if (entry.selectionCount === 0) throw new Error("The image-gen workbench canvas has no selection to use as a reference image. Ask the user to select the shapes (for example their sketch) on the infinite canvas, then retry.");
7041
+ const attachmentIds = [];
7042
+ let attachmentBacked = 0;
7043
+ for (const item of entry.selectionItems) {
7044
+ if (item.kind !== "image" || typeof item.attachmentId !== "string" || item.attachmentId.length === 0) continue;
7045
+ attachmentIds.push(item.attachmentId);
7046
+ attachmentBacked += 1;
7047
+ }
7048
+ const fullCoverage = entry.selectionItems.length === entry.selectionCount;
7049
+ const originals = [];
7050
+ if (attachmentIds.length > 0 && input.agent !== void 0) {
7051
+ const refs = findReferenceImages(input.agent.session.deriveMessages(), attachmentIds);
7052
+ for (const ref of refs) {
7053
+ const stored = await input.attachments.readImage(ref, input.signal);
7054
+ if (input.maxBytes !== void 0 && stored.data.byteLength > input.maxBytes) throw new Error(`edit_image canvas selection image is too large (${stored.data.byteLength} bytes; maximum ${input.maxBytes})`);
7055
+ originals.push({
7056
+ data: stored.data,
7057
+ mediaType: stored.ref.mediaType
7058
+ });
7059
+ }
7060
+ }
7061
+ if (originals.length > 0 && fullCoverage && attachmentBacked === entry.selectionItems.length && originals.length === attachmentIds.length) return originals;
7062
+ const screenshot = readSelectionScreenshot(entry, input);
7063
+ if (screenshot !== void 0) return [...originals, screenshot];
7064
+ if (originals.length > 0) return originals;
7065
+ throw new Error("The image-gen workbench canvas selection has no usable reference image yet. Ask the user to keep the shapes selected for a moment longer (the canvas pushes a screenshot about a second after the selection settles), then retry.");
7066
+ }
7067
+ /** The mirrored selection screenshot bytes, or undefined when none was pushed. */
7068
+ function readSelectionScreenshot(entry, input) {
7069
+ const image = entry.selectionImage;
7070
+ if (image === void 0) return void 0;
7071
+ if (input.maxBytes !== void 0 && image.data.byteLength > input.maxBytes) throw new Error(`edit_image canvas selection screenshot is too large (${image.data.byteLength} bytes; maximum ${input.maxBytes})`);
7072
+ return {
7073
+ data: image.data,
7074
+ mediaType: image.mediaType
7075
+ };
5232
7076
  }
5233
7077
  //#endregion
5234
7078
  //#region lib/types/comfyui-workflow.js
@@ -5271,8 +7115,8 @@ function prepareComfyUIWorkflow(workflowJson, prompt, seed = randomSeed(), image
5271
7115
  };
5272
7116
  let imageInputs = 0;
5273
7117
  const prepared = Object.fromEntries(Object.entries(workflow).map(([nodeId, value]) => {
5274
- const node = record$7(value);
5275
- const inputs = node === void 0 ? void 0 : record$7(node.inputs);
7118
+ const node = record$6(value);
7119
+ const inputs = node === void 0 ? void 0 : record$6(node.inputs);
5276
7120
  if (inputs === void 0) return [nodeId, value];
5277
7121
  const nextInputs = inject(inputs);
5278
7122
  if (isImagePlaceholder(nextInputs[IMAGE_INPUT_KEY])) {
@@ -5308,7 +7152,7 @@ function parseWorkflow(workflowJson) {
5308
7152
  }
5309
7153
  if (!isRecord(value) || Object.keys(value).length === 0) throw new Error("ComfyUI workflow must be a non-empty API-format JSON object");
5310
7154
  if (!Object.values(value).some((node) => {
5311
- const inputs = record$7(node)?.inputs;
7155
+ const inputs = record$6(node)?.inputs;
5312
7156
  return inputs !== void 0 && containsPromptPlaceholder(inputs);
5313
7157
  })) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} (or ${LEGACY_PROMPT_PLACEHOLDER}) in a text input`);
5314
7158
  return value;
@@ -5324,7 +7168,7 @@ function containsPromptPlaceholder(value) {
5324
7168
  function isRecord(value) {
5325
7169
  return typeof value === "object" && value !== null && !Array.isArray(value);
5326
7170
  }
5327
- function record$7(value) {
7171
+ function record$6(value) {
5328
7172
  return isRecord(value) ? value : void 0;
5329
7173
  }
5330
7174
  //#endregion
@@ -5458,9 +7302,9 @@ async function waitForOutput(baseURL, promptId, signal) {
5458
7302
  });
5459
7303
  const text = await readBoundedText$3(response, MAX_HISTORY_BYTES);
5460
7304
  if (!response.ok) throw new Error(`ComfyUI history request failed (${response.status}): ${redactSecrets(text).slice(0, ERROR_LIMIT$3)}`);
5461
- const entry = record$6(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
7305
+ const entry = record$5(parseJsonRecord(text, "ComfyUI history returned invalid JSON")[promptId]);
5462
7306
  if (entry !== void 0) {
5463
- const status = record$6(entry.status);
7307
+ const status = record$5(entry.status);
5464
7308
  if (status?.status_str === "error") throw new Error(`ComfyUI workflow failed: ${redactSecrets(JSON.stringify(status.messages ?? status)).slice(0, ERROR_LIMIT$3)}`);
5465
7309
  const output = firstOutputImage(entry.outputs);
5466
7310
  if (output !== void 0) return output;
@@ -5470,13 +7314,13 @@ async function waitForOutput(baseURL, promptId, signal) {
5470
7314
  }
5471
7315
  }
5472
7316
  function firstOutputImage(value) {
5473
- const outputs = record$6(value);
7317
+ const outputs = record$5(value);
5474
7318
  if (outputs === void 0) return void 0;
5475
7319
  for (const nodeOutput of Object.values(outputs)) {
5476
- const images = record$6(nodeOutput)?.images;
7320
+ const images = record$5(nodeOutput)?.images;
5477
7321
  if (!Array.isArray(images)) continue;
5478
7322
  for (const image of images) {
5479
- const item = record$6(image);
7323
+ const item = record$5(image);
5480
7324
  if (typeof item?.filename !== "string" || item.filename.length === 0) continue;
5481
7325
  const output = {
5482
7326
  filename: item.filename,
@@ -5497,7 +7341,7 @@ async function downloadOutput(baseURL, output, maxBytes, signal) {
5497
7341
  signal
5498
7342
  });
5499
7343
  if (!response.ok) throw new Error(`ComfyUI image download failed (${response.status})`);
5500
- const mediaType = imageMediaType$4(response.headers.get("content-type")) ?? imageMediaTypeFromName(output.filename);
7344
+ const mediaType = imageMediaType$3(response.headers.get("content-type")) ?? imageMediaTypeFromName(output.filename);
5501
7345
  if (mediaType === void 0) throw new Error("ComfyUI image download returned an unsupported content type");
5502
7346
  return {
5503
7347
  data: await readBoundedBytes$2(response, maxBytes),
@@ -5517,7 +7361,7 @@ function comfyUIBaseURL(value) {
5517
7361
  function endpoint(baseURL, path) {
5518
7362
  return new URL(path, baseURL);
5519
7363
  }
5520
- function record$6(value) {
7364
+ function record$5(value) {
5521
7365
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
5522
7366
  }
5523
7367
  function parseJsonRecord(text, message) {
@@ -5527,11 +7371,11 @@ function parseJsonRecord(text, message) {
5527
7371
  } catch {
5528
7372
  throw new Error(message);
5529
7373
  }
5530
- const parsed = record$6(value);
7374
+ const parsed = record$5(value);
5531
7375
  if (parsed === void 0) throw new Error(message);
5532
7376
  return parsed;
5533
7377
  }
5534
- function imageMediaType$4(value) {
7378
+ function imageMediaType$3(value) {
5535
7379
  const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
5536
7380
  return mediaType === "image/png" || mediaType === "image/jpeg" || mediaType === "image/webp" || mediaType === "image/gif" ? mediaType : void 0;
5537
7381
  }
@@ -5752,13 +7596,13 @@ async function requestGoogleImage(input) {
5752
7596
  };
5753
7597
  }
5754
7598
  function outputImage(value) {
5755
- const interaction = record$5(value);
7599
+ const interaction = record$4(value);
5756
7600
  if (interaction === void 0) return void 0;
5757
7601
  const direct = imageContent(interaction.output_image, false);
5758
7602
  if (direct !== void 0) return direct;
5759
7603
  if (!Array.isArray(interaction.steps)) return void 0;
5760
7604
  for (const step of interaction.steps) {
5761
- const modelOutput = record$5(step);
7605
+ const modelOutput = record$4(step);
5762
7606
  if (modelOutput?.type !== "model_output" || !Array.isArray(modelOutput.content)) continue;
5763
7607
  for (const content of modelOutput.content) {
5764
7608
  const image = imageContent(content, true);
@@ -5767,14 +7611,14 @@ function outputImage(value) {
5767
7611
  }
5768
7612
  }
5769
7613
  function imageContent(value, requiresImageType) {
5770
- const image = record$5(value);
7614
+ const image = record$4(value);
5771
7615
  if (image === void 0 || requiresImageType && image.type !== "image" || typeof image.data !== "string") return void 0;
5772
7616
  return {
5773
7617
  data: image.data,
5774
7618
  mime_type: image.mime_type
5775
7619
  };
5776
7620
  }
5777
- function record$5(value) {
7621
+ function record$4(value) {
5778
7622
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
5779
7623
  }
5780
7624
  function mediaTypeOf(value) {
@@ -5812,220 +7656,6 @@ async function readBoundedText$2(response, maxBytes, label) {
5812
7656
  return new TextDecoder().decode(joined);
5813
7657
  }
5814
7658
  //#endregion
5815
- //#region lib/types/reference-image.js
5816
- /** Latest-DSH compatibility boundary for resolving conversation image references. */
5817
- /**
5818
- * Resolve one or more edit references while keeping every DSH-specific detail
5819
- * behind this compatibility boundary. Explicit selectors preserve caller order;
5820
- * without selectors, all images in the newest image-bearing message are used.
5821
- */
5822
- async function resolveReferenceImages(input) {
5823
- const sourceAttachmentIds = mergeSelectors({
5824
- single: input.sourceAttachmentId,
5825
- multiple: input.sourceAttachmentIds,
5826
- singleName: "source_attachment_id",
5827
- multipleName: "source_attachment_ids",
5828
- equal: attachmentIdsEqual
5829
- });
5830
- const sourcePaths = mergeSelectors({
5831
- single: input.sourcePath,
5832
- multiple: input.sourcePaths,
5833
- singleName: "source_path",
5834
- multipleName: "source_paths",
5835
- equal: (left, right) => left.trim() === right.trim()
5836
- });
5837
- if (sourceAttachmentIds !== void 0 && sourcePaths !== void 0) throw new Error("edit_image accepts only one of source_attachment_id, source_attachment_ids, source_path, or source_paths");
5838
- if (sourcePaths !== void 0) return Promise.all(sourcePaths.map((sourcePath) => readWorkspaceReferenceImage({
5839
- sourcePath,
5840
- ...input.agent?.session.header?.cwd === void 0 ? {} : { workspaceRoot: input.agent.session.header.cwd },
5841
- ...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes },
5842
- signal: input.signal
5843
- })));
5844
- if (input.agent === void 0) throw new Error("edit_image requires an active DSH agent session to resolve a reference image");
5845
- const refs = findReferenceImages(input.agent.session.deriveMessages(), sourceAttachmentIds);
5846
- if (refs.length === 0) {
5847
- if (sourceAttachmentIds !== void 0) throw new Error(`edit_image could not find image attachment ${sourceAttachmentIds[0]} in the current conversation`);
5848
- throw new Error("edit_image requires an image in the current conversation; upload or generate an image first");
5849
- }
5850
- if (sourceAttachmentIds !== void 0 && refs.length !== sourceAttachmentIds.length) {
5851
- const missing = sourceAttachmentIds.find((id) => !refs.some((ref) => attachmentIdsEqual(String(ref.attachmentId), id)));
5852
- throw new Error(`edit_image could not find image attachment ${missing ?? "unknown"} in the current conversation`);
5853
- }
5854
- return Promise.all(refs.map(async (ref) => {
5855
- const stored = await input.attachments.readImage(ref, input.signal);
5856
- if (input.maxBytes !== void 0 && stored.data.byteLength > input.maxBytes) throw new Error(`edit_image source image is too large (${stored.data.byteLength} bytes; maximum ${input.maxBytes})`);
5857
- return {
5858
- data: stored.data,
5859
- mediaType: stored.ref.mediaType
5860
- };
5861
- }));
5862
- }
5863
- /** Find images in caller order, or every image in the newest image-bearing message. */
5864
- function findReferenceImages(messages, sourceAttachmentIds) {
5865
- if (sourceAttachmentIds !== void 0) return sourceAttachmentIds.flatMap((id) => {
5866
- const ref = findReferenceImage(messages, id);
5867
- return ref === void 0 ? [] : [ref];
5868
- });
5869
- const latestHumanMessage = [...messages].reverse().find((message) => message.source?.kind === "user");
5870
- if (latestHumanMessage !== void 0) {
5871
- const refs = collectInBlocks(latestHumanMessage.content);
5872
- if (refs.length > 0) return refs;
5873
- }
5874
- for (let index = messages.length - 1; index >= 0; index -= 1) {
5875
- const refs = collectInBlocks(messages[index]?.content ?? []);
5876
- if (refs.length > 0) return refs;
5877
- }
5878
- return [];
5879
- }
5880
- /**
5881
- * Read an explicitly named workspace image without exposing filesystem or DSH
5882
- * details to provider adapters. Both lexical and real-path containment are
5883
- * enforced so absolute paths, parent traversal, and symlink escapes fail.
5884
- */
5885
- async function readWorkspaceReferenceImage(input) {
5886
- const requested = input.sourcePath.trim();
5887
- if (requested.length === 0) throw new Error("edit_image source_path must not be empty");
5888
- if (input.workspaceRoot === void 0) throw new Error("edit_image source_path requires an active DSH session workspace");
5889
- const root = resolve(input.workspaceRoot);
5890
- const candidate = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);
5891
- if (!containsPath$1(root, candidate)) throw new Error("edit_image source_path must stay inside the session workspace: " + requested);
5892
- let realRoot;
5893
- let realCandidate;
5894
- try {
5895
- [realRoot, realCandidate] = await Promise.all([realpath(root), realpath(candidate)]);
5896
- } catch (error) {
5897
- if (error.code === "ENOENT") throw new Error("edit_image could not find workspace image: " + requested);
5898
- throw error;
5899
- }
5900
- if (!containsPath$1(realRoot, realCandidate)) throw new Error("edit_image source_path resolves outside the session workspace: " + requested);
5901
- const file = await stat(realCandidate);
5902
- if (!file.isFile()) throw new Error("edit_image source_path is not a file: " + requested);
5903
- if (input.maxBytes !== void 0 && file.size > input.maxBytes) throw new Error("edit_image source image is too large (" + file.size + " bytes; maximum " + input.maxBytes + ")");
5904
- const data = await readFile(realCandidate, { signal: input.signal });
5905
- const mediaType = detectImageMediaType(data);
5906
- if (mediaType === void 0) throw new Error("edit_image source_path is not a supported PNG, JPEG, WebP, or GIF image: " + requested);
5907
- return {
5908
- data: new Uint8Array(data),
5909
- mediaType
5910
- };
5911
- }
5912
- /** Find the newest matching image in the effective, replacement-aware history. */
5913
- function findReferenceImage(messages, sourceAttachmentId) {
5914
- for (let index = messages.length - 1; index >= 0; index -= 1) {
5915
- const found = findInBlocks(messages[index]?.content ?? [], sourceAttachmentId);
5916
- if (found !== void 0) return found;
5917
- }
5918
- }
5919
- /** Validate an untrusted serialized image reference at an HTTP/UI boundary. */
5920
- function parseImageAttachmentRef(value) {
5921
- const ref = record$4(value);
5922
- if (ref === void 0) return void 0;
5923
- if (typeof ref.attachmentId !== "string" || !imageMediaType$3(ref.mediaType)) return void 0;
5924
- if (!nonNegativeInteger(ref.bytes) || !positiveInteger(ref.width) || !positiveInteger(ref.height)) return void 0;
5925
- if (ref.name !== void 0 && typeof ref.name !== "string") return void 0;
5926
- const originalDimensions = ref.originalDimensions === void 0 ? void 0 : parseDimensions(ref.originalDimensions);
5927
- if (ref.originalDimensions !== void 0 && originalDimensions === void 0) return void 0;
5928
- return {
5929
- attachmentId: ref.attachmentId,
5930
- mediaType: ref.mediaType,
5931
- bytes: ref.bytes,
5932
- width: ref.width,
5933
- height: ref.height,
5934
- ...typeof ref.name === "string" ? { name: ref.name } : {},
5935
- ...originalDimensions === void 0 ? {} : { originalDimensions }
5936
- };
5937
- }
5938
- function findInBlocks(blocks, sourceAttachmentId) {
5939
- for (let index = blocks.length - 1; index >= 0; index -= 1) {
5940
- const block = blocks[index];
5941
- if (block === void 0) continue;
5942
- if (block.type === "image") {
5943
- if (sourceAttachmentId === void 0 || attachmentIdsEqual(String(block.attachment.attachmentId), sourceAttachmentId)) return block.attachment;
5944
- continue;
5945
- }
5946
- if (block.type === "tool-result") {
5947
- const nested = findInBlocks(block.content, sourceAttachmentId);
5948
- if (nested !== void 0) return nested;
5949
- }
5950
- }
5951
- }
5952
- function parseDimensions(value) {
5953
- const dimensions = record$4(value);
5954
- if (dimensions === void 0 || !positiveInteger(dimensions.width) || !positiveInteger(dimensions.height)) return void 0;
5955
- return {
5956
- width: dimensions.width,
5957
- height: dimensions.height
5958
- };
5959
- }
5960
- function imageMediaType$3(value) {
5961
- return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
5962
- }
5963
- function collectInBlocks(blocks) {
5964
- const refs = [];
5965
- for (const block of blocks) {
5966
- if (block.type === "image") refs.push(block.attachment);
5967
- if (block.type === "tool-result") refs.push(...collectInBlocks(block.content));
5968
- }
5969
- return refs;
5970
- }
5971
- function attachmentIdsEqual(actual, requested) {
5972
- if (actual === requested) return true;
5973
- const actualDigest = sha256Digest(actual);
5974
- const requestedDigest = sha256Digest(requested);
5975
- return actualDigest !== void 0 && actualDigest === requestedDigest;
5976
- }
5977
- function sha256Digest(value) {
5978
- return /^(?:sha256:)?([0-9a-f]{64})$/i.exec(value.trim())?.[1]?.toLowerCase();
5979
- }
5980
- function mergeSelectors(input) {
5981
- if (input.multiple === void 0) return input.single === void 0 ? void 0 : [input.single];
5982
- if (input.multiple.length === 0) {
5983
- if (input.single !== void 0) return [input.single];
5984
- throw new Error(`edit_image ${input.multipleName} must not be empty`);
5985
- }
5986
- const single = input.single;
5987
- if (single !== void 0 && !input.multiple.some((value) => input.equal(single, value))) throw new Error(`edit_image ${input.singleName} must also appear in ${input.multipleName} when both are provided`);
5988
- return input.multiple;
5989
- }
5990
- function detectImageMediaType(data) {
5991
- if (startsWith(data, [
5992
- 137,
5993
- 80,
5994
- 78,
5995
- 71,
5996
- 13,
5997
- 10,
5998
- 26,
5999
- 10
6000
- ])) return "image/png";
6001
- if (startsWith(data, [
6002
- 255,
6003
- 216,
6004
- 255
6005
- ])) return "image/jpeg";
6006
- if (ascii(data, 0, 6) === "GIF87a" || ascii(data, 0, 6) === "GIF89a") return "image/gif";
6007
- if (ascii(data, 0, 4) === "RIFF" && ascii(data, 8, 4) === "WEBP") return "image/webp";
6008
- }
6009
- function startsWith(data, signature) {
6010
- return signature.every((byte, index) => data[index] === byte);
6011
- }
6012
- function ascii(data, offset, length) {
6013
- return String.fromCharCode(...data.subarray(offset, offset + length));
6014
- }
6015
- function containsPath$1(parent, child) {
6016
- const rel = relative(parent, child);
6017
- return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute(rel);
6018
- }
6019
- function positiveInteger(value) {
6020
- return typeof value === "number" && Number.isInteger(value) && value > 0;
6021
- }
6022
- function nonNegativeInteger(value) {
6023
- return typeof value === "number" && Number.isInteger(value) && value >= 0;
6024
- }
6025
- function record$4(value) {
6026
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
6027
- }
6028
- //#endregion
6029
7659
  //#region lib/types/workspace-save.js
6030
7660
  /** Persist one generated image as a file under the session workspace. */
6031
7661
  /** File extension for each supported image media type. */
@@ -6505,10 +8135,11 @@ async function downloadImage$1(url, provider, input) {
6505
8135
  ...input.apiKey === void 0 ? {} : { headers: { authorization: `Bearer ${input.apiKey}` } }
6506
8136
  });
6507
8137
  if (!response.ok) throw new Error(`${provider} image download failed (${response.status})`);
6508
- const mediaType = imageMediaType$2(response.headers.get("content-type"));
8138
+ const data = await readBoundedBytes$1(response, input.maxBytes);
8139
+ const mediaType = detectImageMediaType(data) ?? imageMediaType$2(response.headers.get("content-type"));
6509
8140
  if (mediaType === void 0) throw new Error(`${provider} image download returned unsupported content type`);
6510
8141
  return {
6511
- data: await readBoundedBytes$1(response, input.maxBytes),
8142
+ data,
6512
8143
  mediaType
6513
8144
  };
6514
8145
  }
@@ -17897,6 +19528,8 @@ async function serveStudio(req, res, deps) {
17897
19528
  if (req.method !== "POST") return jsonError$1(res, 405, "method-not-allowed");
17898
19529
  if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return jsonError$1(res, 415, "json-required");
17899
19530
  const controller = new AbortController();
19531
+ const upstreamTimeout = AbortSignal.timeout(15e4);
19532
+ const signal = AbortSignal.any([controller.signal, upstreamTimeout]);
17900
19533
  const onConnectionClose = () => {
17901
19534
  if (!res.writableEnded) controller.abort(/* @__PURE__ */ new Error("The browser closed the image generation request."));
17902
19535
  };
@@ -17912,10 +19545,11 @@ async function serveStudio(req, res, deps) {
17912
19545
  } catch (error) {
17913
19546
  return jsonError$1(res, 400, errorMessage(error, "invalid-request"));
17914
19547
  }
17915
- const output = await deps.generate(input, controller.signal);
19548
+ const output = await deps.generate(input, signal);
17916
19549
  if (!res.headersSent && !res.writableEnded && !res.destroyed) json$1(res, 200, output);
17917
19550
  } catch (error) {
17918
19551
  if (res.headersSent || res.writableEnded || res.destroyed) return;
19552
+ if (upstreamTimeout.aborted) return jsonError$1(res, 504, "上游生成超时(150 秒未返回),请检查网络或代理后重试");
17919
19553
  jsonError$1(res, controller.signal.aborted ? 499 : 502, errorMessage(error, "generation-failed"));
17920
19554
  } finally {
17921
19555
  res.off("close", onConnectionClose);
@@ -18364,6 +19998,7 @@ function providerOverrideOf(value) {
18364
19998
  function apply(ctx, config = {}) {
18365
19999
  let current = () => migrateOpenAICompatConfig(config);
18366
20000
  const knownWorkspaceRoots = /* @__PURE__ */ new Set();
20001
+ const canvasMirror = new CanvasMirror();
18367
20002
  installImageSettings(ctx, config, {
18368
20003
  setSource: (source) => {
18369
20004
  current = () => migrateOpenAICompatConfig(source());
@@ -18422,6 +20057,27 @@ function apply(ctx, config = {}) {
18422
20057
  config: () => current()
18423
20058
  })
18424
20059
  }), "dsh-image-gen: test connection route");
20060
+ ctx.effect(() => ctx.webServer.register({
20061
+ kind: "exact",
20062
+ path: CANVAS_STATE_ROUTE,
20063
+ handler: (req, res) => serveCanvasState(req, res, {
20064
+ mirror: canvasMirror,
20065
+ maxBodyBytes: Math.ceil(ctx.attachments.imageLimits.maxImageBytes * 1.4) + 262144,
20066
+ maxImageBytes: ctx.attachments.imageLimits.maxImageBytes
20067
+ })
20068
+ }), "dsh-image-gen: canvas state route");
20069
+ ctx.inject(["systemPrompt"], (promptCtx) => {
20070
+ promptCtx.systemPrompt.context({
20071
+ name: "dsh-image-gen:canvas",
20072
+ order: 60,
20073
+ text: () => canvasMirror.digest()
20074
+ });
20075
+ });
20076
+ registerCanvasTools(ctx, canvasMirror, { persistSelectionImage: (image) => ctx.attachments.saveImage({
20077
+ data: image.data,
20078
+ mediaType: image.mediaType,
20079
+ name: "canvas-selection"
20080
+ }) });
18425
20081
  ctx.effect(() => ctx.webServer.register({
18426
20082
  kind: "exact",
18427
20083
  path: STUDIO_ROUTE,
@@ -18568,7 +20224,7 @@ function apply(ctx, config = {}) {
18568
20224
  }));
18569
20225
  ctx.tools.register(defineTool({
18570
20226
  name: "edit_image",
18571
- description: "Edit, combine, or restyle existing images with the configured provider. Images attached inline to the latest human message are already readable DSH attachments even when no workspace file exists. In that case, call edit_image immediately with prompt only; NEVER call read_image, glob, or shell to locate them, and NEVER invent @ paths. All inline images will be used in upload order. For specific older conversation images use source_attachment_id or source_attachment_ids; both canonical sha256: IDs and full bare SHA-256 digests are accepted. For files the user explicitly names in the workspace use source_path or source_paths. Provide exactly one selector field. Without a selector, images from the latest human message take priority; only when that message has no images does editing fall back to the newest conversation image.",
20227
+ description: "Edit, combine, or restyle existing images with the configured provider. Images attached inline to the latest human message are already readable DSH attachments even when no workspace file exists. In that case, call edit_image immediately with prompt only; NEVER call read_image, glob, or shell to locate them, and NEVER invent @ paths. All inline images will be used in upload order. For specific older conversation images use source_attachment_id or source_attachment_ids; both canonical sha256: IDs and full bare SHA-256 digests are accepted. For files the user explicitly names in the workspace use source_path or source_paths. For what the user selected or drew on the image-gen workbench canvas (for example a hand-drawn sketch) use source=canvas_selection; canvas_state can verify a selection exists first. Provide exactly one selector field. Without a selector, images from the latest human message take priority; only when that message has no images does editing fall back to the newest conversation image.",
18572
20228
  parameters: {
18573
20229
  prompt: {
18574
20230
  type: "string",
@@ -18593,6 +20249,11 @@ function apply(ctx, config = {}) {
18593
20249
  type: "string",
18594
20250
  description: "Optional model name for this call only, overriding the configured model. Not used by ComfyUI (use workflow instead)."
18595
20251
  },
20252
+ source: {
20253
+ type: "string",
20254
+ enum: ["canvas_selection"],
20255
+ description: "Use the current selection on the image-gen workbench infinite canvas as the reference image(s): full-resolution originals when the selection is conversation-generated images, plus a screenshot of the whole selection when it also contains other content (hand-drawn strokes, pasted images). Choose this when the user refers to what they selected or drew on the canvas; combine with no other selector field."
20256
+ },
18596
20257
  source_attachment_id: {
18597
20258
  type: "string",
18598
20259
  description: "Optional attachment id of a specific image already present in the current conversation."
@@ -18645,7 +20306,15 @@ function apply(ctx, config = {}) {
18645
20306
  output: imageOutput("Edited"),
18646
20307
  async execute(args, exec) {
18647
20308
  const active = resolveProvider(withProviderOverrides(current(), providerOverrideOf(args.provider), args.model));
18648
- const sourceImages = await resolveReferenceImages({
20309
+ const canvasSelection = args.source === "canvas_selection";
20310
+ if (canvasSelection && (args.source_attachment_id !== void 0 || Array.isArray(args.source_attachment_ids) || args.source_path !== void 0 || Array.isArray(args.source_paths))) throw new Error("edit_image source=canvas_selection cannot be combined with source_attachment_id, source_attachment_ids, source_path, or source_paths; provide exactly one selector");
20311
+ const sourceImages = canvasSelection ? await resolveCanvasSelectionReferences({
20312
+ mirror: canvasMirror,
20313
+ attachments: ctx.attachments,
20314
+ ...exec.agent === void 0 ? {} : { agent: exec.agent },
20315
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
20316
+ signal: exec.signal
20317
+ }) : await resolveReferenceImages({
18649
20318
  ...exec.agent === void 0 ? {} : { agent: exec.agent },
18650
20319
  attachments: ctx.attachments,
18651
20320
  ...typeof args.source_attachment_id === "string" ? { sourceAttachmentId: args.source_attachment_id } : {},
@@ -18896,4 +20565,4 @@ function installImageSettings(ctx, config, hooks) {
18896
20565
  ctx.logger.warn("dsh-image-gen: this DSH exposes neither settings API generation; settings UI stays on the composition entry");
18897
20566
  }
18898
20567
  //#endregion
18899
- export { Config, DELETE_ROUTE, IMAGE_ROUTE, INSPIRATION_ROUTE, SAVE_WORKSPACE_ROUTE, STUDIO_ROUTE, TEST_CONNECTION_ROUTE, apply, imageAttachmentFromMeta, inject, name };
20568
+ export { CANVAS_STATE_ROUTE, Config, DELETE_ROUTE, IMAGE_ROUTE, INSPIRATION_ROUTE, SAVE_WORKSPACE_ROUTE, STUDIO_ROUTE, TEST_CONNECTION_ROUTE, apply, imageAttachmentFromMeta, inject, name };