fix(ptc): align caller cancellation with native tools
This commit is contained in:
1 parent
aa455f5a40
commit
9e979a71e6
19 files changed
+151
-84
No files matched your search
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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`。
|
||||
@@ -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
|
||||
|
||||
@@ -59,6 +59,8 @@ New sub-calls use `<parent>:ptc:<n>` 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.
|
||||
|
||||
@@ -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) — 配置校验和作用域注册;工具描述和批次执行。
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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' : ''}.`
|
||||
|
||||
@@ -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<undefined>()
|
||||
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<void>((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<AgentCancelCause>([
|
||||
{ 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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
-----
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
|
||||
@@ -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`。
|
||||
|
||||
-----
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
|
||||
@@ -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<string, PtcBindingNamespace>): Promise<PtcRunResult> {
|
||||
|
||||
@@ -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: "<ref *1> { 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<undefined>()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -186,14 +186,9 @@ describe('Node program process', () => {
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
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 () => {
|
||||
|
||||
@@ -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"}}}}
|
||||
@@ -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' } } },
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user