diff --git a/docs/subsystems/ptc-runtime.i18n.yaml b/docs/subsystems/ptc-runtime.i18n.yaml index 27b9db6eca..5a1f0506c1 100644 --- a/docs/subsystems/ptc-runtime.i18n.yaml +++ b/docs/subsystems/ptc-runtime.i18n.yaml @@ -12,8 +12,8 @@ en: 182147ddf388c600 zh: c0a7d77602c78bcd /ptc-runtime/captured-output-and-the-failure-taxonomy: - en: 033ce544b9974ba8 - zh: b7be881985ca3c34 + en: 78c6146c246092a2 + zh: a875326e7af33994 /ptc-runtime/the-service: en: a82f828fa006c7e9 zh: 2279d36867335b57 diff --git a/docs/subsystems/ptc-runtime.md b/docs/subsystems/ptc-runtime.md index de7f602c25..4d6d1f4d61 100644 --- a/docs/subsystems/ptc-runtime.md +++ b/docs/subsystems/ptc-runtime.md @@ -95,13 +95,11 @@ interface PtcRunResult { */ value?: PtcJsonValue /** - * Captured text. Each source channel preserves emission order; interleaving + * Captured text and inspected images. Each source channel preserves emission order; interleaving * across independent channels is backend-dependent. Bounded only as part of * the outer result. */ - logs: string[] - /** Images explicitly inspected by the program, ordered among captured text entries. */ - images?: PtcImageOutput[] + output: PtcOutput[] /** Present iff the run failed; see {@link PtcRunFailure} for the taxonomy. */ error?: PtcRunFailure } @@ -186,7 +184,7 @@ type PtcBindingFunction = (args: unknown) => Promise ## Captured output and the failure taxonomy -Logs are plain strings. Each source channel preserves emission order; interleaving across independent channels is backend-dependent. The Node provider also captures explicitly inspected images as `PtcImageOutput` entries, whose `afterLog` positions place their attachment references among text entries. Host bindings return `PtcImage` descriptors containing the attachment reference and a readable execution-world `path`; the program receives an `Image` with that path and the stored pixel dimensions. The Node provider caps serialized logs, image attachment references, and completion or diagnostic payloads together; overflow is an explicit failure. +`output` contains text and image entries in emission order: `{ type: 'text', text }` or `{ type: 'image', attachment }`. Each source channel preserves emission order; interleaving across independent channels is backend-dependent. Host bindings return `PtcImage` descriptors containing the attachment reference and a readable execution-world `path`; the program receives an `Image` with that path and the stored pixel dimensions. The Node provider caps serialized text, image descriptors, and completion or diagnostic payloads together; overflow is an explicit failure. ```ts public-api /** A stored image readable from the program's execution environment. */ @@ -206,13 +204,8 @@ declare class PtcImage { ``` ```ts type-equiv -/** One inspected image positioned among captured text entries. */ -interface PtcImageOutput { - /** Number of text entries emitted before this image. */ - afterLog: number - /** Stored image selected by the program. */ - attachment: ImageAttachmentRef -} +/** One captured text entry or explicitly inspected image, in emission order. */ +type PtcOutput = { type: 'text'; text: string } | { type: 'image'; attachment: ImageAttachmentRef } ``` Failure kinds are **orthogonal outcomes reported independently** (per [defensive-patterns](../defensive-patterns/general.md)): a budget expiry is not an exception, an abort is not a timeout, and a substrate death (e.g. OOM) is neither: diff --git a/docs/subsystems/ptc-runtime.zh.md b/docs/subsystems/ptc-runtime.zh.md index 649238ebfe..76b98ea08a 100644 --- a/docs/subsystems/ptc-runtime.zh.md +++ b/docs/subsystems/ptc-runtime.zh.md @@ -95,13 +95,11 @@ interface PtcRunResult { */ value?: PtcJsonValue /** - * Captured text. Each source channel preserves emission order; interleaving + * Captured text and inspected images. Each source channel preserves emission order; interleaving * across independent channels is backend-dependent. Bounded only as part of * the outer result. */ - logs: string[] - /** Images explicitly inspected by the program, ordered among captured text entries. */ - images?: PtcImageOutput[] + output: PtcOutput[] /** Present iff the run failed; see {@link PtcRunFailure} for the taxonomy. */ error?: PtcRunFailure } @@ -186,7 +184,7 @@ type PtcBindingFunction = (args: unknown) => Promise ## 捕获的输出与失败分类体系 -日志是普通字符串。每个输出通道保留发出顺序;独立通道之间的交错取决于后端。Node 提供方还将显式展示的图片记录为 `PtcImageOutput`,通过 `afterLog` 将附件引用定位在文本条目之间。宿主绑定返回包含附件引用及执行环境可读取的 `path` 的 `PtcImage` 描述;程序接收包含该路径和已存储图片像素尺寸的 `Image`。Node 提供方对序列化日志、图片附件引用以及完成值或诊断信息统一计算预算,超限会明确失败。 +`output` 按发出顺序保存文本和图片条目:文本使用 `{ type: 'text', text }`,图片使用 `{ type: 'image', attachment }`。每个来源通道都保留发出顺序;独立通道之间的交错顺序由后端决定。宿主绑定返回 `PtcImage` 描述符,包含附件引用和可在执行环境读取的 `path`;程序收到带有该路径和存储像素尺寸的 `Image`。Node 提供方对序列化文本、图片描述符、完成值或诊断一起计量;超限会显式失败。 ```ts public-api /** A stored image readable from the program's execution environment. */ @@ -206,13 +204,8 @@ declare class PtcImage { ``` ```ts type-equiv -/** One inspected image positioned among captured text entries. */ -interface PtcImageOutput { - /** Number of text entries emitted before this image. */ - afterLog: number - /** Stored image selected by the program. */ - attachment: ImageAttachmentRef -} +/** One captured text entry or explicitly inspected image, in emission order. */ +type PtcOutput = { type: 'text'; text: string } | { type: 'image'; attachment: ImageAttachmentRef } ``` 失败类型是**正交的结果,独立报告**(见 [defensive-patterns](../defensive-patterns/general.zh.md)):预算耗尽不是异常,中止不是超时,基底崩溃(如 OOM)也不是二者中的任何一个: diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index b322fee615..67eb7778a8 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -72,8 +72,8 @@ en: 8c8643ef46fd075f zh: 8c8643ef46fd075f /tool-schema-catalog/deepseek-ai-dsh-tool-executor-ptc/run-code: - en: 23692e8d6b486a5a - zh: 575c5447eca59290 + en: c90851ae4760fbb7 + zh: f601e1b17936fe6f /tool-schema-catalog/deepseek-ai-dsh-plan-mode: en: b500b530845e26a9 zh: b500b530845e26a9 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 89d6b1b244..20491c9c43 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -623,7 +623,7 @@ ask_user_question keeps the original blocking behavior by default; set `mode: ti ### `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. Show selected results with inspect(...values). Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically. +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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically. ```json { diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 646f0ce5bb..67bff88ba8 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -627,7 +627,7 @@ ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才 ### `run_code` -针对可用工具执行 TypeScript 程序。接受两个必填参数:`code`,异步函数的函数体(仅限可擦除语法,支持顶层 `await` 和 `return`);以及 `description`,简要说明程序做什么。请按系统提示词中的声明,以 `await tools.name(args)` 调用工具。使用 inspect(...values) 展示选定的结果。将图片传给 inspect 以展示。程序是异步 TypeScript 函数的函数体,只接受可擦除语法,不支持 enum 或 namespace。类型标注只作提示。支持顶层 await 和 return。Console 不可用。使用 inspect(...values) 输出。每次调用在新的 Node 进程中运行,可通过 await import(...) 使用 Node API。相对路径以传入的工作目录为准;process.env 初始为空。直接文件系统访问遵循本次执行的沙箱策略。每个程序从 Session 的当前目录启动,运行中保留初始目录。沙箱提权只为本次完整程序执行授权。嵌套工具保留自身策略与审批。只有在获得拒绝证据后才请求更宽访问。先前效果可能已经发生,请在显式重试前检查。程序不会自动重放。 +针对可用工具执行 TypeScript 程序。接受两个必填参数:`code`,异步函数的函数体(仅限可擦除语法,支持顶层 `await` 和 `return`);以及 `description`,简要说明程序做什么。请按系统提示词中的声明,以 `await tools.name(args)` 调用工具。使用 inspect(...values) 展示选定的文本和图片。程序是异步 TypeScript 函数的函数体,只接受可擦除语法,不支持 enum 或 namespace。类型标注只作提示。支持顶层 await 和 return。Console 不可用。使用 inspect(...values) 输出。每次调用在新的 Node 进程中运行,可通过 await import(...) 使用 Node API。相对路径以传入的工作目录为准;process.env 初始为空。直接文件系统访问遵循本次执行的沙箱策略。每个程序从 Session 的当前目录启动,运行中保留初始目录。沙箱提权只为本次完整程序执行授权。嵌套工具保留自身策略与审批。只有在获得拒绝证据后才请求更宽访问。先前效果可能已经发生,请在显式重试前检查。程序不会自动重放。 ```json { diff --git a/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.i18n.yaml b/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.i18n.yaml index 9832df4746..d15f51896a 100644 --- a/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.i18n.yaml @@ -12,5 +12,5 @@ en: 3c69ee58e2670c62 zh: 5876ec7a7d7d66a6 /explicit-output-in-typescript-ptc-programs/migration: - en: a988a960cdb06da3 - zh: 9dfa1a0728d4856e + en: c9f77e7cf6d8c14f + zh: aea760b8fefc9884 diff --git a/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.md b/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.md index 819118157e..0c58b36cf6 100644 --- a/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.md +++ b/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.md @@ -39,4 +39,5 @@ CUA screenshots and `read_image` return ordinary `Image` values in TypeScript pr ``` 3. For integrations that require console methods, set `enableConsole: true` on the `@deepseek-ai/dsh-ptc-runtime-node` provider row. Direct runtime consumers can set `discardCompletion: true` to select the same explicit-output behavior. -4. Run an updated program and verify that inspected values appear once and its final expression adds no output. +4. Direct runtime consumers read `result.output`: text entries are `{ type: 'text', text }`, image entries are `{ type: 'image', attachment }`, and array order is display order. +5. Run an updated program and verify that inspected values appear once and its final expression adds no output. diff --git a/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.zh.md b/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.zh.md index 9f3680a750..2f501e0891 100644 --- a/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.zh.md +++ b/docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.zh.md @@ -39,4 +39,5 @@ CUA 截图与 `read_image` 在 TypeScript 程序中返回普通 `Image`,其 `p ``` 3. 对于需要 console 方法的集成,在 `@deepseek-ai/dsh-ptc-runtime-node` 提供方配置行中设置 `enableConsole: true`。直接调用运行时的消费方可设置 `discardCompletion: true`,选择相同的显式输出行为。 -4. 运行更新后的程序,确认 inspect 的值只出现一次,且末尾表达式不额外输出。 +4. 直接消费运行时结果的调用方改为读取 `result.output`:文本条目为 `{ type: 'text', text }`,图片条目为 `{ type: 'image', attachment }`,数组顺序就是展示顺序。 +5. 运行更新后的程序,确认 inspect 的值只出现一次,且末尾表达式不额外输出。 diff --git a/packages/client/ui-tool/tests/spill-policy-terminal.client.spec.ts b/packages/client/ui-tool/tests/spill-policy-terminal.client.spec.ts index e732dff04c..a3e0a43a42 100644 --- a/packages/client/ui-tool/tests/spill-policy-terminal.client.spec.ts +++ b/packages/client/ui-tool/tests/spill-policy-terminal.client.spec.ts @@ -62,7 +62,7 @@ async function executeShell(text: string, nested: boolean, name = 'bash', maxInl if (tool === undefined) throw new Error('missing fixture binding') const blocks = await tool(shellArgs) expect(blocks).toEqual([{ type: 'text', text }]) - return { logs: ['true'] } + return { output: [{ type: 'text', text: 'true' }] } } } await ctx.plugin(BindingRuntime) diff --git a/packages/core/agent-loop/tests/tool-calls.spec.ts b/packages/core/agent-loop/tests/tool-calls.spec.ts index 3ea1089e61..f479363062 100644 --- a/packages/core/agent-loop/tests/tool-calls.spec.ts +++ b/packages/core/agent-loop/tests/tool-calls.spec.ts @@ -942,7 +942,7 @@ describe('PTC mode native-tool denial through the agent loop', () => { readonly language = 'typescript' readonly isolation = 'fake' as const async run(_request: PtcRunRequest): Promise { - return { logs: [] } + return { output: [] } } } diff --git a/packages/core/agent-tool-presentation/tests/agent-tool-presentation.spec.ts b/packages/core/agent-tool-presentation/tests/agent-tool-presentation.spec.ts index 4f43b87f08..2ce71441bc 100644 --- a/packages/core/agent-tool-presentation/tests/agent-tool-presentation.spec.ts +++ b/packages/core/agent-tool-presentation/tests/agent-tool-presentation.spec.ts @@ -25,7 +25,7 @@ class StubRuntime extends PtcRuntime { readonly isolation = 'stub' run(_request: PtcRunRequest): Promise { - return Promise.resolve({ logs: [] }) + return Promise.resolve({ output: [] }) } } diff --git a/packages/core/tool-executor-ptc/src/ptc.ts b/packages/core/tool-executor-ptc/src/ptc.ts index 3c3e4ac61b..4b13f5d7f1 100644 --- a/packages/core/tool-executor-ptc/src/ptc.ts +++ b/packages/core/tool-executor-ptc/src/ptc.ts @@ -14,7 +14,7 @@ import { createUserMessage, HarnessError, resolveImageAttachmentAccess } from '@ import type { ContentBlock, ToolCallId, ToolSchema } from '@deepseek-ai/dsh-llm' import { PtcImage } from '@deepseek-ai/dsh-ptc-runtime' -import type { PtcImageOutput } from '@deepseek-ai/dsh-ptc-runtime' +import type { PtcOutput } from '@deepseek-ai/dsh-ptc-runtime' import type { PtcBindingFunction, PtcRunResult, PtcRunSandbox, PtcRuntime } from '@deepseek-ai/dsh-ptc-runtime' import { approveEscalation, ESCALATION_TARGETS, validateEscalationArgs } from '@deepseek-ai/dsh-sandbox' import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox' @@ -44,27 +44,26 @@ interface RunCodeFlavor { } /** Keep tool-call rules here; the runtime owns evaluation syntax and state retention. */ -function runCodeFlavor(language: string, execution = { +function runCodeFlavor(language: string, outputInstruction: string, execution = { programDescription: 'the program to execute', - outputInstruction: 'Curate the program output.', codeDescription: `The ${language} program to execute, following this tool's execution instructions.`, }): RunCodeFlavor { return { description: `Execute a ${language} program against the available tools. Takes two required arguments: ` + `\`code\`, ${execution.programDescription}, and \`description\`, a short summary of what the program does. ` + 'Call tools as `await tools.name(args)` per the declarations in the system prompt. ' - + `${execution.outputInstruction}${language === 'TypeScript' ? ' Pass images to inspect to display them.' : ' Image-bearing subtool results are attached after the run.'}`, + + outputInstruction, codeDescription: execution.codeDescription, } } -const TYPESCRIPT_FLAVOR = runCodeFlavor('TypeScript') -const TYPESCRIPT_FUNCTION_FLAVOR = runCodeFlavor('TypeScript', { +const TYPESCRIPT_OUTPUT_INSTRUCTION = 'Show selected text and images with inspect(...values).' +const TYPESCRIPT_FLAVOR = runCodeFlavor('TypeScript', TYPESCRIPT_OUTPUT_INSTRUCTION) +const TYPESCRIPT_FUNCTION_FLAVOR = runCodeFlavor('TypeScript', TYPESCRIPT_OUTPUT_INSTRUCTION, { programDescription: 'the BODY of an async function (erasable syntax only; top-level `await` and `return` work)', - outputInstruction: 'Show selected results with inspect(...values).', codeDescription: 'The program: the body of an async TypeScript function.', }) -const PYTHON_FLAVOR = runCodeFlavor('Python') +const PYTHON_FLAVOR = runCodeFlavor('Python', 'Curate the program output. Image-bearing subtool results are attached after the run.') /** Languages with both a transport schema and an SDK renderer. */ export type PtcSdkLanguage = 'typescript' | 'python' @@ -256,17 +255,19 @@ type RunCodeOutput = { logs: string[]; result?: JsonValue; sandbox?: PtcRunSandb function renderRunOutput( value: RunCodeOutput, guidance: string, - images: PtcImageOutput[] = [], + output: PtcOutput[] = value.logs.map(text => ({ type: 'text', text })), ): ContentBlock[] { const content: ContentBlock[] = [] const appendText = (text: string): void => { if (text.length > 0) content.push({ type: 'text', text }) } - let position = 0 - for (const output of images) { - appendText(value.logs.slice(position, output.afterLog).join('\n')) - position = output.afterLog - content.push({ type: 'image', attachment: output.attachment }) + const remaining: string[] = [] + for (const block of output) { + if (block.type === 'text') remaining.push(block.text) + else { + appendText(remaining.join('\n')) + remaining.length = 0 + content.push(block) + } } - const remaining = value.logs.slice(position) if (value.result !== undefined) remaining.push(renderValue(value.result)) if (value.sandbox?.enforcement === 'partial') remaining.push('File sandbox enforcement is partial on this host.') if (value.sandbox?.denied) remaining.push(`The ${value.sandbox.mode} file sandbox denied an operation.${guidance}`) @@ -322,7 +323,7 @@ export function createRunCodeTool( } type ImageRef = Extract['attachment'] - const inspections = new WeakMap() + const inspections = new WeakMap() const programImage = (ref: ImageRef): PtcImage => { const attachments = ctx.get('attachments') const fs = ctx.get('fs') @@ -742,9 +743,9 @@ export function createRunCodeTool( await drainDispatches() } - if (result.images !== undefined && result.images.length > 0) { - inspections.set(exec, { images: result.images, logs: result.logs }) - } + const logs = result.output.flatMap(block => block.type === 'text' ? [block.text] : []) + const hasImages = result.output.some(block => block.type === 'image') + if (hasImages) inspections.set(exec, { output: result.output }) if (result.error) { const limitError = runController.signal.reason instanceof ToolCallLimitExceededError ? runController.signal.reason : undefined @@ -754,8 +755,8 @@ export function createRunCodeTool( error.name = 'AbortError' throw error } - const logsText = result.logs.length > 0 && (result.images?.length ?? 0) === 0 - ? `\nCaptured output:\n${result.logs.join('\n')}` : '' + const logsText = logs.length > 0 && !hasImages + ? `\nCaptured output:\n${logs.join('\n')}` : '' const sandboxText = result.sandbox === undefined ? '' : `\nFile sandbox: ${result.sandbox.mode}${result.sandbox.enforcement === undefined ? '' : `; enforcement: ${result.sandbox.enforcement}`}${result.sandbox.denied ? '; operation denied' : ''}.` const failureText = limitError === undefined || limitError.message === result.error.message @@ -763,13 +764,11 @@ export function createRunCodeTool( throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${failureText}${logsText}${sandboxText}${result.sandbox?.denied ? escalationGuidance(runtime.sandboxMode) : ''}`) } const value: RunCodeOutput = { - logs: result.logs, + logs, ...result.sandbox === undefined ? {} : { sandbox: result.sandbox }, ...runtime.language !== 'typescript' && result.value !== undefined ? { result: result.value } : {}, } - if (result.images !== undefined && result.images.length > 0) { - inspections.set(exec, { value, images: result.images, logs: result.logs }) - } + if (hasImages) inspections.set(exec, { value, output: result.output }) return value } finally { callSignal.removeEventListener('abort', onOuterAbort) @@ -785,10 +784,10 @@ export function createRunCodeTool( if (selected === undefined) return undefined if (result.isError) { if (selected.value !== undefined) return undefined - return [...result.content, ...renderRunOutput({ logs: selected.logs }, '', selected.images)] + return [...result.content, ...renderRunOutput({ logs: [] }, '', selected.output)] } if (selected.value === undefined || !isDeepStrictEqual(selected.value, result.value)) return undefined - return renderRunOutput(selected.value, escalationGuidance(ctx.get('ptcRuntime')?.sandboxMode), selected.images) + return renderRunOutput(selected.value, escalationGuidance(ctx.get('ptcRuntime')?.sandboxMode), selected.output) }, // The model-authored description is the call's always-visible UI label // (the bash `description` precedent); the program itself rides rawInput. diff --git a/packages/core/tool-executor-ptc/tests/ptc.spec.ts b/packages/core/tool-executor-ptc/tests/ptc.spec.ts index 1e33f122e5..8d92c438ec 100644 --- a/packages/core/tool-executor-ptc/tests/ptc.spec.ts +++ b/packages/core/tool-executor-ptc/tests/ptc.spec.ts @@ -49,7 +49,7 @@ class FakeRuntime extends PtcRuntime { readonly language: string readonly isolation = 'fake' - behavior: (request: PtcRunRequest) => Promise = () => Promise.resolve({ logs: [] }) + behavior: (request: PtcRunRequest) => Promise = () => Promise.resolve({ output: [] }) lastRequest?: PtcRunRequest constructor(ctx: Context, config: { language?: string } = {}) { @@ -65,7 +65,7 @@ class FakeRuntime extends PtcRuntime { /** Output from a program that explicitly prints a selected JSON value. */ function printed(value: JsonValue): PtcRunResult { - return { logs: [typeof value === 'string' ? value : JSON.stringify(value, null, 2)] } + return { output: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }] } } interface SetupOptions { @@ -239,7 +239,7 @@ describe('mode-aware wire contribution', () => { ctx.tools.register(definition, { when: () => true }) await expect(functions.conditional!({})).rejects.toThrow('unknown tool "conditional"') } - return { logs: [] } + return { output: [] } } const result = await executeCall(ctx, { agent, toolSet, signal: testToolSignal, callId: ToolCallId('conditional-program'), @@ -684,7 +684,7 @@ describe('the per-program tool-call limit', () => { await expect(echo({ value: 'rejected' })).rejects.toThrow(message) expect(request.signal?.aborted).toBe(true) await expect(echo({ value: 'caught the error' })).rejects.toThrow('run_code run is over') - return { logs: [], error: { kind: 'abort', message } } + return { output: [], error: { kind: 'abort', message } } } const result = await runCode(ctx, 'program', { agent }) expect(result.error).toMatchObject({ info: { code: 'CODE_RUN_FAILED' } }) @@ -717,7 +717,7 @@ describe('the per-program tool-call limit', () => { for (let n = 0; n < limit; n++) await echo({ value: String(n) }) await expect(echo({ value: 'rejected' })).rejects.toThrow(`limit of ${limit} tool calls (4 per minute of its timeout); call ${limit + 1} was not dispatched`) expect(request.signal?.aborted).toBe(true) - return { logs: [], error: { kind: 'abort', message: String(request.signal?.reason) } } + return { output: [], error: { kind: 'abort', message: String(request.signal?.reason) } } } expect((await runCode(ctx, 'program', { timeoutMs: requestedMs })).isError).toBe(true) expect(calls).toHaveLength(limit) @@ -733,7 +733,7 @@ describe('the per-program tool-call limit', () => { const calls = registerEcho(ctx) runtime.behavior = async (request) => { for (let n = 0; n < 3; n++) await request.bindings[0]!.functions.echo!({ value: String(n) }) - return { logs: [] } + return { output: [] } } expect((await runCode(ctx, 'program')).isError).toBe(false) expect(calls).toHaveLength(3) @@ -765,7 +765,7 @@ describe('the per-program tool-call limit', () => { void functions.queued!({ value: 'queued' }).catch(() => { outcomes.push('queued abandoned') }) await expect(functions.queued!({ value: 'excess' })).rejects.toThrow('limit of 2 tool calls (1 per minute of its timeout); call 3 was not dispatched') expect(request.signal?.aborted).toBe(true) - return { logs: [], error: { kind: 'abort', message: 'execution canceled' } } + return { output: [], error: { kind: 'abort', message: 'execution canceled' } } } const result = await runCode(ctx, 'program', { agent }) expect(result.error).toMatchObject({ info: { code: 'CODE_RUN_FAILED' } }) @@ -794,7 +794,7 @@ describe('the per-program tool-call limit', () => { await expect(echo({ value: 'excess' })).rejects.toThrow('limit of 2 tool calls') expect(request.signal?.aborted).toBe(true) if (cancelCaller) controller.abort('later caller cancellation') - return { logs: ['before termination'], error: failure } + return { output: [{ type: 'text', text: 'before termination' }], error: failure } } const result = await runCode(ctx, 'program', { signal: controller.signal }) expect(result.error).toMatchObject({ info: { code: 'CODE_RUN_FAILED' } }) @@ -823,7 +823,7 @@ describe('the per-program tool-call limit', () => { await expect(functions.fail!({})).rejects.toThrow('tool failed') await expect(functions.echo!({ value: 'rejected' })).rejects.toThrow('limit of 3 tool calls (3 per minute of its timeout); call 4 was not dispatched') expect(request.signal?.aborted).toBe(true) - return { logs: [], error: { kind: 'abort', message: String(request.signal?.reason) } } + return { output: [], error: { kind: 'abort', message: String(request.signal?.reason) } } } expect((await runCode(ctx, 'program')).isError).toBe(true) expect(calls).toEqual([]) @@ -1233,7 +1233,7 @@ describe('the run_code dispatch bridge', () => { const first = await tools.echo!({ value: 'one' }) const second = await tools.echo!({ value: 'two' }) if (typeof first !== 'string' || typeof second !== 'string') throw new Error('echo returned a non-string') - return { logs: [`saw ${first}`, second] } + return { output: [{ type: 'text', text: `saw ${first}` }, { type: 'text', text: second }] } } const result = await runCode(ctx, 'const …: string = …', { agent }) expect(result.isError).toBe(false) @@ -1607,7 +1607,7 @@ describe('the run_code dispatch bridge', () => { runtime.behavior = async (request) => { const args = Object.assign(Object.create(null) as Record, { value: 'x', nested: ['same'] }) await request.bindings[0]!.functions.echo!(args) - return { logs: [] } + return { output: [] } } await runCode(ctx, 'program', { agent }) expect(calls).toEqual([{ value: 'x', nested: ['same'] }]) @@ -1727,7 +1727,7 @@ describe('the run_code dispatch bridge', () => { }) runtime.behavior = async (request) => { await request.bindings[0]!.functions.echo!({ value: 'x' }) - return { logs: [], error: { kind: 'exception', message: 'program failed later' } } + return { output: [], error: { kind: 'exception', message: 'program failed later' } } } const result = await runCode(ctx, 'program') @@ -1747,7 +1747,7 @@ describe('the run_code dispatch bridge', () => { ] as const)('reports a timeout %s with its captured output', async (phase, logs) => { const { ctx, runtime } = await setup({ mode: 'ptc' }) runtime.behavior = () => Promise.resolve({ - logs: [...logs], + output: logs.map(text => ({ type: 'text' as const, text })), error: { kind: 'timeout', message: `execution timed out ${phase} (300ms); retained variables were cleared` }, }) const result = await runCode(ctx, 'program') @@ -1791,7 +1791,7 @@ describe('the run_code dispatch bridge', () => { await Promise.all(calls) // A real runtime would be terminated by the abort; the fake honors the // contract by reporting the abort as the run failure. - return { logs: [], error: { kind: 'abort', message: 'user-cancel' } } + return { output: [], error: { kind: 'abort', message: 'user-cancel' } } } const result = await runCode(ctx, 'program', { signal: controller.signal }) expect(result.isError).toBe(true) @@ -1870,7 +1870,7 @@ describe('the run_code dispatch bridge', () => { const { agent, events } = fakeAgent() runtime.behavior = async (request) => { await request.bindings[0]!.functions.echo!({ value: 'blocked' }) - return { logs: [] } + return { output: [] } } const result = await runCode(ctx, 'return await tools.echo({ value: "blocked" })', { agent }) @@ -1908,10 +1908,10 @@ describe('the run_code dispatch bridge', () => { }) it.each([ - ['logs only', { logs: ['printed'] }, 'printed'], - ['completion only', { logs: [], value: 'returned' }, '(run_code completed with no output)'], - ['logs plus completion', { logs: ['printed'], value: 'returned' }, 'printed'], - ['no output', { logs: [] }, '(run_code completed with no output)'], + ['logs only', { output: [{ type: 'text', text: 'printed' }] }, 'printed'], + ['completion only', { output: [], value: 'returned' }, '(run_code completed with no output)'], + ['logs plus completion', { output: [{ type: 'text', text: 'printed' }], value: 'returned' }, 'printed'], + ['no output', { output: [] }, '(run_code completed with no output)'], ] as [string, PtcRunResult, string][])('keeps %s in durable content without a result presenter', async (_name, output, text) => { const { ctx, runtime } = await setup({ mode: 'ptc' }) runtime.behavior = () => Promise.resolve(output) @@ -1929,7 +1929,7 @@ describe('the run_code dispatch bridge', () => { it('keeps a post-policy spill preview in durable content without a result presenter', async () => { const { ctx, runtime } = await setup({ mode: 'ptc' }) const preview = 'HEAD\n\n(Omitted 100 bytes. Full formatted result stored at: /tmp/run-code.txt.)\n\nTAIL' - runtime.behavior = () => Promise.resolve({ logs: ['printed'], value: 'returned' }) + runtime.behavior = () => Promise.resolve({ output: [{ type: 'text', text: 'printed' }], value: 'returned' }) ctx.on('tools/post-execute', (exec, _result, next): Promise => { if (exec.name !== RUN_CODE_NAME) return next() return Promise.resolve({ kind: 'accept', content: [{ type: 'text', text: preview }] }) @@ -1945,7 +1945,7 @@ describe('the run_code dispatch bridge', () => { it('keeps canonical failure content durable without a result presenter', async () => { const { ctx, runtime } = await setup({ mode: 'ptc' }) runtime.behavior = () => Promise.resolve({ - logs: ['captured before failure'], + output: [{ type: 'text', text: 'captured before failure' }], error: { kind: 'output-limit', message: 'outer output exceeded 8 bytes' }, }) @@ -2088,7 +2088,7 @@ describe('the run_code dispatch bridge', () => { })) runtime.behavior = async (request) => { await request.bindings[0]!.functions.mutator!({ list: ['original'] }) - return { logs: [] } + return { output: [] } } const result = await runCode(ctx, 'program', { agent }) expect(result.isError).toBe(false) @@ -2122,22 +2122,22 @@ describe('the run_code dispatch bridge', () => { it('renders Python completion values as pretty JSON while preserving strings raw', async () => { const { ctx, runtime } = await setup({ mode: 'ptc', runtime: { language: 'python' } }) - runtime.behavior = () => Promise.resolve({ logs: [], value: { n: 42, ok: true } }) + runtime.behavior = () => Promise.resolve({ output: [], value: { n: 42, ok: true } }) expect((await runCode(ctx, 'object')).content[0]).toEqual({ type: 'text', text: '{\n "n": 42,\n "ok": true\n}' }) - runtime.behavior = () => Promise.resolve({ logs: [], value: {} }) + runtime.behavior = () => Promise.resolve({ output: [], value: {} }) expect((await runCode(ctx, 'empty object')).content[0]).toEqual({ type: 'text', text: '{}' }) const nested = { outer: [{ inner: true }] } - runtime.behavior = () => Promise.resolve({ logs: [], value: nested }) + runtime.behavior = () => Promise.resolve({ output: [], value: nested }) expect((await runCode(ctx, 'nested')).content[0]).toEqual({ type: 'text', text: JSON.stringify(nested, null, 2) }) - runtime.behavior = () => Promise.resolve({ logs: [], value: ['x', 2] }) + runtime.behavior = () => Promise.resolve({ output: [], value: ['x', 2] }) expect((await runCode(ctx, 'array')).content[0]).toEqual({ type: 'text', text: '[\n "x",\n 2\n]' }) - runtime.behavior = () => Promise.resolve({ logs: [], value: [] }) + runtime.behavior = () => Promise.resolve({ output: [], value: [] }) expect((await runCode(ctx, 'empty array')).content[0]).toEqual({ type: 'text', text: '[]' }) - runtime.behavior = () => Promise.resolve({ logs: [], value: null }) + runtime.behavior = () => Promise.resolve({ output: [], value: null }) expect((await runCode(ctx, 'null')).content[0]).toEqual({ type: 'text', text: 'null' }) - runtime.behavior = () => Promise.resolve({ logs: [], value: 'raw' }) + runtime.behavior = () => Promise.resolve({ output: [], value: 'raw' }) expect((await runCode(ctx, 'string')).content[0]).toEqual({ type: 'text', text: 'raw' }) - runtime.behavior = () => Promise.resolve({ logs: [] }) + runtime.behavior = () => Promise.resolve({ output: [] }) const absent = await runCode(ctx, 'undefined') expect(absent.content[0]).toEqual({ type: 'text', text: '(run_code completed with no output)' }) expect(absent.isError ? undefined : absent.value).toEqual({ logs: [] }) @@ -2152,7 +2152,7 @@ describe('the run_code dispatch bridge', () => { record: { first: true, second: null }, } for (let depth = 0; depth < 5_000; depth++) value = [value] - runtime.behavior = () => Promise.resolve({ logs: [], value }) + runtime.behavior = () => Promise.resolve({ output: [], value }) const result = await runCode(ctx, 'deep result') @@ -2169,7 +2169,7 @@ describe('the run_code dispatch bridge', () => { const calls = registerEcho(ctx) runtime.behavior = (request) => { // The fake honors the seam contract for an already-aborted signal. - if (request.signal?.aborted) return Promise.resolve({ logs: [], error: { kind: 'abort' as const, message: String(request.signal.reason) } }) + if (request.signal?.aborted) return Promise.resolve({ output: [], error: { kind: 'abort' as const, message: String(request.signal.reason) } }) return Promise.resolve(printed('unreachable')) } const controller = new AbortController() @@ -2215,7 +2215,7 @@ describe('the run_code dispatch bridge', () => { const message = `execution canceled; retained variables were ${state}` runtime.behavior = async () => { controller.abort({ kind: 'user' }) - return { logs: [], error: { kind: 'abort', message } } + return { output: [], error: { kind: 'abort', message } } } const result = await runCode(ctx, 'program', { signal: controller.signal }) expect(result.error).toEqual({ @@ -2241,7 +2241,7 @@ describe('the run_code dispatch bridge', () => { expect(request.signal?.reason).toBe(controller.signal.reason) const reason: unknown = request.signal?.reason const message = reason instanceof Error ? reason.message : String(reason) - return { logs: [], error: { kind: 'abort', message } } + return { output: [], error: { kind: 'abort', message } } } const result = await runCode(ctx, 'program', { signal: controller.signal }) @@ -2267,7 +2267,7 @@ describe('the run_code dispatch bridge', () => { const controller = new AbortController() runtime.behavior = async () => { if (cancelled) controller.abort({ kind: 'user' }) - return { logs: ['got this far'], error: failure } + return { output: [{ type: 'text', text: 'got this far' }], error: failure } } const result = await runCode(ctx, 'program', { signal: controller.signal }) @@ -2586,7 +2586,7 @@ describe('host environment reset', () => { void request.bindings[0]!.functions.blocked!({}).catch(() => {}) return program.promise } - const release = vi.spyOn(runtime, 'release').mockImplementation(async () => { program.resolve({ logs: [], error: { kind: 'abort', message: 'environment disposed' } }) }) + const release = vi.spyOn(runtime, 'release').mockImplementation(async () => { program.resolve({ output: [], error: { kind: 'abort', message: 'environment disposed' } }) }) const running = runCode(ctx, 'await tools.blocked({}); while (true) {}', { agent }) await toolStarted.promise let settled = false @@ -2598,7 +2598,7 @@ describe('host environment reset', () => { cleanup.resolve(undefined) await resetting expect((await running).isError).toBe(true) - } finally { cleanup.resolve(undefined); program.resolve({ logs: [] }); await Promise.all([running, resetting]) } + } finally { cleanup.resolve(undefined); program.resolve({ output: [] }); await Promise.all([running, resetting]) } }) it('reports failed cleanup, refuses further code, and allows explicit reset retry', async () => { @@ -2650,7 +2650,7 @@ describe('PTC standing file policy and sandbox outcomes', () => { it('preserves partial enforcement and observed denial in successful output', async () => { const { ctx, runtime } = await setup() try { - runtime.behavior = async () => ({ logs: [], sandbox: { mode: 'read-only', enforcement: 'partial', denied: true } }) + runtime.behavior = async () => ({ output: [], sandbox: { mode: 'read-only', enforcement: 'partial', denied: true } }) const result = await runCode(ctx, 'return 1') expect(result.value).toEqual({ logs: [], sandbox: { mode: 'read-only', enforcement: 'partial', denied: true } }) expect(result.content).toEqual([{ type: 'text', text: 'File sandbox enforcement is partial on this host.\nThe read-only file sandbox denied an operation.' }]) @@ -2663,7 +2663,7 @@ describe('PTC standing file policy and sandbox outcomes', () => { ])('includes the available sandbox facts with a failed program ($mode)', async (sandbox) => { const { ctx, runtime } = await setup() try { - runtime.behavior = async () => ({ logs: [], error: { kind: 'exception', message: 'failed' }, sandbox }) + runtime.behavior = async () => ({ output: [], error: { kind: 'exception', message: 'failed' }, sandbox }) const result = await runCode(ctx, 'throw new Error("failed")') expect(result.isError).toBe(true) const text = result.content.filter(block => block.type === 'text').map(block => block.text).join('\n') @@ -2842,7 +2842,7 @@ describe('per-program execution controls', () => { let effects = 0 runtime.behavior = () => { effects++ - return Promise.resolve({ logs: ['first effect completed'], + return Promise.resolve({ output: [{ type: 'text', text: 'first effect completed' }], error: { kind: 'exception', message: 'access denied' }, sandbox: { mode: 'read-only', denied: true, enforcement: 'full' } }) } diff --git a/packages/document/tool-office/tests/activation.spec.ts b/packages/document/tool-office/tests/activation.spec.ts index 916a1fe6e0..71a954ef64 100644 --- a/packages/document/tool-office/tests/activation.spec.ts +++ b/packages/document/tool-office/tests/activation.spec.ts @@ -55,7 +55,7 @@ class SkillRuntime extends PtcRuntime { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } } async run(request: PtcRunRequest): Promise { - return { logs: [], value: await request.bindings[0]!.functions.skill!({ name: this.skill }) } + return { output: [], value: await request.bindings[0]!.functions.skill!({ name: this.skill }) } } } diff --git a/packages/document/tool-office/tests/tool-office.spec.ts b/packages/document/tool-office/tests/tool-office.spec.ts index 85d33898ba..7e8b6f802f 100644 --- a/packages/document/tool-office/tests/tool-office.spec.ts +++ b/packages/document/tool-office/tests/tool-office.spec.ts @@ -76,7 +76,7 @@ class CatalogAdapter extends LlmAdapter { class FakeRuntime extends PtcRuntime { readonly language = 'typescript' readonly isolation = 'fake' - behavior: (request: PtcRunRequest) => Promise = () => Promise.resolve({ logs: [] }) + behavior: (request: PtcRunRequest) => Promise = () => Promise.resolve({ output: [] }) resolve(request: PtcRunRequest): PtcRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } } @@ -653,7 +653,7 @@ describe('PTC dispatch', () => { const runtime = ctx.ptcRuntime as FakeRuntime runtime.behavior = async (request) => { const value = await request.bindings[0]!.functions.office_render!({ input: 'deck.pptx' }) - return { logs: [], value } + return { output: [], value } } const agent = { ...agentOn('vision'), ctx } await loadOffice(ctx, agent) diff --git a/packages/experimental/ptc-runtime-python/README.i18n.yaml b/packages/experimental/ptc-runtime-python/README.i18n.yaml index 2e4e629ec7..ba0434fd52 100644 --- a/packages/experimental/ptc-runtime-python/README.i18n.yaml +++ b/packages/experimental/ptc-runtime-python/README.i18n.yaml @@ -54,8 +54,8 @@ en: 40294c1c31b74824 zh: d805c6bd7270505e /deepseek-ai-dsh-experimental-ptc-runtime-python/known-limitations-and-deferred-work: - en: 5d62ed07fefbefc4 - zh: 2a03d9bc9265a200 + en: 6df63c3fc7f9357d + zh: 8f1730b8b33cb7e9 /deepseek-ai-dsh-experimental-ptc-runtime-python/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/experimental/ptc-runtime-python/README.md b/packages/experimental/ptc-runtime-python/README.md index c610c5c273..5b6f9d0ac6 100644 --- a/packages/experimental/ptc-runtime-python/README.md +++ b/packages/experimental/ptc-runtime-python/README.md @@ -133,12 +133,12 @@ These limits define what the package does and does not cover; they are current p - **A binding REPLY value has no seam-level byte or depth cap** — `maxValueBytes` meters only the done frame's completion value; a wide binding reply is rebuilt host-side (`snapshotJsonValue` traversal) and encoded whole, bounded on both sides only by process memory (like a binding argument, which has no child-side budget either). - **No shipped profile mounts this provider** — the keyless `ptc-python-turn` snapshot replaces the headless PTC runtime through the real Loader; released profiles use the sandboxed Node process backend. - **Workflow execution requires Node** — compositions using this Python provider disable `workflow-ptc`, `tool-workflow`, and `tool-ralph`; the workflow provider rejects an incompatible runtime when loaded. -- **Cross-channel log interleaving is backend-dependent** — Python stdout, stderr, and fd-3 log frames travel independently; each channel preserves its own order, while their total order in `result.logs` may differ. +- **Cross-channel log interleaving is backend-dependent** — Python stdout, stderr, and fd-3 log frames travel independently; each channel preserves its own order, while their total order in `result.output` may differ. - **CPython 3.10 or newer is required** — the configured executable is resolved and version-probed at load; unsupported interpreters fail before `ctx.ptcRuntime` is registered. - **Execution is local and Unix-only** — the interpreter, working directory, and sandbox runner must belong to the host running this plugin. The provider uses POSIX resource limits and process-group signals; it does not support Windows or remote execution providers. - **Sandbox enforcement inherits backend limits** — a successful program can report partial enforcement, and file confinement does not establish multi-tenant isolation or complete descendant cleanup. - **Diagnostic prefixes omit the package's experimental qualifier** — the marker `[dsh-ptc-runtime-python] log capture truncated at bytes` identifies this provider independently of its npm package name. The protocol mirror verifies identical marker bytes in TypeScript and Python. -- **`run()` is one-shot** — `logs` become available only after `PtcRunResult` resolves; there is no streaming-log or progress interface for output produced by a running program. +- **`run()` is one-shot** — `output` becomes available only after `PtcRunResult` resolves; there is no streaming-log or progress interface for output produced by a running program. - **No state persists across runs** — every request executes in a fresh subprocess; a persistent REPL-style kernel stays deferred until a backend brings its own logging scheme. - **An fd-3 frame whose raw length exceeds the effective frame parse cap settles the run as a worker-exit** — the cap is 64 MiB, or lower when the host's configured heap cannot safely parse a near-cap frame (`hostFrameParseCeiling`); `maxLogBytes`/`maxValueBytes` are load-bounded to the same cap so an honest child's frames always fit; a model-constructed binding ARGUMENT above the cap (a value with no seam-level budget) trips it too — an accepted residual of the OOM guard. - **A child that stops reading its replies settles the run as a worker-exit once the reply backlog passes 1024 frames** — the host writes replies one at a time, waiting for `drain` when the pipe is full; a child that keeps sending calls without consuming replies would otherwise grow the retained backlog (and the binding results it pins) until the wall clock, so the backlog cap fails the run early. Binding results carry no seam-level byte cap, so this is a count bound, not a byte bound. diff --git a/packages/experimental/ptc-runtime-python/README.zh.md b/packages/experimental/ptc-runtime-python/README.zh.md index 7feb516be7..e17dd14549 100644 --- a/packages/experimental/ptc-runtime-python/README.zh.md +++ b/packages/experimental/ptc-runtime-python/README.zh.md @@ -133,12 +133,12 @@ The program is the body of an async Python function. Top-level await and return - **binding 回复值没有 seam 级字节或深度上限**——`maxValueBytes` 只计量 done 帧的完成值;宽 binding 回复在宿主侧重建(`snapshotJsonValue` 遍历)并整帧编码,两侧都只受进程内存约束(与没有子进程侧预算的 binding 实参一样)。 - **已发布 profile 均不挂载本提供方**——keyless `ptc-python-turn` 快照通过真实 Loader 替换 headless PTC 运行时;已发布 profile 使用沙箱 Node 进程后端。 - **工作流执行需要 Node**——使用本 Python 提供方的组合禁用 `workflow-ptc`、`tool-workflow` 和 `tool-ralph`;工作流提供方在加载时拒绝不兼容的运行时。 -- **跨通道日志交错由后端决定**——Python stdout、stderr 与 fd-3 日志帧彼此独立传输;每个通道保留自身顺序,但它们在 `result.logs` 中的总顺序可能不同。 +- **跨通道日志交错由后端决定**——Python stdout、stderr 与 fd-3 日志帧彼此独立传输;每个通道保留自身顺序,但它们在 `result.output` 中的总顺序可能不同。 - **需要 CPython 3.10 或更高版本**——配置的可执行文件会在加载期完成解析与版本探测;不受支持的解释器会在 `ctx.ptcRuntime` 注册前失败。 - **执行限于本地 Unix 宿主**——解释器、工作目录与沙箱 runner 必须位于运行本插件的宿主上。提供方使用 POSIX 资源限制和进程组信号;不支持 Windows 或远程执行提供方。 - **沙箱强制能力继承后端限制**——成功的程序也可能报告部分强制能力,文件约束不构成多租户隔离或完整的后代清理保证。 - **诊断前缀省略包名中的 experimental 限定词**——标记 `[dsh-ptc-runtime-python] log capture truncated at bytes` 独立于 npm 包名来标识本提供方。协议镜像检查 TypeScript 与 Python 的标记字节完全相同。 -- **`run()` 是一次性的**——`logs` 只有在 `PtcRunResult` resolve 后才能获得;没有为运行中程序产生的输出提供流式日志或进度接口。 +- **`run()` 是一次性的**——`output` 只有在 `PtcRunResult` resolve 后才能获得;没有为运行中程序产生的输出提供流式日志或进度接口。 - **运行之间不保留状态**——每次请求都在全新子进程中执行;持久 REPL 风格内核在某个后端带来自己的日志方案之前保持延期。 - **原始长度超过有效帧解析上限的 fd-3 帧会让本次运行以 worker-exit 结算**——上限为 64 MiB,或当宿主的配置堆无法安全解析接近上限的帧时更低(`hostFrameParseCeiling`);`maxLogBytes`/`maxValueBytes` 在加载期被限制到同一上限,因此诚实子进程的帧总能放得下;模型构造的超过该上限的 binding 实参(一个在 seam 层没有预算的值)会触发同一上限——这是该 OOM 防护的已接受残余。 - **停止读取回复的子进程会在回复积压超过 1024 帧时以 worker-exit 结算运行**——宿主每次写一条回复,管道满时等待 `drain`;只持续发送调用而不消费回复的子进程会让保留的积压(及其钉住的 binding 结果)一直增长到墙钟,因此积压上限让运行提前失败。binding 结果在 seam 层没有字节上限,所以这是计数上限而非字节上限。 diff --git a/packages/experimental/ptc-runtime-python/src/index.ts b/packages/experimental/ptc-runtime-python/src/index.ts index a1e8b8556c..58eb7d2c07 100644 --- a/packages/experimental/ptc-runtime-python/src/index.ts +++ b/packages/experimental/ptc-runtime-python/src/index.ts @@ -1022,7 +1022,7 @@ export class PythonPtcRuntime extends PtcRuntime { try { input = bootstrapInput() } catch (error: unknown) { - return { logs: [], sandbox, error: { kind: 'worker-exit', message: `failed to read the python bootstrap: ${messageOf(error)}` } } + return { output: [], sandbox, error: { kind: 'worker-exit', message: `failed to read the python bootstrap: ${messageOf(error)}` } } } let confined: ConfinedArgv | undefined // Unbuffered native output stays observable before process-group teardown. @@ -1045,7 +1045,7 @@ export class PythonPtcRuntime extends PtcRuntime { return await pending } catch (error: unknown) { return { - logs: [], sandbox, + output: [], sandbox, error: controller.signal.aborted ? controller.signal.reason as PtcRunFailure : { kind: error instanceof SandboxUnavailableError ? 'sandbox-unavailable' : 'worker-exit', message: messageOf(error) }, } @@ -1176,7 +1176,7 @@ export class PythonPtcRuntime extends PtcRuntime { } } catch (error: unknown) { complete() - return Promise.resolve({ logs: [], sandbox, error: { + return Promise.resolve({ output: [], sandbox, error: { kind: confined !== undefined && isRunnerSpawnFailure(error, argv[0], request.cwd) ? 'sandbox-unavailable' : 'worker-exit', message: `python spawn error: ${messageOf(error)}`, } }) @@ -2147,16 +2147,16 @@ export class PythonPtcRuntime extends PtcRuntime { let resolved = false // The decided terminal result for a live child, recorded by finish() and // read by the `close` handler that settles it once the pipes have drained. - let decided: Omit + let decided: Omit // The single settlement point: resolve run() with the decided result and // mark the fiber quiescent. Idempotent — the first call wins, so a later // `close` after done/timeout/abort is absorbed as a no-op. - const settle = (result: Omit): void => { + const settle = (result: Omit): void => { if (resolved) return resolved = true if (closeDeadline !== undefined) clearTimeout(closeDeadline) - resolve({ ...result, logs, sandbox: { ...sandbox } }) + resolve({ ...result, output: logs.map(text => ({ type: 'text', text })), sandbox: { ...sandbox } }) // Mark the fiber quiescent for THIS run: drop it from `live` and resolve // `finished` (what teardown awaits). Deferred until the process group is // actually empty — dropping from `live` before then would let a @@ -2240,7 +2240,7 @@ export class PythonPtcRuntime extends PtcRuntime { pollGroup() } - const finish = (result: Omit): void => { + const finish = (result: Omit): void => { if (settled) return settled = true decided = result diff --git a/packages/experimental/ptc-runtime-python/tests/runtime.spec.ts b/packages/experimental/ptc-runtime-python/tests/runtime.spec.ts index cf28d3fc10..799417acdd 100644 --- a/packages/experimental/ptc-runtime-python/tests/runtime.spec.ts +++ b/packages/experimental/ptc-runtime-python/tests/runtime.spec.ts @@ -562,7 +562,7 @@ describe('PythonPtcRuntime — seam descriptors and misuse', () => { const result = await runtime.run(runtime.resolve({ program: 'return 1', bindings: [], signal })) expect(result.error?.kind).toBe('abort') expect(result.error?.message).toContain('already-cancelled') - expect(result.logs).toEqual([]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([]) }) it('short-circuits on an already-aborted signal whose reason cannot be converted', async () => { @@ -576,7 +576,7 @@ describe('PythonPtcRuntime — seam descriptors and misuse', () => { const result = await runtime.run(runtime.resolve({ program: 'return 1', bindings: [], signal })) expect(result.error?.kind).toBe('abort') expect(result.error?.message).toBe('') - expect(result.logs).toEqual([]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([]) }) it('delivers packaged Python modules without exposing a writable bootstrap file', async () => { @@ -671,7 +671,7 @@ describe('PythonPtcRuntime — seam descriptors and misuse', () => { const result = await runtime.run(runtime.resolve({ program: 'return 42', bindings: [] })) expect(result.error?.kind).toBe('worker-exit') expect(result.error?.message).toContain('failed to read the python bootstrap') - expect(result.logs).toEqual([]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([]) expect(result.value).toBeUndefined() } finally { failNextReadOf.value = undefined @@ -972,7 +972,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { expect(result.value).toEqual({ answer: 42 }) // `print` in Python emits: text, ' ', text, '\n'. Concat the captured // fragments and assert the model-visible message survives. - expect(result.logs.join('')).toContain('hello 42') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('hello 42') // 15s: this is usually the suite's first real subprocess — a cold python3 // start (interpreter + asyncio import) on a loaded CI runner can exceed // the 5s default alone; later tests reuse the warm page cache. @@ -994,7 +994,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { })) expect(result.error).toBeUndefined() expect(result.value).toEqual({ tmpdir: tmpdir(), path: null, home: null, token: null }) - expect(result.logs).toEqual([]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([]) }) it('bridges binding calls both ways and rejects the program-side call on a host rejection', async () => { @@ -1269,7 +1269,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['a b', 'c']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['a b', 'c']) }) it('flushes a print with no trailing newline', async () => { @@ -1279,7 +1279,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['partial']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['partial']) }) it('aggregates a large newline-free native write into one log entry, not one per pipe chunk', async () => { @@ -1296,7 +1296,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['A'.repeat(size)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['A'.repeat(size)]) }) it('splits native output on its own newlines, one entry per line', async () => { @@ -1308,7 +1308,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['one', 'two', 'three']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['one', 'two', 'three']) }) it('preserves each native stream order while allowing backend-dependent interleaving', async () => { @@ -1325,8 +1325,8 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.indexOf('stdout-one')).toBeLessThan(result.logs.indexOf('stdout-two')) - expect(result.logs.indexOf('stderr-one')).toBeLessThan(result.logs.indexOf('stderr-two')) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).indexOf('stdout-one')).toBeLessThan(result.output.filter(block => block.type === 'text').map(block => block.text).indexOf('stdout-two')) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).indexOf('stderr-one')).toBeLessThan(result.output.filter(block => block.type === 'text').map(block => block.text).indexOf('stderr-two')) }) it('bounds a newline-free native flood by the ledger instead of buffering it whole', async () => { @@ -1342,9 +1342,9 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.at(-1)).toBe(logTruncationMarker(4096)) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).at(-1)).toBe(logTruncationMarker(4096)) // The retained output is bounded by the budget, not the 2 MB flood. - expect(result.logs.join('').length).toBeLessThan(4096) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('').length).toBeLessThan(4096) }) it('bounds a newline-free single-character Python write drip by the fragment cap, not OOM', async () => { @@ -1369,7 +1369,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.at(-1)).toBe(logTruncationMarker(4096)) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).at(-1)).toBe(logTruncationMarker(4096)) }) it('bounds a control-char-dense native residual by serialized cost, not raw length', async () => { @@ -1387,7 +1387,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.at(-1)).toBe(logTruncationMarker(4096)) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).at(-1)).toBe(logTruncationMarker(4096)) }) it('rejects an output budget that could breach addressSpaceMb during encode at load', async () => { @@ -1460,7 +1460,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.at(-1)).toBe(logTruncationMarker(4096)) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).at(-1)).toBe(logTruncationMarker(4096)) }) it('drops a second stray line in the same chunk once the first truncated the ledger', async () => { @@ -1479,8 +1479,8 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.at(-1)).toBe(logTruncationMarker(64)) - expect(result.logs.join('\n')).not.toContain('SECOND') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).at(-1)).toBe(logTruncationMarker(64)) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('\n')).not.toContain('SECOND') }) it('charges a broken multibyte sequence its U+FFFD bytes, split across pipe chunks', async () => { @@ -1502,8 +1502,8 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.join('')).toContain('A') - expect(result.logs.join('')).toContain('�') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('A') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('�') }) it('charges the exact serialized cost of short-escape and quote/backslash characters', async () => { @@ -1519,7 +1519,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['\ta"b\\c\x01é']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['\ta"b\\c\x01é']) }) it('fails a completion dict with a non-string key as invalid-output (no key coercion)', async () => { @@ -2065,7 +2065,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['ab']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['ab']) }, 15_000) it('keeps a SEALED open hold when the run ends with it still open', async () => { @@ -2084,7 +2084,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['x' + 'a'.repeat(3000)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['x' + 'a'.repeat(3000)]) }, 15_000) it('keeps a flushed unterminated line when the run ends with it still open', async () => { @@ -2100,7 +2100,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['committed']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['committed']) }, 15_000) it('skips the hold for a zero-content open continuation', async () => { @@ -2120,7 +2120,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['xy']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['xy']) }, 15_000) it('seals the open hold past MAX_PENDING_CHUNKS without changing the merged entry', async () => { @@ -2142,7 +2142,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['x' + 'a'.repeat(3000) + 'y']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['x' + 'a'.repeat(3000) + 'y']) }, 15_000) it('bounds a forged open-frame flood against the log budget', async () => { @@ -2162,7 +2162,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['a'.repeat(60), logTruncationMarker(64)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['a'.repeat(60), logTruncationMarker(64)]) }, 15_000) it('commits a sealed open hold before the truncation marker', async () => { @@ -2186,8 +2186,8 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs[0]).toBe('x' + 'a'.repeat(3000)) - expect(result.logs[result.logs.length - 1]).toBe(logTruncationMarker(65536)) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)[0]).toBe('x' + 'a'.repeat(3000)) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)[result.output.filter(block => block.type === 'text').map(block => block.text).length - 1]).toBe(logTruncationMarker(65536)) }, 15_000) it('commits a flushed open prefix before the truncation marker', async () => { @@ -2204,7 +2204,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['committed', logTruncationMarker(64)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['committed', logTruncationMarker(64)]) }, 15_000) it('drops a forged fd-3 frame with illegal UTF-8 instead of accepting a mangled value', async () => { @@ -2241,7 +2241,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['a'.repeat(60), logTruncationMarker(64)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['a'.repeat(60), logTruncationMarker(64)]) }, 15_000) it('bills a merged open entry once, not per fragment', async () => { @@ -2262,7 +2262,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['x'.repeat(16)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['x'.repeat(16)]) }, 15_000) it('admits a compliant merged entry whose closing frame fits the remaining budget', async () => { @@ -2281,7 +2281,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['a'.repeat(30) + 'b'.repeat(25)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['a'.repeat(30) + 'b'.repeat(25)]) }, 15_000) it('rejects an open frame that would overflow the ledger by one byte', async () => { @@ -2301,7 +2301,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual([logTruncationMarker(64)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([logTruncationMarker(64)]) }, 15_000) it('bills the closing frame as the merged tail under an exact-fit budget', async () => { @@ -2319,7 +2319,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['a'.repeat(30) + 'b'.repeat(30)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['a'.repeat(30) + 'b'.repeat(30)]) }, 15_000) it('does not over-reject an exact-fit closing line while an open entry accumulates', async () => { @@ -2344,7 +2344,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['a'.repeat(29) + 'b'.repeat(30)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['a'.repeat(29) + 'b'.repeat(30)]) }, 15_000) @@ -2364,7 +2364,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['a'.repeat(59), logTruncationMarker(64)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['a'.repeat(59), logTruncationMarker(64)]) }, 15_000) it('truncates when the closing frame of a merged entry overflows the budget', async () => { @@ -2382,7 +2382,7 @@ describe('PythonPtcRuntime — programs and bindings', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['x'.repeat(40), logTruncationMarker(64)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['x'.repeat(40), logTruncationMarker(64)]) }, 15_000) it('keeps a float completion exact when the program mutates the decimal context', async () => { @@ -2487,8 +2487,8 @@ describe('PythonPtcRuntime — programs and bindings', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('ok') - expect(result.logs).toContain('first') - expect(result.logs).toContain('second') + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toContain('first') + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toContain('second') }) it('raises the declared errorClass with the member name on rejection', async () => { @@ -3473,7 +3473,7 @@ describe('PythonPtcRuntime — budgets, termination, disposal', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('escaped') - expect(result.logs).toContain('leader-diagnostic-no-newline') + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toContain('leader-diagnostic-no-newline') }, 8000) it('closes the child stdin so a program read sees EOF instead of blocking', async () => { @@ -3869,7 +3869,7 @@ describe('PythonPtcRuntime — hostile peer', () => { bindings: [], })) expect(result.error).toBeUndefined() - const markers = result.logs.filter(line => line.includes('log capture truncated at 128 bytes')) + const markers = result.output.filter(block => block.type === 'text').map(block => block.text).filter(line => line.includes('log capture truncated at 128 bytes')) expect(markers.length).toBeGreaterThanOrEqual(1) }) @@ -4446,7 +4446,7 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs.some(line => line.includes('log capture truncated'))).toBe(true) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).some(line => line.includes('log capture truncated'))).toBe(true) }, 20_000) it('stops host capture at the child ledger truncation, keeping exactly one marker', async () => { @@ -4473,11 +4473,11 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - const markers = result.logs.filter(line => line.includes('log capture truncated')) + const markers = result.output.filter(block => block.type === 'text').map(block => block.text).filter(line => line.includes('log capture truncated')) expect(markers).toHaveLength(1) // The marker is the LAST entry: nothing was retained after truncation. - expect(result.logs.at(-1)).toBe(markers[0]) - expect(result.logs.join('\n')).not.toContain('AFTER') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).at(-1)).toBe(markers[0]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('\n')).not.toContain('AFTER') }, 20_000) it('keeps one marker when a program forges repeated truncation frames', async () => { @@ -4500,13 +4500,13 @@ describe('PythonPtcRuntime — hostile peer', () => { expect(result.error).toBeUndefined() expect(result.value).toBe('done') // The non-boolean flag did not truncate, so its text was captured normally. - expect(result.logs).toContain('first') + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toContain('first') // The first genuine flag stopped capture and emitted the HOST's own marker; // the frame's own text is discarded, so neither payload appears. - expect(result.logs).not.toContain('MARK-A') - expect(result.logs).not.toContain('MARK-B') - expect(result.logs.at(-1)).toBe(logTruncationMarker(4096)) - expect(result.logs.filter(line => line.includes('log capture truncated'))).toHaveLength(1) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).not.toContain('MARK-A') + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).not.toContain('MARK-B') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).at(-1)).toBe(logTruncationMarker(4096)) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).filter(line => line.includes('log capture truncated'))).toHaveLength(1) }, 20_000) it('discards the text of a forged truncation frame instead of retaining it', async () => { @@ -4530,8 +4530,8 @@ describe('PythonPtcRuntime — hostile peer', () => { expect(result.value).toBe('done') // Only the host marker is kept, so the total stays orders of magnitude below // what the forgery carried — and below the cap it was trying to escape. - expect(result.logs).toEqual([logTruncationMarker(64)]) - expect(result.logs.join('').length).toBeLessThan(forgedBytes / 1000) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([logTruncationMarker(64)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('').length).toBeLessThan(forgedBytes / 1000) }, 30_000) it('coalesces unframed fd-3 fragments without recopying the sealed prefix', async () => { @@ -4587,7 +4587,7 @@ describe('PythonPtcRuntime — hostile peer', () => { } expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs).toContain('after-trickle') + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toContain('after-trickle') // Sealing appends a finished block rather than re-merging everything held, so // each byte is copied once. Re-concatenating the whole buffer at every // threshold made the cumulative copy volume quadratic — 10 MiB trickled a @@ -4654,9 +4654,9 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs.some(line => line.includes('log capture truncated'))).toBe(true) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).some(line => line.includes('log capture truncated'))).toBe(true) // The retained text is bounded by the budget, not the 1 MB the program wrote. - expect(result.logs.join('\n').length).toBeLessThan(4096) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('\n').length).toBeLessThan(4096) }, 20_000) it('discards empty writes instead of buffering one list slot each', async () => { @@ -4679,7 +4679,7 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs).toEqual([]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([]) }, 30_000) it('stops scanning a single-write newline flood once the log ledger truncates', async () => { @@ -4695,7 +4695,7 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs.filter(line => line.includes('log capture truncated'))).toHaveLength(1) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).filter(line => line.includes('log capture truncated'))).toHaveLength(1) expect(Date.now() - start).toBeLessThan(8_000) }, 15_000) @@ -4722,7 +4722,7 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs).toEqual([logTruncationMarker(256)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([logTruncationMarker(256)]) }, 40_000) it('bounds a newline-free write against the already-buffered chunks before joining them', async () => { @@ -4753,7 +4753,7 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs).toEqual([logTruncationMarker(256)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([logTruncationMarker(256)]) }, 40_000) it('bounds a newline-terminated write against the already-buffered chunks before joining them', async () => { @@ -4780,7 +4780,7 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs).toEqual([logTruncationMarker(256)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([logTruncationMarker(256)]) }, 40_000) it('emits pending text on an explicit flush, before the run can be killed', async () => { @@ -4800,7 +4800,7 @@ describe('PythonPtcRuntime — hostile peer', () => { bindings: [], })) expect(result.error?.kind).toBe('timeout') - expect(result.logs).toContain('before hang') + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toContain('before hang') }, 15_000) it('marks a dropped tail when the ledger lands on exactly zero remaining', async () => { @@ -4825,10 +4825,10 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs).toContain('y'.repeat(100)) - expect(result.logs.filter(line => line.includes('log capture truncated'))).toHaveLength(1) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toContain('y'.repeat(100)) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).filter(line => line.includes('log capture truncated'))).toHaveLength(1) // The dropped tail is not retained, but its loss is now reported. - expect(result.logs.some(line => line.includes('z'))).toBe(false) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).some(line => line.includes('z'))).toBe(false) }, 15_000) it('keeps an admitted log within the serialized array envelope at the exact limit', async () => { @@ -4850,9 +4850,9 @@ describe('PythonPtcRuntime — hostile peer', () => { expect(result.value).toBe('done') // The 60-character line was admitted; the 61-character line was not (a // single 'b' would also match the marker's "bytes", so check for the line). - expect(result.logs).toContain('a'.repeat(60)) - expect(result.logs.some(line => line.includes('b'.repeat(61)))).toBe(false) - expect(result.logs.some(line => line.includes('log capture truncated'))).toBe(true) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toContain('a'.repeat(60)) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).some(line => line.includes('b'.repeat(61)))).toBe(false) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).some(line => line.includes('log capture truncated'))).toBe(true) }, 15_000) it('rejects a log budget too small to serialize the truncation marker', async () => { @@ -4877,10 +4877,10 @@ describe('PythonPtcRuntime — hostile peer', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.some(line => line.includes('log capture truncated'))).toBe(true) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).some(line => line.includes('log capture truncated'))).toBe(true) // Serialized (escaped) size of retained entries stays in the budget's // neighborhood: well under the ~30 kB an uncharged flood would retain. - const serialized = Buffer.byteLength(JSON.stringify(result.logs), 'utf8') + const serialized = Buffer.byteLength(JSON.stringify(result.output.filter(block => block.type === 'text').map(block => block.text)), 'utf8') expect(serialized).toBeLessThan(1024) }) @@ -4903,9 +4903,9 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs.filter(line => line.includes('log capture truncated'))).toHaveLength(1) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).filter(line => line.includes('log capture truncated'))).toHaveLength(1) // Nothing of the line itself was retained: the ledger refused the whole entry. - expect(result.logs.every(line => !line.includes(String.fromCharCode(0)))).toBe(true) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).every(line => !line.includes(String.fromCharCode(0)))).toBe(true) }, 30_000) it('bounds a huge unterminated tail after an early newline without copying it whole', async () => { @@ -4933,7 +4933,7 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs.some(line => line.includes('log capture truncated'))).toBe(true) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).some(line => line.includes('log capture truncated'))).toBe(true) }, 30_000) it('flushes logs before framing the value so their peaks do not add against RLIMIT_AS', async () => { @@ -5368,8 +5368,8 @@ describe('PythonPtcRuntime — hostile peer', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.length).toBeLessThanOrEqual(65) - expect(result.logs.some(line => line.includes('log capture truncated'))).toBe(true) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).length).toBeLessThanOrEqual(65) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).some(line => line.includes('log capture truncated'))).toBe(true) }) it('reassembles multibyte UTF-8 split across stray-output pipe chunks', async () => { @@ -5399,7 +5399,7 @@ describe('PythonPtcRuntime — hostile peer', () => { bindings: [], })) expect(result.error).toBeUndefined() - const text = result.logs.join('') + const text = result.output.filter(block => block.type === 'text').map(block => block.text).join('') expect(text).toContain('\u0900\u4f60\u597d\u{1f600}\u{10ffff}') expect(text).not.toContain('\ufffd') }) @@ -5421,7 +5421,7 @@ describe('PythonPtcRuntime — hostile peer', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.join('')).toContain('�') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('�') }) it('rejects reserved words of EITHER backend language as binding globals', async () => { @@ -5452,8 +5452,8 @@ describe('PythonPtcRuntime — hostile peer', () => { bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.join('')).toContain('stray stdout') - expect(result.logs.join('')).toContain('stray stderr') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('stray stdout') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('stray stderr') }) it('flushes bytes written through sys.__stdout__/sys.__stderr__ before the done frame', async () => { @@ -5483,8 +5483,8 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs.join('')).toContain('orig stdout') - expect(result.logs.join('')).toContain('orig stderr') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('orig stdout') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('orig stderr') }, 15_000) it('escalates to SIGKILL when the program traps SIGTERM and ignores the grace period', async () => { @@ -5577,7 +5577,7 @@ describe('PythonPtcRuntime — hostile peer', () => { expect(result.error).toBeUndefined() expect(result.value).toBe('settled') // The frame was dropped as one truncation marker, not retained. - expect(result.logs).toEqual([logTruncationMarker(128)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([logTruncationMarker(128)]) // The escaped copy (~144 MiB) was never materialized. expect(process.memoryUsage().heapUsed - before).toBeLessThan(256 * 1024 * 1024) }, 90_000) @@ -5598,7 +5598,7 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('settled') - expect(result.logs).toEqual([logTruncationMarker(64)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([logTruncationMarker(64)]) }, 8000) it('caps a forged done error.message from its code-unit prefix, never encoding the whole message', async () => { @@ -5835,7 +5835,7 @@ describe('PythonPtcRuntime — hostile peer', () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs).toContain('after-cap-frames') + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toContain('after-cap-frames') }, 120_000) it('rejects an oversized first frame that lands on the sealing threshold with a newline', async () => { diff --git a/packages/experimental/ptc-runtime-python/tests/stray-fragments.spec.ts b/packages/experimental/ptc-runtime-python/tests/stray-fragments.spec.ts index b3de03c978..515de87caa 100644 --- a/packages/experimental/ptc-runtime-python/tests/stray-fragments.spec.ts +++ b/packages/experimental/ptc-runtime-python/tests/stray-fragments.spec.ts @@ -45,7 +45,7 @@ it('seals stray fragments without recopying the sealed prefix', async () => { })) expect(result.error).toBeUndefined() expect(result.value).toBe('done') - expect(result.logs).toEqual(['x'.repeat(60_000)]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['x'.repeat(60_000)]) // Sealing copies each byte at most twice; merging every accumulated prefix // instead copies over a megabyte for these 60,001 controlled fragments. expect(maxFragments).toBeLessThanOrEqual(1024) @@ -75,7 +75,7 @@ it.each([ bindings: [], })) expect(result.error).toBeUndefined() - expect(result.logs.at(-1)).toBe(logTruncationMarker(3072)) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).at(-1)).toBe(logTruncationMarker(3072)) // Each raw byte decodes to U+FFFD (three UTF-8 bytes), so a 3072-byte // budget flushes near 1024 raw bytes. Charging raw or structural widths // instead retains over 2048 bytes before flushing these payloads. diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 4d1680a48a..bc8828b314 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -8166,14 +8166,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'PtcDispatchLog', declaration: 'export interface PtcDispatchLog {\n readonly exec: ToolExecution;\n readonly agent?: Agent;\n readonly subCallId: ToolCallId;\n readonly name: string;\n readonly isError: boolean;\n readonly content: ContentBlock[];\n}', }, - { - name: 'PtcImageOutput', - declaration: 'export interface PtcImageOutput {\n afterLog: number;\n attachment: ImageAttachmentRef;\n}', - }, { name: 'PtcJsonValue', declaration: 'export type PtcJsonValue = null | boolean | number | string | PtcJsonValue[] | {\n [key: string]: PtcJsonValue;\n};', }, + { + name: 'PtcOutput', + declaration: 'export type PtcOutput = {\n type: \'text\';\n text: string;\n} | {\n type: \'image\';\n attachment: ImageAttachmentRef;\n};', + }, { name: 'PtcRunFailure', declaration: 'export interface PtcRunFailure {\n kind: \'exception\' | \'timeout\' | \'abort\' | \'worker-exit\' | \'invalid-output\' | \'output-limit\' | \'protocol\' | \'sandbox-unavailable\';\n message: string;\n}', @@ -8184,7 +8184,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'PtcRunResult', - declaration: 'export interface PtcRunResult {\n sandbox?: PtcRunSandbox;\n value?: PtcJsonValue;\n logs: string[];\n images?: PtcImageOutput[];\n error?: PtcRunFailure;\n}', + declaration: 'export interface PtcRunResult {\n sandbox?: PtcRunSandbox;\n value?: PtcJsonValue;\n output: PtcOutput[];\n error?: PtcRunFailure;\n}', }, { name: 'PtcRunSandbox', diff --git a/packages/fs/tool-fs/tests/apply-patch.spec.ts b/packages/fs/tool-fs/tests/apply-patch.spec.ts index 6fb69fc0ec..4bda7ba731 100644 --- a/packages/fs/tool-fs/tests/apply-patch.spec.ts +++ b/packages/fs/tool-fs/tests/apply-patch.spec.ts @@ -163,7 +163,7 @@ class PatchPtcRuntime extends PtcRuntime { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } } async run(request: PtcRunSpec) { - return { logs: [JSON.stringify(await request.bindings[0]!.functions.apply_patch!({ input: this.patch }))] } + return { output: [{ type: 'text' as const, text: JSON.stringify(await request.bindings[0]!.functions.apply_patch!({ input: this.patch })) }] } } } diff --git a/packages/fs/tool-fs/tests/integration.spec.ts b/packages/fs/tool-fs/tests/integration.spec.ts index 32d42fd4bb..d0341c4119 100644 --- a/packages/fs/tool-fs/tests/integration.spec.ts +++ b/packages/fs/tool-fs/tests/integration.spec.ts @@ -640,7 +640,7 @@ describe('current working directory', () => { if (read === undefined) throw new Error('missing read binding') const value = await read({ file_path: 'note.txt' }) expect(JSON.stringify(value)).toContain('current') - return { logs: [], value } + return { output: [], value } } } await ctx.plugin(ReadBindingRuntime) diff --git a/packages/fs/tool-fs/tests/read-image.spec.ts b/packages/fs/tool-fs/tests/read-image.spec.ts index 446fceab51..2d2eae7f3d 100644 --- a/packages/fs/tool-fs/tests/read-image.spec.ts +++ b/packages/fs/tool-fs/tests/read-image.spec.ts @@ -80,7 +80,7 @@ class FakeRuntime extends PtcRuntime { readonly language = 'typescript' readonly isolation = 'fake' - behavior: (request: PtcRunRequest) => Promise = () => Promise.resolve({ logs: [] }) + behavior: (request: PtcRunRequest) => Promise = () => Promise.resolve({ output: [] }) run(request: PtcRunRequest): Promise { return this.behavior(request) @@ -307,7 +307,7 @@ describe('read_image happy path', () => { if (!(value instanceof PtcImage)) throw new Error('read_image must return an Image') expect(await readFile(value.path)).toEqual(PNG_1X1) expect([value.attachment.width, value.attachment.height]).toEqual([1, 1]) - return { logs: ['before', 'after'], images: [{ afterLog: 1, attachment: value.attachment }] } + return { output: [{ type: 'text', text: 'before' }, { type: 'image', attachment: value.attachment }, { type: 'text', text: 'after' }] } } const result = await call(ctx, RUN_CODE_NAME, { @@ -332,7 +332,7 @@ describe('read_image happy path', () => { const image = await request.bindings[0]!.functions.read_image!({ file_path: 'red.png' }) if (!(image instanceof PtcImage)) throw new Error('read_image must return an Image') return { - logs: ['before', 'after'], images: [{ afterLog: 1, attachment: image.attachment }], + output: [{ type: 'text', text: 'before' }, { type: 'image', attachment: image.attachment }, { type: 'text', text: 'after' }], error: { kind, message: 'program stopped' }, } } diff --git a/packages/fs/tool-fs/tests/tools.spec.ts b/packages/fs/tool-fs/tests/tools.spec.ts index f1082845f2..cac7394a4a 100644 --- a/packages/fs/tool-fs/tests/tools.spec.ts +++ b/packages/fs/tool-fs/tests/tools.spec.ts @@ -1225,7 +1225,7 @@ class GuidancePtcRuntime extends PtcRuntime { readonly language = 'typescript' readonly isolation = 'fake' - run() { return Promise.resolve({ logs: [] }) } + run() { return Promise.resolve({ output: [] }) } } describe('scope-aware PTC guidance', () => { diff --git a/packages/mcp/mcp-resources/tests/resources.spec.ts b/packages/mcp/mcp-resources/tests/resources.spec.ts index b351920b31..da118da711 100644 --- a/packages/mcp/mcp-resources/tests/resources.spec.ts +++ b/packages/mcp/mcp-resources/tests/resources.spec.ts @@ -18,7 +18,7 @@ class BindingProbe extends PtcRuntime { readonly language = 'typescript' readonly isolation = 'in-process-test' behavior: (spec: PtcRunSpec) => Promise = async spec => ({ - logs: [JSON.stringify(Object.keys(spec.bindings[0]!.functions).sort())], + output: [{ type: 'text', text: JSON.stringify(Object.keys(spec.bindings[0]!.functions).sort()) }], }) resolve(request: PtcRunRequest): PtcRunSpec { @@ -213,13 +213,13 @@ describe('MCP resource tools', () => { expect(await binding({ server: 'docs' })).toEqual({ resources: [] }) remove() await expect(binding({ server: 'docs' })).rejects.toThrow('unknown tool') - return { logs: [] } + return { output: [] } } const result = await call(ctx, 'run_code', { code: 'invoke and remove provider', description: 'Use resource binding' }) expect(result.isError).toBe(false) expect(request).toHaveBeenCalledOnce() expect(renderPrompt(await ctx.systemPrompt.assemble())).not.toContain('mcp') - runtime.behavior = async spec => ({ logs: [JSON.stringify(Object.keys(spec.bindings[0]!.functions))] }) + runtime.behavior = async spec => ({ output: [{ type: 'text', text: JSON.stringify(Object.keys(spec.bindings[0]!.functions)) }] }) expect(await call(ctx, 'run_code', { code: 'inspect bindings', description: 'Inspect bindings' })) .toMatchObject({ isError: false, value: { logs: ['[]'] } }) }) diff --git a/packages/plan/plan-mode/tests/plan-mode.spec.ts b/packages/plan/plan-mode/tests/plan-mode.spec.ts index 99d72afd1d..ac218a02b1 100644 --- a/packages/plan/plan-mode/tests/plan-mode.spec.ts +++ b/packages/plan/plan-mode/tests/plan-mode.spec.ts @@ -503,7 +503,7 @@ describe('the soft layer', () => { readonly language = 'typescript' readonly isolation = 'fake' - run(_request: PtcRunRequest): Promise { return Promise.resolve({ logs: [] }) } + run(_request: PtcRunRequest): Promise { return Promise.resolve({ output: [] }) } } const ctx = new Context() await ctx.plugin(SystemPrompt) @@ -528,7 +528,7 @@ describe('the soft layer', () => { readonly language = 'typescript' readonly isolation = 'fake' - run(_request: PtcRunRequest): Promise { return Promise.resolve({ logs: [] }) } + run(_request: PtcRunRequest): Promise { return Promise.resolve({ output: [] }) } } const withPlanMode = new Context() await withPlanMode.plugin(SystemPrompt) @@ -944,7 +944,7 @@ describe('exit_plan_mode', () => { async run(request: PtcRunRequest): Promise { const exit = request.bindings[0]?.functions[EXIT_PLAN_MODE] if (exit === undefined) throw new Error('missing exit_plan_mode binding') - return { logs: [], value: await exit({ plan }) } + return { output: [], value: await exit({ plan }) } } } const ctx = new Context() diff --git a/packages/preset/agent-preset-registry/tests/tool-executors.spec.ts b/packages/preset/agent-preset-registry/tests/tool-executors.spec.ts index 942aebeab0..be4261b4e4 100644 --- a/packages/preset/agent-preset-registry/tests/tool-executors.spec.ts +++ b/packages/preset/agent-preset-registry/tests/tool-executors.spec.ts @@ -88,7 +88,7 @@ class ProgramFixture extends PtcRuntime { expect(request.program).toBe('inspect(await tools.echo({ value: "ptc" }))') const value = await request.bindings[0]!.functions.echo!({ value: 'ptc' }) if (typeof value !== 'string') throw new Error('expected a string echo result') - return { logs: [value] } + return { output: [{ type: 'text', text: value }] } } } diff --git a/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml b/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml index 858eb7e827..575ce81bf2 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml +++ b/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml @@ -21,8 +21,8 @@ en: 4ff4b322224196a5 zh: a157cb7ecd52e766 /deepseek-ai-dsh-ptc-runtime-node/use-this-package/execution-and-results: - en: 1f3d4bd7fe1f12cc - zh: 3398503f3a3e4cf0 + en: 873db0d91998de78 + zh: 25d947cb91a9c7cf /deepseek-ai-dsh-ptc-runtime-node/use-this-package/in-memory-modes: en: b731fd3de96dfbc8 zh: 4891bcb431014678 diff --git a/packages/ptc-runtime/ptc-runtime-node/README.md b/packages/ptc-runtime/ptc-runtime-node/README.md index 2865671b88..ce6c53a137 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.md +++ b/packages/ptc-runtime/ptc-runtime-node/README.md @@ -69,7 +69,7 @@ Profile editors can import `resolvePersistentExecution` from `@deepseek-ai/dsh-p ### Execution and results -In `per-call` mode, programs are async function bodies: top-level `await` and `return` work, and only erasable TypeScript is accepted. Node globals remain available, and a function body may declare a local `process`, including `const process = await import("node:process")`. A successful call returns its lossless-JSON value as `result.value` and captured text as `result.logs`. `result.sandbox` reports the selected mode, observed denial and the backend's full or partial enforcement independently of the program outcome. +In `per-call` mode, programs are async function bodies: top-level `await` and `return` work, and only erasable TypeScript is accepted. Node globals remain available, and a function body may declare a local `process`, including `const process = await import("node:process")`. A successful call returns its lossless-JSON value as `result.value` and captured text and images as ordered `result.output` entries. `result.sandbox` reports the selected mode, observed denial and the backend's full or partial enforcement independently of the program outcome. `inspect(...values)` renders each argument independently, joins them with spaces, and appends one captured log entry. The console global is absent by default: direct access raises `ReferenceError`, while `typeof console` returns `"undefined"`. Set `enableConsole: true` to expose its five methods through the same inspection function. With the default configuration, programs may declare their own console variables. diff --git a/packages/ptc-runtime/ptc-runtime-node/README.zh.md b/packages/ptc-runtime/ptc-runtime-node/README.zh.md index c048427cdc..248c3e436b 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.zh.md +++ b/packages/ptc-runtime/ptc-runtime-node/README.zh.md @@ -69,7 +69,7 @@ profile 编辑器可从 `@deepseek-ai/dsh-ptc-runtime-node/configuration` 导入 ### 执行与结果 -`per-call` 模式中的程序是异步函数体:支持顶层 `await` 与 `return`,且只接受可擦除 TypeScript。Node 全局变量仍然可用,函数体可声明局部 `process`,例如 `const process = await import("node:process")`。成功调用以 `result.value` 返回无损 JSON 值,以 `result.logs` 返回捕获文本。`result.sandbox` 独立于程序结果报告所选模式、观察到的拒绝,以及后端完整或部分的强制能力。 +`per-call` 模式中的程序是异步函数体:支持顶层 `await` 与 `return`,且只接受可擦除 TypeScript。Node 全局变量仍然可用,函数体可声明局部 `process`,例如 `const process = await import("node:process")`。成功调用以 `result.value` 返回无损 JSON 值,以 `result.output` 按顺序返回捕获的文本和图片。`result.sandbox` 独立于程序结果报告所选模式、观察到的拒绝,以及后端完整或部分的强制能力。 `inspect(...values)` 独立渲染各个参数,以空格连接后追加一条捕获日志。默认没有 console 全局变量:直接访问会抛出 `ReferenceError`,而 `typeof console` 返回 `"undefined"`。设置 `enableConsole: true` 可通过同一个 inspection 函数提供五个 console 方法。默认配置下,程序可以自行声明 console 变量。 diff --git a/packages/ptc-runtime/ptc-runtime-node/src/output-ledger.ts b/packages/ptc-runtime/ptc-runtime-node/src/output-ledger.ts index 4d5cc8be89..966caffeb8 100644 --- a/packages/ptc-runtime/ptc-runtime-node/src/output-ledger.ts +++ b/packages/ptc-runtime/ptc-runtime-node/src/output-ledger.ts @@ -1,64 +1,55 @@ /** Combined byte accounting for retained program output. */ -import type { PtcImageOutput, PtcJsonValue, PtcRunFailure, PtcRunResult } from '@deepseek-ai/dsh-ptc-runtime' +import type { PtcOutput, PtcJsonValue, PtcRunFailure, PtcRunResult } from '@deepseek-ai/dsh-ptc-runtime' import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from './output-json.ts' /** One run's combined outer-output ledger; binding values never enter it. */ export class OutputLedger { - private bytes = 2 // JSON serialization of the empty logs array: [] + private bytes = 2 // JSON serialization of the empty output array: [] private entries = 0 - private imageEntries = 0 constructor(private readonly maxBytes: number, private readonly overflowMessage?: string) {} - /** Admit one exact log entry, or report that the hard cap was crossed. */ - admit(text: string, sink: string[]): boolean { + /** Admit one ordered text or image entry against the combined output budget. */ + admit(entry: PtcOutput, sink: PtcOutput[]): boolean { const separatorBytes = this.entries > 0 ? 1 : 0 - const stringBytes = jsonStringBytesUpTo(text, this.maxBytes - this.bytes - separatorBytes) - if (stringBytes === undefined) return false - this.bytes += stringBytes + separatorBytes - this.entries += 1 - sink.push(text) - return true - } - - /** Admit an image descriptor against the same outer-output budget as text. */ - admitImage(output: PtcImageOutput): boolean { - const overhead = this.imageEntries === 0 ? 2 : 1 - const bytes = jsonValueBytesUpTo( - { afterLog: output.afterLog, attachment: { ...output.attachment } }, this.maxBytes - this.bytes - overhead, - ) + const remaining = this.maxBytes - this.bytes - separatorBytes + const bytes = entry.type === 'text' ? jsonStringBytesUpTo(entry.text, remaining) + : jsonValueBytesUpTo({ ...entry, attachment: { ...entry.attachment } }, remaining) if (bytes === undefined) return false - this.bytes += bytes + overhead - this.imageEntries += 1 + this.bytes += bytes + separatorBytes + this.entries += 1 + sink.push(entry) return true } /** Finalize a successful absent-or-JSON completion against the combined cap. */ - success(logs: string[], value?: PtcJsonValue): PtcRunResult { - if (value !== undefined && jsonValueBytesUpTo(value, this.maxBytes - this.bytes) === undefined) return this.limit(logs) - return { logs, ...value !== undefined ? { value } : {} } + success(output: PtcOutput[], value?: PtcJsonValue): PtcRunResult { + if (value !== undefined && jsonValueBytesUpTo(value, this.maxBytes - this.bytes) === undefined) return this.limit(output) + return { output, ...value !== undefined ? { value } : {} } } /** Finalize a failure diagnostic, with output-limit taking precedence when combined bytes exceed the cap. */ - failure(logs: string[], error: PtcRunFailure): PtcRunResult { - if (jsonStringBytesUpTo(error.message, this.maxBytes - this.bytes) === undefined) return this.limit(logs) - return { logs, error } + failure(output: PtcOutput[], error: PtcRunFailure): PtcRunResult { + if (jsonStringBytesUpTo(error.message, this.maxBytes - this.bytes) === undefined) return this.limit(output) + return { output, error } } /** Build the explicit output-limit failure while retaining a fitting prefix of the final log. */ - limit(logs: string[]): PtcRunResult { + limit(output: PtcOutput[]): PtcRunResult { const fullMessage = this.overflowMessage ?? `outer output exceeded ${this.maxBytes} bytes` // The fixed diagnostic is ASCII, so every character is one byte plus the quotes. const messageBytes = fullMessage.length + 2 - const retained: string[] = [] + const retained: PtcOutput[] = [] let retainedBytes = 2 const logBudget = this.maxBytes - messageBytes - for (const text of logs) { + for (const entry of output) { + if (entry.type === 'image') continue + const text = entry.text const separatorBytes = retained.length > 0 ? 1 : 0 const availableBytes = logBudget - retainedBytes - separatorBytes const stringBytes = jsonStringBytesUpTo(text, availableBytes) if (stringBytes !== undefined) { - retained.push(text) + retained.push({ type: 'text', text }) retainedBytes += stringBytes + separatorBytes continue } @@ -67,13 +58,13 @@ export class OutputLedger { const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes) /* v8 ignore next -- truncateJsonStringBytes guarantees its returned prefix fits the same budget. */ if (prefixBytes === undefined) throw new Error('output ledger produced an oversized log prefix') - retained.push(prefix) + retained.push({ type: 'text', text: prefix }) retainedBytes += prefixBytes + separatorBytes } break } const availableMessageBytes = this.maxBytes - retainedBytes const message = truncateJsonStringBytes(fullMessage, availableMessageBytes) - return { logs: retained, error: { kind: 'output-limit', message } } + return { output: retained, error: { kind: 'output-limit', message } } } } diff --git a/packages/ptc-runtime/ptc-runtime-node/src/worker.ts b/packages/ptc-runtime/ptc-runtime-node/src/worker.ts index ca5c5f8854..95a47f2e89 100644 --- a/packages/ptc-runtime/ptc-runtime-node/src/worker.ts +++ b/packages/ptc-runtime/ptc-runtime-node/src/worker.ts @@ -1,7 +1,7 @@ /** Managed Node process shared by temporary and owner-retained evaluations. */ import { randomUUID } from 'node:crypto' import { PtcImage } from '@deepseek-ai/dsh-ptc-runtime' -import type { PtcImageOutput } from '@deepseek-ai/dsh-ptc-runtime' +import type { PtcOutput } from '@deepseek-ai/dsh-ptc-runtime' import { AsyncLocalStorage } from 'node:async_hooks' import { Context } from '@deepseek-ai/cordis' import type { PtcRunSpec, PtcRunResult, PtcRunSandbox, PtcRunFailure, PtcBindingNamespace, PtcJsonValue } from '@deepseek-ai/dsh-ptc-runtime' @@ -124,10 +124,9 @@ export class ProgramWorker { return output.failure([], failure) } this.stderr = '' - const output = this.output() - const logs: string[] = [] + const ledger = this.output() + const output: PtcOutput[] = [] let overflowed = false - const images: PtcImageOutput[] = [] const result = Promise.withResolvers() const timer = spec.timeoutMs === null ? undefined : setTimeout(() => { const phase = evaluation.completion !== undefined ? 'during execution' : 'before code started' @@ -153,28 +152,27 @@ export class ProgramWorker { invoke: AsyncLocalStorage.bind((binding, args) => invokeBinding(binding, args, image => this.registerImage(image))), admit: (text) => { if (overflowed) return - if (!output.admit(text, logs)) { + if (!ledger.admit({ type: 'text', text }, output)) { overflowed = true - logs.push(text) + output.push({ type: 'text', text }) evaluation.finish({ kind: 'output-limit', message: 'output limit exceeded' }, undefined, true) } }, image: (attachment) => { if (overflowed || evaluation.settled) return - if (!output.admitImage({ afterLog: logs.length, attachment })) { + if (!ledger.admit({ type: 'image', attachment }, output)) { overflowed = true evaluation.finish({ kind: 'output-limit', message: 'output limit exceeded' }, undefined, true) return } - images.push({ afterLog: logs.length, attachment }) }, finish: (error, value, discard = false) => { if (evaluation.settled) return evaluation.settled = true clearTimeout(timer) spec.signal?.removeEventListener('abort', onAbort) - const complete = (): PtcRunResult => overflowed || error?.kind === 'output-limit' ? output.limit(logs) - : error ? output.failure(logs, error) : output.success(logs, value) + const complete = (): PtcRunResult => overflowed || error?.kind === 'output-limit' ? ledger.limit(output) + : error ? ledger.failure(output, error) : ledger.success(output, value) const outcome = complete() const cleanup = async (): Promise => { const interrupted = error?.kind === 'timeout' || error?.kind === 'abort' @@ -188,14 +186,14 @@ export class ProgramWorker { const completed = complete() // The final interruption diagnostic also consumes output budget. if (completed.error?.kind === 'output-limit' && preserved) drained = await this.terminate() - return drained ? completed : output.failure(logs, { kind: 'worker-exit', message: 'managed process exited but its output did not drain cleanly' }) + return drained ? completed : ledger.failure(output, { kind: 'worker-exit', message: 'managed process exited but its output did not drain cleanly' }) } void cleanup().then((completed) => { this.active = undefined - result.resolve(images.length > 0 && completed.error?.kind !== 'output-limit' ? { ...completed, images } : completed) + result.resolve(completed) }, (failure: unknown) => { this.active = undefined - result.resolve(output.failure(logs, { kind: 'worker-exit', message: `managed process cleanup failed: ${messageOf(failure)}` })) + result.resolve(ledger.failure(output, { kind: 'worker-exit', message: `managed process cleanup failed: ${messageOf(failure)}` })) }) }, } diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/built-lib.e2e.ts b/packages/ptc-runtime/ptc-runtime-node/tests/built-lib.e2e.ts index 5675a914ae..4ffe1a815e 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/built-lib.e2e.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/built-lib.e2e.ts @@ -72,13 +72,13 @@ describe.skipIf(!built)('built lib real load path (plain node)', () => { expect(resultProcess.signal).toBeUndefined() expect(resultProcess.exitCode, resultProcess.stderr).toBe(0) const lastLine = resultProcess.stdout.trim().split('\n').at(-1) ?? '' - const result = JSON.parse(lastLine) as { value?: unknown; logs: string[]; error?: unknown } + const result = JSON.parse(lastLine) as { value?: unknown; output: { type: 'text'; text: string }[]; error?: unknown } expect(result.error).toBeUndefined() expect(result.value).toEqual({ doubled: 42, failure: { typed: true, name: 'ToolCallError', toolName: 'fail', message: 'denied' }, env: [], }) - expect(result.logs).toContain('halfway 42') + expect(result.output).toContainEqual({ type: 'text', text: 'halfway 42' }) }) }) diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/execution-mode.spec.ts b/packages/ptc-runtime/ptc-runtime-node/tests/execution-mode.spec.ts index 73c7495530..1c3c11f0b5 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/execution-mode.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/execution-mode.spec.ts @@ -69,7 +69,7 @@ it.each([false, true])('advertises Script syntax and its selected retention %s', expect(runtime.executionInstructions.includes('Each call runs in a fresh Node process')).toBe(!scriptPersistence) const stateOwner = {} const run = (program: string) => runtime.run(runtime.resolve({ program, bindings: [], stateOwner })) - expect(await run('let retained: number = 17; retained')).toMatchObject({ logs: [], value: 17 }) + expect(await run('let retained: number = 17; retained')).toMatchObject({ output: [], value: 17 }) expect(await run('typeof retained')).toMatchObject({ value: scriptPersistence ? 'number' : 'undefined' }) await runtime.release(stateOwner) expect(await run('typeof retained')).toMatchObject({ value: 'undefined' }) 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..9d188bfaf4 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 @@ -78,7 +78,7 @@ describe('Node runtime host failures', () => { const h = await setup({}, 'read-only') vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(async argv => confinement([...argv])) h.onBoot(() => { h.complete({ value: encodePtcJsonWire(42) }) }) - expect(await h.start()).toEqual({ logs: [], value: 42, sandbox: { mode: 'read-only', denied: false, enforcement: 'partial' } }) + expect(await h.start()).toEqual({ output: [], value: 42, sandbox: { mode: 'read-only', denied: false, enforcement: 'partial' } }) }) it.each([['EACCES: blocked', true], ['EPERM: unrelated dialect', false]] as const)('uses only the selected denial dialect for %s', async (message, denied) => { @@ -396,7 +396,7 @@ describe('Node runtime host failures', () => { }) const result = await h.start() expect(result.error?.kind).toBe('output-limit') - expect(result.logs).toEqual(['retained']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['retained']) }) it.each([undefined, { kind: 'exception', message: 'program failed' }] as const)('bounds incomplete raw output after completion with %j', async (failure) => { @@ -429,9 +429,9 @@ describe('Node runtime host failures', () => { }) const result = await h.start() expect(result.error).toBeUndefined() - expect(result.logs.filter(text => text === '\uFEFF')).toHaveLength(2) - expect(result.logs.filter(text => text === '€')).toHaveLength(2) - expect(result.logs.filter(text => text === '�')).toHaveLength(2) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).filter(text => text === '\uFEFF')).toHaveLength(2) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).filter(text => text === '€')).toHaveLength(2) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).filter(text => text === '�')).toHaveLength(2) }) it('keeps the fitting native-output prefix and ignores later overflow chunks', async () => { @@ -444,8 +444,8 @@ describe('Node runtime host failures', () => { h.onBoot(() => { h.stdout.write('x'.repeat(1024)) }) const result = await h.start() expect(result.error?.kind).toBe('output-limit') - expect(result.logs.join('')).toMatch(/^x+$/) - expect(Buffer.byteLength(JSON.stringify(result.logs))).toBeLessThanOrEqual(256) + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toMatch(/^x+$/) + expect(Buffer.byteLength(JSON.stringify(result.output.filter(block => block.type === 'text').map(block => block.text)))).toBeLessThanOrEqual(256) }) it.each(['stdout', 'stderr'] as const)('reports a broken raw %s pipe', async (name) => { diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/output-ledger.spec.ts b/packages/ptc-runtime/ptc-runtime-node/tests/output-ledger.spec.ts index 98cbef90da..4df1176088 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/output-ledger.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/output-ledger.spec.ts @@ -1,42 +1,44 @@ import { AttachmentId } from '@deepseek-ai/dsh-attachment' import { expect, it } from 'vitest' +import type { PtcOutput } from '@deepseek-ai/dsh-ptc-runtime' import { OutputLedger } from '../src/output-ledger.ts' it('accounts for exact JSON escaping, separators and optional completion values', () => { const ledger = new OutputLedger(20) - const logs: string[] = [] - expect(ledger.admit('first', logs)).toBe(true) - expect(ledger.admit('second', logs)).toBe(true) - expect(ledger.admit('third', logs)).toBe(false) - expect(ledger.success(logs)).toEqual({ logs: ['first', 'second'] }) - expect(ledger.success(logs, 1)).toEqual({ logs: ['first', 'second'], value: 1 }) - expect(ledger.success(logs, 'too long').error?.kind).toBe('output-limit') + const output: PtcOutput[] = [] + expect(ledger.admit({ type: 'text', text: 'first' }, output)).toBe(true) + expect(ledger.admit({ type: 'text', text: 'second' }, output)).toBe(true) + expect(ledger.admit({ type: 'text', text: 'third' }, output)).toBe(false) + expect(ledger.success(output)).toEqual({ output: [{ type: 'text', text: 'first' }, { type: 'text', text: 'second' }] }) + expect(ledger.success(output, 1)).toEqual({ output: [{ type: 'text', text: 'first' }, { type: 'text', text: 'second' }], value: 1 }) + expect(ledger.success(output, 'too long').error?.kind).toBe('output-limit') }) it('retains a bounded prefix when logs or a failure diagnostic exceed the limit', () => { for (const maxBytes of [4, 8, 40, 80]) { const ledger = new OutputLedger(maxBytes) - const result = ledger.limit(['a', 'b', '你好🙂'.repeat(50)]) + const result = ledger.limit(['a', 'b', '你好🙂'.repeat(50)].map(text => ({ type: 'text', text }))) expect(result.error?.kind).toBe('output-limit') - const bytes = Buffer.byteLength(JSON.stringify(result.logs)) + Buffer.byteLength(JSON.stringify(result.error?.message)) + const bytes = Buffer.byteLength(JSON.stringify(result.output.filter(block => block.type === 'text').map(block => block.text))) + Buffer.byteLength(JSON.stringify(result.error?.message)) expect(bytes).toBeLessThanOrEqual(maxBytes) } const ledger = new OutputLedger(80) - expect(ledger.failure([], { kind: 'exception', message: 'short' })).toEqual({ logs: [], error: { kind: 'exception', message: 'short' } }) + expect(ledger.failure([], { kind: 'exception', message: 'short' })).toEqual({ output: [], error: { kind: 'exception', message: 'short' } }) expect(ledger.failure([], { kind: 'exception', message: 'x'.repeat(100) }).error?.kind).toBe('output-limit') }) it('charges ordered image descriptors and text against one budget', () => { const attachment = { attachmentId: AttachmentId('sha256:image'), mediaType: 'image/png' as const, bytes: 64, width: 8, height: 4 } - const entry = { afterLog: 0, attachment } + const entry: PtcOutput = { type: 'image', attachment } const bytes = Buffer.byteLength(JSON.stringify([entry])) - const ledger = new OutputLedger(bytes + 2) - expect(ledger.admitImage(entry)).toBe(true) - expect(ledger.admitImage(entry)).toBe(false) - expect(ledger.admit('text', [])).toBe(false) - expect(ledger.success([])).toEqual({ logs: [] }) - const multiple = new OutputLedger(bytes * 2 + 2) - expect(multiple.admitImage(entry)).toBe(true) - expect(multiple.admitImage(entry)).toBe(true) + const ledger = new OutputLedger(bytes) + const output: PtcOutput[] = [] + expect(ledger.admit(entry, output)).toBe(true) + expect(ledger.admit(entry, output)).toBe(false) + expect(ledger.admit({ type: 'text', text: 'text' }, output)).toBe(false) + expect(ledger.success(output)).toEqual({ output: [entry] }) + const multiple = new OutputLedger(Buffer.byteLength(JSON.stringify([entry, entry]))) + expect(multiple.admit(entry, [])).toBe(true) + expect(multiple.admit(entry, [])).toBe(true) }) diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/output.spec.ts b/packages/ptc-runtime/ptc-runtime-node/tests/output.spec.ts index 8714a52784..fa7970dfdc 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/output.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/output.spec.ts @@ -26,7 +26,7 @@ describe.each(modes)('program output in $executionMode with retention $scriptPer const { run } = await setup() const result = await run('inspect("%s %d %j %%", "text", 7, { n: 1 }, [2, 3]);') expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['%s %d %j %% text 7 { n: 1 } [ 2, 3 ]']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['%s %d %j %% text 7 { n: 1 } [ 2, 3 ]']) expect(result.value).toBeUndefined() }) @@ -37,10 +37,10 @@ describe.each(modes)('program output in $executionMode with retention $scriptPer inspect(Buffer.alloc(16 * 1024 * 1024), new Uint8Array(16 * 1024 * 1024)); `) expect(result.error).toBeUndefined() - expect(result.logs).toHaveLength(1) - expect(result.logs[0]).toContain(' block.type === 'text').map(block => block.text)).toHaveLength(1) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)[0]).toContain(' block.type === 'text').map(block => block.text)[0]).toContain('Uint8Array(') + expect(result.output.filter(block => block.type === 'text').map(block => block.text)[0]!.length).toBeLessThan(2_000) }) it('emits selected images among text and objects and preserves immutable image properties', async () => { @@ -64,18 +64,20 @@ describe.each(modes)('program output in $executionMode with retention $scriptPer discardCompletion: true, })) expect(result.error).toBeUndefined() - expect(result.logs[0]).toBe('true sha256:before /images/before.png 8 4 true') - expect(result.logs[1]).toBe('before { windowId: 23 }') - expect(result.logs[2]).toContain('after [') - expect(result.logs[2]).toContain("attachmentId: 'sha256:before'") - expect(result.logs[2]).toContain("attachmentId: 'sha256:after'") - expect(result.images).toEqual([ - { afterLog: 2, attachment: before.attachment }, - { afterLog: 3, attachment: after.attachment }, - { afterLog: 3, attachment: before.attachment }, + expect(result.output).toEqual([ + { type: 'text', text: 'true sha256:before /images/before.png 8 4 true' }, + { type: 'text', text: 'before { windowId: 23 }' }, + { type: 'image', attachment: before.attachment }, + { type: 'text', text: expect.stringContaining('after [') as string }, + { type: 'image', attachment: after.attachment }, + { type: 'image', attachment: before.attachment }, + { type: 'text', text: expect.stringContaining('forged:') as string }, ]) - expect(result.images?.[0]?.attachment).toBe(before.attachment) - expect(result.images?.[1]?.attachment).toBe(after.attachment) + const preview = result.output[3] + if (preview?.type !== 'text') throw new Error('Expected the nested image preview') + expect(preview.text).toContain("attachmentId: 'sha256:before'") + expect(preview.text).toContain("attachmentId: 'sha256:after'") + expect(result.output.find(block => block.type === 'image')?.attachment).toBe(before.attachment) }) it('keeps uninspected images in the program and exposes image output before an exception', async () => { @@ -83,10 +85,9 @@ describe.each(modes)('program output in $executionMode with retention $scriptPer const image = new PtcImage('/images/selected.png', { attachmentId: AttachmentId('sha256:selected'), mediaType: 'image/png', bytes: 64, width: 8, height: 4 }) const bindings = [{ global: 'tools', functions: { image: () => Promise.resolve(image) } }] const quiet = await runtime.run(runtime.resolve({ program: 'await tools.image({})', bindings, discardCompletion: true })) - expect(quiet.images).toBeUndefined() - expect(quiet.logs).toEqual([]) + expect(quiet.output).toEqual([]) const failed = await runtime.run(runtime.resolve({ program: 'inspect(await tools.image({})); throw new Error("after image")', bindings, discardCompletion: true })) - expect(failed.images).toEqual([{ afterLog: 0, attachment: image.attachment }]) + expect(failed.output).toEqual([{ type: 'image', attachment: image.attachment }]) expect(failed.error?.kind).toBe('exception') }) @@ -100,13 +101,26 @@ describe.each(modes)('program output in $executionMode with retention $scriptPer bindings: [{ global: 'tools', functions: { image: () => Promise.resolve(image) } }], })) expect(first.error).toBeUndefined() - expect(first.images).toBeUndefined() + expect(first.output).toEqual([]) const second = await runtime.run(runtime.resolve({ program: 'inspect(retainedImage)', stateOwner, discardCompletion: true, bindings: [] })) expect(second.error).toBeUndefined() - expect(second.images).toEqual([{ afterLog: 0, attachment: image.attachment }]) + expect(second.output).toEqual([{ type: 'image', attachment: image.attachment }]) await runtime.release(stateOwner) }) + it('retains a text prefix when output overflows after an image', async () => { + const { runtime } = await mountRuntime(new Context(), { ...mode, maxOutputBytes: 256 }) + const image = new PtcImage('/images/selected.png', { attachmentId: AttachmentId('sha256:selected'), mediaType: 'image/png', bytes: 64, width: 8, height: 4 }) + const result = await runtime.run(runtime.resolve({ + program: 'inspect("before", await tools.image({}), "x".repeat(512))', + bindings: [{ global: 'tools', functions: { image: () => Promise.resolve(image) } }], + discardCompletion: true, + })) + expect(result.error?.kind).toBe('output-limit') + expect(result.output[0]).toEqual({ type: 'text', text: 'before' }) + expect(result.output.every(block => block.type === 'text')).toBe(true) + }) + it('bounds explicitly inspected image descriptors while allowing uninspected images', async () => { const { runtime } = await mountRuntime(new Context(), { ...mode, maxOutputBytes: 128 }) const image = new PtcImage('/images/large.png', { attachmentId: AttachmentId('sha256:large'), mediaType: 'image/png', bytes: 64, width: 8, height: 4, name: 'x'.repeat(200) }) @@ -115,20 +129,20 @@ describe.each(modes)('program output in $executionMode with retention $scriptPer expect(quiet.error).toBeUndefined() const selected = await runtime.run(runtime.resolve({ program: 'inspect(await tools.image({}))', bindings, discardCompletion: true })) expect(selected.error?.kind).toBe('output-limit') - expect(selected.images).toBeUndefined() + expect(selected.output).toEqual([]) }) it('discards completion values without reading their getters or requiring lossless JSON', async () => { const { run, completion } = await setup() const object = await run(completion('({ get value() { throw new Error("completion getter ran") } })')) expect(object.error).toBeUndefined() - expect(object.logs).toEqual([]) + expect(object.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([]) expect(object.value).toBeUndefined() const bigint = await run(completion('42n')) expect(bigint.error).toBeUndefined() expect(bigint.value).toBeUndefined() const captured = await run(`inspect("selected"); ${completion('"hidden"')}`) - expect(captured.logs).toEqual(['selected']) + expect(captured.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['selected']) expect(captured.value).toBeUndefined() }) @@ -141,17 +155,17 @@ describe.each(modes)('program output in $executionMode with retention $scriptPer } `) expect(caught.error).toBeUndefined() - expect(caught.logs).toEqual(['undefined undefined false', ...Array(2).fill('ReferenceError console is not defined')]) + expect(caught.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['undefined undefined false', ...Array(2).fill('ReferenceError console is not defined')]) const failed = await run('void console') expect(failed.error?.kind).toBe('exception') expect(failed.error?.message).toContain('ReferenceError: console is not defined') expect(runtime.executionInstructions).toContain('Console is unavailable. Use inspect(...values) for output.') const local = await run('{ const console = { value: 42 }; inspect(console.value) }') expect(local.error).toBeUndefined() - expect(local.logs).toEqual(['42']) + expect(local.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['42']) if (runtime.retainsState) { expect((await run('var console = { value: 17 }')).error).toBeUndefined() - expect((await run('inspect(console.value)')).logs).toEqual(['17']) + expect((await run('inspect(console.value)')).output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['17']) } }) @@ -165,7 +179,7 @@ describe.each(modes)('program output in $executionMode with retention $scriptPer } `) expect(result.error).toBeUndefined() - expect(result.logs).toEqual(['selected { n: 1 }', 'true', '%s log', 'true', '%s info', 'true', '%s warn', 'true', '%s error', 'true', '%s debug']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['selected { n: 1 }', 'true', '%s log', 'true', '%s info', 'true', '%s warn', 'true', '%s error', 'true', '%s debug']) expect(runtime.executionInstructions).toContain('Use inspect(...values) for output.') expect(runtime.executionInstructions).not.toContain('console') }) @@ -179,8 +193,8 @@ describe.each(modes)('program output in $executionMode with retention $scriptPer expect(result.error?.message).toContain(enabled ? 'console.dir is not a function' : 'console is not defined') } else { expect(result.error).toBeUndefined() - expect(result.logs.join('\n')).toContain('module warning') - expect(result.logs.join('\n')).toContain('dependency: true') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('\n')).toContain('module warning') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('\n')).toContain('dependency: true') } }) diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/persistent-built.e2e.ts b/packages/ptc-runtime/ptc-runtime-node/tests/persistent-built.e2e.ts index 5f315d1ef9..f037b7060e 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/persistent-built.e2e.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/persistent-built.e2e.ts @@ -38,6 +38,6 @@ describe.skipIf(!built)('persistent PTC published exports', () => { expect(result.exitCode, result.stderr).toBe(0) const observed = JSON.parse(result.stdout.trim().split('\n').at(-1) ?? '') as { first: { error?: unknown } } expect(observed.first.error).toBeUndefined() - expect(observed).toMatchObject({ second: { logs: ['b'], value: 42 }, interrupted: { error: { kind: 'timeout' } }, preserved: { value: 43 }, fresh: { logs: [], value: 'undefined' } }) + expect(observed).toMatchObject({ second: { output: [{ type: 'text', text: 'b' }], value: 42 }, interrupted: { error: { kind: 'timeout' } }, preserved: { value: 43 }, fresh: { output: [], value: 'undefined' } }) }) }) diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/persistent-host.spec.ts b/packages/ptc-runtime/ptc-runtime-node/tests/persistent-host.spec.ts index 0eba47ddeb..2bece38c19 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/persistent-host.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/persistent-host.spec.ts @@ -233,11 +233,11 @@ describe('persistent PTC host protocol', () => { h.stderr.write('stderr') h.complete({}) }) - expect(await h.start()).toMatchObject({ logs: ['stdout', 'stderr'] }) + expect(await h.start()).toMatchObject({ output: [{ type: 'text', text: 'stdout' }, { type: 'text', text: 'stderr' }] }) h.stdout.write('idle stdout') h.stderr.write('idle stderr') h.onBoot(() => { h.complete({}) }) - expect(await h.start(request, NO_INITIAL_FRAME)).toMatchObject({ logs: [] }) + expect(await h.start(request, NO_INITIAL_FRAME)).toMatchObject({ output: [] }) expect(h.terminate).not.toHaveBeenCalled() }) @@ -340,7 +340,7 @@ describe('persistent PTC host protocol', () => { }) const result = await h.start() expect(result.error?.kind).toBe('worker-exit') - expect(result.logs).toEqual([]) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual([]) } }) @@ -357,12 +357,12 @@ describe('persistent PTC host protocol', () => { ])('bounds cleanup failure after parse diagnostics overflow a $maxOutputBytes-byte budget', async ({ maxOutputBytes, kind }) => { const h = await setup({ maxOutputBytes }) h.onBoot(() => { h.complete({}) }) - expect(await h.start()).toMatchObject({ logs: [] }) + expect(await h.start()).toMatchObject({ output: [] }) h.waitForExit.mockRejectedValue(new Error('x')) const result = await h.runtime.run(h.runtime.resolve({ ...request, program: 'return 1' })) expect(result.error?.kind).toBe(kind) if (kind === 'worker-exit') expect(result.error?.message).toContain('cleanup failed: x') - const bytes = Buffer.byteLength(JSON.stringify(result.logs)) + Buffer.byteLength(JSON.stringify(result.error?.message)) + const bytes = Buffer.byteLength(JSON.stringify(result.output.filter(block => block.type === 'text').map(block => block.text))) + Buffer.byteLength(JSON.stringify(result.error?.message)) expect(bytes).toBeLessThanOrEqual(maxOutputBytes) expect(h.terminate).toHaveBeenCalledTimes(1) expect(h.waitForExit).toHaveBeenCalledTimes(1) @@ -608,6 +608,6 @@ it('preserves logs in a result-level failure when temporary process pipes do not h.stdout.destroy() }) const result = await h.start({ program: '1', bindings: [] }) - expect(result.logs).toEqual(['captured before cleanup']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['captured before cleanup']) expect(result.error).toEqual({ kind: 'worker-exit', message: 'managed process exited but its output did not drain cleanly' }) }) 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 a4db4dbe8d..8ade43f3b1 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/persistent.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/persistent.spec.ts @@ -75,7 +75,7 @@ describe.each([ it('retains declarations, closures, object identity, and imports across evaluations', async () => { const { run, cwd } = await setup() expect((await run('const box: { count: number } = { count: 1 }; let alias = box; function increment() { return ++box.count }; const path = await import("node:path");')).error).toBeUndefined() - expect(await run('inspect("second call"); ({ count: increment(), same: box === alias, dir: path.basename(process.cwd()), env: Object.keys(process.env) })')).toMatchObject({ logs: ['second call'], value: { count: 2, same: true, dir: cwd.split(/[\\/]/).at(-1), env: [] } }) + expect(await run('inspect("second call"); ({ count: increment(), same: box === alias, dir: path.basename(process.cwd()), env: Object.keys(process.env) })')).toMatchObject({ output: [{ type: 'text', text: 'second call' }], value: { count: 2, same: true, dir: cwd.split(/[\\/]/).at(-1), env: [] } }) }) @@ -95,9 +95,9 @@ describe.each([ const { run } = await setup() await run('let total = 0;') const code = '{ const amount = await tools.value({}); total += amount; inspect(amount); }' - expect(await run(code, { bindings: bindings(async () => 2) })).toMatchObject({ logs: ['2'] }) - expect(await run(code, { bindings: bindings(async () => 3) })).toMatchObject({ logs: ['3'] }) - expect(await run('({ total, local: typeof amount })')).toMatchObject({ logs: [], value: { total: 5, local: 'undefined' } }) + expect(await run(code, { bindings: bindings(async () => 2) })).toMatchObject({ output: [{ type: 'text', text: '2' }] }) + expect(await run(code, { bindings: bindings(async () => 3) })).toMatchObject({ output: [{ type: 'text', text: '3' }] }) + expect(await run('({ total, local: typeof amount })')).toMatchObject({ output: [], value: { total: 5, local: 'undefined' } }) }) it('uses the current error descriptor through a saved binding and retains matching constructor identity', async () => { @@ -134,7 +134,7 @@ describe.each([ await run('let retained = 17; let trigger; let rejection; const expired = new Promise(resolve => { trigger = resolve }); void expired.then(async () => { try { await tools.value({}) } catch (error) { rejection = error.message } });', options) expect((await run('trigger(); await new Promise(resolve => setImmediate(resolve)); rejection', options)).value).toBe('PTC evaluation ended') expect(called).not.toHaveBeenCalled() - expect(await run('({ retained, answer: await tools.value({}) })', options)).toMatchObject({ logs: [], value: { retained: 17, answer: 42 } }) + expect(await run('({ retained, answer: await tools.value({}) })', options)).toMatchObject({ output: [], value: { retained: 17, answer: 42 } }) expect(called).toHaveBeenCalledTimes(1) }) @@ -144,8 +144,8 @@ describe.each([ const result = await run(`let retained = 17; const child = await import("node:child_process"); child.execFileSync(process.execPath, ["-e", ${JSON.stringify(childProgram)}], { stdio: "inherit" }); retained`) expect(result.error).toBeUndefined() expect(result.value).toBe(17) - expect(result.logs.join('')).toContain('stdout marker') - expect(result.logs.join('')).toContain('stderr marker') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('stdout marker') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('stderr marker') expect((await run('retained')).value).toBe(17) }) @@ -155,7 +155,7 @@ describe.each([ expect(result.error?.kind).toBe('worker-exit') expect(result.error?.message).toContain('7') expect(result.error?.message).toContain('fatal fixture') - expect(result.logs.join('')).toContain('fatal fixture') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('fatal fixture') expect((await run('42')).value).toBe(42) }) @@ -236,7 +236,7 @@ describe.each([ await entered.promise if (action === 'abort') controller.abort() const result = await pending - expect(result.logs).toEqual(['before stop']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['before stop']) expect(result.error?.kind).toBe(action) expect(result.error?.message).toContain('retained variables were preserved') expect((await run('({ count: kept.count, same: kept === alias, effects })')).value) @@ -259,7 +259,7 @@ describe.each([ await expect.poll(async () => readFile(join(cwd, 'entered-loop'), 'utf8')).toBe('ready') if (action === 'abort') controller.abort() const result = await pending - expect(result.logs).toEqual(['before stop']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['before stop']) expect(result.error?.kind).toBe(action) expect(result.error?.message).toContain('retained variables were cleared') expect((await run('typeof kept')).value).toBe('undefined') @@ -357,7 +357,7 @@ describe.each([ const result = await run(program) expect(result.error?.kind).toBe('output-limit') expect(result.error?.message).toContain('retained variables were cleared') - const outputBytes = Buffer.byteLength(JSON.stringify(result.logs)) + Buffer.byteLength(JSON.stringify(result.error?.message)) + const outputBytes = Buffer.byteLength(JSON.stringify(result.output.filter(block => block.type === 'text').map(block => block.text))) + Buffer.byteLength(JSON.stringify(result.error?.message)) expect(outputBytes).toBeLessThanOrEqual(100) expect((await run('typeof retained')).value).toBe('undefined') }) @@ -367,7 +367,7 @@ describe.each([ await run('let retained = true;') const result = await run('const =') expect(result.error?.kind).toBe('output-limit') - const outputBytes = Buffer.byteLength(JSON.stringify(result.logs)) + Buffer.byteLength(JSON.stringify(result.error?.message)) + const outputBytes = Buffer.byteLength(JSON.stringify(result.output.filter(block => block.type === 'text').map(block => block.text))) + Buffer.byteLength(JSON.stringify(result.error?.message)) expect(outputBytes).toBeLessThanOrEqual(32) expect((await run('typeof retained')).value).toBe('undefined') }) @@ -396,7 +396,7 @@ describe.each([ it.each(['_', '_error'])('keeps %s assignments and subsequent tools in the active evaluation', async (name) => { const { run } = await setupRuntime() const called = vi.fn(async () => 7) - expect(await run(`${name} = 42; await tools.value({}); ${name}`, { bindings: bindings(called) })).toMatchObject({ logs: [], value: 42 }) + expect(await run(`${name} = 42; await tools.value({}); ${name}`, { bindings: bindings(called) })).toMatchObject({ output: [], value: 42 }) expect(called).toHaveBeenCalledOnce() expect((await run(name)).value).toBe(42) }) @@ -438,8 +438,8 @@ it('retains implicit REPL globals and does not attribute a completed callback ex const acknowledged = new Promise(resolve => { acknowledge = resolve }); void fired.then(() => setImmediate(() => { acknowledge(); throw new Error('completed callback failure') })); `) - expect(await run('fireLate(); await acknowledged; implicitValue')).toMatchObject({ logs: [], value: 7 }) - expect(await run('implicitValue')).toMatchObject({ logs: [], value: 7 }) + expect(await run('fireLate(); await acknowledged; implicitValue')).toMatchObject({ output: [], value: 7 }) + expect(await run('implicitValue')).toMatchObject({ output: [], value: 7 }) }) it('settles an asynchronous exception once while a later evaluation awaits a tool', async () => { @@ -447,6 +447,6 @@ it('settles an asynchronous exception once while a later evaluation awaits a too const failed = await run('let retained = 17; let resume; const paused = new Promise(resolve => { resume = resolve }); setImmediate(() => { throw new Error("async failure") }); await paused; retained') expect(failed.error?.message).toContain('async failure') const next = await run('resume(); await tools.value({})', { bindings: bindings(async () => { await setImmediate(); return 42 }) }) - expect(next).toMatchObject({ logs: [], value: 42 }) + expect(next).toMatchObject({ output: [], value: 42 }) expect((await run('retained')).value).toBe(17) }) diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/runtime.spec.ts b/packages/ptc-runtime/ptc-runtime-node/tests/runtime.spec.ts index 70b360fbef..671ab0ee65 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/runtime.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/runtime.spec.ts @@ -51,7 +51,7 @@ describe('Node program process', () => { const result = await run({ program: 'const n: number = 6; inspect("ready"); return { n: n * 7, cwd: process.cwd(), env: { ...process.env } };', bindings: [] }) expect(result.error).toBeUndefined() expect(result.value).toEqual({ n: 42, cwd, env: {} }) - expect(result.logs).toEqual(['ready']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['ready']) expect(result.sandbox).toEqual({ mode: 'danger-full-access', denied: false }) }) @@ -115,7 +115,7 @@ describe('Node program process', () => { const release = Promise.withResolvers() onTestFinished(() => { release.resolve(null) }) const result = await run({ program: 'void tools.slow({}); return 1', bindings: bindings({ slow: () => release.promise }) }) - expect(result).toMatchObject({ logs: [], value: 1 }) + expect(result).toMatchObject({ output: [], value: 1 }) expect(result.error).toBeUndefined() }) @@ -124,8 +124,8 @@ describe('Node program process', () => { const result = await run({ program: 'const fs = await import("node:fs"); fs.writeSync(1,"native-out你好"); fs.writeSync(2,"native-err🙂"); return 42;', bindings: [] }) expect(result.error).toBeUndefined() expect(result.value).toBe(42) - expect(result.logs.join('')).toContain('native-out你好') - expect(result.logs.join('')).toContain('native-err🙂') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('native-out你好') + expect(result.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('native-err🙂') }) it.each(['throw new Error("broken")', 'enum E { A }'])('reports program failure for %s', async (program) => { @@ -142,7 +142,7 @@ describe('Node program process', () => { const { run } = await setup({ maxOutputBytes: 80 }) const result = await run({ program: 'inspect("x".repeat(500)); return 42;', bindings: [] }) expect(result.error?.kind).toBe('output-limit') - const bytes = Buffer.byteLength(JSON.stringify(result.logs)) + Buffer.byteLength(JSON.stringify(result.error?.message)) + const bytes = Buffer.byteLength(JSON.stringify(result.output.filter(block => block.type === 'text').map(block => block.text))) + Buffer.byteLength(JSON.stringify(result.error?.message)) expect(bytes).toBeLessThanOrEqual(80) }) @@ -181,7 +181,7 @@ describe('Node program process', () => { controller.abort('stop') const result = await pending expect(result.error?.kind).toBe('abort') - expect(result.logs).toEqual(['before hot loop']) + expect(result.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['before hot loop']) } finally { controller.abort(); await pending } }) @@ -323,10 +323,10 @@ describe('Node program process', () => { it('preserves empty console entries and bounds native output overflow', async () => { const { run } = await setup({ maxOutputBytes: 100 }) const lines = await run({ program: 'inspect("a"); inspect(""); inspect("b")', bindings: [] }) - expect(lines.logs).toEqual(['a', '', 'b']) + expect(lines.output.filter(block => block.type === 'text').map(block => block.text)).toEqual(['a', '', 'b']) const overflow = await run({ program: 'const fs=await import("node:fs"); fs.writeSync(1,"HEAD-"+"x".repeat(100000));', bindings: [] }) expect(overflow.error?.kind).toBe('output-limit') - expect(overflow.logs.join('')).toContain('HEAD-') - const bytes = Buffer.byteLength(JSON.stringify(overflow.logs)) + Buffer.byteLength(JSON.stringify(overflow.error?.message)) + expect(overflow.output.filter(block => block.type === 'text').map(block => block.text).join('')).toContain('HEAD-') + const bytes = Buffer.byteLength(JSON.stringify(overflow.output.filter(block => block.type === 'text').map(block => block.text))) + Buffer.byteLength(JSON.stringify(overflow.error?.message)) expect(bytes).toBeLessThanOrEqual(100) }) diff --git a/packages/ptc-runtime/ptc-runtime-node/tests/timeout.spec.ts b/packages/ptc-runtime/ptc-runtime-node/tests/timeout.spec.ts index 8d557153f8..ddf25f727a 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/timeout.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/timeout.spec.ts @@ -29,7 +29,7 @@ describe.each([false, true])('deadline reporting with retained state %s', (retai await vi.advanceTimersByTimeAsync(100) expect(settled).not.toHaveBeenCalled() lookup.resolve(process.execPath) - expect(await run).toMatchObject({ logs: [], error: { kind: 'timeout', message: `execution timed out before code started (100ms)${cleared}` } }) + expect(await run).toMatchObject({ output: [], error: { kind: 'timeout', message: `execution timed out before code started (100ms)${cleared}` } }) expect(h.spawn).not.toHaveBeenCalled() } finally { h.controller.abort('test cleanup') @@ -49,7 +49,7 @@ describe.each([false, true])('deadline reporting with retained state %s', (retai await spawned.promise h.stderr.write('startup diagnostic') await vi.advanceTimersByTimeAsync(100) - expect(await run).toMatchObject({ logs: ['startup diagnostic'], error: { kind: 'timeout', message: `execution timed out before code started (100ms)${cleared}` } }) + expect(await run).toMatchObject({ output: [{ type: 'text', text: 'startup diagnostic' }], error: { kind: 'timeout', message: `execution timed out before code started (100ms)${cleared}` } }) expect(h.messages).not.toContainEqual(expect.objectContaining({ m: 'run' })) expect(h.terminate).toHaveBeenCalledOnce() expect(h.waitForExit).toHaveBeenCalledOnce() @@ -84,7 +84,7 @@ describe.each([false, true])('deadline reporting with retained state %s', (retai exited.resolve(true) } const suffix = retained ? '; retained variables were preserved' : '' - expect(await run).toMatchObject({ logs: ['partial output'], error: { kind: 'timeout', message: `execution timed out during execution (100ms)${suffix}` } }) + expect(await run).toMatchObject({ output: [{ type: 'text', text: 'partial output' }], error: { kind: 'timeout', message: `execution timed out during execution (100ms)${suffix}` } }) } finally { h.controller.abort('test cleanup') exited.resolve(true) @@ -161,7 +161,7 @@ describe.each(['timeout', 'abort'] as const)('retained %s', (action) => { expect(h.messages).toContainEqual({ t: 's', i: 'queued', e: { message: 'PTC evaluation ended before the binding completed' } }) h.complete({}) expect((await pending).error?.message).toContain('retained variables were preserved') - expect((await pending).logs).toEqual([]) + expect((await pending).output.filter(block => block.type === 'text').map(block => block.text)).toEqual([]) await vi.advanceTimersByTimeAsync(100) expect(h.terminate).not.toHaveBeenCalled() } finally { await h.runtime.release(h.request.stateOwner); vi.useRealTimers() } @@ -271,7 +271,7 @@ it('keeps the timeout outcome when run completes during recovery and ignores a d await vi.advanceTimersByTimeAsync(0) expect(settled).not.toHaveBeenCalled() h.complete({}) - expect(await next).toMatchObject({ logs: [] }) + expect(await next).toMatchObject({ output: [] }) expect((await next).error).toBeUndefined() expect(h.spawn).toHaveBeenCalledOnce() expect(h.terminate).not.toHaveBeenCalled() diff --git a/packages/ptc-runtime/ptc-runtime/README.i18n.yaml b/packages/ptc-runtime/ptc-runtime/README.i18n.yaml index 17789ff0c4..d503b76e3c 100644 --- a/packages/ptc-runtime/ptc-runtime/README.i18n.yaml +++ b/packages/ptc-runtime/ptc-runtime/README.i18n.yaml @@ -18,8 +18,8 @@ en: 346f62bbfd0e6d9d zh: bf86b2d7a26e8193 /deepseek-ai-dsh-ptc-runtime/use-this-package/run-a-program: - en: 280cd8a6f6a0d946 - zh: 995402924714d36d + en: eb20b9e698ce07b4 + zh: 815af1e728cb1316 /deepseek-ai-dsh-ptc-runtime/use-this-package/choose-a-backend: en: fc0d515f0c3664f3 zh: 0a1b646b5cf92232 @@ -39,8 +39,8 @@ en: 1d7a792c9a78a517 zh: b49bddf18c259edc /deepseek-ai-dsh-ptc-runtime/understand-the-implementation/vocabulary: - en: 855cb8366e3bd293 - zh: da6d7a0ce51f97fc + en: a499ed426b20e296 + zh: 53a2bdb062cf85ba /deepseek-ai-dsh-ptc-runtime/understand-the-implementation/portable-identifiers: en: f76c4f5d4877d746 zh: 21ec1e77e3861fcc @@ -57,8 +57,8 @@ en: 40294c1c31b74824 zh: 081cebc2e2650f03 /deepseek-ai-dsh-ptc-runtime/known-limitations-and-deferred-work: - en: e20364d073f78c60 - zh: 6cac5c46ff91b3ca + en: 751969896e53743d + zh: b66d61ebd4870ad0 /deepseek-ai-dsh-ptc-runtime/known-limitations-and-deferred-work/dev-note: en: d56846da29ad869c zh: 0397151a7e29b8ba diff --git a/packages/ptc-runtime/ptc-runtime/README.md b/packages/ptc-runtime/ptc-runtime/README.md index 5cb0f57512..1ad624769a 100644 --- a/packages/ptc-runtime/ptc-runtime/README.md +++ b/packages/ptc-runtime/ptc-runtime/README.md @@ -29,11 +29,11 @@ Choose this package when you compose a deployment that executes model-written pr ### Run a program -Call `resolve(request)` followed by `run(spec)` with a program and binding namespaces. Resolution validates cwd, timeout and sandbox choices and applies provider defaults. `programFormat: PtcProgramFormat` selects an async function body with top-level `await` and `return`, or REPL/Script statements with top-level declarations, `await`, and a final expression. The Node Script provider lowers top-level await and awaits the completion value; its [declaration limitations](../ptc-runtime-node/README.md#known-limitations-and-deferred-work) apply to transformed programs. Lossless-JSON completion becomes `result.value`, text becomes `result.logs`, and failures become `result.error`. +Call `resolve(request)` followed by `run(spec)` with a program and binding namespaces. Resolution validates cwd, timeout and sandbox choices and applies provider defaults. `programFormat: PtcProgramFormat` selects an async function body with top-level `await` and `return`, or REPL/Script statements with top-level declarations, `await`, and a final expression. The Node Script provider lowers top-level await and awaits the completion value; its [declaration limitations](../ptc-runtime-node/README.md#known-limitations-and-deferred-work) apply to transformed programs. Lossless-JSON completion becomes `result.value`, captured text and images become ordered `result.output` entries, and failures become `result.error`. Set `discardCompletion: true` to omit the successful completion without JSON validation or serialization; captured logs and failures are still returned. The TypeScript tool executor selects this option for explicit program output, while workflow consumers retain completion values. -Host bindings can return `new PtcImage(path, attachment)` to expose a stored image in Node programs. Explicit inspection returns the same attachment reference in `result.images`, positioned among logs by `afterLog`. +Host bindings can return `new PtcImage(path, attachment)` to expose a stored image in Node programs. Explicit inspection appends `{ type: 'image', attachment }` entries to `result.output`, alongside `{ type: 'text', text }` entries in emission order. ```text const spec = ctx.ptcRuntime.resolve({ @@ -84,7 +84,7 @@ The exhaustive semantics live in the [PTC runtime subsystem reference](../../../ ### Vocabulary -`PtcRunRequest` carries the program, host bindings, cancellation and optional execution choices. `PtcRunSpec` requires the resolved cwd and an explicit numeric or null deadline choice. `PtcBindingNamespace` declares program globals and optional typed rejection constructors. `PtcRunResult` separates logs/value, failure and `PtcRunSandbox` facts; exact fields and provider obligations live in [`src/types.ts`](src/types.ts). +`PtcRunRequest` carries the program, host bindings, cancellation and optional execution choices. `PtcRunSpec` requires the resolved cwd and an explicit numeric or null deadline choice. `PtcBindingNamespace` declares program globals and optional typed rejection constructors. `PtcRunResult` separates output/value, failure and `PtcRunSandbox` facts; exact fields and provider obligations live in [`src/types.ts`](src/types.ts). ### Portable identifiers @@ -130,7 +130,7 @@ No direct invalidation; the named consumer owns any request-prefix changes. These limits define what the seam cannot do; they are current package constraints, not a task backlog. -- **`run()` is one-shot** — `logs` arrive only on the resolved `PtcRunResult`; the seam exposes no streaming-log or progress API for a live program's output. +- **`run()` is one-shot** — `output` arrives only on the resolved `PtcRunResult`; the seam exposes no streaming-log or progress API for a live program's output. - **Retained memory is disposable**: providers may keep state for an explicit owner; restart, release, and provider-specific failures discard it. Programs, binding calls and results remain logged by consumers; live objects are neither serialized nor replayed. - **Providers have different confinement capabilities** — the shipped Node provider enforces a resolved file policy, while the private experimental Python provider rejects an explicit policy. No container provider is supplied. - **No uniform binding byte cap applies across providers** — each provider owns its transport limits; a binding can still allocate memory before its result reaches those limits. diff --git a/packages/ptc-runtime/ptc-runtime/README.zh.md b/packages/ptc-runtime/ptc-runtime/README.zh.md index f9afc7f489..78f5dc3c8c 100644 --- a/packages/ptc-runtime/ptc-runtime/README.zh.md +++ b/packages/ptc-runtime/ptc-runtime/README.zh.md @@ -29,11 +29,11 @@ kind: "package-reference" ### 运行一个程序 -传入程序和绑定命名空间,依次调用 `resolve(request)` 和 `run(spec)`。解析阶段验证 cwd、超时与沙盒选项,并应用提供方默认值。`programFormat: PtcProgramFormat` 选择支持顶层 `await`、`return` 的异步函数体,或支持顶层声明、`await` 和末尾表达式的 REPL/Script 语句。Node Script 提供方转换顶层 await 并等待完成值;其[声明限制](../ptc-runtime-node/README.zh.md#known-limitations-and-deferred-work)适用于转换后的程序。无损 JSON 完成值进入 `result.value`,文本进入 `result.logs`,失败进入 `result.error`。 +传入程序和绑定命名空间,依次调用 `resolve(request)` 和 `run(spec)`。解析阶段验证 cwd、超时与沙盒选项,并应用提供方默认值。`programFormat: PtcProgramFormat` 选择支持顶层 `await`、`return` 的异步函数体,或支持顶层声明、`await` 和末尾表达式的 REPL/Script 语句。Node Script 提供方转换顶层 await 并等待完成值;其[声明限制](../ptc-runtime-node/README.zh.md#known-limitations-and-deferred-work)适用于转换后的程序。无损 JSON 完成值进入 `result.value`,文本和图片按发出顺序进入 `result.output`,失败进入 `result.error`。 设置 `discardCompletion: true` 可跳过成功完成值的 JSON 验证与序列化,并从结果中省略该值;捕获日志与失败信息仍会返回。TypeScript 工具执行器通过此选项提供显式程序输出,workflow 消费方则继续保留完成值。 -宿主绑定可以返回 `new PtcImage(path, attachment)`,向 Node 程序提供已存储的图片。显式展示后,`result.images` 返回同一附件引用,并通过 `afterLog` 定位在日志条目之间。 +宿主绑定可以返回 `new PtcImage(path, attachment)`,向 Node 程序提供已存储的图片。显式展示会将 `{ type: 'image', attachment }` 条目追加到 `result.output`,与 `{ type: 'text', text }` 条目按发出顺序排列。 ```text const spec = ctx.ptcRuntime.resolve({ @@ -84,7 +84,7 @@ binding-global 与 error-class 名称是语言可移植的:必须匹配 `[A-Za ### 词汇 -`PtcRunRequest` 携带程序、Host 绑定、取消和可选执行选择。`PtcRunSpec` 要求已解析的 cwd 和明确的数值或 null 截止选择。`PtcBindingNamespace` 声明程序全局对象与可选的类型化拒绝构造器。`PtcRunResult` 将日志/值、失败与 `PtcRunSandbox` 事实分开;确切字段与提供方义务见 [`src/types.ts`](src/types.ts)。 +`PtcRunRequest` 携带程序、Host 绑定、取消和可选执行选择。`PtcRunSpec` 要求已解析的 cwd 和明确的数值或 null 截止选择。`PtcBindingNamespace` 声明程序全局对象与可选的类型化拒绝构造器。`PtcRunResult` 将输出/值、失败与 `PtcRunSandbox` 事实分开;确切字段与提供方义务见 [`src/types.ts`](src/types.ts)。 ### 可移植标识符 @@ -130,7 +130,7 @@ binding-global 与 error-class 名称是语言可移植的:必须匹配标识 这些限制说明 seam 不能做什么;它们是当前包约束,不是任务积压。 -- **`run()` 是一次性的**——`logs` 只有在 `PtcRunResult` resolve 后才能获得;seam 不提供正在运行的程序所产生输出的流式日志或进度接口。 +- **`run()` 是一次性的**——`output` 只有在 `PtcRunResult` resolve 后才能获得;seam 不提供正在运行的程序所产生输出的流式日志或进度接口。 - **保留内存可以丢失**:提供方可以为显式所有者保留状态;重启、释放以及提供方指定的失败会清除状态。程序、绑定调用和结果仍由消费者记录;运行中的对象不会被序列化或回放。 - **提供方的约束能力不同**——已发布 Node 提供方强制执行已解析文件策略,私有实验性 Python 提供方拒绝显式策略。不提供容器提供方。 - **提供方之间没有统一的绑定字节上限**——各提供方负责自己的传输限制;绑定仍可能在结果到达这些限制前分配内存。 diff --git a/packages/ptc-runtime/ptc-runtime/src/image.ts b/packages/ptc-runtime/ptc-runtime/src/image.ts index 06c80443a9..251f87a71b 100644 --- a/packages/ptc-runtime/ptc-runtime/src/image.ts +++ b/packages/ptc-runtime/ptc-runtime/src/image.ts @@ -21,11 +21,3 @@ export class PtcImage { Object.freeze(this) } } - -/** One inspected image positioned among captured text entries. */ -export interface PtcImageOutput { - /** Number of text entries emitted before this image. */ - afterLog: number - /** Stored image selected by the program. */ - attachment: ImageAttachmentRef -} diff --git a/packages/ptc-runtime/ptc-runtime/src/index.ts b/packages/ptc-runtime/ptc-runtime/src/index.ts index 855d67a382..009526424c 100644 --- a/packages/ptc-runtime/ptc-runtime/src/index.ts +++ b/packages/ptc-runtime/ptc-runtime/src/index.ts @@ -9,7 +9,6 @@ import type { PtcProgramFormat, PtcRunRequest, PtcRunResult, PtcRunSpec } from ' import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' export { PtcImage } from './image.ts' -export type { PtcImageOutput } from './image.ts' export type { PtcBindingErrorClass, @@ -20,6 +19,7 @@ export type { PtcRunFailure, PtcRunRequest, PtcRunResult, + PtcOutput, PtcRunSpec, PtcRunSandbox, } from './types.ts' diff --git a/packages/ptc-runtime/ptc-runtime/src/types.ts b/packages/ptc-runtime/ptc-runtime/src/types.ts index 331352855a..14371fe07c 100644 --- a/packages/ptc-runtime/ptc-runtime/src/types.ts +++ b/packages/ptc-runtime/ptc-runtime/src/types.ts @@ -6,9 +6,12 @@ * @module @deepseek-ai/dsh-ptc-runtime/src/types */ -import type { PtcImageOutput } from './image.ts' +import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' import type { SandboxEnforcement, SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox' +/** One captured text entry or explicitly inspected image, in emission order. */ +export type PtcOutput = { type: 'text'; text: string } | { type: 'image'; attachment: ImageAttachmentRef } + /** Source evaluation form, independent of retained program state. */ export type PtcProgramFormat = 'function-body' | 'repl' | 'script' @@ -167,13 +170,11 @@ export interface PtcRunResult { */ value?: PtcJsonValue /** - * Captured text. Each source channel preserves emission order; interleaving + * Captured text and inspected images. Each source channel preserves emission order; interleaving * across independent channels is backend-dependent. Bounded only as part of * the outer result. */ - logs: string[] - /** Images explicitly inspected by the program, ordered among captured text entries. */ - images?: PtcImageOutput[] + output: PtcOutput[] /** Present iff the run failed; see {@link PtcRunFailure} for the taxonomy. */ error?: PtcRunFailure } diff --git a/packages/ptc-runtime/ptc-runtime/tests/service.spec.ts b/packages/ptc-runtime/ptc-runtime/tests/service.spec.ts index 57261a3caf..8ba34da893 100644 --- a/packages/ptc-runtime/ptc-runtime/tests/service.spec.ts +++ b/packages/ptc-runtime/ptc-runtime/tests/service.spec.ts @@ -15,12 +15,12 @@ class StubRuntime extends PtcRuntime { readonly language = 'typescript' readonly isolation = 'in-process-stub' requests: PtcRunRequest[] = [] - nextResult: PtcRunResult = { logs: [] } + nextResult: PtcRunResult = { output: [] } async run(request: PtcRunRequest): Promise { this.requests.push(request) if (request.signal?.aborted) { - return { logs: [], error: { kind: 'abort', message: String(request.signal.reason) } } + return { output: [], error: { kind: 'abort', message: String(request.signal.reason) } } } for (const namespace of request.bindings) { for (const fn of Object.values(namespace.functions)) { @@ -55,7 +55,7 @@ describe('PtcRuntime service seam', () => { program: 'return 1', bindings: [{ global: 'tools', functions: { probe: async (args) => { calls.push(args); return null } } }], })) - expect(result).toEqual({ logs: [] }) + expect(result).toEqual({ output: [] }) expect(calls).toEqual([{ from: 'stub' }]) expect(runtime.requests).toHaveLength(1) }) @@ -63,7 +63,7 @@ describe('PtcRuntime service seam', () => { it('reports a failed run as an error field on a resolved result, never a rejection', async () => { const { runtime } = await setup() runtime.nextResult = { - logs: ['boom'], + output: [{ type: 'text', text: 'boom' }], error: { kind: 'exception', message: 'boom' }, } const result = await runtime.run(runtime.resolve({ program: 'throw new Error("boom")', bindings: [] })) diff --git a/packages/shell/tool-bash/tests/integration.spec.ts b/packages/shell/tool-bash/tests/integration.spec.ts index fb4f88097d..d8f108db4b 100644 --- a/packages/shell/tool-bash/tests/integration.spec.ts +++ b/packages/shell/tool-bash/tests/integration.spec.ts @@ -116,7 +116,7 @@ describe('bash tool through the agent loop', () => { if (bash === undefined) throw new Error('missing bash binding') const value = await bash({ command: 'pwd', description: 'Print launch directory' }) expect(value).toMatchObject({ cwd: selected, stdout: { text: `${selected}\n` } }) - return { logs: [], value } + return { output: [], value } } } await ctx.plugin(BashBindingRuntime) diff --git a/packages/skill/skill-office/tests/progressive-loading.spec.ts b/packages/skill/skill-office/tests/progressive-loading.spec.ts index f133f89fef..4037dc6496 100644 --- a/packages/skill/skill-office/tests/progressive-loading.spec.ts +++ b/packages/skill/skill-office/tests/progressive-loading.spec.ts @@ -26,7 +26,7 @@ class SkillProgram extends PtcRuntime { readonly isolation = 'fixture' resolve(request: PtcRunRequest): PtcRunSpec { return { ...request, cwd: process.cwd(), timeoutMs: 120_000 } } async run(request: PtcRunSpec) { - return { logs: [JSON.stringify(await request.bindings[0]!.functions.skill!({ name: request.program }))] } + return { output: [{ type: 'text' as const, text: JSON.stringify(await request.bindings[0]!.functions.skill!({ name: request.program })) }] } } } diff --git a/packages/spill/spill-policy/tests/multimodal.spec.ts b/packages/spill/spill-policy/tests/multimodal.spec.ts index 343d2d8076..e0ccb3fba3 100644 --- a/packages/spill/spill-policy/tests/multimodal.spec.ts +++ b/packages/spill/spill-policy/tests/multimodal.spec.ts @@ -206,7 +206,7 @@ describe('multimodal spill', () => { if (binding === undefined) throw new Error('missing inspect binding') if (failure) await expect(binding({})).rejects.toThrow('capture failed') else expect(await binding({})).toEqual({ content: original }) - return { logs: [], value: 'complete' } + return { output: [], value: 'complete' } } } await ctx.plugin(BindingRuntime) diff --git a/packages/ssh/ssh-helper-runtime/tests/fixtures/driver.ts b/packages/ssh/ssh-helper-runtime/tests/fixtures/driver.ts index 098e87d9f2..193622969e 100644 --- a/packages/ssh/ssh-helper-runtime/tests/fixtures/driver.ts +++ b/packages/ssh/ssh-helper-runtime/tests/fixtures/driver.ts @@ -99,7 +99,7 @@ try { })) assert.equal(result.error, undefined, JSON.stringify(result)) assert.deepEqual(result.value, { value: 'bridge', env: [], executable: hello.executable, cwd: root }) - assert(result.logs.join('').includes('worker-output')) + assert(result.output.filter(block => block.type === 'text').map(block => block.text).join('').includes('worker-output')) assert.equal(await ctx.fs.readText(await ctx.fs.resolve(`${root}/ptc.txt`)), 'embedded') const locked = await runtime.run(runtime.resolve({ program: 'const fs=await import("node:fs"); const {tryLockExclusive}=await import("@deepseek-ai/node-addon-system/flock"); const fd=fs.openSync("native-lock","w"); try { await tryLockExclusive(fd); return "locked" } finally { fs.closeSync(fd) }', diff --git a/packages/subagent/subagent/tests/structured.spec.ts b/packages/subagent/subagent/tests/structured.spec.ts index 60f5f8b19e..75d45805ac 100644 --- a/packages/subagent/subagent/tests/structured.spec.ts +++ b/packages/subagent/subagent/tests/structured.spec.ts @@ -34,7 +34,7 @@ interface PtcRunRequestLike { interface SetupOptions { toolMode?: ('native' | 'ptc') - codeRun?: (request: PtcRunRequestLike) => Promise<{ logs: never[]; value?: unknown }> + codeRun?: (request: PtcRunRequestLike) => Promise<{ output: never[]; value?: unknown }> } const SCHEMA: ObjectJsonSchema = { @@ -57,7 +57,7 @@ async function setup(script: Script, options: SetupOptions = {}) { language: 'typescript', isolation: 'test', resolve: (request: import('@deepseek-ai/dsh-ptc-runtime').PtcRunRequest) => ({ ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: 120_000 }), - run: options.codeRun ?? (() => Promise.resolve({ logs: [] })), + run: options.codeRun ?? (() => Promise.resolve({ output: [] })), } as never) } await mountLocalActivations(ctx) @@ -158,7 +158,7 @@ describe('in-process structured output', () => { const capture = request.bindings.at(0)?.functions[STRUCTURED_OUTPUT_TOOL] if (capture === undefined) throw new Error('structured_output binding missing') await capture({ answer: 42 }) - return { logs: [], value: 'captured' } + return { output: [], value: 'captured' } }, }) const entered = Promise.withResolvers() @@ -597,7 +597,7 @@ describe('in-process structured output', () => { const capture = request.bindings.at(0)?.functions[STRUCTURED_OUTPUT_TOOL] if (!capture) throw new Error('structured_output binding missing') await capture({ answer: 12 }) - return { logs: [], value: 'captured' } + return { output: [], value: 'captured' } }, }) const run = await startTestActivation(ctx, 'spawn', structuredRequest(parent)) @@ -626,7 +626,7 @@ describe('in-process structured output', () => { if (!capture) throw new Error('structured_output binding missing') await capture({ answer: 12 }) return { - logs: [], + output: [], error: { kind: 'runtime', message: 'boom after capture' }, } as never }, @@ -654,7 +654,7 @@ describe('in-process structured output', () => { const capture = request.bindings.at(0)?.functions[STRUCTURED_OUTPUT_TOOL] if (!capture) throw new Error('structured_output binding missing') await capture({ answer: 12 }) - return { logs: [], value: 'captured' } + return { output: [], value: 'captured' } }, }) ctx.on('tools/post-execute', (exec, _result, next) => exec.name === RUN_CODE_NAME diff --git a/packages/test-support/agent-loop-testkit/tests/agent-loop-testkit.spec.ts b/packages/test-support/agent-loop-testkit/tests/agent-loop-testkit.spec.ts index 52af23bf6c..3767994332 100644 --- a/packages/test-support/agent-loop-testkit/tests/agent-loop-testkit.spec.ts +++ b/packages/test-support/agent-loop-testkit/tests/agent-loop-testkit.spec.ts @@ -27,7 +27,7 @@ class FixtureRuntime extends PtcRuntime { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 1_000 } } run(): Promise { - return Promise.resolve({ logs: [] }) + return Promise.resolve({ output: [] }) } } diff --git a/packages/workflow/workflow-ptc/tests/host.spec.ts b/packages/workflow/workflow-ptc/tests/host.spec.ts index 4e1565e9d1..2822e221cd 100644 --- a/packages/workflow/workflow-ptc/tests/host.spec.ts +++ b/packages/workflow/workflow-ptc/tests/host.spec.ts @@ -13,7 +13,7 @@ import { describe, expect, it, onTestFinished, vi } from 'vitest' import PtcWorkflowEngine from '../src/index.ts' import { fakeParent } from './setup.ts' -const completed: PtcRunResult = { logs: [], value: { value: null, stopReason: 'completed', agentsStarted: 0 } } +const completed: PtcRunResult = { output: [], value: { value: null, stopReason: 'completed', agentsStarted: 0 } } type HostBindings = Record class ControlledRuntime extends PtcRuntime { @@ -230,7 +230,7 @@ describe('workflow runtime outcomes', () => { [{ stopReason: 'completed', agentsStarted: 0 }, 'missing its value'], [{ value: null, stopReason: 'error', agentsStarted: 0, error: 5 }, 'error must be a string'], ] as const)('maps an invalid terminal result %j to a workflow error', async (value, message) => { - const { start } = await setup(() => Promise.resolve({ logs: [], value })) + const { start } = await setup(() => Promise.resolve({ output: [], value })) const handle = start() try { const result = await handle.result diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 27b0971d27..e03bb71742 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -1284,8 +1284,8 @@ }, { "doc": "docs/subsystems/ptc-runtime.md", - "symbol": "PtcImageOutput", - "source": "packages/ptc-runtime/ptc-runtime/src/image.ts" + "symbol": "PtcOutput", + "source": "packages/ptc-runtime/ptc-runtime/src/types.ts" }, { "doc": "docs/subsystems/ptc-runtime.md", diff --git a/snapshots/sdk/tool-error-details/tool-schemas.expected.json b/snapshots/sdk/tool-error-details/tool-schemas.expected.json index 7b2b15a67a..99191db532 100644 --- a/snapshots/sdk/tool-error-details/tool-schemas.expected.json +++ b/snapshots/sdk/tool-error-details/tool-schemas.expected.json @@ -643,7 +643,7 @@ [ { "name": "run_code", - "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. Show selected results with inspect(...values). Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/session/advanced-toolchain/tool-schemas.expected.json b/snapshots/session/advanced-toolchain/tool-schemas.expected.json index 05a4484886..1ceea7a6d1 100644 --- a/snapshots/session/advanced-toolchain/tool-schemas.expected.json +++ b/snapshots/session/advanced-toolchain/tool-schemas.expected.json @@ -684,7 +684,7 @@ [ { "name": "run_code", - "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. Show selected results with inspect(...values). Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/session/office-progressive-ptc/tool-schemas.expected.json b/snapshots/session/office-progressive-ptc/tool-schemas.expected.json index d13007b0bf..eadc085907 100644 --- a/snapshots/session/office-progressive-ptc/tool-schemas.expected.json +++ b/snapshots/session/office-progressive-ptc/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "run_code", - "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. Show selected results with inspect(...values). Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { @@ -42,7 +42,7 @@ [ { "name": "run_code", - "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. Show selected results with inspect(...values). Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/session/ptc-console-enabled/tool-schemas.expected.json b/snapshots/session/ptc-console-enabled/tool-schemas.expected.json index 3e8758c9b1..d537f7a968 100644 --- a/snapshots/session/ptc-console-enabled/tool-schemas.expected.json +++ b/snapshots/session/ptc-console-enabled/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "run_code", - "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. Show selected results with inspect(...values). Pass images to inspect to display them. 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. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "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. Show selected text and images with inspect(...values). 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. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/session/ptc-persistent/tool-schemas.expected.json b/snapshots/session/ptc-persistent/tool-schemas.expected.json index c2c8d2deea..29b8c8c68a 100644 --- a/snapshots/session/ptc-persistent/tool-schemas.expected.json +++ b/snapshots/session/ptc-persistent/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "run_code", - "description": "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. Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for output. 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. 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "description": "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for output. 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. 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/session/ptc-reset/tool-schemas.expected.json b/snapshots/session/ptc-reset/tool-schemas.expected.json index 2ad5131f5c..32a943a7e3 100644 --- a/snapshots/session/ptc-reset/tool-schemas.expected.json +++ b/snapshots/session/ptc-reset/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "run_code", - "description": "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. Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for output. The final expression is awaited if it is a Promise. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows the environment's launch-time sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. 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. 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. 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "description": "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for output. The final expression is awaited if it is a Promise. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows the environment's launch-time sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. 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. 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. 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/session/ptc-script-fresh/tool-schemas.expected.json b/snapshots/session/ptc-script-fresh/tool-schemas.expected.json index d630db131b..081e045593 100644 --- a/snapshots/session/ptc-script-fresh/tool-schemas.expected.json +++ b/snapshots/session/ptc-script-fresh/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "run_code", - "description": "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. Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for output. The final expression is awaited if it is a Promise. Each call runs in a fresh Node process. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows this execution's sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process. 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "description": "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for output. The final expression is awaited if it is a Promise. Each call runs in a fresh Node process. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows this execution's sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process. 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/session/ptc-timeout-fallback/tool-schemas.expected.json b/snapshots/session/ptc-timeout-fallback/tool-schemas.expected.json index c01db0d627..6fac1c7350 100644 --- a/snapshots/session/ptc-timeout-fallback/tool-schemas.expected.json +++ b/snapshots/session/ptc-timeout-fallback/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "run_code", - "description": "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. Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for output. The final expression is awaited if it is a Promise. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows the environment's launch-time sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. 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. 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. 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "description": "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for output. The final expression is awaited if it is a Promise. Variables, functions, and imported modules remain in memory for this Agent across calls. Use await import(...) for modules. Relative paths use the supplied working directory. Direct filesystem access follows the environment's launch-time sandbox policy; process.env starts empty. Await asynchronous work: an uncaught callback exception or unhandled rejection terminates the process and clears its variables. 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. 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. 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/session/ptc-turn/tool-schemas.expected.json b/snapshots/session/ptc-turn/tool-schemas.expected.json index b853cc9953..efc975e6ea 100644 --- a/snapshots/session/ptc-turn/tool-schemas.expected.json +++ b/snapshots/session/ptc-turn/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "run_code", - "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. Show selected results with inspect(...values). Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/web/official-subagent-bundle/tool-schemas.expected.json b/snapshots/web/official-subagent-bundle/tool-schemas.expected.json index b853cc9953..efc975e6ea 100644 --- a/snapshots/web/official-subagent-bundle/tool-schemas.expected.json +++ b/snapshots/web/official-subagent-bundle/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "run_code", - "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. Show selected results with inspect(...values). Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/web/optional-string-editor/tool-schemas.expected.json b/snapshots/web/optional-string-editor/tool-schemas.expected.json index b853cc9953..efc975e6ea 100644 --- a/snapshots/web/optional-string-editor/tool-schemas.expected.json +++ b/snapshots/web/optional-string-editor/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "run_code", - "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. Show selected results with inspect(...values). Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/web/ptc-round/tool-schemas.expected.json b/snapshots/web/ptc-round/tool-schemas.expected.json index b853cc9953..efc975e6ea 100644 --- a/snapshots/web/ptc-round/tool-schemas.expected.json +++ b/snapshots/web/ptc-round/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "run_code", - "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. Show selected results with inspect(...values). Pass images to inspect to display them. 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", + "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. Show selected text and images with inspect(...values). 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. Console is unavailable. Use inspect(...values) for 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. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.", "parameters": { "type": "object", "properties": {