@qilin-agent/tools 0.0.0-stage → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,3705 @@
1
+ import { Service } from "@qilin-agent/kylin";
2
+ import z from "@qilin-agent/schemastery";
3
+ import { AnonymousEntries, NamedEntries, ScopedLayers, scopeOf, scopeTarget } from "@qilin-agent/scope";
4
+ import { HarnessError, createUserMessage } from "@qilin-agent/llm";
5
+ import { assertNever, deepFreeze, isJsonValue, snapshotJsonValue } from "@qilin-agent/util-values";
6
+ import { brandString } from "@qilin-agent/brand";
7
+ import { ESCALATION_TARGETS, approveEscalation, validateEscalationArgs } from "@qilin-agent/sandbox";
8
+ //#region lib/types/json-schema.js
9
+ /**
10
+ * Enforced JSON Schema subset shared by tool outputs, generated PTC mode
11
+ * types, subagents, and workflows. The subset accepts any JSON root, an
12
+ * annotation-only schema for unconstrained JSON, one scalar `type`, object
13
+ * `properties`/`required`/boolean `additionalProperties`, array `items`,
14
+ * type-correct scalar `enum`/`const`, and exact-one `oneOf`.
15
+ *
16
+ * Unsupported or misplaced keywords reject rather than being accepted without
17
+ * enforcement. Consumers that require an object root apply
18
+ * {@link assertObjectJsonSchema} before accepting input.
19
+ * @module qilin-tools/json-schema
20
+ */
21
+ /**
22
+ * Thrown when a raw schema falls outside the enforced subset. `violations`
23
+ * lists every offending path instead of stopping at the first author error.
24
+ */
25
+ var JsonSchemaError = class extends HarnessError {
26
+ /** Individual schema violations in walk order. */
27
+ violations;
28
+ constructor(violations) {
29
+ super(`unsupported JSON schema: ${violations.join("; ")}`, "UNSUPPORTED_SCHEMA");
30
+ this.name = "JsonSchemaError";
31
+ this.violations = violations;
32
+ }
33
+ };
34
+ const CONSTRAINT_KEYWORDS = new Set([
35
+ "type",
36
+ "oneOf",
37
+ "properties",
38
+ "required",
39
+ "additionalProperties",
40
+ "items",
41
+ "enum",
42
+ "const"
43
+ ]);
44
+ const ANNOTATION_KEYWORDS = new Set([
45
+ "description",
46
+ "title",
47
+ "default",
48
+ "examples"
49
+ ]);
50
+ const SCHEMA_TYPES = [
51
+ "object",
52
+ "array",
53
+ "string",
54
+ "number",
55
+ "integer",
56
+ "boolean",
57
+ "null"
58
+ ];
59
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
60
+ function hasIntrinsicConstructor(prototype, name) {
61
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
62
+ if (typeof constructor !== "function") return false;
63
+ try {
64
+ return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+ /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
70
+ function isIntrinsicObjectPrototype(value) {
71
+ return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
72
+ }
73
+ /**
74
+ * Test for a realm-agnostic plain JSON record without accepting arrays or
75
+ * exotic objects.
76
+ * @param value - candidate record from any JavaScript realm.
77
+ * @returns Whether the value has a plain-object prototype chain.
78
+ */
79
+ function isPlainJsonRecord(value) {
80
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
81
+ try {
82
+ const prototype = Object.getPrototypeOf(value);
83
+ return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype(prototype);
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+ /** Whether an array uses one realm's intrinsic `Array.prototype`. */
89
+ function hasPlainArrayPrototype(value) {
90
+ const prototype = Object.getPrototypeOf(value);
91
+ if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
92
+ const objectPrototype = Object.getPrototypeOf(prototype);
93
+ return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype(objectPrototype);
94
+ }
95
+ /** Return whether a record contains only own enumerable string keys. */
96
+ function hasOnlyEnumerableStringKeys(value) {
97
+ try {
98
+ return Reflect.ownKeys(value).every((key) => typeof key === "string" && Object.prototype.propertyIsEnumerable.call(value, key));
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+ /**
104
+ * Test for an ordinary schema record whose keys survive JSON projection.
105
+ * @param value - candidate record from any JavaScript realm.
106
+ * @returns Whether the record has an intrinsic prototype and only own enumerable string keys.
107
+ */
108
+ function isJsonSchemaRecord(value) {
109
+ return isPlainJsonRecord(value) && hasOnlyEnumerableStringKeys(value);
110
+ }
111
+ /**
112
+ * Test for a dense ordinary array with no JSON-invisible decorations.
113
+ * @param value - candidate array from any JavaScript realm.
114
+ * @returns Whether the array is intrinsic, dense, and undecorated.
115
+ */
116
+ function isPlainJsonArray(value) {
117
+ if (!Array.isArray(value)) return false;
118
+ try {
119
+ if (!hasPlainArrayPrototype(value) || Reflect.ownKeys(value).length !== value.length + 1) return false;
120
+ for (let index = 0; index < value.length; index++) if (!Object.hasOwn(value, index)) return false;
121
+ return true;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+ /** Lossless finite JSON number, excluding negative zero. */
127
+ function isJsonNumber(value) {
128
+ return typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0);
129
+ }
130
+ /** Whether a scalar is valid for one declared schema type. */
131
+ function scalarMatches(type, value) {
132
+ switch (type) {
133
+ case "string": return typeof value === "string";
134
+ case "number": return isJsonNumber(value);
135
+ case "integer": return isJsonNumber(value) && Number.isInteger(value);
136
+ case "boolean": return typeof value === "boolean";
137
+ case "null": return value === null;
138
+ /* v8 ignore next -- JsonSchemaScalarType is closed; this retains compile-time exhaustiveness. */
139
+ default: return assertNever(type, "JsonSchemaType");
140
+ }
141
+ }
142
+ /** Keywords that are invalid beside `oneOf`. */
143
+ const ONE_OF_SIBLING_KEYWORDS = [
144
+ "properties",
145
+ "required",
146
+ "additionalProperties",
147
+ "items",
148
+ "enum",
149
+ "const"
150
+ ];
151
+ /** Validate object-only fields after its property schemas have been visited. */
152
+ function checkObjectSchemaTail(node, path, properties, violations) {
153
+ const hasRequired = Object.hasOwn(node, "required");
154
+ const required = hasRequired ? node.required : void 0;
155
+ if (hasRequired) if (!isPlainJsonArray(required) || required.some((entry) => typeof entry !== "string")) violations.push(`${path}.required must be an array of strings`);
156
+ else {
157
+ const declared = isJsonSchemaRecord(properties) ? properties : {};
158
+ for (const key of required) if (!Object.hasOwn(declared, key)) violations.push(`${path}.required names "${key}" which is not in properties`);
159
+ }
160
+ if (Object.hasOwn(node, "additionalProperties") && typeof node.additionalProperties !== "boolean") violations.push(`${path}.additionalProperties must be a boolean`);
161
+ }
162
+ /** Collect every violation for one raw schema tree without using the JavaScript call stack. */
163
+ function checkSchemaNode(root, rootPath, violations, seen) {
164
+ const tasks = [{
165
+ kind: "enter",
166
+ node: root,
167
+ path: rootPath
168
+ }];
169
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
170
+ if (task.kind === "leave") {
171
+ seen.delete(task.node);
172
+ continue;
173
+ }
174
+ if (task.kind === "one-of-tail") {
175
+ for (const key of ONE_OF_SIBLING_KEYWORDS) if (Object.hasOwn(task.node, key)) violations.push(`${task.path}.${key} is not supported beside oneOf`);
176
+ continue;
177
+ }
178
+ if (task.kind === "object-tail") {
179
+ checkObjectSchemaTail(task.node, task.path, task.properties, violations);
180
+ continue;
181
+ }
182
+ const { node, path } = task;
183
+ if (!isJsonSchemaRecord(node)) {
184
+ violations.push(`${path} must be a schema object`);
185
+ continue;
186
+ }
187
+ if (seen.has(node)) {
188
+ violations.push(`${path} is circular`);
189
+ continue;
190
+ }
191
+ seen.add(node);
192
+ tasks.push({
193
+ kind: "leave",
194
+ node
195
+ });
196
+ for (const key of Object.keys(node)) {
197
+ if (CONSTRAINT_KEYWORDS.has(key)) continue;
198
+ if (ANNOTATION_KEYWORDS.has(key)) {
199
+ try {
200
+ if (!isJsonValue(node[key])) violations.push(`${path}.${key} annotation must be lossless JSON data`);
201
+ } catch {
202
+ violations.push(`${path}.${key} annotation must be lossless JSON data`);
203
+ }
204
+ continue;
205
+ }
206
+ violations.push(`${path}.${key} is not a supported keyword (subset: type/oneOf/properties/required/additionalProperties/items/enum/const + annotations)`);
207
+ }
208
+ if (Object.hasOwn(node, "description") && typeof node.description !== "string") violations.push(`${path}.description must be a string`);
209
+ if (Object.hasOwn(node, "title") && typeof node.title !== "string") violations.push(`${path}.title must be a string`);
210
+ const hasType = Object.hasOwn(node, "type");
211
+ const hasOneOf = Object.hasOwn(node, "oneOf");
212
+ if (hasType && hasOneOf) {
213
+ violations.push(`${path} cannot declare both type and oneOf`);
214
+ continue;
215
+ }
216
+ if (!hasType && !hasOneOf) {
217
+ for (const key of ONE_OF_SIBLING_KEYWORDS) if (Object.hasOwn(node, key)) violations.push(`${path}.${key} requires type or oneOf`);
218
+ continue;
219
+ }
220
+ if (hasOneOf) {
221
+ const oneOf = node.oneOf;
222
+ tasks.push({
223
+ kind: "one-of-tail",
224
+ node,
225
+ path
226
+ });
227
+ if (!isPlainJsonArray(oneOf) || oneOf.length < 2) violations.push(`${path}.oneOf must be an array of at least two schemas`);
228
+ else for (let index = oneOf.length - 1; index >= 0; index--) tasks.push({
229
+ kind: "enter",
230
+ node: oneOf[index],
231
+ path: `${path}.oneOf[${index}]`
232
+ });
233
+ continue;
234
+ }
235
+ const type = node.type;
236
+ if (typeof type !== "string" || !SCHEMA_TYPES.includes(type)) {
237
+ 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("/")}`);
238
+ continue;
239
+ }
240
+ const schemaType = type;
241
+ for (const [key, types] of Object.entries({
242
+ properties: ["object"],
243
+ required: ["object"],
244
+ additionalProperties: ["object"],
245
+ items: ["array"],
246
+ enum: [
247
+ "string",
248
+ "number",
249
+ "integer",
250
+ "boolean",
251
+ "null"
252
+ ],
253
+ const: [
254
+ "string",
255
+ "number",
256
+ "integer",
257
+ "boolean",
258
+ "null"
259
+ ]
260
+ })) if (Object.hasOwn(node, key) && !types.includes(schemaType)) violations.push(`${path}.${key} is not supported on type "${schemaType}"`);
261
+ switch (schemaType) {
262
+ case "object": {
263
+ const properties = Object.hasOwn(node, "properties") ? node.properties : void 0;
264
+ tasks.push({
265
+ kind: "object-tail",
266
+ node,
267
+ path,
268
+ properties
269
+ });
270
+ if (Object.hasOwn(node, "properties")) if (!isJsonSchemaRecord(properties)) violations.push(`${path}.properties must be an object of schemas`);
271
+ else {
272
+ const entries = Object.entries(properties);
273
+ for (let index = entries.length - 1; index >= 0; index--) {
274
+ const entry = entries[index];
275
+ /* v8 ignore next -- the loop is bounded by the captured entry count. */
276
+ if (entry === void 0) continue;
277
+ tasks.push({
278
+ kind: "enter",
279
+ node: entry[1],
280
+ path: `${path}.properties.${entry[0]}`
281
+ });
282
+ }
283
+ }
284
+ break;
285
+ }
286
+ case "array":
287
+ if (Object.hasOwn(node, "items")) tasks.push({
288
+ kind: "enter",
289
+ node: node.items,
290
+ path: `${path}.items`
291
+ });
292
+ break;
293
+ case "string":
294
+ case "number":
295
+ case "integer":
296
+ case "boolean":
297
+ case "null": {
298
+ const hasEnum = Object.hasOwn(node, "enum");
299
+ const allowed = hasEnum ? node.enum : void 0;
300
+ const enumValid = isPlainJsonArray(allowed) && allowed.length > 0 && allowed.every((entry) => scalarMatches(schemaType, entry));
301
+ if (hasEnum && !enumValid) violations.push(`${path}.enum must be a non-empty array of ${schemaType} values`);
302
+ const hasConst = Object.hasOwn(node, "const");
303
+ const declaredConst = hasConst ? node.const : void 0;
304
+ const constValid = scalarMatches(schemaType, declaredConst);
305
+ if (hasConst) {
306
+ if (!constValid) violations.push(`${path}.const must be a ${schemaType} value`);
307
+ else if (enumValid && !allowed.includes(declaredConst)) violations.push(`${path}.const must be one of ${path}.enum when both are declared`);
308
+ }
309
+ break;
310
+ }
311
+ /* v8 ignore next -- schemaType was narrowed from the closed SCHEMA_TYPES table above. */
312
+ default: assertNever(schemaType, "JsonSchemaType");
313
+ }
314
+ }
315
+ }
316
+ /**
317
+ * Assert that an arbitrary raw schema uses only the enforced subset.
318
+ * Annotation-only schemas are accepted as the standard unconstrained-JSON
319
+ * form; callers that require an object root use {@link assertObjectJsonSchema}.
320
+ * @param schema - untrusted raw JSON Schema.
321
+ * @returns Assertion that the schema belongs to the supported subset.
322
+ */
323
+ function assertSupportedJsonSchema(schema) {
324
+ const violations = [];
325
+ checkSchemaNode(schema, "schema", violations, /* @__PURE__ */ new Set());
326
+ if (violations.length > 0) throw new JsonSchemaError(violations);
327
+ }
328
+ /**
329
+ * Assert the enforced subset plus the object-root constraint retained by
330
+ * subagent and workflow structured outputs.
331
+ * @param schema - untrusted caller-supplied schema.
332
+ * @returns Assertion that the schema belongs to the supported subset and has an object root.
333
+ */
334
+ function assertObjectJsonSchema(schema) {
335
+ const violations = [];
336
+ checkSchemaNode(schema, "schema", violations, /* @__PURE__ */ new Set());
337
+ if (violations.length === 0 && (!isJsonSchemaRecord(schema) || !Object.hasOwn(schema, "type") || schema.type !== "object")) violations.push("schema.type must be \"object\" (structured output is object-rooted)");
338
+ if (violations.length > 0) throw new JsonSchemaError(violations);
339
+ }
340
+ /** Safely test the lossless JSON boundary when a getter may throw. */
341
+ function safelyIsJsonValue(value) {
342
+ try {
343
+ return isJsonValue(value);
344
+ } catch {
345
+ return false;
346
+ }
347
+ }
348
+ /** Root-aware diagnostic path for the parameter validator's empty sentinel. */
349
+ function diagnosticPath(path) {
350
+ return path === "" ? "arguments" : path;
351
+ }
352
+ /** Append one object property without a leading dot at an implicit root. */
353
+ function propertyPath(path, key) {
354
+ return path === "" ? key : `${path}.${key}`;
355
+ }
356
+ /** The generic exception-containment diagnostic owned by one valid schema node. */
357
+ function losslessValueViolation(path) {
358
+ return [`"${diagnosticPath(path)}" must be a lossless JSON value`];
359
+ }
360
+ /** Append diagnostics without spreading a potentially wide child result as call arguments. */
361
+ function appendViolations(target, source) {
362
+ for (const violation of source) target.push(violation);
363
+ }
364
+ /** Initialize one validation frame with empty aggregation state. */
365
+ function valueFrame(node, value, path) {
366
+ return {
367
+ node,
368
+ value,
369
+ path,
370
+ catches: false,
371
+ phase: "start",
372
+ children: [],
373
+ childIndex: 0,
374
+ violations: [],
375
+ tailViolations: [],
376
+ matches: 0
377
+ };
378
+ }
379
+ /** Validate one scalar node after its primitive type check. */
380
+ function checkScalarValue(node, value, path) {
381
+ const allowed = Object.hasOwn(node, "enum") ? node.enum : void 0;
382
+ if (allowed !== void 0 && !allowed.includes(value)) return [`"${diagnosticPath(path)}" must be one of ${JSON.stringify(allowed)}`];
383
+ if (Object.hasOwn(node, "const") && value !== node.const) return [`"${diagnosticPath(path)}" must be ${JSON.stringify(node.const)}`];
384
+ return [];
385
+ }
386
+ /** Validate one trusted schema/value pair with explicit frames rather than recursive calls. */
387
+ function checkValue(schema, value, path) {
388
+ const frames = [valueFrame(schema, value, path)];
389
+ let rootResult;
390
+ const receive = (result) => {
391
+ const parent = frames.at(-1);
392
+ if (parent === void 0) {
393
+ rootResult = result;
394
+ return;
395
+ }
396
+ if (parent.kind === "oneOf") {
397
+ if (result.length === 0) parent.matches++;
398
+ } else appendViolations(parent.violations, result);
399
+ };
400
+ const finish = (result) => {
401
+ frames.pop();
402
+ receive(result);
403
+ };
404
+ while (frames.length > 0) {
405
+ const frame = frames.at(-1);
406
+ /* v8 ignore next -- the loop condition guarantees a current frame. */
407
+ if (frame === void 0) break;
408
+ try {
409
+ if (frame.phase === "children") {
410
+ if (frame.childIndex < frame.children.length) {
411
+ const child = frame.children[frame.childIndex];
412
+ /* v8 ignore next -- childIndex is bounded by children.length. */
413
+ if (child === void 0) throw new Error("missing schema-value child frame");
414
+ frame.childIndex++;
415
+ frames.push(valueFrame(child.node, child.value, child.path));
416
+ continue;
417
+ }
418
+ if (frame.kind === "oneOf") {
419
+ finish(frame.matches === 1 ? [] : [`"${diagnosticPath(frame.path)}" must match exactly one oneOf branch (matched ${frame.matches})`]);
420
+ continue;
421
+ }
422
+ appendViolations(frame.violations, frame.tailViolations);
423
+ if (frame.violations.length > 0) finish(frame.violations);
424
+ else if (frame.kind === "object") finish(safelyIsJsonValue(frame.value) ? [] : [`"${diagnosticPath(frame.path)}" must be a lossless JSON object`]);
425
+ else finish(safelyIsJsonValue(frame.value) ? [] : [`"${diagnosticPath(frame.path)}" must be a dense lossless JSON array`]);
426
+ continue;
427
+ }
428
+ const nodeType = Object.hasOwn(frame.node, "type") ? frame.node.type : void 0;
429
+ frame.catches = !(nodeType !== void 0 && !SCHEMA_TYPES.includes(nodeType));
430
+ const oneOf = Object.hasOwn(frame.node, "oneOf") ? frame.node.oneOf : void 0;
431
+ if (oneOf !== void 0) {
432
+ frame.kind = "oneOf";
433
+ frame.children = Array.from(oneOf, (branch) => ({
434
+ node: branch,
435
+ value: frame.value,
436
+ path: frame.path
437
+ }));
438
+ frame.childIndex = 0;
439
+ frame.matches = 0;
440
+ frame.phase = "children";
441
+ continue;
442
+ }
443
+ if (nodeType === void 0) {
444
+ finish(safelyIsJsonValue(frame.value) ? [] : losslessValueViolation(frame.path));
445
+ continue;
446
+ }
447
+ switch (nodeType) {
448
+ case "object": {
449
+ if (!isPlainJsonRecord(frame.value)) {
450
+ finish([`"${diagnosticPath(frame.path)}" must be an object`]);
451
+ break;
452
+ }
453
+ const properties = Object.hasOwn(frame.node, "properties") ? frame.node.properties ?? {} : {};
454
+ const violations = [];
455
+ const required = Object.hasOwn(frame.node, "required") ? frame.node.required ?? [] : [];
456
+ 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)}"`);
457
+ const children = [];
458
+ for (const [key, child] of Object.entries(properties)) {
459
+ if (!Object.hasOwn(frame.value, key) || frame.value[key] === void 0) continue;
460
+ children.push({
461
+ node: child,
462
+ value: frame.value[key],
463
+ path: propertyPath(frame.path, key)
464
+ });
465
+ }
466
+ const tailViolations = [];
467
+ if (Object.hasOwn(frame.node, "additionalProperties") && frame.node.additionalProperties === false) {
468
+ 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)`);
469
+ }
470
+ frame.kind = "object";
471
+ frame.children = children;
472
+ frame.childIndex = 0;
473
+ frame.violations = violations;
474
+ frame.tailViolations = tailViolations;
475
+ frame.phase = "children";
476
+ break;
477
+ }
478
+ case "array": {
479
+ if (!Array.isArray(frame.value)) {
480
+ finish([`"${diagnosticPath(frame.path)}" must be an array`]);
481
+ break;
482
+ }
483
+ const items = Object.hasOwn(frame.node, "items") ? frame.node.items : void 0;
484
+ const children = items === void 0 ? [] : frame.value.flatMap((entry, index) => [{
485
+ node: items,
486
+ value: entry,
487
+ path: `${frame.path}[${index}]`
488
+ }]);
489
+ frame.kind = "array";
490
+ frame.children = children;
491
+ frame.childIndex = 0;
492
+ frame.violations = [];
493
+ frame.phase = "children";
494
+ break;
495
+ }
496
+ case "string":
497
+ finish(typeof frame.value === "string" ? checkScalarValue(frame.node, frame.value, frame.path) : [`"${diagnosticPath(frame.path)}" must be a string`]);
498
+ break;
499
+ case "number":
500
+ 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));
501
+ break;
502
+ case "integer":
503
+ finish(!isJsonNumber(frame.value) || !Number.isInteger(frame.value) ? [`"${diagnosticPath(frame.path)}" must be an integer`] : checkScalarValue(frame.node, frame.value, frame.path));
504
+ break;
505
+ case "boolean":
506
+ finish(typeof frame.value === "boolean" ? checkScalarValue(frame.node, frame.value, frame.path) : [`"${diagnosticPath(frame.path)}" must be a boolean`]);
507
+ break;
508
+ case "null":
509
+ finish(frame.value === null ? checkScalarValue(frame.node, frame.value, frame.path) : [`"${diagnosticPath(frame.path)}" must be null`]);
510
+ break;
511
+ default: finish(assertNever(nodeType, "JsonSchemaType"));
512
+ }
513
+ } catch (error) {
514
+ let failed = frames.pop();
515
+ while (failed !== void 0 && !failed.catches) failed = frames.pop();
516
+ if (failed === void 0) throw error;
517
+ receive(losslessValueViolation(failed.path));
518
+ }
519
+ }
520
+ /* v8 ignore next -- every root frame finishes or throws. */
521
+ return rootResult ?? losslessValueViolation(path);
522
+ }
523
+ /**
524
+ * Validate a candidate value against an asserted raw schema. The function is
525
+ * total for arbitrary values and returns path-qualified violations.
526
+ * @param schema - a schema accepted by {@link assertSupportedJsonSchema}.
527
+ * @param value - the candidate JSON value.
528
+ * @param path - root label used in diagnostics.
529
+ * @returns All violations in walk order; empty means valid.
530
+ */
531
+ function validateJsonSchemaValue(schema, value, path = "value") {
532
+ return checkValue(schema, value, path);
533
+ }
534
+ //#endregion
535
+ //#region lib/types/schema.js
536
+ /** Unified JSON-value schema DSL, inference, compilation, and typed tool helper. @module qilin-tools/schema */
537
+ const ANNOTATION_KEYS = [
538
+ "description",
539
+ "title",
540
+ "default",
541
+ "examples"
542
+ ];
543
+ /** Throw one author-schema violation through the shared schema error type. */
544
+ function authorError(message) {
545
+ throw new JsonSchemaError([message]);
546
+ }
547
+ /** Copy own annotation fields for validation by the raw-schema boundary. */
548
+ function copyAnnotations(source, target) {
549
+ if (Object.hasOwn(source, "description")) target.description = source.description;
550
+ if (Object.hasOwn(source, "title")) target.title = source.title;
551
+ if (Object.hasOwn(source, "default")) target.default = source.default;
552
+ if (Object.hasOwn(source, "examples")) target.examples = source.examples;
553
+ }
554
+ /** Reject author-only keys outside one node's declared vocabulary. */
555
+ function assertAuthorKeys(source, path, allowed) {
556
+ for (const key of Object.keys(source)) if (!allowed.includes(key)) authorError(`${path}.${key} is not supported by the value schema DSL`);
557
+ }
558
+ /** Install a compiled node without giving `__proto__` assignment semantics. */
559
+ function assignCompiledNode(destination, node) {
560
+ switch (destination.kind) {
561
+ case "root":
562
+ destination.holder.value = node;
563
+ break;
564
+ case "property":
565
+ Object.defineProperty(destination.target, destination.key, {
566
+ value: node,
567
+ enumerable: true,
568
+ configurable: true,
569
+ writable: true
570
+ });
571
+ break;
572
+ case "item":
573
+ destination.target.items = node;
574
+ break;
575
+ case "one-of":
576
+ destination.target[destination.index] = node;
577
+ break;
578
+ }
579
+ }
580
+ /** Install a compiled property map at its root or containing object node. */
581
+ function assignCompiledPropertyMap(destination, compiled) {
582
+ if (destination.kind === "root") destination.holder.value = compiled;
583
+ else destination.target.properties = compiled.properties;
584
+ }
585
+ /** Execute an author-schema compilation task graph without recursive descent. */
586
+ function runSchemaCompiler(initial) {
587
+ const seen = /* @__PURE__ */ new Set();
588
+ const tasks = [initial];
589
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
590
+ if (task.kind === "leave") {
591
+ seen.delete(task.input);
592
+ continue;
593
+ }
594
+ if (task.kind === "property-map-tail") {
595
+ if (task.required.length > 0) {
596
+ task.compiled.required = task.required;
597
+ if (task.destination.kind === "object") task.destination.target.required = task.required;
598
+ }
599
+ continue;
600
+ }
601
+ if (task.kind === "property") {
602
+ if (!isJsonSchemaRecord(task.property)) authorError(`${task.path} must be a value schema object`);
603
+ if (Object.hasOwn(task.property, "required") && task.property.required !== true) authorError(`${task.path}.required must be true when present`);
604
+ if (Object.hasOwn(task.property, "required") && task.property.required === true) task.required.push(task.key);
605
+ tasks.push({
606
+ kind: "value",
607
+ input: task.property,
608
+ path: task.path,
609
+ allowRequired: true,
610
+ destination: {
611
+ kind: "property",
612
+ target: task.properties,
613
+ key: task.key
614
+ }
615
+ });
616
+ continue;
617
+ }
618
+ if (task.kind === "property-map") {
619
+ if (!isJsonSchemaRecord(task.input)) authorError(`${task.path} must be an object of value schemas`);
620
+ if (seen.has(task.input)) authorError(`${task.path} is circular`);
621
+ seen.add(task.input);
622
+ const compiled = { properties: {} };
623
+ const required = [];
624
+ assignCompiledPropertyMap(task.destination, compiled);
625
+ tasks.push({
626
+ kind: "leave",
627
+ input: task.input
628
+ });
629
+ tasks.push({
630
+ kind: "property-map-tail",
631
+ compiled,
632
+ required,
633
+ destination: task.destination
634
+ });
635
+ const entries = Object.entries(task.input);
636
+ for (let index = entries.length - 1; index >= 0; index--) {
637
+ const entry = entries[index];
638
+ /* v8 ignore next -- the loop is bounded by the captured entry count. */
639
+ if (entry === void 0) continue;
640
+ tasks.push({
641
+ kind: "property",
642
+ property: entry[1],
643
+ path: `${task.path}.${entry[0]}`,
644
+ key: entry[0],
645
+ properties: compiled.properties,
646
+ required
647
+ });
648
+ }
649
+ continue;
650
+ }
651
+ const { input, path } = task;
652
+ if (!isJsonSchemaRecord(input)) authorError(`${path} must be a value schema object`);
653
+ if (seen.has(input)) authorError(`${path} is circular`);
654
+ seen.add(input);
655
+ const authorKeys = [...ANNOTATION_KEYS, ...task.allowRequired ? ["required"] : []];
656
+ const node = {};
657
+ assignCompiledNode(task.destination, node);
658
+ tasks.push({
659
+ kind: "leave",
660
+ input
661
+ });
662
+ if (Object.hasOwn(input, "oneOf")) {
663
+ assertAuthorKeys(input, path, [
664
+ ...authorKeys,
665
+ "oneOf",
666
+ "type"
667
+ ]);
668
+ if (Object.hasOwn(input, "type")) authorError(`${path} cannot declare both type and oneOf`);
669
+ if (!isPlainJsonArray(input.oneOf)) authorError(`${path}.oneOf must be an array of at least two value schemas`);
670
+ const branches = [];
671
+ node.oneOf = branches;
672
+ copyAnnotations(input, node);
673
+ for (let index = input.oneOf.length - 1; index >= 0; index--) tasks.push({
674
+ kind: "value",
675
+ input: input.oneOf[index],
676
+ path: `${path}.oneOf[${index}]`,
677
+ allowRequired: false,
678
+ destination: {
679
+ kind: "one-of",
680
+ target: branches,
681
+ index
682
+ }
683
+ });
684
+ continue;
685
+ }
686
+ const inputType = Object.hasOwn(input, "type") ? input.type : void 0;
687
+ switch (inputType) {
688
+ case "json":
689
+ assertAuthorKeys(input, path, [...authorKeys, "type"]);
690
+ copyAnnotations(input, node);
691
+ break;
692
+ case "object":
693
+ assertAuthorKeys(input, path, [
694
+ ...authorKeys,
695
+ "type",
696
+ "properties",
697
+ "additionalProperties"
698
+ ]);
699
+ if (!Object.hasOwn(input, "additionalProperties") || typeof input.additionalProperties !== "boolean") authorError(`${path}.additionalProperties must be explicitly true or false`);
700
+ node.type = "object";
701
+ copyAnnotations(input, node);
702
+ node.additionalProperties = input.additionalProperties;
703
+ if (Object.hasOwn(input, "properties")) tasks.push({
704
+ kind: "property-map",
705
+ input: input.properties,
706
+ path: `${path}.properties`,
707
+ destination: {
708
+ kind: "object",
709
+ target: node
710
+ }
711
+ });
712
+ break;
713
+ case "array":
714
+ assertAuthorKeys(input, path, [
715
+ ...authorKeys,
716
+ "type",
717
+ "items"
718
+ ]);
719
+ node.type = "array";
720
+ copyAnnotations(input, node);
721
+ if (Object.hasOwn(input, "items")) tasks.push({
722
+ kind: "value",
723
+ input: input.items,
724
+ path: `${path}.items`,
725
+ allowRequired: false,
726
+ destination: {
727
+ kind: "item",
728
+ target: node
729
+ }
730
+ });
731
+ break;
732
+ case "string":
733
+ case "number":
734
+ case "integer":
735
+ case "boolean":
736
+ case "null":
737
+ assertAuthorKeys(input, path, [
738
+ ...authorKeys,
739
+ "type",
740
+ "enum",
741
+ "const"
742
+ ]);
743
+ node.type = inputType;
744
+ copyAnnotations(input, node);
745
+ if (Object.hasOwn(input, "enum")) {
746
+ if (!isPlainJsonArray(input.enum)) authorError(`${path}.enum must be a non-empty array of scalar values`);
747
+ node.enum = Array.from(input.enum, (entry) => entry);
748
+ }
749
+ if (Object.hasOwn(input, "const")) node.const = input.const;
750
+ break;
751
+ default: authorError(`${path}.type must be string/number/integer/boolean/null/array/object/json, or use oneOf`);
752
+ }
753
+ }
754
+ }
755
+ /** Compile one implicit property map, collecting per-property requiredness. */
756
+ function compilePropertyMap(input, path) {
757
+ const holder = {};
758
+ runSchemaCompiler({
759
+ kind: "property-map",
760
+ input,
761
+ path,
762
+ destination: {
763
+ kind: "root",
764
+ holder
765
+ }
766
+ });
767
+ /* v8 ignore next -- the root task assigns before scheduling any descendants. */
768
+ return holder.value ?? authorError(`${path} did not compile`);
769
+ }
770
+ /** Compile one author node without applying any consumer root restriction. */
771
+ function compileValueSchema(input, path) {
772
+ const holder = {};
773
+ runSchemaCompiler({
774
+ kind: "value",
775
+ input,
776
+ path,
777
+ allowRequired: false,
778
+ destination: {
779
+ kind: "root",
780
+ holder
781
+ }
782
+ });
783
+ /* v8 ignore next -- the root task assigns before scheduling any descendants. */
784
+ return holder.value ?? authorError(`${path} did not compile`);
785
+ }
786
+ /**
787
+ * Compile one author-facing value schema to the enforced raw JSON Schema
788
+ * subset. The author-only `json` node becomes an annotation-only schema.
789
+ * @param spec - schema for any JSON-value root.
790
+ * @returns The asserted raw schema projection.
791
+ */
792
+ function valueSchemaSpecToJsonSchema(spec) {
793
+ const schema = compileValueSchema(spec, "schema");
794
+ assertSupportedJsonSchema(schema);
795
+ return schema;
796
+ }
797
+ /**
798
+ * Compile the implicit open parameter object into raw JSON Schema.
799
+ * @param spec - per-property parameter definitions.
800
+ * @returns An object-rooted raw schema with no implicit-root openness override.
801
+ */
802
+ function parameterSchemaSpecToJsonSchema(spec) {
803
+ const compiled = compilePropertyMap(spec, "parameters");
804
+ const schema = {
805
+ type: "object",
806
+ properties: compiled.properties,
807
+ ...compiled.required === void 0 ? {} : { required: compiled.required }
808
+ };
809
+ assertSupportedJsonSchema(schema);
810
+ return schema;
811
+ }
812
+ /** Invalid model-generated arguments for a typed tool. */
813
+ var ToolArgsError = class extends HarnessError {
814
+ /** Individual violations in schema-walk order. */
815
+ violations;
816
+ constructor(violations) {
817
+ super(`invalid arguments: ${violations.join("; ")}`, "INVALID_ARGS");
818
+ this.name = "ToolArgsError";
819
+ this.violations = violations;
820
+ }
821
+ };
822
+ /**
823
+ * Validate model-generated arguments against an implicit parameter schema.
824
+ * @param spec - declared parameter schema.
825
+ * @param args - candidate arguments, however malformed.
826
+ * @returns Path-qualified violations; empty means valid.
827
+ */
828
+ function validateArgs(spec, args) {
829
+ return validateJsonSchemaValue(parameterSchemaSpecToJsonSchema(spec), args, "");
830
+ }
831
+ /**
832
+ * Define a first-party tool with inferred arguments and strict execution
833
+ * validation. Replay-only presenters validate softly and fall back to generic
834
+ * rendering for obsolete logged arguments.
835
+ * @param options - typed definition and optional finalizer and presenters.
836
+ * @returns A registry-ready definition.
837
+ */
838
+ function defineTool(options) {
839
+ const userExecute = options.execute;
840
+ const userFinalizeContent = options.finalizeContent;
841
+ const userRender = options.output.render;
842
+ const userPresentationMeta = options.output.presentationMeta;
843
+ const userPresentCall = options.presentCall;
844
+ const userPresentResult = options.presentResult;
845
+ const userIsConcurrencySafe = options.isConcurrencySafe;
846
+ 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`);
847
+ const parameters = parameterSchemaSpecToJsonSchema(options.parameters);
848
+ const outputSchema = valueSchemaSpecToJsonSchema(options.output.schema);
849
+ const validate = (args) => validateJsonSchemaValue(parameters, args, "");
850
+ const tool = {
851
+ name: options.name,
852
+ description: options.description,
853
+ parameters,
854
+ output: {
855
+ schema: outputSchema,
856
+ render(args, value) {
857
+ return userRender(args, value);
858
+ },
859
+ ...userPresentationMeta !== void 0 ? { presentationMeta(args, value) {
860
+ return userPresentationMeta(args, value);
861
+ } } : {}
862
+ },
863
+ ...options.deferLoading === true ? { deferLoading: options.deferLoading } : {},
864
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
865
+ async execute(args, exec) {
866
+ const violations = validate(args);
867
+ if (violations.length > 0) throw new ToolArgsError(violations);
868
+ return userExecute(args, exec);
869
+ }
870
+ };
871
+ if (userFinalizeContent) tool.finalizeContent = (exec, result) => userFinalizeContent(exec, result);
872
+ if (userPresentCall) tool.presentCall = (args) => {
873
+ if (validate(args).length > 0) return void 0;
874
+ return userPresentCall(args);
875
+ };
876
+ if (userPresentResult) tool.presentResult = (args, result) => {
877
+ if (validate(args).length > 0) return void 0;
878
+ return userPresentResult(args, result);
879
+ };
880
+ if (userIsConcurrencySafe) tool.isConcurrencySafe = (args) => {
881
+ if (validate(args).length > 0) return false;
882
+ return userIsConcurrencySafe(args);
883
+ };
884
+ return tool;
885
+ }
886
+ //#endregion
887
+ //#region lib/types/ptc.js
888
+ /**
889
+ * PTC mode `run_code` transport. Programs call the registry's agent-visible
890
+ * tools through nested executions scheduled under the native concurrency
891
+ * contract; each sub-dispatch is logged for reconstruction, while only the
892
+ * outer curated result enters model history.
893
+ * @module @qilin-agent/tools/src/ptc
894
+ */
895
+ /** The model-facing name of the PTC mode tool. */
896
+ const RUN_CODE_NAME = "run_code";
897
+ /**
898
+ * The TypeScript flavor: the fallback for a schema read with no runtime
899
+ * mounted ({@link resolveFlavor} owns which readers reach that). A real
900
+ * assembly always resolves a runtime first, so the model never sees this
901
+ * fallback outside its own language.
902
+ */
903
+ const TYPESCRIPT_FLAVOR = {
904
+ description: "Execute a TypeScript program against the available tools. Takes two required arguments: `description`, a short summary of what the program does, and `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work). 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.",
905
+ codeDescription: "The program: the body of an async TypeScript function."
906
+ };
907
+ /** Per-language `run_code` schema flavors (see {@link RunCodeFlavor}); one entry per {@link PtcSdkLanguage}. */
908
+ const RUN_CODE_FLAVORS = {
909
+ typescript: TYPESCRIPT_FLAVOR,
910
+ python: {
911
+ description: "Execute a Python program against the available tools. Takes two required arguments: `description`, a short summary of what the program does, and `code`, the BODY of an async function (top-level `await` and `return` work). 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.",
912
+ codeDescription: "The program: the body of an async Python function."
913
+ }
914
+ };
915
+ /**
916
+ * The `description` parameter's model-facing description: language-independent
917
+ * (the UI label contract is the same for every runtime), shared between the
918
+ * static spec and the language-aware `parameters` getter so the two emissions
919
+ * can never drift.
920
+ */
921
+ const RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION = "Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Provide `description` before `code` in the arguments. Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\".";
922
+ const RUN_CODE_CONTROLS = {
923
+ timeoutMs: {
924
+ type: "number",
925
+ description: "Positive elapsed-time budget in milliseconds, capped by the deployment maximum."
926
+ },
927
+ sandbox_permissions: {
928
+ type: "string",
929
+ enum: [...ESCALATION_TARGETS],
930
+ description: "Wider sandbox mode for this complete program execution; requires justification and approval."
931
+ },
932
+ justification: {
933
+ type: "string",
934
+ description: "Reason this complete program needs wider access, shown to the user for approval."
935
+ }
936
+ };
937
+ function controlParameters(runtime) {
938
+ if (runtime === void 0) return RUN_CODE_CONTROLS;
939
+ return {
940
+ ...runtime.timeout === void 0 ? {} : { timeoutMs: {
941
+ ...RUN_CODE_CONTROLS.timeoutMs,
942
+ description: `Positive elapsed-time budget in milliseconds, including nested tool and approval waits. Default ${runtime.timeout.defaultMs}; capped at ${runtime.timeout.maxMs}. Zero does not disable the deadline.`
943
+ } },
944
+ ...runtime.sandboxMode === void 0 ? {} : {
945
+ sandbox_permissions: RUN_CODE_CONTROLS.sandbox_permissions,
946
+ justification: RUN_CODE_CONTROLS.justification
947
+ }
948
+ };
949
+ }
950
+ function escalationGuidance(runtime) {
951
+ return runtime?.sandboxMode === void 0 ? "" : " A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.";
952
+ }
953
+ /**
954
+ * Resolve the {@link RunCodeFlavor} for the loaded runtime's language, read at
955
+ * schema-emission time so the model-visible `run_code` schema always matches
956
+ * the SDK section's language. `peekRuntime` returns `undefined` only when no
957
+ * runtime is mounted, which reaches this function through definition readers
958
+ * and `schemas()` — the doc-catalog harvest is the only shipped one, and none
959
+ * of them feeds a model, because `wireSchemas` calls `requirePtcRuntime`
960
+ * before projecting — so that path degrades to {@link TYPESCRIPT_FLAVOR}. A
961
+ * mounted runtime whose language has no flavor entry fails loud, exactly as
962
+ * `requirePtcRuntime` rejects it at assembly. Keeping this table in step with
963
+ * `SDK_RENDERERS` is the compiler's job ({@link PtcSdkLanguage}); what this
964
+ * guard owns is the runtime-supplied language neither table knows, which never
965
+ * yields a wrong-language schema for a real runtime.
966
+ */
967
+ function resolveFlavor(peekRuntime) {
968
+ const runtime = peekRuntime();
969
+ if (runtime === void 0) return TYPESCRIPT_FLAVOR;
970
+ const flavor = RUN_CODE_FLAVORS[runtime.language];
971
+ if (!Object.hasOwn(RUN_CODE_FLAVORS, runtime.language) || flavor === void 0) {
972
+ const known = Object.keys(RUN_CODE_FLAVORS).map((name) => JSON.stringify(name)).join(", ");
973
+ throw new Error(`qilin-tools: no run_code schema flavor registered for runtime language ${JSON.stringify(runtime.language)} (known: ${known})`);
974
+ }
975
+ return flavor;
976
+ }
977
+ /**
978
+ * Thrown by `run_code` when the program run itself failed — a program
979
+ * exception, a budget expiry, an abort, or substrate death. Extends
980
+ * {@link HarnessError} (`code: 'CODE_RUN_FAILED'`); the registry's execution
981
+ * pipeline converts it into a structured `isError` result whose text carries
982
+ * the failure kind plus the captured logs, so the model can self-correct.
983
+ */
984
+ var CodeRunFailedError = class extends HarnessError {
985
+ constructor(message) {
986
+ super(message, "CODE_RUN_FAILED");
987
+ this.name = "CodeRunFailedError";
988
+ }
989
+ };
990
+ /**
991
+ * Snapshot one binding call's argument as lossless JSON, then snapshot that
992
+ * detached value again so dispatch and logging stay independent without
993
+ * reintroducing structured-clone's platform-specific nesting limit.
994
+ */
995
+ function jsonNormalizeArgs(value) {
996
+ let snapshot;
997
+ try {
998
+ snapshot = snapshotJsonValue(value);
999
+ } catch (error) {
1000
+ throw new Error(`tool arguments must be lossless JSON: ${error instanceof Error ? error.message : String(error)}`);
1001
+ }
1002
+ if (snapshot === void 0) throw new Error("tool arguments must be lossless JSON (call the tool with an arguments object, e.g. `{}`)");
1003
+ const logged = snapshotJsonValue(snapshot);
1004
+ /* v8 ignore next -- snapshot is already a detached lossless JSON value. */
1005
+ if (logged === void 0) throw new Error("tool arguments could not be detached for durable logging");
1006
+ return {
1007
+ dispatched: snapshot,
1008
+ logged
1009
+ };
1010
+ }
1011
+ /** Two-space JSON presentation, matching the existing shallow `run_code` text contract. */
1012
+ const JSON_INDENT = " ";
1013
+ /**
1014
+ * ECMAScript caps `JSON.stringify`'s `space` string at ten characters. The
1015
+ * renderer also caps TOTAL indentation there, compacting deeper subtrees, so
1016
+ * formatted output remains linear in the canonical JSON size.
1017
+ */
1018
+ const MAX_JSON_INDENT_CHARS = 10;
1019
+ /** Render one non-string JSON root without recursive traversal or unbounded indentation growth. */
1020
+ function renderJsonValue(value) {
1021
+ const chunks = [];
1022
+ const tasks = [{
1023
+ kind: "value",
1024
+ value,
1025
+ depth: 0,
1026
+ compact: false
1027
+ }];
1028
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
1029
+ if (task.kind === "text") {
1030
+ chunks.push(task.text);
1031
+ continue;
1032
+ }
1033
+ const current = task.value;
1034
+ if (current === null || typeof current === "boolean" || typeof current === "number") {
1035
+ chunks.push(String(current));
1036
+ continue;
1037
+ }
1038
+ if (typeof current === "string") {
1039
+ chunks.push(JSON.stringify(current));
1040
+ continue;
1041
+ }
1042
+ const compact = task.compact || (task.depth + 1) * 2 > MAX_JSON_INDENT_CHARS;
1043
+ const childDepth = task.depth + 1;
1044
+ if (Array.isArray(current)) {
1045
+ chunks.push("[");
1046
+ if (current.length === 0) {
1047
+ chunks.push("]");
1048
+ continue;
1049
+ }
1050
+ tasks.push({
1051
+ kind: "text",
1052
+ text: compact ? "]" : `\n${JSON_INDENT.repeat(task.depth)}]`
1053
+ });
1054
+ for (let index = current.length - 1; index >= 0; index--) {
1055
+ const item = current[index];
1056
+ /* v8 ignore next -- canonical JsonValue arrays are dense. */
1057
+ if (item === void 0) throw new Error("cannot render a sparse JSON array");
1058
+ tasks.push({
1059
+ kind: "value",
1060
+ value: item,
1061
+ depth: childDepth,
1062
+ compact
1063
+ });
1064
+ tasks.push({
1065
+ kind: "text",
1066
+ text: compact ? index === 0 ? "" : "," : `${index === 0 ? "\n" : ",\n"}${JSON_INDENT.repeat(childDepth)}`
1067
+ });
1068
+ }
1069
+ continue;
1070
+ }
1071
+ const keys = Object.keys(current);
1072
+ chunks.push("{");
1073
+ if (keys.length === 0) {
1074
+ chunks.push("}");
1075
+ continue;
1076
+ }
1077
+ tasks.push({
1078
+ kind: "text",
1079
+ text: compact ? "}" : `\n${JSON_INDENT.repeat(task.depth)}}`
1080
+ });
1081
+ for (let index = keys.length - 1; index >= 0; index--) {
1082
+ const key = keys[index];
1083
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
1084
+ if (key === void 0) throw new Error("cannot render a missing JSON object key");
1085
+ const item = current[key];
1086
+ /* v8 ignore next -- canonical JsonValue records contain no undefined properties. */
1087
+ if (item === void 0) throw new Error("cannot render an undefined JSON object property");
1088
+ tasks.push({
1089
+ kind: "value",
1090
+ value: item,
1091
+ depth: childDepth,
1092
+ compact
1093
+ });
1094
+ tasks.push({
1095
+ kind: "text",
1096
+ text: compact ? `${index === 0 ? "" : ","}${JSON.stringify(key)}:` : `${index === 0 ? "\n" : ",\n"}${JSON_INDENT.repeat(childDepth)}${JSON.stringify(key)}: `
1097
+ });
1098
+ }
1099
+ }
1100
+ return chunks.join("");
1101
+ }
1102
+ /** Render one present program completion value for the model-facing result text. */
1103
+ function renderValue(value) {
1104
+ return typeof value === "string" ? value : renderJsonValue(value);
1105
+ }
1106
+ /**
1107
+ * Build the `run_code` {@link ToolDefinition}: required `description` and
1108
+ * `code` parameters, executed through the dispatch bridge described
1109
+ * above. The
1110
+ * registry reserves it as presentation infrastructure under non-native modes,
1111
+ * outside the filterable global/scoped capability layers.
1112
+ * @param registry - the owning registry (sub-calls go through its `execute`,
1113
+ * bindings cover its registered tools).
1114
+ * @param options - the registry-private capabilities described above.
1115
+ * @returns the registry-ready definition.
1116
+ */
1117
+ function createRunCodeTool(registry, options) {
1118
+ const { requireRuntime, peekRuntime, maxParallel, shapeDispatchLog } = options;
1119
+ const definition = defineTool({
1120
+ name: RUN_CODE_NAME,
1121
+ description: TYPESCRIPT_FLAVOR.description,
1122
+ parameters: {
1123
+ description: {
1124
+ type: "string",
1125
+ required: true,
1126
+ description: RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION
1127
+ },
1128
+ code: {
1129
+ type: "string",
1130
+ required: true,
1131
+ description: TYPESCRIPT_FLAVOR.codeDescription
1132
+ },
1133
+ ...RUN_CODE_CONTROLS
1134
+ },
1135
+ output: {
1136
+ schema: {
1137
+ type: "object",
1138
+ additionalProperties: false,
1139
+ properties: {
1140
+ logs: {
1141
+ type: "array",
1142
+ required: true,
1143
+ items: { type: "string" }
1144
+ },
1145
+ result: { type: "json" },
1146
+ sandbox: {
1147
+ type: "object",
1148
+ additionalProperties: false,
1149
+ properties: {
1150
+ mode: {
1151
+ type: "string",
1152
+ required: true,
1153
+ enum: [
1154
+ "read-only",
1155
+ "workspace-write",
1156
+ "danger-full-access"
1157
+ ]
1158
+ },
1159
+ denied: {
1160
+ type: "boolean",
1161
+ required: true
1162
+ },
1163
+ enforcement: {
1164
+ type: "string",
1165
+ enum: ["full", "partial"]
1166
+ }
1167
+ }
1168
+ }
1169
+ }
1170
+ },
1171
+ render: (_args, value) => {
1172
+ const rendered = value.result === void 0 ? "" : renderValue(value.result);
1173
+ const parts = [value.logs.join("\n"), rendered].filter((part) => part.length > 0);
1174
+ if (value.sandbox?.enforcement === "partial") parts.push("File sandbox enforcement is partial on this host.");
1175
+ if (value.sandbox?.denied) parts.push(`The ${value.sandbox.mode} file sandbox denied an operation.${escalationGuidance(peekRuntime())}`);
1176
+ return [{
1177
+ type: "text",
1178
+ text: parts.length > 0 ? parts.join("\n") : "(run_code completed with no output)"
1179
+ }];
1180
+ }
1181
+ },
1182
+ async execute(args, exec) {
1183
+ if (args.description.trim().length === 0) throw new Error("invalid description: expected a non-empty string");
1184
+ const runtime = requireRuntime();
1185
+ validateEscalationArgs(args.sandbox_permissions, args.justification);
1186
+ if (args.timeoutMs !== void 0 && runtime.timeout === void 0) throw new Error("timeoutMs is not available for this PTC runtime");
1187
+ if (args.timeoutMs !== void 0 && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) throw new Error("invalid timeoutMs: expected a positive finite number");
1188
+ const standingPolicy = runtime.sandboxMode === void 0 ? void 0 : options.resolveSandboxPolicy(exec);
1189
+ let policy = standingPolicy;
1190
+ if (args.sandbox_permissions !== void 0 && args.justification !== void 0) {
1191
+ if (standingPolicy === void 0) throw new Error("sandbox_permissions is not available for this PTC runtime");
1192
+ const approvedMode = await approveEscalation({
1193
+ requestedMode: args.sandbox_permissions,
1194
+ justification: args.justification,
1195
+ effectiveMode: standingPolicy.mode,
1196
+ subject: "program"
1197
+ }, {
1198
+ approver: options.peekApprover(),
1199
+ agent: exec.agent,
1200
+ callId: exec.callId,
1201
+ toolName: RUN_CODE_NAME,
1202
+ signal: exec.signal
1203
+ });
1204
+ policy = {
1205
+ ...standingPolicy,
1206
+ mode: approvedMode
1207
+ };
1208
+ }
1209
+ exec.signal.throwIfAborted();
1210
+ const runController = new AbortController();
1211
+ const onOuterAbort = () => {
1212
+ runController.abort(exec.signal.reason);
1213
+ };
1214
+ exec.signal.addEventListener("abort", onOuterAbort, { once: true });
1215
+ let dispatches = 0;
1216
+ const pendingQueue = [];
1217
+ const inFlight = /* @__PURE__ */ new Set();
1218
+ /** Tracked settle-event side work (log-content listener + append), drained at run settlement. */
1219
+ const logWork = /* @__PURE__ */ new Set();
1220
+ const commitQueue = [];
1221
+ let exclusiveActive = false;
1222
+ let driving = false;
1223
+ let driverRun = Promise.resolve();
1224
+ let wake;
1225
+ const wakeup = () => {
1226
+ const release = wake;
1227
+ wake = void 0;
1228
+ release?.();
1229
+ };
1230
+ /**
1231
+ * The single ordered lane. Each pass commits the head-of-line settled
1232
+ * dispatch (ordered post-execute), then starts the next queued entry if
1233
+ * its slot is free (ordered pre-execute), and otherwise sleeps until a
1234
+ * body settles or a new submission arrives. One run reaching the
1235
+ * empty-queues/empty-pool state is quiescence.
1236
+ */
1237
+ const drive = () => {
1238
+ if (driving) return driverRun;
1239
+ driving = true;
1240
+ driverRun = (async () => {
1241
+ try {
1242
+ for (;;) {
1243
+ const signal = new Promise((resolve) => {
1244
+ wake = resolve;
1245
+ });
1246
+ const commitHead = commitQueue[0];
1247
+ if (commitHead !== void 0 && commitHead.settled) {
1248
+ commitQueue.shift();
1249
+ await commitHead.commit();
1250
+ if (commitHead.mode === "exclusive") exclusiveActive = false;
1251
+ continue;
1252
+ }
1253
+ const head = pendingQueue[0];
1254
+ if (head !== void 0) {
1255
+ if (runController.signal.aborted) {
1256
+ pendingQueue.shift();
1257
+ head.abandon();
1258
+ continue;
1259
+ }
1260
+ const mode = head.classify();
1261
+ if (!exclusiveActive && (mode === "exclusive" ? inFlight.size === 0 : inFlight.size < maxParallel)) {
1262
+ if (mode === "exclusive") exclusiveActive = true;
1263
+ head.mode = mode;
1264
+ pendingQueue.shift();
1265
+ commitQueue.push(head);
1266
+ await head.start();
1267
+ const flight = head.flight.finally(() => {
1268
+ inFlight.delete(flight);
1269
+ wakeup();
1270
+ });
1271
+ inFlight.add(flight);
1272
+ continue;
1273
+ }
1274
+ }
1275
+ if (pendingQueue.length === 0 && commitQueue.length === 0 && inFlight.size === 0) return;
1276
+ await signal;
1277
+ }
1278
+ } finally {
1279
+ driving = false;
1280
+ wake = void 0;
1281
+ }
1282
+ })();
1283
+ return driverRun;
1284
+ };
1285
+ /** Every dispatch settled AND committed; nothing can start (the run is aborted at call time). */
1286
+ const drainDispatches = async () => {
1287
+ await drive();
1288
+ while (logWork.size > 0) await Promise.allSettled([...logWork]);
1289
+ };
1290
+ const runOver = () => runController.signal.aborted;
1291
+ const binding = (schema) => async (rawArgs) => {
1292
+ const { name } = schema;
1293
+ if (runOver()) throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} not dispatched`);
1294
+ const normalized = jsonNormalizeArgs(rawArgs);
1295
+ const n = ++dispatches;
1296
+ const subCallId = brandString(`${String(exec.callId)}:ptc:${n}`);
1297
+ const input = {
1298
+ callId: subCallId,
1299
+ rootCallId: exec.rootCallId,
1300
+ name,
1301
+ schema,
1302
+ arguments: normalized.dispatched,
1303
+ ...exec.agent ? { agent: exec.agent } : {},
1304
+ parent: exec.token,
1305
+ signal: runController.signal
1306
+ };
1307
+ const scheduler = registry[TOOL_RUNTIME_SCHEDULER];
1308
+ const outcome = await new Promise((resolve, reject) => {
1309
+ let parked;
1310
+ const settle = (result) => {
1311
+ resolve(result.isError ? {
1312
+ isError: true,
1313
+ message: result.error.message
1314
+ } : {
1315
+ isError: false,
1316
+ value: result.value
1317
+ });
1318
+ const agent = exec.agent;
1319
+ if (agent === void 0) return;
1320
+ const task = (async () => {
1321
+ const logged = await shapeDispatchLog({
1322
+ exec,
1323
+ agent,
1324
+ subCallId,
1325
+ name,
1326
+ isError: result.isError,
1327
+ content: result.content
1328
+ });
1329
+ agent.session.append("tool/ptc-dispatch", {
1330
+ rootCallId: exec.rootCallId,
1331
+ parentCallId: exec.callId,
1332
+ subCallId,
1333
+ name,
1334
+ arguments: normalized.logged,
1335
+ isError: result.isError,
1336
+ ...result.error?.info === void 0 ? {} : { error: result.error.info },
1337
+ content: logged
1338
+ });
1339
+ })().finally(() => {
1340
+ logWork.delete(task);
1341
+ });
1342
+ logWork.add(task);
1343
+ };
1344
+ pendingQueue.push({
1345
+ flight: Promise.resolve(),
1346
+ settled: false,
1347
+ classify: () => registry.executionMode(input).kind,
1348
+ abandon: () => {
1349
+ reject(/* @__PURE__ */ new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`));
1350
+ },
1351
+ async start() {
1352
+ exec.agent?.session.append("tool/ptc-dispatch-start", {
1353
+ rootCallId: exec.rootCallId,
1354
+ parentCallId: exec.callId,
1355
+ subCallId,
1356
+ name,
1357
+ arguments: normalized.logged
1358
+ });
1359
+ const prepared = await scheduler.prepare(input);
1360
+ if (prepared.kind === "dispatch") {
1361
+ this.flight = scheduler.dispatch(prepared.exec).then((dispatchOutcome) => {
1362
+ parked = {
1363
+ kind: dispatchOutcome.kind,
1364
+ exec: prepared.exec,
1365
+ result: dispatchOutcome.result
1366
+ };
1367
+ this.settled = true;
1368
+ });
1369
+ return;
1370
+ }
1371
+ parked = {
1372
+ kind: prepared.kind,
1373
+ exec: prepared.exec,
1374
+ result: prepared.result
1375
+ };
1376
+ this.settled = true;
1377
+ },
1378
+ async commit() {
1379
+ /* v8 ignore next -- commit() runs only after `settled` flipped, which set parked. */
1380
+ if (parked === void 0) return;
1381
+ const result = parked.kind === "post-result" ? await scheduler.finalize(parked.exec, parked.result) : scheduler.finish(parked.exec, parked.result);
1382
+ if (!result.isError && result.content.some((block) => block.type === "image")) exec.deferContext(createUserMessage({
1383
+ content: result.content,
1384
+ source: { kind: "ptc-mode" }
1385
+ }));
1386
+ for (const context of result.additionalContexts ?? []) exec.deferContext(context);
1387
+ if (result.concludesTurn) exec.concludeTurn();
1388
+ settle(result);
1389
+ while (logWork.size > maxParallel) await Promise.race(logWork);
1390
+ }
1391
+ });
1392
+ wakeup();
1393
+ drive();
1394
+ });
1395
+ if (runOver()) throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} result discarded`);
1396
+ if (outcome.isError) throw new Error(outcome.message);
1397
+ return outcome.value;
1398
+ };
1399
+ const functions = Object.create(null);
1400
+ for (const schema of registry.schemas(exec.agent)) {
1401
+ if (schema.name === "run_code") continue;
1402
+ Object.defineProperty(functions, schema.name, {
1403
+ enumerable: true,
1404
+ value: binding(deepFreeze(schema))
1405
+ });
1406
+ }
1407
+ try {
1408
+ let result;
1409
+ try {
1410
+ result = await runtime.run(runtime.resolve({
1411
+ program: args.code,
1412
+ bindings: [{
1413
+ global: "tools",
1414
+ functions,
1415
+ errorClass: {
1416
+ name: "ToolCallError",
1417
+ memberNameProperty: "toolName"
1418
+ }
1419
+ }],
1420
+ signal: runController.signal,
1421
+ ...exec.agent?.session.header.cwd !== void 0 ? { cwd: exec.agent.session.header.cwd } : {},
1422
+ ...policy !== void 0 ? { sandboxPolicy: policy } : {},
1423
+ ...args.timeoutMs !== void 0 ? { timeoutMs: args.timeoutMs } : {}
1424
+ }));
1425
+ } finally {
1426
+ runController.abort("run_code settled");
1427
+ await drainDispatches();
1428
+ }
1429
+ if (result.error) {
1430
+ const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.join("\n")}` : "";
1431
+ const sandboxText = result.sandbox === void 0 ? "" : `\nFile sandbox: ${result.sandbox.mode}${result.sandbox.enforcement === void 0 ? "" : `; enforcement: ${result.sandbox.enforcement}`}${result.sandbox.denied ? "; operation denied" : ""}.`;
1432
+ throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}${sandboxText}${result.sandbox?.denied ? escalationGuidance(runtime) : ""}`);
1433
+ }
1434
+ return {
1435
+ logs: result.logs,
1436
+ ...result.sandbox === void 0 ? {} : { sandbox: result.sandbox },
1437
+ ...result.value !== void 0 ? { result: result.value } : {}
1438
+ };
1439
+ } finally {
1440
+ exec.signal.removeEventListener("abort", onOuterAbort);
1441
+ }
1442
+ },
1443
+ presentCall: (args) => ({
1444
+ card: "generic",
1445
+ title: args.description,
1446
+ kind: "execute",
1447
+ rawInput: args.code
1448
+ })
1449
+ });
1450
+ Object.defineProperty(definition, "description", {
1451
+ enumerable: true,
1452
+ get: () => {
1453
+ const runtime = peekRuntime();
1454
+ const instructions = runtime?.executionInstructions;
1455
+ return resolveFlavor(peekRuntime).description + (instructions ? ` ${instructions}` : "") + (runtime === void 0 ? "" : " The working directory is the Session's current directory.") + escalationGuidance(runtime);
1456
+ }
1457
+ });
1458
+ Object.defineProperty(definition, "parameters", {
1459
+ enumerable: true,
1460
+ get: () => parameterSchemaSpecToJsonSchema({
1461
+ description: {
1462
+ type: "string",
1463
+ required: true,
1464
+ description: RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION
1465
+ },
1466
+ code: {
1467
+ type: "string",
1468
+ required: true,
1469
+ description: resolveFlavor(peekRuntime).codeDescription
1470
+ },
1471
+ ...controlParameters(peekRuntime())
1472
+ })
1473
+ });
1474
+ return definition;
1475
+ }
1476
+ //#endregion
1477
+ //#region lib/types/ts-types.js
1478
+ /**
1479
+ * PTC mode codegen: the pure projection from registered tool schemas to the TypeScript SDK
1480
+ * text the model programs against (the `tools:sdk` prompt section). Sibling of
1481
+ * `json-schema.ts` — `schemas()` (native function calling) and this module (the generated
1482
+ * `declare const tools` API) are two projections of the same store.
1483
+ * @module @qilin-agent/tools/src/ts-types
1484
+ */
1485
+ /** Property names that are valid bare TS identifiers; anything else is quoted. */
1486
+ const IDENTIFIER$1 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1487
+ /** Render an object key: bare when it is a valid identifier, quoted otherwise (every name stays reachable, no aliasing). */
1488
+ function renderKey(name) {
1489
+ return IDENTIFIER$1.test(name) ? name : JSON.stringify(name);
1490
+ }
1491
+ /** One `indent`-deep line prefix (two spaces per level). */
1492
+ function pad$1(indent) {
1493
+ return " ".repeat(indent);
1494
+ }
1495
+ /** A one-line JSDoc block for a schema `description`, or no lines when there is none. */
1496
+ function docLines$1(description, indent) {
1497
+ if (typeof description !== "string" || description.length === 0) return [];
1498
+ const collapsed = description.replace(/\s+/g, " ").trim();
1499
+ return [`${pad$1(indent)}/** ${collapsed.replaceAll("*/", String.raw`*\/`)} */`];
1500
+ }
1501
+ /** Render one scalar already validated by the unified schema boundary. */
1502
+ function renderScalar(value) {
1503
+ return JSON.stringify(value);
1504
+ }
1505
+ /** Render a validated scalar `const`/`enum`, falling back to the broad type. */
1506
+ function renderConstrainedScalar$1(node, type) {
1507
+ const broad = type === "integer" ? "number" : type;
1508
+ if (Object.hasOwn(node, "const")) return renderScalar(node.const);
1509
+ if (Object.hasOwn(node, "enum")) return node.enum.map(renderScalar).join(" | ");
1510
+ return broad;
1511
+ }
1512
+ /** Build one document from captured parts while retaining the legacy array-parenthesization test. */
1513
+ function typeDocumentFrom(parts) {
1514
+ return {
1515
+ parts,
1516
+ containsUnionOrIntersection: parts.some((part) => typeof part === "string" ? part.includes("|") || part.includes("&") : part.containsUnionOrIntersection)
1517
+ };
1518
+ }
1519
+ /** Build a small document without an intermediate array at each call site. */
1520
+ function typeDocument(...parts) {
1521
+ return typeDocumentFrom(parts);
1522
+ }
1523
+ /** Flatten a nested document with an explicit work stack. */
1524
+ function flattenTypeDocument(document) {
1525
+ const chunks = [];
1526
+ const tasks = [document];
1527
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
1528
+ if (typeof task === "string") {
1529
+ chunks.push(task);
1530
+ continue;
1531
+ }
1532
+ for (let index = task.parts.length - 1; index >= 0; index--) {
1533
+ const part = task.parts[index];
1534
+ /* v8 ignore next -- the loop is bounded by the captured part count. */
1535
+ if (part !== void 0) tasks.push(part);
1536
+ }
1537
+ }
1538
+ return chunks.join("");
1539
+ }
1540
+ /** Initialize one schema-render frame with empty aggregation state. */
1541
+ function schemaRenderFrame(node, indent) {
1542
+ return {
1543
+ node,
1544
+ indent,
1545
+ phase: "start",
1546
+ children: [],
1547
+ childIndex: 0,
1548
+ childDocuments: [],
1549
+ entries: []
1550
+ };
1551
+ }
1552
+ /** Render an already asserted schema to a composable document. */
1553
+ function renderSupportedSchema(schema, indent) {
1554
+ const frames = [schemaRenderFrame(schema, indent)];
1555
+ let rootDocument;
1556
+ const finish = (document) => {
1557
+ frames.pop();
1558
+ const parent = frames.at(-1);
1559
+ if (parent === void 0) rootDocument = document;
1560
+ else parent.childDocuments.push(document);
1561
+ };
1562
+ while (frames.length > 0) {
1563
+ const frame = frames.at(-1);
1564
+ /* v8 ignore next -- the loop condition guarantees a current frame. */
1565
+ if (frame === void 0) break;
1566
+ if (frame.phase === "children") {
1567
+ if (frame.childIndex < frame.children.length) {
1568
+ const child = frame.children[frame.childIndex];
1569
+ /* v8 ignore next -- childIndex is bounded by children.length. */
1570
+ if (child === void 0) throw new Error("missing schema render child");
1571
+ frame.childIndex++;
1572
+ frames.push(schemaRenderFrame(child.node, child.indent));
1573
+ continue;
1574
+ }
1575
+ if (frame.kind === "oneOf") {
1576
+ const parts = [];
1577
+ for (let index = 0; index < frame.childDocuments.length; index++) {
1578
+ if (index > 0) parts.push(" | ");
1579
+ const child = frame.childDocuments[index];
1580
+ /* v8 ignore next -- child documents correspond one-to-one with children. */
1581
+ if (child !== void 0) parts.push(child);
1582
+ }
1583
+ finish(typeDocumentFrom(parts));
1584
+ continue;
1585
+ }
1586
+ if (frame.kind === "array") {
1587
+ const child = frame.childDocuments[0];
1588
+ /* v8 ignore next -- array frames always schedule exactly one child. */
1589
+ if (child === void 0) throw new Error("missing array item type");
1590
+ finish(child.containsUnionOrIntersection ? typeDocument("(", child, ")[]") : typeDocument(child, "[]"));
1591
+ continue;
1592
+ }
1593
+ const required = new Set(frame.node.required);
1594
+ const parts = ["{"];
1595
+ for (let index = 0; index < frame.entries.length; index++) {
1596
+ const entry = frame.entries[index];
1597
+ const child = frame.childDocuments[index];
1598
+ /* v8 ignore next -- object entries and child documents have the same length. */
1599
+ if (entry === void 0 || child === void 0) throw new Error("missing object property type");
1600
+ const [name, prop] = entry;
1601
+ for (const line of docLines$1(prop.description, frame.indent + 1)) parts.push("\n", line);
1602
+ parts.push("\n", `${pad$1(frame.indent + 1)}${renderKey(name)}${required.has(name) ? "" : "?"}: `, child, ";");
1603
+ }
1604
+ parts.push("\n", `${pad$1(frame.indent)}}`);
1605
+ const declared = typeDocumentFrom(parts);
1606
+ finish(frame.node.additionalProperties === false ? declared : typeDocument(declared, " & Record<string, JsonValue>"));
1607
+ continue;
1608
+ }
1609
+ const node = frame.node;
1610
+ if (node.oneOf !== void 0) {
1611
+ frame.kind = "oneOf";
1612
+ frame.children = Array.from(node.oneOf, (child) => ({
1613
+ node: child,
1614
+ indent: frame.indent
1615
+ }));
1616
+ frame.childIndex = 0;
1617
+ frame.childDocuments = [];
1618
+ frame.phase = "children";
1619
+ continue;
1620
+ }
1621
+ if (node.type === void 0) {
1622
+ finish(typeDocument("JsonValue"));
1623
+ continue;
1624
+ }
1625
+ switch (node.type) {
1626
+ case "string":
1627
+ case "number":
1628
+ case "integer":
1629
+ case "boolean":
1630
+ case "null":
1631
+ finish(typeDocument(renderConstrainedScalar$1(node, node.type)));
1632
+ break;
1633
+ case "array":
1634
+ if (node.items === void 0) finish(typeDocument("JsonValue[]"));
1635
+ else {
1636
+ frame.kind = "array";
1637
+ frame.children = [{
1638
+ node: node.items,
1639
+ indent: frame.indent
1640
+ }];
1641
+ frame.childIndex = 0;
1642
+ frame.childDocuments = [];
1643
+ frame.phase = "children";
1644
+ }
1645
+ break;
1646
+ case "object": {
1647
+ const open = node.additionalProperties !== false;
1648
+ const entries = Object.entries(node.properties ?? {});
1649
+ if (entries.length === 0) finish(typeDocument(open ? "Record<string, JsonValue>" : "Record<string, never>"));
1650
+ else {
1651
+ frame.kind = "object";
1652
+ frame.entries = entries;
1653
+ frame.children = entries.map(([, child]) => ({
1654
+ node: child,
1655
+ indent: frame.indent + 1
1656
+ }));
1657
+ frame.childIndex = 0;
1658
+ frame.childDocuments = [];
1659
+ frame.phase = "children";
1660
+ }
1661
+ break;
1662
+ }
1663
+ /* v8 ignore next -- assertSupportedJsonSchema narrowed this closed type union. */
1664
+ default: finish(typeDocument("unknown"));
1665
+ }
1666
+ }
1667
+ /* v8 ignore next -- every root frame produces one document. */
1668
+ return rootDocument ?? typeDocument("unknown");
1669
+ }
1670
+ /**
1671
+ * Map one enforced JSON-Schema node to a TypeScript type literal. Supports
1672
+ * every unified schema construct and returns `unknown` for malformed or
1673
+ * unsupported inputs without throwing.
1674
+ * @param schema - the JSON-Schema node (any shape; hostile inputs degrade).
1675
+ * @param indent - the indentation level for nested object members.
1676
+ * @returns the TS type text (multi-line for objects with properties).
1677
+ */
1678
+ function jsonSchemaToTs(schema, indent = 0) {
1679
+ try {
1680
+ assertSupportedJsonSchema(schema);
1681
+ return flattenTypeDocument(renderSupportedSchema(schema, indent));
1682
+ } catch {
1683
+ return "unknown";
1684
+ }
1685
+ }
1686
+ /** The fixed model-facing usage contract rendered above the declarations (see the PTC mode Agent Note's "What the model sees"). */
1687
+ const SDK_INSTRUCTIONS$1 = `## Writing code for run_code
1688
+
1689
+ \`run_code\` takes two required arguments: \`description\`, a short summary of what the program does, and \`code\` — the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped). The declarations below are SDK bindings for this program. A declaration does not make its name a directly callable tool; only names supplied as separate tool schemas may be called directly.`;
1690
+ const SDK_PROGRAM_INSTRUCTIONS = `Inside the program:
1691
+
1692
+ - 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.
1693
+ - 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.
1694
+ - 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\`.
1695
+ - 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.
1696
+
1697
+ Program-only SDK bindings:`;
1698
+ /** Whether one string schema accepts the literal used by the bash example. */
1699
+ function acceptsExampleString(schema, value) {
1700
+ return schema?.type === "string" && (schema.const === void 0 || schema.const === value) && (schema.enum === void 0 || schema.enum.includes(value));
1701
+ }
1702
+ /** Render the bash example only when its literal arguments satisfy the current parameter schema. */
1703
+ function renderBashExample(schemas) {
1704
+ const bash = schemas.find((schema) => schema.name === "bash");
1705
+ if (bash === void 0) return "";
1706
+ const parameters = bash.parameters;
1707
+ if (parameters.type !== "object") return "";
1708
+ const required = parameters.required ?? [];
1709
+ if (required.some((name) => name !== "command" && name !== "description")) return "";
1710
+ if (!acceptsExampleString(parameters.properties?.command, "pwd")) return "";
1711
+ const needsDescription = required.includes("description");
1712
+ if (needsDescription && !acceptsExampleString(parameters.properties?.description, "Show current directory")) return "";
1713
+ return ` When no separate \`bash\` schema is supplied, invoke a declared \`bash\` binding inside \`run_code\`:\n\n\`run_code({ description: "Show current directory", code: "return await tools.bash({ ${needsDescription ? "description: 'Show current directory', " : ""}command: 'pwd' })" })\``;
1714
+ }
1715
+ /**
1716
+ * Render the full `tools:sdk` prompt section: the fixed usage instructions
1717
+ * plus one `declare const tools` interface covering every given tool.
1718
+ * Deterministic — tools are emitted in lexicographic name order, so an
1719
+ * unchanged tool set produces byte-identical text across assemblies. The sort
1720
+ * is not a total order on byte-equal names, so two schemas sharing a name
1721
+ * would render in argument order; the caller's visible-capability map is keyed
1722
+ * by name, so the input never carries a duplicate.
1723
+ * @param schemas - the tool schemas to declare (the caller excludes
1724
+ * `run_code` itself).
1725
+ * @returns the complete section text.
1726
+ */
1727
+ function renderToolsSdk(schemas) {
1728
+ const sorted = [...schemas].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1729
+ const argsMembers = [];
1730
+ const outputMembers = [];
1731
+ for (const schema of sorted) {
1732
+ argsMembers.push(...docLines$1(schema.description, 1));
1733
+ argsMembers.push(`${pad$1(1)}${renderKey(schema.name)}: ${jsonSchemaToTs(schema.parameters, 1)};`);
1734
+ outputMembers.push(`${pad$1(1)}${renderKey(schema.name)}: ${jsonSchemaToTs(schema.output, 1)};`);
1735
+ }
1736
+ const declaration = [
1737
+ `interface ToolArgsMap {${argsMembers.length > 0 ? `\n${argsMembers.join("\n")}\n` : ""}}`,
1738
+ `interface ToolOutputMap {${outputMembers.length > 0 ? `\n${outputMembers.join("\n")}\n` : ""}}`,
1739
+ "type ToolName = keyof ToolOutputMap",
1740
+ [
1741
+ "declare class ToolCallError extends Error {",
1742
+ " readonly name: \"ToolCallError\";",
1743
+ " readonly toolName: ToolName;",
1744
+ "}"
1745
+ ].join("\n"),
1746
+ [
1747
+ "declare const tools: {",
1748
+ " [K in ToolName]: (args: ToolArgsMap[K]) => Promise<ToolOutputMap[K]>;",
1749
+ "}"
1750
+ ].join("\n")
1751
+ ].join("\n\n");
1752
+ return `${SDK_INSTRUCTIONS$1}${renderBashExample(sorted)}\n\n${SDK_PROGRAM_INSTRUCTIONS}\n\n\`\`\`ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\n${declaration}\n\`\`\``;
1753
+ }
1754
+ //#endregion
1755
+ //#region lib/types/py-types.js
1756
+ /**
1757
+ * PTC mode codegen — Python flavor. The pure projection from registered tool schemas to the
1758
+ * Python SDK text the model programs against under `runtime.language === 'python'`. Sibling of
1759
+ * {@link ./ts-types.ts | ts-types.ts}; the two files are two projections of the same registry
1760
+ * store, keyed by the loaded {@link @qilin-agent/ptc-runtime#PtcRuntime.language | PTC
1761
+ * runtime's language}.
1762
+ *
1763
+ * Under `mode: 'ptc'` the native tool schemas are omitted from the request, so this generated
1764
+ * SDK is the model's ONLY source for each tool's argument names, required fields, types,
1765
+ * descriptions, and canonical output shapes; under `mode: 'both'` the native schemas ship
1766
+ * alongside it and it is one of two. Object-shaped arguments and outputs therefore render as one
1767
+ * named `TypedDict` per tool (and per nested object), not an opaque `dict[str, Any]`, so the
1768
+ * shape survives into the program under the mode that has nothing else to carry it.
1769
+ * @module @qilin-agent/tools/src/py-types
1770
+ */
1771
+ /**
1772
+ * The reference grammar's `xid_start xid_continue*` — the set
1773
+ * `str.isidentifier()` accepts on a CPython whose Unicode tables match the
1774
+ * engine's. See {@link isBareIdentifier} for what a version skew does.
1775
+ */
1776
+ const IDENTIFIER = /^[\p{XID_Start}_]\p{XID_Continue}*$/u;
1777
+ /**
1778
+ * Whether a name can be emitted as a bare Python identifier rather than
1779
+ * routed to the subscript/`dict[str, Any]` path.
1780
+ *
1781
+ * Python identifiers are not ASCII: `路径` is as legal a field name as `path`,
1782
+ * and rejecting it would degrade the whole enclosing object, dropping every
1783
+ * field's name, requiredness, and type — information whose only source under
1784
+ * `mode: 'ptc'` is this generated text.
1785
+ *
1786
+ * NFKC stability is a second and separate condition, because CPython
1787
+ * normalizes identifiers at compile time while JSON keys are compared as
1788
+ * written: `field` would be declared and reachable as `field`, so the SDK would
1789
+ * advertise a key under a spelling the harness never accepts, and two keys
1790
+ * that normalize together would collapse into one declaration. Those names
1791
+ * take the subscript path, which carries their exact bytes.
1792
+ *
1793
+ * `IDENTIFIER` matches `str.isidentifier()` (measured on Node 22.23.1 vs
1794
+ * CPython 3.9.6 tables): the equivalence holds inside the two versions' shared
1795
+ * tables, and the skew characters below are exactly where that pair diverges.
1796
+ * The predicate as a whole is deliberately stricter than `isidentifier()`,
1797
+ * which does not test NFKC stability: `'field'.isidentifier()` is True and
1798
+ * this returns false.
1799
+ *
1800
+ * Both conditions are evaluated against the ENGINE's Unicode tables, and the
1801
+ * two sides are versioned independently — `\p{XID_Start}`/`\p{XID_Continue}`
1802
+ * follow the running engine (Node 22.23.1 reports Unicode 17.0) while CPython
1803
+ * follows its own (3.9.6 reports 13.0.0). The skew is not symmetric. A CPython
1804
+ * older than the engine is the dangerous direction: a character added to either
1805
+ * property since its tables (U+10570 Vithkuqi and U+1E290 Toto, 14.0; U+1E4D0
1806
+ * Nag Mundari, 15.0; U+1C89 Cyrillic TJE, 16.0 — ages per `DerivedAge.txt`; all
1807
+ * four are NFKC-stable and accepted here, and all four are `Cn` on that 3.9.6,
1808
+ * which rejects them) is emitted bare and its tokenizer refuses the character,
1809
+ * taking the whole SDK block down — the same parseability invariant
1810
+ * {@link UNPRINTABLE}, {@link LONE_SURROGATE} and {@link MAX_LIST_NESTING}
1811
+ * exist for. Both properties carry it: a character added only to `XID_Continue`
1812
+ * passes the trailing `\p{XID_Continue}*` in a tail position and fails the same
1813
+ * way — U+200C ZWNJ and U+200D ZWJ are that case, gaining `XID_Continue` in UCD
1814
+ * 15.1 and absent from it in 13.0.0, 14.0.0 and 15.0.0, so `a\u{200C}b` is
1815
+ * emitted bare here while `isidentifier()` is False on 3.9.6 and on 3.12.13
1816
+ * (15.0.0). A CPython newer than the engine only routes a legal name to the
1817
+ * subscript/`dict[str, Any]` path: less readable, still correct. The NFKC
1818
+ * condition reduces to the same skew, since normalization stability guarantees
1819
+ * an assigned character's normalization never changes afterwards.
1820
+ *
1821
+ * This predicate is not the only reader of engine tables. {@link camelCase}
1822
+ * reads them at three further points — its split set, its head test, and its
1823
+ * `toUpperCase()` case mapping — and this predicate's verdict gates none of
1824
+ * them: a class name derived there reaches emitted text whenever any object
1825
+ * shape in the tool's schema declares a `TypedDict`, including for a tool this
1826
+ * predicate rejected. A tool named `zz-\u{1E4D0}x` with such parameters never
1827
+ * reaches the skew here (the `-` rejects it outright) yet emits `class
1828
+ * Zz\u{1E4D0}xArgs`, which that same 3.9.6 refuses — Nag Mundari arrived two
1829
+ * releases after its tables. The case mapping is a separate table rather than
1830
+ * an XID membership test, and it fails on names both conditions above accept:
1831
+ * `\u{019B}` is XID_Start and NFKC-stable, so this predicate accepts it and
1832
+ * `async def \u{019B}` compiles on 3.9.6, but Node uppercases it to
1833
+ * `\u{A7DC}` — unassigned in that CPython, whose own `.upper()` is the identity
1834
+ * here — and the declared `class \u{A7DC}Args` fails with `invalid
1835
+ * non-printable character U+A7DC`. Closing the exposure therefore covers all
1836
+ * four read points, not this predicate alone; it needs the target interpreter's
1837
+ * version, which the backend reporting `language: 'python'` owns; the
1838
+ * language-dispatch Agent Note records the deferral.
1839
+ *
1840
+ * The `ts-types` sibling keeps its own ASCII rule rather than sharing this
1841
+ * one: ECMAScript identifiers are a different set (`$`) and are never
1842
+ * normalized, so one predicate cannot be correct for both. ZWJ/ZWNJ are not
1843
+ * part of that difference — both sets carry them on the engine's tables; what
1844
+ * separates the two there is the CPython table version above.
1845
+ * @param name - the raw schema field or tool name.
1846
+ * @returns whether the name can be emitted bare.
1847
+ */
1848
+ function isBareIdentifier(name) {
1849
+ return IDENTIFIER.test(name) && name.normalize("NFKC") === name;
1850
+ }
1851
+ /**
1852
+ * Python hard keywords: reserved everywhere, so a tool or field named
1853
+ * ``class`` or ``lambda`` is legal on the wire but not as an attribute
1854
+ * (``tools.class`` would be a SyntaxError in the model program) and not as a
1855
+ * class-syntax `TypedDict` field. Such a tool renders under subscript access
1856
+ * and such an object degrades to ``dict[str, Any]`` — the model still reaches
1857
+ * every tool and field without collisions.
1858
+ * Soft keywords (``match``, ``case``, ``type``, ``_`` — the language
1859
+ * reference's whole set) are deliberately ABSENT: each is special in exactly
1860
+ * one syntactic position — a statement head (``match``, ``type``), a ``match``
1861
+ * statement's clause head (``case``), or a pattern (``_``) — so ``match: str``
1862
+ * as a field and ``async def match(...)`` as a method are both legal, and
1863
+ * including them would needlessly degrade common search/regex tool fields to
1864
+ * ``dict[str, Any]``. Underscore-leading names are handled separately, not
1865
+ * here: a non-dunder ``__token`` name-mangles, a dunder present on
1866
+ * ``object``/``type`` resolves before the proxy hook, and implicit
1867
+ * special-method lookup bypasses the hook.
1868
+ */
1869
+ const RESERVED = new Set([
1870
+ "False",
1871
+ "None",
1872
+ "True",
1873
+ "and",
1874
+ "as",
1875
+ "assert",
1876
+ "async",
1877
+ "await",
1878
+ "break",
1879
+ "class",
1880
+ "continue",
1881
+ "def",
1882
+ "del",
1883
+ "elif",
1884
+ "else",
1885
+ "except",
1886
+ "finally",
1887
+ "for",
1888
+ "from",
1889
+ "global",
1890
+ "if",
1891
+ "import",
1892
+ "in",
1893
+ "is",
1894
+ "lambda",
1895
+ "nonlocal",
1896
+ "not",
1897
+ "or",
1898
+ "pass",
1899
+ "raise",
1900
+ "return",
1901
+ "try",
1902
+ "while",
1903
+ "with",
1904
+ "yield",
1905
+ "__debug__"
1906
+ ]);
1907
+ /** `typing` symbols this module may emit, in the deterministic import order. */
1908
+ const TYPING_ORDER = [
1909
+ "Any",
1910
+ "Literal",
1911
+ "NotRequired",
1912
+ "Protocol",
1913
+ "TypedDict"
1914
+ ];
1915
+ /** `indent`-deep line prefix (four spaces per level to match PEP 8 output). */
1916
+ function pad(indent) {
1917
+ return " ".repeat(indent);
1918
+ }
1919
+ /**
1920
+ * The `Cc` code points that survive the whitespace collapse in {@link describe}
1921
+ * and have no printable form: the C0 controls, DEL, and the C1 controls. Only
1922
+ * U+0009 to U+000D are absent, because ECMAScript `\s` already collapsed them —
1923
+ * `\s` is TAB/VT/FF/SP/NBSP/ZWNBSP/Zs plus LF/CR/LS/PS, so no C1 code point is
1924
+ * in it and the whole U+0080 to U+009F block reaches this rule intact. Those
1925
+ * are not hypothetical input: they are what Windows-1252 bytes 0x80 to 0x9F
1926
+ * (smart quotes, em dash) become when decoded as Latin-1.
1927
+ * CPython rejects source containing a NUL outright
1928
+ * (`SyntaxError: source code string cannot contain null bytes`), whether it
1929
+ * sits in a docstring or in a comment, so one such byte anywhere in a schema
1930
+ * description would make the whole generated SDK unparseable — under
1931
+ * `mode: 'ptc'`, the model's only declaration of the tools. The rest are
1932
+ * legal but invisible; escaping them with the same rule keeps the emitted text
1933
+ * readable and the treatment uniform.
1934
+ *
1935
+ * The boundary is the category, not per-code-point addressability: `\xNN`
1936
+ * addresses U+0000 to U+00FF, so one escape form covers `Cc` exactly. The
1937
+ * invisible `Cf` formatting characters pass through by design — of them only
1938
+ * U+00AD soft hyphen would fit `\xNN` at all, and escaping that one while
1939
+ * U+200B ZWSP, U+200E/U+200F bidi marks, and U+2060 word joiner passed through
1940
+ * would leave a rule that is neither category- nor addressability-shaped. The
1941
+ * whole family is legal in both consumers, since only LF and CR terminate a
1942
+ * Python string literal or a `#` comment. That set is the tokenizer's, not
1943
+ * `str.splitlines()`': NEL (U+0085), LS (U+2028), and PS (U+2029) split a
1944
+ * string at run time but do not end a physical line in source — measured on
1945
+ * CPython 3.9.6 and 3.12.13, each accepted in both positions with the value
1946
+ * round-tripping — so they are safe raw wherever they reach emitted text
1947
+ * unescaped, which for all three is `JSON.stringify`, at two call sites:
1948
+ * {@link pyScalar}'s literal path, and the subscript tool-name comment's own
1949
+ * call, which a name carrying any of them always reaches, none being
1950
+ * `XID_Continue`. The `description` path escapes NEL under the class above and
1951
+ * folds LS and PS in {@link describe}'s `\s+` collapse, both being `\s`.
1952
+ */
1953
+ const UNPRINTABLE = /[\u0000-\u0008\u000e-\u001f\u007f-\u009f]/g;
1954
+ /**
1955
+ * Unpaired surrogate code points, escaped by {@link describe} as `\uNNNN` —
1956
+ * its own form, since `\xNN` stops at U+00FF. The `u` flag is what makes this
1957
+ * the LONE ones: in Unicode mode a well-formed pair is a single astral code
1958
+ * point outside D800 to DFFF, so an emoji in a description survives untouched.
1959
+ *
1960
+ * This is the NUL case from {@link UNPRINTABLE}, not the invisible-character
1961
+ * case. Python source must be UTF-8-encodable and a lone surrogate is not, so
1962
+ * `compile()` raises `UnicodeEncodeError: surrogates not allowed` for one
1963
+ * anywhere in the text — measured on 3.9 for a string literal and for a `#`
1964
+ * comment alike. A raw or MCP tool description reaches this: `JSON.parse` on a
1965
+ * wire `"\ud800"` escape yields exactly such a code point.
1966
+ */
1967
+ const LONE_SURROGATE = /[\ud800-\udfff]/gu;
1968
+ /**
1969
+ * The collapsed one-line `description` of a schema node (byte-stable across
1970
+ * formatting churn), or `undefined` when the node carries none. Every caller
1971
+ * passes an object — a validated property node, the `ToolSdkSchema` itself, or
1972
+ * the `{ description }` wrapper {@link docLines} synthesizes — so only the
1973
+ * description field needs guarding. A description that collapses
1974
+ * to nothing (empty, or whitespace only) is `undefined` too: it documents the
1975
+ * node no better than an absent one, and emitting it would leave an empty
1976
+ * `"""` docstring or a bare `# ` line in the SDK. Only ECMAScript whitespace
1977
+ * folds, so a description of whitespace plus one surviving control character is
1978
+ * NOT absent: it collapses to that character's visible escape.
1979
+ *
1980
+ * Control characters left over after the whitespace collapse are rendered as
1981
+ * their `\xNN` escapes (see {@link UNPRINTABLE}) and unpaired surrogates as
1982
+ * their `\uNNNN` escapes (see {@link LONE_SURROGATE}); the escape's own backslash is
1983
+ * emitted literally by both consumers, since {@link docLines} doubles it into a
1984
+ * Python source escape and a `#` comment carries it verbatim.
1985
+ */
1986
+ function describe(schema) {
1987
+ const description = schema.description;
1988
+ if (typeof description !== "string") return void 0;
1989
+ 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();
1990
+ return collapsed.length === 0 ? void 0 : collapsed;
1991
+ }
1992
+ /**
1993
+ * One-line docstring for a tool `description`, or no lines when there is none.
1994
+ * Backslashes are doubled first, every quote is escaped, and a trailing
1995
+ * backslash cannot survive: a description ending in `"` or an odd backslash
1996
+ * would otherwise merge with (or escape) the closing triple quote and make
1997
+ * the generated block — PTC mode's only SDK — syntactically invalid Python.
1998
+ */
1999
+ function docLines(description, indent) {
2000
+ const collapsed = describe({ description });
2001
+ if (collapsed === void 0) return [];
2002
+ const escaped = collapsed.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
2003
+ return [`${pad(indent)}"""${escaped}"""`];
2004
+ }
2005
+ /**
2006
+ * CamelCase a name into a Python type identifier: non-identifier characters
2007
+ * split words, `_` splits too (it is `XID_Continue`, so the split set names it
2008
+ * explicitly), and a head that cannot start an identifier takes a `Tool`
2009
+ * prefix. Unicode survives, so a `路径` field yields `路径`-based class names
2010
+ * instead of collapsing to the bare prefix. A character that is not
2011
+ * `XID_Continue` splits even when it is a letter, so a name whose NFKC folding
2012
+ * would leave the identifier set is not carried through — the split set is the
2013
+ * grammar's, not an ASCII approximation of it.
2014
+ *
2015
+ * The result is NFKC-normalized: these names are generated, never matched
2016
+ * against a JSON key, so normalizing is free here and keeps what CPython
2017
+ * compiles identical to what is emitted — unlike {@link isBareIdentifier},
2018
+ * which must reject unstable names outright. Normalizing AFTER the prefix
2019
+ * decision is what makes that hold at the seam the prefix creates: `Tool` +
2020
+ * a combining-mark head composes there (`U+0301` gives `Tooĺ`, U+013A), so
2021
+ * normalizing only the un-prefixed part would emit a name CPython compiles to
2022
+ * a different symbol. The second call is idempotent on the un-prefixed arm.
2023
+ *
2024
+ * The split set, the head test, and `toUpperCase()` all read the engine's
2025
+ * Unicode tables, so this function carries the same version skew
2026
+ * {@link isBareIdentifier} documents, by paths independent of it: a class name
2027
+ * derived here reaches emitted text whenever any object shape in the tool's
2028
+ * schema declares a `TypedDict`, and the predicate's verdict on the tool name
2029
+ * does not gate that. The case mapping is the one that can fail on a name the
2030
+ * predicate accepted; the worked example is there.
2031
+ * @param raw - the schema field or tool name to derive from.
2032
+ * @returns a class-name segment safe to emit.
2033
+ */
2034
+ function camelCase(raw) {
2035
+ 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");
2036
+ return (/^\p{XID_Start}/u.test(joined) ? joined : `Tool${joined}`).normalize("NFKC");
2037
+ }
2038
+ /** Class-name base cap keeping each emitted name — and total text — linear in schema depth. */
2039
+ const MAX_CLASS_NAME_BASE = 120;
2040
+ /**
2041
+ * Deepest `list[…]` nesting emitted into one annotation before the item type
2042
+ * degrades to `Any`. CPython's tokenizer rejects a logical line holding more
2043
+ * than 200 simultaneously-open brackets (`MAXLEVEL`, `SyntaxError: too many
2044
+ * nested parentheses`), so an array chain deeper than that would render an SDK
2045
+ * block that is not valid Python at all — the same failure the docstring
2046
+ * escaping in {@link docLines} exists to prevent. 180 leaves headroom for the
2047
+ * few brackets an annotation can add around the chain, all of which count
2048
+ * toward the same limit. Per emission site, counting brackets open at the
2049
+ * chain's innermost point:
2050
+ *
2051
+ * - Return annotation, `async def f(self, args: X) -> chain:` — 180 `list[`
2052
+ * plus an innermost `Literal[`. The parameter list's `(` closed at the `)`
2053
+ * before the `->`, so it is NOT open here: 181.
2054
+ * - TypedDict field, `field: NotRequired[chain]` — a class-body line with no
2055
+ * other open bracket, and its children start at `listDepth: 1` to reserve
2056
+ * the `NotRequired[`, so 179 `list[` plus `Literal[`: 181. Required fields
2057
+ * share that start for uniformity, spending one level of representable depth
2058
+ * on a bracket they never emit.
2059
+ * - Argument annotation, `async def f(self, args: chain) -> Y:` — the `(` IS
2060
+ * still open around it: 180 `list[` plus `Literal[` plus the paren, 182, the
2061
+ * worst case. Reachable only through a raw `register()` whose `parameters`
2062
+ * is an array reached from the root through `oneOf` arms alone — the root
2063
+ * array itself, or one nested under any depth of unions, since an arm
2064
+ * inherits the enclosing depth unchanged (`A | B` opens no bracket). An
2065
+ * object ancestor takes it out of this case: its fields restart the chain at
2066
+ * the 181 site. `defineTool` compiles an object root, so the annotation is a
2067
+ * bare TypedDict class name or a one-bracket `dict[str, Any]` when that
2068
+ * object degrades — never a chain.
2069
+ *
2070
+ * A CPython grammar limit, not a deployment choice, so it is fixed rather than
2071
+ * configurable. The sibling `ts-types` renderer needs no counterpart: nothing
2072
+ * in the TypeScript grammar bounds nesting, and its SDK block is never type-
2073
+ * checked. Only bracket nesting counts — a `oneOf` renders as a flat `A | B`
2074
+ * chain and nested objects render as separate `class` statements, so neither
2075
+ * accumulates open brackets at any depth. The invariant this cap serves is
2076
+ * grammatical validity; see the `oneOf` arm in {@link renderType} for the one
2077
+ * interpreter limit deliberately left uncapped.
2078
+ */
2079
+ const MAX_LIST_NESTING = 180;
2080
+ /**
2081
+ * Cap a class-name base at {@link MAX_CLASS_NAME_BASE} (see the callers for
2082
+ * why capping keeps the render linear). `slice` counts UTF-16 code units, so
2083
+ * an astral character straddling the boundary would be cut in half and leave a
2084
+ * lone surrogate — not an identifier character, and not even well-formed text;
2085
+ * drop it rather than emit it.
2086
+ */
2087
+ function capClassNameBase(base) {
2088
+ if (base.length <= MAX_CLASS_NAME_BASE) return base;
2089
+ const capped = base.slice(0, MAX_CLASS_NAME_BASE);
2090
+ return /[\uD800-\uDBFF]$/.test(capped) ? capped.slice(0, -1) : capped;
2091
+ }
2092
+ /**
2093
+ * Reserve a unique class name from a base, suffixing `2`, `3`, … on collision.
2094
+ * The base is capped at {@link MAX_CLASS_NAME_BASE} first: child class names
2095
+ * derive from their parent's allocated name (`ParentChild`), so an unbounded
2096
+ * schema of single-field objects would otherwise grow each name by one field
2097
+ * per level and the sum of all names to Θ(depth²). Capping the base keeps each
2098
+ * name — and the total emitted text — linear in depth. Collisions resume from
2099
+ * the per-base counter in `state.nextClassCounter` rather than rescanning from
2100
+ * `2`, so a deep chain sharing one capped base stays O(1) per allocation
2101
+ * (amortized) instead of Θ(depth²) in time.
2102
+ */
2103
+ function allocateClassName(base, state) {
2104
+ const capped = capClassNameBase(base);
2105
+ let name = capped;
2106
+ if (state.usedClassNames.has(name)) {
2107
+ let n = state.nextClassCounter.get(capped) ?? 2;
2108
+ while (state.usedClassNames.has(`${capped}${n}`)) n++;
2109
+ name = `${capped}${n}`;
2110
+ state.nextClassCounter.set(capped, n + 1);
2111
+ }
2112
+ state.usedClassNames.add(name);
2113
+ return name;
2114
+ }
2115
+ /**
2116
+ * Append a child-name segment to a parent class-name base, capping the result
2117
+ * at {@link MAX_CLASS_NAME_BASE}. Capping AT PROPAGATION (not only inside
2118
+ * {@link allocateClassName}) keeps each level O(1): a deep `oneOf`- or
2119
+ * object-chain would otherwise carry an ever-growing ConsString down the tree
2120
+ * and re-materialize it (via `.length`/`.slice`) at every level — Θ(depth²).
2121
+ * The bounded base plus the collision counter still yields unique names.
2122
+ *
2123
+ * The join is NFKC-normalized because both sides are separately normalized yet
2124
+ * their concatenation need not be: a base ending in a Hangul L jamo or LV
2125
+ * syllable composes with a following V or T jamo head (`가` + `ᆨ` gives `각`),
2126
+ * so the emitted class name would differ from the symbol CPython compiles, and
2127
+ * two byte-distinct names could fold onto one — `usedClassNames` dedupes by the
2128
+ * raw bytes, so the collision counter would not see it. Normalizing costs
2129
+ * O(cap + segment) per level, the same order as the `slice` it feeds. The other
2130
+ * two join points need no counterpart: `Args`/`Output` start with `A`/`O` and
2131
+ * {@link allocateClassName}'s suffix is digits, none of which compose backwards.
2132
+ */
2133
+ function childClassName(base, segment) {
2134
+ return capClassNameBase(`${base}${segment}`.normalize("NFKC"));
2135
+ }
2136
+ /**
2137
+ * Render one validated scalar as Python literal text (`True`/`False`,
2138
+ * JSON-quoted strings, bare numbers). `null` cannot reach here: the `null`
2139
+ * type renders directly as `None`, and the unified validator rejects a null
2140
+ * `const`/`enum` entry on every other scalar type.
2141
+ *
2142
+ * A beyond-safe-range integral number takes `BigInt` digits rather than
2143
+ * `String`: Python integers are arbitrary-precision, so the emitted digits ARE
2144
+ * the value the model programs against, and `String` can give a different
2145
+ * integer than the double holds (`2 ** 60` prints the rounded `...847000`, not
2146
+ * the exact `...846976`) or no integer literal at all (`1e21` prints `1e+21`).
2147
+ * `String`'s rounding is not a bug in it: `Number::toString` emits the shortest
2148
+ * decimal string that re-reads to the same double, then pads to the exponent
2149
+ * with zeros (1 significant digit for `1e20`, 16 for `2 ** 60`) — and when the
2150
+ * shortest string is shorter than the double's exact value, those padded digits
2151
+ * name an integer no double holds. Passing one back would have to cross the
2152
+ * argument boundary as a JSON number — a double again — so the SDK would
2153
+ * document a value no program can pass. `BigInt` needs no case split: where
2154
+ * `String` is already exact (`2 ** 53`, `1e20`) the two agree byte for byte,
2155
+ * and where it is not, `BigInt` is the exact one. The TS flavor needs no
2156
+ * counterpart at all: its literal is re-read by a JS parser back into the same
2157
+ * double.
2158
+ *
2159
+ * `JSON.stringify` is also what keeps this path's output parseable, and it is
2160
+ * the only thing that does. It covers both classes of hazard: the two kinds of
2161
+ * code point CPython refuses anywhere in source — NUL among the C0 controls,
2162
+ * and the whole D800–DFFF unpaired-surrogate block, escaped under ES2019
2163
+ * well-formed stringification, which the engines range guarantees — and the
2164
+ * ones that break this line in particular, a bare `"` closing the literal
2165
+ * early, a trailing odd backslash eating the closing quote, and a bare LF/CR
2166
+ * ending it before its terminator. The `description` path carries
2167
+ * {@link UNPRINTABLE} and {@link LONE_SURROGATE} because nothing quotes it,
2168
+ * and folds newlines in {@link describe}.
2169
+ *
2170
+ * That leans on a coincidence worth naming: every escape `JSON.stringify` can
2171
+ * emit (`\"`, `\\`, `\b`, `\f`, `\n`, `\r`, `\t`, `\uXXXX`) is also a Python
2172
+ * escape denoting the same character, so the emitted `Literal[...]` both
2173
+ * parses and decodes back to the value the schema declared. DEL, the C1
2174
+ * controls (NEL among them), and LS/PS (U+2028/U+2029) do reach it raw —
2175
+ * legal but invisible, byte-for-byte as in the TS flavor; escaping them is a
2176
+ * both-flavors change. Those last three are legal here for the reason
2177
+ * {@link UNPRINTABLE} records: they are `str.splitlines()` boundaries, not
2178
+ * tokenizer line terminators. The subscript tool-name comment quotes its name
2179
+ * through its own call to the same `JSON.stringify`, never through this
2180
+ * function, and inherits both halves — escapes and pass-throughs alike.
2181
+ */
2182
+ function pyScalar(value) {
2183
+ if (value === true) return "True";
2184
+ if (value === false) return "False";
2185
+ if (typeof value === "string") return JSON.stringify(value);
2186
+ if (typeof value === "number" && Number.isInteger(value) && !Number.isSafeInteger(value)) return BigInt(value).toString();
2187
+ return String(value);
2188
+ }
2189
+ /**
2190
+ * Render a validated scalar `const`/`enum` as `Literal[...]`, falling back to
2191
+ * the broad type. Deliberately deviates from PEP 586, which restricts `Literal`
2192
+ * parameters to int/bool/str/bytes/enum/None: a non-integral number
2193
+ * `const`/`enum` emits a float literal (`Literal[1.5]`) a strict checker would
2194
+ * reject. An integral one does not deviate — {@link pyScalar} emits int digits,
2195
+ * including for the beyond-safe-range values it widens through `BigInt`, and
2196
+ * PEP 586 admits int parameters. Harmless either way — the stub is advisory
2197
+ * prompt text, only required to parse — and keeping the exact value
2198
+ * communicates the constraint to the model.
2199
+ */
2200
+ function renderConstrainedScalar(node, broad, state) {
2201
+ if (node.const !== void 0) {
2202
+ state.typing.add("Literal");
2203
+ return `Literal[${pyScalar(node.const)}]`;
2204
+ }
2205
+ if (node.enum !== void 0) {
2206
+ state.typing.add("Literal");
2207
+ return `Literal[${node.enum.map(pyScalar).join(", ")}]`;
2208
+ }
2209
+ return broad;
2210
+ }
2211
+ /**
2212
+ * Map one JSON-Schema node to a Python type expression, threading `state` to
2213
+ * collect the `TypedDict` declarations and `typing` symbols a full render
2214
+ * needs. `className` is the name to give an object node with properties (and
2215
+ * the prefix for its nested objects). Handles every unified schema construct —
2216
+ * `oneOf` (→ `X | Y`), `const`/`enum` (→ `Literal[...]`), `integer` (→ `int`),
2217
+ * `null` (→ `None`) — and degrades an unsupported or malformed schema to `Any`
2218
+ * without throwing, the same trusted-after-validation stance as the sibling
2219
+ * {@link ./ts-types.ts | ts-types} renderer. {@link jsonSchemaToPy} is the
2220
+ * context-free entry point; this is the collecting core.
2221
+ */
2222
+ function renderType(schema, className, state) {
2223
+ const newFrame = (schema, className, listDepth) => ({
2224
+ schema,
2225
+ className,
2226
+ phase: "start",
2227
+ listDepth,
2228
+ children: [],
2229
+ childIndex: 0,
2230
+ childTypes: [],
2231
+ entries: []
2232
+ });
2233
+ try {
2234
+ assertSupportedJsonSchema(schema);
2235
+ const frames = [newFrame(schema, className, 0)];
2236
+ let result;
2237
+ const finish = (type) => {
2238
+ frames.pop();
2239
+ const parent = frames.at(-1);
2240
+ if (parent === void 0) result = type;
2241
+ else parent.childTypes.push(type);
2242
+ };
2243
+ while (frames.length > 0) {
2244
+ const frame = frames.at(-1);
2245
+ /* v8 ignore next -- the loop condition guarantees a current frame. */
2246
+ if (frame === void 0) break;
2247
+ if (frame.phase === "children") {
2248
+ if (frame.childIndex < frame.children.length) {
2249
+ const child = frame.children[frame.childIndex];
2250
+ /* v8 ignore next -- childIndex is bounded by children.length. */
2251
+ if (child === void 0) throw new Error("missing python render child");
2252
+ frame.childIndex++;
2253
+ frames.push(newFrame(child.schema, child.className, child.listDepth));
2254
+ continue;
2255
+ }
2256
+ if (frame.kind === "oneOf") {
2257
+ let union = "";
2258
+ for (const [index, childType] of frame.childTypes.entries()) union = index === 0 ? childType : `${union} | ${childType}`;
2259
+ finish(union);
2260
+ continue;
2261
+ }
2262
+ if (frame.kind === "array") {
2263
+ /* v8 ignore next -- the ?? arm needs a childless array frame, which start never builds. */
2264
+ finish(`list[${frame.childTypes[0] ?? "Any"}]`);
2265
+ continue;
2266
+ }
2267
+ const node = frame.node;
2268
+ const name = frame.allocated;
2269
+ /* v8 ignore next -- typeddict frames always set node and allocated at start. */
2270
+ if (node === void 0 || name === void 0) throw new Error("missing typeddict frame state");
2271
+ const required = new Set(node.required);
2272
+ const lines = [`class ${name}(TypedDict):`];
2273
+ for (let index = 0; index < frame.entries.length; index++) {
2274
+ const entry = frame.entries[index];
2275
+ const fieldType = frame.childTypes[index];
2276
+ /* v8 ignore next -- entries and childTypes correspond one-to-one. */
2277
+ if (entry === void 0 || fieldType === void 0) throw new Error("missing typeddict field type");
2278
+ const [field, fieldSchema] = entry;
2279
+ const description = describe(fieldSchema);
2280
+ if (description !== void 0) lines.push(`${pad(1)}# ${description}`);
2281
+ if (required.has(field)) lines.push(`${pad(1)}${field}: ${fieldType}`);
2282
+ else {
2283
+ state.typing.add("NotRequired");
2284
+ lines.push(`${pad(1)}${field}: NotRequired[${fieldType}]`);
2285
+ }
2286
+ }
2287
+ if (node.additionalProperties !== false) lines.push(`${pad(1)}# Additional keys beyond those declared are allowed.`);
2288
+ if (lines.length === 1) lines.push(`${pad(1)}pass`);
2289
+ state.classes.push(lines.join("\n"));
2290
+ finish(name);
2291
+ continue;
2292
+ }
2293
+ frame.phase = "children";
2294
+ const node = frame.schema;
2295
+ if (node.oneOf !== void 0) {
2296
+ frame.kind = "oneOf";
2297
+ frame.children = node.oneOf.map((branch, index) => ({
2298
+ schema: branch,
2299
+ className: childClassName(frame.className, `${index + 1}`),
2300
+ listDepth: frame.listDepth
2301
+ }));
2302
+ continue;
2303
+ }
2304
+ if (node.type === void 0) {
2305
+ state.typing.add("Any");
2306
+ finish("Any");
2307
+ continue;
2308
+ }
2309
+ switch (node.type) {
2310
+ case "string":
2311
+ finish(renderConstrainedScalar(node, "str", state));
2312
+ break;
2313
+ case "number":
2314
+ finish(renderConstrainedScalar(node, "float", state));
2315
+ break;
2316
+ case "integer":
2317
+ finish(renderConstrainedScalar(node, "int", state));
2318
+ break;
2319
+ case "boolean":
2320
+ finish(renderConstrainedScalar(node, "bool", state));
2321
+ break;
2322
+ case "null":
2323
+ finish("None");
2324
+ break;
2325
+ case "array":
2326
+ if (node.items === void 0) {
2327
+ state.typing.add("Any");
2328
+ finish("list[Any]");
2329
+ break;
2330
+ }
2331
+ if (frame.listDepth >= MAX_LIST_NESTING) {
2332
+ state.typing.add("Any");
2333
+ finish("Any");
2334
+ break;
2335
+ }
2336
+ frame.kind = "array";
2337
+ frame.children = [{
2338
+ schema: node.items,
2339
+ className: frame.className,
2340
+ listDepth: frame.listDepth + 1
2341
+ }];
2342
+ break;
2343
+ case "object": {
2344
+ const entries = Object.entries(node.properties ?? {});
2345
+ if (className === "" || !entries.every(([name]) => isBareIdentifier(name) && !RESERVED.has(name) && !(name.startsWith("__") && !name.endsWith("__")))) {
2346
+ state.typing.add("Any");
2347
+ finish("dict[str, Any]");
2348
+ break;
2349
+ }
2350
+ if (entries.length === 0 && node.additionalProperties !== false) {
2351
+ state.typing.add("Any");
2352
+ finish("dict[str, Any]");
2353
+ break;
2354
+ }
2355
+ frame.kind = "typeddict";
2356
+ frame.node = node;
2357
+ frame.allocated = allocateClassName(frame.className, state);
2358
+ state.typing.add("TypedDict");
2359
+ frame.entries = entries;
2360
+ /* v8 ignore next -- allocated is always set before children are built. */
2361
+ frame.children = entries.map(([field, child]) => ({
2362
+ schema: child,
2363
+ className: childClassName(frame.allocated ?? "", camelCase(field)),
2364
+ listDepth: 1
2365
+ }));
2366
+ break;
2367
+ }
2368
+ /* v8 ignore next 4 -- assertSupportedJsonSchema narrowed this closed type union. */
2369
+ default:
2370
+ state.typing.add("Any");
2371
+ finish("Any");
2372
+ }
2373
+ }
2374
+ /* v8 ignore next -- every root frame produces one expression. */
2375
+ return result ?? "Any";
2376
+ } catch {
2377
+ state.typing.add("Any");
2378
+ return "Any";
2379
+ }
2380
+ }
2381
+ /**
2382
+ * Map one JSON-Schema node to a context-free Python type expression from the
2383
+ * `typing` module. Handles every unified schema construct — `object` (degraded
2384
+ * to `dict[str, Any]`: naming a `TypedDict` requires the render context that
2385
+ * {@link renderToolsSdkPy} supplies), `const`/`enum` (→ `Literal[...]`),
2386
+ * `oneOf` (→ union), `string`/`number`/`integer`/`boolean`/`null`, `array`
2387
+ * (`items` → `list[T]`) — and returns `Any` for an unsupported or malformed
2388
+ * schema, matching the TS flavor's `unknown` fallback. Type annotations in the
2389
+ * emitted SDK are advisory: Python does not enforce them at runtime.
2390
+ * @param schema - the JSON-Schema node.
2391
+ * @returns the Python type text.
2392
+ */
2393
+ function jsonSchemaToPy(schema) {
2394
+ return renderType(schema, "", {
2395
+ classes: [],
2396
+ usedClassNames: /* @__PURE__ */ new Set(),
2397
+ nextClassCounter: /* @__PURE__ */ new Map(),
2398
+ typing: /* @__PURE__ */ new Set()
2399
+ });
2400
+ }
2401
+ /** The fixed model-facing usage contract rendered above the declarations. */
2402
+ const SDK_INSTRUCTIONS = `## Writing code for run_code
2403
+
2404
+ \`run_code\` takes two required arguments: \`description\`, a short summary of what the program does, and \`code\` — the body of an async Python function (top-level \`await\` and \`return\` both work). 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:
2405
+
2406
+ - 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.
2407
+ - 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.
2408
+ - 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\`.
2409
+ - 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.
2410
+
2411
+ The available tools:`;
2412
+ /**
2413
+ * Render the full `tools:sdk` prompt section under `runtime.language ===
2414
+ * 'python'`: the Python-flavored usage instructions plus one named `TypedDict`
2415
+ * per tool argument or output object (and per nested object) and one awaitable
2416
+ * method per visible tool on a `Tools` protocol — typed args in, the tool's
2417
+ * canonical output value out — with a `tools: Tools` singleton the model calls
2418
+ * into. The `typing` import line lists exactly the symbols the render used.
2419
+ * Deterministic — tools are emitted in lexicographic name order, and class
2420
+ * declarations precede the protocol in that same order (nested classes before
2421
+ * the parent that references them), so an unchanged tool set produces
2422
+ * byte-identical text across assemblies. The sort is not a total order on
2423
+ * byte-equal names, so two schemas sharing a name would render in argument
2424
+ * order; the caller's visible-capability map is keyed by name, so the input
2425
+ * never carries a duplicate.
2426
+ * @param schemas - the tool schemas plus canonical output schemas to declare
2427
+ * (the caller excludes `run_code` itself).
2428
+ * @returns the complete section text.
2429
+ */
2430
+ function renderToolsSdkPy(schemas) {
2431
+ const sorted = [...schemas].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
2432
+ const state = {
2433
+ classes: [],
2434
+ usedClassNames: /* @__PURE__ */ new Set(),
2435
+ nextClassCounter: /* @__PURE__ */ new Map(),
2436
+ typing: new Set(["Protocol"])
2437
+ };
2438
+ const members = [];
2439
+ let statements = 0;
2440
+ for (const schema of sorted) {
2441
+ const argType = renderType(schema.parameters, `${camelCase(schema.name)}Args`, state);
2442
+ const outputType = renderType(schema.output, `${camelCase(schema.name)}Output`, state);
2443
+ if (isBareIdentifier(schema.name) && !RESERVED.has(schema.name) && !schema.name.startsWith("_")) {
2444
+ const doc = docLines(schema.description, 2);
2445
+ 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}: ...`);
2446
+ members.push(...doc);
2447
+ statements += 1;
2448
+ } else {
2449
+ members.push(`${pad(1)}# tools[${JSON.stringify(schema.name)}](args: ${argType}) -> ${outputType}`);
2450
+ const description = describe(schema);
2451
+ if (description !== void 0) members.push(`${pad(1)}# ${description}`);
2452
+ }
2453
+ }
2454
+ const body = (statements > 0 ? members : [`${pad(1)}pass`, ...members]).join("\n");
2455
+ const imports = TYPING_ORDER.filter((symbol) => state.typing.has(symbol));
2456
+ const classBlock = state.classes.length > 0 ? `${state.classes.join("\n\n")}\n\n` : "";
2457
+ return `${SDK_INSTRUCTIONS}\n\n\`\`\`python\n${`from typing import ${imports.join(", ")}\n\nclass ToolCallError(Exception):
2458
+ toolName: str\n\n${classBlock}class Tools(Protocol):\n${body}\n\ntools: Tools`}\n\`\`\``;
2459
+ }
2460
+ //#endregion
2461
+ //#region lib/types/testing.js
2462
+ /** Canonical tool-definition fixtures for repository tests. @module qilin-tools/testing */
2463
+ const CONTENT_VALUE_SCHEMA = {
2464
+ type: "array",
2465
+ items: { type: "json" }
2466
+ };
2467
+ /**
2468
+ * Define a test fixture that deliberately uses its content blocks as the
2469
+ * canonical JSON value. Product tools must declare domain-owned DTOs instead.
2470
+ * @param options - ordinary fixture fields plus a content-producing body.
2471
+ * @returns a registry-ready tool with an explicit JSON-array output contract.
2472
+ * @internal
2473
+ */
2474
+ function defineContentToolFixture(options) {
2475
+ const execute = options.execute;
2476
+ return defineTool({
2477
+ ...options,
2478
+ output: {
2479
+ schema: CONTENT_VALUE_SCHEMA,
2480
+ render: (_args, value) => value
2481
+ },
2482
+ async execute(args, exec) {
2483
+ return await execute(args, exec);
2484
+ }
2485
+ });
2486
+ }
2487
+ //#endregion
2488
+ //#region lib/types/index.js
2489
+ /**
2490
+ * Tool registry, model presentation modes, and pre/guard/around/post/result
2491
+ * execution pipeline.
2492
+ * @module @qilin-agent/tools
2493
+ */
2494
+ /**
2495
+ * Language → SDK-section renderer. The registry looks up the loaded
2496
+ * `ctx.ptcRuntime.language` in this table when assembling the `tools:sdk`
2497
+ * section under a non-native mode; a runtime whose language is not a key
2498
+ * fails the assembly loudly (same idiom as `toolOrder` violations). Adding a
2499
+ * new backend language is three parallel edits — a {@link PtcSdkLanguage}
2500
+ * member, an entry here, and a `RUN_CODE_FLAVORS` entry in `ptc.ts` for
2501
+ * its `run_code` schema strings — plus the renderer function this table points
2502
+ * at. The `satisfies` clause pins this table's key set to that union, which
2503
+ * the flavor table is checked against too, so any of the three left out is a
2504
+ * typecheck failure. What no check reaches is the prose that names the values
2505
+ * instead of deriving them: the seam's `qilin-ptc-runtime` README pair, its
2506
+ * `PtcRuntime.language` JSDoc, and `docs/subsystems/ptc-runtime.md`
2507
+ * with its zh pair, plus this package's own README pair and the
2508
+ * {@link Config.mode} JSDoc.
2509
+ */
2510
+ /**
2511
+ * The model-facing statement of the `ptc` collapse. Names the consequence
2512
+ * (the call fails) and the route (inside the program), because a rule the
2513
+ * model can only discover by being denied is one it corrects too late.
2514
+ */
2515
+ const PTC_ONLY_INSTRUCTION = `\`${RUN_CODE_NAME}\` is the only tool you can call directly — a tool call naming any other tool fails. Reach every tool the SDK declares below from inside the program.`;
2516
+ const SDK_RENDERERS = {
2517
+ typescript: renderToolsSdk,
2518
+ python: renderToolsSdkPy
2519
+ };
2520
+ /**
2521
+ * Scheduler entry point omitted from the generated named service API.
2522
+ * The registry symbol is process-global (`Symbol.for`) because src-launch can
2523
+ * load this package twice — the plugin entry through the profile resolution
2524
+ * and bare-name imports through the built `lib` — and each copy mints its own
2525
+ * `Symbol`, so a per-copy symbol makes the scheduler lookup miss.
2526
+ * @internal
2527
+ */
2528
+ const TOOL_RUNTIME_SCHEDULER = Symbol.for("@qilin-agent/tools.scheduler");
2529
+ /** Canonical error code for cancellation after a tool body was invoked. */
2530
+ const TOOL_ABORTED = "ABORTED";
2531
+ /** Canonical error code for cancellation before a tool body was invoked. */
2532
+ const TOOL_ABORTED_BEFORE_DISPATCH = "ABORTED_BEFORE_DISPATCH";
2533
+ /**
2534
+ * Thrown (internally) when the model requests a tool that isn't registered.
2535
+ * Extends {@link HarnessError} (`code: 'UNKNOWN_TOOL'`) so an unknown-tool
2536
+ * failure is as routable as a tool-thrown one — retry/sandbox/replay code can
2537
+ * distinguish it from a tool body's own error.
2538
+ */
2539
+ var ToolNotFoundError = class extends HarnessError {
2540
+ /**
2541
+ * @param toolName - the name the caller asked for.
2542
+ * @param reachableFrom - how the model reaches this tool instead, when the
2543
+ * name IS visible and only the presentation denies calling it directly.
2544
+ * Omitted for a name that is registered nowhere.
2545
+ */
2546
+ constructor(toolName, reachableFrom) {
2547
+ super(reachableFrom === void 0 ? `unknown tool "${toolName}"` : `unknown tool "${toolName}": ${reachableFrom}`, "UNKNOWN_TOOL");
2548
+ this.name = "ToolNotFoundError";
2549
+ }
2550
+ };
2551
+ /** Thrown when a tool body or post-policy value violates its declared output. */
2552
+ var ToolOutputError = class extends HarnessError {
2553
+ /** Schema/value violations in validation order. */
2554
+ violations;
2555
+ constructor(toolName, violations) {
2556
+ super(`tool "${toolName}" returned invalid output: ${violations.join("; ")}`, "INVALID_TOOL_OUTPUT");
2557
+ this.name = "ToolOutputError";
2558
+ this.violations = violations;
2559
+ }
2560
+ };
2561
+ /** Convert one projector exception into the canonical invalid-output failure. */
2562
+ function projectionError(toolName, projector, error) {
2563
+ return new ToolOutputError(toolName, [`output.${projector} failed: ${errorMessage(error)}`]);
2564
+ }
2565
+ /** Snapshot one projector result before later durable-result materialization. */
2566
+ function snapshotProjection(toolName, projector, candidate) {
2567
+ try {
2568
+ const detached = snapshotJsonValue(candidate);
2569
+ if (detached === void 0) throw new ToolOutputError(toolName, [`output.${projector} returned non-lossless JSON`]);
2570
+ return detached;
2571
+ } catch (error) {
2572
+ if (error instanceof ToolOutputError) throw error;
2573
+ throw projectionError(toolName, projector, error);
2574
+ }
2575
+ }
2576
+ /** Snapshot one body or policy value into the canonical invalid-output failure class. */
2577
+ function snapshotToolValue(toolName, candidate) {
2578
+ try {
2579
+ const detached = snapshotJsonValue(candidate);
2580
+ if (detached === void 0) throw new ToolOutputError(toolName, ["value is not lossless JSON"]);
2581
+ return detached;
2582
+ } catch (error) {
2583
+ if (error instanceof ToolOutputError) throw error;
2584
+ throw new ToolOutputError(toolName, [`value snapshot failed: ${errorMessage(error)}`]);
2585
+ }
2586
+ }
2587
+ /**
2588
+ * Best-effort human-readable message from an arbitrary thrown value: Error
2589
+ * instances use `.message`; non-Error objects with a string `message`
2590
+ * property (e.g. `throw { message: 'denied' }`) use it too; everything else
2591
+ * is stringified.
2592
+ */
2593
+ function errorMessage(error) {
2594
+ try {
2595
+ if (error instanceof Error) return error.message;
2596
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") return error.message;
2597
+ return String(error);
2598
+ } catch {
2599
+ return "<unprintable thrown value>";
2600
+ }
2601
+ }
2602
+ /** Derive one failure message from policy feedback without changing its rendered blocks. */
2603
+ function failureMessageFromContent(content) {
2604
+ const text = content.map((block) => block.type === "text" ? block.text : `[${block.type} content]`).join("\n");
2605
+ return text.length > 0 ? text : "tool result blocked by post-execute policy";
2606
+ }
2607
+ /** Snapshot and freeze one durable tool-result projection or reject lossy data. */
2608
+ function materializePresentation(candidate) {
2609
+ const detached = snapshotJsonValue(candidate);
2610
+ if (detached === void 0) throw new TypeError("tool result must be losslessly JSON-serializable");
2611
+ return deepFreeze(detached);
2612
+ }
2613
+ /** Structured `{ name, code }` for a thrown HarnessError, else undefined. */
2614
+ function errorInfo(error) {
2615
+ try {
2616
+ return error instanceof HarnessError ? {
2617
+ name: error.name,
2618
+ code: error.code
2619
+ } : void 0;
2620
+ } catch {
2621
+ return;
2622
+ }
2623
+ }
2624
+ /** One scope's complete tool-registry contribution. */
2625
+ var ToolLayer = class {
2626
+ tools;
2627
+ restrictions = new AnonymousEntries();
2628
+ guards = new AnonymousEntries();
2629
+ /**
2630
+ * Presentation this scope's agent declared for itself, shadowing the
2631
+ * deployment default. One cell rather than an entry table: two answers to
2632
+ * "which form does the model see" is a contradiction, not a merge.
2633
+ */
2634
+ mode;
2635
+ constructor(scope) {
2636
+ 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`));
2637
+ }
2638
+ /** Whether every contribution table in this aggregate layer is empty. */
2639
+ isEmpty() {
2640
+ return this.tools.isEmpty() && this.restrictions.isEmpty() && this.guards.isEmpty() && this.mode === void 0;
2641
+ }
2642
+ /** Whether every compiled restriction in this layer admits a global tool name. */
2643
+ admits(name) {
2644
+ 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;
2645
+ return true;
2646
+ }
2647
+ /** First monotonic denial from this layer's live guard registrations. */
2648
+ guardReason(exec) {
2649
+ for (const guard of this.guards.values()) {
2650
+ const reason = guard(exec);
2651
+ if (reason !== void 0) return reason;
2652
+ }
2653
+ }
2654
+ };
2655
+ /** Resolve the run_code overlap cap at the owning config boundary (direct construction bypasses the Loader schema). */
2656
+ function resolveMaxParallelSubCalls(value) {
2657
+ const maxParallelSubCalls = value ?? 10;
2658
+ if (!Number.isInteger(maxParallelSubCalls) || maxParallelSubCalls < 1) throw new Error("maxParallelSubCalls must be a positive integer");
2659
+ return maxParallelSubCalls;
2660
+ }
2661
+ /**
2662
+ * Tool registry and execution pipeline. Scoped registrations shadow globals;
2663
+ * one visibility resolver feeds presentation, lookup, and dispatch.
2664
+ */
2665
+ var ToolRuntime = class extends Service {
2666
+ static inject = ["systemPrompt"];
2667
+ static Config = z.object({
2668
+ mode: z.union([
2669
+ "native",
2670
+ "ptc",
2671
+ "both"
2672
+ ]).default("native"),
2673
+ maxParallelSubCalls: z.natural().min(1).default(10)
2674
+ });
2675
+ /** Internal staged view consumed by `qilin-agent-loop`'s parallel scheduler. */
2676
+ [TOOL_RUNTIME_SCHEDULER] = {
2677
+ prepare: (exec) => this.prepareScheduledExecution(exec),
2678
+ dispatch: (exec) => this.dispatchScheduledExecution(exec),
2679
+ finalize: (exec, result) => this.finalizeScheduledExecution(exec, result),
2680
+ finish: (exec, result) => this.finishScheduledExecution(exec, result)
2681
+ };
2682
+ /** Context deferred by a running tool body, keyed by its scheduler-owned execution. */
2683
+ deferredContexts = /* @__PURE__ */ new WeakMap();
2684
+ /** Executions whose tool body declared the current turn complete. */
2685
+ concludingExecutions = /* @__PURE__ */ new WeakSet();
2686
+ /** Original caller cancellation, kept outside the wrapper-mutable execution object. */
2687
+ cancellationStates = /* @__PURE__ */ new WeakMap();
2688
+ /** Definition-owned final content transform snapshotted before policy begins. */
2689
+ contentFinalizers = /* @__PURE__ */ new WeakMap();
2690
+ layers = new ScopedLayers((scope) => new ToolLayer(scope), () => {
2691
+ this.ctx.emit("tools/change");
2692
+ });
2693
+ /** Presentation for scopes that declare none; {@link presentAs} shadows it per scope. */
2694
+ defaultMode;
2695
+ maxParallelSubCalls;
2696
+ /**
2697
+ * Reserved presentation transport, kept outside the filterable registration
2698
+ * layers. Built on first need rather than at construction: which agents run
2699
+ * a PTC mode is no longer known when the service is constructed, and the
2700
+ * transport is stateless beyond its closures over `this`.
2701
+ */
2702
+ ptcTransport;
2703
+ constructor(ctx, config = {}) {
2704
+ super(ctx, "tools");
2705
+ this.defaultMode = config.mode ?? "native";
2706
+ this.maxParallelSubCalls = resolveMaxParallelSubCalls(config.maxParallelSubCalls);
2707
+ ctx.systemPrompt.tools((context) => this.wireSchemas(context.scope));
2708
+ if (this.defaultMode !== "native") {
2709
+ ctx.systemPrompt.section(this.collapseSection());
2710
+ ctx.systemPrompt.section(this.sdkSection());
2711
+ }
2712
+ }
2713
+ /**
2714
+ * The prompt statement of the `ptc` executor collapse, registered wherever
2715
+ * {@link sdkSection} is and rendering empty outside an effective `ptc`.
2716
+ *
2717
+ * Every tool contributes its own guidance section naming its tool, none of
2718
+ * them qualify how that tool is reached, and they all render before the SDK.
2719
+ * Without this the model reads a catalog of tools it is told to use and no
2720
+ * statement that only `run_code` may be called, so it emits a native call,
2721
+ * receives `UNKNOWN_TOOL` for a tool the prompt just declared, and concludes
2722
+ * the deployment is inconsistent. Its order places the rule before that
2723
+ * guidance rather than after it.
2724
+ *
2725
+ * `both` renders empty: native calls do execute there, so the rule is false.
2726
+ * @returns the section registration.
2727
+ */
2728
+ collapseSection() {
2729
+ return {
2730
+ name: "tools:ptc-only",
2731
+ order: this.ctx.systemPrompt.getSectionOrder("PTC_ONLY"),
2732
+ text: (context) => this.modeFor(context.scope) === "ptc" ? PTC_ONLY_INSTRUCTION : ""
2733
+ };
2734
+ }
2735
+ /**
2736
+ * The generated-SDK prompt section, registered globally by a PTC mode
2737
+ * deployment and per scope by {@link presentAs}.
2738
+ *
2739
+ * The body regenerates from the CALLING scope, and renders empty for an
2740
+ * agent presenting natively — an agent that opted out under a PTC mode
2741
+ * deployment still sees the global registration, and an empty section is
2742
+ * dropped from the rendered prompt.
2743
+ * @returns the section registration.
2744
+ */
2745
+ sdkSection() {
2746
+ return {
2747
+ name: "tools:sdk",
2748
+ order: this.ctx.systemPrompt.getSectionOrder("TOOLS_SDK"),
2749
+ interpolate: false,
2750
+ text: (context) => {
2751
+ const mode = this.modeFor(context.scope);
2752
+ if (mode === "native") return "";
2753
+ const runtime = this.requirePtcRuntime(mode);
2754
+ const render = SDK_RENDERERS[runtime.language];
2755
+ /* v8 ignore next -- requirePtcRuntime rejects an unknown language before this runs. */
2756
+ if (render === void 0) throw new Error(`qilin-tools: no SDK renderer for ${runtime.language}`);
2757
+ return render(this.sdkSchemas(context.scope));
2758
+ }
2759
+ };
2760
+ }
2761
+ /**
2762
+ * The presentation one scope's agent sees: its own declaration, else the
2763
+ * deployment default.
2764
+ * @param scope - the calling agent, or undefined for the global view.
2765
+ * @returns the resolved presentation mode.
2766
+ */
2767
+ modeFor(scope) {
2768
+ const layers = this.layers.chainLayers(scope);
2769
+ for (let index = layers.length - 1; index >= 0; index -= 1) {
2770
+ const mode = layers[index]?.mode;
2771
+ if (mode !== void 0) return mode;
2772
+ }
2773
+ return this.defaultMode;
2774
+ }
2775
+ /**
2776
+ * The reserved `run_code` transport, built on first need.
2777
+ *
2778
+ * It never enters the global layer: per-agent restrictions must not remove
2779
+ * it, and a scoped registration must not shadow it. The visibility resolver
2780
+ * appends it after resolving the filterable global/scoped capability layers,
2781
+ * and only for scopes whose mode actually presents it.
2782
+ * @returns the shared transport definition.
2783
+ */
2784
+ requirePtcTransport() {
2785
+ this.ptcTransport ??= createRunCodeTool(this, {
2786
+ requireRuntime: () => this.requirePtcRuntime(this.defaultMode),
2787
+ peekApprover: () => this.ctx.get("approval"),
2788
+ resolveSandboxPolicy: (exec) => {
2789
+ const policy = this.ctx.get("sandboxPolicy");
2790
+ if (policy === void 0) throw new Error("qilin-tools: confined PTC runtime requires sandboxPolicy");
2791
+ return policy.resolve(exec.agent === void 0 ? {} : { session: exec.agent.session });
2792
+ },
2793
+ peekRuntime: () => this.ctx.get("ptcRuntime"),
2794
+ maxParallel: this.maxParallelSubCalls,
2795
+ shapeDispatchLog: (dispatch) => this.shapeDispatchLog(dispatch)
2796
+ });
2797
+ return this.ptcTransport;
2798
+ }
2799
+ /**
2800
+ * Present the calling scope's tools in `mode` instead of the deployment
2801
+ * default. Nearest scope on the chain wins, so a preset's standing
2802
+ * declaration covers every agent joined under it.
2803
+ *
2804
+ * Scoped only, and one declaration per scope: this is how an agent preset
2805
+ * composes PTC mode agents beside native ones in the same process, and a
2806
+ * process-global override would be the `mode` config field instead.
2807
+ * @param mode - the presentation the covered agents' models see.
2808
+ * @returns the exact disposer that restores the deployment default.
2809
+ */
2810
+ presentAs(mode) {
2811
+ const ctx = this.ctx;
2812
+ 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");
2813
+ return ctx.effect(function* () {
2814
+ yield this.layers.effect(ctx, (layer) => {
2815
+ 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`);
2816
+ layer.mode = mode;
2817
+ return () => {
2818
+ layer.mode = void 0;
2819
+ };
2820
+ }, { label: "tools.presentAs()" });
2821
+ if (mode !== "native") {
2822
+ yield ctx.systemPrompt.section(this.collapseSection());
2823
+ yield ctx.systemPrompt.section(this.sdkSection());
2824
+ }
2825
+ }.bind(this), "tools.presentAs()");
2826
+ }
2827
+ /**
2828
+ * Build one scope's wire schemas and names for prompt-order validation.
2829
+ * Restrictions do not make known tools invalid, but a mode collapse does.
2830
+ */
2831
+ wireSchemas(scope) {
2832
+ const view = this.view(scope);
2833
+ const mode = this.modeFor(scope);
2834
+ if (mode === "native") return {
2835
+ schemas: [...view.visible.values()].map((definition) => this.schemaOf(definition, false)),
2836
+ knownNames: [...view.knownNames]
2837
+ };
2838
+ this.requirePtcRuntime(mode);
2839
+ const schemas = [...view.visible.values()].map((definition) => this.schemaOf(definition, false));
2840
+ if (mode === "ptc") return {
2841
+ schemas: schemas.filter((schema) => schema.name === RUN_CODE_NAME),
2842
+ knownNames: [RUN_CODE_NAME]
2843
+ };
2844
+ return {
2845
+ schemas,
2846
+ knownNames: [...view.knownNames, RUN_CODE_NAME]
2847
+ };
2848
+ }
2849
+ /**
2850
+ * Resolve the PTC runtime or throw the actionable misconfiguration error.
2851
+ * Read at use time (assembly / run_code execution), NOT via static
2852
+ * `inject`: an inject entry would hold `ctx.tools` — and every tool plugin
2853
+ * behind it — hostage to a PTC runtime existing even under `mode:
2854
+ * 'native'`.
2855
+ *
2856
+ * Assembly and `run_code` execution read separately, so the language is not
2857
+ * bound to a request. Harmless while one published backend exists — both
2858
+ * reads return the same flavor — but a reload that swapped in a second
2859
+ * language between them would hand a program written against one SDK to the
2860
+ * other. Binding it is deferred until a second backend ships (the first
2861
+ * point it is testable).
2862
+ */
2863
+ requirePtcRuntime(mode) {
2864
+ const runtime = this.ctx.get("ptcRuntime");
2865
+ if (!runtime) throw new Error(`qilin-tools: mode "${mode}" requires a PTC runtime — load a ctx.ptcRuntime implementation (e.g. @qilin-agent/ptc-runtime-node) or set tools mode to "native"`);
2866
+ if (!Object.hasOwn(SDK_RENDERERS, runtime.language)) {
2867
+ const known = Object.keys(SDK_RENDERERS).map((name) => JSON.stringify(name)).join(", ");
2868
+ throw new Error(`qilin-tools: no SDK renderer registered for runtime language ${JSON.stringify(runtime.language)} (known: ${known})`);
2869
+ }
2870
+ return runtime;
2871
+ }
2872
+ /**
2873
+ * Register globally or in the calling agent scope. Scoped tools shadow
2874
+ * globals; duplicates within one layer and the reserved `run_code` name fail.
2875
+ * @param definition - tool schema, execution, and optional finalization/presentation callbacks.
2876
+ * @returns the exact disposer that unregisters the tool.
2877
+ */
2878
+ register(definition) {
2879
+ const name = definition.name;
2880
+ const output = definition.output;
2881
+ 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? }`);
2882
+ assertSupportedJsonSchema(output.schema);
2883
+ const timeoutMs = definition.timeoutMs;
2884
+ if (timeoutMs !== void 0 && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) throw new TypeError(`tool "${name}" timeoutMs must be a positive finite number`);
2885
+ if (name === "run_code") throw new Error(`tool name "${RUN_CODE_NAME}" is reserved for the PTC mode presentation transport and cannot be registered or shadowed`);
2886
+ return this.layers.effect(this.ctx, (layer) => layer.tools.insert(name, definition), { label: "tools.register()" });
2887
+ }
2888
+ /**
2889
+ * Restrict global tools for the calling agent scope. Empty filters, unknown
2890
+ * names, scope-local names, and reserved transport names fail. Restrictions
2891
+ * intersect; scoped registrations remain visible.
2892
+ * @param filter - global-tool mask: `allow` (keep only) and/or `deny` (remove).
2893
+ * @returns the exact disposer that lifts this restriction.
2894
+ */
2895
+ restrict(filter) {
2896
+ const scope = scopeOf(this.ctx);
2897
+ 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");
2898
+ const allow = filter.allow;
2899
+ const deny = filter.deny;
2900
+ 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)");
2901
+ const compiled = {
2902
+ ...allow !== void 0 ? { allow: new Set(allow) } : {},
2903
+ ...deny !== void 0 ? { deny: new Set(deny) } : {}
2904
+ };
2905
+ if ([...allow ?? [], ...deny ?? []].includes("run_code")) throw new Error(`tools.restrict() cannot name reserved PTC mode presentation transport "${RUN_CODE_NAME}"; restrict end-capability tools instead`);
2906
+ const known = this.view(scope).restrictableNames;
2907
+ const unknown = [...allow ?? [], ...deny ?? []].filter((name) => !known.has(name));
2908
+ 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)"}`);
2909
+ return this.layers.effect(this.ctx, (layer) => layer.restrictions.append(compiled), { label: "tools.restrict()" });
2910
+ }
2911
+ /**
2912
+ * Register a monotonic guard after the extensible `tools/pre-execute`
2913
+ * waterfall. A plain-context guard applies globally; one registered through
2914
+ * `agent.ctx` applies only to that agent. Any matching guard may deny by
2915
+ * returning a reason, while no guard can force-allow a call another guard
2916
+ * denied. The exact effect disposer is returned for ordered ownership and
2917
+ * HMR cleanup.
2918
+ * @param guard - synchronous check; a returned string denies the execution.
2919
+ * @returns the exact disposer that unregisters the guard.
2920
+ */
2921
+ guard(guard) {
2922
+ return this.layers.effect(this.ctx, (layer) => layer.guards.append(guard), {
2923
+ label: "tools.guard()",
2924
+ notify: false
2925
+ });
2926
+ }
2927
+ /** First monotonic denial from the global then the scope chain's guard layers, farthest first. */
2928
+ guardReason(exec) {
2929
+ const globalReason = this.layers.global.guardReason(exec);
2930
+ if (globalReason !== void 0) return globalReason;
2931
+ if (exec.agent === void 0) return void 0;
2932
+ for (const layer of this.layers.chainLayers(exec.agent)) {
2933
+ const reason = layer.guardReason(exec);
2934
+ if (reason !== void 0) return reason;
2935
+ }
2936
+ }
2937
+ /**
2938
+ * Resolve every registry fact one scope needs in one layer traversal. The
2939
+ * visible map applies restrictions to the INHERITED surface, then the
2940
+ * scope's own registrations and the reserved presentation transport; the
2941
+ * other sets retain the pre-restriction facts needed by restriction and
2942
+ * prompt-order validation.
2943
+ *
2944
+ * A restriction filters what a scope inherits — the global layer and every
2945
+ * ancestor layer on its chain — and never what its OWN layer registers.
2946
+ * That exemption is what a per-child capability filter has to keep intact:
2947
+ * the delegation runtime registers a child's structured-output tool into the
2948
+ * child's own layer, and a filter naming the capabilities the child may use
2949
+ * must not strip the machinery it answers through.
2950
+ *
2951
+ * Reading the exempt set as "the global layer" instead of "not mine" held
2952
+ * only while every model-facing tool sat in the host composition. Once
2953
+ * presets moved them onto the agent plane they became an ANCESTOR
2954
+ * contribution, so a child's filter silently stopped constraining anything
2955
+ * it was given.
2956
+ * @param scope - the viewing scope (the agent), or undefined for the global view.
2957
+ * @returns the complete derived view for that scope.
2958
+ */
2959
+ view(scope) {
2960
+ const layers = this.layers.chainLayers(scope);
2961
+ const own = this.layers.peek(scope);
2962
+ const inherited = new Map(this.layers.global.tools.entries());
2963
+ for (const layer of layers) {
2964
+ if (layer === own) continue;
2965
+ for (const [name, definition] of layer.tools.entries()) inherited.set(name, definition);
2966
+ }
2967
+ const visible = /* @__PURE__ */ new Map();
2968
+ const knownNames = /* @__PURE__ */ new Set();
2969
+ const restrictableNames = /* @__PURE__ */ new Set();
2970
+ for (const [name, definition] of inherited) {
2971
+ knownNames.add(name);
2972
+ restrictableNames.add(name);
2973
+ if (layers.every((layer) => layer.admits(name))) visible.set(name, definition);
2974
+ }
2975
+ if (own !== void 0) for (const [name, definition] of own.tools.entries()) {
2976
+ knownNames.add(name);
2977
+ visible.set(name, definition);
2978
+ }
2979
+ if (this.modeFor(scope) !== "native") visible.set(RUN_CODE_NAME, this.requirePtcTransport());
2980
+ return {
2981
+ visible,
2982
+ knownNames,
2983
+ restrictableNames
2984
+ };
2985
+ }
2986
+ /**
2987
+ * Look up a tool as one scope sees it (scoped
2988
+ * shadows global; a restricted-away global reads as absent). Presenters pass
2989
+ * the calling agent so the rendered card matches the definition that
2990
+ * actually executed.
2991
+ * @param name - the tool name as registered.
2992
+ * @param scope - the viewing scope (the agent); omitted = the global view.
2993
+ * @returns the definition the scope resolves, or undefined when none is visible.
2994
+ */
2995
+ get(name, scope) {
2996
+ return this.view(scope).visible.get(name);
2997
+ }
2998
+ /**
2999
+ * Resolve the definition that MAY EXECUTE for a call, applying the mode
3000
+ * collapse at the operation boundary that owns it. The registry view
3001
+ * (`get`) is presentation-agnostic; here a MODEL-DIRECT call under `ptc`
3002
+ * may only name the reserved `run_code` transport, while a nested
3003
+ * sub-dispatch (a `parent` token set — the `run_code` SDK calling a tool
3004
+ * it bound) may call any visible tool. Denial surfaces as `UNKNOWN_TOOL`
3005
+ * through the executor, matching an absent definition.
3006
+ * @param name - the tool name as registered.
3007
+ * @param scope - the viewing scope (the agent); omitted = the global view.
3008
+ * @param nested - whether the call is a transport sub-dispatch, not a model-direct call.
3009
+ * @returns the definition that may run, or undefined when the call must be rejected.
3010
+ */
3011
+ resolveExecution(name, scope, nested) {
3012
+ const tool = this.get(name, scope);
3013
+ if (tool === void 0) return void 0;
3014
+ if (this.collapses(name, scope, nested)) return void 0;
3015
+ return tool;
3016
+ }
3017
+ /**
3018
+ * Project visible definitions onto the allowlisted model-facing schema fields,
3019
+ * excluding execution and presentation callbacks.
3020
+ * @param scope - the viewing scope (the agent); omitted = the global view.
3021
+ * @returns one deep-cloned schema per visible tool.
3022
+ */
3023
+ schemas(scope) {
3024
+ return [...this.view(scope).visible.values()].map((definition) => this.schemaOf(definition, true));
3025
+ }
3026
+ /** Project visible callable tools onto the generated PTC mode SDK contract. */
3027
+ sdkSchemas(scope) {
3028
+ return [...this.view(scope).visible.values()].filter((definition) => definition.name !== RUN_CODE_NAME).map((definition) => {
3029
+ const output = snapshotJsonValue(definition.output.schema);
3030
+ /* v8 ignore next -- registration already validated and retained this schema as lossless JSON. */
3031
+ if (output === void 0) throw new Error(`tool "${definition.name}" output schema must be lossless JSON before SDK projection`);
3032
+ return {
3033
+ ...this.schemaOf(definition, true),
3034
+ output
3035
+ };
3036
+ });
3037
+ }
3038
+ /** Project one definition onto the model-facing schema fields. */
3039
+ schemaOf(definition, detachParameters) {
3040
+ const { name, description, parameters, deferLoading } = definition;
3041
+ const detached = detachParameters ? snapshotJsonValue(parameters) : parameters;
3042
+ if (detached === void 0) throw new Error(`tool "${name}" parameters must be lossless JSON before schema projection`);
3043
+ return {
3044
+ name,
3045
+ description,
3046
+ parameters: detached,
3047
+ ...deferLoading === true ? { deferLoading } : {}
3048
+ };
3049
+ }
3050
+ /**
3051
+ * Classify a pending call through the caller's visible tool definition. Only
3052
+ * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or
3053
+ * throwing classifiers are exclusive.
3054
+ * @param exec - call name, parsed arguments, and optional agent scope.
3055
+ * @returns the fail-closed scheduling mode.
3056
+ */
3057
+ executionMode(exec) {
3058
+ const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== void 0);
3059
+ if (!tool?.isConcurrencySafe) return { kind: "exclusive" };
3060
+ try {
3061
+ return tool.isConcurrencySafe(exec.arguments) === true ? { kind: "parallel" } : { kind: "exclusive" };
3062
+ } catch {
3063
+ return { kind: "exclusive" };
3064
+ }
3065
+ }
3066
+ /**
3067
+ * Run the `tools/ptc-dispatch-log` waterfall over one settled sub-dispatch
3068
+ * and return the content the bridge should log on `tool/ptc-dispatch`.
3069
+ * Contained: when a listener throws, the method logs the original settled
3070
+ * content; that failure must not fail the dispatch or omit the settle event. Private:
3071
+ * the ONE consumer is the `run_code` bridge this registry constructs, which
3072
+ * receives it as a capability parameter (the `requireRuntime` idiom) — the
3073
+ * waterfall, not this invoker, is the public extension point.
3074
+ */
3075
+ async shapeDispatchLog(dispatch) {
3076
+ try {
3077
+ return await this.ctx.waterfall(scopeTarget(this, dispatch.agent), "tools/ptc-dispatch-log", dispatch, () => Promise.resolve(dispatch.content));
3078
+ } catch (error) {
3079
+ this.ctx.logger.warn(`tools: ptc-dispatch-log listener failed for ${dispatch.name}: ${errorMessage(error)}; logging the original settled content`);
3080
+ return dispatch.content;
3081
+ }
3082
+ }
3083
+ /**
3084
+ * Whether the `ptc` mode collapse denies a model-direct call: only the
3085
+ * reserved `run_code` transport may be named. Nested sub-dispatches (a
3086
+ * `parent` token set) bypass the collapse. One home for the
3087
+ * security-relevant predicate, shared by {@link resolveExecution} and
3088
+ * {@link createExecution} so the two can never drift apart.
3089
+ *
3090
+ * Resolved through {@link modeFor}, NOT `defaultMode`: an agent given `ptc`
3091
+ * by an agent preset under a native deployment is the composition
3092
+ * `qilin-agent-tool-presentation` exists for, and reading the deployment default would
3093
+ * leave exactly that agent uncollapsed — announcing one surface while
3094
+ * executing another, which is the bypass this collapse closes.
3095
+ * @param name - the tool name as registered.
3096
+ * @param scope - the viewing scope whose effective presentation mode applies.
3097
+ * @param nested - whether the call is a transport sub-dispatch, not a model-direct call.
3098
+ */
3099
+ collapses(name, scope, nested) {
3100
+ return !nested && this.modeFor(scope) === "ptc" && name !== "run_code";
3101
+ }
3102
+ /**
3103
+ * Execute through pre-policy, guards, around-dispatch, post-policy,
3104
+ * definition-owned content finalization, and final notification. Tool and
3105
+ * listener failures resolve as materialized error results; an invisible tool
3106
+ * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen
3107
+ * snapshot final observers receive. Cancellation
3108
+ * arriving after entry and before final result materialization skips a
3109
+ * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a
3110
+ * successful started outcome with `ABORTED`; already-started work is still
3111
+ * drained and may retain a tool-owned structured error.
3112
+ * @param exec - the typed same-process call input. The registry assigns its
3113
+ * correlation token before policy begins.
3114
+ * @returns the materialized final result.
3115
+ */
3116
+ async execute(exec) {
3117
+ return this.prepareExecution(exec, (prepared) => this.completeScheduledExecution(prepared));
3118
+ }
3119
+ async completeScheduledExecution(prepared) {
3120
+ switch (prepared.kind) {
3121
+ case "dispatch": {
3122
+ const dispatched = await this.dispatchScheduledExecution(prepared.exec);
3123
+ return dispatched.kind === "post-result" ? await this.finalizeScheduledExecution(prepared.exec, dispatched.result) : this.finishScheduledExecution(prepared.exec, dispatched.result);
3124
+ }
3125
+ case "post-result": return await this.finalizeScheduledExecution(prepared.exec, prepared.result);
3126
+ case "final-result": return this.finishScheduledExecution(prepared.exec, prepared.result);
3127
+ /* v8 ignore next -- closed-union exhaustiveness guard */
3128
+ default: return assertNever(prepared, "scheduled tool preparation");
3129
+ }
3130
+ }
3131
+ createExecution(exec) {
3132
+ const deferredContexts = [];
3133
+ const token = createExecutionToken();
3134
+ const callId = exec.callId;
3135
+ const rootCallId = exec.rootCallId ?? callId;
3136
+ const name = exec.name;
3137
+ const agent = exec.agent;
3138
+ const parent = exec.parent;
3139
+ const signal = exec.signal;
3140
+ const visible = this.get(name, agent);
3141
+ const collapsed = visible !== void 0 && this.collapses(name, agent, parent !== void 0);
3142
+ const concludingExecutions = this.concludingExecutions;
3143
+ const base = {
3144
+ token,
3145
+ callId,
3146
+ rootCallId,
3147
+ name,
3148
+ signal,
3149
+ ...agent !== void 0 ? { agent } : {},
3150
+ ...parent !== void 0 ? { parent } : {},
3151
+ ...exec.schema !== void 0 ? { schema: exec.schema } : {},
3152
+ deferContext(context) {
3153
+ deferredContexts.push(context);
3154
+ },
3155
+ concludeTurn() {
3156
+ concludingExecutions.add(this);
3157
+ }
3158
+ };
3159
+ const capturedFinalizer = visible?.finalizeContent?.bind(visible);
3160
+ const finalizerFor = () => collapsed && !signal.aborted ? void 0 : capturedFinalizer;
3161
+ try {
3162
+ const detached = snapshotJsonValue(exec.arguments);
3163
+ if (detached === void 0) throw new TypeError("tool execution arguments must be losslessly JSON-serializable");
3164
+ const execution = {
3165
+ ...base,
3166
+ arguments: deepFreeze(detached)
3167
+ };
3168
+ this.deferredContexts.set(execution, deferredContexts);
3169
+ this.contentFinalizers.set(execution, finalizerFor());
3170
+ this.cancellationStates.set(execution, {
3171
+ callerSignal: signal,
3172
+ bodyInvoked: false
3173
+ });
3174
+ if (collapsed) {
3175
+ if (signal.aborted) return {
3176
+ kind: "final-result",
3177
+ exec: execution,
3178
+ result: toolAbortedBeforeDispatchResult()
3179
+ };
3180
+ return {
3181
+ kind: "final-result",
3182
+ exec: execution,
3183
+ result: toolErrorResult(new ToolNotFoundError(name, `only \`${RUN_CODE_NAME}\` is callable directly — call \`${name}\` from inside a \`${RUN_CODE_NAME}\` program instead`))
3184
+ };
3185
+ }
3186
+ return {
3187
+ kind: "ready",
3188
+ exec: execution
3189
+ };
3190
+ } catch (error) {
3191
+ const execution = {
3192
+ ...base,
3193
+ arguments: void 0
3194
+ };
3195
+ this.contentFinalizers.set(execution, finalizerFor());
3196
+ return {
3197
+ kind: "final-result",
3198
+ exec: execution,
3199
+ result: toolErrorResult(error)
3200
+ };
3201
+ }
3202
+ }
3203
+ /**
3204
+ * Run the ordered pre-execute and monotonic guard stages for the scheduler.
3205
+ * @param input - the caller-supplied execution input.
3206
+ * @returns the prepared execution plus the next scheduler stage.
3207
+ * @internal
3208
+ */
3209
+ async prepareScheduledExecution(input) {
3210
+ return this.prepareExecution(input, (prepared) => prepared);
3211
+ }
3212
+ async prepareExecution(input, next) {
3213
+ const created = this.createExecution(input);
3214
+ if (created.kind !== "ready") return next(created);
3215
+ const exec = created.exec;
3216
+ if (this.callerCancelled(exec)) return next({
3217
+ kind: "final-result",
3218
+ exec,
3219
+ result: toolAbortedBeforeDispatchResult()
3220
+ });
3221
+ try {
3222
+ const carrier = scopeTarget(this, exec.agent);
3223
+ const gate = await this.ctx.waterfall(carrier, "tools/pre-execute", exec, () => Promise.resolve({ kind: "allow" }));
3224
+ const askResolution = gate.kind === "ask" ? await this.serviceAsk(exec, gate) : {
3225
+ decision: gate,
3226
+ approvalCancelled: false
3227
+ };
3228
+ const { decision } = askResolution;
3229
+ if (this.callerCancelled(exec) && askResolution.approvalCancelled) return await next({
3230
+ kind: "post-result",
3231
+ exec,
3232
+ result: toolAbortedBeforeDispatchResult()
3233
+ });
3234
+ if (decision.kind === "cancel") return await next({
3235
+ kind: "post-result",
3236
+ exec,
3237
+ result: toolAbortedBeforeDispatchResult()
3238
+ });
3239
+ const denialReason = decision.kind === "allow" ? this.guardReason(exec) : decision.reason;
3240
+ const denialInfo = decision.kind === "deny" ? decision.info : void 0;
3241
+ if (denialReason !== void 0) return await next({
3242
+ kind: "post-result",
3243
+ exec,
3244
+ result: this.materializeFinalResult({
3245
+ content: [{
3246
+ type: "text",
3247
+ text: `Error: ${denialReason}`
3248
+ }],
3249
+ isError: true,
3250
+ error: {
3251
+ message: denialReason,
3252
+ ...denialInfo === void 0 ? {} : { info: denialInfo }
3253
+ }
3254
+ })
3255
+ });
3256
+ if (this.callerCancelled(exec)) return await next({
3257
+ kind: "post-result",
3258
+ exec,
3259
+ result: toolAbortedBeforeDispatchResult()
3260
+ });
3261
+ return await next({
3262
+ kind: "dispatch",
3263
+ exec
3264
+ });
3265
+ } catch (error) {
3266
+ return next({
3267
+ kind: "final-result",
3268
+ exec,
3269
+ result: toolErrorResult(error)
3270
+ });
3271
+ }
3272
+ }
3273
+ /** Whether the original caller signal is currently aborted. */
3274
+ callerCancelled(exec) {
3275
+ const state = this.cancellationStates.get(exec);
3276
+ /* v8 ignore next -- only registry-minted executions reach the staged scheduler methods */
3277
+ if (state === void 0) throw new Error("tool registry scheduler invariant violated: missing cancellation state");
3278
+ return state.callerSignal.aborted;
3279
+ }
3280
+ /** Canonical cancellation outcome selected by whether the tool body started. */
3281
+ cancellationResult(exec, prior) {
3282
+ const state = this.cancellationStates.get(exec);
3283
+ /* v8 ignore next -- only registry-minted executions reach the staged scheduler methods */
3284
+ if (state === void 0) throw new Error("tool registry scheduler invariant violated: missing cancellation state");
3285
+ return state.bodyInvoked ? toolAbortedResult(prior) : toolAbortedBeforeDispatchResult(prior);
3286
+ }
3287
+ /**
3288
+ * Dispatch the registered body with the original caller signal fused back
3289
+ * into any around-wrapper replacement. Cancellation never abandons the body:
3290
+ * a started promise reaches quiescence before its outcome becomes `ABORTED`.
3291
+ */
3292
+ async dispatchToolBody(exec) {
3293
+ const state = this.cancellationStates.get(exec);
3294
+ /* v8 ignore next -- only registry-minted executions reach the staged scheduler methods */
3295
+ if (state === void 0) throw new Error("tool registry scheduler invariant violated: missing cancellation state");
3296
+ const wrapperSignal = exec.signal;
3297
+ const fused = fuseToolSignals(state.callerSignal, wrapperSignal);
3298
+ const signal = fused.signal;
3299
+ if (isAborted(signal)) {
3300
+ fused.dispose();
3301
+ return toolAbortedBeforeDispatchResult();
3302
+ }
3303
+ exec.signal = signal;
3304
+ try {
3305
+ const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== void 0);
3306
+ if (!tool) throw new ToolNotFoundError(exec.name);
3307
+ state.bodyInvoked = true;
3308
+ const returned = await tool.execute(exec.arguments, exec);
3309
+ const result = this.createSuccessResult(exec, tool, returned);
3310
+ return isAborted(signal) ? toolAbortedResult(result) : result;
3311
+ } catch (error) {
3312
+ return toolErrorResult(error);
3313
+ } finally {
3314
+ fused.dispose();
3315
+ exec.signal = wrapperSignal;
3316
+ }
3317
+ }
3318
+ /**
3319
+ * Run around-dispatch and the tool body. Tool and unknown-tool failures still
3320
+ * receive post-execute; pipeline failures are already final.
3321
+ * @param exec - the prepared execution.
3322
+ * @returns whether the result still needs post-execute.
3323
+ * @internal
3324
+ */
3325
+ async dispatchScheduledExecution(exec) {
3326
+ try {
3327
+ const mutableExec = exec;
3328
+ const carrier = scopeTarget(this, exec.agent);
3329
+ const result = await this.ctx.waterfall(carrier, "tools/execute", mutableExec, () => this.dispatchToolBody(mutableExec));
3330
+ const normalized = this.normalizeDispatchResult(exec, result);
3331
+ const deferredContexts = this.deferredContexts.get(exec);
3332
+ /* v8 ignore next -- dispatch only receives executions minted by this registry's prepare stage */
3333
+ if (deferredContexts === void 0) throw new Error("tool registry scheduler invariant violated: unprepared execution");
3334
+ const resultWithDeferredContexts = deferredContexts.length === 0 ? normalized : this.markCanonical(exec, {
3335
+ ...normalized,
3336
+ additionalContexts: [...deferredContexts, ...normalized.additionalContexts ?? []]
3337
+ });
3338
+ return {
3339
+ kind: "post-result",
3340
+ result: this.callerCancelled(exec) && !resultWithDeferredContexts.isError ? this.cancellationResult(exec, resultWithDeferredContexts) : resultWithDeferredContexts
3341
+ };
3342
+ } catch (error) {
3343
+ return {
3344
+ kind: "final-result",
3345
+ result: toolErrorResult(error)
3346
+ };
3347
+ }
3348
+ }
3349
+ /**
3350
+ * Run ordered post-execute, then apply definition-owned content finalization,
3351
+ * materialize, and notify the final outcome.
3352
+ * @param exec - the prepared execution.
3353
+ * @param result - dispatch/pre result that still needs post-execute.
3354
+ * @returns the materialized final result.
3355
+ * @internal
3356
+ */
3357
+ async finalizeScheduledExecution(exec, result) {
3358
+ try {
3359
+ const postResult = await this.postExecute(exec, result);
3360
+ return this.finishScheduledExecution(exec, this.callerCancelled(exec) && !postResult.isError ? this.cancellationResult(exec, postResult) : postResult);
3361
+ } catch (error) {
3362
+ return this.finishScheduledExecution(exec, toolErrorResult(error));
3363
+ }
3364
+ }
3365
+ /**
3366
+ * Materialize the candidate, apply definition-owned content finalization,
3367
+ * then materialize and notify the authoritative result.
3368
+ * @param exec - the prepared execution.
3369
+ * @param result - final result.
3370
+ * @returns the materialized final result.
3371
+ * @internal
3372
+ */
3373
+ finishScheduledExecution(exec, result) {
3374
+ let materializedResult;
3375
+ try {
3376
+ materializedResult = this.materializeFinalResult(result);
3377
+ } catch (error) {
3378
+ materializedResult = this.materializeFinalResult(toolErrorResult(error));
3379
+ }
3380
+ let finalResult;
3381
+ try {
3382
+ finalResult = this.materializeFinalResult(this.applyFinalContent(exec, materializedResult));
3383
+ } catch (error) {
3384
+ finalResult = this.materializeFinalResult(toolErrorResult(error));
3385
+ }
3386
+ this.notifyResult(exec, finalResult);
3387
+ return finalResult;
3388
+ }
3389
+ /** Apply the snapshotted tool-owned content transform without exposing other result fields. */
3390
+ applyFinalContent(exec, result) {
3391
+ const finalizeContent = this.contentFinalizers.get(exec);
3392
+ if (finalizeContent === void 0) return result;
3393
+ const content = finalizeContent(exec, result);
3394
+ return content === void 0 ? result : {
3395
+ ...result,
3396
+ content
3397
+ };
3398
+ }
3399
+ /** Notify observers without exposing a mutation or error channel into the outcome. */
3400
+ notifyResult(exec, result) {
3401
+ Object.freeze(exec);
3402
+ const { name: toolName, callId } = exec;
3403
+ const reportFailure = (error) => {
3404
+ this.ctx.logger.warn(`tool "${toolName}" (${callId}): tools/result observer failed: ${errorMessage(error)}`);
3405
+ };
3406
+ const callbacks = this.ctx.events.dispatch("emit", [
3407
+ scopeTarget(this, exec.agent),
3408
+ "tools/result",
3409
+ exec,
3410
+ result
3411
+ ]);
3412
+ for (const callback of callbacks) try {
3413
+ const returned = callback(exec, result);
3414
+ Promise.resolve(returned).catch(reportFailure);
3415
+ } catch (error) {
3416
+ reportFailure(error);
3417
+ }
3418
+ }
3419
+ /**
3420
+ * Resolve an `ask` decision to allow/deny through the approval seam. The
3421
+ * seam is consumed opportunistically with `ctx.get('approval')` — a
3422
+ * deployment that composes no ApprovalService keeps the historical degrade
3423
+ * to deny, and an unmount mid-session degrades the same way on the next ask.
3424
+ * An agent-less execution also degrades: without an agent there is no
3425
+ * session to audit to and no UI to route to. Otherwise the outcome maps
3426
+ * one-to-one — `allowed-once` proceeds; the three non-grants deny with
3427
+ * distinct reasons so the model can tell a human "no" from an absent
3428
+ * approval channel.
3429
+ */
3430
+ async serviceAsk(exec, ask) {
3431
+ const approval = this.ctx.get("approval");
3432
+ if (approval === void 0) return {
3433
+ decision: {
3434
+ kind: "deny",
3435
+ reason: ask.reason ?? `tool "${exec.name}" requires approval (not yet supported)`
3436
+ },
3437
+ approvalCancelled: false
3438
+ };
3439
+ if (exec.agent === void 0) return {
3440
+ decision: {
3441
+ kind: "deny",
3442
+ reason: `tool "${exec.name}" requires approval, but the call has no agent to route it through`
3443
+ },
3444
+ approvalCancelled: false
3445
+ };
3446
+ const outcome = await approval.request({
3447
+ agent: exec.agent,
3448
+ toolName: exec.name,
3449
+ callId: exec.callId,
3450
+ ...ask.reason !== void 0 ? { reason: ask.reason } : {},
3451
+ ...ask.displayReason !== void 0 ? { displayReason: ask.displayReason } : {},
3452
+ signal: exec.signal
3453
+ });
3454
+ switch (outcome) {
3455
+ case "allowed-once": return {
3456
+ decision: { kind: "allow" },
3457
+ approvalCancelled: false
3458
+ };
3459
+ case "rejected": return {
3460
+ decision: {
3461
+ kind: "deny",
3462
+ reason: `the user rejected tool "${exec.name}"`
3463
+ },
3464
+ approvalCancelled: false
3465
+ };
3466
+ case "cancelled": return {
3467
+ decision: {
3468
+ kind: "deny",
3469
+ reason: `approval for tool "${exec.name}" was cancelled`
3470
+ },
3471
+ approvalCancelled: true
3472
+ };
3473
+ case "unavailable": return {
3474
+ decision: {
3475
+ kind: "deny",
3476
+ reason: `tool "${exec.name}" requires approval, but no approval channel is available`
3477
+ },
3478
+ approvalCancelled: false
3479
+ };
3480
+ default: return assertNever(outcome, "ApprovalOutcome");
3481
+ }
3482
+ }
3483
+ /**
3484
+ * Run the `tools/post-execute` waterfall over a dispatched `result` and apply
3485
+ * its {@link PostToolDecision}: `accept` keeps the call successful (replacing
3486
+ * `content` when given), `block` turns it into an `isError` whose content is
3487
+ * the corrective `feedback`. Either decision may attach `additionalContexts`,
3488
+ * which are ferried on the returned result for the loop's active-batch FIFO.
3489
+ * Context deferred by the tool body survives an accepted result but is
3490
+ * discarded when the outer call is blocked; a block exposes only context the
3491
+ * blocking decision explicitly supplied.
3492
+ * Runs inside `execute`'s outer try/catch (a throwing listener → isError).
3493
+ */
3494
+ async postExecute(exec, result) {
3495
+ const decision = await this.ctx.waterfall(scopeTarget(this, exec.agent), "tools/post-execute", exec, result, () => Promise.resolve({ kind: "accept" }));
3496
+ const decisionContexts = decision.additionalContexts ?? [];
3497
+ if (decision.kind === "block") {
3498
+ const message = failureMessageFromContent(decision.feedback);
3499
+ return this.markCanonical(exec, {
3500
+ content: decision.feedback,
3501
+ isError: true,
3502
+ error: { message },
3503
+ ...decisionContexts.length > 0 ? { additionalContexts: decisionContexts } : {}
3504
+ });
3505
+ }
3506
+ if (Object.hasOwn(decision, "content") && Object.hasOwn(decision, "value")) throw new TypeError("tools/post-execute accept decision cannot replace both value and content");
3507
+ const additionalContexts = [...result.additionalContexts ?? [], ...decisionContexts];
3508
+ if (Object.hasOwn(decision, "value")) {
3509
+ if (result.isError) throw new TypeError("tools/post-execute cannot replace the value of a failed result");
3510
+ const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== void 0);
3511
+ if (tool === void 0) throw new ToolNotFoundError(exec.name);
3512
+ const replaced = this.createSuccessResult(exec, tool, decision.value);
3513
+ return this.markCanonical(exec, {
3514
+ ...replaced,
3515
+ ...additionalContexts.length > 0 ? { additionalContexts } : {}
3516
+ });
3517
+ }
3518
+ return this.markCanonical(exec, {
3519
+ ...result,
3520
+ ...decision.content !== void 0 ? { content: decision.content } : {},
3521
+ ...additionalContexts.length > 0 ? { additionalContexts } : {}
3522
+ });
3523
+ }
3524
+ /** Registry-normalized results and the exact dispatch that validated each value. */
3525
+ canonicalResults = /* @__PURE__ */ new WeakMap();
3526
+ /** Mark one registry-normalized result as canonical only for its owning dispatch. */
3527
+ markCanonical(exec, result) {
3528
+ this.canonicalResults.set(result, exec.token);
3529
+ return result;
3530
+ }
3531
+ /** Snapshot, validate, render, and optionally project one successful body value. */
3532
+ createSuccessResult(exec, tool, candidate) {
3533
+ const detached = snapshotToolValue(tool.name, candidate);
3534
+ const violations = validateJsonSchemaValue(tool.output.schema, detached, "value");
3535
+ if (violations.length > 0) throw new ToolOutputError(tool.name, violations);
3536
+ const value = deepFreeze(detached);
3537
+ let rendered;
3538
+ try {
3539
+ rendered = tool.output.render(exec.arguments, value);
3540
+ } catch (error) {
3541
+ throw projectionError(tool.name, "render", error);
3542
+ }
3543
+ const content = snapshotProjection(tool.name, "render", rendered);
3544
+ let meta;
3545
+ if (exec.parent === void 0 && tool.output.presentationMeta !== void 0) {
3546
+ let projected;
3547
+ try {
3548
+ projected = tool.output.presentationMeta(exec.arguments, value);
3549
+ } catch (error) {
3550
+ throw projectionError(tool.name, "presentationMeta", error);
3551
+ }
3552
+ meta = snapshotProjection(tool.name, "presentationMeta", projected);
3553
+ }
3554
+ const concludesTurn = this.concludingExecutions.has(exec);
3555
+ return this.markCanonical(exec, this.materializeFinalResult({
3556
+ isError: false,
3557
+ value,
3558
+ content,
3559
+ ...meta !== void 0 ? { meta } : {},
3560
+ ...concludesTurn ? { concludesTurn: true } : {}
3561
+ }));
3562
+ }
3563
+ /** Normalize an around-dispatch wrapper's authored result through the owning output contract. */
3564
+ normalizeDispatchResult(exec, result) {
3565
+ if (this.canonicalResults.get(result) === exec.token) return result;
3566
+ if (result.isError) return this.markCanonical(exec, {
3567
+ isError: true,
3568
+ error: result.error,
3569
+ content: result.content,
3570
+ ...result.meta !== void 0 ? { meta: result.meta } : {},
3571
+ ...result.additionalContexts !== void 0 ? { additionalContexts: result.additionalContexts } : {}
3572
+ });
3573
+ const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== void 0);
3574
+ if (tool === void 0) throw new ToolNotFoundError(exec.name);
3575
+ const normalized = this.createSuccessResult(exec, tool, result.value);
3576
+ return this.markCanonical(exec, {
3577
+ ...normalized,
3578
+ ...result.additionalContexts !== void 0 ? { additionalContexts: result.additionalContexts } : {}
3579
+ });
3580
+ }
3581
+ /** Materialize the authoritative commit outcome once, immediately before `tools/result`. */
3582
+ materializeFinalResult(result) {
3583
+ const presentation = {
3584
+ content: result.content,
3585
+ ...result.meta !== void 0 ? { meta: result.meta } : {},
3586
+ ...result.additionalContexts !== void 0 ? { additionalContexts: result.additionalContexts } : {}
3587
+ };
3588
+ if (result.isError) return materializePresentation({
3589
+ isError: true,
3590
+ error: result.error,
3591
+ ...presentation
3592
+ });
3593
+ return deepFreeze({
3594
+ ...materializePresentation({
3595
+ isError: false,
3596
+ ...presentation,
3597
+ ...result.concludesTurn === true ? { concludesTurn: true } : {}
3598
+ }),
3599
+ value: result.value
3600
+ });
3601
+ }
3602
+ };
3603
+ /** Mint a same-process correlation token whose identity is its value. */
3604
+ function createExecutionToken() {
3605
+ return Symbol("qilin.tool.execution");
3606
+ }
3607
+ function toolErrorResult(error) {
3608
+ const info = errorInfo(error);
3609
+ const message = errorMessage(error);
3610
+ return {
3611
+ content: [{
3612
+ type: "text",
3613
+ text: `Error: ${message}`
3614
+ }],
3615
+ isError: true,
3616
+ error: {
3617
+ message,
3618
+ ...info ? { info } : {}
3619
+ }
3620
+ };
3621
+ }
3622
+ /** Read live abort state across an await without treating it as synchronously immutable. */
3623
+ function isAborted(signal) {
3624
+ return signal.aborted;
3625
+ }
3626
+ /**
3627
+ * Fuse caller and wrapper cancellation without nesting `AbortSignal.any`.
3628
+ * Keeping the relay dispatch-scoped also removes listeners when work settles.
3629
+ */
3630
+ function fuseToolSignals(caller, wrapper) {
3631
+ if (caller === wrapper) return {
3632
+ signal: caller,
3633
+ dispose() {}
3634
+ };
3635
+ const controller = new AbortController();
3636
+ let listening = false;
3637
+ const dispose = () => {
3638
+ if (!listening) return;
3639
+ listening = false;
3640
+ caller.removeEventListener("abort", abortFromCaller);
3641
+ wrapper.removeEventListener("abort", abortFromWrapper);
3642
+ };
3643
+ const abortFrom = (source) => {
3644
+ const reason = source.reason;
3645
+ controller.abort(reason);
3646
+ dispose();
3647
+ };
3648
+ const abortFromCaller = () => {
3649
+ abortFrom(caller);
3650
+ };
3651
+ const abortFromWrapper = () => {
3652
+ abortFrom(wrapper);
3653
+ };
3654
+ if (wrapper.aborted) abortFromWrapper();
3655
+ else if (caller.aborted) abortFromCaller();
3656
+ else {
3657
+ listening = true;
3658
+ caller.addEventListener("abort", abortFromCaller, { once: true });
3659
+ wrapper.addEventListener("abort", abortFromWrapper, { once: true });
3660
+ }
3661
+ return {
3662
+ signal: controller.signal,
3663
+ dispose
3664
+ };
3665
+ }
3666
+ /** Canonical result when cancellation supersedes success after body invocation. */
3667
+ function toolAbortedResult(prior) {
3668
+ const additionalContexts = prior?.additionalContexts ?? [];
3669
+ return {
3670
+ content: [{
3671
+ type: "text",
3672
+ text: "Error: tool call aborted"
3673
+ }],
3674
+ isError: true,
3675
+ error: {
3676
+ message: "tool call aborted",
3677
+ info: {
3678
+ name: "AbortError",
3679
+ code: TOOL_ABORTED
3680
+ }
3681
+ },
3682
+ ...additionalContexts.length > 0 ? { additionalContexts } : {}
3683
+ };
3684
+ }
3685
+ /** Canonical result when cancellation prevents tool body invocation. */
3686
+ function toolAbortedBeforeDispatchResult(prior) {
3687
+ const additionalContexts = prior?.additionalContexts ?? [];
3688
+ return {
3689
+ content: [{
3690
+ type: "text",
3691
+ text: "Error: tool call aborted before dispatch"
3692
+ }],
3693
+ isError: true,
3694
+ error: {
3695
+ message: "tool call aborted before dispatch",
3696
+ info: {
3697
+ name: "AbortError",
3698
+ code: TOOL_ABORTED_BEFORE_DISPATCH
3699
+ }
3700
+ },
3701
+ ...additionalContexts.length > 0 ? { additionalContexts } : {}
3702
+ };
3703
+ }
3704
+ //#endregion
3705
+ export { CodeRunFailedError, JsonSchemaError, RUN_CODE_NAME, TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH, TOOL_RUNTIME_SCHEDULER, ToolArgsError, ToolNotFoundError, ToolOutputError, ToolRuntime, ToolRuntime as default, assertObjectJsonSchema, assertSupportedJsonSchema, defineContentToolFixture, defineTool, jsonSchemaToPy, jsonSchemaToTs, parameterSchemaSpecToJsonSchema, renderToolsSdk, renderToolsSdkPy, validateArgs, validateJsonSchemaValue, valueSchemaSpecToJsonSchema };