@qilin-agent/tools 3.1.5 → 3.1.6

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/types/ptc.js CHANGED
@@ -5,43 +5,37 @@
5
5
  * outer curated result enters model history.
6
6
  * @module @qilin-agent/tools/src/ptc
7
7
  */
8
+ import { symbols } from '@qilin-agent/kylin';
8
9
  import { brandString } from '@qilin-agent/brand';
9
10
  import { createUserMessage, HarnessError } from '@qilin-agent/llm';
10
11
  import { approveEscalation, ESCALATION_TARGETS, validateEscalationArgs } from '@qilin-agent/sandbox';
11
12
  import { deepFreeze, snapshotJsonValue } from '@qilin-agent/util-values';
12
13
  import { defineTool, parameterSchemaSpecToJsonSchema } from "./schema.js";
14
+ import { PtcCalls } from "./ptc-calls.js";
13
15
  import { TOOL_RUNTIME_SCHEDULER } from "./index.js";
14
16
  /** The model-facing name of the PTC mode tool. */
15
17
  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.',
18
+ /** Keep tool-call rules here; the runtime owns evaluation syntax and state retention. */
19
+ function runCodeFlavor(language, execution = {
20
+ programDescription: 'the program to execute',
21
+ outputInstruction: 'Curate the program output.',
22
+ codeDescription: `The ${language} program to execute, following this tool's execution instructions.`,
23
+ }) {
24
+ return {
25
+ description: `Execute a ${language} program against the available tools. Takes two required arguments: `
26
+ + `\`description\`, a short summary of what the program does, and \`code\`, ${execution.programDescription}. `
27
+ + 'Call tools as `await tools.name(args)` per the declarations in the system prompt. '
28
+ + `${execution.outputInstruction} Image-bearing subtool results are attached after the run.`,
29
+ codeDescription: execution.codeDescription,
30
+ };
31
+ }
32
+ const TYPESCRIPT_FLAVOR = runCodeFlavor('TypeScript');
33
+ const TYPESCRIPT_FUNCTION_FLAVOR = runCodeFlavor('TypeScript', {
34
+ programDescription: 'the BODY of an async function (erasable syntax only; top-level `await` and `return` work)',
35
+ outputInstruction: 'Only what you print or return is program output — curate it.',
29
36
  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
- };
37
+ });
38
+ const PYTHON_FLAVOR = runCodeFlavor('Python');
45
39
  /** Per-language `run_code` schema flavors (see {@link RunCodeFlavor}); one entry per {@link PtcSdkLanguage}. */
46
40
  const RUN_CODE_FLAVORS = {
47
41
  typescript: TYPESCRIPT_FLAVOR,
@@ -111,7 +105,8 @@ function resolveFlavor(peekRuntime) {
111
105
  const known = Object.keys(RUN_CODE_FLAVORS).map(name => JSON.stringify(name)).join(', ');
112
106
  throw new Error(`qilin-tools: no run_code schema flavor registered for runtime language ${JSON.stringify(runtime.language)} (known: ${known})`);
113
107
  }
114
- return flavor;
108
+ return runtime.language === 'typescript' && runtime.programFormat === 'function-body'
109
+ ? TYPESCRIPT_FUNCTION_FLAVOR : flavor;
115
110
  }
116
111
  /**
117
112
  * Thrown by `run_code` when the program run itself failed — a program
@@ -231,18 +226,21 @@ function renderValue(value) {
231
226
  return typeof value === 'string' ? value : renderJsonValue(value);
232
227
  }
233
228
  /**
234
- * Build the `run_code` {@link ToolDefinition}: required `description` and
235
- * `code` parameters, executed through the dispatch bridge described
229
+ * Build the `run_code` {@link ToolDefinition}: required `code` and
230
+ * `description` parameters, executed through the dispatch bridge described
236
231
  * above. The
237
232
  * registry reserves it as presentation infrastructure under non-native modes,
238
233
  * outside the filterable global/scoped capability layers.
239
234
  * @param registry - the owning registry (sub-calls go through its `execute`,
240
235
  * bindings cover its registered tools).
241
236
  * @param options - the registry-private capabilities described above.
237
+ * @param ctx - Owning registry context, independent of the calling Agent.
242
238
  * @returns the registry-ready definition.
243
239
  */
