diff --git a/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml b/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml index 2c4d3e2909..c62a05a0b8 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml +++ b/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml @@ -24,8 +24,8 @@ en: 2a9fa3292bf07355 zh: db0b5684820915eb /deepseek-ai-dsh-ptc-runtime-node/use-this-package/in-memory-modes: - en: c82e89a3221bfa3f - zh: 5b37a48d50291a06 + en: 22c1d0a06eaebe80 + zh: af1c8f0e815906c9 /deepseek-ai-dsh-ptc-runtime-node/use-this-package/deadlines-and-cancellation: en: d383ec7aeab8afec zh: 7c9c49e205fbbcd8 @@ -39,8 +39,8 @@ en: f27d477e3ccb10de zh: 0f14a4899bae6c39 /deepseek-ai-dsh-ptc-runtime-node/understand-the-implementation/source-and-built-bootstraps: - en: 93c117a1e404ddca - zh: 6902de0d96efb03b + en: 24a5864c1552c69a + zh: a5406693d86d6183 /deepseek-ai-dsh-ptc-runtime-node/understand-the-implementation/runner-import-handling: en: e2458b4653a620bb zh: 4ae26bd1e6a69430 diff --git a/packages/ptc-runtime/ptc-runtime-node/README.md b/packages/ptc-runtime/ptc-runtime-node/README.md index 745df68fa8..f4ecfdfc6f 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.md +++ b/packages/ptc-runtime/ptc-runtime-node/README.md @@ -91,7 +91,7 @@ The `persistent-in-memory` mode and Script mode with `scriptPersistence: true` r executionMode: persistent-in-memory ``` -Save the patch and pass its path to `dsh headless --patch ` in a composition with PTC tool presentation. The `per-call` mode and Script mode with `scriptPersistence: false` retain no program memory between calls. Changing the configuration reloads the provider and releases its execution resources. Packaged single-executable hosts require a `node-script` launch with `launch.executable` pointing to Node and `launch.bootstrapPath` pointing to this package's built `process.js` for in-memory execution. +Save the patch and pass its path to `dsh headless --patch ` in a composition with PTC tool presentation. The `per-call` mode and Script mode with `scriptPersistence: false` retain no program memory between calls. Changing the configuration reloads the provider and releases its execution resources. Packaged single-executable hosts use their embedded carrier by default; the worker selects `persistent-process.js` for retained execution. Remote script deployments use a `node-script` launch with `launch.bootstrapPath` pointing to this package's built `persistent-process.js`. In REPL mode, each live Agent keeps its own process and top-level variables, functions, and imported modules across `run_code` calls and turns. Use `await import(...)`, `console.log(...)`, and a final JSON expression; top-level `return` is not supported. Assignments to undeclared names create retained globals under Node REPL’s non-strict evaluation. `_` and `_error` are ordinary retained globals, without interactive REPL result setters. Node REPL declaration rules apply, including errors on redeclaring existing `let`, `const`, or `class` names; assign to a mutable binding or choose a new name. A leading `{ ... }` is a statement block and can contain top-level await and temporary declarations; parenthesize an object expression to return it. Retained functions and saved tool methods call the current evaluation's bindings. All tool calls must be awaited; callbacks from a completed evaluation cannot invoke tools in a later evaluation. @@ -137,7 +137,7 @@ Per-call completion leaves unfinished binding waits pending and stops binding ca ### Source and built bootstraps -Source execution loads an erasable-only bootstrap closure without relying on sibling built exports; its evaluator modules and installed Acorn and birpc dependencies must be available in the execution world. Built script execution uses `process.js` for every mode; `persistent-process.js` delegates to the same loop with a REPL default for hosts that omit program syntax. Persistent mode requires a `node-script` launch. A remote script deployment supplies the matching `launch.bootstrapPath`. Embedded carriers launch their private worker through `launch.executable`. `resolveLaunch()` selects the local default during construction; an explicit remote invocation never depends on whether the client itself is packaged. A host path is never assumed to name the same remote file. +Source execution loads an erasable-only bootstrap closure without relying on sibling built exports; its evaluator modules and installed Acorn and birpc dependencies must be available in the execution world. Built script execution uses `process.js` for per-call programs or `persistent-process.js` for retained execution; both delegate to the same loop with a REPL default for hosts that omit program syntax. Embedded carriers launch their private worker through `launch.executable` and set `DSH_PTC_RUNTIME_NODE=persistent` for retained execution. A remote script deployment supplies the matching `launch.bootstrapPath`. `resolveLaunch()` selects the local default during construction; an explicit remote invocation never depends on whether the client itself is packaged. A host path is never assumed to name the same remote file. Source containing `await` is checked for top-level await before native compilation, which can otherwise treat `await` as an identifier. Acorn parses an async wrapper, and acorn-walk traverses it using Node's REPL visitors, declaration hoisting and character-position edits. Computed class-method keys are traversed while method bodies remain separate evaluations. The transform boxes the final expression; the evaluator unwraps and awaits its value. Source that needs no lowering uses native compilation; submissions without `await` retain native syntax diagnostics. Syntax errors preserve the parser message and available source location and caret, without compiler or transformer stack frames. Dynamic `import()` expressions remain native and use the compiling Script's `importModuleDynamically` option. Both private process bundles include Acorn, acorn-walk and birpc; source execution imports the installed packages. diff --git a/packages/ptc-runtime/ptc-runtime-node/README.zh.md b/packages/ptc-runtime/ptc-runtime-node/README.zh.md index ff793aa587..c841dd46b7 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.zh.md +++ b/packages/ptc-runtime/ptc-runtime-node/README.zh.md @@ -91,7 +91,7 @@ profile 编辑器可从 `@deepseek-ai/dsh-ptc-runtime-node/configuration` 导入 executionMode: persistent-in-memory ``` -保存补丁,在使用 PTC 工具展示的配置中通过 `dsh headless --patch ` 加载。`per-call` 模式和 `scriptPersistence: false` 的 Script 模式不跨调用保留程序内存。修改配置会重载提供方并释放其执行资源。单文件打包宿主使用内存保留模式时,需要使用 `node-script` 启动方式,将 `launch.executable` 指向 Node,并将 `launch.bootstrapPath` 指向本包构建出的 `process.js`。 +保存补丁,在使用 PTC 工具展示的配置中通过 `dsh headless --patch ` 加载。`per-call` 模式和 `scriptPersistence: false` 的 Script 模式不跨调用保留程序内存。修改配置会重载提供方并释放其执行资源。单文件打包宿主默认使用内嵌载体,保留模式由 worker 选择 `persistent-process.js`;远端脚本部署使用 `node-script` 启动方式,并将 `launch.bootstrapPath` 指向本包构建出的 `persistent-process.js`。 在 REPL 模式下,每个存活 Agent 保留独立进程,跨 `run_code` 调用和轮次保留顶层变量、函数及导入模块。使用 `await import(...)`、`console.log(...)` 和末尾 JSON 表达式;不支持顶层 `return`。Node REPL 使用非严格模式,给未声明的名称赋值会创建保留的全局变量。`_` 和 `_error` 是普通的保留全局变量,不使用交互式 REPL 的结果设置器。声明遵循 Node REPL 规则,包括重复声明已有 `let`、`const` 或 `class` 名称时报错;应给可变绑定赋值或选择新名称。开头的 `{ ... }` 按语句块执行,可以包含顶层 await 和临时声明;需要返回对象表达式时,用圆括号包裹。保留的函数和工具方法引用调用当前执行的绑定。必须等待所有工具调用;已完成执行的回调不能在后续执行中调用工具。 @@ -137,7 +137,7 @@ Host 通过有序控制管道上的 birpc 调用子进程的 `run()`。同一时 ### 源代码与构建后 bootstrap -源代码执行加载仅含可擦除语法的 bootstrap 依赖,不依赖同级包的构建后导出;执行世界中必须提供求值器模块和已安装的 Acorn 与 birpc 依赖。构建后脚本在所有模式下都使用 `process.js`;`persistent-process.js` 委托同一个循环,供未指定程序语法的宿主默认使用 REPL。保留模式要求 `node-script` 启动方式。远端脚本部署提供对应的 `launch.bootstrapPath`。内嵌载体通过 `launch.executable` 启动私有 worker。`resolveLaunch()` 在构造时选择本地默认值;显式的远端调用不依赖客户端自身是否打包。不会假设 Host 路径对应同一个远程文件。 +源代码执行加载仅含可擦除语法的 bootstrap 依赖,不依赖同级包的构建后导出;执行世界中必须提供求值器模块和已安装的 Acorn 与 birpc 依赖。构建后脚本对一次执行使用 `process.js`,对保留执行使用 `persistent-process.js`;两者委托同一个循环,供未指定程序语法的宿主默认使用 REPL。内嵌载体通过 `launch.executable` 启动私有 worker,并在保留模式下设置 `DSH_PTC_RUNTIME_NODE=persistent`。远端脚本部署提供对应的 `launch.bootstrapPath`。`resolveLaunch()` 在构造时选择本地默认值;显式的远端调用不依赖客户端自身是否打包。不会假设 Host 路径对应同一个远程文件。 包含 `await` 的源码会在原生编译前检查顶层 await,否则原生编译可能将 `await` 当作标识符。Acorn 解析包住源码的异步函数,acorn-walk 沿用 Node REPL 的 visitor、声明提升和按字符位置修改源码的方式进行遍历。类方法的计算属性名参与遍历,方法体仍独立求值。转换包装末尾表达式,求值器解包并等待其值。无需转换的源码使用原生编译;不含 `await` 的提交保留原生语法诊断。语法错误保留解析器消息、可用的源码位置与光标,不包含编译器或转换器的调用帧。动态 `import()` 表达式保持原生形式,使用编译该 Script 时的 `importModuleDynamically` 选项。两个私有进程 bundle 都包含 Acorn、acorn-walk 和 birpc;源码执行导入已安装的包。 diff --git a/packages/ptc-runtime/ptc-runtime-node/package.json b/packages/ptc-runtime/ptc-runtime-node/package.json index e10075f7fd..dd3bcebbca 100644 --- a/packages/ptc-runtime/ptc-runtime-node/package.json +++ b/packages/ptc-runtime/ptc-runtime-node/package.json @@ -27,6 +27,10 @@ "types": "./lib/types/process-entry.d.ts", "default": "./lib/process.js" }, + "./persistent-process": { + "types": "./lib/types/persistent-process-entry.d.ts", + "default": "./lib/persistent-process.js" + }, "./locale/*.json": "./locale/*.json" }, "files": [ diff --git a/packages/ptc-runtime/ptc-runtime-node/src/execution.ts b/packages/ptc-runtime/ptc-runtime-node/src/execution.ts index 1ae7fde1ab..b4162212dc 100644 --- a/packages/ptc-runtime/ptc-runtime-node/src/execution.ts +++ b/packages/ptc-runtime/ptc-runtime-node/src/execution.ts @@ -52,9 +52,7 @@ class NodeExecution { constructor( private readonly ctx: Context, private readonly config: ResolvedConfig, private readonly processes: PolicyProcesses, readonly mode: ExecutionMode, - ) { - if (mode.retainsState && this.config.launch.kind === 'embedded') throw new Error('Persistent PTC requires a node-script launch') - } + ) {} /** Stop every owned process, retaining failed cleanup for a later retry. */ async dispose(): Promise { diff --git a/packages/ptc-runtime/ptc-runtime-node/src/launch.ts b/packages/ptc-runtime/ptc-runtime-node/src/launch.ts index 8bd425b800..1931eaf4be 100644 --- a/packages/ptc-runtime/ptc-runtime-node/src/launch.ts +++ b/packages/ptc-runtime/ptc-runtime-node/src/launch.ts @@ -38,13 +38,11 @@ export function resolveLaunch(config?: LaunchConfig): LaunchConfig { * @param fs - Filesystem mapping host bootstrap assets into the process world. * @param config - resolved worker invocation. * @param maxMessageBytes - Validated frame and queued-write limit. - * @param persistent - Whether the selected mode requires retained-process launch support. * @returns Arguments following the resolved Node executable. */ -export function bootstrapArgs(fs: Pick, config: LaunchConfig, maxMessageBytes: number, persistent = false): string[] { +export function bootstrapArgs(fs: Pick, config: LaunchConfig, maxMessageBytes: number): string[] { switch (config.kind) { case 'embedded': - if (persistent) throw new Error('Persistent PTC requires a node-script launch') return [String(maxMessageBytes)] case 'node-script': return ['--experimental-vm-modules', ...scriptBootstrapArgs(fs, config.bootstrapPath, maxMessageBytes)] /* v8 ignore next -- Callers supply the resolved, closed LaunchConfig union. */ diff --git a/packages/ptc-runtime/ptc-runtime-node/src/worker.ts b/packages/ptc-runtime/ptc-runtime-node/src/worker.ts index 17708de189..6390f36323 100644 --- a/packages/ptc-runtime/ptc-runtime-node/src/worker.ts +++ b/packages/ptc-runtime/ptc-runtime-node/src/worker.ts @@ -282,7 +282,7 @@ export class ProgramWorker { const packaged = this.config.launch.kind === 'embedded' const heapFlag = `--max-old-space-size=${this.config.maxOldGenerationSizeMb}` const argv = [executable, ...packaged ? [] : [heapFlag], - ...bootstrapArgs(this.ctx.fs, this.config.launch, this.config.maxMessageBytes, this.retainsState)] + ...bootstrapArgs(this.ctx.fs, this.config.launch, this.config.maxMessageBytes)] const policy = this.policy this.confined = policy.mode === 'danger-full-access' ? undefined : await this.ctx.sandbox.confine(argv, { ...policy, mode: policy.mode }, this.controller.signal) // oxlint-disable-next-line typescript/no-unnecessary-condition -- Confinement may yield while disposal closes the process. @@ -291,7 +291,7 @@ export class ProgramWorker { .filter(key => !STARTUP_ENVIRONMENT_NAMES.has(key.toUpperCase()) && key.toUpperCase() !== 'ELECTRON_RUN_AS_NODE') .map(key => [key, undefined])) if (packaged) { - env.DSH_PTC_RUNTIME_NODE = '1' + env.DSH_PTC_RUNTIME_NODE = this.retainsState ? 'persistent' : '1' env.NODE_OPTIONS = `${heapFlag} --experimental-vm-modules` } const handle = this.ctx.subprocess.spawn({ argv: this.confined?.argv ?? argv, cwd, env, stdio: { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe', control: 'pipe' }, graceMs: this.config.graceMs }) diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/embedded-carrier.e2e.ts b/packages/ptc-runtime/ptc-runtime-node/tests/embedded-carrier.e2e.ts new file mode 100644 index 0000000000..7831b099fb --- /dev/null +++ b/packages/ptc-runtime/ptc-runtime-node/tests/embedded-carrier.e2e.ts @@ -0,0 +1,31 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { Context } from '@deepseek-ai/cordis' +import { describe, expect, it, onTestFinished } from 'vitest' +import { mountRuntime } from './setup.ts' + +const runtimePath = process.env.DSH_PACKAGED_RUNTIME + +describe.skipIf(runtimePath === undefined)('packaged embedded carrier', () => { + it('retains state across evaluations through the packaged runtime', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'dsh-packaged-ptc-')) + const ctx = new Context() + onTestFinished(async () => { await ctx.fiber.dispose(); await rm(cwd, { recursive: true, force: true }) }) + const { runtime } = await mountRuntime(ctx, { + executionMode: 'persistent-in-memory', + launch: { kind: 'embedded', executable: runtimePath as string }, + }) + const owner = {} + const run = (program: string) => + runtime.run(runtime.resolve({ program, bindings: [], cwd, stateOwner: owner })) + + expect((await run('var retained = 40; retained')).value).toBe(40) + expect((await run('retained += 2; retained')).value).toBe(42) + // pkg exposes the packaged tree at this URL; the CUA provider prompt uses the same scheme. + const bridge = await run('var bridge = await import("file:///snapshot/node/node_modules/@deepseek-ai/dsh-cua/lib/bridge.js"); typeof bridge.connectCua') + expect(bridge.value).toBe('function') + await runtime.release(owner) + expect((await run('typeof retained')).value).toBe('undefined') + }, 120_000) +}) diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/host-failures.spec.ts b/packages/ptc-runtime/ptc-runtime-node/tests/host-failures.spec.ts index 40a85c4a92..a8338a52ba 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/host-failures.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/host-failures.spec.ts @@ -501,6 +501,22 @@ describe('Node runtime host failures', () => { expect(Object.fromEntries(Object.entries(spec?.env ?? {}).filter(([, value]) => value !== undefined))) .toEqual({ DSH_PTC_RUNTIME_NODE: '1', NODE_OPTIONS: '--max-old-space-size=512 --experimental-vm-modules' }) }) + + it('selects the persistent embedded bootstrap for retained state', async () => { + const h = await setup({ + executionMode: 'persistent-in-memory', + maxMessageBytes: 2048, + launch: { kind: 'embedded', executable: '/remote/helper' }, + }) + h.onBoot(() => { h.complete({ value: encodePtcJsonWire(1) }) }) + const owner = {} + expect((await h.start({ program: '1', bindings: [], stateOwner: owner })).error).toBeUndefined() + const spec = h.spawn.mock.calls[0]?.[0] + expect(spec?.env?.DSH_PTC_RUNTIME_NODE).toBe('persistent') + expect(spec?.argv).toEqual([process.execPath, '2048']) + expect(Object.fromEntries(Object.entries(spec?.env ?? {}).filter(([, value]) => value !== undefined))) + .toEqual({ DSH_PTC_RUNTIME_NODE: 'persistent', NODE_OPTIONS: '--max-old-space-size=512 --experimental-vm-modules' }) + }) }) it('keeps failed per-call cleanup registered until process observation succeeds', async () => { diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/launch.spec.ts b/packages/ptc-runtime/ptc-runtime-node/tests/launch.spec.ts index ac6c620b88..fdbe674766 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/launch.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/launch.spec.ts @@ -34,9 +34,9 @@ it('resolves the local carrier once while preserving an explicit remote script c expect(resolveLaunch()).toEqual({ kind: 'node-script', executable: process.execPath }) }) -it('requires a script launch for persistent processes and selects their bootstrap', () => { - expect(() => bootstrapArgs(noMapping, { kind: 'embedded', executable: '/remote/helper' }, 2048, true)) - .toThrow('requires a node-script launch') - expect(bootstrapArgs(noMapping, { kind: 'node-script', executable: '/remote/node', bootstrapPath: '/remote/persistent-process.js' }, 2048, true)) +it('selects embedded and script carriers without a persistent-only launch restriction', () => { + expect(bootstrapArgs(noMapping, { kind: 'embedded', executable: '/remote/helper' }, 2048)) + .toEqual(['2048']) + expect(bootstrapArgs(noMapping, { kind: 'node-script', executable: '/remote/node', bootstrapPath: '/remote/persistent-process.js' }, 2048)) .toEqual(['--experimental-vm-modules', '/remote/persistent-process.js', '2048']) }) diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/persistent.spec.ts b/packages/ptc-runtime/ptc-runtime-node/tests/persistent.spec.ts index 8edc48c196..68cd9a6afb 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/persistent.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/persistent.spec.ts @@ -372,13 +372,14 @@ describe.each([ expect((await run('typeof retained')).value).toBe('undefined') }) - it('rejects a packaged host without a persistent bootstrap during provider loading', async () => { + it('accepts an embedded carrier for a packaged host', async () => { const previous = Object.getOwnPropertyDescriptor(process, 'pkg') try { Object.defineProperty(process, 'pkg', { configurable: true, value: {} }) - await expect(setup()).rejects.toThrow('requires a node-script launch') - const { runtime } = await setup({ launch: { kind: 'node-script', executable: process.execPath, bootstrapPath: join(process.cwd(), 'persistent-process.js') } }) - expect(runtime.programFormat).toBe(format) + const embedded = await setup() + expect(embedded.runtime.programFormat).toBe(format) + const scripted = await setup({ launch: { kind: 'node-script', executable: process.execPath, bootstrapPath: join(process.cwd(), 'persistent-process.js') } }) + expect(scripted.runtime.programFormat).toBe(format) } finally { if (previous === undefined) Reflect.deleteProperty(process, 'pkg') else Object.defineProperty(process, 'pkg', previous) diff --git a/packages/ssh/ssh-helper-runtime/src/index.ts b/packages/ssh/ssh-helper-runtime/src/index.ts index ecd0e20299..6ff0f8a80c 100644 --- a/packages/ssh/ssh-helper-runtime/src/index.ts +++ b/packages/ssh/ssh-helper-runtime/src/index.ts @@ -27,9 +27,15 @@ export async function runSshHelperRuntime(): Promise { const selection = process.env.DSH_SUBPROCESS_RUNNER Reflect.deleteProperty(process.env, 'DSH_SUBPROCESS_RUNNER') - if (process.env.DSH_PTC_RUNTIME_NODE === '1') { + if (process.env.DSH_PTC_RUNTIME_NODE !== undefined) { + const mode = process.env.DSH_PTC_RUNTIME_NODE Reflect.deleteProperty(process.env, 'DSH_PTC_RUNTIME_NODE') - await import('@deepseek-ai/dsh-ptc-runtime-node/process') + if (mode !== '1' && mode !== 'persistent') { + throw new Error('DSH_PTC_RUNTIME_NODE must be 1 or persistent') + } + await import(mode === 'persistent' + ? '@deepseek-ai/dsh-ptc-runtime-node/persistent-process' + : '@deepseek-ai/dsh-ptc-runtime-node/process') } else if (selection !== undefined) { const { runSelectedSubprocessRunner } = await import('@deepseek-ai/dsh-subprocess-local/runner') await runSelectedSubprocessRunner(selection) diff --git a/python/sdk-runtime/runtime-bootstrap.mjs b/python/sdk-runtime/runtime-bootstrap.mjs index e84efa7dc3..fb79f52233 100644 --- a/python/sdk-runtime/runtime-bootstrap.mjs +++ b/python/sdk-runtime/runtime-bootstrap.mjs @@ -84,9 +84,15 @@ if (resource !== undefined) { } else if (aclRunner !== undefined && process.argv[2] === aclRunner) { process.argv.splice(1, 1) await import('@deepseek-ai/dsh-sandbox-windows-acl/runner') -} else if (process.env.DSH_PTC_RUNTIME_NODE === '1') { +} else if (process.env.DSH_PTC_RUNTIME_NODE !== undefined) { + const mode = process.env.DSH_PTC_RUNTIME_NODE Reflect.deleteProperty(process.env, 'DSH_PTC_RUNTIME_NODE') - await import('@deepseek-ai/dsh-ptc-runtime-node/process') + if (mode !== '1' && mode !== 'persistent') { + throw new Error('DSH_PTC_RUNTIME_NODE must be 1 or persistent') + } + await import(mode === 'persistent' + ? '@deepseek-ai/dsh-ptc-runtime-node/persistent-process' + : '@deepseek-ai/dsh-ptc-runtime-node/process') } else if (selection === undefined) { // A DSH_PRIMARY_RUNTIME set or inherited from the environment, including the empty opt-out, takes precedence over the // downloaded cache.