返回源码地图

packages/core/tools/src/ptc.ts

main snapshot · da00f7f5358f · 正文引用章节 08 / 11 / 11 / 11 / 11 / 11;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

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