244
- export function createRunCodeTool(registry, options) {
240
+ export function createRunCodeTool(registry, options, ctx) {
245
241
  const { requireRuntime, peekRuntime, maxParallel, shapeDispatchLog } = options;
242
+ const stateOwners = new WeakMap();
243
+ const calls = new PtcCalls(ctx);
246
244
  const definition = defineTool({
247
245
  name: RUN_CODE_NAME,
248
246
  // The description and `code` parameter description are placeholders here:
@@ -294,331 +292,354 @@ export function createRunCodeTool(registry, options) {
294
292
  throw new Error('invalid description: expected a non-empty string');
295
293
  }
296
294
  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
- const cwd = await options.resolveWorkingDirectory(exec);
321
- exec.signal.throwIfAborted();
322
- // The run-scoped abort: follows the outer signal in, and fires when the
323
- // run settles for ANY reason, so an in-flight sub-dispatch is aborted
324
- // (its executor kills on this signal) instead of orphaned, and
325
- // queued-unstarted dispatches are abandoned.
326
- const runController = new AbortController();
327
- const onOuterAbort = () => { runController.abort(exec.signal.reason); };
328
- exec.signal.addEventListener('abort', onOuterAbort, { once: true });
329
- let dispatches = 0;
330
- const pendingQueue = [];
331
- const inFlight = new Set();
332
- /** Tracked settle-event side work (log-content listener + append), drained at run settlement. */
333
- const logWork = new Set();
334
- const commitQueue = [];
335
- let exclusiveActive = false;
336
- let driving = false;
337
- let driverRun = Promise.resolve();
338
- let wake;
339
- const wakeup = () => {
340
- const release = wake;
341
- wake = undefined;
342
- release?.();
343
- };
344
- /**
345
- * The single ordered lane. Each pass commits the head-of-line settled
346
- * dispatch (ordered post-execute), then starts the next queued entry if
347
- * its slot is free (ordered pre-execute), and otherwise sleeps until a
348
- * body settles or a new submission arrives. One run reaching the
349
- * empty-queues/empty-pool state is quiescence.
350
- */
351
- const drive = () => {
352
- if (driving)
353
- return driverRun;
354
- driving = true;
355
- driverRun = (async () => {
356
- try {
357
- for (;;) {
358
- // Create the wakeup promise before inspecting state so a settle or submission arriving
359
- // between the checks and the await below cannot be lost.
360
- const signal = new Promise((resolve) => { wake = resolve; });
361
- const commitHead = commitQueue[0];
362
- if (commitHead !== undefined && commitHead.settled) {
363
- commitQueue.shift();
364
- await commitHead.commit();
365
- // The barrier covers post-execute: later starts wait for the
366
- // exclusive call's full pipeline, as under the native loop.
367
- if (commitHead.mode === 'exclusive')
368
- exclusiveActive = false;
369
- continue;
370
- }
371
- const head = pendingQueue[0];
372
- if (head !== undefined) {
373
- if (runController.signal.aborted) {
374
- pendingQueue.shift();
375
- head.abandon();
295
+ const run = async (callSignal) => {
296
+ validateEscalationArgs(args.sandbox_permissions, args.justification);
297
+ if (args.timeoutMs !== undefined && runtime.timeout === undefined) {
298
+ throw new Error('timeoutMs is not available for this PTC runtime');
299
+ }
300
+ if (args.timeoutMs !== undefined && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) {
301
+ throw new Error('invalid timeoutMs: expected a positive finite number');
302
+ }
303
+ const standingPolicy = runtime.sandboxMode === undefined ? undefined : options.resolveSandboxPolicy(exec);
304
+ let policy = standingPolicy;
305
+ if (args.sandbox_permissions !== undefined && args.justification !== undefined) {
306
+ if (standingPolicy === undefined)
307
+ throw new Error('sandbox_permissions is not available for this PTC runtime');
308
+ const approvedMode = await approveEscalation({
309
+ requestedMode: args.sandbox_permissions,
310
+ justification: args.justification,
311
+ effectiveMode: standingPolicy.mode,
312
+ subject: 'program',
313
+ }, {
314
+ approver: options.peekApprover(), agent: exec.agent, callId: exec.callId,
315
+ toolName: RUN_CODE_NAME, signal: callSignal,
316
+ });
317
+ policy = { ...standingPolicy, mode: approvedMode };
318
+ }
319
+ const cwd = await options.resolveWorkingDirectory({ ...exec, signal: callSignal });
320
+ callSignal.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(callSignal.reason); };
327
+ callSignal.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;
376
368
  continue;
377
369
  }
378
- // Reclassify at start time (fail-closed on registry changes).
379
- const mode = head.classify();
380
- const capacity = !exclusiveActive
381
- && (mode === 'exclusive' ? inFlight.size === 0 : inFlight.size < maxParallel);
382
- if (capacity) {
383
- if (mode === 'exclusive')
384
- exclusiveActive = true;
385
- head.mode = mode;
386
- pendingQueue.shift();
387
- // Joined before start() so the commit cursor sees submission
388
- // order; nothing commits it until `settled` flips.
389
- commitQueue.push(head);
390
- await head.start();
391
- const flight = head.flight.finally(() => {
392
- inFlight.delete(flight);
393
- wakeup();
394
- });
395
- inFlight.add(flight);
396
- continue;
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
397
  }
398
+ if (pendingQueue.length === 0 && commitQueue.length === 0 && inFlight.size === 0)
399
+ return;
400
+ await signal;
398
401
  }
399
- if (pendingQueue.length === 0 && commitQueue.length === 0 && inFlight.size === 0)
400
- return;
401
- await signal;
402
402
  }
403
- }
404
- finally {
405
- driving = false;
406
- wake = undefined;
407
- }
408
- })();
409
- return driverRun;
410
- };
411
- /** Every dispatch settled AND committed; nothing can start (the run is aborted at call time). */
412
- const drainDispatches = async () => {
413
- // The abort already fired: the driver abandons queued-unstarted
414
- // entries, awaits the live pool, and drains the ordered commit lane —
415
- // including a commit already in progress when the program returned.
416
- await drive();
417
- // Every settle event is appended inside the open run_code turn
418
- // (tasks self-remove on settlement).
419
- while (logWork.size > 0)
420
- await Promise.allSettled([...logWork]);
421
- };
422
- // Read through a call, not a bare property: the abort state genuinely
423
- // changes across awaits, and a direct `.aborted` re-check after one
424
- // would be narrowed away by control flow analysis.
425
- const runOver = () => runController.signal.aborted;
426
- const binding = (schema) => async (rawArgs) => {
427
- const { name } = schema;
428
- if (runOver()) {
429
- throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} not dispatched`);
430
- }
431
- const normalized = jsonNormalizeArgs(rawArgs);
432
- const n = ++dispatches;
433
- const subCallId = brandString(`${String(exec.callId)}:ptc:${n}`);
434
- const input = {
435
- callId: subCallId,
436
- rootCallId: exec.rootCallId,
437
- name,
438
- schema,
439
- arguments: normalized.dispatched,
440
- ...exec.agent ? { agent: exec.agent } : {},
441
- parent: exec.token,
442
- signal: runController.signal,
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]);
443
420
  };
444
- const scheduler = registry[TOOL_RUNTIME_SCHEDULER];
445
- const outcome = await new Promise((resolve, reject) => {
446
- // Set by the dispatch stage (or start() for a pre-settled result): what commit() finalizes in submission order.
447
- let parked;
448
- const settle = (result) => {
449
- // The program gets its value NOW: the log-content listener (for
450
- // example, a spill backend) must never delay the binding or occupy
451
- // a dispatch slot. The event append is tracked side work; the run's
452
- // settlement drains logWork so every settle event is still appended
453
- // inside the open turn (shapeDispatchLog is contained, so this
454
- // chain cannot reject).
455
- resolve(result.isError
456
- ? { isError: true, message: result.error.message }
457
- : { isError: false, value: result.value });
458
- const agent = exec.agent;
459
- if (agent === undefined)
460
- return;
461
- const task = (async () => {
462
- // The listener may replace the durable copy with a preview and
463
- // locator; the program's value and model-visible result are
464
- // untouched.
465
- const logged = await shapeDispatchLog({
466
- exec, agent, subCallId, name, isError: result.isError,
467
- // The registry deep-froze this projection at result
468
- // finalization; append snapshots the final copy again, so
469
- // the log stays detached.
470
- content: result.content,
471
- });
472
- agent.session.append('tool/ptc-dispatch', {
473
- rootCallId: exec.rootCallId,
474
- parentCallId: exec.callId,
475
- subCallId,
476
- name,
477
- // The SIBLING parse of the dispatched value: byte-identical JSON,
478
- // but a separate object — a tool mutating its args cannot desync
479
- // this record from what it actually received.
480
- arguments: normalized.logged,
481
- isError: result.isError,
482
- ...result.error?.info === undefined ? {} : { error: result.error.info },
483
- content: logged,
484
- ...result.meta === undefined ? {} : { meta: result.meta },
485
- });
486
- })().finally(() => { logWork.delete(task); });
487
- logWork.add(task);
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,
488
442
  };
489
- pendingQueue.push({
490
- flight: Promise.resolve(),
491
- settled: false,
492
- // Re-read per driver pass against the same agent view the SDK
493
- // declared; fail-closed exclusive when undeclared/invalid.
494
- classify: () => registry.executionMode(input).kind,
495
- abandon: () => {
496
- reject(new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`));
497
- },
498
- async start() {
499
- exec.agent?.session.append('tool/ptc-dispatch-start', {
500
- rootCallId: exec.rootCallId,
501
- parentCallId: exec.callId,
502
- subCallId,
503
- name,
504
- arguments: normalized.logged,
505
- });
506
- // Ordered prepare runs INSIDE the driver lane: the next entry's
507
- // pre-execute waits for this resolution, as under the native
508
- // scheduler. Only the launched body below overlaps.
509
- const prepared = await scheduler.prepare(input);
510
- if (prepared.kind === 'dispatch') {
511
- this.flight = scheduler.dispatch(prepared.exec).then((dispatchOutcome) => {
512
- parked = { kind: dispatchOutcome.kind, exec: prepared.exec, result: dispatchOutcome.result };
513
- this.settled = true;
514
- });
515
- return;
516
- }
517
- parked = { kind: prepared.kind, exec: prepared.exec, result: prepared.result };
518
- this.settled = true;
519
- },
520
- async commit() {
521
- /* v8 ignore next -- commit() runs only after `settled` flipped, which set parked. */
522
- if (parked === undefined)
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)
523
459
  return;
524
- const result = parked.kind === 'post-result'
525
- ? await scheduler.finalize(parked.exec, parked.result)
526
- : scheduler.finish(parked.exec, parked.result);
527
- if (!result.isError && result.content.some(block => block.type === 'image')) {
528
- exec.deferContext(createUserMessage({
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.
529
469
  content: result.content,
530
- source: { kind: 'ptc-mode' },
531
- }));
532
- }
533
- for (const context of result.additionalContexts ?? []) {
534
- exec.deferContext(context);
535
- }
536
- // The composite forwards `additionalContexts` above and
537
- // `concludesTurn` here from the nested result. Only a successful
538
- // nested result can carry the terminal marker
539
- // (ToolExecutionFailure types it never), so a policy-converted
540
- // failure cannot stop the turn through a recovering program.
541
- if (result.concludesTurn)
542
- exec.concludeTurn();
543
- settle(result);
544
- // Backpressure on pending event-append tasks: each task retains
545
- // a full result while a slow backend stores it, so the pool cap
546
- // bounds their count. Beyond the cap, the
547
- // ordered lane waits, so later sub-calls cannot start and
548
- // pending I/O/memory cannot grow without bound.
549
- while (logWork.size > maxParallel)
550
- await Promise.race(logWork);
551
- },
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
+ ...result.meta === undefined ? {} : { meta: result.meta },
484
+ });
485
+ })().finally(() => { logWork.delete(task); });
486
+ logWork.add(task);
487
+ };
488
+ pendingQueue.push({
489
+ flight: Promise.resolve(),
490
+ settled: false,
491
+ // Re-read per driver pass against the same agent view the SDK
492
+ // declared; fail-closed exclusive when undeclared/invalid.
493
+ classify: () => registry.executionMode(input).kind,
494
+ abandon: () => {
495
+ reject(new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`));
496
+ },
497
+ async start() {
498
+ exec.agent?.session.append('tool/ptc-dispatch-start', {
499
+ rootCallId: exec.rootCallId,
500
+ parentCallId: exec.callId,
501
+ subCallId,
502
+ name,
503
+ arguments: normalized.logged,
504
+ });
505
+ // Ordered prepare runs INSIDE the driver lane: the next entry's
506
+ // pre-execute waits for this resolution, as under the native
507
+ // scheduler. Only the launched body below overlaps.
508
+ const prepared = await scheduler.prepare(input);
509
+ if (prepared.kind === 'dispatch') {
510
+ this.flight = scheduler.dispatch(prepared.exec).then((dispatchOutcome) => {
511
+ parked = { kind: dispatchOutcome.kind, exec: prepared.exec, result: dispatchOutcome.result };
512
+ this.settled = true;
513
+ });
514
+ return;
515
+ }
516
+ parked = { kind: prepared.kind, exec: prepared.exec, result: prepared.result };
517
+ this.settled = true;
518
+ },
519
+ async commit() {
520
+ /* v8 ignore next -- commit() runs only after `settled` flipped, which set parked. */
521
+ if (parked === undefined)
522
+ return;
523
+ const result = parked.kind === 'post-result'
524
+ ? await scheduler.finalize(parked.exec, parked.result)
525
+ : scheduler.finish(parked.exec, parked.result);
526
+ if (!result.isError && result.content.some(block => block.type === 'image')) {
527
+ exec.deferContext(createUserMessage({
528
+ content: result.content,
529
+ source: { kind: 'ptc-mode' },
530
+ }));
531
+ }
532
+ for (const context of result.additionalContexts ?? []) {
533
+ exec.deferContext(context);
534
+ }
535
+ // The composite forwards `additionalContexts` above and
536
+ // `concludesTurn` here from the nested result. Only a successful
537
+ // nested result can carry the terminal marker
538
+ // (ToolExecutionFailure types it never), so a policy-converted
539
+ // failure cannot stop the turn through a recovering program.
540
+ if (result.concludesTurn)
541
+ exec.concludeTurn();
542
+ settle(result);
543
+ // Backpressure on pending event-append tasks: each task retains
544
+ // a full result while a slow backend stores it, so the pool cap
545
+ // bounds their count. Beyond the cap, the
546
+ // ordered lane waits, so later sub-calls cannot start and
547
+ // pending I/O/memory cannot grow without bound.
548
+ while (logWork.size > maxParallel)
549
+ await Promise.race(logWork);
550
+ },
551
+ });
552
+ wakeup();
553
+ void drive();
552
554
  });
553
- wakeup();
554
- void drive();
555
- });
556
- // A budget expiry or outer cancel that occurs while this call was in
557
- // flight already aborted the dispatch; stop the program now rather
558
- // than hand it a result from a run that is over.
559
- if (runOver()) {
560
- throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} result discarded`);
555
+ // A budget expiry or outer cancel that occurs while this call was in
556
+ // flight already aborted the dispatch; stop the program now rather
557
+ // than hand it a result from a run that is over.
558
+ if (runOver()) {
559
+ throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} result discarded`);
560
+ }
561
+ // The worker turns a binding rejection into ToolCallError and adds
562
+ // only the binding name. Native content and internal error metadata
563
+ // stay outside the program-facing failure contract.
564
+ if (outcome.isError)
565
+ throw new Error(outcome.message);
566
+ return outcome.value;
567
+ };
568
+ // Null-prototype + defineProperty, mirroring the worker-side namespace
569
+ // build: a registered tool named `__proto__` must become an ordinary
570
+ // own key (a plain-object assignment would hit the prototype setter,
571
+ // silently dropping the binding), and the runtime host resolves
572
+ // binding names as own properties only.
573
+ const functions = Object.create(null);
574
+ // Enumerate the CALLING AGENT's visible set (scoped tools join,
575
+ // restricted globals vanish) — the same view the SDK section declared,
576
+ // so a program can bind exactly what its prompt promised; sub-dispatch
577
+ // re-resolves per call through the same view (exec.agent threads down).
578
+ for (const schema of registry.schemas(exec.agent)) {
579
+ if (schema.name === RUN_CODE_NAME)
580
+ continue;
581
+ Object.defineProperty(functions, schema.name, { enumerable: true, value: binding(deepFreeze(schema)) });
561
582
  }
562
- // The worker turns a binding rejection into ToolCallError and adds
563
- // only the binding name. Native content and internal error metadata
564
- // stay outside the program-facing failure contract.
565
- if (outcome.isError)
566
- throw new Error(outcome.message);
567
- return outcome.value;
568
- };
569
- // Null-prototype + defineProperty, mirroring the worker-side namespace
570
- // build: a registered tool named `__proto__` must become an ordinary
571
- // own key (a plain-object assignment would hit the prototype setter,
572
- // silently dropping the binding), and the runtime host resolves
573
- // binding names as own properties only.
574
- const functions = Object.create(null);
575
- // Enumerate the CALLING AGENT's visible set (scoped tools join,
576
- // restricted globals vanish) — the same view the SDK section declared,
577
- // so a program can bind exactly what its prompt promised; sub-dispatch
578
- // re-resolves per call through the same view (exec.agent threads down).
579
- for (const schema of registry.schemas(exec.agent)) {
580
- if (schema.name === RUN_CODE_NAME)
581
- continue;
582
- Object.defineProperty(functions, schema.name, { enumerable: true, value: binding(deepFreeze(schema)) });
583
- }
584
- try {
585
- let result;
586
583
  try {
587
- result = await runtime.run(runtime.resolve({
588
- program: args.code,
589
- bindings: [{
590
- global: 'tools',
591
- functions,
592
- errorClass: { name: 'ToolCallError', memberNameProperty: 'toolName' },
593
- }],
594
- signal: runController.signal,
595
- ...cwd !== undefined ? { cwd } : {},
596
- ...policy !== undefined ? { sandboxPolicy: policy } : {},
597
- ...args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {},
598
- }));
584
+ let result;
585
+ try {
586
+ if (exec.agent !== undefined) {
587
+ const agent = exec.agent;
588
+ let runtimes = stateOwners.get(agent);
589
+ if (runtimes === undefined)
590
+ stateOwners.set(agent, runtimes = new WeakSet());
591
+ // Cordis can wrap service returns repeatedly; cleanup belongs to the original provider.
592
+ let provider = runtime;
593
+ for (;;) {
594
+ const original = provider[symbols.original];
595
+ if (original === undefined)
596
+ break;
597
+ provider = original;
598
+ }
599
+ if (!runtimes.has(provider)) {
600
+ agent.ctx.effect(() => () => runtime.release(agent), 'PTC retained environment');
601
+ runtimes.add(provider);
602
+ }
603
+ }
604
+ result = await runtime.run(runtime.resolve({
605
+ program: args.code,
606
+ ...exec.agent === undefined || policy?.mode !== standingPolicy?.mode ? {} : { stateOwner: exec.agent },
607
+ bindings: [{
608
+ global: 'tools',
609
+ functions,
610
+ errorClass: { name: 'ToolCallError', memberNameProperty: 'toolName' },
611
+ }],
612
+ signal: runController.signal,
613
+ ...cwd !== undefined ? { cwd } : {},
614
+ ...policy !== undefined ? { sandboxPolicy: policy } : {},
615
+ ...args.timeoutMs !== undefined ? { timeoutMs: args.timeoutMs } : {},
616
+ }));
617
+ }
618
+ finally {
619
+ // Abort sub-dispatches and drain every in-flight dispatch before
620
+ // closing the turn (queued-unstarted ones are abandoned unlogged).
621
+ // Binding failures remain observable through their individual promises.
622
+ runController.abort('run_code settled');
623
+ await drainDispatches();
624
+ }
625
+ if (result.error) {
626
+ const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.join('\n')}` : '';
627
+ const sandboxText = result.sandbox === undefined ? ''
628
+ : `\nFile sandbox: ${result.sandbox.mode}${result.sandbox.enforcement === undefined ? '' : `; enforcement: ${result.sandbox.enforcement}`}${result.sandbox.denied ? '; operation denied' : ''}.`;
629
+ throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}${sandboxText}${result.sandbox?.denied ? escalationGuidance(runtime) : ''}`);
630
+ }
631
+ return {
632
+ logs: result.logs,
633
+ ...result.sandbox === undefined ? {} : { sandbox: result.sandbox },
634
+ ...result.value !== undefined ? { result: result.value } : {},
635
+ };
599
636
  }
600
637
  finally {
601
- // Abort sub-dispatches and drain every in-flight dispatch before
602
- // closing the turn (queued-unstarted ones are abandoned unlogged).
603
- // Binding failures remain observable through their individual promises.
604
- runController.abort('run_code settled');
605
- await drainDispatches();
606
- }
607
- if (result.error) {
608
- const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.join('\n')}` : '';
609
- const sandboxText = result.sandbox === undefined ? ''
610
- : `\nFile sandbox: ${result.sandbox.mode}${result.sandbox.enforcement === undefined ? '' : `; enforcement: ${result.sandbox.enforcement}`}${result.sandbox.denied ? '; operation denied' : ''}.`;
611
- throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}${sandboxText}${result.sandbox?.denied ? escalationGuidance(runtime) : ''}`);
638
+ callSignal.removeEventListener('abort', onOuterAbort);
612
639
  }
613
- return {
614
- logs: result.logs,
615
- ...result.sandbox === undefined ? {} : { sandbox: result.sandbox },
616
- ...result.value !== undefined ? { result: result.value } : {},
617
- };
618
- }
619
- finally {
620
- exec.signal.removeEventListener('abort', onOuterAbort);
621
- }
640
+ };
641
+ return runtime.sandboxMode === undefined || exec.agent === undefined ? run(exec.signal)
642
+ : calls.run(exec.agent.session.id, exec.signal, () => options.resolveSandboxPolicy(exec).mode, run);
622
643
  },
623
644
  // The model-authored description is the call's always-visible UI label
624
645
  // (the bash `description` precedent); the program itself rides rawInput.