run_code now discards completion values for TypeScript runtimes, so the scoped REPL guidance shares the inspect output sentence with the other modes. Tests read the output field, cover environment initialization with copied bindings, and the ptc-scoped-repl snapshot is re-recorded with inspect output.
183 lines
14 KiB
TypeScript
183 lines
14 KiB
TypeScript
/** Select program syntax and own temporary or retained Node processes. */
|
|
import { stripTypeScriptTypes } from 'node:module'
|
|
import { isAbsolute } from 'node:path'
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
import type { PtcProgramFormat, PtcRunRequest, PtcRunResult, PtcRunSpec } from '@deepseek-ai/dsh-ptc-runtime'
|
|
import { assertNever } from '@deepseek-ai/dsh-util-values'
|
|
import { clampTimeout } from '@deepseek-ai/dsh-timeout'
|
|
import type { ResolvedConfig } from './config.ts'
|
|
import { validateBindings } from './bindings.ts'
|
|
import type { PolicyProcesses } from './policy-processes.ts'
|
|
import { ProgramWorker } from './worker.ts'
|
|
|
|
/** Syntax, state retention and model guidance selected during provider construction. */
|
|
export interface ExecutionMode {
|
|
/** Child evaluator selected independently of process lifetime. */
|
|
readonly format: PtcProgramFormat
|
|
/** Whether caller-owned objects retain processes between runs. */
|
|
readonly retainsState: boolean
|
|
/** Model-facing syntax, output and recovery rules. */
|
|
readonly executionInstructions: string
|
|
/**
|
|
* Erase TypeScript in the selected syntax without executing the program.
|
|
* @param program - Model-written TypeScript source.
|
|
* @returns JavaScript accepted by the selected evaluator.
|
|
*/
|
|
stripTypes(program: string): string
|
|
}
|
|
|
|
/**
|
|
* Select program semantics and create their shared process owner.
|
|
* @param ctx - Provider lifecycle and execution services.
|
|
* @param config - Validated configuration with loader defaults.
|
|
* @param processes - Policy invalidation and process cleanup coordination.
|
|
* @returns Mode selection and temporary or retained execution through one process implementation.
|
|
*/
|
|
export function createExecution(ctx: Context, config: ResolvedConfig, processes: PolicyProcesses): NodeExecution {
|
|
return new NodeExecution(ctx, config, processes, resolveMode(config))
|
|
}
|
|
|
|
/** Temporary processes and optional object-owned reuse share one process implementation. */
|
|
class NodeExecution {
|
|
private readonly owners = new WeakMap<object, ProgramWorker>()
|
|
private readonly workers = new Set<ProgramWorker>()
|
|
|
|
/**
|
|
* Own process instances and optional owner lookup for one provider.
|
|
* @param ctx - Provider lifecycle and execution services.
|
|
* @param config - Validated limits and selected executable.
|
|
* @param processes - Provider-owned policy invalidation and cleanup.
|
|
* @param mode - Program syntax and TypeScript erasure.
|
|
*/
|
|
constructor(
|
|
private readonly ctx: Context, private readonly config: ResolvedConfig,
|
|
private readonly processes: PolicyProcesses, readonly mode: ExecutionMode,
|
|
) {}
|
|
|
|
/** Stop every owned process, retaining failed cleanup for a later retry. */
|
|
async dispose(): Promise<void> {
|
|
const results = await Promise.allSettled([...this.workers].map(async (worker) => {
|
|
await worker.dispose()
|
|
this.workers.delete(worker)
|
|
}))
|
|
const failed = results.find(result => result.status === 'rejected')
|
|
if (failed?.status === 'rejected') throw failed.reason
|
|
}
|
|
|
|
/**
|
|
* Resolve the common file policy, directory and deadline defaults.
|
|
* @param request - Program, bindings, owner, directory and elapsed budget.
|
|
* @returns Resolved inputs with the selected file policy.
|
|
*/
|
|
resolve(request: PtcRunRequest): PtcRunSpec {
|
|
const sandboxPolicy = request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve()
|
|
const cwd = request.cwd ?? sandboxPolicy.workspaceRoot
|
|
if (!isAbsolute(cwd)) throw new Error(`${this.mode.retainsState ? 'persistent PTC' : 'ptc-runtime-node'}: cwd must be absolute`)
|
|
return { ...request, sandboxPolicy, cwd, timeoutMs: request.timeoutMs === null ? null : clampTimeout(request.timeoutMs, this.config.timeoutMs, this.config.maxTimeoutMs, this.mode.retainsState ? 'persistent PTC timeoutMs' : 'ptc-runtime-node: timeoutMs') }
|
|
}
|
|
|
|
/**
|
|
* Evaluate in a temporary process or reuse an enabled state owner's process.
|
|
* @param spec - Resolved request; concurrent calls for one owner are rejected.
|
|
* @param initialization - Type-erased source for new environments, or undefined when none is registered.
|
|
* @returns Captured logs, JSON completion, or a structured execution failure.
|
|
*/
|
|
async run(spec: PtcRunSpec, initialization: string | undefined): Promise<PtcRunResult> {
|
|
if (spec.sandboxPolicy === undefined) throw new Error('ptc-runtime-node: run requires a resolved sandbox policy')
|
|
if (!isAbsolute(spec.cwd) || (spec.timeoutMs !== null && (!Number.isFinite(spec.timeoutMs) || spec.timeoutMs <= 0 || spec.timeoutMs > this.config.maxTimeoutMs))) throw new Error('ptc-runtime-node: run requires resolved cwd and timeout')
|
|
const bindings = validateBindings(spec)
|
|
const owner = this.mode.retainsState ? spec.stateOwner : undefined
|
|
let worker = owner === undefined ? undefined : this.owners.get(owner)
|
|
worker?.assertIdle()
|
|
if (worker && !worker.closed && (worker.policy.mode !== spec.sandboxPolicy.mode
|
|
|| worker.policy.workspaceRoot !== spec.sandboxPolicy.workspaceRoot
|
|
|| worker.policy.sessionId !== spec.sandboxPolicy.sessionId)) {
|
|
throw new Error('persistent PTC: release the retained environment before changing its sandbox policy')
|
|
}
|
|
// Retained processes keep their launch-time directory grants until release.
|
|
if (!worker || worker.closed) {
|
|
if (worker) this.workers.delete(worker)
|
|
// Direct callers can own mutable policy arrays; the retained process owns its grant snapshot.
|
|
worker = new ProgramWorker(this.ctx, this.config, {
|
|
...spec.sandboxPolicy,
|
|
...spec.sandboxPolicy.additionalWritableRoots === undefined
|
|
? {}
|
|
: { additionalWritableRoots: [...spec.sandboxPolicy.additionalWritableRoots] },
|
|
}, this.processes, this.mode, owner !== undefined, initialization)
|
|
this.workers.add(worker)
|
|
if (owner !== undefined) this.owners.set(owner, worker)
|
|
}
|
|
try { return await worker.run(spec, bindings) } finally {
|
|
if (worker.cleanupComplete) this.workers.delete(worker)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Release retained state and await its process cleanup.
|
|
* @param owner - Exact object supplied to run.
|
|
* @returns Completion after the owned process and output streams stop.
|
|
*/
|
|
async release(owner: object): Promise<void> {
|
|
const worker = this.owners.get(owner)
|
|
if (!worker) return
|
|
await worker.dispose()
|
|
if (this.owners.get(owner) === worker) this.owners.delete(owner)
|
|
this.workers.delete(worker)
|
|
}
|
|
}
|
|
|
|
const STRIP_PREFIX = 'async function __dsh_program__() {\n'
|
|
const STRIP_SUFFIX = '\n}'
|
|
const RETAINED_STATE_INSTRUCTIONS = 'Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Do not redeclare existing let, const, or class names; reuse mutable bindings or choose new names. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation.'
|
|
/** Scoped REPL guidance; its output sentence is shared with the other modes. */
|
|
function scopedReplMode(outputInstructions: string): ExecutionMode {
|
|
return {
|
|
format: 'scoped-repl',
|
|
retainsState: true,
|
|
executionInstructions: 'The program contains TypeScript REPL statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. ' + outputInstructions + ' The program runs in strict mode: assigning an undeclared name throws, so use globalThis for intentional globals; eval and new Function are unavailable. Variables, functions, and imported modules remain in memory for this Agent across calls. Each call starts with copies of the top-level bindings left by earlier calls and may declare any of those names again with any keyword. A new declaration applies to this and later calls; functions and still-running code from earlier calls keep the values they captured. Assigning to an earlier name without declaring it, even one declared with const, changes only the copy seen by this and later calls. A let, const, or class declaration cannot read the earlier value of its own name, so let total = total + 1 throws; assign to the name or use a new name instead. A var declaration of an earlier name starts from the earlier value. var declarations become top-level names of the call even inside blocks; other declarations inside blocks stay in their block. Assignments that earlier code makes later, such as from timers, are not visible to later calls; keep shared mutable state in objects. If a call fails before a let, const, class, or var declaration of an existing name finishes, later calls keep that name\'s earlier value. Use await import(...) for modules; require is unavailable, and relative specifiers resolve from the working directory of this environment\'s first call. Direct filesystem access follows the environment\'s launch-time sandbox policy; process.env starts empty. Timeout and cancellation results say whether retained variables were preserved or cleared. Restart, sandbox changes, output overflow, or process failure clears them. Recreate missing variables and resources; conversation history does not restore state. Earlier mutations and tool effects are not rolled back. Approved sandbox escalations use a fresh environment and discard its variables afterward. Await tools and cleanup before returning; unfinished tools reject. After completion, timeout, or cancellation, new tool calls are rejected and captured output is discarded; background work may still change state. Await child-process completion before returning; raw output belongs to the active evaluation.',
|
|
stripTypes: stripTypeScriptTypes,
|
|
}
|
|
}
|
|
|
|
function resolveMode(config: ResolvedConfig): ExecutionMode {
|
|
const outputInstructions = 'Use inspect(...values) to display output. Do not use console.'
|
|
switch (config.executionMode) {
|
|
case 'per-call': return {
|
|
format: 'function-body',
|
|
retainsState: false,
|
|
executionInstructions: 'The program is the body of an async TypeScript function; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level await and return are supported. ' + outputInstructions + ' Each call runs in a fresh Node process. Node APIs are available through await import(...). Relative paths use the supplied working directory; process.env starts empty. Direct filesystem access follows this execution\'s sandbox policy.',
|
|
stripTypes(program) {
|
|
const stripped = stripTypeScriptTypes(STRIP_PREFIX + program + STRIP_SUFFIX)
|
|
return stripped.slice(STRIP_PREFIX.length, stripped.length - STRIP_SUFFIX.length)
|
|
},
|
|
}
|
|
case 'persistent-in-memory': return config.replScoped ? scopedReplMode(outputInstructions) : {
|
|
format: 'repl',
|
|
retainsState: true,
|
|
executionInstructions: 'The program contains TypeScript REPL statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. ' + outputInstructions + ' Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Direct filesystem access follows the environment\'s launch-time sandbox policy; process.env starts empty. ' + RETAINED_STATE_INSTRUCTIONS,
|
|
stripTypes: stripTypeScriptTypes,
|
|
}
|
|
case 'vm-script': {
|
|
const lifetime = config.scriptPersistence
|
|
? 'Variables, functions, and imported modules remain in memory for this Agent across calls.'
|
|
: 'Each call runs in a fresh Node process.'
|
|
const filePolicy = config.scriptPersistence
|
|
? 'Direct filesystem access follows the environment\'s launch-time sandbox policy; process.env starts empty.'
|
|
: 'Direct filesystem access follows this execution\'s sandbox policy; process.env starts empty.'
|
|
const asynchronousFailure = 'Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process'
|
|
+ (config.scriptPersistence ? ' and clears its variables.' : '.')
|
|
const recovery = config.scriptPersistence
|
|
? ' ' + RETAINED_STATE_INSTRUCTIONS + ' If a native top-level let, const, or class initializer throws, its name may remain permanently uninitialized; use a new name or restart the environment.'
|
|
: ''
|
|
return {
|
|
format: 'script',
|
|
retainsState: config.scriptPersistence,
|
|
executionInstructions: 'The program contains TypeScript script statements; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level declarations and await are supported; top-level return is not supported. ' + outputInstructions + ' The final expression is awaited if it is a Promise. ' + lifetime + ' Use await import(...) for modules. Relative paths use the supplied working directory. ' + filePolicy + ' ' + asynchronousFailure + ' Values from bindings and imports can belong to another realm; use Array.isArray for arrays and the supplied binding error classes for failures, because instanceof Array or Error may be false.' + recovery,
|
|
stripTypes: stripTypeScriptTypes,
|
|
}
|
|
}
|
|
/* v8 ignore next -- Loader validates the closed execution-mode union before selection. */
|
|
default: return assertNever(config.executionMode)
|
|
}
|
|
}
|