fix(ptc): preserve policy and evaluation lifecycle across memory runs
This commit is contained in:
1 parent
3a0c2ae3bb
commit
bf95e3a74b
33 files changed
+155
-118
No files matched your search
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: e42db40b879fdbf4
|
||||
zh: 6017da9c4da17835
|
||||
/agent-note-node-ptc-execution-modes-and-in-memory-state/decision:
|
||||
en: c2c41c4566fd4f8f
|
||||
zh: 780f2cb168f537e7
|
||||
en: 93922437c705143c
|
||||
zh: adeb0932fd3e07d7
|
||||
/agent-note-node-ptc-execution-modes-and-in-memory-state/alternatives-considered:
|
||||
en: d20d210ecfa615e4
|
||||
zh: 0136bc91993ce518
|
||||
|
||||
@@ -14,7 +14,7 @@ The existing `@deepseek-ai/dsh-ptc-runtime-node` plugin selects its execution mo
|
||||
|
||||
Mode names describe state retention rather than a process implementation or source syntax. `per-call` retains state for one invocation; `in-memory` retains it across invocations until the process or owner ends. `durable` names the deferred ability to save and restore execution state across restarts. It is not an accepted configuration value: a future implementation must define its recovery point, recoverable values and external-resource handling before exposing that mode. File confinement remains a separate property; in-memory execution currently rejects explicit sandbox policies.
|
||||
|
||||
One npm package and one plugin entry keep existing installation and loading paths stable. Communication, binding proxies, JSON encoding and output limits remain package-local modules. The Node provider delegates retained process ownership to an internal execution object, which registers no additional Cordis service. Per-call and in-memory execution keep their distinct lifecycle rules.
|
||||
One npm package and one plugin entry keep existing installation and loading paths stable. Communication, binding proxies, JSON encoding and output limits remain package-local modules. The Node provider delegates retained process ownership to an internal execution object, which registers no additional Cordis service. Per-call and in-memory execution keep their distinct lifecycle rules. `programFormat` describes source evaluation independently of retention: a REPL need not retain state. The runtime owns syntax, completion and retention instructions through `executionInstructions`; tools own call arguments, SDK declarations and image attachment. Workflow uses `programFormat` to wrap its program and refuses restricted file policies when the selected runtime cannot enforce them.
|
||||
|
||||
The Session records submitted programs, nested tool calls and returned output. It does not serialize the REPL heap or rerun earlier programs after restart, resume or fork: replaying automation could repeat external side effects. A missing variable is a normal program error, and the model-visible instructions require recreating variables and reacquiring resources. No environment identifier or new durable Session event is required because the provider exposes no unlogged model input.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
模式名称描述状态保留方式,不绑定进程实现或代码语法。`per-call` 只为一次调用保留状态;`in-memory` 跨调用保留状态,直到进程或所有者结束。`durable` 用于命名未来跨重启保存与恢复执行状态的能力。它不是可接受的配置值:实现必须先明确恢复点、可恢复的值以及外部资源的处理方式,才能公开这个模式。文件约束是独立属性;内存保留模式目前拒绝显式沙箱策略。
|
||||
|
||||
一个 npm 包和一个插件入口保持现有安装与加载路径稳定。通信、绑定代理、JSON 编码和输出限制继续作为包内模块复用。Node 提供方将保留进程的所有权交给内部执行对象,该对象不注册额外的 Cordis 服务。逐次执行和内存保留各自管理不同的生命周期。
|
||||
一个 npm 包和一个插件入口保持现有安装与加载路径稳定。通信、绑定代理、JSON 编码和输出限制继续作为包内模块复用。Node 提供方将保留进程的所有权交给内部执行对象,该对象不注册额外的 Cordis 服务。逐次执行和内存保留各自管理不同的生命周期。`programFormat` 描述代码求值形式,与状态保留独立:REPL 不必跨调用保留状态。运行时通过 `executionInstructions` 负责语法、完成值和状态保留指引;工具层负责调用参数、SDK 声明和图片附加。工作流根据 `programFormat` 包装程序;所选运行时无法执行受限文件策略时,工作流拒绝执行。
|
||||
|
||||
Session 记录提交的程序、嵌套工具调用和返回输出。它不序列化 REPL 堆,也不在重启、恢复或派生后重新执行旧程序,因为重放自动化可能重复外部操作。缺失变量产生普通程序错误,模型指引要求重新创建变量并获取资源。提供方没有引入日志之外的模型输入,因此不需要环境标识或新的持久 Session 事件。
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
en: 76fbbf0fe38aef10
|
||||
zh: 76fbbf0fe38aef10
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tools/run-code:
|
||||
en: 8358b1f3dd09e202
|
||||
zh: 0dbce569ba4b4c76
|
||||
en: 7d2c94c6353d5fb4
|
||||
zh: a7a9c93c5c28b47b
|
||||
/tool-schema-catalog/deepseek-ai-dsh-plan-mode:
|
||||
en: b500b530845e26a9
|
||||
zh: b500b530845e26a9
|
||||
|
||||
@@ -523,7 +523,7 @@ ask_user_question pauses the tool call until the active UI provider returns a hu
|
||||
|
||||
### `run_code`
|
||||
|
||||
Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return is program output — curate it. Image-bearing subtool results are attached after the run.
|
||||
Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the program to execute, and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Curate the program output. Image-bearing subtool results are attached after the run.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -531,7 +531,7 @@ Execute a TypeScript program against the available tools. Takes two required arg
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "The program: the body of an async TypeScript function."
|
||||
"description": "The TypeScript program to execute, following this tool's execution instructions."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
|
||||
@@ -527,7 +527,7 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类
|
||||
|
||||
### `run_code`
|
||||
|
||||
针对可用工具执行 TypeScript 程序。接受两个必填参数:`code`,即异步函数的**函数体**(仅使用可擦除语法;支持顶层 `await` 和 `return`);以及 `description`,简要说明该程序做什么。请根据系统提示词中的声明,以 `await tools.name(args)` 形式调用工具。只有打印或返回的内容属于程序输出,请谨慎筛选。含图片的子工具结果会在运行结束后附加。
|
||||
针对可用工具执行 TypeScript 程序。接受两个必填参数:`code`,即待执行的程序;以及 `description`,简要说明该程序做什么。请根据系统提示词中的声明,以 `await tools.name(args)` 形式调用工具。程序输出需要筛选。含图片的子工具结果会在运行结束后附加。
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -535,7 +535,7 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "The program: the body of an async TypeScript function."
|
||||
"description": "The TypeScript program to execute, following this tool's execution instructions."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 02fce53c21a33d74
|
||||
zh: 01110c4714dfd602
|
||||
/deepseek-ai-dsh-tools/use-this-package/configure-the-presentation-mode:
|
||||
en: f843dfaf6ab6e19e
|
||||
zh: 8e3b8816a4e51e7b
|
||||
en: cfab7b7ef652bbac
|
||||
zh: a29806515c9cc5b4
|
||||
/deepseek-ai-dsh-tools/use-this-package/restrict-tools-per-agent:
|
||||
en: c6f98b2283cc2e47
|
||||
zh: c2cdce5d13473f15
|
||||
|
||||
@@ -61,7 +61,7 @@ The unified schema DSL supports `string`, `number`, `integer`, `boolean`, `null`
|
||||
|
||||
### Configure the presentation mode
|
||||
|
||||
The `mode` config decides what the model sees: `native` (every visible schema), `ptc` (only `run_code` plus a generated SDK), or `both`.
|
||||
The `mode` config decides what the model sees: `native` (every visible schema), `ptc` (only `run_code` plus a generated SDK), or `both`. The runtime supplies evaluation syntax, output syntax and state-retention guidance through `executionInstructions`; this package owns tool-call rules, SDK declarations and image-result attachment.
|
||||
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-tools'
|
||||
|
||||
@@ -61,7 +61,7 @@ ctx.tools.register(defineTool({
|
||||
|
||||
### 配置呈现模式
|
||||
|
||||
`mode` 配置决定模型看到什么:`native`(每个可见 schema)、`ptc`(只有 `run_code` 加一份生成 SDK)或 `both`。
|
||||
`mode` 配置决定模型看到什么:`native`(每个可见 schema)、`ptc`(只有 `run_code` 加一份生成 SDK)或 `both`。运行时通过 `executionInstructions` 提供求值语法、输出方式和状态保留指引;本包负责工具调用规则、SDK 声明和图片结果附加。
|
||||
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-tools'
|
||||
|
||||
@@ -58,7 +58,7 @@ declare module '@deepseek-ai/dsh-llm' {
|
||||
*/
|
||||
const PTC_ONLY_INSTRUCTION = `\`${RUN_CODE_NAME}\` is the only tool you can call directly — a tool call naming any other tool fails. Reach every tool the SDK declares below from inside the program.`
|
||||
|
||||
const SDK_RENDERERS: Record<string, (schemas: ToolSdkSchema[], format?: PtcRuntime['programFormat']) => string> = {
|
||||
const SDK_RENDERERS: Record<string, (schemas: ToolSdkSchema[]) => string> = {
|
||||
typescript: renderToolsSdk,
|
||||
python: renderToolsSdkPy,
|
||||
} satisfies Record<PtcSdkLanguage, (schemas: ToolSdkSchema[]) => string>
|
||||
@@ -908,7 +908,7 @@ export class ToolRuntime extends Service {
|
||||
const render = SDK_RENDERERS[runtime.language]
|
||||
/* v8 ignore next -- requirePtcRuntime rejects an unknown language before this runs. */
|
||||
if (render === undefined) throw new Error(`dsh-tools: no SDK renderer for ${runtime.language}`)
|
||||
return render(this.sdkSchemas(context.scope), runtime.programFormat)
|
||||
return render(this.sdkSchemas(context.scope))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,43 +45,19 @@ interface RunCodeFlavor {
|
||||
readonly codeDescription: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The TypeScript flavor: the fallback for a schema read with no runtime
|
||||
* mounted ({@link resolveFlavor} owns which readers reach that). A real
|
||||
* assembly always resolves a runtime first, so the model never sees this
|
||||
* fallback outside its own language.
|
||||
*/
|
||||
const TYPESCRIPT_FLAVOR: RunCodeFlavor = {
|
||||
description:
|
||||
'Execute a TypeScript program against the available tools. Takes two required '
|
||||
+ 'arguments: `code`, the BODY of an async function (erasable syntax only; top-level '
|
||||
+ '`await` and `return` work), and `description`, a short summary of what the program '
|
||||
+ 'does. Call tools as `await tools.name(args)` per the declarations in the system '
|
||||
+ 'prompt. Only what you print or return is program output — curate it. Image-bearing '
|
||||
+ 'subtool results are attached after the run.',
|
||||
codeDescription: 'The program: the body of an async TypeScript function.',
|
||||
/** Keep tool-call rules here; the runtime owns evaluation syntax and state retention. */
|
||||
function runCodeFlavor(language: string): RunCodeFlavor {
|
||||
return {
|
||||
description: `Execute a ${language} program against the available tools. Takes two required arguments: `
|
||||
+ '`code`, the program to execute, and `description`, a short summary of what the program does. '
|
||||
+ 'Call tools as `await tools.name(args)` per the declarations in the system prompt. '
|
||||
+ 'Curate the program output. Image-bearing subtool results are attached after the run.',
|
||||
codeDescription: `The ${language} program to execute, following this tool's execution instructions.`,
|
||||
}
|
||||
}
|
||||
|
||||
const REPL_FLAVOR: RunCodeFlavor = {
|
||||
description: 'Execute TypeScript in an in-memory REPL. Top-level declarations and await are supported; only erasable TypeScript is accepted, with no enum or namespaces. Use console.log or a final JSON expression for output. Only printed values and that final expression become program output; curate them. Top-level return is not supported. Call tools as await tools.name(args) using the declarations in the system prompt. Image-bearing subtool results are attached after the run.',
|
||||
codeDescription: 'TypeScript REPL statements with optional top-level await and a final JSON expression.',
|
||||
}
|
||||
|
||||
/**
|
||||
* The Python flavor: the body of an async function, top-level `await` and
|
||||
* `return`, answer via `print` and/or the returned value, matching
|
||||
* {@link ./py-types.ts}'s SDK instructions.
|
||||
*/
|
||||
const PYTHON_FLAVOR: RunCodeFlavor = {
|
||||
description:
|
||||
'Execute a Python program against the available tools. Takes two required '
|
||||
+ 'arguments: `code`, the BODY of an async function (top-level `await` and `return` '
|
||||
+ 'work), and `description`, a short summary of what the program does. Call tools as '
|
||||
+ '`await tools.name(args)` per the declarations in the system prompt. Use '
|
||||
+ '`print(...)` and/or `return <value>` for program output — curate it. Image-bearing '
|
||||
+ 'subtool results are attached after the run.',
|
||||
codeDescription: 'The program: the body of an async Python function.',
|
||||
}
|
||||
const TYPESCRIPT_FLAVOR = runCodeFlavor('TypeScript')
|
||||
const PYTHON_FLAVOR = runCodeFlavor('Python')
|
||||
|
||||
/**
|
||||
* The languages PTC mode ships a presentation for. Both per-language tables —
|
||||
@@ -167,10 +143,6 @@ function resolveFlavor(peekRuntime: () => PtcRuntime | undefined): RunCodeFlavor
|
||||
const known = Object.keys(RUN_CODE_FLAVORS).map(name => JSON.stringify(name)).join(', ')
|
||||
throw new Error(`dsh-tools: no run_code schema flavor registered for runtime language ${JSON.stringify(runtime.language)} (known: ${known})`)
|
||||
}
|
||||
if (runtime.programFormat === 'repl') {
|
||||
if (runtime.language !== 'typescript') throw new Error('dsh-tools: REPL presentation requires TypeScript')
|
||||
return REPL_FLAVOR
|
||||
}
|
||||
return flavor
|
||||
}
|
||||
|
||||
|
||||
@@ -733,12 +733,12 @@ export function jsonSchemaToPy(schema: unknown): string {
|
||||
/** The fixed model-facing usage contract rendered above the declarations. */
|
||||
const SDK_INSTRUCTIONS = `## Writing code for run_code
|
||||
|
||||
\`run_code\` takes two required arguments: \`code\` — the body of an async Python function (top-level \`await\` and \`return\` both work) — and \`description\`, a short summary of what the program does. At run time exactly two of the names declared below are bound: \`tools\` and \`ToolCallError\`. Everything else is a STATIC STUB describing argument and return types — in particular the \`TypedDict\` classes do NOT exist at run time, so build arguments as plain \`dict\`/\`list\` JSON values: \`await tools.name({"field": 1})\`, never \`FooArgs(field=1)\`, which raises \`NameError\`. Inside the program:
|
||||
\`run_code\` takes two required arguments: \`code\` — a Python program following the execution instructions in its tool description — and \`description\`, a short summary of what the program does. At run time exactly two of the names declared below are bound: \`tools\` and \`ToolCallError\`. Everything else is a STATIC STUB describing argument and return types — in particular the \`TypedDict\` classes do NOT exist at run time, so build arguments as plain \`dict\`/\`list\` JSON values: \`await tools.name({"field": 1})\`, never \`FooArgs(field=1)\`, which raises \`NameError\`. Inside the program:
|
||||
|
||||
- Call tools as \`await tools.name(args)\` — subscript access for exotic, reserved, or underscore-leading names: \`await tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value (each method's return type below). Tool arguments must be lossless JSON.
|
||||
- A FAILED tool call raises \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose message is human-readable — wrap in \`try/except\` to handle and continue.
|
||||
- Independent read-only calls MAY overlap under \`asyncio.gather\` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with \`await\`.
|
||||
- Emit the run's answer with \`print(...)\` and/or a top-level \`return <value>\`; the returned value must be lossless JSON. Only what you print and return is program output. A successful tool result containing an image is attached after the run so you can inspect it on the next step; every other intermediate result stays out of the conversation, so extract just what you need.
|
||||
- Only values emitted as program output enter the conversation. A successful tool result containing an image is attached after the run so you can inspect it on the next step; every other intermediate result stays out of the conversation, so extract just what you need.
|
||||
|
||||
The available tools:`
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
|
||||
import type { PtcRuntime } from '@deepseek-ai/dsh-ptc-runtime'
|
||||
import { assertSupportedJsonSchema } from './json-schema.ts'
|
||||
import type { JsonSchemaNode, JsonSchemaScalar } from './json-schema.ts'
|
||||
/** Internal PTC mode projection: the model-facing schema plus the canonical output schema. */
|
||||
@@ -250,18 +249,16 @@ export function jsonSchemaToTs(schema: unknown, indent = 0): string {
|
||||
/** The fixed model-facing usage contract rendered above the declarations (see the PTC mode Agent Note's "What the model sees"). */
|
||||
const SDK_INSTRUCTIONS = `## Writing code for run_code
|
||||
|
||||
\`run_code\` takes two required arguments: \`code\` — the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped) — and \`description\`, a short summary of what the program does. The declarations below are SDK bindings for this program. A declaration does not make its name a directly callable tool; only names supplied as separate tool schemas may be called directly.`
|
||||
\`run_code\` takes two required arguments: \`code\` — a TypeScript program following the execution instructions in its tool description — and \`description\`, a short summary of what the program does. The declarations below are SDK bindings for this program. A declaration does not make its name a directly callable tool; only names supplied as separate tool schemas may be called directly.`
|
||||
|
||||
function programInstructions(output: string): string {
|
||||
return `Inside the program:
|
||||
const SDK_PROGRAM_INSTRUCTIONS = `Inside the program:
|
||||
|
||||
- Call tools as \`await tools.name(args)\` — quoted access for exotic names: \`tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
|
||||
- A FAILED tool call rejects with \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose \`message\` is human-readable — \`try/catch\` it to handle and continue.
|
||||
- Independent read-only calls MAY overlap under \`Promise.all\` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with \`await\`.
|
||||
- ${output} A successful tool result containing an image is attached after the run so you can inspect it on the next step; every other intermediate result stays out of the conversation, so extract just what you need.
|
||||
- Only values emitted as program output enter the conversation. A successful tool result containing an image is attached after the run so you can inspect it on the next step; every other intermediate result stays out of the conversation, so extract just what you need.
|
||||
|
||||
Program-only SDK bindings:`
|
||||
}
|
||||
|
||||
/** Whether one string schema accepts the literal used by the bash example. */
|
||||
function acceptsExampleString(schema: JsonSchemaNode | undefined, value: string): boolean {
|
||||
@@ -271,7 +268,7 @@ function acceptsExampleString(schema: JsonSchemaNode | undefined, value: string)
|
||||
}
|
||||
|
||||
/** Render the bash example only when its literal arguments satisfy the current parameter schema. */
|
||||
function renderBashExample(schemas: ToolSdkSchema[], format: PtcRuntime['programFormat']): string {
|
||||
function renderBashExample(schemas: ToolSdkSchema[]): string {
|
||||
const bash = schemas.find(schema => schema.name === 'bash')
|
||||
if (bash === undefined) return ''
|
||||
const parameters = bash.parameters as JsonSchemaNode
|
||||
@@ -282,7 +279,7 @@ function renderBashExample(schemas: ToolSdkSchema[], format: PtcRuntime['program
|
||||
const needsDescription = required.includes('description')
|
||||
if (needsDescription && !acceptsExampleString(parameters.properties?.description, 'Show current directory')) return ''
|
||||
const description = needsDescription ? ", description: 'Show current directory'" : ''
|
||||
return ` When no separate \`bash\` schema is supplied, invoke a declared \`bash\` binding inside \`run_code\`:\n\n\`run_code({ code: "${format === 'repl' ? '' : 'return '}await tools.bash({ command: 'pwd'${description} })", description: "Show current directory" })\``
|
||||
return ` When no separate \`bash\` schema is supplied, invoke a declared \`bash\` binding inside \`run_code\`:\n\n\`run_code({ code: "console.log(await tools.bash({ command: 'pwd'${description} }))", description: "Show current directory" })\``
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,10 +292,9 @@ function renderBashExample(schemas: ToolSdkSchema[], format: PtcRuntime['program
|
||||
* by name, so the input never carries a duplicate.
|
||||
* @param schemas - the tool schemas to declare (the caller excludes
|
||||
* `run_code` itself).
|
||||
* @param format - Whether the program is an async body or REPL statements.
|
||||
* @returns the complete section text.
|
||||
*/
|
||||
export function renderToolsSdk(schemas: ToolSdkSchema[], format: PtcRuntime['programFormat'] = 'function-body'): string {
|
||||
export function renderToolsSdk(schemas: ToolSdkSchema[]): string {
|
||||
const sorted = [...schemas].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
||||
const argsMembers: string[] = []
|
||||
const outputMembers: string[] = []
|
||||
@@ -317,11 +313,5 @@ export function renderToolsSdk(schemas: ToolSdkSchema[], format: PtcRuntime['pro
|
||||
['declare const tools: {', ' [K in ToolName]: (args: ToolArgsMap[K]) => Promise<ToolOutputMap[K]>;', '}'].join('\n'),
|
||||
].join('\n\n')
|
||||
const jsonValue = 'type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }'
|
||||
const instructions = format === 'repl'
|
||||
? '## Writing code for run_code\n\n`run_code` takes `code`, TypeScript REPL statements with erasable types (no `enum` or namespaces) and top-level `await`, and `description`, a short summary. Use `await import(...)` for modules. The declarations below describe program-only SDK bindings.'
|
||||
: SDK_INSTRUCTIONS
|
||||
const outputInstructions = programInstructions(format === 'repl'
|
||||
? 'Emit results with `console.log(...)` or a final JSON expression. Top-level `return` is not supported. Only printed values and that final expression become program output; curate them.'
|
||||
: 'Emit results with `return` and/or `console.log(...)`. Only what you print or return is program output.')
|
||||
return `${instructions}${renderBashExample(sorted, format)}\n\n${outputInstructions}\n\n\`\`\`ts\n${jsonValue}\n\n${declaration}\n\`\`\``
|
||||
return `${SDK_INSTRUCTIONS}${renderBashExample(sorted)}\n\n${SDK_PROGRAM_INSTRUCTIONS}\n\n\`\`\`ts\n${jsonValue}\n\n${declaration}\n\`\`\``
|
||||
}
|
||||
@@ -436,7 +436,7 @@ describe('mode-aware wire contribution', () => {
|
||||
const sdk = assembly.sections.find(section => section.name === 'tools:sdk')
|
||||
expect(sdk?.text).toContain('class Tools(Protocol):')
|
||||
expect(sdk?.text).toContain('async def echo(self, args:')
|
||||
expect(sdk?.text).toContain('top-level `await`')
|
||||
expect(sdk?.text).toContain('execution instructions in its tool description')
|
||||
})
|
||||
|
||||
it("assembles under a python runtime in mode 'both' as well, SDK and schema together", async () => {
|
||||
@@ -461,13 +461,13 @@ describe('mode-aware wire contribution', () => {
|
||||
const assembly = await systemPrompt.assemble()
|
||||
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
|
||||
expect(runCodeSchema?.description).toContain('Execute a TypeScript program')
|
||||
expect(runCodeSchema?.description).toContain('BODY of an')
|
||||
expect(runCodeSchema?.description).toContain('`code`, the program to execute')
|
||||
// Both required arguments are named here, not only in the parameter
|
||||
// schema: prose that describes the call as "pass the program" is what
|
||||
// leads a model to emit `{code}` alone and fail INVALID_ARGS.
|
||||
expect(runCodeSchema?.description).toContain('`description`')
|
||||
const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code
|
||||
expect(codeParam.description).toBe('The program: the body of an async TypeScript function.')
|
||||
expect(codeParam.description).toBe("The TypeScript program to execute, following this tool's execution instructions.")
|
||||
})
|
||||
|
||||
it('emits a Python-flavored run_code schema under a python runtime (matches the SDK language)', async () => {
|
||||
@@ -476,11 +476,11 @@ describe('mode-aware wire contribution', () => {
|
||||
const assembly = await systemPrompt.assemble()
|
||||
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
|
||||
expect(runCodeSchema?.description).toContain('Execute a Python program')
|
||||
expect(runCodeSchema?.description).toContain('`return <value>`')
|
||||
expect(runCodeSchema?.description).toContain('`code`')
|
||||
expect(runCodeSchema?.description).toContain('`description`')
|
||||
expect(runCodeSchema?.description).not.toContain('TypeScript')
|
||||
const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code
|
||||
expect(codeParam.description).toBe('The program: the body of an async Python function.')
|
||||
expect(codeParam.description).toBe("The Python program to execute, following this tool's execution instructions.")
|
||||
})
|
||||
|
||||
it('resolves the run_code schema flavor lazily and fails loud on a language absent from the flavor table', async () => {
|
||||
@@ -511,7 +511,7 @@ describe('mode-aware wire contribution', () => {
|
||||
const definition = ctx.tools.get(RUN_CODE_NAME)
|
||||
expect(definition?.description).toContain('Execute a TypeScript program')
|
||||
const params = definition?.parameters as { properties: { code: { description: string } } }
|
||||
expect(params.properties.code.description).toBe('The program: the body of an async TypeScript function.')
|
||||
expect(params.properties.code.description).toBe("The TypeScript program to execute, following this tool's execution instructions.")
|
||||
})
|
||||
|
||||
it("rejects the assembly when toolOrder names a native tool that mode 'ptc' no longer contributes", async () => {
|
||||
@@ -2385,7 +2385,7 @@ describe('PTC state ownership', () => {
|
||||
const format = vi.spyOn(runtime, 'programFormat', 'get').mockReturnValue(programFormat)
|
||||
const release = vi.spyOn(runtime, 'release')
|
||||
try {
|
||||
expect(ctx.tools.schemas().find(tool => tool.name === 'run_code')?.description).toContain(programFormat === 'repl' ? 'in-memory REPL' : 'async function')
|
||||
expect(ctx.tools.schemas().find(tool => tool.name === 'run_code')?.description).not.toContain('in-memory')
|
||||
expect((await runCode(ctx, 'let value = 1', { agent })).isError).toBe(false)
|
||||
expect((await runCode(ctx, 'value', { agent })).isError).toBe(false)
|
||||
expect(runtime.lastRequest?.stateOwner).toBe(agent)
|
||||
@@ -2396,10 +2396,15 @@ describe('PTC state ownership', () => {
|
||||
} finally { format.mockRestore(); release.mockRestore(); await agentContext.fiber.dispose(); await ctx.fiber.dispose() }
|
||||
})
|
||||
|
||||
it('rejects an unsupported language and REPL combination during schema assembly', async () => {
|
||||
it('keeps provider evaluation instructions independent of language and program format', async () => {
|
||||
const { ctx, runtime } = await setup({ runtime: { language: 'python' } })
|
||||
const format = vi.spyOn(runtime, 'programFormat', 'get').mockReturnValue('repl')
|
||||
try { expect(() => ctx.tools.schemas()).toThrow('REPL presentation requires TypeScript') }
|
||||
finally { format.mockRestore(); await ctx.fiber.dispose() }
|
||||
const instructions = vi.spyOn(runtime, 'executionInstructions', 'get').mockReturnValue('Each call evaluates Python expressions in a fresh environment.')
|
||||
try {
|
||||
const description = ctx.tools.schemas().find(tool => tool.name === 'run_code')?.description
|
||||
expect(description).toContain('Each call evaluates Python expressions in a fresh environment.')
|
||||
expect(description).not.toContain('in-memory')
|
||||
expect(description).not.toContain('TypeScript')
|
||||
} finally { format.mockRestore(); instructions.mockRestore(); await ctx.fiber.dispose() }
|
||||
})
|
||||
})
|
||||
@@ -151,7 +151,7 @@ describe('renderToolsSdkPy', () => {
|
||||
expect(text).toContain('# tools["my-mcp.tool"](args: dict[str, Any]) -> str')
|
||||
expect(text).toContain('# tools["class"](args: dict[str, Any]) -> str')
|
||||
// Fixed instruction lines the model relies on.
|
||||
expect(text).toContain('top-level `await`')
|
||||
expect(text).toContain('execution instructions in its tool description')
|
||||
// The binding boundary: `tools`/`ToolCallError` are bound, the TypedDicts
|
||||
// are not. Both halves are pinned — dropping either one turns a correct
|
||||
// contract into a wrong one (a model that reads only "STATIC STUB" would
|
||||
|
||||
@@ -146,7 +146,7 @@ describe('renderToolsSdk', () => {
|
||||
expect(text).toContain('[K in ToolName]: (args: ToolArgsMap[K]) => Promise<ToolOutputMap[K]>;')
|
||||
expect(text).toContain('/** Run a shell command. */')
|
||||
// The fixed instruction lines the model relies on.
|
||||
expect(text).toContain('erasable syntax only')
|
||||
expect(text).toContain('execution instructions in its tool description')
|
||||
expect(text).toContain('rejects with `ToolCallError`')
|
||||
expect(text).toContain('MAY overlap under `Promise.all`')
|
||||
expect(text).toContain('lossless JSON')
|
||||
@@ -165,7 +165,7 @@ describe('renderToolsSdk', () => {
|
||||
const text = renderToolsSdk([bash])
|
||||
expect(text).toContain('A declaration does not make its name a directly callable tool')
|
||||
expect(text).toContain('only names supplied as separate tool schemas may be called directly')
|
||||
expect(text).toContain('`run_code({ code: "return await tools.bash({ command: \'pwd\', description: \'Show current directory\' })"')
|
||||
expect(text).toContain('`run_code({ code: "console.log(await tools.bash({ command: \'pwd\', description: \'Show current directory\' }))"')
|
||||
expect(text).toContain('Program-only SDK bindings:')
|
||||
expect(text).not.toContain('The available tools:')
|
||||
})
|
||||
@@ -216,13 +216,12 @@ describe('renderToolsSdk', () => {
|
||||
expect(renderToolsSdk([constrained])).toContain('tools.bash({ command: \'pwd\' })')
|
||||
})
|
||||
|
||||
it('presents REPL statements and expression output without a top-level return example', () => {
|
||||
const text = renderToolsSdk([bash], 'repl')
|
||||
expect(text).toContain('TypeScript REPL statements')
|
||||
expect(text).toContain('final JSON expression')
|
||||
expect(text).toContain('await tools.bash')
|
||||
it('uses an output example accepted by both Node execution modes', () => {
|
||||
const text = renderToolsSdk([bash])
|
||||
expect(text).toContain('console.log(await tools.bash(')
|
||||
expect(text).not.toContain('return await tools.bash')
|
||||
expect(text).not.toContain('body of an async TypeScript function')
|
||||
expect(text).not.toContain('TypeScript REPL statements')
|
||||
})
|
||||
|
||||
it('is deterministic: same tool set, byte-identical text regardless of input order', () => {
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
en: ab4a08f301de599d
|
||||
zh: e698b35ccf3ce665
|
||||
/deepseek-ai-dsh-experimental-ptc-runtime-python/model-experience:
|
||||
en: 42064c81cda8f059
|
||||
zh: fa68d1acac16e263
|
||||
en: c509cebe744d72e1
|
||||
zh: 3d2f3dd286c1c8be
|
||||
/deepseek-ai-dsh-experimental-ptc-runtime-python/model-experience/kv-cache-effect:
|
||||
en: 40294c1c31b74824
|
||||
zh: d805c6bd7270505e
|
||||
|
||||
@@ -101,6 +101,8 @@ Read these when the runtime contract is not enough. They move from the seam defi
|
||||
|
||||
Indirectly, through PTC mode in `dsh-tools` when an explicit composition mounts this provider; it renders the program's completion value or failure into a retained `run_code` result, and no shipped profile mounts this experimental package.
|
||||
|
||||
The provider supplies async-function syntax, `print`/`return` output and fresh-process state rules through `executionInstructions`; the tool consumer appends them to `run_code`.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
No direct invalidation; the named consumer owns any request-prefix changes.
|
||||
|
||||
@@ -101,6 +101,8 @@ kind: "package-reference"
|
||||
|
||||
间接地,通过 `dsh-tools` 中的 PTC mode;当显式组合挂载本提供方时,它会把程序的完成值或失败渲染成保留的 `run_code` 结果,且已发布 profile 均不挂载这个实验包。
|
||||
|
||||
提供方通过 `executionInstructions` 说明异步函数语法、`print`/`return` 输出和每次启动新进程的状态规则;工具消费者将这些指引附加到 `run_code`。
|
||||
|
||||
#### KV Cache 效应
|
||||
|
||||
无直接失效;指定的消费方拥有任何请求前缀变化。
|
||||
|
||||
@@ -815,6 +815,10 @@ export class PythonPtcRuntime extends PtcRuntime {
|
||||
readonly language = 'python'
|
||||
readonly isolation = 'process'
|
||||
|
||||
override get executionInstructions(): string {
|
||||
return 'The program is the body of an async Python function. Top-level await and return are supported. Emit output with print(...) or return a lossless JSON value. Only printed values and the returned value become program output. Each call runs in a fresh Python process; variables and imported modules do not persist across calls.'
|
||||
}
|
||||
|
||||
private readonly config: ResolvedConfig
|
||||
private readonly pythonBin: string
|
||||
// The frame cap this instance enforces: the protocol cap, or the host's
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
/deepseek-ai-dsh-ptc-runtime-node/use-this-package/execution-and-results:
|
||||
en: c79baab0c0a49ca7
|
||||
zh: b90b78d0d75280fb
|
||||
/deepseek-ai-dsh-ptc-runtime-node/use-this-package/persistent-repl:
|
||||
en: cf58c5bd3ff65aa0
|
||||
zh: 4f8b020c21ca527b
|
||||
/deepseek-ai-dsh-ptc-runtime-node/use-this-package/in-memory-mode:
|
||||
en: 886ac7a18eb3263f
|
||||
zh: c38c86cfca8ce110
|
||||
/deepseek-ai-dsh-ptc-runtime-node/use-this-package/deadlines-and-cancellation:
|
||||
en: 74825f65e6b59013
|
||||
zh: 928b56f87309b8db
|
||||
en: 6014e01f41d1ec22
|
||||
zh: 33ddebf3cdb1f914
|
||||
/deepseek-ai-dsh-ptc-runtime-node/use-this-package/failures:
|
||||
en: 7682e50f9dcfa25c
|
||||
zh: ddcb3502e12be06f
|
||||
|
||||
@@ -66,7 +66,7 @@ In `per-call` mode, programs are async function bodies: top-level `await` and `r
|
||||
Direct filesystem, network and subprocess operations remain Node operations, subject to the selected OS sandbox. Nested host bindings cross the control channel; PTC tool calls retain the registry's visibility, ordering, logging and approval rules. Running a program does not change the Session's standing policy or automatically replay it after a denial.
|
||||
|
||||
<a id="persistent-repl"></a>
|
||||
### Persistent REPL
|
||||
### In-memory mode
|
||||
|
||||
Set `executionMode: in-memory` on the existing `@deepseek-ai/dsh-ptc-runtime-node` plugin to retain state. Both modes require `fs`, `subprocess`, `sandbox` and `sandboxPolicy`; in-memory execution does not apply a file sandbox and rejects explicit sandbox policies. Enable it deliberately with a profile patch:
|
||||
|
||||
@@ -78,15 +78,15 @@ Set `executionMode: in-memory` on the existing `@deepseek-ai/dsh-ptc-runtime-nod
|
||||
|
||||
Save the patch and pass its path to `dsh headless --patch <path>` in a composition with PTC tool presentation. The default `per-call` mode retains no program memory between calls. Changing the configuration reloads the provider and releases its execution resources. Packaged single-executable hosts require `nodeExecutable` pointing to Node and `bootstrapPath` pointing to this package's built `persistent-process.js` for in-memory execution.
|
||||
|
||||
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. 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.
|
||||
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.
|
||||
|
||||
Memory is best-effort and never saved to disk. Restart, Agent disposal, provider unload, cancellation, timeout, protocol failure, output overflow, or process exit clears it. Missing variables raise normal reference errors; reacquire handles and recreate declarations. Exceptions in the active evaluation settle once and preserve earlier mutations. Exceptions thrown by callbacks from a completed evaluation are not reported in a later result; await asynchronous work before completing an evaluation. The Session still records programs, tool dispatches and visible results; restoring a Session or forking an Agent starts with empty memory. Direct Node access remains unsandboxed; explicit sandbox policies are rejected.
|
||||
|
||||
Service consumers pass an object as `stateOwner` to reuse its environment and await `release(owner)` when finished. Requests without an owner run in temporary processes. Concurrent runs for one owner reject; distinct owners may run concurrently. An idle environment has no expiration timer and keeps its process until release or failure. Raw stdout/stderr have no evaluation identity: bytes received during an evaluation share its output budget, even when written by an earlier child process; idle bytes are discarded. Await child-process completion before ending an evaluation. Early process-exit diagnostics include the exit code and retained stderr tail.
|
||||
Service consumers pass an object as `stateOwner` to reuse its environment and await `release(owner)` when finished. Requests without an owner run in temporary processes. Concurrent runs for one owner reject until execution and required cleanup complete; distinct owners may run concurrently. An idle environment has no expiration timer and keeps its process until release or failure. Raw stdout/stderr have no evaluation identity: bytes received during an evaluation share its output budget, even when written by an earlier child process; idle bytes are discarded. Await child-process completion before ending an evaluation. Early process-exit diagnostics include the exit code and retained stderr tail.
|
||||
|
||||
### Deadlines and cancellation
|
||||
|
||||
The PTC consumer exposes per-call timeout and approved sandbox choices as described in [dsh-tools](../../core/tools/README.md#ptc-mode). The runtime's readonly `timeout` descriptor reports its effective default and maximum to that consumer. The per-call mode’s `executionInstructions` describes fresh Node state, direct Node APIs, the empty program environment and file policy in the model-visible schema.
|
||||
The PTC consumer exposes per-call timeout and approved sandbox choices as described in [dsh-tools](../../core/tools/README.md#ptc-mode). The runtime's readonly `timeout` descriptor reports its effective default and maximum to that consumer. The provider owns execution syntax, output syntax and state-retention guidance in `executionInstructions`. The tool consumer appends those mode-specific rules to its model-visible schema without inferring persistence from `programFormat`.
|
||||
|
||||
Omitting `timeoutMs` uses the configured elapsed default; numeric requests are validated and capped. Service callers can explicitly pass `timeoutMs: null` to omit the elapsed timer, as the workflow adapter does; `run_code` continues to accept only positive numeric overrides. An enabled deadline covers runtime setup and execution, including time awaiting nested tools or approval. It is not a CPU meter. Timeout or cancellation stops a synchronous loop through the host's managed process owner; successful per-call completion also cleans that managed range. The timer stops when an outcome is selected, before cleanup, so the returned call can take longer than its execution deadline while cleanup settles.
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ kind: "package-reference"
|
||||
直接文件系统、网络与子进程操作仍是 Node 操作,受所选 OS 沙箱约束。嵌套 Host 绑定通过控制通道调用;PTC 工具调用保留注册表的可见性、排序、日志和审批规则。运行程序不会改变 Session 的常设策略,也不会在拒绝后自动重放程序。
|
||||
|
||||
<a id="persistent-repl"></a>
|
||||
### 持久 REPL
|
||||
### 内存保留模式
|
||||
|
||||
在现有 `@deepseek-ai/dsh-ptc-runtime-node` 插件上设置 `executionMode: in-memory` 即可保留状态。两种模式都需要 `fs`、`subprocess`、`sandbox` 和 `sandboxPolicy`;内存保留模式不应用文件沙箱,并拒绝显式沙箱策略。通过配置档补丁显式启用:
|
||||
|
||||
@@ -78,15 +78,15 @@ kind: "package-reference"
|
||||
|
||||
保存补丁,在使用 PTC 工具展示的配置中通过 `dsh headless --patch <path>` 加载。默认的 `per-call` 模式不跨调用保留程序内存。修改配置会重载提供方并释放其执行资源。单文件打包宿主使用内存保留模式时,需要将 `nodeExecutable` 指向 Node,并将 `bootstrapPath` 指向本包构建出的 `persistent-process.js`。
|
||||
|
||||
每个存活 Agent 保留独立进程,跨 `run_code` 调用和轮次保留顶层变量、函数及导入模块。使用 `await import(...)`、`console.log(...)` 和末尾 JSON 表达式;不支持顶层 `return`。Node REPL 使用非严格模式,给未声明的名称赋值会创建保留的全局变量。声明遵循 Node REPL 规则,包括重复声明已有 `let`、`const` 或 `class` 名称时报错;应给可变绑定赋值或选择新名称。开头的 `{ ... }` 按语句块执行,可以包含顶层 await 和临时声明;需要返回对象表达式时,用圆括号包裹。保留的函数和工具方法引用调用当前执行的绑定。必须等待所有工具调用;已完成执行的回调不能在后续执行中调用工具。
|
||||
每个存活 Agent 保留独立进程,跨 `run_code` 调用和轮次保留顶层变量、函数及导入模块。使用 `await import(...)`、`console.log(...)` 和末尾 JSON 表达式;不支持顶层 `return`。Node REPL 使用非严格模式,给未声明的名称赋值会创建保留的全局变量。`_` 和 `_error` 是普通的保留全局变量,不使用交互式 REPL 的结果设置器。声明遵循 Node REPL 规则,包括重复声明已有 `let`、`const` 或 `class` 名称时报错;应给可变绑定赋值或选择新名称。开头的 `{ ... }` 按语句块执行,可以包含顶层 await 和临时声明;需要返回对象表达式时,用圆括号包裹。保留的函数和工具方法引用调用当前执行的绑定。必须等待所有工具调用;已完成执行的回调不能在后续执行中调用工具。
|
||||
|
||||
内存按尽力保留处理,不保存到磁盘。重启、Agent 销毁、提供方卸载、取消、超时、协议失败、输出超限或进程退出都会清除内存。缺失变量产生普通引用错误,模型需要重新获取句柄和创建声明。当前执行中的异常只结算一次,并保留此前发生的修改。已完成执行的回调抛出的异常不会在后续结果中报告;结束执行前应等待异步工作完成。Session 仍记录程序、工具分派和可见结果;恢复 Session 或派生 Agent 时从空内存开始。直接 Node 访问没有沙盒约束;显式沙盒策略会被拒绝。
|
||||
|
||||
服务消费者通过 `stateOwner` 对象复用环境,并在结束时等待 `release(owner)`。没有所有者的请求在临时进程中执行。同一所有者的并发调用会被拒绝;不同所有者可以并发。空闲环境没有过期计时器,进程保留到释放或失败为止。原始 stdout/stderr 不携带执行标识:执行期间收到的字节计入本次输出预算,包括较早启动的子进程此时写出的内容;空闲期间收到的字节会被丢弃。结束本次执行前,应等待子进程完成。进程提前退出的诊断包含退出码和保留的 stderr 尾部。
|
||||
服务消费者通过 `stateOwner` 对象复用环境,并在结束时等待 `release(owner)`。没有所有者的请求在临时进程中执行。执行及必要清理完成之前,同一所有者的并发调用会被拒绝;不同所有者可以并发。空闲环境没有过期计时器,进程保留到释放或失败为止。原始 stdout/stderr 不携带执行标识:执行期间收到的字节计入本次输出预算,包括较早启动的子进程此时写出的内容;空闲期间收到的字节会被丢弃。结束本次执行前,应等待子进程完成。进程提前退出的诊断包含退出码和保留的 stderr 尾部。
|
||||
|
||||
### 截止时间与取消
|
||||
|
||||
PTC 消费方按 [dsh-tools](../../core/tools/README.zh.md#ptc-mode) 的说明公开逐次超时与经审批的沙箱选择。运行时只读 `timeout` 描述符向该消费方报告有效默认值与上限。逐次执行模式的 `executionInstructions` 在面向模型的 schema 中说明全新 Node 状态、直接 Node API、空程序环境和文件策略。
|
||||
PTC 消费方按 [dsh-tools](../../core/tools/README.zh.md#ptc-mode) 的说明公开逐次超时与经审批的沙箱选择。运行时只读 `timeout` 描述符向该消费方报告有效默认值与上限。提供方通过 `executionInstructions` 负责求值语法、输出方式和状态保留指引。工具消费者将对应模式的规则附加到面向模型的 schema,不根据 `programFormat` 推断是否保留状态。
|
||||
|
||||
省略 `timeoutMs` 使用配置的经过时间默认值;数值请求经过验证并封顶。服务调用方可以显式传入 `timeoutMs: null` 来省略经过时间定时器,工作流适配器即如此;`run_code` 仍只接受正数覆盖值。启用的截止覆盖运行时准备和执行,包括等待嵌套工具或审批的时间。它不是 CPU 计量器。超时或取消通过 Host 的受管进程所有者停止同步循环;逐次执行成功完成也会清理该受管范围。选择结果后、清理前停止计时器,因此调用可能要在执行截止之后等待清理结算才返回。
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export class NodePtcRuntime extends PtcRuntime {
|
||||
override get programFormat(): 'function-body' | 'repl' { return this.inMemory ? 'repl' : 'function-body' }
|
||||
override get executionInstructions(): string {
|
||||
if (this.inMemory) return this.inMemory.executionInstructions
|
||||
return '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.'
|
||||
return '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. Emit output with console.log(...) or return a lossless JSON value. Only printed values and the returned value become program output. 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.'
|
||||
}
|
||||
private readonly config: ResolvedConfig
|
||||
private readonly live = new Set<LiveRun>()
|
||||
|
||||
@@ -49,6 +49,10 @@ export async function runPersistentMain(stream: Duplex, maxMessageBytes: number,
|
||||
callback()
|
||||
} })
|
||||
const repl = start({ input, output, terminal: false, prompt: '', useGlobal: false, ignoreUndefined: true })
|
||||
// Interactive result setters print notices; program assignments use ordinary retained globals.
|
||||
for (const name of ['_', '_error']) {
|
||||
Object.defineProperty(repl.context, name, { configurable: true, enumerable: true, writable: true, value: undefined })
|
||||
}
|
||||
let current: Evaluation | undefined
|
||||
const send = (id: number, payload: ProgramToHost): void => {
|
||||
void channel.send({ id, payload }).catch(() => { channel.close(); closed.resolve() })
|
||||
|
||||
@@ -38,6 +38,8 @@ class Kernel {
|
||||
private handle?: SubprocessHandle
|
||||
private channel?: JsonChannel
|
||||
private active: Evaluation | undefined
|
||||
/** Admission remains reserved until the result includes completed process cleanup. */
|
||||
private running = false
|
||||
private ready = false
|
||||
private sequence = 0
|
||||
private nextCall = 1
|
||||
@@ -51,11 +53,15 @@ class Kernel {
|
||||
constructor(private readonly ctx: Context, private readonly config: ResolvedConfig) {}
|
||||
|
||||
assertIdle(): void {
|
||||
if (this.active) throw new Error('persistent PTC does not accept concurrent runs for one state owner')
|
||||
if (this.running) throw new Error('persistent PTC does not accept concurrent runs for one state owner')
|
||||
}
|
||||
|
||||
async run(spec: PtcRunSpec, bindings: Map<string, PtcBindingNamespace>, code: string): Promise<PtcRunResult> {
|
||||
this.assertIdle()
|
||||
this.running = true
|
||||
try { return await this.evaluate(spec, bindings, code) } finally { this.running = false }
|
||||
}
|
||||
|
||||
private async evaluate(spec: PtcRunSpec, bindings: Map<string, PtcBindingNamespace>, code: string): Promise<PtcRunResult> {
|
||||
if (this.exitOutcome !== undefined) {
|
||||
const output = persistentOutput(this.config.maxOutputBytes)
|
||||
const failure: PtcRunFailure = spec.signal?.aborted
|
||||
@@ -244,7 +250,7 @@ export class InMemoryExecution {
|
||||
* @returns Usage guidance appended to the run_code schema.
|
||||
*/
|
||||
get executionInstructions(): string {
|
||||
return 'Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. This environment has full host access without a file sandbox; process.env starts empty. Restart, Agent disposal, cancellation, timeout, output overflow, or process failure clears the environment. If a variable is undefined, recreate it and reacquire its resources. State is not restored from the conversation, and failed evaluations can leave earlier mutations in memory. Redeclaring an existing let, const, or class name fails; assign to a mutable binding or use a new name. Await all tool calls; background callbacks cannot call tools after their evaluation ends. Await child-process completion before ending an evaluation; raw subprocess output belongs to whichever evaluation is active when it arrives.'
|
||||
return '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. Emit output with console.log(...) or a final lossless JSON expression. Only printed values and that final expression become program output. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. This environment has full host access without a file sandbox; process.env starts empty. Restart, Agent disposal, cancellation, timeout, output overflow, or process failure clears the environment. If a variable is undefined, recreate it and reacquire its resources. State is not restored from the conversation, and failed evaluations can leave earlier mutations in memory. Redeclaring an existing let, const, or class name fails; assign to a mutable binding or use a new name. Await all tool calls; background callbacks cannot call tools after their evaluation ends. Await child-process completion before ending an evaluation; raw subprocess output belongs to whichever evaluation is active when it arrives.'
|
||||
}
|
||||
private readonly owners = new WeakMap<object, Kernel>()
|
||||
private readonly kernels = new Set<Kernel>()
|
||||
|
||||
@@ -279,3 +279,37 @@ describe('persistent Node REPL', () => {
|
||||
expect((await run('typeof retained')).value).toBe('undefined')
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['_', '_error'])('keeps %s assignments and subsequent tools in the active evaluation', async (name) => {
|
||||
const { run } = await setup()
|
||||
const called = vi.fn(async () => 7)
|
||||
expect(await run(`${name} = 42; await tools.value({}); ${name}`, { bindings: bindings(called) })).toEqual({ logs: [], value: 42 })
|
||||
expect(called).toHaveBeenCalledOnce()
|
||||
expect((await run(name)).value).toBe(42)
|
||||
})
|
||||
|
||||
it('rejects a new run until cancellation cleanup has completed for its owner', async () => {
|
||||
const { run, ctx } = await setup()
|
||||
const entered = Promise.withResolvers<true>()
|
||||
const cleanup = Promise.withResolvers<true>()
|
||||
const released = Promise.withResolvers<boolean>()
|
||||
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
|
||||
const intercepted = vi.spyOn(ctx.subprocess, 'spawn').mockImplementationOnce((spec) => {
|
||||
const handle = spawn(spec)
|
||||
return { ...handle, waitForExit: async () => { await handle.waitForExit(); cleanup.resolve(true); return released.promise } }
|
||||
})
|
||||
const abort = new AbortController()
|
||||
const pending = run('await tools.value({}); await new Promise(() => {})', {
|
||||
signal: abort.signal, bindings: bindings(async () => { entered.resolve(true); return null }),
|
||||
})
|
||||
try {
|
||||
await entered.promise
|
||||
abort.abort()
|
||||
await cleanup.promise
|
||||
await expect(run('42')).rejects.toThrow('concurrent runs')
|
||||
expect(intercepted).toHaveBeenCalledTimes(1)
|
||||
released.resolve(true)
|
||||
expect((await pending).error?.kind).toBe('abort')
|
||||
expect((await run('42')).value).toBe(42)
|
||||
} finally { released.resolve(true); await pending; intercepted.mockRestore() }
|
||||
})
|
||||
@@ -24,8 +24,8 @@
|
||||
en: 483134c11525910a
|
||||
zh: d2ad48023e4a4be9
|
||||
/deepseek-ai-dsh-workflow-ptc/use-this-package/file-policy-and-cancellation:
|
||||
en: 613e4be5d158e25a
|
||||
zh: 03ae4a9fb6efe870
|
||||
en: 712337f7eced4ee6
|
||||
zh: 2297bbbfbdd1872a
|
||||
/deepseek-ai-dsh-workflow-ptc/understand-the-implementation:
|
||||
en: 52aee3bea250e968
|
||||
zh: d606ae0fbb5c97b4
|
||||
|
||||
@@ -56,7 +56,7 @@ Invalid metadata, an unparseable body, an unavailable provider route or a per-ru
|
||||
|
||||
### File policy and cancellation
|
||||
|
||||
The engine resolves the calling Session's standing file policy and cwd. It passes file policy only to a provider that supports confinement. The optional persistent Node provider executes an async wrapper in a temporary unsandboxed environment, so workflow-local variables never join the Agent's retained REPL. Network access is not restricted by the file policy.
|
||||
The engine resolves the calling Session's standing file policy and cwd. A runtime without confinement is accepted only for `danger-full-access`; `read-only` and `workspace-write` fail before execution. Node in-memory mode executes an async wrapper in a temporary environment, so workflow-local variables never join the Agent's retained REPL. Network access is not restricted by the file policy.
|
||||
|
||||
The workflow requests `timeoutMs: null` from PTC. Its initial VM slice still has `syncTimeoutMs`, and a caller's abort signal still applies, including an enclosing tool deadline. Cancellation immediately aborts the PTC process and pending or active subagents. The caller must dispose every run and await child cleanup; there is no separate workflow cleanup timer.
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ Node PTC 提供方的 `maxPendingCalls` 也限制工作流并发:子 agent 启
|
||||
|
||||
### 文件策略与取消
|
||||
|
||||
引擎解析调用方 Session 的常设文件策略和 cwd,只向支持约束的提供方传递文件策略。可选持久 Node 提供方在临时无沙盒环境中执行异步包装函数,因此工作流局部变量不会进入 Agent 保留的 REPL。文件策略不限制网络访问。
|
||||
引擎解析调用方 Session 的常设文件策略和 cwd。不支持文件约束的运行时只允许用于 `danger-full-access`;`read-only` 和 `workspace-write` 在执行前失败。Node 内存保留模式在临时环境中执行异步包装函数,因此工作流局部变量不会进入 Agent 保留的 REPL。文件策略不限制网络访问。
|
||||
|
||||
工作流向 PTC 请求 `timeoutMs: null`。最初的 VM 片段仍受 `syncTimeoutMs` 限制,调用方的中止信号仍然生效,包括外层工具的截止。取消立即中止 PTC 进程及待启动或活跃的 subagent。调用方必须释放每次运行并等待子 agent 清理;不另设工作流清理定时器。
|
||||
|
||||
|
||||
@@ -273,6 +273,9 @@ export class PtcWorkflowRun implements WorkflowRun {
|
||||
private async drive(): Promise<WorkflowResult> {
|
||||
let result: WorkflowResult
|
||||
try {
|
||||
if (this.runtime.sandboxMode === undefined && this.policy.mode !== 'danger-full-access') {
|
||||
throw new Error(`workflow runtime cannot enforce the ${this.policy.mode} file policy`)
|
||||
}
|
||||
const outcome = await this.runtime.run(this.runtime.resolve({
|
||||
program: this.runtime.programFormat === 'repl' ? `await (async () => {\n${PROGRAM}\n})()` : PROGRAM,
|
||||
bindings: [{ global: 'workflowHost', functions: this.bindings() }],
|
||||
|
||||
@@ -110,7 +110,8 @@ describe('workflow host callback validation', () => {
|
||||
const handle = start()
|
||||
try {
|
||||
const result = await handle.result
|
||||
expect(result.stopReason, result.error).toBe('completed')
|
||||
expect(result.stopReason, result.error).toBe(confined ? 'completed' : 'error')
|
||||
if (!confined) expect(result.error).toContain('cannot enforce the read-only file policy')
|
||||
} finally { await handle.dispose() }
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,9 +8,9 @@ import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||
import PtcWorkflowEngine from '../src/index.ts'
|
||||
import { fakeParent, mountPtcRuntime } from './setup.ts'
|
||||
|
||||
async function setup(mode: SandboxMode) {
|
||||
async function setup(mode: SandboxMode, executionMode: 'per-call' | 'in-memory' = 'per-call') {
|
||||
const ctx = new Context()
|
||||
const files = await mountPtcRuntime(ctx, mode)
|
||||
const files = await mountPtcRuntime(ctx, mode, executionMode)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
ctx.subagents.registerProvider({
|
||||
name: 'spawn',
|
||||
@@ -59,3 +59,18 @@ return 42`,
|
||||
else await expect(readFile(inside, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
} finally { await run.dispose() }
|
||||
})
|
||||
|
||||
it.each(['read-only', 'workspace-write'] as const)('refuses an in-memory workflow when the Session requires %s', async (mode) => {
|
||||
const { ctx, parent, cwd } = await setup(mode, 'in-memory')
|
||||
const target = join(cwd, 'unconfined.txt')
|
||||
const run = ctx.workflowEngine.start({
|
||||
script: 'const proc = log.constructor(\'return process\')(); proc.getBuiltinModule(\'node:fs\').writeFileSync(args.target, \'changed\'); return 42;',
|
||||
args: { target },
|
||||
meta: { name: 'unsupported-confinement', description: 'preserve requested file policy' },
|
||||
parent,
|
||||
})
|
||||
try {
|
||||
expect(await run.result).toMatchObject({ stopReason: 'error', error: `workflow runtime cannot enforce the ${mode} file policy` })
|
||||
await expect(readFile(target)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
} finally { await run.dispose() }
|
||||
})
|
||||
Reference in new issue
Block a user