dsh-image-gen 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,10 +1,4866 @@
1
- import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
- import * as dshSettings from "@deepseek-ai/dsh-settings";
3
- import { defineTool } from "@deepseek-ai/dsh-tools";
1
+ import { createRequire } from "node:module";
2
+ import { Context, Service } from "@deepseek-ai/cordis";
4
3
  import z from "@deepseek-ai/schemastery";
5
- import { lstat, mkdir, readFile, readdir, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
6
4
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { lstat, mkdir, readFile, readdir, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
7
6
  import { createHash, randomUUID } from "node:crypto";
7
+ //#region \0rolldown/runtime.js
8
+ var __defProp = Object.defineProperty;
9
+ var __exportAll = (all, no_symbols) => {
10
+ let target = {};
11
+ for (var name in all) __defProp(target, name, {
12
+ get: all[name],
13
+ enumerable: true
14
+ });
15
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
16
+ return target;
17
+ };
18
+ //#endregion
19
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-credential_07859f6f9ce036fd17600077e6a695b1/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
20
+ /**
21
+ * Service Definition for the credential-reference capability seam (`ctx.credentials`). Settings and composition files carry
22
+ * *references* to secrets — environment-variable names — while providers own
23
+ * the actual values and their storage. Consumers resolve a reference once per
24
+ * operation, so a changed credential reaches the next operation without any
25
+ * plugin restart, and configuration surfaces describe a reference without
26
+ * ever seeing its value.
27
+ * @module @deepseek-ai/dsh-credentials
28
+ */
29
+ const REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
30
+ /**
31
+ * Brand a raw string as a {@link CredentialRef}.
32
+ * @param value - candidate reference; a POSIX shell identifier such as `DEEPSEEK_API_KEY`.
33
+ * @returns the branded reference.
34
+ */
35
+ function credentialRef(value) {
36
+ if (!isCredentialRefName(value)) throw new TypeError(`credential ref "${value}" must match ${String(REF_PATTERN)}`);
37
+ return value;
38
+ }
39
+ /**
40
+ * Whether a raw string could name a reference at all. Consumers that receive
41
+ * environment-variable names from somewhere else — a provider library's own
42
+ * ambient discovery, a hook payload — ask this before resolving, because a name
43
+ * outside the grammar has no reference to miss and should read as "not set"
44
+ * rather than as a thrown error.
45
+ * @param value - candidate reference.
46
+ * @returns true when {@link credentialRef} would accept it.
47
+ */
48
+ function isCredentialRefName(value) {
49
+ return REF_PATTERN.test(value);
50
+ }
51
+ //#endregion
52
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-value_e176751d487506fbf1ea91463d745d9b/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
53
+ /**
54
+ * Compare JSON-compatible values structurally.
55
+ * @param a - one JSON-compatible value.
56
+ * @param b - the other JSON-compatible value.
57
+ * @returns whether both values contain the same JSON data.
58
+ */
59
+ function deepEqualJson(a, b) {
60
+ if (a === b) return true;
61
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
62
+ if (Array.isArray(a) || Array.isArray(b)) {
63
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
64
+ return a.every((entry, index) => deepEqualJson(entry, b[index]));
65
+ }
66
+ const left = a;
67
+ const right = b;
68
+ const keys = Object.keys(left);
69
+ if (keys.length !== Object.keys(right).length) return false;
70
+ return keys.every((key) => key in right && deepEqualJson(left[key], right[key]));
71
+ }
72
+ /**
73
+ * Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
74
+ * @param value - value to freeze.
75
+ * @returns the same value after every reachable enumerable child is frozen.
76
+ */
77
+ function deepFreeze$1(value) {
78
+ const seen = /* @__PURE__ */ new WeakSet();
79
+ const pending = [{
80
+ kind: "visit",
81
+ node: value
82
+ }];
83
+ while (pending.length > 0) {
84
+ const task = pending.pop();
85
+ /* v8 ignore next -- the loop condition guarantees one pending task. */
86
+ if (task === void 0) continue;
87
+ if (task.kind === "property") {
88
+ pending.push({
89
+ kind: "visit",
90
+ node: task.source[task.key]
91
+ });
92
+ continue;
93
+ }
94
+ const node = task.node;
95
+ if (node === null || typeof node !== "object") continue;
96
+ if (node instanceof AbortSignal) continue;
97
+ if (seen.has(node)) continue;
98
+ seen.add(node);
99
+ Object.freeze(node);
100
+ const keys = Object.keys(node);
101
+ for (let index = keys.length - 1; index >= 0; index--) {
102
+ const key = keys[index];
103
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
104
+ if (key === void 0) continue;
105
+ pending.push({
106
+ kind: "property",
107
+ source: node,
108
+ key
109
+ });
110
+ }
111
+ }
112
+ return value;
113
+ }
114
+ //#endregion
115
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-settings@0_ee3e2f9fab981d3a5a4a6c717af48d35/node_modules/@deepseek-ai/dsh-settings/lib/index.js
116
+ var lib_exports = /* @__PURE__ */ __exportAll({
117
+ SettingsConflictError: () => SettingsConflictError,
118
+ SettingsProvider: () => SettingsProvider,
119
+ default: () => SettingsProvider,
120
+ redactSecrets: () => redactSecrets
121
+ });
122
+ /**
123
+ * Structural secret redaction for settings values. `role('secret')` fields are
124
+ * removed from a value before it crosses a wire boundary; a sidecar records
125
+ * each schema-declared secret position and whether it currently holds a value,
126
+ * so a configuration surface can render a write-only input without ever
127
+ * receiving the secret itself.
128
+ * @module @deepseek-ai/dsh-settings/redact
129
+ */
130
+ /** Whether a value is a plain data object the walker may recurse into. */
131
+ function isRecord$1(value) {
132
+ return typeof value === "object" && value !== null && !Array.isArray(value);
133
+ }
134
+ function walk(node, value, path, secrets) {
135
+ if (node === void 0) return value;
136
+ if (node.meta?.role === "secret") {
137
+ secrets.push({
138
+ path,
139
+ set: value !== void 0
140
+ });
141
+ return;
142
+ }
143
+ switch (node.type) {
144
+ case "object": {
145
+ const properties = node.dict ?? {};
146
+ const source = isRecord$1(value) ? value : void 0;
147
+ const rebuilt = {};
148
+ if (source !== void 0) for (const [key, entry] of Object.entries(source)) {
149
+ if (key in properties) continue;
150
+ rebuilt[key] = entry;
151
+ }
152
+ for (const [key, child] of Object.entries(properties)) {
153
+ const stripped = walk(child, source?.[key], [...path, key], secrets);
154
+ if (stripped !== void 0) rebuilt[key] = stripped;
155
+ }
156
+ return source === void 0 && Object.keys(rebuilt).length === 0 ? value : rebuilt;
157
+ }
158
+ case "dict": {
159
+ if (!isRecord$1(value)) return value;
160
+ const rebuilt = {};
161
+ for (const [key, entry] of Object.entries(value)) {
162
+ const stripped = walk(node.inner, entry, [...path, key], secrets);
163
+ if (stripped !== void 0) rebuilt[key] = stripped;
164
+ }
165
+ return rebuilt;
166
+ }
167
+ case "array":
168
+ if (!Array.isArray(value)) return value;
169
+ return value.map((entry, index) => walk(node.inner, entry, [...path, String(index)], secrets));
170
+ default: return value;
171
+ }
172
+ }
173
+ /**
174
+ * Remove every `role('secret')` field a schema declares from a value. The
175
+ * walker follows `object`, `dict`, and `array` containers; a secret must be
176
+ * declared directly on a field reachable through those containers (a secret
177
+ * buried inside a union branch or transform is not reachable and must not be
178
+ * modeled that way). The input is never mutated.
179
+ * @param schema - live schemastery schema describing the value.
180
+ * @param value - the value to strip; `undefined` yields an empty record with
181
+ * object-property secret slots still enumerated.
182
+ * @returns the stripped detached value and the ordered secret positions.
183
+ */
184
+ function redactSecrets(schema, value) {
185
+ const secrets = [];
186
+ return {
187
+ value: walk(schema, value, [], secrets),
188
+ secrets
189
+ };
190
+ }
191
+ /**
192
+ * Service Definition for the user-settings capability seam (`ctx.settings`). Providers store one raw document of
193
+ * per-namespace sections; plugins register a namespace schema and read the
194
+ * resolved value, which layers schema defaults, the registrant's composition
195
+ * `base`, and the user document section, in that order.
196
+ * @module @deepseek-ai/dsh-settings
197
+ */
198
+ const NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*$/;
199
+ function parseSettingsNamespace(value) {
200
+ if (!NAMESPACE_PATTERN.test(value)) throw new TypeError(`settings namespace "${value}" must match ${String(NAMESPACE_PATTERN)}`);
201
+ return value;
202
+ }
203
+ /**
204
+ * A write refused because the namespace moved since the caller read it. The
205
+ * Service Definition's serialized write queue orders writes; it cannot tell a fresh writer
206
+ * from one holding a stale snapshot, which is what this reports.
207
+ */
208
+ var SettingsConflictError = class extends Error {
209
+ /** Stable machine code for wire layers mapping this to their own taxonomy. */
210
+ code = "SETTINGS_CONFLICT";
211
+ /** The revision the write expected. */
212
+ expected;
213
+ /** The revision the namespace actually stands at. */
214
+ actual;
215
+ /**
216
+ * @param ns - the namespace whose write was refused.
217
+ * @param expected - the revision the caller sent.
218
+ * @param actual - the revision now stored.
219
+ */
220
+ constructor(ns, expected, actual) {
221
+ super(`settings namespace "${ns}" changed since it was read (expected revision ${String(expected)}, now ${String(actual)})`);
222
+ this.name = "SettingsConflictError";
223
+ this.expected = expected;
224
+ this.actual = actual;
225
+ }
226
+ };
227
+ /** Whether a value is a plain data object (not an array, null, or class instance). */
228
+ function isPlainObject(value) {
229
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
230
+ const proto = Object.getPrototypeOf(value);
231
+ return proto === Object.prototype || proto === null;
232
+ }
233
+ /** Apply one path op to a detached section, returning the next section. */
234
+ function applyPathOp(section, op) {
235
+ const [head, ...rest] = op.path;
236
+ if (head === void 0) {
237
+ if (op.op === "unset") return {};
238
+ if (!isPlainObject(op.value)) throw new TypeError("settings mutate: setting the section root requires a plain object");
239
+ return { ...op.value };
240
+ }
241
+ if (rest.length === 0) {
242
+ if (op.op === "set") return {
243
+ ...section,
244
+ [head]: op.value
245
+ };
246
+ const { [head]: _removed, ...kept } = section;
247
+ return kept;
248
+ }
249
+ const child = section[head];
250
+ if (!isPlainObject(child)) {
251
+ if (op.op === "unset") return section;
252
+ return {
253
+ ...section,
254
+ [head]: applyPathOp({}, {
255
+ ...op,
256
+ path: rest
257
+ })
258
+ };
259
+ }
260
+ return {
261
+ ...section,
262
+ [head]: applyPathOp(child, {
263
+ ...op,
264
+ path: rest
265
+ })
266
+ };
267
+ }
268
+ /** Human label for a value that lossless JSON cannot represent (numbers reject inline). */
269
+ function describeRejected(value) {
270
+ if (value === void 0) return "undefined";
271
+ if (typeof value === "object" && value !== null) {
272
+ const name = Object.getPrototypeOf(value)?.constructor?.name;
273
+ return name === void 0 || name === "Object" ? "a non-plain object" : `a ${name}`;
274
+ }
275
+ return `a ${typeof value}`;
276
+ }
277
+ /**
278
+ * Detach and validate one write input in a single walk before persistence:
279
+ * only JSON data (plain objects, arrays, strings, finite numbers,
280
+ * booleans, `null`) may reach a provider document. `structuredClone` alone
281
+ * would admit Dates, Maps, BigInts, and cycles that YAML/JSON storage then
282
+ * silently distorts on the reload round-trip. `undefined` entries in objects
283
+ * are skipped — the same sparse-patch semantics as {@link mergeLayers} — while
284
+ * an `undefined` array entry is rejected rather than coerced.
285
+ * @param root - plain-object write input (caller-checked).
286
+ * @param reject - builds the validation error from a value label and its `$`-rooted path.
287
+ * @returns the detached JSON-compatible clone.
288
+ */
289
+ function cloneJsonShaped(root, reject) {
290
+ const visiting = /* @__PURE__ */ new WeakSet();
291
+ const clone = (value, path) => {
292
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
293
+ if (typeof value === "number") {
294
+ if (!Number.isFinite(value)) throw reject("a non-finite number", path);
295
+ return value;
296
+ }
297
+ if (Array.isArray(value)) {
298
+ if (visiting.has(value)) throw reject("a circular reference", path);
299
+ visiting.add(value);
300
+ const entries = value.map((entry, index) => clone(entry, `${path}[${index}]`));
301
+ visiting.delete(value);
302
+ return entries;
303
+ }
304
+ if (isPlainObject(value)) {
305
+ if (visiting.has(value)) throw reject("a circular reference", path);
306
+ visiting.add(value);
307
+ const out = {};
308
+ for (const [key, entry] of Object.entries(value)) {
309
+ if (entry === void 0) continue;
310
+ out[key] = clone(entry, `${path}.${key}`);
311
+ }
312
+ visiting.delete(value);
313
+ return out;
314
+ }
315
+ throw reject(describeRejected(value), path);
316
+ };
317
+ return clone(root, "$");
318
+ }
319
+ /**
320
+ * Layer `over` onto `under`: plain objects merge recursively, every other
321
+ * value (arrays included) replaces the lower layer wholesale. `over` never
322
+ * carries `undefined` entries — sections come from parsed documents and write
323
+ * snapshots pass {@link cloneJsonShaped}, which strips them so a sparse patch
324
+ * cannot erase lower keys.
325
+ */
326
+ function mergeLayers(under, over) {
327
+ if (over === void 0) return under;
328
+ if (!isPlainObject(under) || !isPlainObject(over)) return over;
329
+ const merged = { ...under };
330
+ for (const [key, value] of Object.entries(over)) merged[key] = key in merged ? mergeLayers(merged[key], value) : value;
331
+ return merged;
332
+ }
333
+ /**
334
+ * Abstract settings service. Providers implement raw-document storage
335
+ * (`load`/`persist`) and push external changes through {@link Settings.publish};
336
+ * the base class owns namespace registration, resolution, validation, change
337
+ * detection, and the `settings/updated` commit event.
338
+ */
339
+ var SettingsProvider = class extends Service {
340
+ registrations = /* @__PURE__ */ new Map();
341
+ /** Latest published raw document; empty until the provider's first publish. */
342
+ document = {};
343
+ /** Per-namespace write chains; settled tails, so a failure never poisons the queue. */
344
+ writeQueues = /* @__PURE__ */ new Map();
345
+ /** In-flight watcher invocation segments, drained by the dispose teardown. */
346
+ pendingTails = /* @__PURE__ */ new Set();
347
+ /** Set at service dispose: refuse new writes while queued ones drain. */
348
+ stopped = false;
349
+ /** Opaque read of {@link stopped}: control flow cannot narrow it across awaits. */
350
+ isStopped() {
351
+ return this.stopped;
352
+ }
353
+ constructor(ctx) {
354
+ super(ctx, "settings");
355
+ }
356
+ /**
357
+ * Load the provider's document once and publish it before the service
358
+ * becomes injectable, and register the write-drain teardown. Providers with
359
+ * their own init (watchers, connections) delegate here first via
360
+ * `yield* super[Service.init]()`; their disposers then run before the drain.
361
+ */
362
+ async *[Service.init]() {
363
+ yield async () => {
364
+ this.stopped = true;
365
+ await Promise.allSettled([...this.writeQueues.values(), ...this.pendingTails]);
366
+ };
367
+ this.publish(await this.load());
368
+ }
369
+ /**
370
+ * Absolute path of the provider's user-editable document, when its storage
371
+ * is one local file. Configuration surfaces use this only as availability
372
+ * metadata; the guarded open operation resolves the path again Host-side.
373
+ * Non-file providers leave it undefined and expose no open-document affordance.
374
+ * @returns the absolute local document path, or undefined for non-file storage.
375
+ */
376
+ get documentPath() {}
377
+ /**
378
+ * Prepare the provider's user-editable document for a native editor. File
379
+ * providers may materialize an absent document before returning its path;
380
+ * non-file providers return undefined.
381
+ * @returns the absolute local document path, or undefined for non-file storage.
382
+ */
383
+ prepareDocument() {
384
+ return Promise.resolve(this.documentPath);
385
+ }
386
+ /**
387
+ * Register a namespace schema and receive its owner scope. The registration
388
+ * is an effect on the calling plugin's fiber: disposing that fiber removes
389
+ * the namespace and its observers. An invalid stored section fails the
390
+ * registration itself — the earliest point where the schema can judge it.
391
+ * @param ns - unique namespace; duplicate registration fails loud.
392
+ * @param schema - schemastery schema resolving this namespace's value.
393
+ * @param options - composition `base` layer and effect timing.
394
+ * @returns the owner scope for reads, observation, and updates.
395
+ * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
396
+ */
397
+ register(ns, schema, options) {
398
+ const parsedNs = parseSettingsNamespace(ns);
399
+ if (this.registrations.has(parsedNs)) throw new Error(`settings namespace "${parsedNs}" is already registered`);
400
+ const registration = {
401
+ ns: parsedNs,
402
+ schema,
403
+ base: options?.base,
404
+ applies: options?.applies ?? "live",
405
+ ...options?.validate === void 0 ? {} : { validate: options.validate },
406
+ resolved: deepFreeze$1(this.resolve(schema, options?.base, this.section(parsedNs), options?.validate)),
407
+ revision: 0,
408
+ watchers: /* @__PURE__ */ new Set()
409
+ };
410
+ this.ctx.effect(() => {
411
+ this.registrations.set(parsedNs, registration);
412
+ return () => this.registrations.delete(parsedNs);
413
+ }, `settings.register(${JSON.stringify(String(parsedNs))})`);
414
+ return {
415
+ get: () => registration.resolved,
416
+ watch: (callback) => {
417
+ const watcher = {
418
+ callback,
419
+ tail: Promise.resolve(),
420
+ active: true
421
+ };
422
+ registration.watchers.add(watcher);
423
+ return () => {
424
+ watcher.active = false;
425
+ registration.watchers.delete(watcher);
426
+ };
427
+ },
428
+ update: (patch) => this.update(parsedNs, patch),
429
+ replace: (section) => this.replace(parsedNs, section)
430
+ };
431
+ }
432
+ /**
433
+ * Attach one optional-settings consumer to this provider. The consumer
434
+ * registers its composition entry as the base layer while this provider is
435
+ * present, then falls back to that entry if the provider detaches.
436
+ * @param owner - consumer context whose unload suppresses fallback work.
437
+ * @param ns - consumer-owned settings namespace.
438
+ * @param schema - schema resolving the namespace.
439
+ * @param entry - composition entry used as the base and fallback value.
440
+ * @param hooks - source sink, change notification, and optional validation.
441
+ * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
442
+ */
443
+ installSection(owner, ns, schema, entry, hooks) {
444
+ const scope = this.register(ns, schema, {
445
+ base: entry,
446
+ ...hooks.validate === void 0 ? {} : { validate: hooks.validate }
447
+ });
448
+ hooks.setSource(() => scope.get());
449
+ this.ctx.effect(() => () => {
450
+ if (isUnloading(owner)) return;
451
+ hooks.setSource(() => entry);
452
+ hooks.onChange();
453
+ });
454
+ hooks.onChange();
455
+ scope.watch(() => {
456
+ if (isUnloading(owner)) return;
457
+ hooks.onChange();
458
+ });
459
+ }
460
+ /**
461
+ * Describe every registered namespace for configuration surfaces, including
462
+ * the composition `base` and raw user layers so a form can mark which fields
463
+ * the user overrode (presence in `user`) and what a reset returns to.
464
+ * @param options - redaction switch; wire surfaces must redact.
465
+ * @returns one descriptor per registered namespace, in registration order.
466
+ */
467
+ describe(options) {
468
+ return [...this.registrations.values()].map((registration) => {
469
+ let user;
470
+ try {
471
+ user = this.section(registration.ns);
472
+ } catch {
473
+ user = void 0;
474
+ }
475
+ const base = registration.base === void 0 ? void 0 : structuredClone(registration.base);
476
+ const detachedUser = user === void 0 ? void 0 : structuredClone(user);
477
+ const descriptor = {
478
+ ns: registration.ns,
479
+ schema: registration.schema.toJSON(),
480
+ value: registration.resolved,
481
+ revision: registration.revision,
482
+ ...base === void 0 ? {} : { base },
483
+ ...detachedUser === void 0 ? {} : { user: detachedUser },
484
+ applies: registration.applies
485
+ };
486
+ if (options?.redactSecrets !== true) return descriptor;
487
+ const schema = registration.schema;
488
+ const redacted = redactSecrets(schema, registration.resolved);
489
+ return {
490
+ ...descriptor,
491
+ value: redacted.value,
492
+ ...base === void 0 ? {} : { base: redactSecrets(schema, base).value },
493
+ ...detachedUser === void 0 ? {} : { user: redactSecrets(schema, detachedUser).value },
494
+ secrets: redacted.secrets
495
+ };
496
+ });
497
+ }
498
+ /**
499
+ * Read one registered namespace's resolved value.
500
+ * @param ns - the namespace to read.
501
+ * @returns the resolved value, or `undefined` while unregistered.
502
+ * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
503
+ */
504
+ get(ns) {
505
+ return this.registrations.get(parseSettingsNamespace(ns))?.resolved;
506
+ }
507
+ /**
508
+ * Merge a patch into one registered namespace's user layer, validate the
509
+ * resolved candidate, persist through the provider, then commit and emit.
510
+ * A validation failure rejects before anything is persisted. Writes to one
511
+ * namespace are serialized: concurrent updates apply in call order, each
512
+ * merging over the previous write's committed section.
513
+ * @param ns - the registered namespace to update.
514
+ * @param patch - plain-object patch over the user section.
515
+ * @param expectedRevision - the descriptor `revision` the caller read; a
516
+ * namespace that moved past it rejects with {@link SettingsConflictError}.
517
+ * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
518
+ */
519
+ async update(ns, patch, expectedRevision) {
520
+ return this.write(parseSettingsNamespace(ns), patch, "merge", expectedRevision);
521
+ }
522
+ /**
523
+ * Replace one registered namespace's user section wholesale, validate,
524
+ * persist, then commit and emit. Keys absent from `section` fall back to the
525
+ * composition `base` and schema defaults — this is the removal/reset path a
526
+ * merge-only patch cannot express (`replace({})` re-inherits everything).
527
+ * @param ns - the registered namespace to replace.
528
+ * @param section - the complete next user section.
529
+ * @param expectedRevision - the descriptor `revision` the caller read; a
530
+ * namespace that moved past it rejects with {@link SettingsConflictError}.
531
+ * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
532
+ */
533
+ async replace(ns, section, expectedRevision) {
534
+ return this.write(parseSettingsNamespace(ns), section, "replace", expectedRevision);
535
+ }
536
+ /**
537
+ * Apply path-addressed edits to one registered namespace's user section,
538
+ * validate, persist, then commit and emit. The ops are applied to the
539
+ * section as it stands when the write reaches the front of the queue, so a
540
+ * caller never has to restate fields it did not touch — and, crucially,
541
+ * cannot delete fields it never saw. This is the write path for any caller
542
+ * holding a redacted view; `replace` remains the wholesale reset.
543
+ * @param ns - the registered namespace to edit.
544
+ * @param ops - ordered path edits; later ops observe earlier ones.
545
+ * @param expectedRevision - the descriptor `revision` the caller read; a
546
+ * namespace that moved past it rejects with {@link SettingsConflictError}.
547
+ * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
548
+ */
549
+ async mutate(ns, ops, expectedRevision) {
550
+ const parsedNs = parseSettingsNamespace(ns);
551
+ if (!Array.isArray(ops)) throw new TypeError(`settings mutate for "${parsedNs}" must be an array of path ops`);
552
+ for (const op of ops) {
553
+ if (!isPlainObject(op) || op["op"] !== "set" && op["op"] !== "unset") throw new TypeError(`settings mutate for "${parsedNs}" ops must be {op:'set'|'unset', path}`);
554
+ if (!Array.isArray(op["path"]) || op["path"].some((part) => typeof part !== "string")) throw new TypeError(`settings mutate for "${parsedNs}" op paths must be arrays of strings`);
555
+ }
556
+ return this.write(parsedNs, ops, "mutate", expectedRevision);
557
+ }
558
+ /** Validate a write, then queue it on the namespace's serialized write chain. */
559
+ write(ns, input, mode, expectedRevision) {
560
+ const verb = mode === "merge" ? "update" : mode === "replace" ? "replace" : "mutate";
561
+ const registration = this.registrations.get(ns);
562
+ if (registration === void 0) throw new Error(`settings namespace "${ns}" is not registered`);
563
+ if (this.isStopped()) throw new Error(`settings service is disposed: "${ns}" cannot be written`);
564
+ if (!this.writable) throw new Error(`settings provider is read-only: "${ns}" cannot be updated in-process`);
565
+ let payload;
566
+ if (mode === "mutate") payload = { ops: input };
567
+ else {
568
+ if (!isPlainObject(input)) throw new TypeError(`settings ${verb} for "${ns}" must be a plain object`);
569
+ payload = input;
570
+ }
571
+ const snapshot = cloneJsonShaped(payload, (label, path) => /* @__PURE__ */ new TypeError(`settings ${verb} for "${ns}" must contain only JSON-compatible data (found ${label} at ${path})`));
572
+ const run = (this.writeQueues.get(ns) ?? Promise.resolve()).catch(() => void 0).then(async () => {
573
+ if (this.isStopped()) throw new Error(`settings service was disposed before the queued "${ns}" ${verb} ran`);
574
+ if (this.registrations.get(ns) !== registration) throw new Error(`settings namespace "${ns}" registration was disposed before the queued ${verb} ran`);
575
+ const current = this.section(ns) ?? {};
576
+ if (expectedRevision !== void 0 && expectedRevision !== registration.revision) throw new SettingsConflictError(ns, expectedRevision, registration.revision);
577
+ const section = mode === "merge" ? mergeLayers(current, snapshot) : mode === "replace" ? snapshot : snapshot["ops"].reduce(applyPathOp, current);
578
+ const next = deepFreeze$1(this.resolve(registration.schema, registration.base, section, registration.validate));
579
+ await this.persist(ns, section);
580
+ this.document[ns] = section;
581
+ if (this.registrations.get(ns) === registration && !this.isStopped()) {
582
+ this.bumpRevision(registration, current, section);
583
+ this.commit(registration, next, "update");
584
+ }
585
+ });
586
+ this.writeQueues.set(ns, run);
587
+ return run;
588
+ }
589
+ /**
590
+ * Provider hook: commit a complete raw document observed in storage. Each
591
+ * registered namespace re-resolves; an invalid section keeps that
592
+ * namespace's last good value and warns, other namespaces still commit.
593
+ * @param doc - the detached raw document (unregistered sections preserved).
594
+ * @param source - change origin; defaults to `provider`.
595
+ */
596
+ publish(doc, source = "provider") {
597
+ const before = /* @__PURE__ */ new Map();
598
+ for (const registration of this.registrations.values()) try {
599
+ before.set(registration.ns, this.section(registration.ns));
600
+ } catch {
601
+ before.set(registration.ns, void 0);
602
+ }
603
+ this.document = doc;
604
+ for (const registration of this.registrations.values()) {
605
+ let next;
606
+ try {
607
+ next = deepFreeze$1(this.resolve(registration.schema, registration.base, this.section(registration.ns), registration.validate));
608
+ } catch (error) {
609
+ this.ctx.logger.warn("settings: keeping last good \"%s\" after invalid stored section", registration.ns);
610
+ this.ctx.logger.warn(error);
611
+ continue;
612
+ }
613
+ this.bumpRevision(registration, before.get(registration.ns), this.section(registration.ns));
614
+ this.commit(registration, next, source);
615
+ }
616
+ }
617
+ /** Read one namespace's raw user section, rejecting non-object sections. */
618
+ section(ns) {
619
+ const section = this.document[ns];
620
+ if (section === void 0) return void 0;
621
+ if (!isPlainObject(section)) throw new TypeError(`settings section "${ns}" must be an object of keys`);
622
+ return section;
623
+ }
624
+ /** Resolve one namespace value: schema defaults, then `base`, then the user layer. */
625
+ resolve(schema, base, section, validate) {
626
+ const value = schema(mergeLayers(base, section));
627
+ validate?.(value);
628
+ return value;
629
+ }
630
+ /**
631
+ * Advance a namespace's revision when its RAW section changed, and announce
632
+ * it. Deliberately independent of {@link commit}'s resolved-value equality:
633
+ * storing an override equal to the composition base leaves the resolved
634
+ * value alone but changes what the document says, which is exactly what a
635
+ * configuration surface must re-read.
636
+ */
637
+ bumpRevision(registration, before, after) {
638
+ if (deepEqualJson(before, after)) return;
639
+ registration.revision += 1;
640
+ this.emitDocumentUpdated(registration.ns, registration.revision);
641
+ }
642
+ /** Contained fan-out of `settings/document-updated`, mirroring {@link commit}'s. */
643
+ emitDocumentUpdated(ns, revision) {
644
+ let invariantFailure;
645
+ const args = [
646
+ "settings/document-updated",
647
+ ns,
648
+ revision
649
+ ];
650
+ for (const listener of this.ctx.events.dispatch("emit", args)) try {
651
+ const returned = listener(ns, revision);
652
+ if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
653
+ this.warnListenerFailure(ns, error);
654
+ });
655
+ } catch (error) {
656
+ if (error?.code === "INVARIANT") {
657
+ invariantFailure ??= error;
658
+ continue;
659
+ }
660
+ this.warnListenerFailure(ns, error);
661
+ }
662
+ if (invariantFailure !== void 0) throw invariantFailure;
663
+ }
664
+ /** Commit a resolved value when changed: swap, notify watchers, emit the event. */
665
+ commit(registration, next, source) {
666
+ const prev = registration.resolved;
667
+ if (deepEqualJson(next, prev)) return;
668
+ registration.resolved = next;
669
+ for (const watcher of [...registration.watchers]) {
670
+ const segment = watcher.tail.then(() => {
671
+ if (!watcher.active || this.isStopped()) return;
672
+ return watcher.callback(next, prev);
673
+ }).then(() => void 0, (error) => {
674
+ this.warnWatcherFailure(registration.ns, error);
675
+ });
676
+ watcher.tail = segment;
677
+ this.pendingTails.add(segment);
678
+ segment.then(() => this.pendingTails.delete(segment));
679
+ }
680
+ let invariantFailure;
681
+ const args = [
682
+ "settings/updated",
683
+ registration.ns,
684
+ next,
685
+ prev,
686
+ source
687
+ ];
688
+ for (const listener of this.ctx.events.dispatch("emit", args)) try {
689
+ const returned = listener(registration.ns, next, prev, source);
690
+ if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
691
+ this.warnListenerFailure(registration.ns, error);
692
+ });
693
+ } catch (error) {
694
+ if (error?.code === "INVARIANT") {
695
+ invariantFailure ??= error;
696
+ continue;
697
+ }
698
+ this.warnListenerFailure(registration.ns, error);
699
+ }
700
+ if (invariantFailure !== void 0) throw invariantFailure;
701
+ }
702
+ /** Contained-watcher diagnostic shared by the sync and async failure paths. */
703
+ warnWatcherFailure(ns, error) {
704
+ this.ctx.logger.warn("settings: watcher for \"%s\" failed", ns);
705
+ this.ctx.logger.warn(error);
706
+ }
707
+ /** Contained-listener diagnostic shared by the sync and async failure paths. */
708
+ warnListenerFailure(ns, error) {
709
+ this.ctx.logger.warn("settings: a settings/updated listener for \"%s\" failed", ns);
710
+ this.ctx.logger.warn(error);
711
+ }
712
+ };
713
+ /**
714
+ * Value mirror of the `FiberState` members {@link isUnloading} compares
715
+ * against: a const enum has no runtime object to import, and the value is
716
+ * needed at runtime (same rationale as the CLI boot driver's mirror).
717
+ */
718
+ const FIBER_DISPOSED = 4;
719
+ const FIBER_UNLOADING = 5;
720
+ /** Whether the consumer's own fiber is tearing down (not just losing the settings service). */
721
+ function isUnloading(ctx) {
722
+ const state = ctx.fiber.state;
723
+ return state === FIBER_UNLOADING || state === FIBER_DISPOSED;
724
+ }
725
+ //#endregion
726
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-scope@0.1._7f15c7653b2619acc206cea5c06e49b3/node_modules/@deepseek-ai/dsh-scope/lib/index.js
727
+ /**
728
+ * Shared insertion-ordered storage and effect ownership for scope-aware registries.
729
+ *
730
+ * @module @deepseek-ai/dsh-scope
731
+ */
732
+ /**
733
+ * Insertion-ordered named entries with caller-owned duplicate diagnostics.
734
+ *
735
+ * Values are borrowed. Iterators are live within one nonempty table
736
+ * generation; draining the table detaches them from later insertions. Each
737
+ * successful insertion returns an idempotent undo for that exact entry.
738
+ */
739
+ var NamedEntries = class {
740
+ duplicateError;
741
+ data = /* @__PURE__ */ new Map();
742
+ constructor(duplicateError) {
743
+ this.duplicateError = duplicateError;
744
+ }
745
+ /**
746
+ * Insert one unique name.
747
+ * @param name - name unique within this table.
748
+ * @param value - borrowed value to retain.
749
+ * @returns an idempotent undo that removes only this insertion.
750
+ */
751
+ insert(name, value) {
752
+ const data = this.data;
753
+ if (data.has(name)) throw this.duplicateError(name);
754
+ data.set(name, value);
755
+ let active = true;
756
+ return () => {
757
+ if (!active) return;
758
+ active = false;
759
+ data.delete(name);
760
+ if (data.size === 0 && this.data === data) this.data = /* @__PURE__ */ new Map();
761
+ };
762
+ }
763
+ /**
764
+ * Read one named value.
765
+ * @param name - name to resolve.
766
+ * @returns the retained value, or `undefined` when absent.
767
+ */
768
+ get(name) {
769
+ return this.data.get(name);
770
+ }
771
+ /**
772
+ * Test one name for membership.
773
+ * @param name - name to test.
774
+ * @returns whether the table contains that name.
775
+ */
776
+ has(name) {
777
+ return this.data.has(name);
778
+ }
779
+ /**
780
+ * Iterate live names in insertion order.
781
+ * @returns the native live key iterator.
782
+ */
783
+ keys() {
784
+ return this.data.keys();
785
+ }
786
+ /**
787
+ * Iterate live entries in insertion order.
788
+ * @returns the native live entry iterator.
789
+ */
790
+ entries() {
791
+ return this.data.entries();
792
+ }
793
+ /**
794
+ * Iterate live values in insertion order.
795
+ * @returns the native live value iterator.
796
+ */
797
+ values() {
798
+ return this.data.values();
799
+ }
800
+ /**
801
+ * Test whether this table has no entries.
802
+ * @returns whether the table is empty.
803
+ */
804
+ isEmpty() {
805
+ return this.data.size === 0;
806
+ }
807
+ };
808
+ /**
809
+ * Insertion-ordered anonymous entries with independent registration identity.
810
+ *
811
+ * Equal values remain separate registrations. Values are borrowed, and
812
+ * iterators are live within one nonempty table generation; draining the table
813
+ * detaches them from later appends.
814
+ */
815
+ var AnonymousEntries = class {
816
+ data = /* @__PURE__ */ new Map();
817
+ /**
818
+ * Append one independently owned value.
819
+ * @param value - borrowed value to retain.
820
+ * @returns an idempotent undo for this exact append.
821
+ */
822
+ append(value) {
823
+ const data = this.data;
824
+ const key = Symbol();
825
+ data.set(key, value);
826
+ let active = true;
827
+ return () => {
828
+ if (!active) return;
829
+ active = false;
830
+ data.delete(key);
831
+ if (data.size === 0 && this.data === data) this.data = /* @__PURE__ */ new Map();
832
+ };
833
+ }
834
+ /**
835
+ * Iterate live values in insertion order.
836
+ * @returns the native live value iterator.
837
+ */
838
+ values() {
839
+ return this.data.values();
840
+ }
841
+ /**
842
+ * Test whether this table has no entries.
843
+ * @returns whether the table is empty.
844
+ */
845
+ isEmpty() {
846
+ return this.data.size === 0;
847
+ }
848
+ };
849
+ /**
850
+ * Own the global and exact-scope layers for one registry.
851
+ *
852
+ * Reads never create scoped layers. Registrations derive both visibility and
853
+ * effect ownership from the supplied Cordis context, collect undo before
854
+ * notification, and reclaim only a completely empty aggregate layer.
855
+ */
856
+ var ScopedLayers = class {
857
+ createLayer;
858
+ onChange;
859
+ /** The eagerly constructed context-global layer. */
860
+ global;
861
+ scoped = /* @__PURE__ */ new Map();
862
+ constructor(createLayer, onChange) {
863
+ this.createLayer = createLayer;
864
+ this.onChange = onChange;
865
+ this.global = createLayer(void 0);
866
+ }
867
+ /**
868
+ * Read an existing exact-scope overlay. Deliberately chain-blind: callers
869
+ * addressing one scope's OWN contributions (its restrictions, its guards)
870
+ * must not silently pick up an ancestor's — use {@link chainLayers} where
871
+ * inheritance is the point.
872
+ * @param scope - exact scope key; `undefined` denotes no overlay.
873
+ * @returns the existing scoped layer, or `undefined` without creating one.
874
+ */
875
+ peek(scope) {
876
+ if (scope === void 0) return void 0;
877
+ return this.scoped.get(scope);
878
+ }
879
+ /**
880
+ * Existing overlays along the scope's parent chain ({@link scopeChainOf}),
881
+ * farthest ancestor first and the exact scope last, so a caller layering
882
+ * them in order gives the nearest scope the final word.
883
+ * @param scope - viewing scope, or `undefined` for no overlays.
884
+ * @returns the existing layers, nearest last; absent overlays are skipped.
885
+ */
886
+ chainLayers(scope) {
887
+ const layers = [];
888
+ for (const key of scopeChainOf(scope).reverse()) {
889
+ const layer = this.scoped.get(key);
890
+ if (layer !== void 0) layers.push(layer);
891
+ }
892
+ return layers;
893
+ }
894
+ /**
895
+ * Materialize global named entries followed by scope-chain shadows,
896
+ * farthest ancestor first, so the nearest scope's entry wins a name.
897
+ * @param scope - viewing scope, or `undefined` for the global view.
898
+ * @param pick - select the named table from a layer.
899
+ * @returns an insertion-ordered effective map.
900
+ */
901
+ merge(scope, pick) {
902
+ const merged = new Map(pick(this.global).entries());
903
+ for (const layer of this.chainLayers(scope)) for (const [name, value] of pick(layer).entries()) merged.set(name, value);
904
+ return merged;
905
+ }
906
+ /**
907
+ * Attach one synchronous layer mutation to its registration context.
908
+ * @param ctx - context that determines both scope visibility and effect ownership.
909
+ * @param action - atomic mutation returning its synchronous undo.
910
+ * @param options - Cordis effect label and optional change notification.
911
+ * @returns the exact disposer returned by `ctx.effect()`.
912
+ */
913
+ effect(ctx, action, options) {
914
+ const scope = scopeOf(ctx);
915
+ const notify = options.notify ?? true;
916
+ return ctx.effect(function* () {
917
+ let layer;
918
+ let created = false;
919
+ if (scope === void 0) layer = this.global;
920
+ else {
921
+ const existing = this.scoped.get(scope);
922
+ if (existing === void 0) {
923
+ layer = this.createLayer(scope);
924
+ this.scoped.set(scope, layer);
925
+ created = true;
926
+ } else layer = existing;
927
+ }
928
+ let undo;
929
+ try {
930
+ undo = action(layer);
931
+ } catch (error) {
932
+ if (scope !== void 0 && created && layer.isEmpty()) this.scoped.delete(scope);
933
+ throw error;
934
+ }
935
+ yield () => {
936
+ undo();
937
+ if (scope !== void 0 && layer.isEmpty()) this.scoped.delete(scope);
938
+ if (notify) this.onChange();
939
+ };
940
+ if (notify) this.onChange();
941
+ }.bind(this), options.label);
942
+ }
943
+ };
944
+ /**
945
+ * Scoped-context primitive: mint a Cordis context that tags registrations with
946
+ * an opaque identity and build routing-only event carriers for that identity.
947
+ *
948
+ * @module @deepseek-ai/dsh-scope
949
+ */
950
+ /** Context tag written by {@link createScope}. */
951
+ const kScope = Symbol("dsh.scope");
952
+ /** The key associated with each carrier. Presence distinguishes an unkeyed carrier from a non-carrier. */
953
+ const carrierKeys = /* @__PURE__ */ new WeakMap();
954
+ /**
955
+ * The enclosing scope of each key. One relation powers both directions of
956
+ * scope nesting: registration views inherit DOWN the chain (a child scope
957
+ * sees its ancestors' layers — {@link ScopedLayers}), and event admission
958
+ * extends UP it (a listener tagged with an ancestor receives events dispatched
959
+ * to a descendant key — {@link scopeTarget}).
960
+ */
961
+ const scopeParents = /* @__PURE__ */ new WeakMap();
962
+ /**
963
+ * The chain from a key to its root ancestor.
964
+ * @param key - the starting key, or `undefined` for the empty chain.
965
+ * @returns keys nearest-first: `[key, parent, grandparent, …]`.
966
+ */
967
+ function scopeChainOf(key) {
968
+ const chain = [];
969
+ for (let cursor = key; cursor !== void 0; cursor = scopeParents.get(cursor)) chain.push(cursor);
970
+ return chain;
971
+ }
972
+ /**
973
+ * Read the nearest scope tag inherited by a context.
974
+ * @param ctx - context to inspect.
975
+ * @returns its scope key, or `undefined` for an unscoped context.
976
+ */
977
+ function scopeOf(ctx) {
978
+ return ctx[kScope];
979
+ }
980
+ /**
981
+ * Build an opaque receiver that preserves the base filter, admits untagged
982
+ * listeners globally, and admits tagged listeners for a matching key or any
983
+ * of its ancestors ({@link bindScopeParent}): a listener owned by an enclosing
984
+ * scope receives every descendant scope's events, which is what lets one
985
+ * standing composition observe each of the agents composed under it. A tag
986
+ * BELOW the dispatch key stays excluded — events flow up the chain, never
987
+ * down.
988
+ * @param base - subject or service whose existing Cordis filter is preserved.
989
+ * @param key - routed scope identity, or `undefined` for an unscoped subject.
990
+ * @returns a carrier whose subject remains available only through event arguments.
991
+ */
992
+ function scopeTarget(base, key) {
993
+ const baseFilter = base[Context.filter];
994
+ const carrier = { [Context.filter](ctx) {
995
+ if (baseFilter !== void 0 && !baseFilter.call(base, ctx)) return false;
996
+ const tag = scopeOf(ctx);
997
+ if (tag === void 0) return true;
998
+ for (let cursor = key; cursor !== void 0; cursor = scopeParents.get(cursor)) if (cursor === tag) return true;
999
+ return false;
1000
+ } };
1001
+ carrierKeys.set(carrier, key);
1002
+ return carrier;
1003
+ }
1004
+ //#endregion
1005
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-timeout@0._13401da53fdd229a45877adbff64345d/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
1006
+ /** Largest delay Node schedules without clamping it to one millisecond. */
1007
+ const MAX_TIMER_DELAY_MS = 2147483647;
1008
+ //#endregion
1009
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.1-_de8559ed89b7370843bac1bad71a6196/node_modules/@deepseek-ai/dsh-llm/lib/index.js
1010
+ /**
1011
+ * dsh-llm's owned branded ids: tool-call correlation and provider request
1012
+ * diagnostics.
1013
+ *
1014
+ * The `Branded<B>` primitive itself lives in `@deepseek-ai/dsh-brand` (a
1015
+ * zero-dependency type-only package) so every owner of a cross-boundary id can
1016
+ * brand it without depending on dsh-llm; see that package's README for the
1017
+ * nominal-typing policy.
1018
+ *
1019
+ * @module @deepseek-ai/dsh-llm/brand
1020
+ */
1021
+ /**
1022
+ * Brand a message identifier.
1023
+ * @param id - the opaque message identifier.
1024
+ * @returns the same string, branded; no validation is performed.
1025
+ */
1026
+ function MessageId(id) {
1027
+ return id;
1028
+ }
1029
+ /**
1030
+ * Brand a string as a {@link CallId}.
1031
+ * @param id - the provider-issued (or synthesized) call id.
1032
+ * @returns the same string, branded; no validation is performed.
1033
+ */
1034
+ function CallId(id) {
1035
+ return id;
1036
+ }
1037
+ /**
1038
+ * Deep-freeze a value in place with an iterative traversal, guarding cycles,
1039
+ * so later mutation throws without imposing a JavaScript call-stack depth cap.
1040
+ * {@link AbortSignal} objects are deliberately skipped because they are the
1041
+ * request's live cancellation channel and freezing them breaks abort.
1042
+ * @param value - the value to freeze in place.
1043
+ * @returns the same value, frozen.
1044
+ */
1045
+ function deepFreeze(value) {
1046
+ const seen = /* @__PURE__ */ new WeakSet();
1047
+ const pending = [{
1048
+ kind: "visit",
1049
+ node: value
1050
+ }];
1051
+ while (pending.length > 0) {
1052
+ const task = pending.pop();
1053
+ /* v8 ignore next -- the loop condition guarantees one pending task. */
1054
+ if (task === void 0) continue;
1055
+ if (task.kind === "property") {
1056
+ pending.push({
1057
+ kind: "visit",
1058
+ node: task.source[task.key]
1059
+ });
1060
+ continue;
1061
+ }
1062
+ const node = task.node;
1063
+ if (node === null || typeof node !== "object") continue;
1064
+ if (node instanceof AbortSignal) continue;
1065
+ if (seen.has(node)) continue;
1066
+ seen.add(node);
1067
+ Object.freeze(node);
1068
+ const keys = Object.keys(node);
1069
+ for (let index = keys.length - 1; index >= 0; index--) {
1070
+ const key = keys[index];
1071
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
1072
+ if (key === void 0) continue;
1073
+ pending.push({
1074
+ kind: "property",
1075
+ source: node,
1076
+ key
1077
+ });
1078
+ }
1079
+ }
1080
+ return value;
1081
+ }
1082
+ /**
1083
+ * Detach and deep-freeze a message whose identity already exists.
1084
+ * @param message - complete message, including its stable identity.
1085
+ * @returns an immutable snapshot that preserves the identity.
1086
+ */
1087
+ function freezeMessage(message) {
1088
+ return deepFreeze(structuredClone(message));
1089
+ }
1090
+ /**
1091
+ * Create one identified message and freeze it before publication.
1092
+ * @param input - complete role, content, and source for a new message.
1093
+ * @returns an immutable message with a fresh stable identity.
1094
+ */
1095
+ function createMessage(input) {
1096
+ return freezeMessage({
1097
+ ...input,
1098
+ id: MessageId(crypto.randomUUID())
1099
+ });
1100
+ }
1101
+ /**
1102
+ * Create one identified user-role message and freeze it before publication.
1103
+ * @param input - complete content and source for a new user message.
1104
+ * @returns an immutable user message with a fresh stable identity.
1105
+ */
1106
+ function createUserMessage(input) {
1107
+ return createMessage({
1108
+ ...input,
1109
+ role: "user"
1110
+ });
1111
+ }
1112
+ /**
1113
+ * Harness error base with a stable machine-routable code and chained cause.
1114
+ * Package errors extend it so tool results and replay can retain failure class.
1115
+ * @module @deepseek-ai/dsh-llm/error
1116
+ */
1117
+ /**
1118
+ * Base class for all harness errors. Carries a `code` (stable, programmatic —
1119
+ * e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
1120
+ * human-readable `message`, and supports `cause` chaining via the standard
1121
+ * `ErrorOptions`. `name` defaults to the subclass constructor name.
1122
+ */
1123
+ var HarnessError = class extends Error {
1124
+ /** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
1125
+ code;
1126
+ constructor(message, code, options) {
1127
+ super(message, options);
1128
+ this.code = code;
1129
+ this.name = new.target.name;
1130
+ }
1131
+ };
1132
+ /**
1133
+ * Canonical provider-neutral code for a response that completed normally but
1134
+ * carried no content blocks at all. Providers occasionally emit a degenerate
1135
+ * completion (a terminal stop with zero output); adapters classify it as this
1136
+ * failure instead of yielding an empty assistant message, because an empty
1137
+ * message silently ends the turn with nothing for the user or the loop to act
1138
+ * on. The attempt produced nothing durable, so retry policy treats it as safe
1139
+ * to repeat.
1140
+ */
1141
+ const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
1142
+ new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
1143
+ new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
1144
+ new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
1145
+ /**
1146
+ * Provider-owned request-retry policy configuration and resolution.
1147
+ *
1148
+ * Adapters expose one resolved policy per registered provider route; the
1149
+ * optional dsh-llm-retry plugin executes it on the agent's failed-step extension point.
1150
+ *
1151
+ * @module @deepseek-ai/dsh-llm/retry-policy
1152
+ */
1153
+ const DEFAULT_MAX_RETRIES = 5;
1154
+ const DEFAULT_INITIAL_DELAY_MS = 500;
1155
+ const DEFAULT_MAX_DELAY_MS = 1e4;
1156
+ const DEFAULT_JITTER_RATIO = .1;
1157
+ const DEFAULT_RETRYABLE_CODES = Object.freeze([
1158
+ EMPTY_RESPONSE_CODE,
1159
+ "RATE_LIMIT",
1160
+ "SERVER",
1161
+ "TIMEOUT",
1162
+ "TRANSPORT"
1163
+ ]);
1164
+ const backoffSchema = z.object({
1165
+ initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
1166
+ maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
1167
+ jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
1168
+ });
1169
+ const normalPolicySchema = z.object({
1170
+ mode: z.const("normal").required(),
1171
+ maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
1172
+ retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
1173
+ backoff: backoffSchema
1174
+ });
1175
+ const alwaysPolicySchema = z.object({
1176
+ mode: z.const("always").required(),
1177
+ backoff: backoffSchema
1178
+ });
1179
+ z.union([normalPolicySchema, alwaysPolicySchema]);
1180
+ /**
1181
+ * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
1182
+ * adapters from drifting. See
1183
+ * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
1184
+ *
1185
+ * App-attribution vocabulary for provider requests.
1186
+ * @module @deepseek-ai/dsh-llm/attribution
1187
+ */
1188
+ const { version } = createRequire(import.meta.url)("../package.json");
1189
+ /**
1190
+ * Exhaustiveness helper for closed core unions. Use {@link assertNever} at the default branch so a
1191
+ * new variant fails compilation at every required handler. Do not use it for declaration-merged
1192
+ * unions such as session events or content blocks: handle known variants and explicitly fall
1193
+ * through because plugins may add valid unknown cases.
1194
+ * @module @deepseek-ai/dsh-llm/never
1195
+ */
1196
+ /**
1197
+ * Mark an unreachable closed-union branch. A newly unhandled typed variant fails at the call site;
1198
+ * a value that escaped its type throws with diagnostics at runtime.
1199
+ * @param value - the impossible value; typed `never` so an unhandled variant fails compilation at the call site.
1200
+ * @param context - optional label (e.g. the switch site) prefixed into the throw message.
1201
+ * @returns never — it always throws, with the offending value JSON-rendered in the message.
1202
+ */
1203
+ function assertNever(value, context) {
1204
+ const rendered = JSON.stringify(value) ?? String(value);
1205
+ throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
1206
+ }
1207
+ //#endregion
1208
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-session@0._f3475d2e0abd209e639ed06267209bdd/node_modules/@deepseek-ai/dsh-session/lib/index.js
1209
+ /** Lossless-JSON validation and detached snapshots for durable session data. @module @deepseek-ai/dsh-session/json */
1210
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
1211
+ function hasIntrinsicConstructor$1(prototype, name) {
1212
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
1213
+ if (typeof constructor !== "function") return false;
1214
+ try {
1215
+ return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
1216
+ } catch {
1217
+ return false;
1218
+ }
1219
+ }
1220
+ /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
1221
+ function isIntrinsicObjectPrototype$1(value) {
1222
+ return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor$1(value, "Object");
1223
+ }
1224
+ /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
1225
+ function hasPlainArrayPrototype$1(value) {
1226
+ const prototype = Object.getPrototypeOf(value);
1227
+ if (!Array.isArray(prototype) || !hasIntrinsicConstructor$1(prototype, "Array")) return false;
1228
+ const objectPrototype = Object.getPrototypeOf(prototype);
1229
+ return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype$1(objectPrototype);
1230
+ }
1231
+ /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
1232
+ function hasPlainObjectPrototype(value) {
1233
+ const prototype = Object.getPrototypeOf(value);
1234
+ return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype$1(prototype);
1235
+ }
1236
+ /** Return every JSON-visible object key, or reject own data JSON would discard. */
1237
+ function enumerableStringKeys(value) {
1238
+ const keys = Reflect.ownKeys(value);
1239
+ if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
1240
+ return keys;
1241
+ }
1242
+ /** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
1243
+ function walkJsonValue(value, detach) {
1244
+ const ancestors = /* @__PURE__ */ new Set();
1245
+ let root;
1246
+ const assign = (destination, item) => {
1247
+ if (destination === void 0) return;
1248
+ if (destination.kind === "root") root = item;
1249
+ else if (destination.kind === "array") destination.target[destination.index] = item;
1250
+ else Object.defineProperty(destination.target, destination.key, {
1251
+ value: item,
1252
+ enumerable: true,
1253
+ configurable: true,
1254
+ writable: true
1255
+ });
1256
+ };
1257
+ const tasks = [{
1258
+ kind: "visit",
1259
+ value,
1260
+ ...detach ? { destination: { kind: "root" } } : {}
1261
+ }];
1262
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
1263
+ if (task.kind === "leave") {
1264
+ ancestors.delete(task.source);
1265
+ continue;
1266
+ }
1267
+ if (task.kind === "array-item") {
1268
+ if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
1269
+ tasks.push({
1270
+ kind: "visit",
1271
+ value: task.source[task.index],
1272
+ ...task.target === void 0 ? {} : { destination: {
1273
+ kind: "array",
1274
+ target: task.target,
1275
+ index: task.index
1276
+ } }
1277
+ });
1278
+ continue;
1279
+ }
1280
+ if (task.kind === "object-property") {
1281
+ tasks.push({
1282
+ kind: "visit",
1283
+ value: task.source[task.key],
1284
+ ...task.target === void 0 ? {} : { destination: {
1285
+ kind: "object",
1286
+ target: task.target,
1287
+ key: task.key
1288
+ } }
1289
+ });
1290
+ continue;
1291
+ }
1292
+ const current = task.value;
1293
+ if (current === null) {
1294
+ assign(task.destination, null);
1295
+ continue;
1296
+ }
1297
+ if (typeof current === "boolean" || typeof current === "string") {
1298
+ assign(task.destination, current);
1299
+ continue;
1300
+ }
1301
+ if (typeof current === "number") {
1302
+ if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
1303
+ assign(task.destination, current);
1304
+ continue;
1305
+ }
1306
+ if (typeof current !== "object") return void 0;
1307
+ if (ancestors.has(current)) return void 0;
1308
+ if (Array.isArray(current)) {
1309
+ if (!hasPlainArrayPrototype$1(current)) return void 0;
1310
+ const length = current.length;
1311
+ if (Reflect.ownKeys(current).length !== length + 1) return void 0;
1312
+ const target = detach ? [] : void 0;
1313
+ if (target !== void 0) assign(task.destination, target);
1314
+ ancestors.add(current);
1315
+ tasks.push({
1316
+ kind: "leave",
1317
+ source: current
1318
+ });
1319
+ for (let index = length - 1; index >= 0; index--) tasks.push({
1320
+ kind: "array-item",
1321
+ source: current,
1322
+ index,
1323
+ ...target === void 0 ? {} : { target }
1324
+ });
1325
+ continue;
1326
+ }
1327
+ if (!hasPlainObjectPrototype(current)) return void 0;
1328
+ const keys = enumerableStringKeys(current);
1329
+ if (keys === void 0) return void 0;
1330
+ const target = detach ? {} : void 0;
1331
+ if (target !== void 0) assign(task.destination, target);
1332
+ ancestors.add(current);
1333
+ tasks.push({
1334
+ kind: "leave",
1335
+ source: current
1336
+ });
1337
+ for (let index = keys.length - 1; index >= 0; index--) {
1338
+ const key = keys[index];
1339
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
1340
+ if (key === void 0) return void 0;
1341
+ tasks.push({
1342
+ kind: "object-property",
1343
+ source: current,
1344
+ key,
1345
+ ...target === void 0 ? {} : { target }
1346
+ });
1347
+ }
1348
+ }
1349
+ return detach ? root : true;
1350
+ }
1351
+ /**
1352
+ * Validate and detach lossless JSON in one read per property, so a stateful
1353
+ * getter cannot change between validation and copying. Traversal is iterative,
1354
+ * so valid nesting is bounded by available memory rather than the JavaScript
1355
+ * call stack. Accepts ordinary arrays, plain or null-prototype objects, and JSON
1356
+ * scalars; rejects sparse, cyclic, exotic, negative-zero, and non-finite values.
1357
+ * Getter throws propagate.
1358
+ *
1359
+ * @param value - the candidate value to validate and detach.
1360
+ * @returns the detached snapshot, or `undefined` when the value is not
1361
+ * losslessly JSON-serializable.
1362
+ */
1363
+ function snapshotJsonValue(value) {
1364
+ return walkJsonValue(value, true);
1365
+ }
1366
+ /**
1367
+ * Test the same lossless JSON boundary as {@link snapshotJsonValue} without
1368
+ * detaching it. Only own enumerable string properties participate; `toJSON`
1369
+ * is ignored and getters run, so persistence boundaries use the snapshotter.
1370
+ * @param value - the candidate event data to test.
1371
+ * @returns whether `value` survives JSON round-trip losslessly.
1372
+ */
1373
+ function isJsonValue(value) {
1374
+ return walkJsonValue(value, false) === true;
1375
+ }
1376
+ //#endregion
1377
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-tools@0.1._7117760508de0db46d4e3da5e4a645e6/node_modules/@deepseek-ai/dsh-tools/lib/index.js
1378
+ /**
1379
+ * Enforced JSON Schema subset shared by tool outputs, generated Code Mode
1380
+ * types, subagents, and workflows. The subset accepts any JSON root, an
1381
+ * annotation-only schema for unconstrained JSON, one scalar `type`, object
1382
+ * `properties`/`required`/boolean `additionalProperties`, array `items`,
1383
+ * type-correct scalar `enum`/`const`, and exact-one `oneOf`.
1384
+ *
1385
+ * Unsupported or misplaced keywords reject rather than being accepted without
1386
+ * enforcement. Consumers that require an object root apply
1387
+ * {@link assertObjectJsonSchema} before accepting input.
1388
+ * @module dsh-tools/json-schema
1389
+ */
1390
+ /**
1391
+ * Thrown when a raw schema falls outside the enforced subset. `violations`
1392
+ * lists every offending path instead of stopping at the first author error.
1393
+ */
1394
+ var JsonSchemaError = class extends HarnessError {
1395
+ /** Individual schema violations in walk order. */
1396
+ violations;
1397
+ constructor(violations) {
1398
+ super(`unsupported JSON schema: ${violations.join("; ")}`, "UNSUPPORTED_SCHEMA");
1399
+ this.name = "JsonSchemaError";
1400
+ this.violations = violations;
1401
+ }
1402
+ };
1403
+ const CONSTRAINT_KEYWORDS = /* @__PURE__ */ new Set([
1404
+ "type",
1405
+ "oneOf",
1406
+ "properties",
1407
+ "required",
1408
+ "additionalProperties",
1409
+ "items",
1410
+ "enum",
1411
+ "const"
1412
+ ]);
1413
+ const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
1414
+ "description",
1415
+ "title",
1416
+ "default",
1417
+ "examples"
1418
+ ]);
1419
+ const SCHEMA_TYPES = [
1420
+ "object",
1421
+ "array",
1422
+ "string",
1423
+ "number",
1424
+ "integer",
1425
+ "boolean",
1426
+ "null"
1427
+ ];
1428
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
1429
+ function hasIntrinsicConstructor(prototype, name) {
1430
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
1431
+ if (typeof constructor !== "function") return false;
1432
+ try {
1433
+ return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
1434
+ } catch {
1435
+ return false;
1436
+ }
1437
+ }
1438
+ /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
1439
+ function isIntrinsicObjectPrototype(value) {
1440
+ return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
1441
+ }
1442
+ /**
1443
+ * Test for a realm-agnostic plain JSON record without accepting arrays or
1444
+ * exotic objects.
1445
+ * @param value - candidate record from any JavaScript realm.
1446
+ * @returns Whether the value has a plain-object prototype chain.
1447
+ */
1448
+ function isPlainJsonRecord(value) {
1449
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1450
+ try {
1451
+ const prototype = Object.getPrototypeOf(value);
1452
+ return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype(prototype);
1453
+ } catch {
1454
+ return false;
1455
+ }
1456
+ }
1457
+ /** Whether an array uses one realm's intrinsic `Array.prototype`. */
1458
+ function hasPlainArrayPrototype(value) {
1459
+ const prototype = Object.getPrototypeOf(value);
1460
+ if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
1461
+ const objectPrototype = Object.getPrototypeOf(prototype);
1462
+ return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype(objectPrototype);
1463
+ }
1464
+ /** Return whether a record contains only own enumerable string keys. */
1465
+ function hasOnlyEnumerableStringKeys(value) {
1466
+ try {
1467
+ return Reflect.ownKeys(value).every((key) => typeof key === "string" && Object.prototype.propertyIsEnumerable.call(value, key));
1468
+ } catch {
1469
+ return false;
1470
+ }
1471
+ }
1472
+ /**
1473
+ * Test for an ordinary schema record whose keys survive JSON projection.
1474
+ * @param value - candidate record from any JavaScript realm.
1475
+ * @returns Whether the record has an intrinsic prototype and only own enumerable string keys.
1476
+ */
1477
+ function isJsonSchemaRecord(value) {
1478
+ return isPlainJsonRecord(value) && hasOnlyEnumerableStringKeys(value);
1479
+ }
1480
+ /**
1481
+ * Test for a dense ordinary array with no JSON-invisible decorations.
1482
+ * @param value - candidate array from any JavaScript realm.
1483
+ * @returns Whether the array is intrinsic, dense, and undecorated.
1484
+ */
1485
+ function isPlainJsonArray(value) {
1486
+ if (!Array.isArray(value)) return false;
1487
+ try {
1488
+ if (!hasPlainArrayPrototype(value) || Reflect.ownKeys(value).length !== value.length + 1) return false;
1489
+ for (let index = 0; index < value.length; index++) if (!Object.hasOwn(value, index)) return false;
1490
+ return true;
1491
+ } catch {
1492
+ return false;
1493
+ }
1494
+ }
1495
+ /** Lossless finite JSON number, excluding negative zero. */
1496
+ function isJsonNumber(value) {
1497
+ return typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0);
1498
+ }
1499
+ /** Whether a scalar is valid for one declared schema type. */
1500
+ function scalarMatches(type, value) {
1501
+ switch (type) {
1502
+ case "string": return typeof value === "string";
1503
+ case "number": return isJsonNumber(value);
1504
+ case "integer": return isJsonNumber(value) && Number.isInteger(value);
1505
+ case "boolean": return typeof value === "boolean";
1506
+ case "null": return value === null;
1507
+ /* v8 ignore next -- JsonSchemaScalarType is closed; this retains compile-time exhaustiveness. */
1508
+ default: return assertNever(type, "JsonSchemaType");
1509
+ }
1510
+ }
1511
+ /** Keywords that are invalid beside `oneOf`. */
1512
+ const ONE_OF_SIBLING_KEYWORDS = [
1513
+ "properties",
1514
+ "required",
1515
+ "additionalProperties",
1516
+ "items",
1517
+ "enum",
1518
+ "const"
1519
+ ];
1520
+ /** Validate object-only fields after its property schemas have been visited. */
1521
+ function checkObjectSchemaTail(node, path, properties, violations) {
1522
+ const hasRequired = Object.hasOwn(node, "required");
1523
+ const required = hasRequired ? node.required : void 0;
1524
+ if (hasRequired) if (!isPlainJsonArray(required) || required.some((entry) => typeof entry !== "string")) violations.push(`${path}.required must be an array of strings`);
1525
+ else {
1526
+ const declared = isJsonSchemaRecord(properties) ? properties : {};
1527
+ for (const key of required) if (!Object.hasOwn(declared, key)) violations.push(`${path}.required names "${key}" which is not in properties`);
1528
+ }
1529
+ if (Object.hasOwn(node, "additionalProperties") && typeof node.additionalProperties !== "boolean") violations.push(`${path}.additionalProperties must be a boolean`);
1530
+ }
1531
+ /** Collect every violation for one raw schema tree without using the JavaScript call stack. */
1532
+ function checkSchemaNode(root, rootPath, violations, seen) {
1533
+ const tasks = [{
1534
+ kind: "enter",
1535
+ node: root,
1536
+ path: rootPath
1537
+ }];
1538
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
1539
+ if (task.kind === "leave") {
1540
+ seen.delete(task.node);
1541
+ continue;
1542
+ }
1543
+ if (task.kind === "one-of-tail") {
1544
+ for (const key of ONE_OF_SIBLING_KEYWORDS) if (Object.hasOwn(task.node, key)) violations.push(`${task.path}.${key} is not supported beside oneOf`);
1545
+ continue;
1546
+ }
1547
+ if (task.kind === "object-tail") {
1548
+ checkObjectSchemaTail(task.node, task.path, task.properties, violations);
1549
+ continue;
1550
+ }
1551
+ const { node, path } = task;
1552
+ if (!isJsonSchemaRecord(node)) {
1553
+ violations.push(`${path} must be a schema object`);
1554
+ continue;
1555
+ }
1556
+ if (seen.has(node)) {
1557
+ violations.push(`${path} is circular`);
1558
+ continue;
1559
+ }
1560
+ seen.add(node);
1561
+ tasks.push({
1562
+ kind: "leave",
1563
+ node
1564
+ });
1565
+ for (const key of Object.keys(node)) {
1566
+ if (CONSTRAINT_KEYWORDS.has(key)) continue;
1567
+ if (ANNOTATION_KEYWORDS.has(key)) {
1568
+ try {
1569
+ if (!isJsonValue(node[key])) violations.push(`${path}.${key} annotation must be lossless JSON data`);
1570
+ } catch {
1571
+ violations.push(`${path}.${key} annotation must be lossless JSON data`);
1572
+ }
1573
+ continue;
1574
+ }
1575
+ violations.push(`${path}.${key} is not a supported keyword (subset: type/oneOf/properties/required/additionalProperties/items/enum/const + annotations)`);
1576
+ }
1577
+ if (Object.hasOwn(node, "description") && typeof node.description !== "string") violations.push(`${path}.description must be a string`);
1578
+ if (Object.hasOwn(node, "title") && typeof node.title !== "string") violations.push(`${path}.title must be a string`);
1579
+ const hasType = Object.hasOwn(node, "type");
1580
+ const hasOneOf = Object.hasOwn(node, "oneOf");
1581
+ if (hasType && hasOneOf) {
1582
+ violations.push(`${path} cannot declare both type and oneOf`);
1583
+ continue;
1584
+ }
1585
+ if (!hasType && !hasOneOf) {
1586
+ for (const key of ONE_OF_SIBLING_KEYWORDS) if (Object.hasOwn(node, key)) violations.push(`${path}.${key} requires type or oneOf`);
1587
+ continue;
1588
+ }
1589
+ if (hasOneOf) {
1590
+ const oneOf = node.oneOf;
1591
+ tasks.push({
1592
+ kind: "one-of-tail",
1593
+ node,
1594
+ path
1595
+ });
1596
+ if (!isPlainJsonArray(oneOf) || oneOf.length < 2) violations.push(`${path}.oneOf must be an array of at least two schemas`);
1597
+ else for (let index = oneOf.length - 1; index >= 0; index--) tasks.push({
1598
+ kind: "enter",
1599
+ node: oneOf[index],
1600
+ path: `${path}.oneOf[${index}]`
1601
+ });
1602
+ continue;
1603
+ }
1604
+ const type = node.type;
1605
+ if (typeof type !== "string" || !SCHEMA_TYPES.includes(type)) {
1606
+ violations.push(Array.isArray(type) ? `${path}.type must be a single type string (type arrays are not supported)` : `${path}.type must be one of ${SCHEMA_TYPES.join("/")}`);
1607
+ continue;
1608
+ }
1609
+ const schemaType = type;
1610
+ for (const [key, types] of Object.entries({
1611
+ properties: ["object"],
1612
+ required: ["object"],
1613
+ additionalProperties: ["object"],
1614
+ items: ["array"],
1615
+ enum: [
1616
+ "string",
1617
+ "number",
1618
+ "integer",
1619
+ "boolean",
1620
+ "null"
1621
+ ],
1622
+ const: [
1623
+ "string",
1624
+ "number",
1625
+ "integer",
1626
+ "boolean",
1627
+ "null"
1628
+ ]
1629
+ })) if (Object.hasOwn(node, key) && !types.includes(schemaType)) violations.push(`${path}.${key} is not supported on type "${schemaType}"`);
1630
+ switch (schemaType) {
1631
+ case "object": {
1632
+ const properties = Object.hasOwn(node, "properties") ? node.properties : void 0;
1633
+ tasks.push({
1634
+ kind: "object-tail",
1635
+ node,
1636
+ path,
1637
+ properties
1638
+ });
1639
+ if (Object.hasOwn(node, "properties")) if (!isJsonSchemaRecord(properties)) violations.push(`${path}.properties must be an object of schemas`);
1640
+ else {
1641
+ const entries = Object.entries(properties);
1642
+ for (let index = entries.length - 1; index >= 0; index--) {
1643
+ const entry = entries[index];
1644
+ /* v8 ignore next -- the loop is bounded by the captured entry count. */
1645
+ if (entry === void 0) continue;
1646
+ tasks.push({
1647
+ kind: "enter",
1648
+ node: entry[1],
1649
+ path: `${path}.properties.${entry[0]}`
1650
+ });
1651
+ }
1652
+ }
1653
+ break;
1654
+ }
1655
+ case "array":
1656
+ if (Object.hasOwn(node, "items")) tasks.push({
1657
+ kind: "enter",
1658
+ node: node.items,
1659
+ path: `${path}.items`
1660
+ });
1661
+ break;
1662
+ case "string":
1663
+ case "number":
1664
+ case "integer":
1665
+ case "boolean":
1666
+ case "null": {
1667
+ const hasEnum = Object.hasOwn(node, "enum");
1668
+ const allowed = hasEnum ? node.enum : void 0;
1669
+ const enumValid = isPlainJsonArray(allowed) && allowed.length > 0 && allowed.every((entry) => scalarMatches(schemaType, entry));
1670
+ if (hasEnum && !enumValid) violations.push(`${path}.enum must be a non-empty array of ${schemaType} values`);
1671
+ const hasConst = Object.hasOwn(node, "const");
1672
+ const declaredConst = hasConst ? node.const : void 0;
1673
+ const constValid = scalarMatches(schemaType, declaredConst);
1674
+ if (hasConst) {
1675
+ if (!constValid) violations.push(`${path}.const must be a ${schemaType} value`);
1676
+ else if (enumValid && !allowed.includes(declaredConst)) violations.push(`${path}.const must be one of ${path}.enum when both are declared`);
1677
+ }
1678
+ break;
1679
+ }
1680
+ /* v8 ignore next -- schemaType was narrowed from the closed SCHEMA_TYPES table above. */
1681
+ default: assertNever(schemaType, "JsonSchemaType");
1682
+ }
1683
+ }
1684
+ }
1685
+ /**
1686
+ * Assert that an arbitrary raw schema uses only the enforced subset.
1687
+ * Annotation-only schemas are accepted as the standard unconstrained-JSON
1688
+ * form; callers that require an object root use {@link assertObjectJsonSchema}.
1689
+ * @param schema - untrusted raw JSON Schema.
1690
+ * @returns Assertion that the schema belongs to the supported subset.
1691
+ */
1692
+ function assertSupportedJsonSchema(schema) {
1693
+ const violations = [];
1694
+ checkSchemaNode(schema, "schema", violations, /* @__PURE__ */ new Set());
1695
+ if (violations.length > 0) throw new JsonSchemaError(violations);
1696
+ }
1697
+ /** Safely test the lossless JSON boundary when a getter may throw. */
1698
+ function safelyIsJsonValue(value) {
1699
+ try {
1700
+ return isJsonValue(value);
1701
+ } catch {
1702
+ return false;
1703
+ }
1704
+ }
1705
+ /** Root-aware diagnostic path for the parameter validator's empty sentinel. */
1706
+ function diagnosticPath(path) {
1707
+ return path === "" ? "arguments" : path;
1708
+ }
1709
+ /** Append one object property without a leading dot at an implicit root. */
1710
+ function propertyPath(path, key) {
1711
+ return path === "" ? key : `${path}.${key}`;
1712
+ }
1713
+ /** The generic exception-containment diagnostic owned by one valid schema node. */
1714
+ function losslessValueViolation(path) {
1715
+ return [`"${diagnosticPath(path)}" must be a lossless JSON value`];
1716
+ }
1717
+ /** Append diagnostics without spreading a potentially wide child result as call arguments. */
1718
+ function appendViolations(target, source) {
1719
+ for (const violation of source) target.push(violation);
1720
+ }
1721
+ /** Initialize one validation frame with empty aggregation state. */
1722
+ function valueFrame(node, value, path) {
1723
+ return {
1724
+ node,
1725
+ value,
1726
+ path,
1727
+ catches: false,
1728
+ phase: "start",
1729
+ children: [],
1730
+ childIndex: 0,
1731
+ violations: [],
1732
+ tailViolations: [],
1733
+ matches: 0
1734
+ };
1735
+ }
1736
+ /** Validate one scalar node after its primitive type check. */
1737
+ function checkScalarValue(node, value, path) {
1738
+ const allowed = Object.hasOwn(node, "enum") ? node.enum : void 0;
1739
+ if (allowed !== void 0 && !allowed.includes(value)) return [`"${diagnosticPath(path)}" must be one of ${JSON.stringify(allowed)}`];
1740
+ if (Object.hasOwn(node, "const") && value !== node.const) return [`"${diagnosticPath(path)}" must be ${JSON.stringify(node.const)}`];
1741
+ return [];
1742
+ }
1743
+ /** Validate one trusted schema/value pair with explicit frames rather than recursive calls. */
1744
+ function checkValue(schema, value, path) {
1745
+ const frames = [valueFrame(schema, value, path)];
1746
+ let rootResult;
1747
+ const receive = (result) => {
1748
+ const parent = frames.at(-1);
1749
+ if (parent === void 0) {
1750
+ rootResult = result;
1751
+ return;
1752
+ }
1753
+ if (parent.kind === "oneOf") {
1754
+ if (result.length === 0) parent.matches++;
1755
+ } else appendViolations(parent.violations, result);
1756
+ };
1757
+ const finish = (result) => {
1758
+ frames.pop();
1759
+ receive(result);
1760
+ };
1761
+ while (frames.length > 0) {
1762
+ const frame = frames.at(-1);
1763
+ /* v8 ignore next -- the loop condition guarantees a current frame. */
1764
+ if (frame === void 0) break;
1765
+ try {
1766
+ if (frame.phase === "children") {
1767
+ if (frame.childIndex < frame.children.length) {
1768
+ const child = frame.children[frame.childIndex];
1769
+ /* v8 ignore next -- childIndex is bounded by children.length. */
1770
+ if (child === void 0) throw new Error("missing schema-value child frame");
1771
+ frame.childIndex++;
1772
+ frames.push(valueFrame(child.node, child.value, child.path));
1773
+ continue;
1774
+ }
1775
+ if (frame.kind === "oneOf") {
1776
+ finish(frame.matches === 1 ? [] : [`"${diagnosticPath(frame.path)}" must match exactly one oneOf branch (matched ${frame.matches})`]);
1777
+ continue;
1778
+ }
1779
+ appendViolations(frame.violations, frame.tailViolations);
1780
+ if (frame.violations.length > 0) finish(frame.violations);
1781
+ else if (frame.kind === "object") finish(safelyIsJsonValue(frame.value) ? [] : [`"${diagnosticPath(frame.path)}" must be a lossless JSON object`]);
1782
+ else finish(safelyIsJsonValue(frame.value) ? [] : [`"${diagnosticPath(frame.path)}" must be a dense lossless JSON array`]);
1783
+ continue;
1784
+ }
1785
+ const nodeType = Object.hasOwn(frame.node, "type") ? frame.node.type : void 0;
1786
+ frame.catches = !(nodeType !== void 0 && !SCHEMA_TYPES.includes(nodeType));
1787
+ const oneOf = Object.hasOwn(frame.node, "oneOf") ? frame.node.oneOf : void 0;
1788
+ if (oneOf !== void 0) {
1789
+ frame.kind = "oneOf";
1790
+ frame.children = Array.from(oneOf, (branch) => ({
1791
+ node: branch,
1792
+ value: frame.value,
1793
+ path: frame.path
1794
+ }));
1795
+ frame.childIndex = 0;
1796
+ frame.matches = 0;
1797
+ frame.phase = "children";
1798
+ continue;
1799
+ }
1800
+ if (nodeType === void 0) {
1801
+ finish(safelyIsJsonValue(frame.value) ? [] : losslessValueViolation(frame.path));
1802
+ continue;
1803
+ }
1804
+ switch (nodeType) {
1805
+ case "object": {
1806
+ if (!isPlainJsonRecord(frame.value)) {
1807
+ finish([`"${diagnosticPath(frame.path)}" must be an object`]);
1808
+ break;
1809
+ }
1810
+ const properties = Object.hasOwn(frame.node, "properties") ? frame.node.properties ?? {} : {};
1811
+ const violations = [];
1812
+ const required = Object.hasOwn(frame.node, "required") ? frame.node.required ?? [] : [];
1813
+ for (const key of required) if (!Object.hasOwn(frame.value, key) || frame.value[key] === void 0) violations.push(`missing required property "${propertyPath(frame.path, key)}"`);
1814
+ const children = [];
1815
+ for (const [key, child] of Object.entries(properties)) {
1816
+ if (!Object.hasOwn(frame.value, key) || frame.value[key] === void 0) continue;
1817
+ children.push({
1818
+ node: child,
1819
+ value: frame.value[key],
1820
+ path: propertyPath(frame.path, key)
1821
+ });
1822
+ }
1823
+ const tailViolations = [];
1824
+ if (Object.hasOwn(frame.node, "additionalProperties") && frame.node.additionalProperties === false) {
1825
+ for (const key of Object.keys(frame.value)) if (!Object.hasOwn(properties, key)) tailViolations.push(`"${propertyPath(frame.path, key)}" is not a declared property (additionalProperties: false)`);
1826
+ }
1827
+ frame.kind = "object";
1828
+ frame.children = children;
1829
+ frame.childIndex = 0;
1830
+ frame.violations = violations;
1831
+ frame.tailViolations = tailViolations;
1832
+ frame.phase = "children";
1833
+ break;
1834
+ }
1835
+ case "array": {
1836
+ if (!Array.isArray(frame.value)) {
1837
+ finish([`"${diagnosticPath(frame.path)}" must be an array`]);
1838
+ break;
1839
+ }
1840
+ const items = Object.hasOwn(frame.node, "items") ? frame.node.items : void 0;
1841
+ const children = items === void 0 ? [] : frame.value.flatMap((entry, index) => [{
1842
+ node: items,
1843
+ value: entry,
1844
+ path: `${frame.path}[${index}]`
1845
+ }]);
1846
+ frame.kind = "array";
1847
+ frame.children = children;
1848
+ frame.childIndex = 0;
1849
+ frame.violations = [];
1850
+ frame.phase = "children";
1851
+ break;
1852
+ }
1853
+ case "string":
1854
+ finish(typeof frame.value === "string" ? checkScalarValue(frame.node, frame.value, frame.path) : [`"${diagnosticPath(frame.path)}" must be a string`]);
1855
+ break;
1856
+ case "number":
1857
+ finish(typeof frame.value !== "number" ? [`"${diagnosticPath(frame.path)}" must be a number`] : !isJsonNumber(frame.value) ? [`"${diagnosticPath(frame.path)}" must be a finite JSON number`] : checkScalarValue(frame.node, frame.value, frame.path));
1858
+ break;
1859
+ case "integer":
1860
+ finish(!isJsonNumber(frame.value) || !Number.isInteger(frame.value) ? [`"${diagnosticPath(frame.path)}" must be an integer`] : checkScalarValue(frame.node, frame.value, frame.path));
1861
+ break;
1862
+ case "boolean":
1863
+ finish(typeof frame.value === "boolean" ? checkScalarValue(frame.node, frame.value, frame.path) : [`"${diagnosticPath(frame.path)}" must be a boolean`]);
1864
+ break;
1865
+ case "null":
1866
+ finish(frame.value === null ? checkScalarValue(frame.node, frame.value, frame.path) : [`"${diagnosticPath(frame.path)}" must be null`]);
1867
+ break;
1868
+ default: finish(assertNever(nodeType, "JsonSchemaType"));
1869
+ }
1870
+ } catch (error) {
1871
+ let failed = frames.pop();
1872
+ while (failed !== void 0 && !failed.catches) failed = frames.pop();
1873
+ if (failed === void 0) throw error;
1874
+ receive(losslessValueViolation(failed.path));
1875
+ }
1876
+ }
1877
+ /* v8 ignore next -- every root frame finishes or throws. */
1878
+ return rootResult ?? losslessValueViolation(path);
1879
+ }
1880
+ /**
1881
+ * Validate a candidate value against an asserted raw schema. The function is
1882
+ * total for arbitrary values and returns path-qualified violations.
1883
+ * @param schema - a schema accepted by {@link assertSupportedJsonSchema}.
1884
+ * @param value - the candidate JSON value.
1885
+ * @param path - root label used in diagnostics.
1886
+ * @returns All violations in walk order; empty means valid.
1887
+ */
1888
+ function validateJsonSchemaValue(schema, value, path = "value") {
1889
+ return checkValue(schema, value, path);
1890
+ }
1891
+ /** Unified JSON-value schema DSL, inference, compilation, and typed tool helper. @module dsh-tools/schema */
1892
+ const ANNOTATION_KEYS = [
1893
+ "description",
1894
+ "title",
1895
+ "default",
1896
+ "examples"
1897
+ ];
1898
+ /** Throw one author-schema violation through the shared schema error type. */
1899
+ function authorError(message) {
1900
+ throw new JsonSchemaError([message]);
1901
+ }
1902
+ /** Copy own annotation fields for validation by the raw-schema boundary. */
1903
+ function copyAnnotations(source, target) {
1904
+ if (Object.hasOwn(source, "description")) target.description = source.description;
1905
+ if (Object.hasOwn(source, "title")) target.title = source.title;
1906
+ if (Object.hasOwn(source, "default")) target.default = source.default;
1907
+ if (Object.hasOwn(source, "examples")) target.examples = source.examples;
1908
+ }
1909
+ /** Reject author-only keys outside one node's declared vocabulary. */
1910
+ function assertAuthorKeys(source, path, allowed) {
1911
+ for (const key of Object.keys(source)) if (!allowed.includes(key)) authorError(`${path}.${key} is not supported by the value schema DSL`);
1912
+ }
1913
+ /** Install a compiled node without giving `__proto__` assignment semantics. */
1914
+ function assignCompiledNode(destination, node) {
1915
+ switch (destination.kind) {
1916
+ case "root":
1917
+ destination.holder.value = node;
1918
+ break;
1919
+ case "property":
1920
+ Object.defineProperty(destination.target, destination.key, {
1921
+ value: node,
1922
+ enumerable: true,
1923
+ configurable: true,
1924
+ writable: true
1925
+ });
1926
+ break;
1927
+ case "item":
1928
+ destination.target.items = node;
1929
+ break;
1930
+ case "one-of": destination.target[destination.index] = node;
1931
+ }
1932
+ }
1933
+ /** Install a compiled property map at its root or containing object node. */
1934
+ function assignCompiledPropertyMap(destination, compiled) {
1935
+ if (destination.kind === "root") destination.holder.value = compiled;
1936
+ else destination.target.properties = compiled.properties;
1937
+ }
1938
+ /** Execute an author-schema compilation task graph without recursive descent. */
1939
+ function runSchemaCompiler(initial) {
1940
+ const seen = /* @__PURE__ */ new Set();
1941
+ const tasks = [initial];
1942
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
1943
+ if (task.kind === "leave") {
1944
+ seen.delete(task.input);
1945
+ continue;
1946
+ }
1947
+ if (task.kind === "property-map-tail") {
1948
+ if (task.required.length > 0) {
1949
+ task.compiled.required = task.required;
1950
+ if (task.destination.kind === "object") task.destination.target.required = task.required;
1951
+ }
1952
+ continue;
1953
+ }
1954
+ if (task.kind === "property") {
1955
+ if (!isJsonSchemaRecord(task.property)) authorError(`${task.path} must be a value schema object`);
1956
+ if (Object.hasOwn(task.property, "required") && task.property.required !== true) authorError(`${task.path}.required must be true when present`);
1957
+ if (Object.hasOwn(task.property, "required") && task.property.required === true) task.required.push(task.key);
1958
+ tasks.push({
1959
+ kind: "value",
1960
+ input: task.property,
1961
+ path: task.path,
1962
+ allowRequired: true,
1963
+ destination: {
1964
+ kind: "property",
1965
+ target: task.properties,
1966
+ key: task.key
1967
+ }
1968
+ });
1969
+ continue;
1970
+ }
1971
+ if (task.kind === "property-map") {
1972
+ if (!isJsonSchemaRecord(task.input)) authorError(`${task.path} must be an object of value schemas`);
1973
+ if (seen.has(task.input)) authorError(`${task.path} is circular`);
1974
+ seen.add(task.input);
1975
+ const compiled = { properties: {} };
1976
+ const required = [];
1977
+ assignCompiledPropertyMap(task.destination, compiled);
1978
+ tasks.push({
1979
+ kind: "leave",
1980
+ input: task.input
1981
+ });
1982
+ tasks.push({
1983
+ kind: "property-map-tail",
1984
+ compiled,
1985
+ required,
1986
+ destination: task.destination
1987
+ });
1988
+ const entries = Object.entries(task.input);
1989
+ for (let index = entries.length - 1; index >= 0; index--) {
1990
+ const entry = entries[index];
1991
+ /* v8 ignore next -- the loop is bounded by the captured entry count. */
1992
+ if (entry === void 0) continue;
1993
+ tasks.push({
1994
+ kind: "property",
1995
+ property: entry[1],
1996
+ path: `${task.path}.${entry[0]}`,
1997
+ key: entry[0],
1998
+ properties: compiled.properties,
1999
+ required
2000
+ });
2001
+ }
2002
+ continue;
2003
+ }
2004
+ const { input, path } = task;
2005
+ if (!isJsonSchemaRecord(input)) authorError(`${path} must be a value schema object`);
2006
+ if (seen.has(input)) authorError(`${path} is circular`);
2007
+ seen.add(input);
2008
+ const authorKeys = [...ANNOTATION_KEYS, ...task.allowRequired ? ["required"] : []];
2009
+ const node = {};
2010
+ assignCompiledNode(task.destination, node);
2011
+ tasks.push({
2012
+ kind: "leave",
2013
+ input
2014
+ });
2015
+ if (Object.hasOwn(input, "oneOf")) {
2016
+ assertAuthorKeys(input, path, [
2017
+ ...authorKeys,
2018
+ "oneOf",
2019
+ "type"
2020
+ ]);
2021
+ if (Object.hasOwn(input, "type")) authorError(`${path} cannot declare both type and oneOf`);
2022
+ if (!isPlainJsonArray(input.oneOf)) authorError(`${path}.oneOf must be an array of at least two value schemas`);
2023
+ const branches = [];
2024
+ node.oneOf = branches;
2025
+ copyAnnotations(input, node);
2026
+ for (let index = input.oneOf.length - 1; index >= 0; index--) tasks.push({
2027
+ kind: "value",
2028
+ input: input.oneOf[index],
2029
+ path: `${path}.oneOf[${index}]`,
2030
+ allowRequired: false,
2031
+ destination: {
2032
+ kind: "one-of",
2033
+ target: branches,
2034
+ index
2035
+ }
2036
+ });
2037
+ continue;
2038
+ }
2039
+ const inputType = Object.hasOwn(input, "type") ? input.type : void 0;
2040
+ switch (inputType) {
2041
+ case "json":
2042
+ assertAuthorKeys(input, path, [...authorKeys, "type"]);
2043
+ copyAnnotations(input, node);
2044
+ break;
2045
+ case "object":
2046
+ assertAuthorKeys(input, path, [
2047
+ ...authorKeys,
2048
+ "type",
2049
+ "properties",
2050
+ "additionalProperties"
2051
+ ]);
2052
+ if (!Object.hasOwn(input, "additionalProperties") || typeof input.additionalProperties !== "boolean") authorError(`${path}.additionalProperties must be explicitly true or false`);
2053
+ node.type = "object";
2054
+ copyAnnotations(input, node);
2055
+ node.additionalProperties = input.additionalProperties;
2056
+ if (Object.hasOwn(input, "properties")) tasks.push({
2057
+ kind: "property-map",
2058
+ input: input.properties,
2059
+ path: `${path}.properties`,
2060
+ destination: {
2061
+ kind: "object",
2062
+ target: node
2063
+ }
2064
+ });
2065
+ break;
2066
+ case "array":
2067
+ assertAuthorKeys(input, path, [
2068
+ ...authorKeys,
2069
+ "type",
2070
+ "items"
2071
+ ]);
2072
+ node.type = "array";
2073
+ copyAnnotations(input, node);
2074
+ if (Object.hasOwn(input, "items")) tasks.push({
2075
+ kind: "value",
2076
+ input: input.items,
2077
+ path: `${path}.items`,
2078
+ allowRequired: false,
2079
+ destination: {
2080
+ kind: "item",
2081
+ target: node
2082
+ }
2083
+ });
2084
+ break;
2085
+ case "string":
2086
+ case "number":
2087
+ case "integer":
2088
+ case "boolean":
2089
+ case "null":
2090
+ assertAuthorKeys(input, path, [
2091
+ ...authorKeys,
2092
+ "type",
2093
+ "enum",
2094
+ "const"
2095
+ ]);
2096
+ node.type = inputType;
2097
+ copyAnnotations(input, node);
2098
+ if (Object.hasOwn(input, "enum")) {
2099
+ if (!isPlainJsonArray(input.enum)) authorError(`${path}.enum must be a non-empty array of scalar values`);
2100
+ node.enum = Array.from(input.enum, (entry) => entry);
2101
+ }
2102
+ if (Object.hasOwn(input, "const")) node.const = input.const;
2103
+ break;
2104
+ default: authorError(`${path}.type must be string/number/integer/boolean/null/array/object/json, or use oneOf`);
2105
+ }
2106
+ }
2107
+ }
2108
+ /** Compile one implicit property map, collecting per-property requiredness. */
2109
+ function compilePropertyMap(input, path) {
2110
+ const holder = {};
2111
+ runSchemaCompiler({
2112
+ kind: "property-map",
2113
+ input,
2114
+ path,
2115
+ destination: {
2116
+ kind: "root",
2117
+ holder
2118
+ }
2119
+ });
2120
+ /* v8 ignore next -- the root task assigns before scheduling any descendants. */
2121
+ return holder.value ?? authorError(`${path} did not compile`);
2122
+ }
2123
+ /** Compile one author node without applying any consumer root restriction. */
2124
+ function compileValueSchema(input, path) {
2125
+ const holder = {};
2126
+ runSchemaCompiler({
2127
+ kind: "value",
2128
+ input,
2129
+ path,
2130
+ allowRequired: false,
2131
+ destination: {
2132
+ kind: "root",
2133
+ holder
2134
+ }
2135
+ });
2136
+ /* v8 ignore next -- the root task assigns before scheduling any descendants. */
2137
+ return holder.value ?? authorError(`${path} did not compile`);
2138
+ }
2139
+ /**
2140
+ * Compile one author-facing value schema to the enforced raw JSON Schema
2141
+ * subset. The author-only `json` node becomes an annotation-only schema.
2142
+ * @param spec - schema for any JSON-value root.
2143
+ * @returns The asserted raw schema projection.
2144
+ */
2145
+ function valueSchemaSpecToJsonSchema(spec) {
2146
+ const schema = compileValueSchema(spec, "schema");
2147
+ assertSupportedJsonSchema(schema);
2148
+ return schema;
2149
+ }
2150
+ /**
2151
+ * Compile the implicit open parameter object into raw JSON Schema.
2152
+ * @param spec - per-property parameter definitions.
2153
+ * @returns An object-rooted raw schema with no implicit-root openness override.
2154
+ */
2155
+ function parameterSchemaSpecToJsonSchema(spec) {
2156
+ const compiled = compilePropertyMap(spec, "parameters");
2157
+ const schema = {
2158
+ type: "object",
2159
+ properties: compiled.properties,
2160
+ ...compiled.required === void 0 ? {} : { required: compiled.required }
2161
+ };
2162
+ assertSupportedJsonSchema(schema);
2163
+ return schema;
2164
+ }
2165
+ /** Invalid model-generated arguments for a typed tool. */
2166
+ var ToolArgsError = class extends HarnessError {
2167
+ /** Individual violations in schema-walk order. */
2168
+ violations;
2169
+ constructor(violations) {
2170
+ super(`invalid arguments: ${violations.join("; ")}`, "INVALID_ARGS");
2171
+ this.name = "ToolArgsError";
2172
+ this.violations = violations;
2173
+ }
2174
+ };
2175
+ /**
2176
+ * Define a first-party tool with inferred arguments and strict execution
2177
+ * validation. Replay-only presenters validate softly and fall back to generic
2178
+ * rendering for obsolete logged arguments.
2179
+ * @param options - typed definition and optional finalizer and presenters.
2180
+ * @returns A registry-ready definition.
2181
+ */
2182
+ function defineTool(options) {
2183
+ const userExecute = options.execute;
2184
+ const userFinalizeContent = options.finalizeContent;
2185
+ const userRender = options.output.render;
2186
+ const userPresentationMeta = options.output.presentationMeta;
2187
+ const userPresentCall = options.presentCall;
2188
+ const userPresentResult = options.presentResult;
2189
+ const userIsConcurrencySafe = options.isConcurrencySafe;
2190
+ if (options.timeoutMs !== void 0 && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)) throw new Error(`defineTool(${options.name}): timeoutMs must be a positive finite number`);
2191
+ const parameters = parameterSchemaSpecToJsonSchema(options.parameters);
2192
+ const outputSchema = valueSchemaSpecToJsonSchema(options.output.schema);
2193
+ const validate = (args) => validateJsonSchemaValue(parameters, args, "");
2194
+ const tool = {
2195
+ name: options.name,
2196
+ description: options.description,
2197
+ parameters,
2198
+ output: {
2199
+ schema: outputSchema,
2200
+ render(args, value) {
2201
+ return userRender(args, value);
2202
+ },
2203
+ ...userPresentationMeta !== void 0 ? { presentationMeta(args, value) {
2204
+ return userPresentationMeta(args, value);
2205
+ } } : {}
2206
+ },
2207
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
2208
+ async execute(args, exec) {
2209
+ const violations = validate(args);
2210
+ if (violations.length > 0) throw new ToolArgsError(violations);
2211
+ return userExecute(args, exec);
2212
+ }
2213
+ };
2214
+ if (userFinalizeContent) tool.finalizeContent = (exec, result) => userFinalizeContent(exec, result);
2215
+ if (userPresentCall) tool.presentCall = (args) => {
2216
+ if (validate(args).length > 0) return void 0;
2217
+ return userPresentCall(args);
2218
+ };
2219
+ if (userPresentResult) tool.presentResult = (args, result) => {
2220
+ if (validate(args).length > 0) return void 0;
2221
+ return userPresentResult(args, result);
2222
+ };
2223
+ if (userIsConcurrencySafe) tool.isConcurrencySafe = (args) => {
2224
+ if (validate(args).length > 0) return false;
2225
+ return userIsConcurrencySafe(args);
2226
+ };
2227
+ return tool;
2228
+ }
2229
+ /**
2230
+ * Code Mode `run_code` transport. Programs call the registry's agent-visible
2231
+ * tools through nested executions scheduled under the native concurrency
2232
+ * contract; each sub-dispatch is logged for reconstruction, while only the
2233
+ * outer curated result enters model history.
2234
+ * @module @deepseek-ai/dsh-tools/src/code-mode
2235
+ */
2236
+ /** The model-facing name of the Code Mode tool. */
2237
+ const RUN_CODE_NAME = "run_code";
2238
+ /**
2239
+ * The TypeScript flavor: the fallback for a schema read with no runtime
2240
+ * mounted ({@link resolveFlavor} owns which readers reach that). A real
2241
+ * assembly always resolves a runtime first, so the model never sees this
2242
+ * fallback outside its own language.
2243
+ */
2244
+ const TYPESCRIPT_FLAVOR = {
2245
+ description: "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return is program output — curate it. Image-bearing subtool results are attached after the run.",
2246
+ codeDescription: "The program: the body of an async TypeScript function."
2247
+ };
2248
+ /** Per-language `run_code` schema flavors (see {@link RunCodeFlavor}); one entry per {@link CodeSdkLanguage}. */
2249
+ const RUN_CODE_FLAVORS = {
2250
+ typescript: TYPESCRIPT_FLAVOR,
2251
+ python: {
2252
+ description: "Execute a Python program against the available tools. Takes two required arguments: `code`, the BODY of an async function (top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Use `print(...)` and/or `return <value>` for program output — curate it. Image-bearing subtool results are attached after the run.",
2253
+ codeDescription: "The program: the body of an async Python function."
2254
+ }
2255
+ };
2256
+ /**
2257
+ * The `description` parameter's model-facing description: language-independent
2258
+ * (the UI label contract is the same for every runtime), shared between the
2259
+ * static spec and the language-aware `parameters` getter so the two emissions
2260
+ * can never drift.
2261
+ */
2262
+ const RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION = "Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\".";
2263
+ /**
2264
+ * Resolve the {@link RunCodeFlavor} for the loaded runtime's language, read at
2265
+ * schema-emission time so the model-visible `run_code` schema always matches
2266
+ * the SDK section's language. `peekRuntime` returns `undefined` only when no
2267
+ * runtime is mounted, which reaches this function through definition readers
2268
+ * and `schemas()` — the doc-catalog harvest is the only shipped one, and none
2269
+ * of them feeds a model, because `wireSchemas` calls `requireCodeRuntime`
2270
+ * before projecting — so that path degrades to {@link TYPESCRIPT_FLAVOR}. A
2271
+ * mounted runtime whose language has no flavor entry fails loud, exactly as
2272
+ * `requireCodeRuntime` rejects it at assembly. Keeping this table in step with
2273
+ * `SDK_RENDERERS` is the compiler's job ({@link CodeSdkLanguage}); what this
2274
+ * guard owns is the runtime-supplied language neither table knows, which never
2275
+ * yields a wrong-language schema for a real runtime.
2276
+ */
2277
+ function resolveFlavor(peekRuntime) {
2278
+ const runtime = peekRuntime();
2279
+ if (runtime === void 0) return TYPESCRIPT_FLAVOR;
2280
+ const flavor = RUN_CODE_FLAVORS[runtime.language];
2281
+ if (!Object.hasOwn(RUN_CODE_FLAVORS, runtime.language) || flavor === void 0) {
2282
+ const known = Object.keys(RUN_CODE_FLAVORS).map((name) => JSON.stringify(name)).join(", ");
2283
+ throw new Error(`dsh-tools: no run_code schema flavor registered for runtime language ${JSON.stringify(runtime.language)} (known: ${known})`);
2284
+ }
2285
+ return flavor;
2286
+ }
2287
+ /**
2288
+ * Thrown by `run_code` when the program run itself failed — a program
2289
+ * exception, a budget expiry, an abort, or substrate death. Extends
2290
+ * {@link HarnessError} (`code: 'CODE_RUN_FAILED'`); the registry's execution
2291
+ * pipeline converts it into a structured `isError` result whose text carries
2292
+ * the failure kind plus the captured logs, so the model can self-correct.
2293
+ */
2294
+ var CodeRunFailedError = class extends HarnessError {
2295
+ constructor(message) {
2296
+ super(message, "CODE_RUN_FAILED");
2297
+ this.name = "CodeRunFailedError";
2298
+ }
2299
+ };
2300
+ /**
2301
+ * Snapshot one binding call's argument as lossless JSON, then snapshot that
2302
+ * detached value again so dispatch and logging stay independent without
2303
+ * reintroducing structured-clone's platform-specific nesting limit.
2304
+ */
2305
+ function jsonNormalizeArgs(value) {
2306
+ let snapshot;
2307
+ try {
2308
+ snapshot = snapshotJsonValue(value);
2309
+ } catch (error) {
2310
+ throw new Error(`tool arguments must be lossless JSON: ${error instanceof Error ? error.message : String(error)}`);
2311
+ }
2312
+ if (snapshot === void 0) throw new Error("tool arguments must be lossless JSON (call the tool with an arguments object, e.g. `{}`)");
2313
+ const logged = snapshotJsonValue(snapshot);
2314
+ /* v8 ignore next -- snapshot is already a detached lossless JSON value. */
2315
+ if (logged === void 0) throw new Error("tool arguments could not be detached for durable logging");
2316
+ return {
2317
+ dispatched: snapshot,
2318
+ logged
2319
+ };
2320
+ }
2321
+ /** Two-space JSON presentation, matching the existing shallow `run_code` text contract. */
2322
+ const JSON_INDENT = " ";
2323
+ /**
2324
+ * ECMAScript caps `JSON.stringify`'s `space` string at ten characters. The
2325
+ * renderer also caps TOTAL indentation there, compacting deeper subtrees, so
2326
+ * formatted output remains linear in the canonical JSON size.
2327
+ */
2328
+ const MAX_JSON_INDENT_CHARS = 10;
2329
+ /** Render one non-string JSON root without recursive traversal or unbounded indentation growth. */
2330
+ function renderJsonValue(value) {
2331
+ const chunks = [];
2332
+ const tasks = [{
2333
+ kind: "value",
2334
+ value,
2335
+ depth: 0,
2336
+ compact: false
2337
+ }];
2338
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
2339
+ if (task.kind === "text") {
2340
+ chunks.push(task.text);
2341
+ continue;
2342
+ }
2343
+ const current = task.value;
2344
+ if (current === null || typeof current === "boolean" || typeof current === "number") {
2345
+ chunks.push(String(current));
2346
+ continue;
2347
+ }
2348
+ if (typeof current === "string") {
2349
+ chunks.push(JSON.stringify(current));
2350
+ continue;
2351
+ }
2352
+ const compact = task.compact || (task.depth + 1) * 2 > MAX_JSON_INDENT_CHARS;
2353
+ const childDepth = task.depth + 1;
2354
+ if (Array.isArray(current)) {
2355
+ chunks.push("[");
2356
+ if (current.length === 0) {
2357
+ chunks.push("]");
2358
+ continue;
2359
+ }
2360
+ tasks.push({
2361
+ kind: "text",
2362
+ text: compact ? "]" : `\n${JSON_INDENT.repeat(task.depth)}]`
2363
+ });
2364
+ for (let index = current.length - 1; index >= 0; index--) {
2365
+ const item = current[index];
2366
+ /* v8 ignore next -- canonical JsonValue arrays are dense. */
2367
+ if (item === void 0) throw new Error("cannot render a sparse JSON array");
2368
+ tasks.push({
2369
+ kind: "value",
2370
+ value: item,
2371
+ depth: childDepth,
2372
+ compact
2373
+ });
2374
+ tasks.push({
2375
+ kind: "text",
2376
+ text: compact ? index === 0 ? "" : "," : `${index === 0 ? "\n" : ",\n"}${JSON_INDENT.repeat(childDepth)}`
2377
+ });
2378
+ }
2379
+ continue;
2380
+ }
2381
+ const keys = Object.keys(current);
2382
+ chunks.push("{");
2383
+ if (keys.length === 0) {
2384
+ chunks.push("}");
2385
+ continue;
2386
+ }
2387
+ tasks.push({
2388
+ kind: "text",
2389
+ text: compact ? "}" : `\n${JSON_INDENT.repeat(task.depth)}}`
2390
+ });
2391
+ for (let index = keys.length - 1; index >= 0; index--) {
2392
+ const key = keys[index];
2393
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
2394
+ if (key === void 0) throw new Error("cannot render a missing JSON object key");
2395
+ const item = current[key];
2396
+ /* v8 ignore next -- canonical JsonValue records contain no undefined properties. */
2397
+ if (item === void 0) throw new Error("cannot render an undefined JSON object property");
2398
+ tasks.push({
2399
+ kind: "value",
2400
+ value: item,
2401
+ depth: childDepth,
2402
+ compact
2403
+ });
2404
+ tasks.push({
2405
+ kind: "text",
2406
+ text: compact ? `${index === 0 ? "" : ","}${JSON.stringify(key)}:` : `${index === 0 ? "\n" : ",\n"}${JSON_INDENT.repeat(childDepth)}${JSON.stringify(key)}: `
2407
+ });
2408
+ }
2409
+ }
2410
+ return chunks.join("");
2411
+ }
2412
+ /** Render one present program completion value for the model-facing result text. */
2413
+ function renderValue(value) {
2414
+ return typeof value === "string" ? value : renderJsonValue(value);
2415
+ }
2416
+ /**
2417
+ * Build the `run_code` {@link ToolDefinition}: required `code` and
2418
+ * `description` parameters, executed through the dispatch bridge described
2419
+ * above. The
2420
+ * registry reserves it as presentation infrastructure under non-native modes,
2421
+ * outside the filterable global/scoped capability layers.
2422
+ * @param registry - the owning registry (sub-calls go through its `execute`,
2423
+ * bindings cover its registered tools).
2424
+ * @param options - the registry-private capabilities described above.
2425
+ * @returns the registry-ready definition.
2426
+ */
2427
+ function createRunCodeTool(registry, options) {
2428
+ const { requireRuntime, peekRuntime, maxParallel, shapeDispatchLog } = options;
2429
+ const definition = defineTool({
2430
+ name: RUN_CODE_NAME,
2431
+ description: TYPESCRIPT_FLAVOR.description,
2432
+ parameters: {
2433
+ code: {
2434
+ type: "string",
2435
+ required: true,
2436
+ description: TYPESCRIPT_FLAVOR.codeDescription
2437
+ },
2438
+ description: {
2439
+ type: "string",
2440
+ required: true,
2441
+ description: RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION
2442
+ }
2443
+ },
2444
+ output: {
2445
+ schema: {
2446
+ type: "object",
2447
+ additionalProperties: false,
2448
+ properties: {
2449
+ logs: {
2450
+ type: "array",
2451
+ required: true,
2452
+ items: { type: "string" }
2453
+ },
2454
+ result: { type: "json" }
2455
+ }
2456
+ },
2457
+ render: (_args, value) => {
2458
+ const rendered = value.result === void 0 ? "" : renderValue(value.result);
2459
+ const parts = [value.logs.join("\n"), rendered].filter((part) => part.length > 0);
2460
+ return [{
2461
+ type: "text",
2462
+ text: parts.length > 0 ? parts.join("\n") : "(run_code completed with no output)"
2463
+ }];
2464
+ }
2465
+ },
2466
+ async execute(args, exec) {
2467
+ if (args.description.trim().length === 0) throw new Error("invalid description: expected a non-empty string");
2468
+ const runtime = requireRuntime();
2469
+ const runController = new AbortController();
2470
+ const onOuterAbort = () => {
2471
+ runController.abort(exec.signal.reason);
2472
+ };
2473
+ exec.signal.addEventListener("abort", onOuterAbort, { once: true });
2474
+ let dispatches = 0;
2475
+ const pendingQueue = [];
2476
+ const inFlight = /* @__PURE__ */ new Set();
2477
+ /** Tracked settle-event side work (log-content listener + append), drained at run settlement. */
2478
+ const logWork = /* @__PURE__ */ new Set();
2479
+ const commitQueue = [];
2480
+ let exclusiveActive = false;
2481
+ let driving = false;
2482
+ let driverRun = Promise.resolve();
2483
+ let wake;
2484
+ const wakeup = () => {
2485
+ const release = wake;
2486
+ wake = void 0;
2487
+ release?.();
2488
+ };
2489
+ /**
2490
+ * The single ordered lane. Each pass commits the head-of-line settled
2491
+ * dispatch (ordered post-execute), then starts the next queued entry if
2492
+ * its slot is free (ordered pre-execute), and otherwise sleeps until a
2493
+ * body settles or a new submission arrives. One run reaching the
2494
+ * empty-queues/empty-pool state is quiescence.
2495
+ */
2496
+ const drive = () => {
2497
+ if (driving) return driverRun;
2498
+ driving = true;
2499
+ driverRun = (async () => {
2500
+ try {
2501
+ for (;;) {
2502
+ const signal = new Promise((resolve) => {
2503
+ wake = resolve;
2504
+ });
2505
+ const commitHead = commitQueue[0];
2506
+ if (commitHead !== void 0 && commitHead.settled) {
2507
+ commitQueue.shift();
2508
+ await commitHead.commit();
2509
+ if (commitHead.mode === "exclusive") exclusiveActive = false;
2510
+ continue;
2511
+ }
2512
+ const head = pendingQueue[0];
2513
+ if (head !== void 0) {
2514
+ if (runController.signal.aborted) {
2515
+ pendingQueue.shift();
2516
+ head.abandon();
2517
+ continue;
2518
+ }
2519
+ const mode = head.classify();
2520
+ if (!exclusiveActive && (mode === "exclusive" ? inFlight.size === 0 : inFlight.size < maxParallel)) {
2521
+ if (mode === "exclusive") exclusiveActive = true;
2522
+ head.mode = mode;
2523
+ pendingQueue.shift();
2524
+ commitQueue.push(head);
2525
+ await head.start();
2526
+ const flight = head.flight.finally(() => {
2527
+ inFlight.delete(flight);
2528
+ wakeup();
2529
+ });
2530
+ inFlight.add(flight);
2531
+ continue;
2532
+ }
2533
+ }
2534
+ if (pendingQueue.length === 0 && commitQueue.length === 0 && inFlight.size === 0) return;
2535
+ await signal;
2536
+ }
2537
+ } finally {
2538
+ driving = false;
2539
+ wake = void 0;
2540
+ }
2541
+ })();
2542
+ return driverRun;
2543
+ };
2544
+ /** Every dispatch settled AND committed; nothing can start (the run is aborted at call time). */
2545
+ const drainDispatches = async () => {
2546
+ await drive();
2547
+ while (logWork.size > 0) await Promise.allSettled([...logWork]);
2548
+ };
2549
+ const runOver = () => runController.signal.aborted;
2550
+ const binding = (name) => async (rawArgs) => {
2551
+ if (runOver()) throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} not dispatched`);
2552
+ const normalized = jsonNormalizeArgs(rawArgs);
2553
+ const n = ++dispatches;
2554
+ const subCallId = CallId(`${String(exec.callId)}:code:${n}`);
2555
+ const input = {
2556
+ callId: subCallId,
2557
+ rootCallId: exec.rootCallId,
2558
+ name,
2559
+ arguments: normalized.dispatched,
2560
+ ...exec.agent ? { agent: exec.agent } : {},
2561
+ parent: exec.token,
2562
+ signal: runController.signal
2563
+ };
2564
+ const scheduler = registry[TOOL_RUNTIME_SCHEDULER];
2565
+ const outcome = await new Promise((resolve, reject) => {
2566
+ let parked;
2567
+ const settle = (result) => {
2568
+ resolve(result.isError ? {
2569
+ isError: true,
2570
+ message: result.error.message
2571
+ } : {
2572
+ isError: false,
2573
+ value: result.value
2574
+ });
2575
+ const agent = exec.agent;
2576
+ if (agent === void 0) return;
2577
+ const task = (async () => {
2578
+ const logged = await shapeDispatchLog({
2579
+ exec,
2580
+ agent,
2581
+ subCallId,
2582
+ name,
2583
+ isError: result.isError,
2584
+ content: result.content
2585
+ });
2586
+ agent.session.append("tool/code-dispatch", {
2587
+ rootCallId: exec.rootCallId,
2588
+ parentCallId: exec.callId,
2589
+ subCallId,
2590
+ name,
2591
+ arguments: normalized.logged,
2592
+ isError: result.isError,
2593
+ content: logged
2594
+ });
2595
+ })().finally(() => {
2596
+ logWork.delete(task);
2597
+ });
2598
+ logWork.add(task);
2599
+ };
2600
+ pendingQueue.push({
2601
+ flight: Promise.resolve(),
2602
+ settled: false,
2603
+ classify: () => registry.executionMode(input).kind,
2604
+ abandon: () => {
2605
+ reject(/* @__PURE__ */ new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`));
2606
+ },
2607
+ async start() {
2608
+ exec.agent?.session.append("tool/code-dispatch-start", {
2609
+ rootCallId: exec.rootCallId,
2610
+ parentCallId: exec.callId,
2611
+ subCallId,
2612
+ name,
2613
+ arguments: normalized.logged
2614
+ });
2615
+ const prepared = await scheduler.prepare(input);
2616
+ if (prepared.kind === "dispatch") {
2617
+ this.flight = scheduler.dispatch(prepared.exec).then((dispatchOutcome) => {
2618
+ parked = {
2619
+ kind: dispatchOutcome.kind,
2620
+ exec: prepared.exec,
2621
+ result: dispatchOutcome.result
2622
+ };
2623
+ this.settled = true;
2624
+ });
2625
+ return;
2626
+ }
2627
+ parked = {
2628
+ kind: prepared.kind,
2629
+ exec: prepared.exec,
2630
+ result: prepared.result
2631
+ };
2632
+ this.settled = true;
2633
+ },
2634
+ async commit() {
2635
+ /* v8 ignore next -- commit() runs only after `settled` flipped, which set parked. */
2636
+ if (parked === void 0) return;
2637
+ const result = parked.kind === "post-result" ? await scheduler.finalize(parked.exec, parked.result) : scheduler.finish(parked.exec, parked.result);
2638
+ if (!result.isError && result.content.some((block) => block.type === "image")) exec.deferContext(createUserMessage({
2639
+ content: result.content,
2640
+ source: {
2641
+ kind: "plugin",
2642
+ plugin: "tools-code-mode"
2643
+ }
2644
+ }));
2645
+ for (const context of result.additionalContexts ?? []) exec.deferContext(context);
2646
+ if (result.concludesTurn) exec.concludeTurn();
2647
+ settle(result);
2648
+ while (logWork.size > maxParallel) await Promise.race(logWork);
2649
+ }
2650
+ });
2651
+ wakeup();
2652
+ drive();
2653
+ });
2654
+ if (runOver()) throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} result discarded`);
2655
+ if (outcome.isError) throw new Error(outcome.message);
2656
+ return outcome.value;
2657
+ };
2658
+ const functions = Object.create(null);
2659
+ for (const schema of registry.schemas(exec.agent)) {
2660
+ if (schema.name === "run_code") continue;
2661
+ Object.defineProperty(functions, schema.name, {
2662
+ enumerable: true,
2663
+ value: binding(schema.name)
2664
+ });
2665
+ }
2666
+ try {
2667
+ let result;
2668
+ try {
2669
+ result = await runtime.run({
2670
+ program: args.code,
2671
+ bindings: [{
2672
+ global: "tools",
2673
+ functions,
2674
+ errorClass: {
2675
+ name: "ToolCallError",
2676
+ memberNameProperty: "toolName"
2677
+ }
2678
+ }],
2679
+ signal: runController.signal
2680
+ });
2681
+ } finally {
2682
+ runController.abort("run_code settled");
2683
+ await drainDispatches();
2684
+ }
2685
+ if (result.error) {
2686
+ const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.join("\n")}` : "";
2687
+ throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}`);
2688
+ }
2689
+ return {
2690
+ logs: result.logs,
2691
+ ...result.value !== void 0 ? { result: result.value } : {}
2692
+ };
2693
+ } finally {
2694
+ exec.signal.removeEventListener("abort", onOuterAbort);
2695
+ }
2696
+ },
2697
+ presentCall: (args) => ({
2698
+ card: "generic",
2699
+ title: args.description,
2700
+ kind: "execute",
2701
+ rawInput: args.code
2702
+ })
2703
+ });
2704
+ Object.defineProperty(definition, "description", {
2705
+ enumerable: true,
2706
+ get: () => resolveFlavor(peekRuntime).description
2707
+ });
2708
+ Object.defineProperty(definition, "parameters", {
2709
+ enumerable: true,
2710
+ get: () => parameterSchemaSpecToJsonSchema({
2711
+ code: {
2712
+ type: "string",
2713
+ required: true,
2714
+ description: resolveFlavor(peekRuntime).codeDescription
2715
+ },
2716
+ description: {
2717
+ type: "string",
2718
+ required: true,
2719
+ description: RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION
2720
+ }
2721
+ })
2722
+ });
2723
+ return definition;
2724
+ }
2725
+ /**
2726
+ * Code Mode codegen: the pure projection from registered tool schemas to the TypeScript SDK
2727
+ * text the model programs against (the `tools:sdk` prompt section). Sibling of
2728
+ * `json-schema.ts` — `schemas()` (native function calling) and this module (the generated
2729
+ * `declare const tools` API) are two projections of the same store.
2730
+ * @module @deepseek-ai/dsh-tools/src/ts-types
2731
+ */
2732
+ /** Property names that are valid bare TS identifiers; anything else is quoted. */
2733
+ const IDENTIFIER$1 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
2734
+ /** Render an object key: bare when it is a valid identifier, quoted otherwise (every name stays reachable, no aliasing). */
2735
+ function renderKey(name) {
2736
+ return IDENTIFIER$1.test(name) ? name : JSON.stringify(name);
2737
+ }
2738
+ /** One `indent`-deep line prefix (two spaces per level). */
2739
+ function pad$1(indent) {
2740
+ return " ".repeat(indent);
2741
+ }
2742
+ /** A one-line JSDoc block for a schema `description`, or no lines when there is none. */
2743
+ function docLines$1(description, indent) {
2744
+ if (typeof description !== "string" || description.length === 0) return [];
2745
+ const collapsed = description.replace(/\s+/g, " ").trim();
2746
+ return [`${pad$1(indent)}/** ${collapsed.replaceAll("*/", String.raw`*\/`)} */`];
2747
+ }
2748
+ /** Render one scalar already validated by the unified schema boundary. */
2749
+ function renderScalar(value) {
2750
+ return JSON.stringify(value);
2751
+ }
2752
+ /** Render a validated scalar `const`/`enum`, falling back to the broad type. */
2753
+ function renderConstrainedScalar$1(node, type) {
2754
+ const broad = type === "integer" ? "number" : type;
2755
+ if (Object.hasOwn(node, "const")) return renderScalar(node.const);
2756
+ if (Object.hasOwn(node, "enum")) return node.enum.map(renderScalar).join(" | ");
2757
+ return broad;
2758
+ }
2759
+ /** Build one document from captured parts while retaining the legacy array-parenthesization test. */
2760
+ function typeDocumentFrom(parts) {
2761
+ return {
2762
+ parts,
2763
+ containsUnionOrIntersection: parts.some((part) => typeof part === "string" ? part.includes("|") || part.includes("&") : part.containsUnionOrIntersection)
2764
+ };
2765
+ }
2766
+ /** Build a small document without an intermediate array at each call site. */
2767
+ function typeDocument(...parts) {
2768
+ return typeDocumentFrom(parts);
2769
+ }
2770
+ /** Flatten a nested document with an explicit work stack. */
2771
+ function flattenTypeDocument(document) {
2772
+ const chunks = [];
2773
+ const tasks = [document];
2774
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
2775
+ if (typeof task === "string") {
2776
+ chunks.push(task);
2777
+ continue;
2778
+ }
2779
+ for (let index = task.parts.length - 1; index >= 0; index--) {
2780
+ const part = task.parts[index];
2781
+ /* v8 ignore next -- the loop is bounded by the captured part count. */
2782
+ if (part !== void 0) tasks.push(part);
2783
+ }
2784
+ }
2785
+ return chunks.join("");
2786
+ }
2787
+ /** Initialize one schema-render frame with empty aggregation state. */
2788
+ function schemaRenderFrame(node, indent) {
2789
+ return {
2790
+ node,
2791
+ indent,
2792
+ phase: "start",
2793
+ children: [],
2794
+ childIndex: 0,
2795
+ childDocuments: [],
2796
+ entries: []
2797
+ };
2798
+ }
2799
+ /** Render an already asserted schema to a composable document. */
2800
+ function renderSupportedSchema(schema, indent) {
2801
+ const frames = [schemaRenderFrame(schema, indent)];
2802
+ let rootDocument;
2803
+ const finish = (document) => {
2804
+ frames.pop();
2805
+ const parent = frames.at(-1);
2806
+ if (parent === void 0) rootDocument = document;
2807
+ else parent.childDocuments.push(document);
2808
+ };
2809
+ while (frames.length > 0) {
2810
+ const frame = frames.at(-1);
2811
+ /* v8 ignore next -- the loop condition guarantees a current frame. */
2812
+ if (frame === void 0) break;
2813
+ if (frame.phase === "children") {
2814
+ if (frame.childIndex < frame.children.length) {
2815
+ const child = frame.children[frame.childIndex];
2816
+ /* v8 ignore next -- childIndex is bounded by children.length. */
2817
+ if (child === void 0) throw new Error("missing schema render child");
2818
+ frame.childIndex++;
2819
+ frames.push(schemaRenderFrame(child.node, child.indent));
2820
+ continue;
2821
+ }
2822
+ if (frame.kind === "oneOf") {
2823
+ const parts = [];
2824
+ for (let index = 0; index < frame.childDocuments.length; index++) {
2825
+ if (index > 0) parts.push(" | ");
2826
+ const child = frame.childDocuments[index];
2827
+ /* v8 ignore next -- child documents correspond one-to-one with children. */
2828
+ if (child !== void 0) parts.push(child);
2829
+ }
2830
+ finish(typeDocumentFrom(parts));
2831
+ continue;
2832
+ }
2833
+ if (frame.kind === "array") {
2834
+ const child = frame.childDocuments[0];
2835
+ /* v8 ignore next -- array frames always schedule exactly one child. */
2836
+ if (child === void 0) throw new Error("missing array item type");
2837
+ finish(child.containsUnionOrIntersection ? typeDocument("(", child, ")[]") : typeDocument(child, "[]"));
2838
+ continue;
2839
+ }
2840
+ const required = new Set(frame.node.required);
2841
+ const parts = ["{"];
2842
+ for (let index = 0; index < frame.entries.length; index++) {
2843
+ const entry = frame.entries[index];
2844
+ const child = frame.childDocuments[index];
2845
+ /* v8 ignore next -- object entries and child documents have the same length. */
2846
+ if (entry === void 0 || child === void 0) throw new Error("missing object property type");
2847
+ const [name, prop] = entry;
2848
+ for (const line of docLines$1(prop.description, frame.indent + 1)) parts.push("\n", line);
2849
+ parts.push("\n", `${pad$1(frame.indent + 1)}${renderKey(name)}${required.has(name) ? "" : "?"}: `, child, ";");
2850
+ }
2851
+ parts.push("\n", `${pad$1(frame.indent)}}`);
2852
+ const declared = typeDocumentFrom(parts);
2853
+ finish(frame.node.additionalProperties === false ? declared : typeDocument(declared, " & Record<string, JsonValue>"));
2854
+ continue;
2855
+ }
2856
+ const node = frame.node;
2857
+ if (node.oneOf !== void 0) {
2858
+ frame.kind = "oneOf";
2859
+ frame.children = Array.from(node.oneOf, (child) => ({
2860
+ node: child,
2861
+ indent: frame.indent
2862
+ }));
2863
+ frame.childIndex = 0;
2864
+ frame.childDocuments = [];
2865
+ frame.phase = "children";
2866
+ continue;
2867
+ }
2868
+ if (node.type === void 0) {
2869
+ finish(typeDocument("JsonValue"));
2870
+ continue;
2871
+ }
2872
+ switch (node.type) {
2873
+ case "string":
2874
+ case "number":
2875
+ case "integer":
2876
+ case "boolean":
2877
+ case "null":
2878
+ finish(typeDocument(renderConstrainedScalar$1(node, node.type)));
2879
+ break;
2880
+ case "array":
2881
+ if (node.items === void 0) finish(typeDocument("JsonValue[]"));
2882
+ else {
2883
+ frame.kind = "array";
2884
+ frame.children = [{
2885
+ node: node.items,
2886
+ indent: frame.indent
2887
+ }];
2888
+ frame.childIndex = 0;
2889
+ frame.childDocuments = [];
2890
+ frame.phase = "children";
2891
+ }
2892
+ break;
2893
+ case "object": {
2894
+ const open = node.additionalProperties !== false;
2895
+ const entries = Object.entries(node.properties ?? {});
2896
+ if (entries.length === 0) finish(typeDocument(open ? "Record<string, JsonValue>" : "Record<string, never>"));
2897
+ else {
2898
+ frame.kind = "object";
2899
+ frame.entries = entries;
2900
+ frame.children = entries.map(([, child]) => ({
2901
+ node: child,
2902
+ indent: frame.indent + 1
2903
+ }));
2904
+ frame.childIndex = 0;
2905
+ frame.childDocuments = [];
2906
+ frame.phase = "children";
2907
+ }
2908
+ break;
2909
+ }
2910
+ /* v8 ignore next -- assertSupportedJsonSchema narrowed this closed type union. */
2911
+ default: finish(typeDocument("unknown"));
2912
+ }
2913
+ }
2914
+ /* v8 ignore next -- every root frame produces one document. */
2915
+ return rootDocument ?? typeDocument("unknown");
2916
+ }
2917
+ /**
2918
+ * Map one enforced JSON-Schema node to a TypeScript type literal. Supports
2919
+ * every unified schema construct and returns `unknown` for malformed or
2920
+ * unsupported inputs without throwing.
2921
+ * @param schema - the JSON-Schema node (any shape; hostile inputs degrade).
2922
+ * @param indent - the indentation level for nested object members.
2923
+ * @returns the TS type text (multi-line for objects with properties).
2924
+ */
2925
+ function jsonSchemaToTs(schema, indent = 0) {
2926
+ try {
2927
+ assertSupportedJsonSchema(schema);
2928
+ return flattenTypeDocument(renderSupportedSchema(schema, indent));
2929
+ } catch {
2930
+ return "unknown";
2931
+ }
2932
+ }
2933
+ /** The fixed model-facing usage contract rendered above the declarations (see the Code Mode Agent Note's "What the model sees"). */
2934
+ const SDK_INSTRUCTIONS$1 = `## Writing code for run_code
2935
+
2936
+ \`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:
2937
+
2938
+ - 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.
2939
+ - 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.
2940
+ - 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\`.
2941
+ - 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.
2942
+
2943
+ The available tools:`;
2944
+ /**
2945
+ * Render the full `tools:sdk` prompt section: the fixed usage instructions
2946
+ * plus one `declare const tools` interface covering every given tool.
2947
+ * Deterministic — tools are emitted in lexicographic name order, so an
2948
+ * unchanged tool set produces byte-identical text across assemblies. The sort
2949
+ * is not a total order on byte-equal names, so two schemas sharing a name
2950
+ * would render in argument order; the caller's visible-capability map is keyed
2951
+ * by name, so the input never carries a duplicate.
2952
+ * @param schemas - the tool schemas to declare (the caller excludes
2953
+ * `run_code` itself).
2954
+ * @returns the complete section text.
2955
+ */
2956
+ function renderToolsSdk(schemas) {
2957
+ const sorted = [...schemas].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
2958
+ const argsMembers = [];
2959
+ const outputMembers = [];
2960
+ for (const schema of sorted) {
2961
+ argsMembers.push(...docLines$1(schema.description, 1));
2962
+ argsMembers.push(`${pad$1(1)}${renderKey(schema.name)}: ${jsonSchemaToTs(schema.parameters, 1)};`);
2963
+ outputMembers.push(`${pad$1(1)}${renderKey(schema.name)}: ${jsonSchemaToTs(schema.output, 1)};`);
2964
+ }
2965
+ return `${SDK_INSTRUCTIONS$1}\n\n\`\`\`ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\n${[
2966
+ `interface ToolArgsMap {${argsMembers.length > 0 ? `\n${argsMembers.join("\n")}\n` : ""}}`,
2967
+ `interface ToolOutputMap {${outputMembers.length > 0 ? `\n${outputMembers.join("\n")}\n` : ""}}`,
2968
+ "type ToolName = keyof ToolOutputMap",
2969
+ [
2970
+ "declare class ToolCallError extends Error {",
2971
+ " readonly name: \"ToolCallError\";",
2972
+ " readonly toolName: ToolName;",
2973
+ "}"
2974
+ ].join("\n"),
2975
+ [
2976
+ "declare const tools: {",
2977
+ " [K in ToolName]: (args: ToolArgsMap[K]) => Promise<ToolOutputMap[K]>;",
2978
+ "}"
2979
+ ].join("\n")
2980
+ ].join("\n\n")}\n\`\`\``;
2981
+ }
2982
+ /**
2983
+ * Code Mode codegen — Python flavor. The pure projection from registered tool schemas to the
2984
+ * Python SDK text the model programs against under `runtime.language === 'python'`. Sibling of
2985
+ * {@link ./ts-types.ts | ts-types.ts}; the two files are two projections of the same registry
2986
+ * store, keyed by the loaded {@link @deepseek-ai/dsh-code-runtime#CodeRuntime.language | code
2987
+ * runtime's language}.
2988
+ *
2989
+ * Under `mode: 'code'` the native tool schemas are omitted from the request, so this generated
2990
+ * SDK is the model's ONLY source for each tool's argument names, required fields, types,
2991
+ * descriptions, and canonical output shapes; under `mode: 'both'` the native schemas ship
2992
+ * alongside it and it is one of two. Object-shaped arguments and outputs therefore render as one
2993
+ * named `TypedDict` per tool (and per nested object), not an opaque `dict[str, Any]`, so the
2994
+ * shape survives into the program under the mode that has nothing else to carry it.
2995
+ * @module @deepseek-ai/dsh-tools/src/py-types
2996
+ */
2997
+ /**
2998
+ * The reference grammar's `xid_start xid_continue*` — the set
2999
+ * `str.isidentifier()` accepts on a CPython whose Unicode tables match the
3000
+ * engine's. See {@link isBareIdentifier} for what a version skew does.
3001
+ */
3002
+ const IDENTIFIER = /^[\p{XID_Start}_]\p{XID_Continue}*$/u;
3003
+ /**
3004
+ * Whether a name can be emitted as a bare Python identifier rather than
3005
+ * routed to the subscript/`dict[str, Any]` path.
3006
+ *
3007
+ * Python identifiers are not ASCII: `路径` is as legal a field name as `path`,
3008
+ * and rejecting it would degrade the whole enclosing object, dropping every
3009
+ * field's name, requiredness, and type — information whose only source under
3010
+ * `mode: 'code'` is this generated text.
3011
+ *
3012
+ * NFKC stability is a second and separate condition, because CPython
3013
+ * normalizes identifiers at compile time while JSON keys are compared as
3014
+ * written: `field` would be declared and reachable as `field`, so the SDK would
3015
+ * advertise a key under a spelling the harness never accepts, and two keys
3016
+ * that normalize together would collapse into one declaration. Those names
3017
+ * take the subscript path, which carries their exact bytes.
3018
+ *
3019
+ * `IDENTIFIER` matches `str.isidentifier()` (measured on Node 22.23.1 vs
3020
+ * CPython 3.9.6 tables): the equivalence holds inside the two versions' shared
3021
+ * tables, and the skew characters below are exactly where that pair diverges.
3022
+ * The predicate as a whole is deliberately stricter than `isidentifier()`,
3023
+ * which does not test NFKC stability: `'field'.isidentifier()` is True and
3024
+ * this returns false.
3025
+ *
3026
+ * Both conditions are evaluated against the ENGINE's Unicode tables, and the
3027
+ * two sides are versioned independently — `\p{XID_Start}`/`\p{XID_Continue}`
3028
+ * follow the running engine (Node 22.23.1 reports Unicode 17.0) while CPython
3029
+ * follows its own (3.9.6 reports 13.0.0). The skew is not symmetric. A CPython
3030
+ * older than the engine is the dangerous direction: a character added to either
3031
+ * property since its tables (U+10570 Vithkuqi and U+1E290 Toto, 14.0; U+1E4D0
3032
+ * Nag Mundari, 15.0; U+1C89 Cyrillic TJE, 16.0 — ages per `DerivedAge.txt`; all
3033
+ * four are NFKC-stable and accepted here, and all four are `Cn` on that 3.9.6,
3034
+ * which rejects them) is emitted bare and its tokenizer refuses the character,
3035
+ * taking the whole SDK block down — the same parseability invariant
3036
+ * {@link UNPRINTABLE}, {@link LONE_SURROGATE} and {@link MAX_LIST_NESTING}
3037
+ * exist for. Both properties carry it: a character added only to `XID_Continue`
3038
+ * passes the trailing `\p{XID_Continue}*` in a tail position and fails the same
3039
+ * way — U+200C ZWNJ and U+200D ZWJ are that case, gaining `XID_Continue` in UCD
3040
+ * 15.1 and absent from it in 13.0.0, 14.0.0 and 15.0.0, so `a\u{200C}b` is
3041
+ * emitted bare here while `isidentifier()` is False on 3.9.6 and on 3.12.13
3042
+ * (15.0.0). A CPython newer than the engine only routes a legal name to the
3043
+ * subscript/`dict[str, Any]` path: less readable, still correct. The NFKC
3044
+ * condition reduces to the same skew, since normalization stability guarantees
3045
+ * an assigned character's normalization never changes afterwards.
3046
+ *
3047
+ * This predicate is not the only reader of engine tables. {@link camelCase}
3048
+ * reads them at three further points — its split set, its head test, and its
3049
+ * `toUpperCase()` case mapping — and this predicate's verdict gates none of
3050
+ * them: a class name derived there reaches emitted text whenever any object
3051
+ * shape in the tool's schema declares a `TypedDict`, including for a tool this
3052
+ * predicate rejected. A tool named `zz-\u{1E4D0}x` with such parameters never
3053
+ * reaches the skew here (the `-` rejects it outright) yet emits `class
3054
+ * Zz\u{1E4D0}xArgs`, which that same 3.9.6 refuses — Nag Mundari arrived two
3055
+ * releases after its tables. The case mapping is a separate table rather than
3056
+ * an XID membership test, and it fails on names both conditions above accept:
3057
+ * `\u{019B}` is XID_Start and NFKC-stable, so this predicate accepts it and
3058
+ * `async def \u{019B}` compiles on 3.9.6, but Node uppercases it to
3059
+ * `\u{A7DC}` — unassigned in that CPython, whose own `.upper()` is the identity
3060
+ * here — and the declared `class \u{A7DC}Args` fails with `invalid
3061
+ * non-printable character U+A7DC`. Closing the exposure therefore covers all
3062
+ * four read points, not this predicate alone; it needs the target interpreter's
3063
+ * version, which the backend reporting `language: 'python'` owns; the
3064
+ * language-dispatch Agent Note records the deferral.
3065
+ *
3066
+ * The `ts-types` sibling keeps its own ASCII rule rather than sharing this
3067
+ * one: ECMAScript identifiers are a different set (`$`) and are never
3068
+ * normalized, so one predicate cannot be correct for both. ZWJ/ZWNJ are not
3069
+ * part of that difference — both sets carry them on the engine's tables; what
3070
+ * separates the two there is the CPython table version above.
3071
+ * @param name - the raw schema field or tool name.
3072
+ * @returns whether the name can be emitted bare.
3073
+ */
3074
+ function isBareIdentifier(name) {
3075
+ return IDENTIFIER.test(name) && name.normalize("NFKC") === name;
3076
+ }
3077
+ /**
3078
+ * Python hard keywords: reserved everywhere, so a tool or field named
3079
+ * ``class`` or ``lambda`` is legal on the wire but not as an attribute
3080
+ * (``tools.class`` would be a SyntaxError in the model program) and not as a
3081
+ * class-syntax `TypedDict` field. Such a tool renders under subscript access
3082
+ * and such an object degrades to ``dict[str, Any]`` — the model still reaches
3083
+ * every tool and field without collisions.
3084
+ * Soft keywords (``match``, ``case``, ``type``, ``_`` — the language
3085
+ * reference's whole set) are deliberately ABSENT: each is special in exactly
3086
+ * one syntactic position — a statement head (``match``, ``type``), a ``match``
3087
+ * statement's clause head (``case``), or a pattern (``_``) — so ``match: str``
3088
+ * as a field and ``async def match(...)`` as a method are both legal, and
3089
+ * including them would needlessly degrade common search/regex tool fields to
3090
+ * ``dict[str, Any]``. Underscore-leading names are handled separately, not
3091
+ * here: a non-dunder ``__token`` name-mangles, a dunder present on
3092
+ * ``object``/``type`` resolves before the proxy hook, and implicit
3093
+ * special-method lookup bypasses the hook.
3094
+ */
3095
+ const RESERVED = /* @__PURE__ */ new Set([
3096
+ "False",
3097
+ "None",
3098
+ "True",
3099
+ "and",
3100
+ "as",
3101
+ "assert",
3102
+ "async",
3103
+ "await",
3104
+ "break",
3105
+ "class",
3106
+ "continue",
3107
+ "def",
3108
+ "del",
3109
+ "elif",
3110
+ "else",
3111
+ "except",
3112
+ "finally",
3113
+ "for",
3114
+ "from",
3115
+ "global",
3116
+ "if",
3117
+ "import",
3118
+ "in",
3119
+ "is",
3120
+ "lambda",
3121
+ "nonlocal",
3122
+ "not",
3123
+ "or",
3124
+ "pass",
3125
+ "raise",
3126
+ "return",
3127
+ "try",
3128
+ "while",
3129
+ "with",
3130
+ "yield",
3131
+ "__debug__"
3132
+ ]);
3133
+ /** `typing` symbols this module may emit, in the deterministic import order. */
3134
+ const TYPING_ORDER = [
3135
+ "Any",
3136
+ "Literal",
3137
+ "NotRequired",
3138
+ "Protocol",
3139
+ "TypedDict"
3140
+ ];
3141
+ /** `indent`-deep line prefix (four spaces per level to match PEP 8 output). */
3142
+ function pad(indent) {
3143
+ return " ".repeat(indent);
3144
+ }
3145
+ /**
3146
+ * The `Cc` code points that survive the whitespace collapse in {@link describe}
3147
+ * and have no printable form: the C0 controls, DEL, and the C1 controls. Only
3148
+ * U+0009 to U+000D are absent, because ECMAScript `\s` already collapsed them —
3149
+ * `\s` is TAB/VT/FF/SP/NBSP/ZWNBSP/Zs plus LF/CR/LS/PS, so no C1 code point is
3150
+ * in it and the whole U+0080 to U+009F block reaches this rule intact. Those
3151
+ * are not hypothetical input: they are what Windows-1252 bytes 0x80 to 0x9F
3152
+ * (smart quotes, em dash) become when decoded as Latin-1.
3153
+ * CPython rejects source containing a NUL outright
3154
+ * (`SyntaxError: source code string cannot contain null bytes`), whether it
3155
+ * sits in a docstring or in a comment, so one such byte anywhere in a schema
3156
+ * description would make the whole generated SDK unparseable — under
3157
+ * `mode: 'code'`, the model's only declaration of the tools. The rest are
3158
+ * legal but invisible; escaping them with the same rule keeps the emitted text
3159
+ * readable and the treatment uniform.
3160
+ *
3161
+ * The boundary is the category, not per-code-point addressability: `\xNN`
3162
+ * addresses U+0000 to U+00FF, so one escape form covers `Cc` exactly. The
3163
+ * invisible `Cf` formatting characters pass through by design — of them only
3164
+ * U+00AD soft hyphen would fit `\xNN` at all, and escaping that one while
3165
+ * U+200B ZWSP, U+200E/U+200F bidi marks, and U+2060 word joiner passed through
3166
+ * would leave a rule that is neither category- nor addressability-shaped. The
3167
+ * whole family is legal in both consumers, since only LF and CR terminate a
3168
+ * Python string literal or a `#` comment. That set is the tokenizer's, not
3169
+ * `str.splitlines()`': NEL (U+0085), LS (U+2028), and PS (U+2029) split a
3170
+ * string at run time but do not end a physical line in source — measured on
3171
+ * CPython 3.9.6 and 3.12.13, each accepted in both positions with the value
3172
+ * round-tripping — so they are safe raw wherever they reach emitted text
3173
+ * unescaped, which for all three is `JSON.stringify`, at two call sites:
3174
+ * {@link pyScalar}'s literal path, and the subscript tool-name comment's own
3175
+ * call, which a name carrying any of them always reaches, none being
3176
+ * `XID_Continue`. The `description` path escapes NEL under the class above and
3177
+ * folds LS and PS in {@link describe}'s `\s+` collapse, both being `\s`.
3178
+ */
3179
+ const UNPRINTABLE = /[\u0000-\u0008\u000e-\u001f\u007f-\u009f]/g;
3180
+ /**
3181
+ * Unpaired surrogate code points, escaped by {@link describe} as `\uNNNN` —
3182
+ * its own form, since `\xNN` stops at U+00FF. The `u` flag is what makes this
3183
+ * the LONE ones: in Unicode mode a well-formed pair is a single astral code
3184
+ * point outside D800 to DFFF, so an emoji in a description survives untouched.
3185
+ *
3186
+ * This is the NUL case from {@link UNPRINTABLE}, not the invisible-character
3187
+ * case. Python source must be UTF-8-encodable and a lone surrogate is not, so
3188
+ * `compile()` raises `UnicodeEncodeError: surrogates not allowed` for one
3189
+ * anywhere in the text — measured on 3.9 for a string literal and for a `#`
3190
+ * comment alike. A raw or MCP tool description reaches this: `JSON.parse` on a
3191
+ * wire `"\ud800"` escape yields exactly such a code point.
3192
+ */
3193
+ const LONE_SURROGATE = /[\ud800-\udfff]/gu;
3194
+ /**
3195
+ * The collapsed one-line `description` of a schema node (byte-stable across
3196
+ * formatting churn), or `undefined` when the node carries none. Every caller
3197
+ * passes an object — a validated property node, the `ToolSdkSchema` itself, or
3198
+ * the `{ description }` wrapper {@link docLines} synthesizes — so only the
3199
+ * description field needs guarding. A description that collapses
3200
+ * to nothing (empty, or whitespace only) is `undefined` too: it documents the
3201
+ * node no better than an absent one, and emitting it would leave an empty
3202
+ * `"""` docstring or a bare `# ` line in the SDK. Only ECMAScript whitespace
3203
+ * folds, so a description of whitespace plus one surviving control character is
3204
+ * NOT absent: it collapses to that character's visible escape.
3205
+ *
3206
+ * Control characters left over after the whitespace collapse are rendered as
3207
+ * their `\xNN` escapes (see {@link UNPRINTABLE}) and unpaired surrogates as
3208
+ * their `\uNNNN` escapes (see {@link LONE_SURROGATE}); the escape's own backslash is
3209
+ * emitted literally by both consumers, since {@link docLines} doubles it into a
3210
+ * Python source escape and a `#` comment carries it verbatim.
3211
+ */
3212
+ function describe(schema) {
3213
+ const description = schema.description;
3214
+ if (typeof description !== "string") return void 0;
3215
+ const collapsed = description.replace(/\s+/g, " ").replace(UNPRINTABLE, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`).replace(LONE_SURROGATE, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`).trim();
3216
+ return collapsed.length === 0 ? void 0 : collapsed;
3217
+ }
3218
+ /**
3219
+ * One-line docstring for a tool `description`, or no lines when there is none.
3220
+ * Backslashes are doubled first, every quote is escaped, and a trailing
3221
+ * backslash cannot survive: a description ending in `"` or an odd backslash
3222
+ * would otherwise merge with (or escape) the closing triple quote and make
3223
+ * the generated block — Code Mode's only SDK — syntactically invalid Python.
3224
+ */
3225
+ function docLines(description, indent) {
3226
+ const collapsed = describe({ description });
3227
+ if (collapsed === void 0) return [];
3228
+ const escaped = collapsed.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
3229
+ return [`${pad(indent)}"""${escaped}"""`];
3230
+ }
3231
+ /**
3232
+ * CamelCase a name into a Python type identifier: non-identifier characters
3233
+ * split words, `_` splits too (it is `XID_Continue`, so the split set names it
3234
+ * explicitly), and a head that cannot start an identifier takes a `Tool`
3235
+ * prefix. Unicode survives, so a `路径` field yields `路径`-based class names
3236
+ * instead of collapsing to the bare prefix. A character that is not
3237
+ * `XID_Continue` splits even when it is a letter, so a name whose NFKC folding
3238
+ * would leave the identifier set is not carried through — the split set is the
3239
+ * grammar's, not an ASCII approximation of it.
3240
+ *
3241
+ * The result is NFKC-normalized: these names are generated, never matched
3242
+ * against a JSON key, so normalizing is free here and keeps what CPython
3243
+ * compiles identical to what is emitted — unlike {@link isBareIdentifier},
3244
+ * which must reject unstable names outright. Normalizing AFTER the prefix
3245
+ * decision is what makes that hold at the seam the prefix creates: `Tool` +
3246
+ * a combining-mark head composes there (`U+0301` gives `Tooĺ`, U+013A), so
3247
+ * normalizing only the un-prefixed part would emit a name CPython compiles to
3248
+ * a different symbol. The second call is idempotent on the un-prefixed arm.
3249
+ *
3250
+ * The split set, the head test, and `toUpperCase()` all read the engine's
3251
+ * Unicode tables, so this function carries the same version skew
3252
+ * {@link isBareIdentifier} documents, by paths independent of it: a class name
3253
+ * derived here reaches emitted text whenever any object shape in the tool's
3254
+ * schema declares a `TypedDict`, and the predicate's verdict on the tool name
3255
+ * does not gate that. The case mapping is the one that can fail on a name the
3256
+ * predicate accepted; the worked example is there.
3257
+ * @param raw - the schema field or tool name to derive from.
3258
+ * @returns a class-name segment safe to emit.
3259
+ */
3260
+ function camelCase(raw) {
3261
+ const joined = raw.split(/[^\p{XID_Continue}]+|_+/u).filter((part) => part.length > 0).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("").normalize("NFKC");
3262
+ return (/^\p{XID_Start}/u.test(joined) ? joined : `Tool${joined}`).normalize("NFKC");
3263
+ }
3264
+ /** Class-name base cap keeping each emitted name — and total text — linear in schema depth. */
3265
+ const MAX_CLASS_NAME_BASE = 120;
3266
+ /**
3267
+ * Deepest `list[…]` nesting emitted into one annotation before the item type
3268
+ * degrades to `Any`. CPython's tokenizer rejects a logical line holding more
3269
+ * than 200 simultaneously-open brackets (`MAXLEVEL`, `SyntaxError: too many
3270
+ * nested parentheses`), so an array chain deeper than that would render an SDK
3271
+ * block that is not valid Python at all — the same failure the docstring
3272
+ * escaping in {@link docLines} exists to prevent. 180 leaves headroom for the
3273
+ * few brackets an annotation can add around the chain, all of which count
3274
+ * toward the same limit. Per emission site, counting brackets open at the
3275
+ * chain's innermost point:
3276
+ *
3277
+ * - Return annotation, `async def f(self, args: X) -> chain:` — 180 `list[`
3278
+ * plus an innermost `Literal[`. The parameter list's `(` closed at the `)`
3279
+ * before the `->`, so it is NOT open here: 181.
3280
+ * - TypedDict field, `field: NotRequired[chain]` — a class-body line with no
3281
+ * other open bracket, and its children start at `listDepth: 1` to reserve
3282
+ * the `NotRequired[`, so 179 `list[` plus `Literal[`: 181. Required fields
3283
+ * share that start for uniformity, spending one level of representable depth
3284
+ * on a bracket they never emit.
3285
+ * - Argument annotation, `async def f(self, args: chain) -> Y:` — the `(` IS
3286
+ * still open around it: 180 `list[` plus `Literal[` plus the paren, 182, the
3287
+ * worst case. Reachable only through a raw `register()` whose `parameters`
3288
+ * is an array reached from the root through `oneOf` arms alone — the root
3289
+ * array itself, or one nested under any depth of unions, since an arm
3290
+ * inherits the enclosing depth unchanged (`A | B` opens no bracket). An
3291
+ * object ancestor takes it out of this case: its fields restart the chain at
3292
+ * the 181 site. `defineTool` compiles an object root, so the annotation is a
3293
+ * bare TypedDict class name or a one-bracket `dict[str, Any]` when that
3294
+ * object degrades — never a chain.
3295
+ *
3296
+ * A CPython grammar limit, not a deployment choice, so it is fixed rather than
3297
+ * configurable. The sibling `ts-types` renderer needs no counterpart: nothing
3298
+ * in the TypeScript grammar bounds nesting, and its SDK block is never type-
3299
+ * checked. Only bracket nesting counts — a `oneOf` renders as a flat `A | B`
3300
+ * chain and nested objects render as separate `class` statements, so neither
3301
+ * accumulates open brackets at any depth. The invariant this cap serves is
3302
+ * grammatical validity; see the `oneOf` arm in {@link renderType} for the one
3303
+ * interpreter limit deliberately left uncapped.
3304
+ */
3305
+ const MAX_LIST_NESTING = 180;
3306
+ /**
3307
+ * Cap a class-name base at {@link MAX_CLASS_NAME_BASE} (see the callers for
3308
+ * why capping keeps the render linear). `slice` counts UTF-16 code units, so
3309
+ * an astral character straddling the boundary would be cut in half and leave a
3310
+ * lone surrogate — not an identifier character, and not even well-formed text;
3311
+ * drop it rather than emit it.
3312
+ */
3313
+ function capClassNameBase(base) {
3314
+ if (base.length <= MAX_CLASS_NAME_BASE) return base;
3315
+ const capped = base.slice(0, MAX_CLASS_NAME_BASE);
3316
+ return /[\uD800-\uDBFF]$/.test(capped) ? capped.slice(0, -1) : capped;
3317
+ }
3318
+ /**
3319
+ * Reserve a unique class name from a base, suffixing `2`, `3`, … on collision.
3320
+ * The base is capped at {@link MAX_CLASS_NAME_BASE} first: child class names
3321
+ * derive from their parent's allocated name (`ParentChild`), so an unbounded
3322
+ * schema of single-field objects would otherwise grow each name by one field
3323
+ * per level and the sum of all names to Θ(depth²). Capping the base keeps each
3324
+ * name — and the total emitted text — linear in depth. Collisions resume from
3325
+ * the per-base counter in `state.nextClassCounter` rather than rescanning from
3326
+ * `2`, so a deep chain sharing one capped base stays O(1) per allocation
3327
+ * (amortized) instead of Θ(depth²) in time.
3328
+ */
3329
+ function allocateClassName(base, state) {
3330
+ const capped = capClassNameBase(base);
3331
+ let name = capped;
3332
+ if (state.usedClassNames.has(name)) {
3333
+ let n = state.nextClassCounter.get(capped) ?? 2;
3334
+ while (state.usedClassNames.has(`${capped}${n}`)) n++;
3335
+ name = `${capped}${n}`;
3336
+ state.nextClassCounter.set(capped, n + 1);
3337
+ }
3338
+ state.usedClassNames.add(name);
3339
+ return name;
3340
+ }
3341
+ /**
3342
+ * Append a child-name segment to a parent class-name base, capping the result
3343
+ * at {@link MAX_CLASS_NAME_BASE}. Capping AT PROPAGATION (not only inside
3344
+ * {@link allocateClassName}) keeps each level O(1): a deep `oneOf`- or
3345
+ * object-chain would otherwise carry an ever-growing ConsString down the tree
3346
+ * and re-materialize it (via `.length`/`.slice`) at every level — Θ(depth²).
3347
+ * The bounded base plus the collision counter still yields unique names.
3348
+ *
3349
+ * The join is NFKC-normalized because both sides are separately normalized yet
3350
+ * their concatenation need not be: a base ending in a Hangul L jamo or LV
3351
+ * syllable composes with a following V or T jamo head (`가` + `ᆨ` gives `각`),
3352
+ * so the emitted class name would differ from the symbol CPython compiles, and
3353
+ * two byte-distinct names could fold onto one — `usedClassNames` dedupes by the
3354
+ * raw bytes, so the collision counter would not see it. Normalizing costs
3355
+ * O(cap + segment) per level, the same order as the `slice` it feeds. The other
3356
+ * two join points need no counterpart: `Args`/`Output` start with `A`/`O` and
3357
+ * {@link allocateClassName}'s suffix is digits, none of which compose backwards.
3358
+ */
3359
+ function childClassName(base, segment) {
3360
+ return capClassNameBase(`${base}${segment}`.normalize("NFKC"));
3361
+ }
3362
+ /**
3363
+ * Render one validated scalar as Python literal text (`True`/`False`,
3364
+ * JSON-quoted strings, bare numbers). `null` cannot reach here: the `null`
3365
+ * type renders directly as `None`, and the unified validator rejects a null
3366
+ * `const`/`enum` entry on every other scalar type.
3367
+ *
3368
+ * A beyond-safe-range integral number takes `BigInt` digits rather than
3369
+ * `String`: Python integers are arbitrary-precision, so the emitted digits ARE
3370
+ * the value the model programs against, and `String` can give a different
3371
+ * integer than the double holds (`2 ** 60` prints the rounded `...847000`, not
3372
+ * the exact `...846976`) or no integer literal at all (`1e21` prints `1e+21`).
3373
+ * `String`'s rounding is not a bug in it: `Number::toString` emits the shortest
3374
+ * decimal string that re-reads to the same double, then pads to the exponent
3375
+ * with zeros (1 significant digit for `1e20`, 16 for `2 ** 60`) — and when the
3376
+ * shortest string is shorter than the double's exact value, those padded digits
3377
+ * name an integer no double holds. Passing one back would have to cross the
3378
+ * argument boundary as a JSON number — a double again — so the SDK would
3379
+ * document a value no program can pass. `BigInt` needs no case split: where
3380
+ * `String` is already exact (`2 ** 53`, `1e20`) the two agree byte for byte,
3381
+ * and where it is not, `BigInt` is the exact one. The TS flavor needs no
3382
+ * counterpart at all: its literal is re-read by a JS parser back into the same
3383
+ * double.
3384
+ *
3385
+ * `JSON.stringify` is also what keeps this path's output parseable, and it is
3386
+ * the only thing that does. It covers both classes of hazard: the two kinds of
3387
+ * code point CPython refuses anywhere in source — NUL among the C0 controls,
3388
+ * and the whole D800–DFFF unpaired-surrogate block, escaped under ES2019
3389
+ * well-formed stringification, which the engines range guarantees — and the
3390
+ * ones that break this line in particular, a bare `"` closing the literal
3391
+ * early, a trailing odd backslash eating the closing quote, and a bare LF/CR
3392
+ * ending it before its terminator. The `description` path carries
3393
+ * {@link UNPRINTABLE} and {@link LONE_SURROGATE} because nothing quotes it,
3394
+ * and folds newlines in {@link describe}.
3395
+ *
3396
+ * That leans on a coincidence worth naming: every escape `JSON.stringify` can
3397
+ * emit (`\"`, `\\`, `\b`, `\f`, `\n`, `\r`, `\t`, `\uXXXX`) is also a Python
3398
+ * escape denoting the same character, so the emitted `Literal[...]` both
3399
+ * parses and decodes back to the value the schema declared. DEL, the C1
3400
+ * controls (NEL among them), and LS/PS (U+2028/U+2029) do reach it raw —
3401
+ * legal but invisible, byte-for-byte as in the TS flavor; escaping them is a
3402
+ * both-flavors change. Those last three are legal here for the reason
3403
+ * {@link UNPRINTABLE} records: they are `str.splitlines()` boundaries, not
3404
+ * tokenizer line terminators. The subscript tool-name comment quotes its name
3405
+ * through its own call to the same `JSON.stringify`, never through this
3406
+ * function, and inherits both halves — escapes and pass-throughs alike.
3407
+ */
3408
+ function pyScalar(value) {
3409
+ if (value === true) return "True";
3410
+ if (value === false) return "False";
3411
+ if (typeof value === "string") return JSON.stringify(value);
3412
+ if (typeof value === "number" && Number.isInteger(value) && !Number.isSafeInteger(value)) return BigInt(value).toString();
3413
+ return String(value);
3414
+ }
3415
+ /**
3416
+ * Render a validated scalar `const`/`enum` as `Literal[...]`, falling back to
3417
+ * the broad type. Deliberately deviates from PEP 586, which restricts `Literal`
3418
+ * parameters to int/bool/str/bytes/enum/None: a non-integral number
3419
+ * `const`/`enum` emits a float literal (`Literal[1.5]`) a strict checker would
3420
+ * reject. An integral one does not deviate — {@link pyScalar} emits int digits,
3421
+ * including for the beyond-safe-range values it widens through `BigInt`, and
3422
+ * PEP 586 admits int parameters. Harmless either way — the stub is advisory
3423
+ * prompt text, only required to parse — and keeping the exact value
3424
+ * communicates the constraint to the model.
3425
+ */
3426
+ function renderConstrainedScalar(node, broad, state) {
3427
+ if (node.const !== void 0) {
3428
+ state.typing.add("Literal");
3429
+ return `Literal[${pyScalar(node.const)}]`;
3430
+ }
3431
+ if (node.enum !== void 0) {
3432
+ state.typing.add("Literal");
3433
+ return `Literal[${node.enum.map(pyScalar).join(", ")}]`;
3434
+ }
3435
+ return broad;
3436
+ }
3437
+ /**
3438
+ * Map one JSON-Schema node to a Python type expression, threading `state` to
3439
+ * collect the `TypedDict` declarations and `typing` symbols a full render
3440
+ * needs. `className` is the name to give an object node with properties (and
3441
+ * the prefix for its nested objects). Handles every unified schema construct —
3442
+ * `oneOf` (→ `X | Y`), `const`/`enum` (→ `Literal[...]`), `integer` (→ `int`),
3443
+ * `null` (→ `None`) — and degrades an unsupported or malformed schema to `Any`
3444
+ * without throwing, the same trusted-after-validation stance as the sibling
3445
+ * {@link ./ts-types.ts | ts-types} renderer. {@link jsonSchemaToPy} is the
3446
+ * context-free entry point; this is the collecting core.
3447
+ */
3448
+ function renderType(schema, className, state) {
3449
+ const newFrame = (schema, className, listDepth) => ({
3450
+ schema,
3451
+ className,
3452
+ phase: "start",
3453
+ listDepth,
3454
+ children: [],
3455
+ childIndex: 0,
3456
+ childTypes: [],
3457
+ entries: []
3458
+ });
3459
+ try {
3460
+ assertSupportedJsonSchema(schema);
3461
+ const frames = [newFrame(schema, className, 0)];
3462
+ let result;
3463
+ const finish = (type) => {
3464
+ frames.pop();
3465
+ const parent = frames.at(-1);
3466
+ if (parent === void 0) result = type;
3467
+ else parent.childTypes.push(type);
3468
+ };
3469
+ while (frames.length > 0) {
3470
+ const frame = frames.at(-1);
3471
+ /* v8 ignore next -- the loop condition guarantees a current frame. */
3472
+ if (frame === void 0) break;
3473
+ if (frame.phase === "children") {
3474
+ if (frame.childIndex < frame.children.length) {
3475
+ const child = frame.children[frame.childIndex];
3476
+ /* v8 ignore next -- childIndex is bounded by children.length. */
3477
+ if (child === void 0) throw new Error("missing python render child");
3478
+ frame.childIndex++;
3479
+ frames.push(newFrame(child.schema, child.className, child.listDepth));
3480
+ continue;
3481
+ }
3482
+ if (frame.kind === "oneOf") {
3483
+ let union = "";
3484
+ for (const [index, childType] of frame.childTypes.entries()) union = index === 0 ? childType : `${union} | ${childType}`;
3485
+ finish(union);
3486
+ continue;
3487
+ }
3488
+ if (frame.kind === "array") {
3489
+ /* v8 ignore next -- the ?? arm needs a childless array frame, which start never builds. */
3490
+ finish(`list[${frame.childTypes[0] ?? "Any"}]`);
3491
+ continue;
3492
+ }
3493
+ const node = frame.node;
3494
+ const name = frame.allocated;
3495
+ /* v8 ignore next -- typeddict frames always set node and allocated at start. */
3496
+ if (node === void 0 || name === void 0) throw new Error("missing typeddict frame state");
3497
+ const required = new Set(node.required);
3498
+ const lines = [`class ${name}(TypedDict):`];
3499
+ for (let index = 0; index < frame.entries.length; index++) {
3500
+ const entry = frame.entries[index];
3501
+ const fieldType = frame.childTypes[index];
3502
+ /* v8 ignore next -- entries and childTypes correspond one-to-one. */
3503
+ if (entry === void 0 || fieldType === void 0) throw new Error("missing typeddict field type");
3504
+ const [field, fieldSchema] = entry;
3505
+ const description = describe(fieldSchema);
3506
+ if (description !== void 0) lines.push(`${pad(1)}# ${description}`);
3507
+ if (required.has(field)) lines.push(`${pad(1)}${field}: ${fieldType}`);
3508
+ else {
3509
+ state.typing.add("NotRequired");
3510
+ lines.push(`${pad(1)}${field}: NotRequired[${fieldType}]`);
3511
+ }
3512
+ }
3513
+ if (node.additionalProperties !== false) lines.push(`${pad(1)}# Additional keys beyond those declared are allowed.`);
3514
+ if (lines.length === 1) lines.push(`${pad(1)}pass`);
3515
+ state.classes.push(lines.join("\n"));
3516
+ finish(name);
3517
+ continue;
3518
+ }
3519
+ frame.phase = "children";
3520
+ const node = frame.schema;
3521
+ if (node.oneOf !== void 0) {
3522
+ frame.kind = "oneOf";
3523
+ frame.children = node.oneOf.map((branch, index) => ({
3524
+ schema: branch,
3525
+ className: childClassName(frame.className, `${index + 1}`),
3526
+ listDepth: frame.listDepth
3527
+ }));
3528
+ continue;
3529
+ }
3530
+ if (node.type === void 0) {
3531
+ state.typing.add("Any");
3532
+ finish("Any");
3533
+ continue;
3534
+ }
3535
+ switch (node.type) {
3536
+ case "string":
3537
+ finish(renderConstrainedScalar(node, "str", state));
3538
+ break;
3539
+ case "number":
3540
+ finish(renderConstrainedScalar(node, "float", state));
3541
+ break;
3542
+ case "integer":
3543
+ finish(renderConstrainedScalar(node, "int", state));
3544
+ break;
3545
+ case "boolean":
3546
+ finish(renderConstrainedScalar(node, "bool", state));
3547
+ break;
3548
+ case "null":
3549
+ finish("None");
3550
+ break;
3551
+ case "array":
3552
+ if (node.items === void 0) {
3553
+ state.typing.add("Any");
3554
+ finish("list[Any]");
3555
+ break;
3556
+ }
3557
+ if (frame.listDepth >= MAX_LIST_NESTING) {
3558
+ state.typing.add("Any");
3559
+ finish("Any");
3560
+ break;
3561
+ }
3562
+ frame.kind = "array";
3563
+ frame.children = [{
3564
+ schema: node.items,
3565
+ className: frame.className,
3566
+ listDepth: frame.listDepth + 1
3567
+ }];
3568
+ break;
3569
+ case "object": {
3570
+ const entries = Object.entries(node.properties ?? {});
3571
+ if (className === "" || !entries.every(([name]) => isBareIdentifier(name) && !RESERVED.has(name) && !(name.startsWith("__") && !name.endsWith("__")))) {
3572
+ state.typing.add("Any");
3573
+ finish("dict[str, Any]");
3574
+ break;
3575
+ }
3576
+ if (entries.length === 0 && node.additionalProperties !== false) {
3577
+ state.typing.add("Any");
3578
+ finish("dict[str, Any]");
3579
+ break;
3580
+ }
3581
+ frame.kind = "typeddict";
3582
+ frame.node = node;
3583
+ frame.allocated = allocateClassName(frame.className, state);
3584
+ state.typing.add("TypedDict");
3585
+ frame.entries = entries;
3586
+ /* v8 ignore next -- allocated is always set before children are built. */
3587
+ frame.children = entries.map(([field, child]) => ({
3588
+ schema: child,
3589
+ className: childClassName(frame.allocated ?? "", camelCase(field)),
3590
+ listDepth: 1
3591
+ }));
3592
+ break;
3593
+ }
3594
+ /* v8 ignore next 4 -- assertSupportedJsonSchema narrowed this closed type union. */
3595
+ default:
3596
+ state.typing.add("Any");
3597
+ finish("Any");
3598
+ }
3599
+ }
3600
+ /* v8 ignore next -- every root frame produces one expression. */
3601
+ return result ?? "Any";
3602
+ } catch {
3603
+ state.typing.add("Any");
3604
+ return "Any";
3605
+ }
3606
+ }
3607
+ /** The fixed model-facing usage contract rendered above the declarations. */
3608
+ const SDK_INSTRUCTIONS = `## Writing code for run_code
3609
+
3610
+ \`run_code\` takes two required arguments: \`code\` — the body of an async Python function (top-level \`await\` and \`return\` both work) — and \`description\`, a short summary of what the program does. At run time exactly two of the names declared below are bound: \`tools\` and \`ToolCallError\`. Everything else is a STATIC STUB describing argument and return types — in particular the \`TypedDict\` classes do NOT exist at run time, so build arguments as plain \`dict\`/\`list\` JSON values: \`await tools.name({"field": 1})\`, never \`FooArgs(field=1)\`, which raises \`NameError\`. Inside the program:
3611
+
3612
+ - Call tools as \`await tools.name(args)\` — subscript access for exotic, reserved, or underscore-leading names: \`await tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value (each method's return type below). Tool arguments must be lossless JSON.
3613
+ - A FAILED tool call raises \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose message is human-readable — wrap in \`try/except\` to handle and continue.
3614
+ - Independent read-only calls MAY overlap under \`asyncio.gather\` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with \`await\`.
3615
+ - Emit the run's answer with \`print(...)\` and/or a top-level \`return <value>\`; the returned value must be lossless JSON. Only what you print and 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.
3616
+
3617
+ The available tools:`;
3618
+ /**
3619
+ * Render the full `tools:sdk` prompt section under `runtime.language ===
3620
+ * 'python'`: the Python-flavored usage instructions plus one named `TypedDict`
3621
+ * per tool argument or output object (and per nested object) and one awaitable
3622
+ * method per visible tool on a `Tools` protocol — typed args in, the tool's
3623
+ * canonical output value out — with a `tools: Tools` singleton the model calls
3624
+ * into. The `typing` import line lists exactly the symbols the render used.
3625
+ * Deterministic — tools are emitted in lexicographic name order, and class
3626
+ * declarations precede the protocol in that same order (nested classes before
3627
+ * the parent that references them), so an unchanged tool set produces
3628
+ * byte-identical text across assemblies. The sort is not a total order on
3629
+ * byte-equal names, so two schemas sharing a name would render in argument
3630
+ * order; the caller's visible-capability map is keyed by name, so the input
3631
+ * never carries a duplicate.
3632
+ * @param schemas - the tool schemas plus canonical output schemas to declare
3633
+ * (the caller excludes `run_code` itself).
3634
+ * @returns the complete section text.
3635
+ */
3636
+ function renderToolsSdkPy(schemas) {
3637
+ const sorted = [...schemas].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
3638
+ const state = {
3639
+ classes: [],
3640
+ usedClassNames: /* @__PURE__ */ new Set(),
3641
+ nextClassCounter: /* @__PURE__ */ new Map(),
3642
+ typing: /* @__PURE__ */ new Set(["Protocol"])
3643
+ };
3644
+ const members = [];
3645
+ let statements = 0;
3646
+ for (const schema of sorted) {
3647
+ const argType = renderType(schema.parameters, `${camelCase(schema.name)}Args`, state);
3648
+ const outputType = renderType(schema.output, `${camelCase(schema.name)}Output`, state);
3649
+ if (isBareIdentifier(schema.name) && !RESERVED.has(schema.name) && !schema.name.startsWith("_")) {
3650
+ const doc = docLines(schema.description, 2);
3651
+ members.push(doc.length > 0 ? `${pad(1)}async def ${schema.name}(self, args: ${argType}) -> ${outputType}:` : `${pad(1)}async def ${schema.name}(self, args: ${argType}) -> ${outputType}: ...`);
3652
+ members.push(...doc);
3653
+ statements += 1;
3654
+ } else {
3655
+ members.push(`${pad(1)}# tools[${JSON.stringify(schema.name)}](args: ${argType}) -> ${outputType}`);
3656
+ const description = describe(schema);
3657
+ if (description !== void 0) members.push(`${pad(1)}# ${description}`);
3658
+ }
3659
+ }
3660
+ const body = (statements > 0 ? members : [`${pad(1)}pass`, ...members]).join("\n");
3661
+ const imports = TYPING_ORDER.filter((symbol) => state.typing.has(symbol));
3662
+ const classBlock = state.classes.length > 0 ? `${state.classes.join("\n\n")}\n\n` : "";
3663
+ return `${SDK_INSTRUCTIONS}\n\n\`\`\`python\n${`from typing import ${imports.join(", ")}\n\nclass ToolCallError(Exception):
3664
+ toolName: str\n\n${classBlock}class Tools(Protocol):\n${body}\n\ntools: Tools`}\n\`\`\``;
3665
+ }
3666
+ /**
3667
+ * Tool registry, model presentation modes, and pre/guard/around/post/result
3668
+ * execution pipeline.
3669
+ * @module @deepseek-ai/dsh-tools
3670
+ */
3671
+ /**
3672
+ * Language → SDK-section renderer. The registry looks up the loaded
3673
+ * `ctx.codeRuntime.language` in this table when assembling the `tools:sdk`
3674
+ * section under a non-native mode; a runtime whose language is not a key
3675
+ * fails the assembly loudly (same idiom as `toolOrder` violations). Adding a
3676
+ * new backend language is three parallel edits — a {@link CodeSdkLanguage}
3677
+ * member, an entry here, and a `RUN_CODE_FLAVORS` entry in `code-mode.ts` for
3678
+ * its `run_code` schema strings — plus the renderer function this table points
3679
+ * at. The `satisfies` clause pins this table's key set to that union, which
3680
+ * the flavor table is checked against too, so any of the three left out is a
3681
+ * typecheck failure. What no check reaches is the prose that names the values
3682
+ * instead of deriving them: the seam's `dsh-code-runtime` README pair, its
3683
+ * `CodeRuntime.language` JSDoc, and `docs/subsystems/code-runtime.md`
3684
+ * with its zh pair, plus this package's own README pair and the
3685
+ * {@link Config.mode} JSDoc.
3686
+ */
3687
+ /**
3688
+ * Prompt order of the `code` collapse statement: after the persona and before
3689
+ * the 100-199 per-tool guidance band, so the model reads which tools it may
3690
+ * call before it reads what each one is for.
3691
+ */
3692
+ const COLLAPSE_SECTION_ORDER = 99;
3693
+ /**
3694
+ * The model-facing statement of the `code` collapse. Names the consequence
3695
+ * (the call fails) and the route (inside the program), because a rule the
3696
+ * model can only discover by being denied is one it corrects too late.
3697
+ */
3698
+ 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.`;
3699
+ const SDK_RENDERERS = {
3700
+ typescript: renderToolsSdk,
3701
+ python: renderToolsSdkPy
3702
+ };
3703
+ /**
3704
+ * Scheduler entry point omitted from the generated named service API.
3705
+ * @internal
3706
+ */
3707
+ const TOOL_RUNTIME_SCHEDULER = Symbol("@deepseek-ai/dsh-tools.scheduler");
3708
+ /** Canonical error code for cancellation after a tool body was invoked. */
3709
+ const TOOL_ABORTED = "ABORTED";
3710
+ /** Canonical error code for cancellation before a tool body was invoked. */
3711
+ const TOOL_ABORTED_BEFORE_DISPATCH = "ABORTED_BEFORE_DISPATCH";
3712
+ /**
3713
+ * Thrown (internally) when the model requests a tool that isn't registered.
3714
+ * Extends {@link HarnessError} (`code: 'UNKNOWN_TOOL'`) so an unknown-tool
3715
+ * failure is as routable as a tool-thrown one — retry/sandbox/replay code can
3716
+ * distinguish it from a tool body's own error.
3717
+ */
3718
+ var ToolNotFoundError = class extends HarnessError {
3719
+ /**
3720
+ * @param toolName - the name the caller asked for.
3721
+ * @param reachableFrom - how the model reaches this tool instead, when the
3722
+ * name IS visible and only the presentation denies calling it directly.
3723
+ * Omitted for a name that is registered nowhere.
3724
+ */
3725
+ constructor(toolName, reachableFrom) {
3726
+ super(reachableFrom === void 0 ? `unknown tool "${toolName}"` : `unknown tool "${toolName}": ${reachableFrom}`, "UNKNOWN_TOOL");
3727
+ this.name = "ToolNotFoundError";
3728
+ }
3729
+ };
3730
+ /** Thrown when a tool body or post-policy value violates its declared output. */
3731
+ var ToolOutputError = class extends HarnessError {
3732
+ /** Schema/value violations in validation order. */
3733
+ violations;
3734
+ constructor(toolName, violations) {
3735
+ super(`tool "${toolName}" returned invalid output: ${violations.join("; ")}`, "INVALID_TOOL_OUTPUT");
3736
+ this.name = "ToolOutputError";
3737
+ this.violations = violations;
3738
+ }
3739
+ };
3740
+ /** Convert one projector exception into the canonical invalid-output failure. */
3741
+ function projectionError(toolName, projector, error) {
3742
+ return new ToolOutputError(toolName, [`output.${projector} failed: ${errorMessage$1(error)}`]);
3743
+ }
3744
+ /** Snapshot one projector result before later durable-result materialization. */
3745
+ function snapshotProjection(toolName, projector, candidate) {
3746
+ try {
3747
+ const detached = snapshotJsonValue(candidate);
3748
+ if (detached === void 0) throw new ToolOutputError(toolName, [`output.${projector} returned non-lossless JSON`]);
3749
+ return detached;
3750
+ } catch (error) {
3751
+ if (error instanceof ToolOutputError) throw error;
3752
+ throw projectionError(toolName, projector, error);
3753
+ }
3754
+ }
3755
+ /** Snapshot one body or policy value into the canonical invalid-output failure class. */
3756
+ function snapshotToolValue(toolName, candidate) {
3757
+ try {
3758
+ const detached = snapshotJsonValue(candidate);
3759
+ if (detached === void 0) throw new ToolOutputError(toolName, ["value is not lossless JSON"]);
3760
+ return detached;
3761
+ } catch (error) {
3762
+ if (error instanceof ToolOutputError) throw error;
3763
+ throw new ToolOutputError(toolName, [`value snapshot failed: ${errorMessage$1(error)}`]);
3764
+ }
3765
+ }
3766
+ /**
3767
+ * Best-effort human-readable message from an arbitrary thrown value: Error
3768
+ * instances use `.message`; non-Error objects with a string `message`
3769
+ * property (e.g. `throw { message: 'denied' }`) use it too; everything else
3770
+ * is stringified.
3771
+ */
3772
+ function errorMessage$1(error) {
3773
+ try {
3774
+ if (error instanceof Error) return error.message;
3775
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") return error.message;
3776
+ return String(error);
3777
+ } catch {
3778
+ return "<unprintable thrown value>";
3779
+ }
3780
+ }
3781
+ /** Derive one failure message from policy feedback without changing its rendered blocks. */
3782
+ function failureMessageFromContent(content) {
3783
+ const text = content.map((block) => block.type === "text" ? block.text : `[${block.type} content]`).join("\n");
3784
+ return text.length > 0 ? text : "tool result blocked by post-execute policy";
3785
+ }
3786
+ /** Snapshot and freeze one durable tool-result projection or reject lossy data. */
3787
+ function materializePresentation(candidate) {
3788
+ const detached = snapshotJsonValue(candidate);
3789
+ if (detached === void 0) throw new TypeError("tool result must be losslessly JSON-serializable");
3790
+ return deepFreeze(detached);
3791
+ }
3792
+ /** Structured `{ name, code }` for a thrown HarnessError, else undefined. */
3793
+ function errorInfo(error) {
3794
+ try {
3795
+ return error instanceof HarnessError ? {
3796
+ name: error.name,
3797
+ code: error.code
3798
+ } : void 0;
3799
+ } catch {
3800
+ return;
3801
+ }
3802
+ }
3803
+ /** One scope's complete tool-registry contribution. */
3804
+ var ToolLayer = class {
3805
+ tools;
3806
+ restrictions = new AnonymousEntries();
3807
+ guards = new AnonymousEntries();
3808
+ /**
3809
+ * Presentation this scope's agent declared for itself, shadowing the
3810
+ * deployment default. One cell rather than an entry table: two answers to
3811
+ * "which form does the model see" is a contradiction, not a merge.
3812
+ */
3813
+ mode;
3814
+ constructor(scope) {
3815
+ this.tools = new NamedEntries((name) => /* @__PURE__ */ new Error(scope === void 0 ? `tool "${name}" is already registered (for a per-agent variant, register through that agent's \`agent.ctx\` instead)` : `tool "${name}" is already registered in this scope`));
3816
+ }
3817
+ /** Whether every contribution table in this aggregate layer is empty. */
3818
+ isEmpty() {
3819
+ return this.tools.isEmpty() && this.restrictions.isEmpty() && this.guards.isEmpty() && this.mode === void 0;
3820
+ }
3821
+ /** Whether every compiled restriction in this layer admits a global tool name. */
3822
+ admits(name) {
3823
+ for (const filter of this.restrictions.values()) if (filter.allow !== void 0 && !filter.allow.has(name) || filter.deny !== void 0 && filter.deny.has(name)) return false;
3824
+ return true;
3825
+ }
3826
+ /** First monotonic denial from this layer's live guard registrations. */
3827
+ guardReason(exec) {
3828
+ for (const guard of this.guards.values()) {
3829
+ const reason = guard(exec);
3830
+ if (reason !== void 0) return reason;
3831
+ }
3832
+ }
3833
+ };
3834
+ /** Resolve the run_code overlap cap at the owning config boundary (direct construction bypasses the Loader schema). */
3835
+ function resolveMaxParallelSubCalls(value) {
3836
+ const maxParallelSubCalls = value ?? 10;
3837
+ if (!Number.isInteger(maxParallelSubCalls) || maxParallelSubCalls < 1) throw new Error("maxParallelSubCalls must be a positive integer");
3838
+ return maxParallelSubCalls;
3839
+ }
3840
+ (class extends Service {
3841
+ static inject = ["systemPrompt"];
3842
+ static Config = z.object({
3843
+ mode: z.union([
3844
+ "native",
3845
+ "code",
3846
+ "both"
3847
+ ]).default("native"),
3848
+ maxParallelSubCalls: z.natural().min(1).default(10)
3849
+ });
3850
+ /** Internal staged view consumed by `dsh-agent-loop`'s parallel scheduler. */
3851
+ [TOOL_RUNTIME_SCHEDULER] = {
3852
+ prepare: (exec) => this.prepareScheduledExecution(exec),
3853
+ dispatch: (exec) => this.dispatchScheduledExecution(exec),
3854
+ finalize: (exec, result) => this.finalizeScheduledExecution(exec, result),
3855
+ finish: (exec, result) => this.finishScheduledExecution(exec, result)
3856
+ };
3857
+ /** Context deferred by a running tool body, keyed by its scheduler-owned execution. */
3858
+ deferredContexts = /* @__PURE__ */ new WeakMap();
3859
+ /** Executions whose tool body declared the current turn complete. */
3860
+ concludingExecutions = /* @__PURE__ */ new WeakSet();
3861
+ /** Original caller cancellation, kept outside the wrapper-mutable execution object. */
3862
+ cancellationStates = /* @__PURE__ */ new WeakMap();
3863
+ /** Definition-owned final content transform snapshotted before policy begins. */
3864
+ contentFinalizers = /* @__PURE__ */ new WeakMap();
3865
+ layers = new ScopedLayers((scope) => new ToolLayer(scope), () => {
3866
+ this.ctx.emit("tools/change");
3867
+ });
3868
+ /** Presentation for scopes that declare none; {@link presentAs} shadows it per scope. */
3869
+ defaultMode;
3870
+ maxParallelSubCalls;
3871
+ /**
3872
+ * Reserved presentation transport, kept outside the filterable registration
3873
+ * layers. Built on first need rather than at construction: which agents run
3874
+ * a code mode is no longer known when the service is constructed, and the
3875
+ * transport is stateless beyond its closures over `this`.
3876
+ */
3877
+ codeTransport;
3878
+ constructor(ctx, config = {}) {
3879
+ super(ctx, "tools");
3880
+ this.defaultMode = config.mode ?? "native";
3881
+ this.maxParallelSubCalls = resolveMaxParallelSubCalls(config.maxParallelSubCalls);
3882
+ ctx.systemPrompt.tools((context) => this.wireSchemas(context.scope));
3883
+ if (this.defaultMode !== "native") {
3884
+ ctx.systemPrompt.section(this.collapseSection());
3885
+ ctx.systemPrompt.section(this.sdkSection());
3886
+ }
3887
+ }
3888
+ /**
3889
+ * The prompt statement of the `code` executor collapse, registered wherever
3890
+ * {@link sdkSection} is and rendering empty outside an effective `code`.
3891
+ *
3892
+ * Every tool contributes its own guidance section naming its tool, none of
3893
+ * them qualify how that tool is reached, and they all render before the SDK
3894
+ * (orders 100-199 against {@link SDK_SECTION_ORDER}). Without this the model
3895
+ * reads a catalog of tools it is told to use and no statement that only
3896
+ * `run_code` may be called, so it emits a native call, receives
3897
+ * `UNKNOWN_TOOL` for a tool the prompt just declared, and concludes the
3898
+ * deployment is inconsistent. {@link COLLAPSE_SECTION_ORDER} places the rule
3899
+ * before that guidance rather than after it.
3900
+ *
3901
+ * `both` renders empty: native calls do execute there, so the rule is false.
3902
+ * @returns the section registration.
3903
+ */
3904
+ collapseSection() {
3905
+ return {
3906
+ name: "tools:code-only",
3907
+ order: COLLAPSE_SECTION_ORDER,
3908
+ text: (context) => this.modeFor(context.scope) === "code" ? CODE_ONLY_INSTRUCTION : ""
3909
+ };
3910
+ }
3911
+ /**
3912
+ * The generated-SDK prompt section, registered globally by a code-mode
3913
+ * deployment and per scope by {@link presentAs}.
3914
+ *
3915
+ * The body regenerates from the CALLING scope, and renders empty for an
3916
+ * agent presenting natively — an agent that opted out under a code-mode
3917
+ * deployment still sees the global registration, and an empty section is
3918
+ * dropped from the rendered prompt.
3919
+ * @returns the section registration.
3920
+ */
3921
+ sdkSection() {
3922
+ return {
3923
+ name: "tools:sdk",
3924
+ order: 150,
3925
+ text: (context) => {
3926
+ const mode = this.modeFor(context.scope);
3927
+ if (mode === "native") return "";
3928
+ const runtime = this.requireCodeRuntime(mode);
3929
+ const render = SDK_RENDERERS[runtime.language];
3930
+ /* v8 ignore next -- requireCodeRuntime rejects an unknown language before this runs. */
3931
+ if (render === void 0) throw new Error(`dsh-tools: no SDK renderer for ${runtime.language}`);
3932
+ return render(this.sdkSchemas(context.scope));
3933
+ }
3934
+ };
3935
+ }
3936
+ /**
3937
+ * The presentation one scope's agent sees: its own declaration, else the
3938
+ * deployment default.
3939
+ * @param scope - the calling agent, or undefined for the global view.
3940
+ * @returns the resolved presentation mode.
3941
+ */
3942
+ modeFor(scope) {
3943
+ const layers = this.layers.chainLayers(scope);
3944
+ for (let index = layers.length - 1; index >= 0; index -= 1) {
3945
+ const mode = layers[index]?.mode;
3946
+ if (mode !== void 0) return mode;
3947
+ }
3948
+ return this.defaultMode;
3949
+ }
3950
+ /**
3951
+ * The reserved `run_code` transport, built on first need.
3952
+ *
3953
+ * It never enters the global layer: per-agent restrictions must not remove
3954
+ * it, and a scoped registration must not shadow it. The visibility resolver
3955
+ * appends it after resolving the filterable global/scoped capability layers,
3956
+ * and only for scopes whose mode actually presents it.
3957
+ * @returns the shared transport definition.
3958
+ */
3959
+ requireCodeTransport() {
3960
+ this.codeTransport ??= createRunCodeTool(this, {
3961
+ requireRuntime: () => this.requireCodeRuntime(this.defaultMode),
3962
+ peekRuntime: () => this.ctx.get("codeRuntime"),
3963
+ maxParallel: this.maxParallelSubCalls,
3964
+ shapeDispatchLog: (dispatch) => this.shapeDispatchLog(dispatch)
3965
+ });
3966
+ return this.codeTransport;
3967
+ }
3968
+ /**
3969
+ * Present the calling scope's tools in `mode` instead of the deployment
3970
+ * default. Nearest scope on the chain wins, so a preset's standing
3971
+ * declaration covers every agent joined under it.
3972
+ *
3973
+ * Scoped only, and one declaration per scope: this is how an agent preset
3974
+ * composes Code Mode agents beside native ones in the same process, and a
3975
+ * process-global override would be the `mode` config field instead.
3976
+ * @param mode - the presentation the covered agents' models see.
3977
+ * @returns the exact disposer that restores the deployment default.
3978
+ */
3979
+ presentAs(mode) {
3980
+ const ctx = this.ctx;
3981
+ if (scopeOf(ctx) === void 0) throw new Error("tools.presentAs() requires a scoped context (agent.ctx): a context-global presentation is the `mode` config field on the tools row");
3982
+ return ctx.effect(function* () {
3983
+ yield this.layers.effect(ctx, (layer) => {
3984
+ if (layer.mode !== void 0) throw new Error(`tools.presentAs("${mode}") conflicts with "${layer.mode}" already declared for this scope; one composition selects one presentation`);
3985
+ layer.mode = mode;
3986
+ return () => {
3987
+ layer.mode = void 0;
3988
+ };
3989
+ }, { label: "tools.presentAs()" });
3990
+ if (mode !== "native") {
3991
+ yield ctx.systemPrompt.section(this.collapseSection());
3992
+ yield ctx.systemPrompt.section(this.sdkSection());
3993
+ }
3994
+ }.bind(this), "tools.presentAs()");
3995
+ }
3996
+ /**
3997
+ * Build one scope's wire schemas and names for prompt-order validation.
3998
+ * Restrictions do not make known tools invalid, but a mode collapse does.
3999
+ */
4000
+ wireSchemas(scope) {
4001
+ const view = this.view(scope);
4002
+ const mode = this.modeFor(scope);
4003
+ if (mode === "native") return {
4004
+ schemas: [...view.visible.values()].map((definition) => this.schemaOf(definition, false)),
4005
+ knownNames: [...view.knownNames]
4006
+ };
4007
+ this.requireCodeRuntime(mode);
4008
+ const schemas = [...view.visible.values()].map((definition) => this.schemaOf(definition, false));
4009
+ if (mode === "code") return {
4010
+ schemas: schemas.filter((schema) => schema.name === RUN_CODE_NAME),
4011
+ knownNames: [RUN_CODE_NAME]
4012
+ };
4013
+ return {
4014
+ schemas,
4015
+ knownNames: [...view.knownNames, RUN_CODE_NAME]
4016
+ };
4017
+ }
4018
+ /**
4019
+ * Resolve the code runtime or throw the actionable misconfiguration error.
4020
+ * Read at use time (assembly / run_code execution), NOT via static
4021
+ * `inject`: an inject entry would hold `ctx.tools` — and every tool plugin
4022
+ * behind it — hostage to a code runtime existing even under `mode:
4023
+ * 'native'` (the loop's optional-backend idiom, same as
4024
+ * `sessionPersistence`).
4025
+ *
4026
+ * Assembly and `run_code` execution read separately, so the language is not
4027
+ * bound to a request. Harmless while one published backend exists — both
4028
+ * reads return the same flavor — but a reload that swapped in a second
4029
+ * language between them would hand a program written against one SDK to the
4030
+ * other. Binding it is deferred until a second backend ships (the first
4031
+ * point it is testable); rationale in the
4032
+ * [language-dispatch note](../../../../.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md).
4033
+ */
4034
+ requireCodeRuntime(mode) {
4035
+ const runtime = this.ctx.get("codeRuntime");
4036
+ if (!runtime) throw new Error(`dsh-tools: mode "${mode}" requires a code runtime — load a ctx.codeRuntime implementation (e.g. @deepseek-ai/dsh-code-runtime-worker-thread) or set tools mode to "native"`);
4037
+ if (!Object.hasOwn(SDK_RENDERERS, runtime.language)) {
4038
+ const known = Object.keys(SDK_RENDERERS).map((name) => JSON.stringify(name)).join(", ");
4039
+ throw new Error(`dsh-tools: no SDK renderer registered for runtime language ${JSON.stringify(runtime.language)} (known: ${known})`);
4040
+ }
4041
+ return runtime;
4042
+ }
4043
+ /**
4044
+ * Register globally or in the calling agent scope. Scoped tools shadow
4045
+ * globals; duplicates within one layer and the reserved `run_code` name fail.
4046
+ * @param definition - tool schema, execution, and optional finalization/presentation callbacks.
4047
+ * @returns the exact disposer that unregisters the tool.
4048
+ */
4049
+ register(definition) {
4050
+ const name = definition.name;
4051
+ const output = definition.output;
4052
+ if (output === void 0 || typeof output !== "object" || typeof output.render !== "function" || output.presentationMeta !== void 0 && typeof output.presentationMeta !== "function") throw new TypeError(`tool "${name}" must declare output { schema, render, presentationMeta? }`);
4053
+ assertSupportedJsonSchema(output.schema);
4054
+ const timeoutMs = definition.timeoutMs;
4055
+ if (timeoutMs !== void 0 && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) throw new TypeError(`tool "${name}" timeoutMs must be a positive finite number`);
4056
+ 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`);
4057
+ return this.layers.effect(this.ctx, (layer) => layer.tools.insert(name, definition), { label: "tools.register()" });
4058
+ }
4059
+ /**
4060
+ * Restrict global tools for the calling agent scope. Empty filters, unknown
4061
+ * names, scope-local names, and reserved transport names fail. Restrictions
4062
+ * intersect; scoped registrations remain visible.
4063
+ * @param filter - global-tool mask: `allow` (keep only) and/or `deny` (remove).
4064
+ * @returns the exact disposer that lifts this restriction.
4065
+ */
4066
+ restrict(filter) {
4067
+ const scope = scopeOf(this.ctx);
4068
+ if (scope === void 0) throw new Error("tools.restrict() requires a scoped context (agent.ctx): a context-global restriction would mask every agent — deny the tool for the intended agent instead");
4069
+ const allow = filter.allow;
4070
+ const deny = filter.deny;
4071
+ if (allow === void 0 && deny === void 0) throw new Error("tools.restrict({}) is a no-op: pass `allow` and/or `deny` (an empty filter is almost always a materialized-empty-config bug)");
4072
+ const compiled = {
4073
+ ...allow !== void 0 ? { allow: new Set(allow) } : {},
4074
+ ...deny !== void 0 ? { deny: new Set(deny) } : {}
4075
+ };
4076
+ 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`);
4077
+ const known = this.view(scope).restrictableNames;
4078
+ const unknown = [...allow ?? [], ...deny ?? []].filter((name) => !known.has(name));
4079
+ 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)"}`);
4080
+ return this.layers.effect(this.ctx, (layer) => layer.restrictions.append(compiled), { label: "tools.restrict()" });
4081
+ }
4082
+ /**
4083
+ * Register a monotonic guard after the extensible `tools/pre-execute`
4084
+ * waterfall. A plain-context guard applies globally; one registered through
4085
+ * `agent.ctx` applies only to that agent. Any matching guard may deny by
4086
+ * returning a reason, while no guard can force-allow a call another guard
4087
+ * denied. The exact effect disposer is returned for ordered ownership and
4088
+ * HMR cleanup.
4089
+ * @param guard - synchronous check; a returned string denies the execution.
4090
+ * @returns the exact disposer that unregisters the guard.
4091
+ */
4092
+ guard(guard) {
4093
+ return this.layers.effect(this.ctx, (layer) => layer.guards.append(guard), {
4094
+ label: "tools.guard()",
4095
+ notify: false
4096
+ });
4097
+ }
4098
+ /** First monotonic denial from the global then the scope chain's guard layers, farthest first. */
4099
+ guardReason(exec) {
4100
+ const globalReason = this.layers.global.guardReason(exec);
4101
+ if (globalReason !== void 0) return globalReason;
4102
+ if (exec.agent === void 0) return void 0;
4103
+ for (const layer of this.layers.chainLayers(exec.agent)) {
4104
+ const reason = layer.guardReason(exec);
4105
+ if (reason !== void 0) return reason;
4106
+ }
4107
+ }
4108
+ /**
4109
+ * Resolve every registry fact one scope needs in one layer traversal. The
4110
+ * visible map applies restrictions to the INHERITED surface, then the
4111
+ * scope's own registrations and the reserved presentation transport; the
4112
+ * other sets retain the pre-restriction facts needed by restriction and
4113
+ * prompt-order validation.
4114
+ *
4115
+ * A restriction filters what a scope inherits — the global layer and every
4116
+ * ancestor layer on its chain — and never what its OWN layer registers.
4117
+ * That exemption is what a per-child capability filter has to keep intact:
4118
+ * the delegation runtime registers a child's reporting and structured-output
4119
+ * tools into the child's own layer, and a filter naming the capabilities the
4120
+ * child may use must not strip the machinery it answers through.
4121
+ *
4122
+ * Reading the exempt set as "the global layer" instead of "not mine" held
4123
+ * only while every model-facing tool sat in the host composition. Once
4124
+ * presets moved them onto the agent plane they became an ANCESTOR
4125
+ * contribution, so a child's filter silently stopped constraining anything
4126
+ * it was given.
4127
+ * @param scope - the viewing scope (the agent), or undefined for the global view.
4128
+ * @returns the complete derived view for that scope.
4129
+ */
4130
+ view(scope) {
4131
+ const layers = this.layers.chainLayers(scope);
4132
+ const own = this.layers.peek(scope);
4133
+ const inherited = new Map(this.layers.global.tools.entries());
4134
+ for (const layer of layers) {
4135
+ if (layer === own) continue;
4136
+ for (const [name, definition] of layer.tools.entries()) inherited.set(name, definition);
4137
+ }
4138
+ const visible = /* @__PURE__ */ new Map();
4139
+ const knownNames = /* @__PURE__ */ new Set();
4140
+ const restrictableNames = /* @__PURE__ */ new Set();
4141
+ for (const [name, definition] of inherited) {
4142
+ knownNames.add(name);
4143
+ restrictableNames.add(name);
4144
+ if (layers.every((layer) => layer.admits(name))) visible.set(name, definition);
4145
+ }
4146
+ if (own !== void 0) for (const [name, definition] of own.tools.entries()) {
4147
+ knownNames.add(name);
4148
+ visible.set(name, definition);
4149
+ }
4150
+ if (this.modeFor(scope) !== "native") visible.set(RUN_CODE_NAME, this.requireCodeTransport());
4151
+ return {
4152
+ visible,
4153
+ knownNames,
4154
+ restrictableNames
4155
+ };
4156
+ }
4157
+ /**
4158
+ * Look up a tool as one scope sees it (scoped
4159
+ * shadows global; a restricted-away global reads as absent). Presenters pass
4160
+ * the calling agent so the rendered card matches the definition that
4161
+ * actually executed.
4162
+ * @param name - the tool name as registered.
4163
+ * @param scope - the viewing scope (the agent); omitted = the global view.
4164
+ * @returns the definition the scope resolves, or undefined when none is visible.
4165
+ */
4166
+ get(name, scope) {
4167
+ return this.view(scope).visible.get(name);
4168
+ }
4169
+ /**
4170
+ * Resolve the definition that MAY EXECUTE for a call, applying the mode
4171
+ * collapse at the operation boundary that owns it. The registry view
4172
+ * (`get`) is presentation-agnostic; here a MODEL-DIRECT call under `code`
4173
+ * may only name the reserved `run_code` transport, while a nested
4174
+ * sub-dispatch (a `parent` token set — the `run_code` SDK calling a tool
4175
+ * it bound) may call any visible tool. Denial surfaces as `UNKNOWN_TOOL`
4176
+ * through the executor, matching an absent definition.
4177
+ * @param name - the tool name as registered.
4178
+ * @param scope - the viewing scope (the agent); omitted = the global view.
4179
+ * @param nested - whether the call is a transport sub-dispatch, not a model-direct call.
4180
+ * @returns the definition that may run, or undefined when the call must be rejected.
4181
+ */
4182
+ resolveExecution(name, scope, nested) {
4183
+ const tool = this.get(name, scope);
4184
+ if (tool === void 0) return void 0;
4185
+ if (this.collapses(name, scope, nested)) return void 0;
4186
+ return tool;
4187
+ }
4188
+ /**
4189
+ * Project visible definitions onto the allowlisted model-facing schema fields,
4190
+ * excluding execution and presentation callbacks.
4191
+ * @param scope - the viewing scope (the agent); omitted = the global view.
4192
+ * @returns one deep-cloned schema per visible tool.
4193
+ */
4194
+ schemas(scope) {
4195
+ return [...this.view(scope).visible.values()].map((definition) => this.schemaOf(definition, true));
4196
+ }
4197
+ /** Project visible callable tools onto the generated Code Mode SDK contract. */
4198
+ sdkSchemas(scope) {
4199
+ return [...this.view(scope).visible.values()].filter((definition) => definition.name !== RUN_CODE_NAME).map((definition) => {
4200
+ const output = snapshotJsonValue(definition.output.schema);
4201
+ /* v8 ignore next -- registration already validated and retained this schema as lossless JSON. */
4202
+ if (output === void 0) throw new Error(`tool "${definition.name}" output schema must be lossless JSON before SDK projection`);
4203
+ return {
4204
+ ...this.schemaOf(definition, true),
4205
+ output
4206
+ };
4207
+ });
4208
+ }
4209
+ /** Project one definition onto the model-facing schema fields. */
4210
+ schemaOf(definition, detachParameters) {
4211
+ const { name, description, parameters } = definition;
4212
+ const detached = detachParameters ? snapshotJsonValue(parameters) : parameters;
4213
+ if (detached === void 0) throw new Error(`tool "${name}" parameters must be lossless JSON before schema projection`);
4214
+ return {
4215
+ name,
4216
+ description,
4217
+ parameters: detached
4218
+ };
4219
+ }
4220
+ /**
4221
+ * Classify a pending call through the caller's visible tool definition. Only
4222
+ * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or
4223
+ * throwing classifiers are exclusive.
4224
+ * @param exec - call name, parsed arguments, and optional agent scope.
4225
+ * @returns the fail-closed scheduling mode.
4226
+ */
4227
+ executionMode(exec) {
4228
+ const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== void 0);
4229
+ if (!tool?.isConcurrencySafe) return { kind: "exclusive" };
4230
+ try {
4231
+ return tool.isConcurrencySafe(exec.arguments) === true ? { kind: "parallel" } : { kind: "exclusive" };
4232
+ } catch {
4233
+ return { kind: "exclusive" };
4234
+ }
4235
+ }
4236
+ /**
4237
+ * Run the `tools/code-dispatch-log` waterfall over one settled sub-dispatch
4238
+ * and return the content the bridge should log on `tool/code-dispatch`.
4239
+ * Contained: when a listener throws, the method logs the original settled
4240
+ * content; that failure must not fail the dispatch or omit the settle event. Private:
4241
+ * the ONE consumer is the `run_code` bridge this registry constructs, which
4242
+ * receives it as a capability parameter (the `requireRuntime` idiom) — the
4243
+ * waterfall, not this invoker, is the public extension point.
4244
+ */
4245
+ async shapeDispatchLog(dispatch) {
4246
+ try {
4247
+ return await this.ctx.waterfall(scopeTarget(this, dispatch.agent), "tools/code-dispatch-log", dispatch, () => Promise.resolve(dispatch.content));
4248
+ } catch (error) {
4249
+ this.ctx.logger.warn(`tools: code-dispatch-log listener failed for ${dispatch.name}: ${errorMessage$1(error)}; logging the original settled content`);
4250
+ return dispatch.content;
4251
+ }
4252
+ }
4253
+ /**
4254
+ * Whether the `code` mode collapse denies a model-direct call: only the
4255
+ * reserved `run_code` transport may be named. Nested sub-dispatches (a
4256
+ * `parent` token set) bypass the collapse. One home for the
4257
+ * security-relevant predicate, shared by {@link resolveExecution} and
4258
+ * {@link createExecution} so the two can never drift apart.
4259
+ *
4260
+ * Resolved through {@link modeFor}, NOT `defaultMode`: an agent given `code`
4261
+ * by an agent preset under a native deployment is the composition
4262
+ * `dsh-agent-tool-presentation` exists for, and reading the deployment default would
4263
+ * leave exactly that agent uncollapsed — announcing one surface while
4264
+ * executing another, which is the bypass this collapse closes.
4265
+ * @param name - the tool name as registered.
4266
+ * @param scope - the viewing scope whose effective presentation mode applies.
4267
+ * @param nested - whether the call is a transport sub-dispatch, not a model-direct call.
4268
+ */
4269
+ collapses(name, scope, nested) {
4270
+ return !nested && this.modeFor(scope) === "code" && name !== "run_code";
4271
+ }
4272
+ /**
4273
+ * Execute through pre-policy, guards, around-dispatch, post-policy,
4274
+ * definition-owned content finalization, and final notification. Tool and
4275
+ * listener failures resolve as materialized error results; an invisible tool
4276
+ * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen
4277
+ * snapshot final observers receive. Cancellation
4278
+ * arriving after entry and before final result materialization skips a
4279
+ * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a
4280
+ * successful started outcome with `ABORTED`; already-started work is still
4281
+ * drained and may retain a tool-owned structured error.
4282
+ * @param exec - the typed same-process call input. The registry assigns its
4283
+ * correlation token before policy begins.
4284
+ * @returns the materialized final result.
4285
+ */
4286
+ async execute(exec) {
4287
+ return this.prepareExecution(exec, (prepared) => this.completeScheduledExecution(prepared));
4288
+ }
4289
+ async completeScheduledExecution(prepared) {
4290
+ switch (prepared.kind) {
4291
+ case "dispatch": {
4292
+ const dispatched = await this.dispatchScheduledExecution(prepared.exec);
4293
+ return dispatched.kind === "post-result" ? await this.finalizeScheduledExecution(prepared.exec, dispatched.result) : this.finishScheduledExecution(prepared.exec, dispatched.result);
4294
+ }
4295
+ case "post-result": return await this.finalizeScheduledExecution(prepared.exec, prepared.result);
4296
+ case "final-result": return this.finishScheduledExecution(prepared.exec, prepared.result);
4297
+ /* v8 ignore next -- closed-union exhaustiveness guard */
4298
+ default: return assertNever(prepared, "scheduled tool preparation");
4299
+ }
4300
+ }
4301
+ createExecution(exec) {
4302
+ const deferredContexts = [];
4303
+ const token = createExecutionToken();
4304
+ const callId = exec.callId;
4305
+ const rootCallId = exec.rootCallId ?? callId;
4306
+ const name = exec.name;
4307
+ const agent = exec.agent;
4308
+ const parent = exec.parent;
4309
+ const signal = exec.signal;
4310
+ const visible = this.get(name, agent);
4311
+ const collapsed = visible !== void 0 && this.collapses(name, agent, parent !== void 0);
4312
+ const concludingExecutions = this.concludingExecutions;
4313
+ const base = {
4314
+ token,
4315
+ callId,
4316
+ rootCallId,
4317
+ name,
4318
+ signal,
4319
+ ...agent !== void 0 ? { agent } : {},
4320
+ ...parent !== void 0 ? { parent } : {},
4321
+ deferContext(context) {
4322
+ deferredContexts.push(context);
4323
+ },
4324
+ concludeTurn() {
4325
+ concludingExecutions.add(this);
4326
+ }
4327
+ };
4328
+ const capturedFinalizer = visible?.finalizeContent?.bind(visible);
4329
+ const finalizerFor = () => collapsed && !signal.aborted ? void 0 : capturedFinalizer;
4330
+ try {
4331
+ const detached = snapshotJsonValue(exec.arguments);
4332
+ if (detached === void 0) throw new TypeError("tool execution arguments must be losslessly JSON-serializable");
4333
+ const execution = {
4334
+ ...base,
4335
+ arguments: deepFreeze(detached)
4336
+ };
4337
+ this.deferredContexts.set(execution, deferredContexts);
4338
+ this.contentFinalizers.set(execution, finalizerFor());
4339
+ this.cancellationStates.set(execution, {
4340
+ callerSignal: signal,
4341
+ bodyInvoked: false
4342
+ });
4343
+ if (collapsed) {
4344
+ if (signal.aborted) return {
4345
+ kind: "final-result",
4346
+ exec: execution,
4347
+ result: toolAbortedBeforeDispatchResult()
4348
+ };
4349
+ return {
4350
+ kind: "final-result",
4351
+ exec: execution,
4352
+ result: toolErrorResult(new ToolNotFoundError(name, `only \`${RUN_CODE_NAME}\` is callable directly — call \`${name}\` from inside a \`${RUN_CODE_NAME}\` program instead`))
4353
+ };
4354
+ }
4355
+ return {
4356
+ kind: "ready",
4357
+ exec: execution
4358
+ };
4359
+ } catch (error) {
4360
+ const execution = {
4361
+ ...base,
4362
+ arguments: void 0
4363
+ };
4364
+ this.contentFinalizers.set(execution, finalizerFor());
4365
+ return {
4366
+ kind: "final-result",
4367
+ exec: execution,
4368
+ result: toolErrorResult(error)
4369
+ };
4370
+ }
4371
+ }
4372
+ /**
4373
+ * Run the ordered pre-execute and monotonic guard stages for the scheduler.
4374
+ * @param input - the caller-supplied execution input.
4375
+ * @returns the prepared execution plus the next scheduler stage.
4376
+ * @internal
4377
+ */
4378
+ async prepareScheduledExecution(input) {
4379
+ return this.prepareExecution(input, (prepared) => prepared);
4380
+ }
4381
+ async prepareExecution(input, next) {
4382
+ const created = this.createExecution(input);
4383
+ if (created.kind !== "ready") return next(created);
4384
+ const exec = created.exec;
4385
+ if (this.callerCancelled(exec)) return next({
4386
+ kind: "final-result",
4387
+ exec,
4388
+ result: toolAbortedBeforeDispatchResult()
4389
+ });
4390
+ try {
4391
+ const carrier = scopeTarget(this, exec.agent);
4392
+ const gate = await this.ctx.waterfall(carrier, "tools/pre-execute", exec, () => Promise.resolve({ kind: "allow" }));
4393
+ const askResolution = gate.kind === "ask" ? await this.serviceAsk(exec, gate) : {
4394
+ decision: gate,
4395
+ approvalCancelled: false
4396
+ };
4397
+ const { decision } = askResolution;
4398
+ if (this.callerCancelled(exec) && askResolution.approvalCancelled) return await next({
4399
+ kind: "post-result",
4400
+ exec,
4401
+ result: toolAbortedBeforeDispatchResult()
4402
+ });
4403
+ const denialReason = decision.kind === "allow" ? this.guardReason(exec) : decision.reason;
4404
+ if (denialReason !== void 0) return await next({
4405
+ kind: "post-result",
4406
+ exec,
4407
+ result: this.materializeFinalResult({
4408
+ content: [{
4409
+ type: "text",
4410
+ text: `Error: ${denialReason}`
4411
+ }],
4412
+ isError: true,
4413
+ error: { message: denialReason }
4414
+ })
4415
+ });
4416
+ if (this.callerCancelled(exec)) return await next({
4417
+ kind: "post-result",
4418
+ exec,
4419
+ result: toolAbortedBeforeDispatchResult()
4420
+ });
4421
+ return await next({
4422
+ kind: "dispatch",
4423
+ exec
4424
+ });
4425
+ } catch (error) {
4426
+ return next({
4427
+ kind: "final-result",
4428
+ exec,
4429
+ result: toolErrorResult(error)
4430
+ });
4431
+ }
4432
+ }
4433
+ /** Whether the original caller signal is currently aborted. */
4434
+ callerCancelled(exec) {
4435
+ const state = this.cancellationStates.get(exec);
4436
+ /* v8 ignore next -- only registry-minted executions reach the staged scheduler methods */
4437
+ if (state === void 0) throw new Error("tool registry scheduler invariant violated: missing cancellation state");
4438
+ return state.callerSignal.aborted;
4439
+ }
4440
+ /** Canonical cancellation outcome selected by whether the tool body started. */
4441
+ cancellationResult(exec, prior) {
4442
+ const state = this.cancellationStates.get(exec);
4443
+ /* v8 ignore next -- only registry-minted executions reach the staged scheduler methods */
4444
+ if (state === void 0) throw new Error("tool registry scheduler invariant violated: missing cancellation state");
4445
+ return state.bodyInvoked ? toolAbortedResult(prior) : toolAbortedBeforeDispatchResult(prior);
4446
+ }
4447
+ /**
4448
+ * Dispatch the registered body with the original caller signal fused back
4449
+ * into any around-wrapper replacement. Cancellation never abandons the body:
4450
+ * a started promise reaches quiescence before its outcome becomes `ABORTED`.
4451
+ */
4452
+ async dispatchToolBody(exec) {
4453
+ const state = this.cancellationStates.get(exec);
4454
+ /* v8 ignore next -- only registry-minted executions reach the staged scheduler methods */
4455
+ if (state === void 0) throw new Error("tool registry scheduler invariant violated: missing cancellation state");
4456
+ const wrapperSignal = exec.signal;
4457
+ const fused = fuseToolSignals(state.callerSignal, wrapperSignal);
4458
+ const signal = fused.signal;
4459
+ if (isAborted(signal)) {
4460
+ fused.dispose();
4461
+ return toolAbortedBeforeDispatchResult();
4462
+ }
4463
+ exec.signal = signal;
4464
+ try {
4465
+ const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== void 0);
4466
+ if (!tool) throw new ToolNotFoundError(exec.name);
4467
+ state.bodyInvoked = true;
4468
+ const returned = await tool.execute(exec.arguments, exec);
4469
+ const result = this.createSuccessResult(exec, tool, returned);
4470
+ return isAborted(signal) ? toolAbortedResult(result) : result;
4471
+ } catch (error) {
4472
+ return toolErrorResult(error);
4473
+ } finally {
4474
+ fused.dispose();
4475
+ exec.signal = wrapperSignal;
4476
+ }
4477
+ }
4478
+ /**
4479
+ * Run around-dispatch and the tool body. Tool and unknown-tool failures still
4480
+ * receive post-execute; pipeline failures are already final.
4481
+ * @param exec - the prepared execution.
4482
+ * @returns whether the result still needs post-execute.
4483
+ * @internal
4484
+ */
4485
+ async dispatchScheduledExecution(exec) {
4486
+ try {
4487
+ const mutableExec = exec;
4488
+ const carrier = scopeTarget(this, exec.agent);
4489
+ const result = await this.ctx.waterfall(carrier, "tools/execute", mutableExec, () => this.dispatchToolBody(mutableExec));
4490
+ const normalized = this.normalizeDispatchResult(exec, result);
4491
+ const deferredContexts = this.deferredContexts.get(exec);
4492
+ /* v8 ignore next -- dispatch only receives executions minted by this registry's prepare stage */
4493
+ if (deferredContexts === void 0) throw new Error("tool registry scheduler invariant violated: unprepared execution");
4494
+ const resultWithDeferredContexts = deferredContexts.length === 0 ? normalized : this.markCanonical(exec, {
4495
+ ...normalized,
4496
+ additionalContexts: [...deferredContexts, ...normalized.additionalContexts ?? []]
4497
+ });
4498
+ return {
4499
+ kind: "post-result",
4500
+ result: this.callerCancelled(exec) && !resultWithDeferredContexts.isError ? this.cancellationResult(exec, resultWithDeferredContexts) : resultWithDeferredContexts
4501
+ };
4502
+ } catch (error) {
4503
+ return {
4504
+ kind: "final-result",
4505
+ result: toolErrorResult(error)
4506
+ };
4507
+ }
4508
+ }
4509
+ /**
4510
+ * Run ordered post-execute, then apply definition-owned content finalization,
4511
+ * materialize, and notify the final outcome.
4512
+ * @param exec - the prepared execution.
4513
+ * @param result - dispatch/pre result that still needs post-execute.
4514
+ * @returns the materialized final result.
4515
+ * @internal
4516
+ */
4517
+ async finalizeScheduledExecution(exec, result) {
4518
+ try {
4519
+ const postResult = await this.postExecute(exec, result);
4520
+ return this.finishScheduledExecution(exec, this.callerCancelled(exec) && !postResult.isError ? this.cancellationResult(exec, postResult) : postResult);
4521
+ } catch (error) {
4522
+ return this.finishScheduledExecution(exec, toolErrorResult(error));
4523
+ }
4524
+ }
4525
+ /**
4526
+ * Materialize the candidate, apply definition-owned content finalization,
4527
+ * then materialize and notify the authoritative result.
4528
+ * @param exec - the prepared execution.
4529
+ * @param result - final result.
4530
+ * @returns the materialized final result.
4531
+ * @internal
4532
+ */
4533
+ finishScheduledExecution(exec, result) {
4534
+ let materializedResult;
4535
+ try {
4536
+ materializedResult = this.materializeFinalResult(result);
4537
+ } catch (error) {
4538
+ materializedResult = this.materializeFinalResult(toolErrorResult(error));
4539
+ }
4540
+ let finalResult;
4541
+ try {
4542
+ finalResult = this.materializeFinalResult(this.applyFinalContent(exec, materializedResult));
4543
+ } catch (error) {
4544
+ finalResult = this.materializeFinalResult(toolErrorResult(error));
4545
+ }
4546
+ this.notifyResult(exec, finalResult);
4547
+ return finalResult;
4548
+ }
4549
+ /** Apply the snapshotted tool-owned content transform without exposing other result fields. */
4550
+ applyFinalContent(exec, result) {
4551
+ const finalizeContent = this.contentFinalizers.get(exec);
4552
+ if (finalizeContent === void 0) return result;
4553
+ const content = finalizeContent(exec, result);
4554
+ return content === void 0 ? result : {
4555
+ ...result,
4556
+ content
4557
+ };
4558
+ }
4559
+ /** Notify observers without exposing a mutation or error channel into the outcome. */
4560
+ notifyResult(exec, result) {
4561
+ Object.freeze(exec);
4562
+ const { name: toolName, callId } = exec;
4563
+ const reportFailure = (error) => {
4564
+ this.ctx.logger.warn(`tool "${toolName}" (${callId}): tools/result observer failed: ${errorMessage$1(error)}`);
4565
+ };
4566
+ const callbacks = this.ctx.events.dispatch("emit", [
4567
+ scopeTarget(this, exec.agent),
4568
+ "tools/result",
4569
+ exec,
4570
+ result
4571
+ ]);
4572
+ for (const callback of callbacks) try {
4573
+ const returned = callback(exec, result);
4574
+ Promise.resolve(returned).catch(reportFailure);
4575
+ } catch (error) {
4576
+ reportFailure(error);
4577
+ }
4578
+ }
4579
+ /**
4580
+ * Resolve an `ask` decision to allow/deny through the approval seam. The
4581
+ * seam is consumed opportunistically with `ctx.get('approval')` — a
4582
+ * deployment that composes no ApprovalService keeps the historical degrade
4583
+ * to deny, and an unmount mid-session degrades the same way on the next ask.
4584
+ * An agent-less execution also degrades: without an agent there is no
4585
+ * session to audit to and no UI to route to. Otherwise the outcome maps
4586
+ * one-to-one — `allowed-once` proceeds; the three non-grants deny with
4587
+ * distinct reasons so the model can tell a human "no" from an absent
4588
+ * approval channel.
4589
+ */
4590
+ async serviceAsk(exec, ask) {
4591
+ const approval = this.ctx.get("approval");
4592
+ if (approval === void 0) return {
4593
+ decision: {
4594
+ kind: "deny",
4595
+ reason: ask.reason ?? `tool "${exec.name}" requires approval (not yet supported)`
4596
+ },
4597
+ approvalCancelled: false
4598
+ };
4599
+ if (exec.agent === void 0) return {
4600
+ decision: {
4601
+ kind: "deny",
4602
+ reason: `tool "${exec.name}" requires approval, but the call has no agent to route it through`
4603
+ },
4604
+ approvalCancelled: false
4605
+ };
4606
+ const outcome = await approval.request({
4607
+ agent: exec.agent,
4608
+ toolName: exec.name,
4609
+ callId: exec.callId,
4610
+ ...ask.reason !== void 0 ? { reason: ask.reason } : {},
4611
+ signal: exec.signal
4612
+ });
4613
+ switch (outcome) {
4614
+ case "allowed-once": return {
4615
+ decision: { kind: "allow" },
4616
+ approvalCancelled: false
4617
+ };
4618
+ case "rejected": return {
4619
+ decision: {
4620
+ kind: "deny",
4621
+ reason: `the user rejected tool "${exec.name}"`
4622
+ },
4623
+ approvalCancelled: false
4624
+ };
4625
+ case "cancelled": return {
4626
+ decision: {
4627
+ kind: "deny",
4628
+ reason: `approval for tool "${exec.name}" was cancelled`
4629
+ },
4630
+ approvalCancelled: true
4631
+ };
4632
+ case "unavailable": return {
4633
+ decision: {
4634
+ kind: "deny",
4635
+ reason: `tool "${exec.name}" requires approval, but no approval channel is available`
4636
+ },
4637
+ approvalCancelled: false
4638
+ };
4639
+ default: return assertNever(outcome, "ApprovalOutcome");
4640
+ }
4641
+ }
4642
+ /**
4643
+ * Run the `tools/post-execute` waterfall over a dispatched `result` and apply
4644
+ * its {@link PostToolDecision}: `accept` keeps the call successful (replacing
4645
+ * `content` when given), `block` turns it into an `isError` whose content is
4646
+ * the corrective `feedback`. Either decision may attach `additionalContexts`,
4647
+ * which are ferried on the returned result for the loop's active-batch FIFO.
4648
+ * Context deferred by the tool body survives an accepted result but is
4649
+ * discarded when the outer call is blocked; a block exposes only context the
4650
+ * blocking decision explicitly supplied.
4651
+ * Runs inside `execute`'s outer try/catch (a throwing listener → isError).
4652
+ */
4653
+ async postExecute(exec, result) {
4654
+ const decision = await this.ctx.waterfall(scopeTarget(this, exec.agent), "tools/post-execute", exec, result, () => Promise.resolve({ kind: "accept" }));
4655
+ const decisionContexts = decision.additionalContexts ?? [];
4656
+ if (decision.kind === "block") {
4657
+ const message = failureMessageFromContent(decision.feedback);
4658
+ return this.markCanonical(exec, {
4659
+ content: decision.feedback,
4660
+ isError: true,
4661
+ error: { message },
4662
+ ...decisionContexts.length > 0 ? { additionalContexts: decisionContexts } : {}
4663
+ });
4664
+ }
4665
+ if (Object.hasOwn(decision, "content") && Object.hasOwn(decision, "value")) throw new TypeError("tools/post-execute accept decision cannot replace both value and content");
4666
+ const additionalContexts = [...result.additionalContexts ?? [], ...decisionContexts];
4667
+ if (Object.hasOwn(decision, "value")) {
4668
+ if (result.isError) throw new TypeError("tools/post-execute cannot replace the value of a failed result");
4669
+ const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== void 0);
4670
+ if (tool === void 0) throw new ToolNotFoundError(exec.name);
4671
+ const replaced = this.createSuccessResult(exec, tool, decision.value);
4672
+ return this.markCanonical(exec, {
4673
+ ...replaced,
4674
+ ...additionalContexts.length > 0 ? { additionalContexts } : {}
4675
+ });
4676
+ }
4677
+ return this.markCanonical(exec, {
4678
+ ...result,
4679
+ ...decision.content !== void 0 ? { content: decision.content } : {},
4680
+ ...additionalContexts.length > 0 ? { additionalContexts } : {}
4681
+ });
4682
+ }
4683
+ /** Registry-normalized results and the exact dispatch that validated each value. */
4684
+ canonicalResults = /* @__PURE__ */ new WeakMap();
4685
+ /** Mark one registry-normalized result as canonical only for its owning dispatch. */
4686
+ markCanonical(exec, result) {
4687
+ this.canonicalResults.set(result, exec.token);
4688
+ return result;
4689
+ }
4690
+ /** Snapshot, validate, render, and optionally project one successful body value. */
4691
+ createSuccessResult(exec, tool, candidate) {
4692
+ const detached = snapshotToolValue(tool.name, candidate);
4693
+ const violations = validateJsonSchemaValue(tool.output.schema, detached, "value");
4694
+ if (violations.length > 0) throw new ToolOutputError(tool.name, violations);
4695
+ const value = deepFreeze(detached);
4696
+ let rendered;
4697
+ try {
4698
+ rendered = tool.output.render(exec.arguments, value);
4699
+ } catch (error) {
4700
+ throw projectionError(tool.name, "render", error);
4701
+ }
4702
+ const content = snapshotProjection(tool.name, "render", rendered);
4703
+ let meta;
4704
+ if (exec.parent === void 0 && tool.output.presentationMeta !== void 0) {
4705
+ let projected;
4706
+ try {
4707
+ projected = tool.output.presentationMeta(exec.arguments, value);
4708
+ } catch (error) {
4709
+ throw projectionError(tool.name, "presentationMeta", error);
4710
+ }
4711
+ meta = snapshotProjection(tool.name, "presentationMeta", projected);
4712
+ }
4713
+ const concludesTurn = this.concludingExecutions.has(exec);
4714
+ return this.markCanonical(exec, this.materializeFinalResult({
4715
+ isError: false,
4716
+ value,
4717
+ content,
4718
+ ...meta !== void 0 ? { meta } : {},
4719
+ ...concludesTurn ? { concludesTurn: true } : {}
4720
+ }));
4721
+ }
4722
+ /** Normalize an around-dispatch wrapper's authored result through the owning output contract. */
4723
+ normalizeDispatchResult(exec, result) {
4724
+ if (this.canonicalResults.get(result) === exec.token) return result;
4725
+ if (result.isError) return this.markCanonical(exec, {
4726
+ isError: true,
4727
+ error: result.error,
4728
+ content: result.content,
4729
+ ...result.meta !== void 0 ? { meta: result.meta } : {},
4730
+ ...result.additionalContexts !== void 0 ? { additionalContexts: result.additionalContexts } : {}
4731
+ });
4732
+ const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== void 0);
4733
+ if (tool === void 0) throw new ToolNotFoundError(exec.name);
4734
+ const normalized = this.createSuccessResult(exec, tool, result.value);
4735
+ return this.markCanonical(exec, {
4736
+ ...normalized,
4737
+ ...result.additionalContexts !== void 0 ? { additionalContexts: result.additionalContexts } : {}
4738
+ });
4739
+ }
4740
+ /** Materialize the authoritative commit outcome once, immediately before `tools/result`. */
4741
+ materializeFinalResult(result) {
4742
+ const presentation = {
4743
+ content: result.content,
4744
+ ...result.meta !== void 0 ? { meta: result.meta } : {},
4745
+ ...result.additionalContexts !== void 0 ? { additionalContexts: result.additionalContexts } : {}
4746
+ };
4747
+ if (result.isError) return materializePresentation({
4748
+ isError: true,
4749
+ error: result.error,
4750
+ ...presentation
4751
+ });
4752
+ return deepFreeze({
4753
+ ...materializePresentation({
4754
+ isError: false,
4755
+ ...presentation,
4756
+ ...result.concludesTurn === true ? { concludesTurn: true } : {}
4757
+ }),
4758
+ value: result.value
4759
+ });
4760
+ }
4761
+ });
4762
+ /** Mint a same-process correlation token whose identity is its value. */
4763
+ function createExecutionToken() {
4764
+ return Symbol("dsh.tool.execution");
4765
+ }
4766
+ function toolErrorResult(error) {
4767
+ const info = errorInfo(error);
4768
+ const message = errorMessage$1(error);
4769
+ return {
4770
+ content: [{
4771
+ type: "text",
4772
+ text: `Error: ${message}`
4773
+ }],
4774
+ isError: true,
4775
+ error: {
4776
+ message,
4777
+ ...info ? { info } : {}
4778
+ }
4779
+ };
4780
+ }
4781
+ /** Read live abort state across an await without treating it as synchronously immutable. */
4782
+ function isAborted(signal) {
4783
+ return signal.aborted;
4784
+ }
4785
+ /**
4786
+ * Fuse caller and wrapper cancellation without nesting `AbortSignal.any`.
4787
+ * Keeping the relay dispatch-scoped also removes listeners when work settles.
4788
+ */
4789
+ function fuseToolSignals(caller, wrapper) {
4790
+ if (caller === wrapper) return {
4791
+ signal: caller,
4792
+ dispose() {}
4793
+ };
4794
+ const controller = new AbortController();
4795
+ let listening = false;
4796
+ const dispose = () => {
4797
+ if (!listening) return;
4798
+ listening = false;
4799
+ caller.removeEventListener("abort", abortFromCaller);
4800
+ wrapper.removeEventListener("abort", abortFromWrapper);
4801
+ };
4802
+ const abortFrom = (source) => {
4803
+ const reason = source.reason;
4804
+ controller.abort(reason);
4805
+ dispose();
4806
+ };
4807
+ const abortFromCaller = () => {
4808
+ abortFrom(caller);
4809
+ };
4810
+ const abortFromWrapper = () => {
4811
+ abortFrom(wrapper);
4812
+ };
4813
+ if (wrapper.aborted) abortFromWrapper();
4814
+ else if (caller.aborted) abortFromCaller();
4815
+ else {
4816
+ listening = true;
4817
+ caller.addEventListener("abort", abortFromCaller, { once: true });
4818
+ wrapper.addEventListener("abort", abortFromWrapper, { once: true });
4819
+ }
4820
+ return {
4821
+ signal: controller.signal,
4822
+ dispose
4823
+ };
4824
+ }
4825
+ /** Canonical result when cancellation supersedes success after body invocation. */
4826
+ function toolAbortedResult(prior) {
4827
+ const additionalContexts = prior?.additionalContexts ?? [];
4828
+ return {
4829
+ content: [{
4830
+ type: "text",
4831
+ text: "Error: tool call aborted"
4832
+ }],
4833
+ isError: true,
4834
+ error: {
4835
+ message: "tool call aborted",
4836
+ info: {
4837
+ name: "AbortError",
4838
+ code: TOOL_ABORTED
4839
+ }
4840
+ },
4841
+ ...additionalContexts.length > 0 ? { additionalContexts } : {}
4842
+ };
4843
+ }
4844
+ /** Canonical result when cancellation prevents tool body invocation. */
4845
+ function toolAbortedBeforeDispatchResult(prior) {
4846
+ const additionalContexts = prior?.additionalContexts ?? [];
4847
+ return {
4848
+ content: [{
4849
+ type: "text",
4850
+ text: "Error: tool call aborted before dispatch"
4851
+ }],
4852
+ isError: true,
4853
+ error: {
4854
+ message: "tool call aborted before dispatch",
4855
+ info: {
4856
+ name: "AbortError",
4857
+ code: TOOL_ABORTED_BEFORE_DISPATCH
4858
+ }
4859
+ },
4860
+ ...additionalContexts.length > 0 ? { additionalContexts } : {}
4861
+ };
4862
+ }
4863
+ //#endregion
8
4864
  //#region lib/types/shared.js
9
4865
  /** Browser route used by the generated-image card. */
10
4866
  const IMAGE_ROUTE = "/plugins/dsh-image-gen/image";
@@ -13477,7 +18333,7 @@ function imagePresentation(result) {
13477
18333
  * so an incompatible host degrades the settings UI instead of failing boot.
13478
18334
  */
13479
18335
  function installImageSettings(ctx, config, hooks) {
13480
- const namespace = dshSettings;
18336
+ const namespace = lib_exports;
13481
18337
  if (typeof namespace.SettingsProvider?.prototype?.installSection === "function") {
13482
18338
  ctx.inject(["settings"], (settingsCtx) => {
13483
18339
  settingsCtx.settings.installSection(ctx, IMAGE_GENERATION_NAMESPACE, Config, config, hooks);