fix(tool-executor-ptc): report blocked resets and queue notices before reuse

This commit is contained in:
_Kerman committed 2026-10-29 21:58:51 +08:00
1 parent 7d20d1c361
commit afde188c6f
12 files changed
+135 -46

No files matched your search

@@ -21,8 +21,8 @@
en: d6d7788495837ef2
zh: 4f7e62d5c2e87022
/deepseek-ai-dsh-tool-executor-ptc/understand-the-implementation/ptc-mode:
en: 44091ca237854496
zh: 86009e91a723ece0
en: 77b321779e07ad12
zh: 0847c71f22454299
/deepseek-ai-dsh-tool-executor-ptc/further-exploration:
en: ee69078c38c655a9
zh: fb0873407f23c8c5
@@ -45,8 +45,8 @@
en: 78906628d634441f
zh: 492f133aea50cfe3
/deepseek-ai-dsh-tool-executor-ptc/known-limitations-and-deferred-work:
en: 5030c17a9c6a2a85
zh: 6334bbe1a1d19cb4
en: 86d79fbb28aced73
zh: aac0ffa1c3ad5e9a
/deepseek-ai-dsh-tool-executor-ptc/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: 3bb0c20c93f16f98
+3 -2
View File
@@ -63,7 +63,7 @@ When the caller cancels and the runtime reports `abort`, `run_code` waits for cl
`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.
Host integrations retain their `PtcExecutor` instance and call `reset(agent)` to clear that Agent's environment; reset is not a model tool or SDK binding. It requires a state-retaining runtime, cancels active calls, invokes `release(agent)`, and waits for process and started nested-tool cleanup without requiring the program to respond. After success, it queues a logged plugin message telling the model that retained variables were cleared; the next program creates a fresh environment. Concurrent resets share cleanup, and new programs wait. Cleanup failure throws `CodeResetFailedError` and blocks programs until an explicit reset retry succeeds. Reset does not undo completed tool effects.
Custom host integrations retain their `PtcExecutor` instance and call `reset(agent)` to clear that Agent's environment; reset is not a model tool or SDK binding. It requires a state-retaining runtime, cancels active calls, invokes `release(agent)`, and waits for process and started nested-tool cleanup without requiring the program to respond. It then queues context about cleared variables before admitting new programs. The notice is logged when consumed; cancellation or Agent disposal can discard it after state is cleared. Concurrent resets share cleanup. Missing or unsupported runtimes reject without blocking admission. Cleanup or notice-admission failure throws `CodeResetFailedError`; later programs report `CodeRunFailedError` with host-reset guidance until an explicit retry succeeds. Reset does not undo completed tool effects.
- [`src/index.ts`](src/index.ts) — Validated configuration and scoped registration; tool descriptions and batch execution.
@@ -120,8 +120,9 @@ Prefix-stable while the PTC mode selection, generated SDK, transport schema, and
<a id="known-limitations-and-deferred-work"></a>
These constraints apply to accepted calls.
These constraints apply to the host integration and accepted calls.
- **No built-in reset control** — the shipped Loader plugin does not expose its executor instance through a service, GUI control or SDK endpoint. Hosts needing reset must mount and retain `PtcExecutor` in their own composition; that integration owns retries after a failed reset.
- **PTC mode's SDK language follows the one loaded runtime, and a presentation is per agent rather than per tool** — PTC execution rejects prompt assembly unless `ctx.ptcRuntime.language` has a registered SDK renderer; within one agent no tool can be native-only while another is ptc-only.
- **PTC mode intermediate values are execution-local and unbounded by bytes** — they cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap.
- **State depends on the runtime**: the default backend starts fresh; the Node REPL and Script mode with `scriptPersistence: true` retain memory per live Agent. Agent disposal awaits release. Restart and execution failures can clear memory; model-visible guidance explains how to recreate missing variables. Programs and results remain in the Session log, without serializing live objects.
+3 -2
View File
@@ -63,7 +63,7 @@ PTC 执行器公开保留的 `run_code` 传输,以及按所加载运行时语
当已挂载运行时支持覆盖时,`run_code` 接受 `timeoutMs`;其 schema 报告配置的默认值和上限以及运行时使用说明。Node 默认值为 120,000 ms,上限为 600,000 ms,包含嵌套工具和审批等待。更宽的 `sandbox_permissions` 模式要求非空 `justification`,并在程序启动前获得审批。schema 提示模型使用当前用户的语言撰写理由。授权仅用于该次完整执行;常驻 Session 策略与嵌套工具保留各自权限。程序不会自动重放:显式重试被拒程序前,应检查先前已发生的效果。 保留状态的 PTC 环境遵循会话文件策略。PTC 观察已提交的 Session 模式变化,并立即取消所有 Node 模式下受影响的调用。后续 PTC 调用等待这些调用的嵌套工具清理后,再解析最新策略。运行时负责进程失效和清理,权限设置保持同步。单次提权省略状态所有者,在临时环境中执行;普通调用仍可访问原环境的变量。
宿主集成保留自己的 `PtcExecutor` 实例,通过 `reset(agent)` 清空该 Agent 的环境;重置不是模型工具或 SDK 绑定。该操作要求运行时保留状态,会取消活动调用、调用 `release(agent)`,并等待进程及已启动嵌套工具的清理,无需程序响应。成功后,它向模型排入一条会写入日志的插件消息,说明保留变量已清空;下次程序调用创建全新环境。并发重置共享清理过程,新程序等待清理。清理失败抛出 `CodeResetFailedError`,并阻止程序执行,直到显式重置重试成功。重置不会撤销已完成的工具操作。
自定义宿主集成保留自己的 `PtcExecutor` 实例,通过 `reset(agent)` 清空该 Agent 的环境;重置不是模型工具或 SDK 绑定。该操作要求运行时保留状态,会取消活动调用、调用 `release(agent)`,并等待进程及已启动嵌套工具的清理,无需程序响应。随后,它先排入变量已清空的上下文,再接纳新程序。通知消费时写入日志;取消或 Agent 销毁可能在状态已清空后丢弃通知。并发重置共享清理过程。运行时缺失或不受支持时,操作拒绝但不阻止后续准入。清理或通知入队失败会抛出 `CodeResetFailedError`;后续程序报告带宿主重置指引的 `CodeRunFailedError`,直到显式重试成功。重置不会撤销已完成的工具操作。
- [`src/index.ts`](src/index.ts) — 配置校验和作用域注册;工具描述和批次执行。
@@ -120,8 +120,9 @@ Program-only SDK bindings:
<a id="known-limitations-and-deferred-work"></a>
以下约束适用于已开始的调用。
以下约束适用于宿主集成及已开始的调用。
- **没有内置重置控件**:自带的 Loader 插件不通过服务、GUI 控件或 SDK 端点暴露执行器实例。需要重置的宿主必须在自己的组合中挂载并保留 `PtcExecutor`,该集成负责失败后的重试。
- **PTC mode 的 SDK 语言由当前加载的运行时决定,且呈现方式按 agent 而非按工具**:PTC 会拒绝组装提示词,除非 `ctx.ptcRuntime.language` 有已注册的 SDK 渲染器;同一个 agent 内不能让一个工具仅使用 Native,而另一个仅使用 PTC。
- **PTC mode 中间值只存在于执行局部,且没有字节上限**:它们无法从会话回放重建,并可能耗尽进程或 worker 内存;只有外层 `run_code` 输出受 worker 可配置的硬上限约束。
- **状态由运行时决定**:默认后端每次重新开始;Node REPL 和启用 `scriptPersistence: true` 的 Script 模式按存活 Agent 保留内存。Agent 销毁时等待环境释放。重启与执行失败可能清除内存;模型说明会指导重新创建缺失变量。程序与结果仍写入 Session 日志,不序列化运行中的对象。
@@ -16,7 +16,7 @@ export class CodeRunFailedError extends HarnessError {
}
}
/** A host reset failed; retained state cannot be trusted until reset succeeds. */
/** A host reset is unsupported or failed; cleanup failures require an explicit retry. */
export class CodeResetFailedError extends HarnessError {
constructor(message: string) {
super(message, 'CODE_RESET_FAILED')
+4 -1
View File
@@ -62,9 +62,12 @@ export class PtcExecutor implements ToolExecutor {
/**
* Clear one Agent's retained environment without executing program code.
* Cancellation or Agent disposal can discard the queued notice after state is cleared.
* @param agent - Exact owner supplied to program executions.
* @returns After process and nested-call cleanup and a queued model-visible state-loss notice.
* @throws CodeResetFailedError if cleanup fails; later programs remain blocked until reset succeeds.
* @throws Error if no supported runtime is available, without changing admission.
* @throws CodeResetFailedError if the runtime does not retain state, without changing admission;
* or if cleanup or notice admission fails, blocking programs until a reset retry succeeds.
*/
reset(agent: Agent): Promise<void> {
return resetPtcEnvironment(this.ctx, this.calls, agent)
@@ -2,6 +2,8 @@
import type { Context } from '@deepseek-ai/cordis'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
import { toolErrorMessage } from '@deepseek-ai/dsh-tools/pipeline'
import { CodeRunFailedError } from './errors.ts'
interface Call {
mode: SandboxMode | undefined
@@ -55,7 +57,9 @@ export class PtcCalls {
for (;;) {
const reset = this.resets.get(sessionId)
if (reset !== undefined) {
await reset.done
try { await reset.done } catch (error) {
throw new CodeRunFailedError(`Program environment is unavailable after a failed host reset: ${toolErrorMessage(error)}. State may not be cleared; ask the host to retry reset before running more code.`)
}
continue
}
const pending = [...this.calls.get(sessionId) ?? []].filter(call => call.signal.aborted)
@@ -83,9 +87,10 @@ export class PtcCalls {
* A failed release blocks new calls until a later reset succeeds.
* @param sessionId - Calling Session.
* @param release - Host operation that terminates and cleans the owned environment.
* @param notify - Queue the state-loss notice after cleanup and before admitting new calls.
* @returns Completion after release and all canceled calls settle; concurrent resets share it.
*/
reset(sessionId: SessionId, release: () => Promise<void>): Promise<void> {
reset(sessionId: SessionId, release: () => Promise<void>, notify?: () => void): Promise<void> {
if (this.disposed) return Promise.reject(new Error('PTC tool executor disposed'))
const previous = this.resets.get(sessionId)
if (previous !== undefined && !previous.settled) return previous.done
@@ -95,13 +100,16 @@ export class PtcCalls {
const calls = [...this.calls.get(sessionId) ?? []]
for (const call of calls) call.controller.abort(new Error('PTC environment reset'))
void Promise.allSettled([Promise.resolve().then(release), ...calls.map(call => call.done)]).then((results) => {
reset.settled = true
const failure = results.find(result => result.status === 'rejected')
if (failure !== undefined) done.reject(failure.reason)
else {
this.resets.delete(sessionId)
done.resolve(undefined)
}
if (failure !== undefined) throw failure.reason
notify?.()
}).then(() => {
reset.settled = true
this.resets.delete(sessionId)
done.resolve(undefined)
}, (error: unknown) => {
reset.settled = true
done.reject(error)
})
return reset.done
}
+7 -6
View File
@@ -8,7 +8,7 @@ import type { PtcCalls } from './ptc-calls.ts'
import { requirePtcRuntime } from './ptc.ts'
/**
* Terminate an owned environment and queue a durable notice for the next model request.
* Terminate an owned environment and queue context for the next model request.
* @param ctx - Executor context containing the runtime provider.
* @param calls - Complete calls whose cancellation and cleanup precede success.
* @param agent - Exact owner of the retained environment.
@@ -18,12 +18,13 @@ export async function resetPtcEnvironment(ctx: Context, calls: PtcCalls, agent:
const runtime = requirePtcRuntime(ctx)
if (!runtime.retainsState) throw new CodeResetFailedError('The PTC runtime does not retain an environment')
try {
await calls.reset(agent.session.id, () => runtime.release(agent))
await calls.reset(agent.session.id, () => runtime.release(agent), () => {
agent.inject(createUserMessage({
content: [{ type: 'text', text: 'The host reset the program environment. All retained variables have been cleared. The next run_code starts fresh. Completed tool effects have not been undone.' }],
source: { kind: 'ptc-mode' },
}))
})
} catch (error) {
throw new CodeResetFailedError(`Program environment reset failed: ${toolErrorMessage(error)}. State may not be cleared; retry the host reset before running more code.`)
}
agent.inject(createUserMessage({
content: [{ type: 'text', text: 'The host reset the program environment. All retained variables have been cleared. The next run_code starts fresh. Completed tool effects have not been undone.' }],
source: { kind: 'ptc-mode' },
}))
}
@@ -4,6 +4,7 @@ import SandboxPolicy, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
import SessionProjections from '@deepseek-ai/dsh-session-projection'
import { expect, it, onTestFinished, vi } from 'vitest'
import { PtcCalls } from '../src/ptc-calls.ts'
import { CodeRunFailedError } from '../src/errors.ts'
async function setup() {
const ctx = new Context()
@@ -96,8 +97,9 @@ it('resets from the host while a call is stuck and blocks admission until releas
let activeSignal: AbortSignal | undefined
const first = calls.run(session.id, signal, () => undefined, async (current) => { activeSignal = current; await cleanup.promise })
const release = vi.fn(async () => { releaseStarted.resolve(undefined); await released.promise })
const reset = calls.reset(session.id, release)
const nextBody = vi.fn(async () => 'fresh')
const notify = vi.fn()
const reset = calls.reset(session.id, release, notify)
const nextBody = vi.fn(async () => { expect(notify).toHaveBeenCalledOnce(); return 'fresh' })
const next = calls.run(session.id, signal, () => undefined, nextBody)
try {
expect(activeSignal?.aborted).toBe(true)
@@ -108,6 +110,7 @@ it('resets from the host while a call is stuck and blocks admission until releas
released.resolve(undefined)
await released.promise
expect(nextBody).not.toHaveBeenCalled()
expect(notify).not.toHaveBeenCalled()
cleanup.resolve(undefined)
await reset
expect(await next).toBe('fresh')
@@ -130,13 +133,24 @@ it('waits for call cleanup after release fails and refuses code until an explici
expect(nextBody).not.toHaveBeenCalled()
cleanup.resolve(undefined)
await Promise.all([first, rejection, nextRejection])
await expect(calls.run(session.id, signal, mode, nextBody)).rejects.toThrow('release failed')
await expect(calls.run(session.id, signal, mode, nextBody)).rejects.toThrow(CodeRunFailedError)
await expect(calls.run(session.id, signal, mode, nextBody)).rejects.toThrow('ask the host to retry reset')
expect(nextBody).not.toHaveBeenCalled()
await calls.reset(session.id, async () => {})
expect(await calls.run(session.id, signal, mode, async () => 'fresh')).toBe('fresh')
} finally { cleanup.resolve(undefined); await Promise.allSettled([first, reset, next]) }
})
it('keeps admission blocked if the reset notice cannot be queued', async () => {
const { session, calls, mode, signal } = await setup()
await expect(calls.reset(session.id, async () => {}, () => { throw new Error('notice failed') })).rejects.toThrow('notice failed')
const next = vi.fn(async () => 'fresh')
await expect(calls.run(session.id, signal, mode, next)).rejects.toThrow('ask the host to retry reset')
expect(next).not.toHaveBeenCalled()
await calls.reset(session.id, async () => {})
expect(await calls.run(session.id, signal, mode, next)).toBe('fresh')
})
it('waits for an idle reset during executor disposal and refuses further resets', async () => {
const { ctx, session, calls } = await setup()
const disposedSignal = Promise.withResolvers<undefined>()
@@ -2448,7 +2448,10 @@ describe('host environment reset', () => {
const run = vi.spyOn(runtime, 'run')
await expect(reset()).rejects.toThrow(CodeResetFailedError)
expect(inject).not.toHaveBeenCalled()
expect((await runCode(ctx, '1', { agent })).isError).toBe(true)
const blocked = await runCode(ctx, '1', { agent })
expect(blocked.isError).toBe(true)
expect(blocked.error?.info?.code).toBe('CODE_RUN_FAILED')
expect(blocked.content.find(block => block.type === 'text')?.text).toContain('ask the host to retry reset')
expect(run).not.toHaveBeenCalled()
await reset()
expect(inject).toHaveBeenCalledOnce()
+15 -2
View File
@@ -1,5 +1,5 @@
/** Exercise host reset and a tool that remains pending until its evaluation ends. */
import { PtcExecutor, inject as executorInject } from '@deepseek-ai/dsh-tool-executor-ptc'
import { CodeResetFailedError, PtcExecutor, inject as executorInject } from '@deepseek-ai/dsh-tool-executor-ptc'
export const name = 'ptc-host-reset-fixture'
export const inject = executorInject
@@ -8,12 +8,25 @@ export const inject = executorInject
export function apply(ctx) {
const executor = new PtcExecutor(ctx)
ctx.tools.setExecutor(executor)
const runtime = ctx.ptcRuntime
const release = runtime.release.bind(runtime)
let failRelease = true
runtime.release = async owner => {
if (failRelease) { failRelease = false; throw new Error('fixture process cleanup failed') }
await release(owner)
}
ctx.effect(() => () => { runtime.release = release })
ctx.on('tools/execute', async (exec, next) => {
if (exec.callId === 'call_keep:ptc:2' && !exec.signal.aborted) {
await new Promise(resolve => exec.signal.addEventListener('abort', resolve, { once: true }))
}
const result = await next()
if (exec.callId === 'call_isolated') await executor.reset(exec.agent)
if (exec.callId === 'call_isolated') {
try { await executor.reset(exec.agent) } catch (error) {
if (!(error instanceof CodeResetFailedError)) throw error
}
}
if (exec.callId === 'call_blocked') await executor.reset(exec.agent)
return result
})
}
@@ -79,6 +79,46 @@
}
]
},
{
"kind": "chunks",
"chunks": [
{
"type": "block-start",
"index": 0,
"blockType": "tool-call"
},
{
"type": "tool-call-delta",
"index": 0,
"id": "call_blocked",
"name": "run_code",
"argumentsDelta": "{\"code\":\"kept.value\",\"description\":\"Check that failed reset blocks code until host recovery\"}"
},
{
"type": "block-end",
"index": 0,
"block": {
"type": "tool-call",
"id": "call_blocked",
"name": "run_code",
"arguments": "{\"code\":\"kept.value\",\"description\":\"Check that failed reset blocks code until host recovery\"}"
}
},
{
"type": "usage",
"usage": {
"inputTokens": 10,
"outputTokens": 5
}
},
{
"type": "finish",
"reason": {
"kind": "tool-calls"
}
}
]
},
{
"kind": "chunks",
"chunks": [
+21 -16
View File
@@ -12,34 +12,39 @@
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"session/title","data":{"title":"Retain a value and write","messageSeqs":[8],"source":{"kind":"fallback"}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_keep","name":"run_code","arguments":"{\"code\":\"var kept = { value: 42 }; var lateEffects = []; await tools.bash({ command: \\\"echo 42 > kept.txt\\\", description: \\\"Write the retained value to a file\\\" }); void tools.bash({ command: \\\"echo should-not-run\\\", description: \\\"Leave a tool pending until the program ends\\\" }).then(() => lateEffects.push(\\\"then\\\")).catch(() => lateEffects.push(\\\"catch\\\")).finally(() => lateEffects.push(\\\"finally\\\")); kept.value\",\"description\":\"Retain an object and leave a tool pending\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1793260283752,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1793260283752,"index":0,"dt":[],"id":"call_keep","name":"run_code","args":["{\"code\":\"var kept = { value: 42 }; var lateEffects = []; await tools.bash({ command: \\\"echo 42 > kept.txt\\\", description: \\\"Write the retained value to a file\\\" }); void tools.bash({ command: \\\"echo should-not-run\\\", description: \\\"Leave a tool pending until the program ends\\\" }).then(() => lateEffects.push(\\\"then\\\")).catch(() => lateEffects.push(\\\"catch\\\")).finally(() => lateEffects.push(\\\"finally\\\")); kept.value\",\"description\":\"Retain an object and leave a tool pending\"}"]},{"type":"chunk","time":1793260283752,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_keep","name":"run_code","arguments":"{\"code\":\"var kept = { value: 42 }; var lateEffects = []; await tools.bash({ command: \\\"echo 42 > kept.txt\\\", description: \\\"Write the retained value to a file\\\" }); void tools.bash({ command: \\\"echo should-not-run\\\", description: \\\"Leave a tool pending until the program ends\\\" }).then(() => lateEffects.push(\\\"then\\\")).catch(() => lateEffects.push(\\\"catch\\\")).finally(() => lateEffects.push(\\\"finally\\\")); kept.value\",\"description\":\"Retain an object and leave a tool pending\"}"}}},{"type":"chunk","time":1793260283752,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1793260283752,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_keep","name":"run_code","arguments":"{\"code\":\"var kept = { value: 42 }; var lateEffects = []; await tools.bash({ command: \\\"echo 42 > kept.txt\\\", description: \\\"Write the retained value to a file\\\" }); void tools.bash({ command: \\\"echo should-not-run\\\", description: \\\"Leave a tool pending until the program ends\\\" }).then(() => lateEffects.push(\\\"then\\\")).catch(() => lateEffects.push(\\\"catch\\\")).finally(() => lateEffects.push(\\\"finally\\\")); kept.value\",\"description\":\"Retain an object and leave a tool pending\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1793281642180,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1793281642180,"index":0,"dt":[],"id":"call_keep","name":"run_code","args":["{\"code\":\"var kept = { value: 42 }; var lateEffects = []; await tools.bash({ command: \\\"echo 42 > kept.txt\\\", description: \\\"Write the retained value to a file\\\" }); void tools.bash({ command: \\\"echo should-not-run\\\", description: \\\"Leave a tool pending until the program ends\\\" }).then(() => lateEffects.push(\\\"then\\\")).catch(() => lateEffects.push(\\\"catch\\\")).finally(() => lateEffects.push(\\\"finally\\\")); kept.value\",\"description\":\"Retain an object and leave a tool pending\"}"]},{"type":"chunk","time":1793281642180,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_keep","name":"run_code","arguments":"{\"code\":\"var kept = { value: 42 }; var lateEffects = []; await tools.bash({ command: \\\"echo 42 > kept.txt\\\", description: \\\"Write the retained value to a file\\\" }); void tools.bash({ command: \\\"echo should-not-run\\\", description: \\\"Leave a tool pending until the program ends\\\" }).then(() => lateEffects.push(\\\"then\\\")).catch(() => lateEffects.push(\\\"catch\\\")).finally(() => lateEffects.push(\\\"finally\\\")); kept.value\",\"description\":\"Retain an object and leave a tool pending\"}"}}},{"type":"chunk","time":1793281642180,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1793281642180,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_keep","name":"run_code","arguments":"{\"code\":\"var kept = { value: 42 }; var lateEffects = []; await tools.bash({ command: \\\"echo 42 > kept.txt\\\", description: \\\"Write the retained value to a file\\\" }); void tools.bash({ command: \\\"echo should-not-run\\\", description: \\\"Leave a tool pending until the program ends\\\" }).then(() => lateEffects.push(\\\"then\\\")).catch(() => lateEffects.push(\\\"catch\\\")).finally(() => lateEffects.push(\\\"finally\\\")); kept.value\",\"description\":\"Retain an object and leave a tool pending\"}"}}
{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_keep","parentCallId":"call_keep","subCallId":"call_keep:ptc:1","name":"bash","arguments":{"command":"echo 42 > kept.txt","description":"Write the retained value to a file"}}}
{"type":"job/registered","data":{"jobId":"{{id:1}}","owner":"{{session:1}}","kind":"bash","label":"echo 42 > kept.txt","startedAt":1793260283926,"toolCall":{"callId":"call_keep:ptc:1","rootCallId":"call_keep"}}}
{"type":"job/settled","data":{"jobId":"{{id:1}}","owner":"{{session:1}}","kind":"bash","label":"echo 42 > kept.txt","startedAt":1793260283926,"toolCall":{"callId":"call_keep:ptc:1","rootCallId":"call_keep"},"finishedAt":1793260283988,"status":"completed","cause":"producer","producerReported":true,"detail":"exit code: 0"}}
{"type":"job/registered","data":{"jobId":"{{id:1}}","owner":"{{session:1}}","kind":"bash","label":"echo 42 > kept.txt","startedAt":1793281642358,"toolCall":{"callId":"call_keep:ptc:1","rootCallId":"call_keep"}}}
{"type":"job/settled","data":{"jobId":"{{id:1}}","owner":"{{session:1}}","kind":"bash","label":"echo 42 > kept.txt","startedAt":1793281642358,"toolCall":{"callId":"call_keep:ptc:1","rootCallId":"call_keep"},"finishedAt":1793281642423,"status":"completed","cause":"producer","producerReported":true,"detail":"exit code: 0"}}
{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_keep","parentCallId":"call_keep","subCallId":"call_keep:ptc:1","name":"bash","arguments":{"command":"echo 42 > kept.txt","description":"Write the retained value to a file"},"isError":false,"content":[{"type":"text","text":"(no output)"}],"meta":{"cwd":"{{cwd}}"}}}
{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_keep","parentCallId":"call_keep","subCallId":"call_keep:ptc:2","name":"bash","arguments":{"command":"echo should-not-run","description":"Leave a tool pending until the program ends"}}}
{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_keep","parentCallId":"call_keep","subCallId":"call_keep:ptc:2","name":"bash","arguments":{"command":"echo should-not-run","description":"Leave a tool pending until the program ends"},"isError":true,"error":{"name":"AbortError","code":"ABORTED_BEFORE_DISPATCH"},"content":[{"type":"text","text":"Error: tool call aborted before dispatch"}]}}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_keep"},"toolCallId":"call_keep","content":[{"type":"text","text":"42"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"step/start","data":{"turn":1,"step":2}}
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_isolated","name":"run_code","arguments":"{\"code\":\"({ retained: kept.value, lateEffects })\",\"description\":\"Inspect retained state after a late tool failure\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1793260284002,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1793260284002,"index":0,"dt":[],"id":"call_isolated","name":"run_code","args":["{\"code\":\"({ retained: kept.value, lateEffects })\",\"description\":\"Inspect retained state after a late tool failure\"}"]},{"type":"chunk","time":1793260284002,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_isolated","name":"run_code","arguments":"{\"code\":\"({ retained: kept.value, lateEffects })\",\"description\":\"Inspect retained state after a late tool failure\"}"}}},{"type":"chunk","time":1793260284002,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1793260284002,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_isolated","name":"run_code","arguments":"{\"code\":\"({ retained: kept.value, lateEffects })\",\"description\":\"Inspect retained state after a late tool failure\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1793281642440,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1793281642441,"index":0,"dt":[],"id":"call_isolated","name":"run_code","args":["{\"code\":\"({ retained: kept.value, lateEffects })\",\"description\":\"Inspect retained state after a late tool failure\"}"]},{"type":"chunk","time":1793281642441,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_isolated","name":"run_code","arguments":"{\"code\":\"({ retained: kept.value, lateEffects })\",\"description\":\"Inspect retained state after a late tool failure\"}"}}},{"type":"chunk","time":1793281642441,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1793281642441,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_isolated","name":"run_code","arguments":"{\"code\":\"({ retained: kept.value, lateEffects })\",\"description\":\"Inspect retained state after a late tool failure\"}"}}
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"The host reset the program environment. All retained variables have been cleared. The next run_code starts fresh. Completed tool effects have not been undone."}],"source":{"kind":"ptc-mode"},"role":"user","id":"{{message:7}}"}]}}
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_isolated"},"toolCallId":"call_isolated","content":[{"type":"text","text":"{\n \"retained\": 42,\n \"lateEffects\": [\n \"catch\",\n \"finally\"\n ]\n}"}],"isError":false,"id":"{{message:8}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_isolated"},"toolCallId":"call_isolated","content":[{"type":"text","text":"{\n \"retained\": 42,\n \"lateEffects\": [\n \"catch\",\n \"finally\"\n ]\n}"}],"isError":false,"id":"{{message:7}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":2}}
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":1,"step":3}}
{"type":"user/message","data":{"content":[{"type":"text","text":"The host reset the program environment. All retained variables have been cleared. The next run_code starts fresh. Completed tool effects have not been undone."}],"source":{"kind":"ptc-mode"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_check","name":"run_code","arguments":"{\"code\":\"({ retained: typeof kept, file: (await tools.bash({ command: \\\"cat kept.txt\\\", description: \\\"Read the saved value after reset\\\" })).stdout.text.trim() })\",\"description\":\"Check cleared variables and completed file effects\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1793260284037,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1793260284037,"index":0,"dt":[],"id":"call_check","name":"run_code","args":["{\"code\":\"({ retained: typeof kept, file: (await tools.bash({ command: \\\"cat kept.txt\\\", description: \\\"Read the saved value after reset\\\" })).stdout.text.trim() })\",\"description\":\"Check cleared variables and completed file effects\"}"]},{"type":"chunk","time":1793260284037,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_check","name":"run_code","arguments":"{\"code\":\"({ retained: typeof kept, file: (await tools.bash({ command: \\\"cat kept.txt\\\", description: \\\"Read the saved value after reset\\\" })).stdout.text.trim() })\",\"description\":\"Check cleared variables and completed file effects\"}"}}},{"type":"chunk","time":1793260284037,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1793260284037,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":3,"callId":"call_check","name":"run_code","arguments":"{\"code\":\"({ retained: typeof kept, file: (await tools.bash({ command: \\\"cat kept.txt\\\", description: \\\"Read the saved value after reset\\\" })).stdout.text.trim() })\",\"description\":\"Check cleared variables and completed file effects\"}"}}
{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_check","parentCallId":"call_check","subCallId":"call_check:ptc:1","name":"bash","arguments":{"command":"cat kept.txt","description":"Read the saved value after reset"}}}
{"type":"job/registered","data":{"jobId":"{{id:2}}","owner":"{{session:1}}","kind":"bash","label":"cat kept.txt","startedAt":1793260284161,"toolCall":{"callId":"call_check:ptc:1","rootCallId":"call_check"}}}
{"type":"job/settled","data":{"jobId":"{{id:2}}","owner":"{{session:1}}","kind":"bash","label":"cat kept.txt","startedAt":1793260284161,"toolCall":{"callId":"call_check:ptc:1","rootCallId":"call_check"},"finishedAt":1793260284215,"status":"completed","cause":"producer","producerReported":true,"detail":"exit code: 0"}}
{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_check","parentCallId":"call_check","subCallId":"call_check:ptc:1","name":"bash","arguments":{"command":"cat kept.txt","description":"Read the saved value after reset"},"isError":false,"content":[{"type":"text","text":"42\n"}],"meta":{"cwd":"{{cwd}}"}}}
{"type":"tool/result","data":{"turn":1,"step":3,"message":{"role":"tool","source":{"kind":"tool","callId":"call_check"},"toolCallId":"call_check","content":[{"type":"text","text":"{\n \"retained\": \"undefined\",\n \"file\": \"42\"\n}"}],"isError":false,"id":"{{message:10}}"}},"sourceEventSeqs":[33],"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_blocked","name":"run_code","arguments":"{\"code\":\"kept.value\",\"description\":\"Check that failed reset blocks code until host recovery\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1793281642459,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1793281642459,"index":0,"dt":[],"id":"call_blocked","name":"run_code","args":["{\"code\":\"kept.value\",\"description\":\"Check that failed reset blocks code until host recovery\"}"]},{"type":"chunk","time":1793281642459,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_blocked","name":"run_code","arguments":"{\"code\":\"kept.value\",\"description\":\"Check that failed reset blocks code until host recovery\"}"}}},{"type":"chunk","time":1793281642459,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1793281642459,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":3,"callId":"call_blocked","name":"run_code","arguments":"{\"code\":\"kept.value\",\"description\":\"Check that failed reset blocks code until host recovery\"}"}}
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"The host reset the program environment. All retained variables have been cleared. The next run_code starts fresh. Completed tool effects have not been undone."}],"source":{"kind":"ptc-mode"},"role":"user","id":"{{message:9}}"}]}}
{"type":"tool/result","data":{"turn":1,"step":3,"message":{"role":"tool","source":{"kind":"tool","callId":"call_blocked"},"toolCallId":"call_blocked","content":[{"type":"text","text":"Error: Program environment is unavailable after a failed host reset: fixture process cleanup failed. State may not be cleared; ask the host to retry reset before running more code."}],"isError":true,"id":"{{message:10}}"},"error":{"name":"CodeRunFailedError","code":"CODE_RUN_FAILED"}},"sourceEventSeqs":[30],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":3}}
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":1,"step":4}}
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"text","text":"The reset cleared the retained variables. The file written before the reset still contains 42."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1793260284228,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1793260284228,"index":0,"dt":[],"texts":["The reset cleared the retained variables. The file written before the reset still contains 42."]},{"type":"chunk","time":1793260284228,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The reset cleared the retained variables. The file written before the reset still contains 42."}}},{"type":"chunk","time":1793260284228,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1793260284228,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"The host reset the program environment. All retained variables have been cleared. The next run_code starts fresh. Completed tool effects have not been undone."}],"source":{"kind":"ptc-mode"},"role":"user","id":"{{message:9}}"},"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_check","name":"run_code","arguments":"{\"code\":\"({ retained: typeof kept, file: (await tools.bash({ command: \\\"cat kept.txt\\\", description: \\\"Read the saved value after reset\\\" })).stdout.text.trim() })\",\"description\":\"Check cleared variables and completed file effects\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1793281642493,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1793281642493,"index":0,"dt":[],"id":"call_check","name":"run_code","args":["{\"code\":\"({ retained: typeof kept, file: (await tools.bash({ command: \\\"cat kept.txt\\\", description: \\\"Read the saved value after reset\\\" })).stdout.text.trim() })\",\"description\":\"Check cleared variables and completed file effects\"}"]},{"type":"chunk","time":1793281642493,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_check","name":"run_code","arguments":"{\"code\":\"({ retained: typeof kept, file: (await tools.bash({ command: \\\"cat kept.txt\\\", description: \\\"Read the saved value after reset\\\" })).stdout.text.trim() })\",\"description\":\"Check cleared variables and completed file effects\"}"}}},{"type":"chunk","time":1793281642493,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1793281642493,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":4,"callId":"call_check","name":"run_code","arguments":"{\"code\":\"({ retained: typeof kept, file: (await tools.bash({ command: \\\"cat kept.txt\\\", description: \\\"Read the saved value after reset\\\" })).stdout.text.trim() })\",\"description\":\"Check cleared variables and completed file effects\"}"}}
{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_check","parentCallId":"call_check","subCallId":"call_check:ptc:1","name":"bash","arguments":{"command":"cat kept.txt","description":"Read the saved value after reset"}}}
{"type":"job/registered","data":{"jobId":"{{id:2}}","owner":"{{session:1}}","kind":"bash","label":"cat kept.txt","startedAt":1793281642627,"toolCall":{"callId":"call_check:ptc:1","rootCallId":"call_check"}}}
{"type":"job/settled","data":{"jobId":"{{id:2}}","owner":"{{session:1}}","kind":"bash","label":"cat kept.txt","startedAt":1793281642627,"toolCall":{"callId":"call_check:ptc:1","rootCallId":"call_check"},"finishedAt":1793281642685,"status":"completed","cause":"producer","producerReported":true,"detail":"exit code: 0"}}
{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_check","parentCallId":"call_check","subCallId":"call_check:ptc:1","name":"bash","arguments":{"command":"cat kept.txt","description":"Read the saved value after reset"},"isError":false,"content":[{"type":"text","text":"42\n"}],"meta":{"cwd":"{{cwd}}"}}}
{"type":"tool/result","data":{"turn":1,"step":4,"message":{"role":"tool","source":{"kind":"tool","callId":"call_check"},"toolCallId":"call_check","content":[{"type":"text","text":"{\n \"retained\": \"undefined\",\n \"file\": \"42\"\n}"}],"isError":false,"id":"{{message:12}}"}},"sourceEventSeqs":[38],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":4}}
{"type":"step/start","data":{"turn":1,"step":5}}
{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"text","text":"The reset cleared the retained variables. The file written before the reset still contains 42."}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:13}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1793281642697,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1793281642697,"index":0,"dt":[],"texts":["The reset cleared the retained variables. The file written before the reset still contains 42."]},{"type":"chunk","time":1793281642697,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The reset cleared the retained variables. The file written before the reset still contains 42."}}},{"type":"chunk","time":1793281642697,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1793281642697,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":5}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}