@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.
@@ -0,0 +1,660 @@
1
+ /**
2
+ * PTC mode `run_code` transport. Programs call the registry's agent-visible
3
+ * tools through nested executions scheduled under the native concurrency
4
+ * contract; each sub-dispatch is logged for reconstruction, while only the
5
+ * outer curated result enters model history.
6
+ * @module @qilin-agent/tools/src/ptc
7
+ */
8
+ import { brandString } from '@qilin-agent/brand';
9
+ import { createUserMessage, HarnessError } from '@qilin-agent/llm';
10
+ import { approveEscalation, ESCALATION_TARGETS, validateEscalationArgs } from '@qilin-agent/sandbox';
11
+ import { deepFreeze, snapshotJsonValue } from '@qilin-agent/util-values';
12
+ import { defineTool, parameterSchemaSpecToJsonSchema } from "./schema.js";
13
+ import { TOOL_RUNTIME_SCHEDULER } from "./index.js";
14
+ /** The model-facing name of the PTC mode tool. */
15
+ export const RUN_CODE_NAME = 'run_code';
16
+ /**
17
+ * The TypeScript flavor: the fallback for a schema read with no runtime
18
+ * mounted ({@link resolveFlavor} owns which readers reach that). A real
19
+ * assembly always resolves a runtime first, so the model never sees this
20
+ * fallback outside its own language.
21
+ */
22
+ const TYPESCRIPT_FLAVOR = {
23
+ description: 'Execute a TypeScript program against the available tools. Takes two required '
24
+ + 'arguments: `description`, a short summary of what the program does, and `code`, '
25
+ + 'the BODY of an async function (erasable syntax only; top-level `await` and '
26
+ + '`return` work). Call tools as `await tools.name(args)` per the declarations in the system '
27
+ + 'prompt. Only what you print or return is program output — curate it. Image-bearing '
28
+ + 'subtool results are attached after the run.',
29
+ codeDescription: 'The program: the body of an async TypeScript function.',
30
+ };
31
+ /**
32
+ * The Python flavor: the body of an async function, top-level `await` and
33
+ * `return`, answer via `print` and/or the returned value, matching
34
+ * {@link ./py-types.ts}'s SDK instructions.
35
+ */
36
+ const PYTHON_FLAVOR = {
37
+ description: 'Execute a Python program against the available tools. Takes two required '
38
+ + 'arguments: `description`, a short summary of what the program does, and `code`, '
39
+ + 'the BODY of an async function (top-level `await` and `return` work). Call tools as '
40
+ + '`await tools.name(args)` per the declarations in the system prompt. Use '
41
+ + '`print(...)` and/or `return <value>` for program output — curate it. Image-bearing '
42
+ + 'subtool results are attached after the run.',
43
+ codeDescription: 'The program: the body of an async Python function.',
44
+ };
45
+ /** Per-language `run_code` schema flavors (see {@link RunCodeFlavor}); one entry per {@link PtcSdkLanguage}. */
46
+ const RUN_CODE_FLAVORS = {
47
+ typescript: TYPESCRIPT_FLAVOR,
48
+ python: PYTHON_FLAVOR,
49
+ };
50
+ /**
51
+ * The `description` parameter's model-facing description: language-independent
52
+ * (the UI label contract is the same for every runtime), shared between the
53
+ * static spec and the language-aware `parameters` getter so the two emissions
54
+ * can never drift.
55
+ */
56
+ const RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION = 'Clear, concise description of what this program does in active voice, '
57
+ + '5-10 words (shown in the UI). Provide `description` before `code` in the arguments. '
58
+ + 'Examples: "Count TODO markers across packages"; '
59
+ + '"Read failing test and its fixture"; "Rename config key in every cordis.yml".';
60
+ const RUN_CODE_CONTROLS = {
61
+ timeoutMs: { type: 'number', description: 'Positive elapsed-time budget in milliseconds, capped by the deployment maximum.' },
62
+ sandbox_permissions: { type: 'string', enum: [...ESCALATION_TARGETS], description: 'Wider sandbox mode for this complete program execution; requires justification and approval.' },
63
+ justification: { type: 'string', description: 'Reason this complete program needs wider access, shown to the user for approval.' },
64
+ };
65
+ function controlParameters(runtime) {
66
+ // Catalog readers have no mounted runtime; real model assembly requires one.
67
+ if (runtime === undefined)
68
+ return RUN_CODE_CONTROLS;
69
+ return {
70
+ ...runtime.timeout === undefined ? {} : {
71
+ timeoutMs: { ...RUN_CODE_CONTROLS.timeoutMs,
72
+ 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.` },
73
+ },
74
+ ...runtime.sandboxMode === undefined ? {} : {
75
+ sandbox_permissions: RUN_CODE_CONTROLS.sandbox_permissions,
76
+ justification: RUN_CODE_CONTROLS.justification,
77
+ },
78
+ };
79
+ }
80
+ function escalationGuidance(runtime) {
81
+ return runtime?.sandboxMode === undefined ? ''
82
+ : ' 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.';
83
+ }
84
+ /**
85
+ * Resolve the {@link RunCodeFlavor} for the loaded runtime's language, read at
86
+ * schema-emission time so the model-visible `run_code` schema always matches
87
+ * the SDK section's language. `peekRuntime` returns `undefined` only when no
88
+ * runtime is mounted, which reaches this function through definition readers
89
+ * and `schemas()` — the doc-catalog harvest is the only shipped one, and none
90
+ * of them feeds a model, because `wireSchemas` calls `requirePtcRuntime`
91
+ * before projecting — so that path degrades to {@link TYPESCRIPT_FLAVOR}. A
92
+ * mounted runtime whose language has no flavor entry fails loud, exactly as
93
+ * `requirePtcRuntime` rejects it at assembly. Keeping this table in step with
94
+ * `SDK_RENDERERS` is the compiler's job ({@link PtcSdkLanguage}); what this
95
+ * guard owns is the runtime-supplied language neither table knows, which never
96
+ * yields a wrong-language schema for a real runtime.
97
+ */
98
+ function resolveFlavor(peekRuntime) {
99
+ const runtime = peekRuntime();
100
+ if (runtime === undefined) {
101
+ // No runtime mounted: reached by definition readers and `schemas()`, of
102
+ // which the doc-catalog harvest is the only shipped one. None feeds a
103
+ // model — `wireSchemas` calls `requirePtcRuntime` before projecting, so
104
+ // the assembly path never arrives here. Degrade to the TS default.
105
+ return TYPESCRIPT_FLAVOR;
106
+ }
107
+ // Own-property read: a language like `toString`/`constructor` would otherwise
108
+ // resolve an inherited Object.prototype member as a flavor.
109
+ const flavor = RUN_CODE_FLAVORS[runtime.language];
110
+ if (!Object.hasOwn(RUN_CODE_FLAVORS, runtime.language) || flavor === undefined) {
111
+ const known = Object.keys(RUN_CODE_FLAVORS).map(name => JSON.stringify(name)).join(', ');
112
+ throw new Error(`qilin-tools: no run_code schema flavor registered for runtime language ${JSON.stringify(runtime.language)} (known: ${known})`);
113
+ }
114
+ return flavor;
115
+ }
116
+ /**
117
+ * Thrown by `run_code` when the program run itself failed — a program
118
+ * exception, a budget expiry, an abort, or substrate death. Extends
119
+ * {@link HarnessError} (`code: 'CODE_RUN_FAILED'`); the registry's execution
120
+ * pipeline converts it into a structured `isError` result whose text carries
121
+ * the failure kind plus the captured logs, so the model can self-correct.
122
+ */
123
+ export class CodeRunFailedError extends HarnessError {
124
+ constructor(message) {
125
+ super(message, 'CODE_RUN_FAILED');
126
+ this.name = 'CodeRunFailedError';
127
+ }
128
+ }
129
+ /**
130
+ * Snapshot one binding call's argument as lossless JSON, then snapshot that
131
+ * detached value again so dispatch and logging stay independent without
132
+ * reintroducing structured-clone's platform-specific nesting limit.
133
+ */
134
+ function jsonNormalizeArgs(value) {
135
+ let snapshot;
136
+ try {
137
+ snapshot = snapshotJsonValue(value);
138
+ }
139
+ catch (error) {
140
+ throw new Error(`tool arguments must be lossless JSON: ${error instanceof Error ? error.message : String(error)}`);
141
+ }
142
+ if (snapshot === undefined) {
143
+ throw new Error('tool arguments must be lossless JSON (call the tool with an arguments object, e.g. `{}`)');
144
+ }
145
+ const logged = snapshotJsonValue(snapshot);
146
+ /* v8 ignore next -- snapshot is already a detached lossless JSON value. */
147
+ if (logged === undefined) {
148
+ throw new Error('tool arguments could not be detached for durable logging');
149
+ }
150
+ return { dispatched: snapshot, logged };
151
+ }
152
+ /** Two-space JSON presentation, matching the existing shallow `run_code` text contract. */
153
+ const JSON_INDENT = ' ';
154
+ /**
155
+ * ECMAScript caps `JSON.stringify`'s `space` string at ten characters. The
156
+ * renderer also caps TOTAL indentation there, compacting deeper subtrees, so
157
+ * formatted output remains linear in the canonical JSON size.
158
+ */
159
+ const MAX_JSON_INDENT_CHARS = 10;
160
+ /** Render one non-string JSON root without recursive traversal or unbounded indentation growth. */
161
+ function renderJsonValue(value) {
162
+ const chunks = [];
163
+ const tasks = [{ kind: 'value', value, depth: 0, compact: false }];
164
+ for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
165
+ if (task.kind === 'text') {
166
+ chunks.push(task.text);
167
+ continue;
168
+ }
169
+ const current = task.value;
170
+ if (current === null || typeof current === 'boolean' || typeof current === 'number') {
171
+ chunks.push(String(current));
172
+ continue;
173
+ }
174
+ if (typeof current === 'string') {
175
+ chunks.push(JSON.stringify(current));
176
+ continue;
177
+ }
178
+ const compact = task.compact || (task.depth + 1) * JSON_INDENT.length > MAX_JSON_INDENT_CHARS;
179
+ const childDepth = task.depth + 1;
180
+ if (Array.isArray(current)) {
181
+ chunks.push('[');
182
+ if (current.length === 0) {
183
+ chunks.push(']');
184
+ continue;
185
+ }
186
+ tasks.push({ kind: 'text', text: compact ? ']' : `\n${JSON_INDENT.repeat(task.depth)}]` });
187
+ for (let index = current.length - 1; index >= 0; index--) {
188
+ const item = current[index];
189
+ /* v8 ignore next -- canonical JsonValue arrays are dense. */
190
+ if (item === undefined)
191
+ throw new Error('cannot render a sparse JSON array');
192
+ tasks.push({ kind: 'value', value: item, depth: childDepth, compact });
193
+ tasks.push({
194
+ kind: 'text',
195
+ text: compact
196
+ ? index === 0 ? '' : ','
197
+ : `${index === 0 ? '\n' : ',\n'}${JSON_INDENT.repeat(childDepth)}`,
198
+ });
199
+ }
200
+ continue;
201
+ }
202
+ const keys = Object.keys(current);
203
+ chunks.push('{');
204
+ if (keys.length === 0) {
205
+ chunks.push('}');
206
+ continue;
207
+ }
208
+ tasks.push({ kind: 'text', text: compact ? '}' : `\n${JSON_INDENT.repeat(task.depth)}}` });
209
+ for (let index = keys.length - 1; index >= 0; index--) {
210
+ const key = keys[index];
211
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
212
+ if (key === undefined)
213
+ throw new Error('cannot render a missing JSON object key');
214
+ const item = current[key];
215
+ /* v8 ignore next -- canonical JsonValue records contain no undefined properties. */
216
+ if (item === undefined)
217
+ throw new Error('cannot render an undefined JSON object property');
218
+ tasks.push({ kind: 'value', value: item, depth: childDepth, compact });
219
+ tasks.push({
220
+ kind: 'text',
221
+ text: compact
222
+ ? `${index === 0 ? '' : ','}${JSON.stringify(key)}:`
223
+ : `${index === 0 ? '\n' : ',\n'}${JSON_INDENT.repeat(childDepth)}${JSON.stringify(key)}: `,
224
+ });
225
+ }
226
+ }
227
+ return chunks.join('');
228
+ }
229
+ /** Render one present program completion value for the model-facing result text. */
230
+ function renderValue(value) {
231
+ return typeof value === 'string' ? value : renderJsonValue(value);
232
+ }
233
+ /**
234
+ * Build the `run_code` {@link ToolDefinition}: required `description` and
235
+ * `code` parameters, executed through the dispatch bridge described
236
+ * above. The
237
+ * registry reserves it as presentation infrastructure under non-native modes,
238
+ * outside the filterable global/scoped capability layers.
239
+ * @param registry - the owning registry (sub-calls go through its `execute`,
240
+ * bindings cover its registered tools).
241
+ * @param options - the registry-private capabilities described above.
242
+ * @returns the registry-ready definition.
243
+ */
244
+ export function createRunCodeTool(registry, options) {
245
+ const { requireRuntime, peekRuntime, maxParallel, shapeDispatchLog } = options;
246
+ const definition = defineTool({
247
+ name: RUN_CODE_NAME,
248
+ // The description and `code` parameter description are placeholders here:
249
+ // the language-aware getters installed below replace both, resolving the
250
+ // loaded runtime's flavor at schema-emission time so the schema the MODEL
251
+ // sees matches the SDK section's language. Argument VALIDATION still keys
252
+ // off this static spec (defineTool closes over it), which is language-
253
+ // independent (one required string `code`).
254
+ description: TYPESCRIPT_FLAVOR.description,
255
+ parameters: {
256
+ description: {
257
+ type: 'string',
258
+ required: true,
259
+ description: RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION,
260
+ },
261
+ code: { type: 'string', required: true, description: TYPESCRIPT_FLAVOR.codeDescription },
262
+ ...RUN_CODE_CONTROLS,
263
+ },
264
+ output: {
265
+ schema: {
266
+ type: 'object',
267
+ additionalProperties: false,
268
+ properties: {
269
+ logs: { type: 'array', required: true, items: { type: 'string' } },
270
+ result: { type: 'json' },
271
+ sandbox: {
272
+ type: 'object',
273
+ additionalProperties: false,
274
+ properties: {
275
+ mode: { type: 'string', required: true, enum: ['read-only', 'workspace-write', 'danger-full-access'] },
276
+ denied: { type: 'boolean', required: true },
277
+ enforcement: { type: 'string', enum: ['full', 'partial'] },
278
+ },
279
+ },
280
+ },
281
+ },
282
+ render: (_args, value) => {
283
+ const rendered = value.result === undefined ? '' : renderValue(value.result);
284
+ const parts = [value.logs.join('\n'), rendered].filter(part => part.length > 0);
285
+ if (value.sandbox?.enforcement === 'partial')
286
+ parts.push('File sandbox enforcement is partial on this host.');
287
+ if (value.sandbox?.denied)
288
+ parts.push(`The ${value.sandbox.mode} file sandbox denied an operation.${escalationGuidance(peekRuntime())}`);
289
+ return [{ type: 'text', text: parts.length > 0 ? parts.join('\n') : '(run_code completed with no output)' }];
290
+ },
291
+ },
292
+ async execute(args, exec) {
293
+ if (args.description.trim().length === 0) {
294
+ throw new Error('invalid description: expected a non-empty string');
295
+ }
296
+ const runtime = requireRuntime();
297
+ validateEscalationArgs(args.sandbox_permissions, args.justification);
298
+ if (args.timeoutMs !== undefined && runtime.timeout === undefined) {
299
+ throw new Error('timeoutMs is not available for this PTC runtime');
300
+ }
301
+ if (args.timeoutMs !== undefined && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) {
302
+ throw new Error('invalid timeoutMs: expected a positive finite number');
303
+ }
304
+ const standingPolicy = runtime.sandboxMode === undefined ? undefined : options.resolveSandboxPolicy(exec);
305
+ let policy = standingPolicy;
306
+ if (args.sandbox_permissions !== undefined && args.justification !== undefined) {
307
+ if (standingPolicy === undefined)
308
+ throw new Error('sandbox_permissions is not available for this PTC runtime');
309
+ const approvedMode = await approveEscalation({
310
+ requestedMode: args.sandbox_permissions,
311
+ justification: args.justification,
312
+ effectiveMode: standingPolicy.mode,
313
+ subject: 'program',
314
+ }, {
315
+ approver: options.peekApprover(), agent: exec.agent, callId: exec.callId,
316
+ toolName: RUN_CODE_NAME, signal: exec.signal,
317
+ });
318
+ policy = { ...standingPolicy, mode: approvedMode };
319
+ }
320
+ exec.signal.throwIfAborted();
321
+ // The run-scoped abort: follows the outer signal in, and fires when the
322
+ // run settles for ANY reason, so an in-flight sub-dispatch is aborted
323
+ // (its executor kills on this signal) instead of orphaned, and
324
+ // queued-unstarted dispatches are abandoned.
325
+ const runController = new AbortController();
326
+ const onOuterAbort = () => { runController.abort(exec.signal.reason); };
327
+ exec.signal.addEventListener('abort', onOuterAbort, { once: true });
328
+ let dispatches = 0;
329
+ const pendingQueue = [];
330
+ const inFlight = new Set();
331
+ /** Tracked settle-event side work (log-content listener + append), drained at run settlement. */
332
+ const logWork = new Set();
333
+ const commitQueue = [];
334
+ let exclusiveActive = false;
335
+ let driving = false;
336
+ let driverRun = Promise.resolve();
337
+ let wake;
338
+ const wakeup = () => {
339
+ const release = wake;
340
+ wake = undefined;
341
+ release?.();
342
+ };
343
+ /**
344
+ * The single ordered lane. Each pass commits the head-of-line settled
345
+ * dispatch (ordered post-execute), then starts the next queued entry if
346
+ * its slot is free (ordered pre-execute), and otherwise sleeps until a
347
+ * body settles or a new submission arrives. One run reaching the
348
+ * empty-queues/empty-pool state is quiescence.
349
+ */
350
+ const drive = () => {
351
+ if (driving)
352
+ return driverRun;
353
+ driving = true;
354
+ driverRun = (async () => {
355
+ try {
356
+ for (;;) {
357
+ // Create the wakeup promise before inspecting state so a settle or submission arriving
358
+ // between the checks and the await below cannot be lost.
359
+ const signal = new Promise((resolve) => { wake = resolve; });
360
+ const commitHead = commitQueue[0];
361
+ if (commitHead !== undefined && commitHead.settled) {
362
+ commitQueue.shift();
363
+ await commitHead.commit();
364
+ // The barrier covers post-execute: later starts wait for the
365
+ // exclusive call's full pipeline, as under the native loop.
366
+ if (commitHead.mode === 'exclusive')
367
+ exclusiveActive = false;
368
+ continue;
369
+ }
370
+ const head = pendingQueue[0];
371
+ if (head !== undefined) {
372
+ if (runController.signal.aborted) {
373
+ pendingQueue.shift();
374
+ head.abandon();
375
+ continue;
376
+ }
377
+ // Reclassify at start time (fail-closed on registry changes).
378
+ const mode = head.classify();
379
+ const capacity = !exclusiveActive
380
+ && (mode === 'exclusive' ? inFlight.size === 0 : inFlight.size < maxParallel);
381
+ if (capacity) {
382
+ if (mode === 'exclusive')
383
+ exclusiveActive = true;
384
+ head.mode = mode;
385
+ pendingQueue.shift();
386
+ // Joined before start() so the commit cursor sees submission
387
+ // order; nothing commits it until `settled` flips.
388
+ commitQueue.push(head);
389
+ await head.start();
390
+ const flight = head.flight.finally(() => {
391
+ inFlight.delete(flight);
392
+ wakeup();
393
+ });
394
+ inFlight.add(flight);
395
+ continue;
396
+ }
397
+ }
398
+ if (pendingQueue.length === 0 && commitQueue.length === 0 && inFlight.size === 0)
399
+ return;
400
+ await signal;
401
+ }
402
+ }
403
+ finally {
404
+ driving = false;
405
+ wake = undefined;
406
+ }
407
+ })();
408
+ return driverRun;
409
+ };
410
+ /** Every dispatch settled AND committed; nothing can start (the run is aborted at call time). */
411
+ const drainDispatches = async () => {
412
+ // The abort already fired: the driver abandons queued-unstarted
413
+ // entries, awaits the live pool, and drains the ordered commit lane —
414
+ // including a commit already in progress when the program returned.
415
+ await drive();
416
+ // Every settle event is appended inside the open run_code turn
417
+ // (tasks self-remove on settlement).
418
+ while (logWork.size > 0)
419
+ await Promise.allSettled([...logWork]);
420
+ };
421
+ // Read through a call, not a bare property: the abort state genuinely
422
+ // changes across awaits, and a direct `.aborted` re-check after one
423
+ // would be narrowed away by control flow analysis.
424
+ const runOver = () => runController.signal.aborted;
425
+ const binding = (schema) => async (rawArgs) => {
426
+ const { name } = schema;
427
+ if (runOver()) {
428
+ throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} not dispatched`);
429
+ }
430
+ const normalized = jsonNormalizeArgs(rawArgs);
431
+ const n = ++dispatches;
432
+ const subCallId = brandString(`${String(exec.callId)}:ptc:${n}`);
433
+ const input = {
434
+ callId: subCallId,
435
+ rootCallId: exec.rootCallId,
436
+ name,
437
+ schema,
438
+ arguments: normalized.dispatched,
439
+ ...exec.agent ? { agent: exec.agent } : {},
440
+ parent: exec.token,
441
+ signal: runController.signal,
442
+ };
443
+ const scheduler = registry[TOOL_RUNTIME_SCHEDULER];
444
+ const outcome = await new Promise((resolve, reject) => {
445
+ // Set by the dispatch stage (or start() for a pre-settled result): what commit() finalizes in submission order.
446
+ let parked;
447
+ const settle = (result) => {
448
+ // The program gets its value NOW: the log-content listener (for
449
+ // example, a spill backend) must never delay the binding or occupy
450
+ // a dispatch slot. The event append is tracked side work; the run's
451
+ // settlement drains logWork so every settle event is still appended
452
+ // inside the open turn (shapeDispatchLog is contained, so this
453
+ // chain cannot reject).
454
+ resolve(result.isError
455
+ ? { isError: true, message: result.error.message }
456
+ : { isError: false, value: result.value });
457
+ const agent = exec.agent;
458
+ if (agent === undefined)
459
+ return;
460
+ const task = (async () => {
461
+ // The listener may replace the durable copy with a preview and
462
+ // locator; the program's value and model-visible result are
463
+ // untouched.
464
+ const logged = await shapeDispatchLog({
465
+ exec, agent, subCallId, name, isError: result.isError,
466
+ // The registry deep-froze this projection at result
467
+ // finalization; append snapshots the final copy again, so
468
+ // the log stays detached.
469
+ content: result.content,
470
+ });
471
+ agent.session.append('tool/ptc-dispatch', {
472
+ rootCallId: exec.rootCallId,
473
+ parentCallId: exec.callId,
474
+ subCallId,
475
+ name,
476
+ // The SIBLING parse of the dispatched value: byte-identical JSON,
477
+ // but a separate object — a tool mutating its args cannot desync
478
+ // this record from what it actually received.
479
+ arguments: normalized.logged,
480
+ isError: result.isError,
481
+ ...result.error?.info === undefined ? {} : { error: result.error.info },
482
+ content: logged,
483
+ });
484
+ })().finally(() => { logWork.delete(task); });
485
+ logWork.add(task);
486
+ };
487
+ pendingQueue.push({
488
+ flight: Promise.resolve(),
489
+ settled: false,
490
+ // Re-read per driver pass against the same agent view the SDK
491
+ // declared; fail-closed exclusive when undeclared/invalid.
492
+ classify: () => registry.executionMode(input).kind,
493
+ abandon: () => {
494
+ reject(new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`));
495
+ },
496
+ async start() {
497
+ exec.agent?.session.append('tool/ptc-dispatch-start', {
498
+ rootCallId: exec.rootCallId,
499
+ parentCallId: exec.callId,
500
+ subCallId,
501
+ name,
502
+ arguments: normalized.logged,
503
+ });
504
+ // Ordered prepare runs INSIDE the driver lane: the next entry's
505
+ // pre-execute waits for this resolution, as under the native
506
+ // scheduler. Only the launched body below overlaps.
507
+ const prepared = await scheduler.prepare(input);
508
+ if (prepared.kind === 'dispatch') {
509
+ this.flight = scheduler.dispatch(prepared.exec).then((dispatchOutcome) => {
510
+ parked = { kind: dispatchOutcome.kind, exec: prepared.exec, result: dispatchOutcome.result };
511
+ this.settled = true;
512
+ });
513
+ return;
514
+ }
515
+ parked = { kind: prepared.kind, exec: prepared.exec, result: prepared.result };
516
+ this.settled = true;
517
+ },
518
+ async commit() {
519
+ /* v8 ignore next -- commit() runs only after `settled` flipped, which set parked. */
520
+ if (parked === undefined)
521
+ return;
522
+ const result = parked.kind === 'post-result'
523
+ ? await scheduler.finalize(parked.exec, parked.result)
524
+ : scheduler.finish(parked.exec, parked.result);
525
+ if (!result.isError && result.content.some(block => block.type === 'image')) {
526
+ exec.deferContext(createUserMessage({
527
+ content: result.content,
528
+ source: { kind: 'ptc-mode' },
529
+ }));
530
+ }
531
+ for (const context of result.additionalContexts ?? []) {
532
+ exec.deferContext(context);
533
+ }
534
+ // The composite forwards `additionalContexts` above and
535
+ // `concludesTurn` here from the nested result. Only a successful
536
+ // nested result can carry the terminal marker
537
+ // (ToolExecutionFailure types it never), so a policy-converted
538
+ // failure cannot stop the turn through a recovering program.
539
+ if (result.concludesTurn)
540
+ exec.concludeTurn();
541
+ settle(result);
542
+ // Backpressure on pending event-append tasks: each task retains
543
+ // a full result while a slow backend stores it, so the pool cap
544
+ // bounds their count. Beyond the cap, the
545
+ // ordered lane waits, so later sub-calls cannot start and
546
+ // pending I/O/memory cannot grow without bound.
547
+ while (logWork.size > maxParallel)
548
+ await Promise.race(logWork);
549
+ },
550
+ });
551
+ wakeup();
552
+ void drive();
553
+ });
554
+ // A budget expiry or outer cancel that occurs while this call was in
555
+ // flight already aborted the dispatch; stop the program now rather
556
+ // than hand it a result from a run that is over.
557
+ if (runOver()) {
558
+ throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} result discarded`);
559
+ }
560
+ // The worker turns a binding rejection into ToolCallError and adds
561
+ // only the binding name. Native content and internal error metadata
562
+ // stay outside the program-facing failure contract.
563
+ if (outcome.isError)
564
+ throw new Error(outcome.message);
565
+ return outcome.value;
566
+ };
567
+ // Null-prototype + defineProperty, mirroring the worker-side namespace
568
+ // build: a registered tool named `__proto__` must become an ordinary
569
+ // own key (a plain-object assignment would hit the prototype setter,
570
+ // silently dropping the binding), and the runtime host resolves
571
+ // binding names as own properties only.
572
+ const functions = Object.create(null);
573
+ // Enumerate the CALLING AGENT's visible set (scoped tools join,
574
+ // restricted globals vanish) — the same view the SDK section declared,
575
+ // so a program can bind exactly what its prompt promised; sub-dispatch
576
+ // re-resolves per call through the same view (exec.agent threads down).
577
+ for (const schema of registry.schemas(exec.agent)) {
578
+ if (schema.name === RUN_CODE_NAME)
579
+ continue;
580
+ Object.defineProperty(functions, schema.name, { enumerable: true, value: binding(deepFreeze(schema)) });
581
+ }
582
+ try {
583
+ let result;
584
+ try {
585
+ result = await runtime.run(runtime.resolve({
586
+ program: args.code,
587
+ bindings: [{
588
+ global: 'tools',
589
+ functions,
590
+ errorClass: { name: 'ToolCallError', memberNameProperty: 'toolName' },
591
+ }],
592
+ signal: runController.signal,
593
+ ...exec.agent?.session.header.cwd !== undefined ? { cwd: exec.agent.session.header.cwd } : {},
594
+ ...policy !== undefined ? { sandboxPolicy: policy } : {},
595
+ ...args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {},
596
+ }));
597
+ }
598
+ finally {
599
+ // Abort sub-dispatches and drain every in-flight dispatch before
600
+ // closing the turn (queued-unstarted ones are abandoned unlogged).
601
+ // Binding failures remain observable through their individual promises.
602
+ runController.abort('run_code settled');
603
+ await drainDispatches();
604
+ }
605
+ if (result.error) {
606
+ const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.join('\n')}` : '';
607
+ const sandboxText = result.sandbox === undefined ? ''
608
+ : `\nFile sandbox: ${result.sandbox.mode}${result.sandbox.enforcement === undefined ? '' : `; enforcement: ${result.sandbox.enforcement}`}${result.sandbox.denied ? '; operation denied' : ''}.`;
609
+ throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}${sandboxText}${result.sandbox?.denied ? escalationGuidance(runtime) : ''}`);
610
+ }
611
+ return {
612
+ logs: result.logs,
613
+ ...result.sandbox === undefined ? {} : { sandbox: result.sandbox },
614
+ ...result.value !== undefined ? { result: result.value } : {},
615
+ };
616
+ }
617
+ finally {
618
+ exec.signal.removeEventListener('abort', onOuterAbort);
619
+ }
620
+ },
621
+ // The model-authored description is the call's always-visible UI label
622
+ // (the bash `description` precedent); the program itself rides rawInput.
623
+ presentCall: args => ({
624
+ card: 'generic',
625
+ title: args.description,
626
+ kind: 'execute',
627
+ rawInput: args.code,
628
+ }),
629
+ // Deliberately no presentResult: the generic card fallback keeps this
630
+ // title and reads durable result content without duplicating a large raw
631
+ // result into the host view payload.
632
+ });
633
+ // Resolve the language flavor lazily, at the moment the registry projects the
634
+ // schema (`schemaOf` destructures `description`/`parameters`). The definition
635
+ // is minted once at registration, before a runtime is known; deferring here
636
+ // is the least invasive point that still emits the loaded runtime's language.
637
+ Object.defineProperty(definition, 'description', {
638
+ enumerable: true,
639
+ get: () => {
640
+ const runtime = peekRuntime();
641
+ const instructions = runtime?.executionInstructions;
642
+ return resolveFlavor(peekRuntime).description
643
+ + (instructions ? ` ${instructions}` : '')
644
+ + (runtime === undefined ? '' : " The working directory is the Session's current directory.")
645
+ + escalationGuidance(runtime);
646
+ },
647
+ });
648
+ Object.defineProperty(definition, 'parameters', {
649
+ enumerable: true,
650
+ // Recompile through the same spec→schema projection defineTool used, so
651
+ // the emitted schema always matches the validated specification.
652
+ get: () => parameterSchemaSpecToJsonSchema({
653
+ description: { type: 'string', required: true, description: RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION },
654
+ code: { type: 'string', required: true, description: resolveFlavor(peekRuntime).codeDescription },
655
+ ...controlParameters(peekRuntime()),
656
+ }),
657
+ });
658
+ return definition;
659
+ }
660
+ //# sourceMappingURL=ptc.js.map