diff --git a/docs/upgrade-guide/v0.2.1-rc.3/ptc-caller-cancellation/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/ptc-caller-cancellation/guide.i18n.yaml new file mode 100644 index 0000000000..50d54a9371 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/ptc-caller-cancellation/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.3/ptc-caller-cancellation/guide.md +/: + en: 6deb73f8bd3b1b2a + zh: 8bafa6e8c6277923 +/ptc-caller-cancellation-uses-the-standard-tool-error-code: + en: c647ac85178ef0ea + zh: a6e5b76d53bbb6ef +/ptc-caller-cancellation-uses-the-standard-tool-error-code/change: + en: 1a9a455bf55e6b81 + zh: ec925abc44d7684f +/ptc-caller-cancellation-uses-the-standard-tool-error-code/migration: + en: e3bbbebda24059c0 + zh: 926e91f3ce5a8a72 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/ptc-caller-cancellation/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/ptc-caller-cancellation/guide.md new file mode 100644 index 0000000000..a53b061808 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/ptc-caller-cancellation/guide.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "Caller-cancelled run_code results use ABORTED instead of CODE_RUN_FAILED." +--- + +# PTC caller cancellation uses the standard tool error code + +English | [中文](guide.zh.md) + +## Change + +When a running `run_code` call is cancelled by its caller and the runtime reports `abort`, its `tool/result` error changes from `CodeRunFailedError` / `CODE_RUN_FAILED` to `AbortError` / `ABORTED`. The result content is `Error: tool call aborted`, replacing the runtime diagnostic, captured output and sandbox summary. SDK clients and Session consumers that classify these results are affected. + +The Agent continues to record the specific cancellation cause in `turn/end`, including a hook's `reason`. Runtime failures such as timeout and cleanup failure, and runtime-local cancellation without caller cancellation, retain `CODE_RUN_FAILED`. Cancellation still waits for cleanup; it does not roll back completed tool effects, and retained runtime variables may be cleared. + +## Migration + +1. In consumers of new `tool/result` events, classify caller cancellation by `data.error.code === 'ABORTED'`. Read the Agent cancellation cause from the corresponding `turn/end` event's `data.reason.reason` when `data.reason.kind === 'aborted'`. +2. Keep support for `CODE_RUN_FAILED` when reading historical PTC cancellation results. Existing Session logs are not rewritten. +3. Cancel a running `run_code` call and confirm its result has code `ABORTED`, followed by a `turn/end` event containing the original Agent cancellation cause. Confirm ordinary runtime failures still report `CODE_RUN_FAILED`. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/ptc-caller-cancellation/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/ptc-caller-cancellation/guide.zh.md new file mode 100644 index 0000000000..fbd17773ec --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/ptc-caller-cancellation/guide.zh.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "调用方取消的 run_code 结果使用 ABORTED,取代 CODE_RUN_FAILED。" +--- + +# PTC 调用方取消使用标准工具错误码 + +[English](guide.md) | 中文 + +## 变更 + +运行中的 `run_code` 被调用方取消且运行时报告 `abort` 时,其 `tool/result` 错误从 `CodeRunFailedError` / `CODE_RUN_FAILED` 变为 `AbortError` / `ABORTED`。结果内容为 `Error: tool call aborted`,取代运行时诊断、捕获输出和沙箱摘要。按这些结果分类的 SDK 客户端和 Session 消费方受到影响。 + +Agent 继续在 `turn/end` 中记录具体取消原因,包括 hook 的 `reason`。超时、清理失败等运行时失败,以及没有调用方取消的运行时内部取消,保留 `CODE_RUN_FAILED`。取消仍等待清理完成;它不会回滚已完成的工具操作,保留的运行时变量可能被清空。 + +## 迁移 + +1. 在新 `tool/result` 事件的消费方中,通过 `data.error.code === 'ABORTED'` 识别调用方取消。当对应 `turn/end` 事件的 `data.reason.kind === 'aborted'` 时,从 `data.reason.reason` 读取 Agent 取消原因。 +2. 读取历史 PTC 取消结果时,继续支持 `CODE_RUN_FAILED`。已有 Session 日志不会被重写。 +3. 取消运行中的 `run_code`,确认其结果错误码为 `ABORTED`,随后的 `turn/end` 事件包含原始 Agent 取消原因。确认普通运行时失败仍报告 `CODE_RUN_FAILED`。 diff --git a/packages/core/tool-executor-ptc/README.i18n.yaml b/packages/core/tool-executor-ptc/README.i18n.yaml index a056d5ebf3..d99f3ac777 100644 --- a/packages/core/tool-executor-ptc/README.i18n.yaml +++ b/packages/core/tool-executor-ptc/README.i18n.yaml @@ -21,8 +21,8 @@ en: 9d4f61c351945d30 zh: 1c575526230b0e02 /deepseek-ai-dsh-tool-executor-ptc/understand-the-implementation/ptc-mode: - en: a87a9af0deeddabe - zh: a4cef6f43a3b1422 + en: 48f90d4c78ba69d7 + zh: 5dcbbf23ebc1b803 /deepseek-ai-dsh-tool-executor-ptc/further-exploration: en: ee69078c38c655a9 zh: fb0873407f23c8c5 diff --git a/packages/core/tool-executor-ptc/README.md b/packages/core/tool-executor-ptc/README.md index 833a798623..fa20171fa9 100644 --- a/packages/core/tool-executor-ptc/README.md +++ b/packages/core/tool-executor-ptc/README.md @@ -59,6 +59,8 @@ New sub-calls use `:ptc:` ids. Consumers treat these ids as opaque an Successful image-bearing subcall results become deferred user-message context with `source.kind` set to `ptc-mode`. Other additional contexts retain their producing tool's attribution. +When the caller cancels and the runtime reports `abort`, `run_code` waits for cleanup and returns the native tool cancellation result: `AbortError` / `ABORTED`, with `Error: tool call aborted` as its content. The Agent records the specific cancellation cause in `turn/end`, including a hook's reason. Other runtime failures retain `CODE_RUN_FAILED` and their diagnostics, even if the caller also cancels. Cancellation can discard retained runtime state and does not undo completed tool effects. + `run_code` accepts `timeoutMs` when the mounted runtime supports an override; its schema reports the configured default and maximum, and the runtime's usage instructions. The Node default is 120,000 ms with a 600,000 ms cap, including nested tool and approval waits. A wider `sandbox_permissions` mode requires a non-empty `justification` and approval before the program starts. The schema asks the model to write that reason in the current user's language. The grant applies to that complete execution; standing Session policy and nested tools retain their own authority. Programs are never replayed automatically: inspect earlier effects before explicitly retrying a denied program. Retained PTC environments follow the standing file policy. PTC observes committed Session mode changes and immediately cancels affected calls in all Node modes. Later PTC calls wait for their nested-tool cleanup before resolving the latest policy. The runtime owns process invalidation and cleanup; permission setters remain synchronous. One-time escalations omit the state owner and complete in a temporary environment; standing variables remain available to ordinary calls. - [`src/index.ts`](src/index.ts) — Validated configuration and scoped registration; tool descriptions and batch execution. diff --git a/packages/core/tool-executor-ptc/README.zh.md b/packages/core/tool-executor-ptc/README.zh.md index c1b3f6cf20..2f56dbcb12 100644 --- a/packages/core/tool-executor-ptc/README.zh.md +++ b/packages/core/tool-executor-ptc/README.zh.md @@ -59,6 +59,8 @@ PTC 执行器公开保留的 `run_code` 传输以及按所加载运行时语言 成功且包含图片的子调用结果会成为延后的 user-message 上下文,其 `source.kind` 为 `ptc-mode`。其他 additional context 保留其生产工具的归属。 +调用方取消且运行时报告 `abort` 时,`run_code` 等待清理完成,并返回原生工具的取消结果:`AbortError` / `ABORTED`,内容为 `Error: tool call aborted`。Agent 在 `turn/end` 中记录具体取消原因,包括 hook 的理由。其他运行时失败保留 `CODE_RUN_FAILED` 及其诊断,即使调用方同时取消。取消可能丢弃保留的运行时状态,也不会撤销已完成的工具操作。 + 当已挂载运行时支持覆盖时,`run_code` 接受 `timeoutMs`;其 schema 报告配置的默认值和上限以及运行时使用说明。Node 默认值为 120,000 ms,上限为 600,000 ms,包含嵌套工具和审批等待。更宽的 `sandbox_permissions` 模式要求非空 `justification`,并在程序启动前获得审批。schema 提示模型使用当前用户的语言撰写理由。授权仅用于该次完整执行;常驻 Session 策略与嵌套工具保留各自权限。程序不会自动重放:显式重试被拒程序前,应检查先前已发生的效果。 保留状态的 PTC 环境遵循会话文件策略。PTC 观察已提交的 Session 模式变化,并立即取消所有 Node 模式下受影响的调用。后续 PTC 调用等待这些调用的嵌套工具清理后,再解析最新策略。运行时负责进程失效和清理,权限设置保持同步。单次提权省略状态所有者,在临时环境中执行;普通调用仍可访问原环境的变量。 - [`src/index.ts`](src/index.ts) — 配置校验和作用域注册;工具描述和批次执行。 diff --git a/packages/core/tool-executor-ptc/src/errors.ts b/packages/core/tool-executor-ptc/src/errors.ts index 67d468e9d0..c2dcd8ec45 100644 --- a/packages/core/tool-executor-ptc/src/errors.ts +++ b/packages/core/tool-executor-ptc/src/errors.ts @@ -3,7 +3,8 @@ import { HarnessError } from '@deepseek-ai/dsh-llm' /** * Thrown by `run_code` when the program run itself failed — a program - * exception, a budget expiry, an abort, or substrate death. Extends + * exception, a budget expiry, a runtime-local abort, or substrate death. Caller + * cancellation uses the shared `AbortError` / `ABORTED` tool outcome. Extends * {@link HarnessError} (`code: 'CODE_RUN_FAILED'`); the registry's execution * pipeline converts it into a structured `isError` result whose text carries * the failure kind plus the captured logs, so the model can self-correct. diff --git a/packages/core/tool-executor-ptc/src/ptc.ts b/packages/core/tool-executor-ptc/src/ptc.ts index d23a37b471..346837f460 100644 --- a/packages/core/tool-executor-ptc/src/ptc.ts +++ b/packages/core/tool-executor-ptc/src/ptc.ts @@ -9,7 +9,7 @@ import type { PtcDispatchLog } from './runtime-types.ts' import { symbols, type Context } from '@deepseek-ai/cordis' import { brandString } from '@deepseek-ai/dsh-brand' -import { createUserMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, HarnessError } from '@deepseek-ai/dsh-llm' import type { ContentBlock, ToolCallId, ToolSchema } from '@deepseek-ai/dsh-llm' import type { PtcBindingFunction, PtcRunResult, PtcRunSandbox, PtcRuntime } from '@deepseek-ai/dsh-ptc-runtime' @@ -19,7 +19,7 @@ import type {} from '@deepseek-ai/dsh-user-approval' import type {} from '@deepseek-ai/dsh-working-directory' import type {} from '@deepseek-ai/dsh-sandbox-policy' import { deepFreeze, snapshotJsonValue, type JsonValue } from '@deepseek-ai/dsh-util-values' -import { defineTool, parameterSchemaSpecToJsonSchema, RUN_CODE_NAME } from '@deepseek-ai/dsh-tools' +import { defineTool, parameterSchemaSpecToJsonSchema, RUN_CODE_NAME, TOOL_ABORTED } from '@deepseek-ai/dsh-tools' import type { ToolExecutionPipeline } from '@deepseek-ai/dsh-tools/pipeline' import { toolErrorMessage } from '@deepseek-ai/dsh-tools/pipeline' import { scopeTarget } from '@deepseek-ai/dsh-scope' @@ -694,6 +694,11 @@ export function createRunCodeTool( } if (result.error) { + if (result.error.kind === 'abort' && exec.signal.aborted) { + const error = new HarnessError('tool call aborted', TOOL_ABORTED) + error.name = 'AbortError' + throw error + } const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.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' : ''}.` diff --git a/packages/core/tool-executor-ptc/tests/ptc.spec.ts b/packages/core/tool-executor-ptc/tests/ptc.spec.ts index 942434e44b..d0c3a400af 100644 --- a/packages/core/tool-executor-ptc/tests/ptc.spec.ts +++ b/packages/core/tool-executor-ptc/tests/ptc.spec.ts @@ -14,11 +14,11 @@ import { createScope } from '@deepseek-ai/dsh-scope' import type { Scope } from '@deepseek-ai/dsh-scope' import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt' import { PtcRuntime } from '@deepseek-ai/dsh-ptc-runtime' -import type { PtcRunRequest, PtcRunResult } from '@deepseek-ai/dsh-ptc-runtime' -import ToolRuntime, { RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DISPATCH, defineTool } from '@deepseek-ai/dsh-tools' +import type { PtcRunFailure, PtcRunRequest, PtcRunResult } from '@deepseek-ai/dsh-ptc-runtime' +import ToolRuntime, { RUN_CODE_NAME, TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH, defineTool } from '@deepseek-ai/dsh-tools' import { defineContentToolFixture } from '@deepseek-ai/dsh-tool-testkit' import type { JsonSchemaNode, PostToolDecision, ToolExecutionResult } from '@deepseek-ai/dsh-tools' -import type { Agent } from '@deepseek-ai/dsh-agent' +import type { Agent, AgentCancelCause } from '@deepseek-ai/dsh-agent' import SessionStore, { Session, SessionId, SessionSeq } from '@deepseek-ai/dsh-session' import ApprovalService, { type ApprovalOutcome, type ApprovalRequest } from '@deepseek-ai/dsh-user-approval' import type { SessionEventMap } from '@deepseek-ai/dsh-session' @@ -1602,7 +1602,9 @@ describe('the run_code dispatch bridge', () => { it('aborting the outer signal aborts the in-flight sub-dispatch and abandons queued ones', async () => { const { ctx, runtime } = await setup({ mode: 'ptc' }) + onTestFinished(async () => { await ctx.fiber.dispose() }) const seen: string[] = [] + const entered = Promise.withResolvers() let sawAbort = false ctx.tools.register(defineContentToolFixture({ name: 'slow', @@ -1611,8 +1613,8 @@ describe('the run_code dispatch bridge', () => { async execute(args, exec) { seen.push(args.id) await new Promise((resolve) => { - const timer = setTimeout(resolve, 500) - exec.signal.addEventListener('abort', () => { sawAbort = true; clearTimeout(timer); resolve() }, { once: true }) + exec.signal.addEventListener('abort', () => { sawAbort = true; resolve() }, { once: true }) + entered.resolve(undefined) }) return [{ type: 'text' as const, text: args.id }] }, @@ -1621,7 +1623,8 @@ describe('the run_code dispatch bridge', () => { runtime.behavior = async (request) => { const tools = request.bindings[0]!.functions const calls = [tools.slow!({ id: 'first' }).catch(() => 'rejected'), tools.slow!({ id: 'second' }).catch(() => 'rejected')] - setTimeout(() => { controller.abort('user-cancel') }, 50) + await entered.promise + controller.abort('user-cancel') 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. @@ -1629,7 +1632,7 @@ describe('the run_code dispatch bridge', () => { } const result = await runCode(ctx, 'program', { signal: controller.signal }) expect(result.isError).toBe(true) - expect((result.content[0] as { text: string }).text).toContain('code run failed (abort)') + expect(result.error).toEqual({ message: 'tool call aborted', info: { name: 'AbortError', code: TOOL_ABORTED } }) expect(seen).toEqual(['first']) expect(sawAbort).toBe(true) }) @@ -2045,6 +2048,54 @@ describe('the run_code dispatch bridge', () => { expect(calls).toEqual([]) }) + it.each([ + { kind: 'user' }, + { kind: 'parent' }, + { kind: 'hook', reason: 'policy stopped the turn' }, + { kind: 'disposed' }, + ])('maps a $kind caller cancellation to the native tool abort result', async (cause) => { + const { ctx, runtime } = await setup({ mode: 'ptc' }) + onTestFinished(async () => { await ctx.fiber.dispose() }) + const controller = new AbortController() + runtime.behavior = async (request) => { + controller.abort(cause) + expect(request.signal?.reason).toBe(cause) + return { logs: [], error: { kind: 'abort', message: '[object Object]' } } + } + + const result = await runCode(ctx, 'program', { signal: controller.signal }) + + expect(result).toEqual({ + content: [{ type: 'text', text: 'Error: tool call aborted' }], + isError: true, + error: { + message: 'tool call aborted', + info: { name: 'AbortError', code: TOOL_ABORTED }, + }, + }) + }) + + it.each<[boolean, PtcRunFailure]>([ + [false, { kind: 'abort', message: 'sandbox mode changed' }], + [true, { kind: 'worker-exit', message: 'managed process cleanup failed' }], + [true, { kind: 'timeout', message: 'execution deadline reached' }], + ])('keeps runtime failure details with caller cancellation %s: %j', async (cancelled, failure) => { + const { ctx, runtime } = await setup({ mode: 'ptc' }) + onTestFinished(async () => { await ctx.fiber.dispose() }) + const controller = new AbortController() + runtime.behavior = async () => { + if (cancelled) controller.abort({ kind: 'user' }) + return { logs: ['got this far'], error: failure } + } + + const result = await runCode(ctx, 'program', { signal: controller.signal }) + + expect(result.error).toEqual({ + message: `code run failed (${failure.kind}): ${failure.message}\nCaptured output:\ngot this far`, + info: { name: 'CodeRunFailedError', code: 'CODE_RUN_FAILED' }, + }) + }) + it('a tool/ptc-dispatch event never derives a model message', () => { const session = Session.create(SessionId('ptc-derive')) session.append('user/message', createUserMessage({ diff --git a/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml b/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml index d9d40d1475..1ecdc1933e 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml +++ b/packages/ptc-runtime/ptc-runtime-node/README.i18n.yaml @@ -30,8 +30,8 @@ en: 3e7ab293fac35688 zh: 617ed4930bbef6c7 /deepseek-ai-dsh-ptc-runtime-node/use-this-package/failures: - en: d4d3294f357b4afc - zh: 69992be84c94cd4a + en: 34a221d0f26b8e47 + zh: 2fd20ac9c9953c7f /deepseek-ai-dsh-ptc-runtime-node/understand-the-implementation: en: 50e6cd38e298da31 zh: c8b252b3acf59147 diff --git a/packages/ptc-runtime/ptc-runtime-node/README.md b/packages/ptc-runtime/ptc-runtime-node/README.md index 012e1d3f75..a7fe249acf 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.md +++ b/packages/ptc-runtime/ptc-runtime-node/README.md @@ -109,8 +109,6 @@ Omitting `timeoutMs` uses the configured elapsed default; numeric requests are v Program parse errors and thrown exceptions are `exception`; deadline expiry is `timeout`; cancellation is `abort`; unparsable or oversized control traffic and invalid program messages are `protocol`; unavailable confinement is `sandbox-unavailable`; early process exit, lost control transport or failed managed cleanup is `worker-exit`. Control I/O failures retain the transport diagnostic alongside the observed process exit status. The substrate-independent failure name remains `worker-exit` for process providers. Lossy completions are `invalid-output`, and an oversized outer result is `output-limit`, retaining the fitting log prefix. Invalid or unsupported options and calls after disposal reject as caller misuse. -Host failure diagnostics keep strings and `Error` messages unchanged and use Node inspection for other values, including objects with circular references. Cancellation diagnostics retain structured cause fields such as `kind` and a hook's `reason`; retained environments append `; retained variables were cleared`. - ----- diff --git a/packages/ptc-runtime/ptc-runtime-node/README.zh.md b/packages/ptc-runtime/ptc-runtime-node/README.zh.md index d936d9d33f..7a2503ce1f 100644 --- a/packages/ptc-runtime/ptc-runtime-node/README.zh.md +++ b/packages/ptc-runtime/ptc-runtime-node/README.zh.md @@ -109,8 +109,6 @@ PTC 消费方按 [dsh-tool-executor-ptc](../../core/tool-executor-ptc/README.zh. 程序解析错误与抛出异常为 `exception`;截止到期为 `timeout`;取消为 `abort`;无法解析或超量的控制通信以及无效的程序消息为 `protocol`;约束不可用为 `sandbox-unavailable`;进程提前退出、控制通道丢失或受管清理失败为 `worker-exit`。控制通道 I/O 失败时,在观测到的进程退出状态之外保留通道诊断原因。进程提供方保留与执行基底无关的失败名 `worker-exit`。有损完成值为 `invalid-output`,外层结果超限为 `output-limit`,并保留能容纳的日志前缀。无效或不支持的选项,以及资源释放后的调用,以调用方误用拒绝。 -Host 失败诊断保留原始字符串与 `Error` 消息,并用 Node 检查格式化其他值,包括含有循环引用的对象。取消诊断保留结构化原因的字段,如 `kind` 与 hook 的 `reason`;保留状态的环境会追加 `; retained variables were cleared`。 - ----- diff --git a/packages/ptc-runtime/ptc-runtime-node/src/worker.ts b/packages/ptc-runtime/ptc-runtime-node/src/worker.ts index 2806de8b24..ff5307c320 100644 --- a/packages/ptc-runtime/ptc-runtime-node/src/worker.ts +++ b/packages/ptc-runtime/ptc-runtime-node/src/worker.ts @@ -1,6 +1,5 @@ /** Managed Node process shared by temporary and owner-retained evaluations. */ import { AsyncLocalStorage } from 'node:async_hooks' -import { inspect } from 'node:util' import { Context } from '@deepseek-ai/cordis' import type { PtcRunSpec, PtcRunResult, PtcRunSandbox, PtcRunFailure, PtcBindingNamespace, PtcJsonValue } from '@deepseek-ai/dsh-ptc-runtime' import { SandboxUnavailableError, classifyRunnerFailure, isRunnerSpawnFailure } from '@deepseek-ai/dsh-sandbox' @@ -19,8 +18,7 @@ import { OutputLedger } from './output-ledger.ts' import { drainOutput, terminateProcess } from './output-stream.ts' import { readProgramResult, type ProgramBootData, type ProgramMethods, type HostMethods } from './protocol.ts' -const messageOf = (error: unknown): string => error instanceof Error ? error.message - : typeof error === 'string' ? error : inspect(error, { customInspect: false }) +const messageOf = (error: unknown): string => error instanceof Error ? error.message : String(error) interface Evaluation { settled: boolean @@ -109,7 +107,7 @@ export class ProgramWorker { } private abortMessage(signal: AbortSignal | undefined): string { - return this.message(messageOf(signal?.reason)) + return this.retainsState ? 'execution canceled; retained variables were cleared' : messageOf(signal?.reason) } private async evaluate(spec: PtcRunSpec, bindings: Map): Promise { 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 929ff480b9..5a5aad761e 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 @@ -129,40 +129,6 @@ describe('Node runtime host failures', () => { expect(h.resolveExecutable).not.toHaveBeenCalled() }) - it.each<[unknown, string]>([ - [{ kind: 'user' }, "{ kind: 'user' }"], - [{ kind: 'parent' }, "{ kind: 'parent' }"], - [{ kind: 'hook', reason: 'policy stopped the turn' }, "{ kind: 'hook', reason: 'policy stopped the turn' }"], - [{ kind: 'disposed' }, "{ kind: 'disposed' }"], - [null, 'null'], - [new Error('stop requested'), 'stop requested'], - ['stop requested', 'stop requested'], - ])('renders pre-start cancellation %j as %s', async (reason, message) => { - const h = await setup() - const result = await h.start({ ...request, signal: AbortSignal.abort(reason) }, NO_INITIAL_FRAME) - expect(result.error).toEqual({ kind: 'abort', message }) - expect(h.resolveExecutable).not.toHaveBeenCalled() - }) - - it('keeps structured startup failure details', async () => { - const h = await setup() - h.resolveExecutable.mockRejectedValue({ code: 'EXECUTABLE_UNAVAILABLE', executable: 'node' }) - const result = await h.start() - expect(result.error).toEqual({ - kind: 'worker-exit', message: "{ code: 'EXECUTABLE_UNAVAILABLE', executable: 'node' }", - }) - expect(h.spawn).not.toHaveBeenCalled() - }) - - it('renders a circular cancellation reason without interrupting settlement', async () => { - const h = await setup() - const reason: { kind: string; cause?: object } = { kind: 'hook' } - reason.cause = reason - const result = await h.start({ ...request, signal: AbortSignal.abort(reason) }, NO_INITIAL_FRAME) - expect(result.error).toEqual({ kind: 'abort', message: " { kind: 'hook', cause: [Circular *1] }" }) - expect(h.resolveExecutable).not.toHaveBeenCalled() - }) - it('keeps a null-deadline run active past the numeric ceiling until cancellation', async () => { const h = await setup({ timeoutMs: 20, maxTimeoutMs: 40 }) const booted = Promise.withResolvers() 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 39b6508d4d..26298852a6 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 @@ -316,11 +316,11 @@ describe('persistent PTC host protocol', () => { h.direct.resolve({ exitCode: 9, signal: null }) await Promise.resolve() const controller = new AbortController() - if (outcome === 'abort') controller.abort({ kind: 'parent' }) + if (outcome === 'abort') controller.abort() if (outcome === 'cleanup') h.waitForExit.mockRejectedValue(new Error('exit cleanup failed')) const result = await h.runtime.run(h.runtime.resolve({ ...request, program: '2', signal: controller.signal })) expect(result.error?.kind).toBe(outcome === 'abort' ? 'abort' : 'worker-exit') - expect(result.error?.message).toContain(outcome === 'abort' ? "{ kind: 'parent' }; retained variables were cleared" : outcome === 'cleanup' ? 'exit cleanup failed' : 'exited (9, signal null)') + expect(result.error?.message).toContain(outcome === 'abort' ? 'execution canceled' : outcome === 'cleanup' ? 'exit cleanup failed' : 'exited (9, signal null)') expect(h.messages).toHaveLength(1) expect(h.messages[0]).toMatchObject({ t: 'q', m: 'run' }) expect(h.spawn).toHaveBeenCalledTimes(1) 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 428b162a61..67cdcc7c8a 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/persistent.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/persistent.spec.ts @@ -207,18 +207,10 @@ describe.each([ signal: controller.signal, bindings: bindings(async () => { entered.resolve(true); return null }), }) - try { - await entered.promise - controller.abort({ kind: 'hook', reason: 'policy stopped the turn' }) - expect((await pending).error).toEqual({ - kind: 'abort', - message: "{ kind: 'hook', reason: 'policy stopped the turn' }; retained variables were cleared", - }) - expect((await run('typeof app')).value).toBe('undefined') - } finally { - controller.abort('test cleanup') - await pending - } + await entered.promise + controller.abort() + expect((await pending).error?.kind).toBe('abort') + expect((await run('typeof app')).value).toBe('undefined') }) it('resets a timed-out environment and does not reuse an exited process', async () => { 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 6c70755fea..03688e1ae4 100644 --- a/packages/ptc-runtime/ptc-runtime-node/tests/runtime.spec.ts +++ b/packages/ptc-runtime/ptc-runtime-node/tests/runtime.spec.ts @@ -186,14 +186,9 @@ describe('Node program process', () => { const entered = Promise.withResolvers() const controller = new AbortController() const active = run({ ...timing, program: 'void tools.enter({}); for (;;) {}', signal: controller.signal, bindings: bindings({ enter: async () => { entered.resolve(undefined); return null } }) }) - try { - await entered.promise - controller.abort({ kind: 'user' }) - expect((await active).error).toEqual({ kind: 'abort', message: "{ kind: 'user' }" }) - } finally { - controller.abort('test cleanup') - await active - } + await entered.promise + controller.abort('stop') + expect((await active).error).toEqual({ kind: 'abort', message: 'stop' }) }) it('closes a still-running descendant before returning the program result', async () => { diff --git a/snapshots/sdk/ptc-cancel/session.v4.jsonl b/snapshots/sdk/ptc-cancel/session.v4.jsonl index e143054de1..f7826ecbbb 100644 --- a/snapshots/sdk/ptc-cancel/session.v4.jsonl +++ b/snapshots/sdk/ptc-cancel/session.v4.jsonl @@ -16,6 +16,6 @@ {"type":"tool/call","data":{"turn":1,"step":1,"callId":"ptc-cancel","name":"run_code","arguments":"{\"code\":\"await tools.working_directory({}); for (;;) {}\",\"description\":\"Run until stopped\"}"}} {"type":"tool/ptc-dispatch-start","data":{"rootCallId":"ptc-cancel","parentCallId":"ptc-cancel","subCallId":"ptc-cancel:ptc:1","name":"working_directory","arguments":{}}} {"type":"tool/ptc-dispatch","data":{"rootCallId":"ptc-cancel","parentCallId":"ptc-cancel","subCallId":"ptc-cancel:ptc:1","name":"working_directory","arguments":{},"isError":false,"content":[{"type":"text","text":"{{cwd}}"}]}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"ptc-cancel"},"toolCallId":"ptc-cancel","content":[{"type":"text","text":"Error: code run failed (abort): { kind: 'user' }\nFile sandbox: danger-full-access."}],"isError":true,"id":"{{message:5}}"},"error":{"name":"CodeRunFailedError","code":"CODE_RUN_FAILED"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"ptc-cancel"},"toolCallId":"ptc-cancel","content":[{"type":"text","text":"Error: tool call aborted"}],"isError":true,"id":"{{message:5}}"},"error":{"name":"AbortError","code":"ABORTED"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"aborted","reason":{"kind":"user"}}}} diff --git a/snapshots/sdk/sdk.snapshot.ts b/snapshots/sdk/sdk.snapshot.ts index 3bd8b0965b..1a687411fc 100644 --- a/snapshots/sdk/sdk.snapshot.ts +++ b/snapshots/sdk/sdk.snapshot.ts @@ -1015,11 +1015,14 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { const actualContext = contextOf(ordered, cwd) if (scenario.name === 'ptc-cancel') { const events = results.flatMap(result => result.events) - expect(events.find(event => event.type === 'tool/result')).toMatchObject({ data: { message: { - toolCallId: 'ptc-cancel', - isError: true, - content: [{ type: 'text', text: "Error: code run failed (abort): { kind: 'user' }\nFile sandbox: danger-full-access." }], - } } }) + expect(events.find(event => event.type === 'tool/result')).toMatchObject({ data: { + message: { + toolCallId: 'ptc-cancel', + isError: true, + content: [{ type: 'text', text: 'Error: tool call aborted' }], + }, + error: { name: 'AbortError', code: 'ABORTED' }, + } }) expect(events.find(event => event.type === 'turn/end')).toMatchObject({ data: { reason: { kind: 'aborted', reason: { kind: 'user' } } }, })