Compare commits
166
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8f7b93a34 | ||
|
|
4e205cd66d | ||
|
|
4861a5022a | ||
|
|
ce6ab42635 | ||
|
|
17d96e0f6d | ||
|
|
b8aed3002d | ||
|
|
176d69bd21 | ||
|
|
a18dbe9138 | ||
|
|
01d868ad1e | ||
|
|
61576d77c6 | ||
|
|
7e3161e461 | ||
|
|
62894e715d | ||
|
|
36496e1495 | ||
|
|
f5ceca4b1d | ||
|
|
1374429891 | ||
|
|
d0d24d7779 | ||
|
|
80510f7543 | ||
|
|
91f70db6a6 | ||
|
|
abb3c004d5 | ||
|
|
79d1dd8915 | ||
|
|
73303a3a01 | ||
|
|
2deb3189c6 | ||
|
|
37e0924f68 | ||
|
|
0874eb2217 | ||
|
|
f9fdb61ca9 | ||
|
|
5ce7c5b1f3 | ||
|
|
dd23e7ae91 | ||
|
|
c30a94a0c4 | ||
|
|
616dbd03bd | ||
|
|
3b8c672e6f | ||
|
|
1a62850eec | ||
|
|
58fa5606b7 | ||
|
|
c6ef2d17da | ||
|
|
561f7f1538 | ||
|
|
85120aaf4a | ||
|
|
1cf3217403 | ||
|
|
26cd436b86 | ||
|
|
5a398a9990 | ||
|
|
ba4d01126c | ||
|
|
793ce790a6 | ||
|
|
b89adf637d | ||
|
|
1aefd4de11 | ||
|
|
1991fa2b22 | ||
|
|
fa7fb44c9a | ||
|
|
badd971c14 | ||
|
|
53c51d8590 | ||
|
|
ffab25266c | ||
|
|
82c5faca9a | ||
|
|
ac84370afc | ||
|
|
d5ebaae1b9 | ||
|
|
77c722d9e3 | ||
|
|
6e68211a28 | ||
|
|
864cc97c99 | ||
|
|
9b0db4b152 | ||
|
|
9b28e62be3 | ||
|
|
a4e6f7ceea | ||
|
|
09559c405f | ||
|
|
a5d0863db3 | ||
|
|
f775b74795 | ||
|
|
5de18e6090 | ||
|
|
35fd76078f | ||
|
|
43dea0aa7a | ||
|
|
55ad0f220b | ||
|
|
947503cb2c | ||
|
|
5ee1c02eb8 | ||
|
|
418f2ec405 | ||
|
|
7526051e60 | ||
|
|
51951b1593 | ||
|
|
4e9adccec7 | ||
|
|
c7f1da5531 | ||
|
|
47fafbbfcb | ||
|
|
5fd443618f | ||
|
|
d79851e05d | ||
|
|
12cec9d6cf | ||
|
|
a3316f7cc0 | ||
|
|
b4e3e18867 | ||
|
|
9eabcd03f9 | ||
|
|
0a398c2968 | ||
|
|
01da025060 | ||
|
|
86984d8358 | ||
|
|
c6116ecdb4 | ||
|
|
0191dbb0cf | ||
|
|
d4785e69c0 | ||
|
|
c2aa91c1ab | ||
|
|
1ac170db78 | ||
|
|
5440d154f8 | ||
|
|
dad69a4636 | ||
|
|
e335c7c8cf | ||
|
|
93ecacdb42 | ||
|
|
c205404fbb | ||
|
|
5d0106771e | ||
|
|
5dbbf3a8cd | ||
|
|
7bba8aa868 | ||
|
|
cfc366a869 | ||
|
|
7d57aa02c9 | ||
|
|
9e61299fdb | ||
|
|
d602624eb3 | ||
|
|
4c55f03a57 | ||
|
|
2bedbf2f2b | ||
|
|
6729530215 | ||
|
|
b877eb6118 | ||
|
|
8a898cac60 | ||
|
|
26427a26e1 | ||
|
|
9b3119f1f9 | ||
|
|
5bcee95efe | ||
|
|
8566fdb439 | ||
|
|
fc251c7b4a | ||
|
|
4e976ee9a9 | ||
|
|
dc240209e3 | ||
|
|
69528e4e88 | ||
|
|
069ba921dc | ||
|
|
f51d105817 | ||
|
|
5f7b65d2a6 | ||
|
|
52f610de0f | ||
|
|
9f8fd9d800 | ||
|
|
ac568aa2d2 | ||
|
|
5dbc29e64e | ||
|
|
20fbd860f8 | ||
|
|
63705dd46c | ||
|
|
e4a2972627 | ||
|
|
f0e9dc1c92 | ||
|
|
e1f0a36193 | ||
|
|
a03cf79fc7 | ||
|
|
4510e1dc57 | ||
|
|
a2ea3970fd | ||
|
|
4f6ccd772d | ||
|
|
1fed265b3b | ||
|
|
1df83be8f6 | ||
|
|
83e3d0ebc0 | ||
|
|
b98e3d070b | ||
|
|
3dbe1c126f | ||
|
|
b9966a12a6 | ||
|
|
4e72e43037 | ||
|
|
724e56b286 | ||
|
|
a0f326da1d | ||
|
|
a7ca24f16e | ||
|
|
571e0e9f48 | ||
|
|
20a51d8806 | ||
|
|
0c2d8cb5b0 | ||
|
|
78b7b27089 | ||
|
|
2a19020b37 | ||
|
|
b7a3fba945 | ||
|
|
790991a6c0 | ||
|
|
4f478969f5 | ||
|
|
a2c69f36f7 | ||
|
|
e688c61aab | ||
|
|
71b5e93457 | ||
|
|
0e12a87b02 | ||
|
|
c1783e9dff | ||
|
|
bf5406f53b | ||
|
|
c47a667a9c | ||
|
|
4eb64b67b1 | ||
|
|
3d80f2774d | ||
|
|
3d5794a545 | ||
|
|
e9f96c2aa8 | ||
|
|
05f557bbf6 | ||
|
|
f8f57aa8e0 | ||
|
|
f50feeef9f | ||
|
|
5e75c3009f | ||
|
|
e5ba59e751 | ||
|
|
32209999ca | ||
|
|
662fb47ff2 | ||
|
|
6229d26d77 | ||
|
|
b42e744c3c | ||
|
|
8a5ebd2351 | ||
|
|
50577597e9 |
No files matched your search
+4
-4
@@ -9,11 +9,11 @@
|
||||
en: a22ddcfb339e503d
|
||||
zh: a1f4fadb46fce1e7
|
||||
/agent-note-native-cua-objects-independent-of-evaluator-ownership/decision:
|
||||
en: 176a9c2f6040f2a1
|
||||
zh: 2617ec13d25de998
|
||||
en: d08345b0b22b0884
|
||||
zh: 18a843df211bcabc
|
||||
/agent-note-native-cua-objects-independent-of-evaluator-ownership/alternatives-considered:
|
||||
en: 3d0bc77e3c432cb9
|
||||
zh: 50a20eda1811f16c
|
||||
en: f2bb7165b7c168a0
|
||||
zh: e6234e3464d971ee
|
||||
/agent-note-native-cua-objects-independent-of-evaluator-ownership/consequences:
|
||||
en: 528c644d5810e60b
|
||||
zh: 9649dfc3b1172a6d
|
||||
@@ -14,7 +14,7 @@ The shared `cua` library owns application/window objects, consent, and logged di
|
||||
|
||||
The experimental CUA package owns a native-app subset of the Codex-style object interface and selected MIT-licensed macOS behavior adapted from Cua Driver in a Swift helper. A source manifest records the adapted upstream files and local implementation. The helper is locally compiled; the package has no external Cua Driver runtime dependency. The existing [provider registration](2026-09-12-computer-use-provider-registration.md) prevents competing computer-use providers within one composition.
|
||||
|
||||
The facade accepts an operation dispatcher. Its tool adapter resolves the current tool binding for each operation, preserving normal authorization, logging and image admission. The execution host owns variable persistence and completion serialization. CUA defines no persistence directive, worker or run_code replacement.
|
||||
The facade accepts an operation dispatcher. Its tool adapter resolves the current tool binding for each operation, preserving normal authorization, logging and image admission. The execution host owns variable persistence and completion serialization. The object API defines no persistence directive or evaluator; [CUA programs outside PTC](2026-11-09-cua-program-tool.md) supersedes the original exclusion of a dedicated program entry by composing the shared PTC factory and Node runtime.
|
||||
|
||||
Native process ownership follows the [shared helper decision](2026-10-09-cua-shared-native-helper.md), with isolated Agent connections. App handles bind process identity and kernel start time; replacing the process or helper never revives old handles. The native helper retains per-app and per-window snapshots. The [object API](2026-11-05-cua-scoped-surfaces-and-input.md) keeps the latest successful observation per application handle. Coordinate actions select an owned window; the Host maps [screenshot coordinates](2026-10-13-cua-hover-routes-and-coordinates.md) to native window-local points. Providers declare keyboard scopes in `keyboard`. macOS supports keyboard input at the application's current focus.
|
||||
|
||||
@@ -22,7 +22,7 @@ The operation schema owns shared data declarations. Host-only limits and launch
|
||||
|
||||
Clipboard paste is distinct from Unicode key input: Return and Tab key events can trigger actions instead of inserting clipboard content. Pasteboard access runs on the AppKit main actor and retains promised data until a reader requests it. AX command completion alone does not establish that the application has fetched the data. Restoration checks ownership before replacing contents; the caller still verifies the application result. After successful command dispatch, `pasteReadTimeoutMs` bounds waiting for a data request, with a default of 1000 ms. Expiry returns `PASTE_NOT_CONSUMED`, closes the data provider against late delivery, and restores the clipboard only while still owned. The failure reports no read within that interval; it does not prove the application will never process its queued Paste command. A delayed command may read restored clipboard contents, so callers inspect the target and never retry automatically. Foreground-dependent consumption was observed in the installed ChatGPT/Codex application (`com.openai.codex`); other applications need their own validation. Guidance presents this as an example and applies recovery instructions to any `PASTE_NOT_CONSUMED` result.
|
||||
|
||||
CUA registers its own Harness tool and returns operation values directly. Screenshot capture requires a resolved image-capable model and the shared attachment service. The tool admits each screenshot once and renders its durable image reference; the window object and image sink retain the same `CuaScreenshot` containing encoded `bytes`, `mimeType`, `pixelWidth`, `pixelHeight` and `windowId` from the validated screenshot result. The bridge preserves this metadata without another upload or image emission. Callers use the reported format and dimensions when saving or declaring images.
|
||||
CUA registers its own Harness tool and returns operation values directly. Screenshot capture requires a resolved image-capable model and the shared attachment service. The attachment pipeline stores the screenshot at the dimensions used for coordinate mapping; the native tool result describes its encoded format and pixels. The program facade returns observation results, including the stored Image for a screenshot. The [explicit PTC output decision](2026-11-09-explicit-ptc-output.md) owns image identity and caller-selected display.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -32,7 +32,7 @@ A CUA-specific persistent evaluator duplicates execution lifecycle policy and pr
|
||||
|
||||
Reusing the MCP result adapter would impose MCP envelopes and wire validation on native CUA operations. Direct Harness tool output and shared attachment admission provide the needed logging and images without a second protocol.
|
||||
|
||||
**Fixing the screenshot format** would require otherwise unnecessary re-encoding and discard the attachment pipeline’s pass-through optimization. A format convention alone still forces callers to guess, and cannot tell them when encoding policy changes. Returning the already computed format and dimensions preserves pass-through and makes each result describe its actual bytes.
|
||||
**Fixing the screenshot format** would require otherwise unnecessary re-encoding and discard the attachment pipeline’s pass-through optimization. The native result describes its encoding; program Images expose the stored file and its pixel dimensions, so callers can use the file without a facade-specific byte representation.
|
||||
|
||||
**Waiting until the full native-operation timeout** hides successful command dispatch behind a generic timeout. A separate configurable read budget reports the missing acknowledgement without treating slow applications as unsupported. Requiring every application to be foreground would claim a prerequisite that the available observations do not establish.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Computer-use 程序需要一致的应用和窗口操作。将这些操作与 CUA
|
||||
|
||||
实验 CUA 包提供 Codex 风格的原生应用对象接口,并在 Swift 辅助进程中改写 Cua Driver 中选取的 MIT 许可 macOS 行为。源码清单记录改写的上游文件和本地实现。辅助进程在本地编译,本包不依赖外部 Cua Driver 运行时。已有的 [provider 注册机制](2026-09-12-computer-use-provider-registration.zh.md)防止同一组合中出现竞争 provider。
|
||||
|
||||
对象接口接收操作分派器。工具适配模块每次操作都获取当前工具绑定,保留标准授权、日志和图片接入。执行宿主负责变量持久化和最终表达式值的序列化。CUA 不定义持久化指令、执行 worker 或 run_code 替代实现。
|
||||
对象接口接收操作分派器。工具适配模块每次操作都获取当前工具绑定,保留标准授权、日志和图片接入。执行宿主负责变量持久化和最终表达式值的序列化。对象 API 不定义持久化指令或求值器;[PTC 之外的 CUA 程序](2026-11-09-cua-program-tool.zh.md)通过组合共享 PTC 工厂和 Node 运行时,取代原先不提供专用程序入口的决定。
|
||||
|
||||
原生进程归属遵循[共享 helper 决策](2026-10-09-cua-shared-native-helper.zh.md),Agent 连接保持隔离。应用句柄绑定进程身份及内核启动时间,替换进程或辅助进程不会使旧句柄重新有效。原生 helper 按应用和可选窗口保存快照。[对象 API](2026-11-05-cua-scoped-surfaces-and-input.zh.md) 为每个应用句柄保留最近一次成功读取的快照。坐标操作选择属于该应用的窗口;Host 按[截图坐标规则](2026-10-13-cua-hover-routes-and-coordinates.zh.md)换算为原生窗口内 point。提供方通过 `keyboard` 声明可用范围。macOS 仅支持向应用当前焦点发送键盘输入。
|
||||
|
||||
@@ -22,7 +22,7 @@ Computer-use 程序需要一致的应用和窗口操作。将这些操作与 CUA
|
||||
|
||||
剪贴板粘贴与 Unicode 按键输入分别实现,因为 Return 和 Tab 按键可能触发动作,而不是插入剪贴板内容。剪贴板访问使用 AppKit 主线程,保留延迟提供的数据直到读取方请求。AX 命令完成本身不能证明应用已取得数据。恢复前检查剪贴板归属;调用方仍需验证应用中的结果。 命令成功分派后,`pasteReadTimeoutMs` 限制等待数据请求的时间,默认 1000 毫秒。到期返回 `PASTE_NOT_CONSUMED`,关闭数据提供方以阻止迟到的数据交付,并仅在仍持有剪贴板时恢复原内容。该失败只说明这段时间内没有读取,不证明应用以后不会处理已排队的 Paste 命令。迟到的命令可能读取恢复后的剪贴板内容,因此调用方必须检查目标,不能自动重试。已安装的 ChatGPT/Codex 应用(`com.openai.codex`)已观察到依赖前台才能读取的行为,其他应用需要分别验证。指引将其写为例子,并对任何 `PASTE_NOT_CONSUMED` 结果提供恢复步骤。
|
||||
|
||||
CUA 注册自己的 Harness 工具,直接返回操作结果。截图要求解析到支持图片输入的模型,并提供共享附件服务。工具只接入一次截图,再渲染持久图片引用;窗口对象和图片输出回调保留同一个 `CuaScreenshot`,其中的编码 `bytes`、`mimeType`、`pixelWidth`、`pixelHeight` 和 `windowId` 来自已校验的截图结果。桥接保留这些元数据,不再次上传或输出图片。调用方落盘或声明图片时使用返回的格式和尺寸。
|
||||
CUA 注册自己的 Harness 工具,直接返回操作结果。截图要求解析到支持图片输入的模型,并提供共享附件服务。附件流水线按坐标换算使用的尺寸保存截图,原生工具结果描述编码格式和像素。程序对象接口返回观察结果,截图操作返回已存储的 `Image`。[PTC 显式输出决策](2026-11-09-explicit-ptc-output.zh.md)负责图片身份及调用方选择展示的规则。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -32,7 +32,7 @@ CUA 专用持久执行器会重复实现执行生命周期规则,使 PTC 无
|
||||
|
||||
复用 MCP 结果适配器会给原生 CUA 操作引入 MCP 信封和协议校验。直接使用 Harness 工具输出与共享附件接入,即可取得所需的日志和图片能力,无需再经过一种协议。
|
||||
|
||||
**固定截图格式**会要求原本不需要的重新编码,并失去附件处理的原样直通优化。仅约定格式仍要求调用方猜测,也无法告知编码策略的变化。返回已经算出的格式和尺寸,既保留直通优化,也让每个结果说明其实际字节。
|
||||
**固定截图格式**会要求原本不需要的重新编码,并失去附件处理的原样直通优化。原生结果描述编码格式;程序 Image 提供已存储文件及其像素尺寸,使调用方直接使用文件,无需对象接口专属的字节表示。
|
||||
|
||||
**等到原生操作的总超时**会用一条通用超时掩盖命令已经成功分派的事实。独立、可配置的读取等待上限能报告缺失的读取确认,而不将响应慢的应用认定为不支持粘贴。要求所有应用都必须位于前台,则超出了现有观察能支持的结论。
|
||||
|
||||
|
||||
+2
-2
@@ -15,5 +15,5 @@
|
||||
en: d6f40bf02df37ab7
|
||||
zh: e5b85ce9d41e8754
|
||||
/agent-note-ship-experimental-capabilities-as-optional-bundles/consequences:
|
||||
en: 3064da835e892958
|
||||
zh: bfcf967b6acc6f85
|
||||
en: 49e2c3f9f641c38b
|
||||
zh: 3aa772fb43c4bb6e
|
||||
+1
-1
@@ -34,4 +34,4 @@ Computer Use is a shipped optional bundle in the experimental group. Its bundle
|
||||
|
||||
Official discovery does not imply experimental status or a different plugin lifecycle. Switching a bundle off keeps its installed files; failed activation retains the existing saved-selection behavior. Bundle rows are ordinary Host rows with their usual switches. Individual and combined composition tests cover the Host rows and every preset's tool catalog; live switching and recorded Sessions cover activation and model-visible behavior.
|
||||
|
||||
Computer Use keeps its permission service and native provider on the Host and registers tools in Agent scopes whose bound preset is `ptc`. Existing Agent events and `createScope()` maintain current, new and resumed Agents. Preset declarations remain unchanged. Only the explicit detail-page action changes the new-session default through the existing settings form; the activation switch does not configure PTC execution.
|
||||
Computer Use keeps its permission service and native provider on the Host and registers tools in Agent scopes. The preset named by `agentPreset` receives raw `cua`; other composed presets receive the [CUA program tool](2026-11-09-cua-program-tool.md). Existing Agent events and `createScope()` maintain current, new and resumed Agents. Preset declarations remain unchanged. Only the explicit detail-page action changes the new-session default through the existing settings form; the activation switch does not configure PTC execution.
|
||||
+1
-1
@@ -34,4 +34,4 @@ Computer Use 是 experimental 包组内随安装提供的可选组合包。组
|
||||
|
||||
官方发现不表示实验性状态,也不引入不同的插件生命周期。关闭 bundle 保留已安装文件;激活失败保留现有的已保存选择行为。Bundle 行是普通 Host 行,使用常规开关。独立与组合测试覆盖 Host 行及每个预设的工具目录;实时切换与已记录 Session 覆盖激活和模型可见行为。
|
||||
|
||||
Computer Use 将权限服务和原生提供方保留在 Host,在已绑定 `ptc` preset 的 Agent 作用域中注册工具。现有 Agent 事件和 `createScope()` 维护当前、新建和恢复的 Agent,preset 声明保持不变。只有详情页的显式操作通过现有设置表单修改新会话默认模式,启用开关不配置 PTC 执行方式。
|
||||
Computer Use 将权限服务和原生提供方保留在 Host,并在 Agent 作用域中注册工具。`agentPreset` 指定的 preset 获得原始 `cua`;其他已组合的 preset 获得 [CUA 程序工具](2026-11-09-cua-program-tool.zh.md)。现有 Agent 事件和 `createScope()` 维护当前、新建和恢复的 Agent,preset 声明保持不变。只有详情页的显式操作通过现有设置表单修改新会话默认模式,启用开关不配置 PTC 执行方式。
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-11-09-cua-program-tool.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 .agents/notes/implemented/architecture/2026-11-09-cua-program-tool.md
|
||||
/agent-note-cua-programs-outside-the-ptc-preset:
|
||||
en: a283f005c51274e7
|
||||
zh: 1f697efb2dbac3ba
|
||||
/agent-note-cua-programs-outside-the-ptc-preset/problem:
|
||||
en: 87ce83e36a6ab68e
|
||||
zh: 8aa98dc7dae03c41
|
||||
/agent-note-cua-programs-outside-the-ptc-preset/decision:
|
||||
en: 13e079e19b5f2d6d
|
||||
zh: 2ce9b6aa981162d7
|
||||
/agent-note-cua-programs-outside-the-ptc-preset/alternatives-considered:
|
||||
en: b5d8e00cbe4b53ae
|
||||
zh: 4ef81611150a8d75
|
||||
/agent-note-cua-programs-outside-the-ptc-preset/consequences:
|
||||
en: 22f25d54373ba06b
|
||||
zh: ee335578caaf1ab3
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: CUA programs outside the PTC preset
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-09-cua-program-tool.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Computer Use needs reusable application and window objects in Standard and other presets without changing their ordinary tool calls or requiring users to change the profile's PTC runtime. Experimental CUA packages cannot appear in shipped preset definitions. Program dispatch must retain the same permission checks, cancellation, nested logs, and screenshots as PTC.
|
||||
|
||||
## Decision
|
||||
|
||||
CUA keeps `agentPreset: ptc` as the default selector for its raw `cua` tool. Every other composed preset receives `cua_run_code`; Agents without a composed preset receive neither tool. `agentPreset: false` retains global raw-tool registration. A base owner uses Agent scopes for registration and disposal, so enabling and disabling the provider affects existing and later Agents without editing presets. Raw registration depends only on base CUA services and preset selection. The private program producer publishes and withdraws its definition by identity; private dependency loss or startup failure leaves raw tools and prompts available.
|
||||
|
||||
`cua_run_code` composes the shared [program-tool factory](../../../../packages/core/tool-executor-ptc/README.md) with one isolated Node `persistent-in-memory` runtime owned by the CUA provider. Agent `stateOwner` identities select separate retained processes. Its only binding is the private `tools.cua` definition; the model calls ordinary session tools directly. Nested CUA operations retain tool policy, guards, dispatch events, and image admission. Programs select text and screenshots through `inspect(...values)`, following the [explicit output decision](2026-11-09-explicit-ptc-output.md). The provider releases held keys when either program tool settles. Provider unload drains its own CUA programs before native teardown; releasing input held by other PTC programs does not wait for their unrelated work. State is in memory, separate from the profile's PTC state, and released with its Agent or provider.
|
||||
|
||||
CUA registers a source program through `registerEnvironmentInitializer` before its isolated runtime admits a run. Each new environment evaluates it once after installing current bindings, including temporary escalated environments. The child awaits Host acceptance before starting model code. Initialization shares the first run's time and output budgets; failure discards the environment and prevents model execution without rolling back external effects. Separate evaluation preserves model source locations. An initializer-owning scope depends only on the runtime, so program-tool dependency reloads retain the registration. PTC keeps its explicit bridge initialization.
|
||||
|
||||
This partially supersedes the exclusion of a CUA program entry in [native CUA objects](2026-09-17-experimental-cua-object-api.md). That note still owns the object API's independence from an evaluator and the split between shared objects and native operations. The shared Node provider owns evaluation; CUA supplies its restricted bindings and initialization. The [optional bundle decision](2026-09-21-experimental-capabilities-as-optional-bundles.md) still owns delivery and activation.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A CUA-specific evaluator** duplicates execution lifecycle policy. Reusing the PTC factory and runtime keeps cancellation, limits, sandboxing, and result handling with their existing owners.
|
||||
|
||||
**The profile's shared runtime** couples CUA state to unrelated PTC and workflow settings. Its default per-call mode cannot retain objects. A separate CUA runtime preserves the profile's chosen execution mode.
|
||||
|
||||
**Global registration of both tool forms** exposes a program runner inside PTC programs and exposes raw CUA calls to other presets. Selecting one form per Agent keeps each mode's intended tool list. Binding every session tool inside `cua_run_code` would duplicate ordinary direct calls and their declarations.
|
||||
|
||||
**Prepending initialization to model code** shifts source locations. Running initialization as a separate tool call fails for temporary escalated environments because the next call starts another process. A separate program within the first runtime execution supports both retained and temporary environments. Passing initialization with every program couples environment setup to tool requests and repeats work in existing environments; registration keeps setup and cleanup with the runtime owner.
|
||||
|
||||
## Consequences
|
||||
|
||||
CUA adds no preset list or shipped preset dependency. Preset IDs select the tool form; custom PTC presets must match `agentPreset`. PTC and CUA share the public factory; CUA owns a runtime initializer registration and drains its program tools before unregistering it. Older custom Node bootstraps require the matching protocol revision, as described in the [upgrade guide](../../../../docs/upgrade-guide/v0.2.2-rc.1/ptc-bootstrap-protocol/guide.md).
|
||||
|
||||
The private binding limits callable Harness tools, not Node APIs. File and process access still follow the selected sandbox policy. Retained variables do not reserve applications or survive process loss; callers recreate objects and observe state before continuing. Composition and recorded-session tests cover tool selection, automatic initialization, retained state, nested output, and removal; native fixtures remain responsible for platform input behavior.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: PTC preset 之外的 CUA 程序
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-09-cua-program-tool.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Computer Use 需要在标准及其他 preset 中复用应用和窗口对象,同时保留普通工具的调用方式,也不要求用户更改 profile 的 PTC 运行时。实验性 CUA 包不能出现在随产品提供的 preset 定义中。程序派发必须保留与 PTC 相同的权限检查、取消、嵌套日志和截图行为。
|
||||
|
||||
## 决策
|
||||
|
||||
CUA 保留 `agentPreset: ptc`,用默认选择器指定获得原始 `cua` 工具的 preset。其他已组合的 preset 获得 `cua_run_code`;未组合 preset 的 agent(智能体)不获得这两个工具。`agentPreset: false` 保留全局原始工具注册。基础注册的所有者通过 Agent 作用域管理注册和销毁,因此启停提供方会影响已有和后续 Agent,无需修改 preset。原始工具的注册只依赖基础 CUA 服务和 preset 选择。私有程序工具的提供方按定义对象的身份发布和撤回其定义;私有依赖缺失或启动失败时,原始工具和提示词仍然可用。
|
||||
|
||||
`cua_run_code` 将共享[程序工具工厂](../../../../packages/core/tool-executor-ptc/README.zh.md)与 CUA 提供方持有的单个隔离 Node `persistent-in-memory` 运行时组合。Agent 的 `stateOwner` 身份选择各自独立的保留进程。它只绑定私有 `tools.cua` 定义;模型直接调用其他普通会话工具。嵌套 CUA 操作保留工具策略、guard、派发事件和图像接纳。程序通过 `inspect(...values)` 选择文字和截图,遵循[显式输出决策](2026-11-09-explicit-ptc-output.zh.md)。任一程序工具结束时,提供方都会释放该程序仍按住的键。提供方卸载时,先等待自己持有的 CUA 程序结束再清理原生资源;释放其他 PTC 程序持有的输入时,不等待这些程序中的其他工作。状态保存在内存中,独立于 profile 的 PTC 状态,随所属 Agent 或提供方释放。
|
||||
|
||||
CUA 在隔离运行时首次接纳运行前,通过 `registerEnvironmentInitializer` 注册一段源码。每个新环境安装本次绑定后执行该程序一次,包括临时提权环境。子进程等待 Host 确认后才开始执行模型代码。初始化与首次运行共享时间和输出预算;失败会丢弃环境并阻止模型代码执行,但不回滚外部效果。独立求值保留模型源码位置。初始化程序所属的作用域只依赖运行时,因此程序工具的依赖重载时仍保留这项注册。PTC 保留显式 bridge 初始化。
|
||||
|
||||
这部分取代[原生 CUA 对象](2026-09-17-experimental-cua-object-api.zh.md)中不提供 CUA 程序入口的决定。原记录继续负责对象 API 独立于求值器,以及共享对象与原生操作之间的职责划分。共享 Node 提供方负责求值,CUA 提供受限的绑定和初始化。[可选组合包决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)继续负责分发和启用方式。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**CUA 专用求值器**会重复实现执行生命周期策略。复用 PTC 工厂和运行时,使取消、限制、沙箱和结果处理继续由现有实现负责。
|
||||
|
||||
**profile 的共享运行时**会使 CUA 状态依赖无关的 PTC 和工作流设置。其默认逐次调用模式无法保留对象。独立的 CUA 运行时保留 profile 选定的执行模式。
|
||||
|
||||
**全局注册两种工具形态**会在 PTC 程序中暴露另一个程序执行器,并向其他 preset 暴露原始 CUA 调用。按 Agent 选择一种形态,可以保留各模式预期的工具列表。在 `cua_run_code` 内绑定所有会话工具,会重复普通的直接调用及其声明。
|
||||
|
||||
**把初始化拼到模型代码前**会偏移源码位置。通过单独的工具调用执行初始化无法适用于临时提权环境,因为下次调用会启动另一个进程。首次运行时执行中的独立程序同时支持保留环境和临时环境。每次提交程序都传入初始化,会使环境准备依赖工具请求,并在已有环境中重复执行;注册使准备和清理继续由运行时所有者负责。
|
||||
|
||||
## 后果
|
||||
|
||||
CUA 不增加 preset 列表,也不向内置 preset 添加依赖。工具形态按 preset ID 选择;自定义 PTC preset 必须与 `agentPreset` 匹配。PTC 与 CUA 共享公开工厂;CUA 拥有运行时初始化程序的注册,注销前会等待自己的程序工具执行结束。旧版自定义 Node bootstrap 需要匹配协议版本,详见[升级指南](../../../../docs/upgrade-guide/v0.2.2-rc.1/ptc-bootstrap-protocol/guide.zh.md)。
|
||||
|
||||
私有绑定限制可调用的 Harness 工具,不限制 Node API。文件与进程访问仍遵循所选沙箱策略。保留变量不会独占应用,也不能跨进程丢失保留;调用方应重建对象并观察状态后再继续。组合测试和已记录会话测试覆盖工具选择、自动初始化、状态保留、嵌套输出及移除;平台输入行为仍由原生 fixture(测试前置数据)负责验证。
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-11-09-explicit-ptc-output.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 .agents/notes/implemented/architecture/2026-11-09-explicit-ptc-output.md
|
||||
/agent-note-explicit-text-and-image-output-in-ptc:
|
||||
en: b62f1fc259e6f4a7
|
||||
zh: 340942c222a38e2a
|
||||
/agent-note-explicit-text-and-image-output-in-ptc/problem:
|
||||
en: 51667474a3f9053d
|
||||
zh: 70c6af69e943f4f5
|
||||
/agent-note-explicit-text-and-image-output-in-ptc/decision:
|
||||
en: ec82c0ce9f6a371d
|
||||
zh: 74596c0ab6a3cdd7
|
||||
/agent-note-explicit-text-and-image-output-in-ptc/alternatives-considered:
|
||||
en: cfbca328947567f3
|
||||
zh: 3b2f619365d2250e
|
||||
/agent-note-explicit-text-and-image-output-in-ptc/consequences:
|
||||
en: 636609d91f12a960
|
||||
zh: b26397c72f972827
|
||||
@@ -0,0 +1,43 @@
|
||||
# Agent Note: Explicit text and image output in PTC
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-09-explicit-ptc-output.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
PTC lets a program acquire and filter intermediate tool results before choosing what the model sees. A final expression or return value can expose incidental data, while automatically attaching every acquired image defeats that selection. Separate screenshot bytes and image-output callbacks also make programs convert images before comparing or displaying them.
|
||||
|
||||
## Decision
|
||||
|
||||
TypeScript `run_code` programs select text, objects and images through top-level `inspect(...values)`. Arguments are rendered independently: strings appear directly, and other values use Node's object formatting without depth, array-length or string-length preview limits. Inspection displays undefined properties, Error objects and circular references; requiring lossless JSON for display would interrupt subsequent work after tools have already executed. Images can occur inside arrays or objects. The runtime Image's custom inspection hook collects images reached by Node's formatter; each formatting call owns its collector. One output sequence preserves the relationship between explanatory text and selected images.
|
||||
|
||||
The tool executor requests `discardCompletion: true`, so completion values are discarded before JSON validation and serialization. This is a consumer choice: direct runtime and workflow consumers retain completion values when their protocols require them. The [PTC foundation](../feature/2026-06-15-ptc.md) owns dispatch and settlement. The [typed-return decision](../feature/2026-07-20-ptc-typed-tool-returns.md) continues to own ordinary JSON binding values, failures and output accounting; this note owns TypeScript output selection and image projection.
|
||||
|
||||
The Node environment supplies `inspect` alongside the complete native console. Native console methods remain available to programs and imported libraries, and their stdout and stderr writes share the captured output sequence. Model instructions direct programs to select output through `inspect`.
|
||||
|
||||
`read_image` and CUA screenshot bindings expose stored `Image` values. Readonly `attachmentId`, `path`, `width` and `height` identify the stored image and its readable execution-world file. The dimensions describe that file's pixels, including the image used for CUA coordinate mapping. The [CUA object API](2026-09-17-experimental-cua-object-api.md) returns screenshot Images and text observations directly to the program. The program passes selected values to `inspect` for display.
|
||||
|
||||
The host carries the attachment reference with the program-readable path. The runtime transports image metadata and keeps the attachment reference behind a process-owned image ID. Inspection resolves that ID directly to the registered reference. Image identity is established by the runtime; matching object properties alone do not grant access to an image output. Retained program Images follow their owning process's lifetime.
|
||||
|
||||
Nested tool dispatches retain their complete rendered content in the Session log. Successful outer `run_code` results interleave inspected images with text in emission order, where model history and UI rendering use the same durable attachment references. Successful outer-result replacement remains authoritative over the selected output.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Display the completion value alongside printed output.** The last expression or return then controls context admission as well as program control flow. Discarding it before validation lets a program finish with an arbitrary internal value while explicitly selecting a smaller visible result.
|
||||
|
||||
**Automatically forward every acquired image.** Acquisition then implies model-context cost, even when the program needs several screenshots to select one. Explicit inspection gives text and images the same selection rule.
|
||||
|
||||
**Keep a CUA byte-returning facade and image sink.** This maintains a separate screenshot representation and a second image-emission path. Returning the runtime's Image lets file reads and screenshots share inspection and preserves the stored attachment reference through display.
|
||||
|
||||
**Discover images with a separate object traversal.** A separate traversal must duplicate Node's handling of collections, symbols and custom formatters and can emit images absent from the text representation. The Image hook ties collection to Node's formatting.
|
||||
|
||||
**Bounded object previews.** Item and string preview limits can omit file contents that a program explicitly selected. Removing those limits keeps selected contents complete within the shared output budget.
|
||||
|
||||
**Restrict console availability.** Missing bindings and incomplete replacements can break program code and imported libraries. The native console preserves Node methods while prompt guidance directs output selection through `inspect`.
|
||||
|
||||
## Consequences
|
||||
|
||||
Programs explicitly choose the output needed for the next model request, including image comparisons and selected nested results. The [upgrade guide](../../../../docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.md) owns caller migration. Acquiring an image still performs storage and processing work; selection controls its presentation to the model.
|
||||
|
||||
The runtime owns process-local image identity and output ordering; the tool executor owns attachment projection and durable result content. Text and image-reference metadata share the outer output budget. Runtime teardown releases retained image identities, while stored attachments follow the attachment store's lifetime.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Agent Note: PTC 显式文本与图片输出
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-09-explicit-ptc-output.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
PTC 允许程序先获取和筛选中间工具结果,再选择模型看到的内容。末尾表达式或返回值可能暴露偶然产生的数据,而自动附加每张已获取的图片会使这种筛选失效。独立的截图字节和图片输出回调也要求程序先转换图片,再进行比较或展示。
|
||||
|
||||
## 决策
|
||||
|
||||
TypeScript `run_code` 程序通过顶层 `inspect(...values)` 选择文本、对象和图片。各参数独立渲染:字符串直接展示,其他值使用 Node 的对象格式,不限制预览深度、数组项数或字符串长度。inspect 展示 undefined 属性、Error 对象和循环引用;要求展示值必须是无损 JSON,会在工具已经执行后中断后续工作。图片也可以嵌入数组或对象。运行时 Image 的自定义 inspect 钩子收集 Node 格式化函数访问到的图片;每次格式化调用拥有自己的收集器。同一输出序列保留说明文字与所选图片的对应关系。
|
||||
|
||||
工具执行器请求 `discardCompletion: true`,在 JSON 校验和序列化前丢弃完成值。这是消费方的选择:直接运行时调用方和工作流消费方在其协议需要时保留完成值。[PTC 基础](../feature/2026-06-15-ptc.zh.md)负责分发与结算。[类型化返回值决策](../feature/2026-07-20-ptc-typed-tool-returns.zh.md)继续负责普通 JSON 绑定值、失败和输出计量;本记录负责 TypeScript 输出选择与图片投影。
|
||||
|
||||
Node 环境同时提供 `inspect` 和完整的原生 console。程序与导入的库均可使用原生 console 方法,其 stdout 和 stderr 写入共用捕获输出序列。模型指引要求程序通过 `inspect` 选择输出。
|
||||
|
||||
`read_image` 和 CUA 截图绑定提供已存储的 `Image` 值。只读的 `attachmentId`、`path`、`width` 和 `height` 标识已存储图片及其在执行环境中可读取的文件。尺寸描述该文件的像素,也对应 CUA 坐标换算所用的图片。[CUA 对象 API](2026-09-17-experimental-cua-object-api.zh.md)将截图的 `Image` 和观察文本直接返回给程序。程序将选中的值传给 `inspect` 展示。
|
||||
|
||||
宿主将附件引用与程序可读取的路径一起传递。运行时传输图片元数据,并用进程拥有的图片 ID 保存对应的附件引用。展示时,该 ID 直接解析为已登记的引用。图片身份由运行时建立,仅有相同对象属性不能取得图片输出权限。保留在程序中的 Image 跟随其所属进程的生命周期。
|
||||
|
||||
嵌套工具分发将完整渲染内容保留在 Session 日志中。成功的外层 `run_code` 结果按输出顺序穿插显式展示的图片和文本,模型历史和 UI 渲染使用相同的持久附件引用。成功外层结果的替换仍对所选输出具有最终决定权。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**同时展示完成值与打印输出。** 末尾表达式或返回值会同时控制上下文纳入和程序控制流。在校验前丢弃完成值,使程序可以用任意内部值结束,并显式选择更小的可见结果。
|
||||
|
||||
**自动转发每张已获取的图片。** 即使程序需要先获取多张截图才能选出一张,获取动作本身也会产生模型上下文成本。显式展示让文本和图片遵循同一选择规则。
|
||||
|
||||
**保留返回字节的 CUA 对象接口和图片输出回调。** 这会维护独立的截图表示和第二条图片输出路径。返回运行时 Image,使文件读取和截图共用展示方式,并在展示过程中保留已存储附件引用。
|
||||
|
||||
**单独遍历对象以发现图片。** 独立遍历必须重复实现 Node 对集合、Symbol 和自定义格式化的处理,还可能输出文本表示中未出现的图片。Image 钩子让收集跟随 Node 的格式化。
|
||||
|
||||
**限制对象预览。** 条目和字符串的预览上限可能省略程序已明确选中的文件内容。取消这些限制,在共享输出预算内完整保留选中的内容。
|
||||
|
||||
**限制 console 的可用性。** 缺失的绑定和不完整的替代实现可能使程序及导入的库出错。原生 console 保留 Node 方法,提示词指引程序通过 `inspect` 选择输出。
|
||||
|
||||
## 后果
|
||||
|
||||
程序显式选择下一次模型请求需要的输出,包括图片比较和嵌套结果中的所选内容。[升级指南](../../../../docs/upgrade-guide/v0.2.2-rc.1/ptc-inspect-output/guide.zh.md)负责调用方迁移。获取图片仍会执行存储和处理工作,选择控制的是向模型展示的内容。
|
||||
|
||||
运行时负责进程内图片身份和输出排序;工具执行器负责附件投影和持久结果内容。文本和图片引用元数据共用外层输出预算。运行时拆卸释放保留的图片身份,已存储附件则遵循附件存储的生命周期。
|
||||
@@ -0,0 +1,73 @@
|
||||
# Bilingual-pair consistency record for 2026-11-09-retained-ptc-scoped-repl.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 .agents/notes/implemented/architecture/2026-11-09-retained-ptc-scoped-repl.md
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc:
|
||||
en: ea465612c52ff8a6
|
||||
zh: 1c4021aecb7550c6
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/problem:
|
||||
en: 9e115eb5c8b8d168
|
||||
zh: d47b2a783e154664
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision:
|
||||
en: 02e060e10da7bb75
|
||||
zh: d4b01038283943bd
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/exact-behavior:
|
||||
en: 14dff09dd91c1533
|
||||
zh: d199bef553f9439c
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/exact-behavior/syntax-and-results:
|
||||
en: 4f0c54f87042943e
|
||||
zh: 34b88dbb1d50a2c6
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/exact-behavior/across-calls:
|
||||
en: d389222669ca94d7
|
||||
zh: cf19a71bce644ee4
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/exact-behavior/environment:
|
||||
en: b1d35c4055c7f381
|
||||
zh: 404b3843b2348fec
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/exact-behavior/errors:
|
||||
en: 5b4209a69447bfac
|
||||
zh: 3b1cb6297962b6c1
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/exact-behavior/asynchronous-failures-and-lifecycle:
|
||||
en: 70c47aba7a23fc4f
|
||||
zh: 1d7d5f0809e748aa
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/comparison-with-node-repl-and-vm-script:
|
||||
en: 013bfbf3d3944637
|
||||
zh: eba400efadf6229b
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/context-and-options:
|
||||
en: 14fc1abb8e437555
|
||||
zh: 38a37fdb8ca7e17c
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/wrapper:
|
||||
en: d886821f764b6804
|
||||
zh: a073534825d639a2
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/kernel-scope-chain-acorn-scan-and-compilation:
|
||||
en: 10d0459a356cd33d
|
||||
zh: 07e7cd70119c4d4d
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/scope-chain-state-and-the-steps-of-a-call:
|
||||
en: fbb63be77e727e07
|
||||
zh: 12f24e0d0672907c
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/failed-calls:
|
||||
en: 64937f1d5acd7147
|
||||
zh: f262dff89139086f
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/asynchronous-failures-aligned-with-the-repl:
|
||||
en: 7f867b69623f3652
|
||||
zh: 551a456448700171
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/dynamic-import:
|
||||
en: 2607ffb07f5c4b4b
|
||||
zh: 5882d8c58d5a775b
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/redeclaration-notice-reserved:
|
||||
en: c3eef0f99686acfe
|
||||
zh: 9093c1e84abff335
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/out-of-scope:
|
||||
en: 725a4139f28961c3
|
||||
zh: b32da1bf86095e65
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/decision/integration:
|
||||
en: b05235293c504467
|
||||
zh: 48f5f534812a12b3
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/alternatives-considered:
|
||||
en: e548d12c174bb44e
|
||||
zh: 9f67908a564d6f78
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/consequences:
|
||||
en: d75ef0e16dcdcee2
|
||||
zh: 22ae67a41c1461ca
|
||||
/agent-note-scoped-repl-evaluator-for-retained-ptc/testing:
|
||||
en: 799de02ac67753f3
|
||||
zh: f3ad0647273fc458
|
||||
@@ -0,0 +1,524 @@
|
||||
# Agent Note: Scoped REPL evaluator for retained PTC
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-09-retained-ptc-scoped-repl.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`persistent-in-memory` runs every `run_code` call of one Agent in one global scope through node:repl ([best-effort persistent PTC](../../implemented/architecture/2026-09-23-best-effort-persistent-ptc.md)). When a later call declares a `const`, `let`, or `class` name again, V8 rejects the program before it runs with `SyntaxError: Identifier 'x' has already been declared`, and no statement executes. Model guidance can only ask for no redeclarations, and models rarely track every name across many calls.
|
||||
|
||||
Rewriting declarations as `var` removes the error, but every call then shares one binding, so a later declaration changes the value that earlier functions, closures, and background tasks read:
|
||||
|
||||
```js
|
||||
// call 1
|
||||
var value = 1
|
||||
var read = () => value
|
||||
// call 2
|
||||
var value = 2 // no error
|
||||
// call 3
|
||||
read() // 2: call 2 changed the read written by call 1
|
||||
```
|
||||
|
||||
Five requirements must hold together:
|
||||
|
||||
1. A later call may declare an earlier name again with any keyword without an error.
|
||||
2. A redeclaration applies to that call and later calls; earlier functions, closures, and still-running code keep their original values.
|
||||
3. Syntax matches the existing REPL: top-level `await` works, the final expression is the result, and top-level `return` is not supported.
|
||||
4. Errors in asynchronous callbacks are handled as in the existing REPL: the process keeps running and variables are retained.
|
||||
5. No scope analysis and no rewriting of declarations or references in model code; acorn only lists top-level declarations and finds the last statement.
|
||||
|
||||
## Decision
|
||||
|
||||
The scoped REPL evaluator is split into a kernel and an evaluator:
|
||||
|
||||
| File | Contents |
|
||||
|---|---|
|
||||
| `packages/ptc-runtime/ptc-runtime-node/src/evaluators/scoped-repl.ts` | Kernel: the `ScopedReplScopes` scope chain, which owns binding state, copies and fallback values, redeclarations, and Script compilation; plus the acorn scan, top-level declarations and keywords, top-level `return` check, final expression, reserved-name check, syntax-error layout, and source generation |
|
||||
| `packages/ptc-runtime/ptc-runtime-node/src/evaluators/scoped-repl-evaluator.ts` | Evaluator: exports `createScopedReplEvaluator(options)` implementing the existing `ProgramEvaluator` interface and connects the kernel to the existing modes: context and options, dynamic imports and the import handler, starting programs from a microtask and reading completions, the domain, and the injected `reportRedeclarations` |
|
||||
| `packages/ptc-runtime/ptc-runtime-node/tests/scoped-repl.spec.ts` | Kernel tests only; runs on its own and covers scanning, source generation, and scope layering across consecutive programs in a plain vm context |
|
||||
| `packages/ptc-runtime/ptc-runtime-node/tests/scoped-repl-evaluator.spec.ts` | Integration tests: completions, environment, modules and imports, errors, and asynchronous failures; every option tested on and off |
|
||||
|
||||
The `scoped-repl` format connects the evaluator to the existing execution modes; see Integration.
|
||||
|
||||
### Exact behavior
|
||||
|
||||
#### Syntax and results
|
||||
|
||||
- A program is a list of statements and supports top-level `await`.
|
||||
- When the last non-empty top-level statement is an expression statement, its value is the result. A Promise value is not awaited: the Promise itself is the result, so the host reports `program completion must be lossless JSON`, as with the existing REPL. The `finalExpression` option controls this and defaults to on; when off, the final value is discarded, results come only from printing, and model guidance must change accordingly. `run_code` passes `discardCompletion: true` for TypeScript runtimes, so the host discards completion values and the model displays output with `inspect(...)`; callers that keep completions, such as workflow-ptc, still take the final expression's value as the result.
|
||||
- When the last statement is a declaration, `if`, `for`, block, or another statement, there is no result.
|
||||
- Top-level `return`, including inside top-level `if`, loops, and blocks, reports `SyntaxError: Illegal return statement`, and no statement of that call runs. Through `run_code`, the host's TypeScript erasure rejects it first with `Return statement is not allowed here`, as with the existing REPL.
|
||||
- A static `import` or `export` reports acorn's `SyntaxError: 'import' and 'export' may appear only with 'sourceType: module'`; acorn also rejects top-level `using` and `await using`, which remain available inside blocks and functions.
|
||||
- A program that starts with `#!` reports `SyntaxError: Hashbang lines are not supported` and does not run: a hashbang is valid only at the very start of a Script, and the generated prefix precedes the program.
|
||||
- Output is the printed text, such as `inspect(...)`, plus the final expression's value when the caller keeps completions.
|
||||
|
||||
#### Across calls
|
||||
|
||||
- Each call starts by copying every top-level name left by earlier calls into a same-named declaration; objects are shared by reference, so changes inside an object are visible on both sides.
|
||||
- A call may declare an earlier name again with any keyword, including a `var`→`let`→`const`→`class`→`function` sequence. A name the call declares with `let`, `const`, `class`, or `function` is not copied, and the call's declaration wins; a name it declares again only with `var` is still copied, so that `var` starts from the earlier value.
|
||||
- Assigning to an earlier name without declaring it changes this call's copy: later calls see the new value, and earlier functions keep the old one. This includes names declared with `const`, which are read-only only in the call that declares them.
|
||||
- After a function is redefined, earlier callers still call the old function.
|
||||
- Assignments that earlier code, such as timers and callbacks, makes to top-level variables after a call starts are not visible to later calls; keep shared mutable state in objects.
|
||||
- Earlier functions cannot see names declared later; referencing them reports `ReferenceError`.
|
||||
- When a call declares a name again with `let`, `const`, or `class`, every reference in that call resolves to the new declaration, including references written before it, and the old value is unreachable: reading the name before its declaration runs reports the native `ReferenceError: Cannot access 'x' before initialization`. Such code is a program error and is not handled. A `var` redeclaration starts from the earlier value until it assigns a new one, so `var count = count + 1` adds to the earlier value; a `function` name holds the new function from the start of the call.
|
||||
- When a call fails partway: declarations completed before the throw are kept; an existing name redeclared but not yet initialized falls back to its earlier value in later calls; an uninitialized new name is dropped.
|
||||
- A `var` declared inside a block, including `try`/`catch`/`finally`, `if`/`else`, loop bodies, `switch`, and labeled statements, hoists to the top level of the call and counts as the call's declaration like a top-level `var`: it carries to later calls and may reuse a name that an earlier call declared with any keyword. A `var` inside a nested function or class does not count.
|
||||
- A `var` redeclaration that does not run, after a throw or in a branch that is not taken, keeps the earlier value, and so does `var x;` without an initializer.
|
||||
|
||||
#### Environment
|
||||
|
||||
These are the defaults; one option controls each of the first two items, see Context and options.
|
||||
|
||||
- Strict mode: assigning an undeclared name reports `ReferenceError`; intentional globals use `globalThis.x`. With the option off, strict mode is not forced: a program that starts with `"use strict"` runs strict, and other programs run sloppy.
|
||||
- No code generation from strings: the context's own `eval` and `new Function` report `EvalError`, and compiling WebAssembly from bytes reports `CompileError`. This is no isolation: main-realm functions copied into the context, such as `setTimeout.constructor`, still generate code in the main realm.
|
||||
- Modules load only through dynamic `import()`, whose relative specifiers resolve from the working directory of the environment's first call and keep doing so after later calls change directory; there is no `require`, `module`, or built-in module global such as `fs` and `path`. Relative file access still follows the current directory.
|
||||
- Top-level `this` is `undefined`, or `globalThis` when a program runs sloppy; top-level `arguments` is an empty arguments object with length 0.
|
||||
- Top-level `var` and `function` declarations do not become `globalThis` properties.
|
||||
- Node globals such as `setTimeout`, `Buffer`, `fetch`, `URL`, and `structuredClone` are available.
|
||||
|
||||
#### Errors
|
||||
|
||||
- Runtime error lines and columns are native positions in the model's code, and functions defined in an earlier call show that call's filename, for example `<directory>/.ptc-repl-1.js:2:9`. On the line of the final expression, columns after the insertion point shift right by 17, which is left as is.
|
||||
- Syntax errors that acorn finds are reported with acorn's message and position, laid out like V8's: filename and line, source line, and caret. Two messages use V8's wording: top-level `return` reports `Illegal return statement`, and an error positioned at the end of the code reports `Unexpected end of input`.
|
||||
- Errors that acorn accepts and V8 rejects during compilation report V8's native message; with the `strict` option on, strict-mode syntax violations such as `with` are reported this way.
|
||||
- These are the evaluator's own reports. Through `run_code`, the host's TypeScript erasure parses the whole program first and reports most syntax errors itself, as with the existing REPL; only code that the erasure accepts and the scan rejects, such as reserved names, static `import`, or top-level `using`, shows the layout above.
|
||||
- Runtime errors report the error name and stack, without an `Uncaught ` prefix and without evaluator frames.
|
||||
|
||||
#### Asynchronous failures and lifecycle
|
||||
|
||||
- Errors thrown by timers, callbacks, Promise continuations, and EventEmitters that a program starts, and unhandled rejections, do not end the process, and variables are retained. Exception: when a callback throws a frozen or non-extensible object, Node's domain fails to add its properties before notifying the listener, and the process ends, as with node:repl; rejecting with such an object is unaffected.
|
||||
- If an evaluation is active, it fails with `Uncaught ` followed by the error text; otherwise the error is discarded. The error text is the error's `stack`, or `name: message` when the stack is not a string; a value that is not an error is formatted with `util.inspect` after removing the `domain` and `domainThrown` properties the domain added; a failure while formatting reports `program threw an unrenderable value`. An error belongs to the evaluation that created its callback, so a timer from an earlier call failing during a later call is not charged to the later call.
|
||||
- Output printed after an evaluation ends is discarded; timeout, cancellation, and reset keep the existing host behavior ([retained PTC interruption](../../implemented/architecture/2026-10-29-retained-ptc-interruption.md)).
|
||||
|
||||
### Comparison with node:repl and vm-script
|
||||
|
||||
✓ means supported, ✗ means not supported, and "—" means not applicable: `vm-script` without persistence starts a new process for every call and has no cross-call state. "Persistent vm-script" is vm-script with `scriptPersistence: true` (#7992), and "both vm-script columns" means it and vm-script without persistence. The "ours" column is the default behavior, and its parenthesis names the columns it matches; the Environment table splits it into strict mode on, the default, and off. The Across calls table adds an implementation expected to support cross-call behavior, called the "expected implementation" in parentheses and notes.
|
||||
|
||||
**Syntax and results**
|
||||
|
||||
| Capability | node:repl (existing REPL evaluator) | Ours | vm-script (no persistence) | vm-script (persistent, #7992) | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| Top-level `return`, including inside `if` blocks | ✗ | ✗ (matches node:repl, vm-script, persistent vm-script) | ✗ | ✗ | All report `Illegal return statement` |
|
||||
| Final expression as the result | ✓ | ✓ (default; matches node:repl, vm-script, persistent vm-script) | ✓ | ✓ | Ours can turn it off with `finalExpression` |
|
||||
| Awaiting a final Promise | ✗ | ✗ (matches node:repl) | ✓ | ✓ | Without awaiting, the host reports a non-JSON completion |
|
||||
| A result when ending with `if`, `for`, or a block | ✓ | ✗ (matches node:repl, vm-script, and persistent vm-script when the code contains `await`) | ✓ | ✓ | node:repl and both vm-script columns have a result only without `await` |
|
||||
| Top-level `await` | ✓ | ✓ (matches node:repl, vm-script, persistent vm-script) | ✓ | ✓ | node:repl and both vm-script columns rewrite the whole program; ours rewrites only the final expression |
|
||||
| Static `import` | ✗ | ✗ (matches node:repl, vm-script, persistent vm-script) | ✗ | ✗ | Ours reports acorn's message |
|
||||
| Top-level `using` | ✗ | ✗ (matches node:repl, vm-script, persistent vm-script) | ✗ | ✗ | Available inside blocks and functions |
|
||||
| A program that starts with `#!` | ✓ | ✗ (matches none of node:repl, vm-script, persistent vm-script) | ✓ | ✓ | Ours reports `Hashbang lines are not supported` |
|
||||
| Two `function` declarations of one name, or a `var` and a `function` of one name, in one call | ✓ | ✓ (matches node:repl, vm-script, persistent vm-script) | ✓ | ✓ | |
|
||||
| Dynamic `import()` | ✓ | ✓ (matches node:repl, vm-script, persistent vm-script) | ✓ | ✓ | |
|
||||
|
||||
**Across calls**
|
||||
|
||||
| Capability | node:repl | An implementation expected to support cross-call behavior | Ours | vm-script | Persistent vm-script | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Variables retained across calls | ✓ | ✓ | ✓ (matches node:repl, the expected implementation, persistent vm-script) | ✗ | ✓ | node:repl and persistent vm-script share one scope; ours copies; the expected implementation copies as well |
|
||||
| Redeclaration with any keyword | ✗ | ✓ | ✓ (matches the expected implementation) | — | ✗ | In node:repl and persistent vm-script, 21 of the 25 keyword pairs report `already declared`, and only `var` and `function` may replace each other; ours reserves a redeclaration notice that prints nothing; the expected implementation prints an extra warning when a `const` is declared again as `const` |
|
||||
| Earlier functions unaffected by a later assignment without declaration | ✗ | ✓ | ✓ (matches the expected implementation) | — | ✗ | |
|
||||
| Assigning to an earlier `const` without declaring it again | ✗ | ✓ | ✓ (matches the expected implementation) | — | ✗ | node:repl and persistent vm-script report `TypeError`; ours prints nothing; the expected implementation prints a warning first |
|
||||
| A `var` redeclaration reads the earlier value of its name (`var n = n + 1`) | ✓ | ✗ | ✓ (matches node:repl, persistent vm-script) | — | ✓ | node:repl and persistent vm-script only when the earlier declaration is also `var`, and report `already declared` after `let`; ours also after `let` or `const`; the expected implementation yields `NaN` |
|
||||
| Earlier callers keep calling the old function after a redefinition | ✗ | ✓ | ✓ (matches the expected implementation) | — | ✗ | |
|
||||
| Later calls see values earlier code assigns afterwards | ✓ | ✗ | ✗ (matches the expected implementation) | — | ✓ | A timer-incremented variable read twice 30 ms apart: node:repl 23, 50; persistent vm-script 22, 44; ours 0, 0; the expected implementation 15, 15 |
|
||||
| Earlier functions see names declared later | ✓ | ✗ | ✗ (matches the expected implementation) | — | ✓ | Ours reports `ReferenceError` |
|
||||
| Changes inside an object visible on both sides | ✓ | ✓ | ✓ (matches node:repl, the expected implementation, persistent vm-script) | — | ✓ | |
|
||||
| A name stays usable after its initializer throws | ✗ | ✓ | ✓ (matches the expected implementation) | — | ✗ | In node:repl and persistent vm-script a new name stays unusable, and declaring it again reports `already declared`; ours falls back to the earlier value for existing names and drops new names |
|
||||
| Declarations completed before a throw are kept | ✓ | ✓ | ✓ (matches node:repl, the expected implementation, persistent vm-script) | — | ✓ | |
|
||||
| A `var` redeclaration not reached before a throw keeps the old value | ✓ | ✓ | ✓ (matches node:repl, the expected implementation, persistent vm-script) | — | ✓ | Ours copies the earlier value into the call's `var`, which also keeps it through an untaken branch; the expected implementation inserts marker code to achieve this |
|
||||
| A new `var` declared in a block is carried to later calls | ✓ | ✗ | ✓ (matches node:repl, persistent vm-script) | — | ✓ | Ours collects block-level `var` declarations |
|
||||
| A block-level `var` with the same name as an earlier `let` is accepted | ✗ | ✗ | ✓ (matches none of node:repl, the expected implementation, persistent vm-script) | — | ✗ | node:repl and persistent vm-script report `already declared`; ours treats it as the call's new declaration; the expected implementation reports `already declared` as well |
|
||||
|
||||
**Environment**
|
||||
|
||||
| Capability | node:repl | Ours (strict, default) | Ours (non-strict) | vm-script | Persistent vm-script | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Strict mode | ✗ | ✓ (matches none of node:repl, vm-script, persistent vm-script) | ✗ (matches node:repl, vm-script, persistent vm-script) | ✗ | ✗ | With the option off, a program's own leading `"use strict"` still applies |
|
||||
| Assigning an undeclared name creates a global | ✓ | ✗ (matches none of node:repl, vm-script, persistent vm-script) | ✓ (matches node:repl, vm-script, persistent vm-script) | ✓ | ✓ | Without strict mode the global is shared by every call and earlier function and is not copied |
|
||||
| Top-level `var` and `function` become `globalThis` properties | ✓ | ✗ (matches none of node:repl, vm-script, persistent vm-script) | ✗ (matches none of node:repl, vm-script, persistent vm-script) | ✓ | ✓ | |
|
||||
| Top-level `this` is `globalThis` | ✓ | ✗ (matches none of node:repl, vm-script, persistent vm-script) | ✓ (matches node:repl, vm-script, persistent vm-script) | ✓ | ✓ | Ours is `undefined` in strict mode |
|
||||
| Top-level `arguments` | ✗ | ✓ (matches none of node:repl, vm-script, persistent vm-script) | ✓ (matches none of node:repl, vm-script, persistent vm-script) | ✗ | ✗ | Ours is an empty arguments object |
|
||||
| `eval` and `new Function` | ✓ | ✗ (default; matches none of node:repl, vm-script, persistent vm-script) | ✗ (default; matches none of node:repl, vm-script, persistent vm-script) | ✓ | ✓ | Can be turned on; limits only the context's own functions, while copied main-realm functions still generate code |
|
||||
| WebAssembly compilation | ✓ | ✗ (default; matches none of node:repl, vm-script, persistent vm-script) | ✗ (default; matches none of node:repl, vm-script, persistent vm-script) | ✓ | ✓ | Same option as `eval` |
|
||||
| Node globals (`setTimeout`, `Buffer`, `fetch`, `URL`, `structuredClone`) | ✓ | ✓ (matches node:repl, vm-script, persistent vm-script) | ✓ (matches node:repl, vm-script, persistent vm-script) | ✓ | ✓ | |
|
||||
| `require`, `module`, and built-in module globals such as `fs` | ✓ | ✗ (matches vm-script, persistent vm-script) | ✗ (matches vm-script, persistent vm-script) | ✗ | ✗ | Only `import()` |
|
||||
| Relative `import()` follows later directory changes | ✓ | ✗ (matches none of node:repl, vm-script, persistent vm-script) | ✗ (matches none of node:repl, vm-script, persistent vm-script) | ✓ | ✓ | Ours keeps the directory of the environment's first call |
|
||||
| `_` and `_error` | ✓ | ✗ (matches vm-script, persistent vm-script) | ✗ (matches vm-script, persistent vm-script) | ✗ | ✗ | |
|
||||
| Code starting with `{ a: 1 }` is an object literal | ✗ | ✗ (matches node:repl, vm-script, persistent vm-script) | ✗ (matches node:repl, vm-script, persistent vm-script) | ✗ | ✗ | node:repl treats it as an object; the existing REPL evaluator forces a block |
|
||||
|
||||
**Errors**
|
||||
|
||||
| Capability | node:repl | Ours | vm-script | Persistent vm-script | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| Accurate error positions (code without `await`) | ✓ | ✓ (matches node:repl, vm-script, persistent vm-script) | ✓ | ✓ | Ours excepts the final expression's line |
|
||||
| Accurate first-line positions in code with `await` | ✗ | ✓ (matches none of node:repl, vm-script, persistent vm-script) | ✗ | ✗ | node:repl and both vm-script columns report column 22 as column 54 |
|
||||
| Syntax errors show the source line and caret | ✓ | ✓ (matches node:repl, vm-script, persistent vm-script) | ✓ | ✓ | Ours lays out acorn's position; this is evaluator behavior, and through `run_code` the host's TypeScript erasure reports most syntax errors first, matching the node:repl column |
|
||||
| Syntax error messages are V8's wording | ✓ | ✗ (matches none of node:repl, vm-script, persistent vm-script) | ✓ | ✓ | Ours uses acorn's message, and V8's wording for errors acorn accepts and V8 rejects |
|
||||
| Incomplete code gets a clear message | ✗ | ✓ (matches vm-script, persistent vm-script) | ✓ | ✓ | node:repl reports an empty message; through `run_code` the host's erasure reports it first as well |
|
||||
| Errors carry the error name and stack | ✓ | ✓ (matches node:repl, vm-script, persistent vm-script) | ✓ | ✓ | |
|
||||
| `Uncaught ` prefix on synchronous and awaited failures | ✓ | ✗ (matches vm-script, persistent vm-script) | ✗ | ✗ | |
|
||||
| `Uncaught ` prefix on asynchronous callback failures | ✓ | ✓ (matches node:repl) | — | — | Both vm-script columns end the process instead of reporting |
|
||||
|
||||
**Asynchronous failures and lifecycle**
|
||||
|
||||
| Capability | node:repl | Ours | vm-script | Persistent vm-script | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| A callback failure after an evaluation keeps the process and variables | ✓ | ✓ (matches node:repl) | — | ✗ | Persistent vm-script exits, the next call reports `worker-exit`, and every variable is lost; without persistence the process has already closed; a callback throwing a frozen object ends the process in node:repl and ours |
|
||||
| A callback failure during an evaluation fails only that evaluation and keeps variables | ✓ | ✓ (matches node:repl) | ✗ | ✗ | Both vm-script columns exit with `worker-exit`, and persistent vm-script loses its variables |
|
||||
| Output printed after an evaluation ends is discarded | ✓ | ✓ (matches node:repl, persistent vm-script) | — | ✓ | |
|
||||
| Variables can survive a timeout or cancellation | ✓ | ✓ (matches node:repl, persistent vm-script) | — | ✓ | Requires the program to acknowledge the stop |
|
||||
Sources: the node:repl, vm-script, and persistent vm-script columns come from running master's evaluators directly (Node v24.18.0). Persistent vm-script reuses one evaluator; its single-call rows equal vm-script's, and its cross-call rows and the two asynchronous rows were measured separately, the asynchronous ones through a real child process. `scoped-repl.spec.ts` tests the cross-call items of the "ours" column and `scoped-repl-evaluator.spec.ts` the rest. The two asynchronous rows of vm-script without persistence are inferred from the current implementation; node:repl, persistent vm-script, and ours share the host's timeout, cancellation, and reset behavior. The expected-implementation column was measured on that implementation with a fresh environment per case.
|
||||
|
||||
### Context and options
|
||||
|
||||
One evaluator owns one context. With the `codeGeneration` option off, the default, it is created as:
|
||||
|
||||
```ts ignore-check
|
||||
const runtimeContext = vm.createContext(Object.create(null), { codeGeneration: { strings: false, wasm: false } })
|
||||
```
|
||||
|
||||
- Node globals are copied by the same rule as `createScriptContext` in `vm-script`: every own property of the main `globalThis` that the new context lacks, with `global` pointing at the new context. The evaluator file contains its own copy of these lines and leaves [vm-script.ts](../../../../packages/ptc-runtime/ptc-runtime-node/src/evaluators/vm-script.ts) unchanged.
|
||||
- The child still writes `tools`, binding error classes, `console`, and `process` to `evaluator.globals`, which is `runtimeContext`, before each evaluation.
|
||||
- The sandbox object has no prototype, as in the vm-script and AsyncFunction evaluators, so global lookups do not reach the main realm's `Function` through a prototype chain, as `constructor.constructor("…")()` would. Main-realm values copied into the context, such as `setTimeout`, `console`, `process`, and tool namespaces, still have the main realm's `Function` as their `constructor`, and the main realm has no `codeGeneration` limits, so the option restricts only the context's own `eval`, `Function`, and WebAssembly and is no isolation; the process sandbox provides isolation.
|
||||
- Only dynamic `import()` loads modules, with no option: each Script gets `importModuleDynamically`. There is no `require`, `module`, or built-in module global; host bindings under those names stay available unchanged. Static `import` is unsupported.
|
||||
- The generated filename is `<directory>/.ptc-repl-<number>.js`; no file exists at that path, and the name starts with a dot as a hidden file. The directory is the working directory when the evaluator is created, which is the directory of the environment's first call, and never changes. Node's default loader uses the filename as the `import()` referrer, so relative imports always resolve from that directory, and the directory cannot be left out. Following later directory changes would need an explicit operation, such as `js_switch_cwd`.
|
||||
- Values from bindings and imports belong to the main realm, so cross-realm checks such as `instanceof Array` can be false, as with the REPL and `vm-script`.
|
||||
|
||||
The three options are evaluator creation parameters, not Config fields:
|
||||
|
||||
```ts ignore-check
|
||||
interface ScopedReplEvaluatorOptions extends ProgramEvaluatorOptions {
|
||||
/** Force strict mode for every program; when off, a program's own leading "use strict" decides. Defaults to true. */
|
||||
strict?: boolean
|
||||
/** Allow eval, new Function and WebAssembly compilation inside the program context; defaults to false. */
|
||||
codeGeneration?: boolean
|
||||
/** Use the last top-level expression statement as the completion value; defaults to true. */
|
||||
finalExpression?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
| Option | On | Off | Notes |
|
||||
|---|---|---|---|
|
||||
| `strict` (default on) | The whole Script starts with `"use strict";`, so every call runs strict | Not forced: when a program starts with a `"use strict"` directive, the inner function body starts with `"use strict";`, so the copy declarations cannot void it; other programs run sloppy | acorn scans with script rules in both states, and V8 reports strict-mode syntax violations at compilation. When off, in calls that run sloppy, assigning an undeclared name creates a global that every call shares without copies, top-level `this` is `globalThis`, and a name declared with a strict-mode reserved word such as `package` makes later strict calls fail to compile; see Risks |
|
||||
| `codeGeneration` (default off) | The context is created without the `codeGeneration` limits | The context is created as above | When on, a `var` declared inside `eval` is not in the scanned list and is not carried; when off, it still restricts only the context's own code generation and is no isolation |
|
||||
| `finalExpression` (default on) | The final expression becomes `return { value: (…) }` | No rewrite; the final value is discarded and the result is empty | When off, output comes only from printing; model guidance and consumers of the completion value, such as workflow-ptc, must change; top-level `return` fails in both states |
|
||||
|
||||
The integration uses the default of every option; the other values are intended uses kept by design, and each has tests.
|
||||
|
||||
### Wrapper
|
||||
|
||||
Each call compiles to its own `vm.Script`. For example, after earlier calls left `a` (`const`), `b` (`let`), and `v` (`var`), the model writes:
|
||||
|
||||
```js
|
||||
let c = a + b
|
||||
c * 2
|
||||
```
|
||||
|
||||
The compiled source is:
|
||||
|
||||
```js
|
||||
"use strict";(function (__$binding_vals, __$binding_getters) { return async function () { let a = __$binding_vals[0]; let b = __$binding_vals[1]; var v = __$binding_vals[2]; __$binding_getters.push(() => a, () => b, () => v, () => c);
|
||||
let c = a + b
|
||||
return { value: (c * 2) }
|
||||
} })
|
||||
```
|
||||
|
||||
| Part | Contents | Reason |
|
||||
|---|---|---|
|
||||
| `"use strict";` | With the `strict` option on, the whole Script is strict; when off, it is omitted, and a program that starts with `"use strict"` gets one at the start of the inner function body instead | On forces strict mode; off keeps the copy declarations from voiding the model's directive |
|
||||
| Outer `function (__$binding_vals, __$binding_getters)` | Receives the copied values and an empty array and returns the inner function; the evaluator calls it synchronously | The internal values reach the program through a closure and stay out of its `arguments`; the outer function returns before the program runs, so its frame never appears in program stacks |
|
||||
| Inner `async function ()` | The model's program is its body | Supports top-level `await`; top-level declarations are function locals, so calls never conflict; it is called without a receiver and without arguments, so `this` is `undefined` (`globalThis` when the program runs sloppy) and `arguments` is an empty arguments object |
|
||||
| Copy declarations | Generated only for names that earlier calls left and this call does not declare or declares again only with `var`; values come from `__$binding_vals` by index | Index access is unaffected by names such as `__proto__` |
|
||||
| `__$binding_getters.push(...)` | Before the model's code, pushes one read function per name, copied or declared, in the order the evaluator generated | The evaluator maps the i-th read function to the i-th name, so the prefix needs no name strings; a read function can be created early and read later, and it throws while its declaration is uninitialized |
|
||||
| Final expression | Inserts `return ` before the statement, `{ value: (` before the expression, and `) }` after it | The same handling of the last statement as Node's `processTopLevelAwait`; the box keeps the async function from awaiting a returned Promise |
|
||||
| Prefix on line 0 | Compiled with `lineOffset: -1` | Line N of the model's code reports as line N |
|
||||
| Closing on its own line | `} })` | The model's last line can end with a `//` comment |
|
||||
|
||||
A copy declaration exists when the call does not declare the name or declares it again only with `var`; when the call declares it with `let`, `const`, `class`, or `function`, there is no copy and the call's declaration wins. Copy keywords:
|
||||
|
||||
| Case | Generated copy |
|
||||
|---|---|
|
||||
| Not declared by this call; earlier `let`, `const`, `function`, or `class` | `let` |
|
||||
| Not declared by this call; earlier `var` | `var` |
|
||||
| Declared again by this call only with `var` | `var` |
|
||||
|
||||
A copy only carries the earlier value in, and writing it as `const` would only forbid later calls from assigning to it, so an earlier `const` is copied as `let` too: later calls can assign to it, changing only the copy, while it stays `const` in the call that declares it. The kernel does not check whether a call assigns the name, so it needs no syntax-tree walk and prints no warning.
|
||||
|
||||
A call that declares a name again only with `var` gets a copy too: repeated `var` declarations in one function are one variable, so that `var` starts from the earlier value and a declaration that does not run never replaces it with `undefined`, as a global `var` behaves in node:repl. A `function` of the same name in the same call initializes at the start of the function and takes precedence over the `var`, so it gets no copy.
|
||||
|
||||
Compilation and execution:
|
||||
|
||||
```ts ignore-check
|
||||
// Kernel: ScopedReplScopes.compile
|
||||
const factory = new Script(source, { filename, lineOffset: -1, ...importOption }).runInContext(context) as ScopedReplFactory
|
||||
const getters: (() => unknown)[] = []
|
||||
const program = factory(values, getters)
|
||||
this.compiled = { bindings, getters, replaced }
|
||||
return program
|
||||
|
||||
// Evaluator: evaluate
|
||||
const filename = resolve(baseDirectory, `.ptc-repl-${++submissions}.js`) // baseDirectory: process.cwd() at creation
|
||||
const program = scopes.compile(code, { filename, context, finalExpression, ...importOption })
|
||||
const start = Reflect.apply.bind(undefined, program, undefined, []) as ScopedReplProgram
|
||||
void programDomain.run(() => Promise.resolve().then(start)).then(
|
||||
(completion) => { finish({ ok: true, value: completion?.value }) },
|
||||
(error: unknown) => { finish({ ok: false, error }) },
|
||||
)
|
||||
```
|
||||
|
||||
- The kernel always compiles with `filename` and `lineOffset: -1`, and the evaluator always passes `importModuleDynamically`: `VmModuleImportLoader.forFile(filename)` with an import handler, otherwise `vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER`.
|
||||
- A microtask calls the program directly, so synchronous throws carry no evaluator frames. The host receives the result with `.then` rather than `await`, which would append a host async frame.
|
||||
- Measured: `this` is `undefined`, `arguments.length` is 0, and a synchronous throw's stack holds only the program's frame; the domain catches timer errors from programs started this way; a boxed Promise is returned without being awaited.
|
||||
- Any occurrence of `__$binding_` in the code, including strings and comments, reports `SyntaxError: Names starting with '__$binding_' are reserved` at the first occurrence, and nothing runs.
|
||||
|
||||
### Kernel: scope chain, acorn scan, and compilation
|
||||
|
||||
The kernel depends only on acorn and `node:vm`, not on the child process or the evaluator, so its tests compile and run consecutive programs in a plain vm context to check scope layering on their own. Its interface:
|
||||
|
||||
```ts ignore-check
|
||||
class ScopedReplScopes {
|
||||
constructor(options: { strict: boolean; onRedeclaration?: (redeclarations: readonly ScopedReplRedeclaration[]) => void })
|
||||
/** Scan and compile one program against the current bindings; throws SyntaxError and leaves the chain unchanged. */
|
||||
compile(code: string, options: { filename: string; context: Context; finalExpression: boolean; importModuleDynamically?: ScriptOptions['importModuleDynamically'] }): ScopedReplProgram
|
||||
}
|
||||
type ScopedReplProgram = () => Promise<{ value: unknown } | undefined>
|
||||
interface ScopedReplRedeclaration { name: string; previous: ScopedReplBindingKind; current: ScopedReplBindingKind }
|
||||
type ScopedReplBindingKind = 'var' | 'let' | 'const' | 'function' | 'class'
|
||||
interface ScopedReplBinding { name: string; kind: ScopedReplBindingKind }
|
||||
interface ScopedReplScan {
|
||||
/** Top-level bindings declared by the program, in source order. */
|
||||
declarations: ReadonlyMap<string, ScopedReplBindingKind>
|
||||
/** Whether the program starts with a "use strict" directive. */
|
||||
useStrict: boolean
|
||||
/** Last top-level statement when it is an expression statement. */
|
||||
finalExpression?: { statementStart: number; expressionStart: number; expressionEnd: number }
|
||||
}
|
||||
/** Parse once; throws a SyntaxError laid out like V8's for every error acorn reports. */
|
||||
function scanProgram(code: string, options: { filename: string }): ScopedReplScan
|
||||
/** Build the Script source and every binding in the order of the read functions it pushes. */
|
||||
function buildProgramSource(
|
||||
code: string, scan: ScopedReplScan, carried: readonly ScopedReplBinding[], options: { strict: boolean; finalExpression: boolean },
|
||||
): { source: string; bindings: ScopedReplBinding[] }
|
||||
```
|
||||
|
||||
**Parsing**: regardless of the `strict` option, the kernel calls `parse(code, { ecmaVersion: 'latest', sourceType: 'script', allowAwaitOutsideFunction: true })`. These rules mostly match an async function body: top-level `await` is valid, top-level `return` is a syntax error, and two `function` declarations of one name, or a `var` and a `function` of one name, are valid. They differ in rejecting top-level `using`, `await using`, and static `import` and `export`. The scan does not decide strictness: with the option on, the Script's leading `"use strict";` does, and V8 reports strict-mode syntax violations at compilation.
|
||||
|
||||
Before parsing, the kernel checks two things: a program that starts with `#!`, or any occurrence of `__$binding_`, reports an error, and the code is not parsed.
|
||||
|
||||
**One parse yields four things**:
|
||||
|
||||
1. The call's own top-level declarations, from top-level statements only:
|
||||
|
||||
| Top-level statement | Collected names | Recorded keyword |
|
||||
|---|---|---|
|
||||
| `const`, `let`, and `var` declarations, including destructuring (objects, arrays, defaults, rest elements) | Every bound name | The original keyword |
|
||||
| `function` declarations, including async and generator functions | The function name | `function`; a `var` of the same name in the same call is recorded as `function` too |
|
||||
| `class` declarations | The class name | `class` |
|
||||
| `var` in the head of `for (var …; …; …)` and `for (var … in/of …)` | Every bound name | `var` |
|
||||
| `var` inside blocks, `try`/`catch`/`finally`, `if`/`else`, loop bodies, `switch`, labeled statements, and `with`, including `for` heads there; nested functions and classes are skipped | Every bound name | `var` |
|
||||
|
||||
2. Whether a top-level `return` exists: acorn reports `'return' outside of function` with its position.
|
||||
3. The last statement: skipping empty statements from the end, the first statement found, if it is an expression statement, records the statement's and expression's offsets.
|
||||
4. Whether the leading directives include `"use strict"`: acorn marks directive statements with `directive`, and the kernel records `useStrict`.
|
||||
|
||||
Out of scope: no search inside nested functions and classes; inside blocks only `var` is collected, while `let`, `const`, `class`, and `function` stay in their blocks; no reference analysis and no assignment search; no rewriting besides the three insertions around the final expression.
|
||||
|
||||
**Syntax errors**: when acorn fails, the kernel throws a `SyntaxError` directly without a V8 compilation. Its `stack` follows V8's layout:
|
||||
|
||||
```text
|
||||
/work/.ptc-repl-3.js:1
|
||||
const x = ;
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected token
|
||||
```
|
||||
|
||||
- The message is acorn's text without its trailing `(line:column)`.
|
||||
- Two messages use V8's wording: `'return' outside of function` reports `Illegal return statement`, and an error positioned at the end of the code reports `Unexpected end of input`.
|
||||
- An occurrence of `__$binding_` reports an error with the same layout at its first position.
|
||||
|
||||
### Scope chain state and the steps of a call
|
||||
|
||||
`ScopedReplScopes` holds:
|
||||
|
||||
- `bindings: Map<string, { kind: ScopedReplBindingKind; read(): unknown }>`: the names available for copying and their read functions. `kind` is the keyword of the name's most recent declaration; a name that is only copied keeps its recorded keyword, so a `function` copied as `let` stays `function`.
|
||||
- `replaced: ReadonlyMap<string, { kind: ScopedReplBindingKind; value: unknown }>`: the values that names redeclared by the most recently adopted program had when it was compiled.
|
||||
- `compiled`: the binding order, read-function array, and `replaced` values of the most recently compiled program.
|
||||
|
||||
Each `compile(code, options)`:
|
||||
|
||||
1. Adopts `compiled`: when the read-function array holds at least one function per name, that program's prefix has run, so `bindings` maps the i-th name to the i-th read function and `replaced` takes its values. The child rejects overlapping evaluations (`overlapping PTC evaluation`), so the previous prefix always runs in a microtask before the next compilation; only two compilations in one synchronous run leave the first program unadopted.
|
||||
2. Scans the program for its declarations and final expression; a syntax error is thrown and the state stays unchanged.
|
||||
3. Calls every read function in `bindings` for its current value. When a read throws because its declaration never finished, the value from `replaced` is used; without either, the name is dropped.
|
||||
4. Copies names the call does not declare or declares again only with `var`, and collects their values in `values`; for existing names the call declares, records the value in the new `replaced` and a redeclaration `{ name, previous, current }`.
|
||||
5. Generates and compiles the source. With the `strict` option off and `useStrict` true, the inner function body starts with `"use strict";`. A V8 compile failure throws the native `SyntaxError` without the compiler frames after the error heading (the same handling as `vm-script`, in a separate copy) and leaves the state unchanged.
|
||||
6. Calls `onRedeclaration(redeclarations)` when there are redeclarations.
|
||||
7. Calls the outer function for the program, records `compiled`, and returns the program.
|
||||
|
||||
Each evaluator `evaluate(code, finish)`:
|
||||
|
||||
1. Increments the submission number, including for submissions with syntax errors; the filename is `<directory at creation>/.ptc-repl-<number>.js`.
|
||||
2. Calls `scopes.compile`; a thrown error ends the evaluation.
|
||||
3. Starts the program from a microtask inside the domain and calls `finish` when its Promise settles, taking `completion?.value` without further awaiting.
|
||||
|
||||
Reads happen in kernel step 3, when a new program is compiled. Later assignments by earlier code change only that code's own variables.
|
||||
|
||||
### Failed calls
|
||||
|
||||
| Call | Code | Result |
|
||||
|---|---|---|
|
||||
| 1 | `const data = 1` | `data` is 1 |
|
||||
| 2 | `const data = await tools.fetch()`, where `fetch` throws | Fails; call 2 declared `data` but never initialized it |
|
||||
| 3 | `data` | 1: reading call 2's `data` in kernel step 3 throws, so the value recorded when call 2 was compiled is used |
|
||||
| 4 | `let fresh = await tools.fetch()`, which throws | Fails; `fresh` is a new name |
|
||||
| 5 | `typeof fresh` | `"undefined"`: there is no earlier value, so the name is dropped |
|
||||
|
||||
- Declarations completed before a throw are kept. For example, after `let y = 5` runs and the call then throws, `y` is still 5 in later calls.
|
||||
- A `var` redeclaration starts from the copied earlier value, so reading it never throws and needs no fallback: an unreached `var` keeps the earlier value. For example, when call 2 throws before `var v = 6`, or the tool call in `var config = await tools.loadConfig({})` fails, later calls still read the earlier value. A `function` name holds the new function from the start of the call.
|
||||
|
||||
### Asynchronous failures: aligned with the REPL
|
||||
|
||||
The REPL catches errors from the asynchronous work a program starts with Node's `domain`. The evaluator does the same:
|
||||
|
||||
- On creation, the evaluator calls `domain.create()` for `programDomain` and starts every program inside `programDomain.run(...)`. Timers, callbacks, Promise continuations, and EventEmitters that the program creates belong to that domain.
|
||||
- `programDomain.on('error', ...)` passes `Uncaught ` followed by the error text to `options.reportError`. The text is the error's `stack`, or `name: message` when the stack is not a string; errors are not formatted with `util.inspect` because the domain adds `domain` and `domainThrown`, which inspect would print. A value that is not an error is formatted with `util.inspect` after deleting those two properties, as node:repl does.
|
||||
- Programs can make this formatting throw through a `stack` accessor, `toString`, or custom inspection. A throw from the listener would end the process, so the formatting runs in `try`/`catch` and a failure reports `program threw an unrenderable value`, the same fallback text as `prepareException` for synchronous failures. The `reportError` call stays outside the `try` because it is the child's own code, whose failures must surface.
|
||||
- When a callback throws a frozen or non-extensible object, Node's domain fails to add its properties before calling the listener, and the process ends, as with node:repl.
|
||||
- The child handles `reportError` unchanged: an active evaluation fails with the error; otherwise the error is discarded. Errors belong to the evaluation that created their callback (AsyncLocalStorage).
|
||||
- No process-wide `uncaughtException` or `unhandledRejection` handler is registered, so errors in the child's own code still surface.
|
||||
- When a program calls a tool, the asynchronous callbacks that the child creates for the call, such as RPC callbacks, also belong to the program's domain: an asynchronous error in the child's own code inside those callbacks is reported as the program's `Uncaught …` failure, and the child keeps running. The existing REPL evaluator behaves the same.
|
||||
|
||||
Verified cases:
|
||||
|
||||
- REPL: timer throws and unhandled Promise rejections are caught both after and during an evaluation; an EventEmitter `error` event is caught after an evaluation.
|
||||
- A standalone domain: in the main realm it catches timer throws, rejections created in timers, failures of unawaited async functions, and EventEmitter `error` events; in a context with the default `codeGeneration` limits it catches timer throws and rejections created in timers; timer throws from programs started by a microtask are caught too.
|
||||
|
||||
### Dynamic import
|
||||
|
||||
- A program's `import()` goes through its Script's `importModuleDynamically`: `vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER` without an import handler, and the existing `VmModuleImportLoader.forFile(filename)` with one, as in `vm-script`.
|
||||
- Relative specifiers resolve from the directory where the evaluator was created, which is the working directory of the environment's first call; later directory changes do not move it. node:repl and `vm-script` follow each call's directory; after the Agent changes directory with `working_directory`, import with an absolute path to use the new directory.
|
||||
- There is no `require`: node:repl's `require` resolves inconsistently (relative paths follow the current directory but are cached after their first resolution; packages resolve from the startup directory), so dynamic `import()` is the only way to load modules.
|
||||
|
||||
### Redeclaration notice (reserved)
|
||||
|
||||
The kernel calls `onRedeclaration` in step 6; the evaluator injects its `reportRedeclarations(redeclarations: readonly ScopedReplRedeclaration[])`, whose body stays empty with one `TODO` comment and prints nothing. A future notice writes to the program's `console` here, which makes it model-visible output.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Memory reclamation: not handled; every call copies every name. Earlier calls without surviving closures are collected; when a call leaves a surviving function, that function keeps every value copied into that call.
|
||||
- Resolving `import()` from the working directory as it changes: an explicit operation such as `js_switch_cwd` can be added when needed.
|
||||
- Reading the earlier value of a name in its own `let`, `const`, or `class` redeclaration, such as `let count = count + 1`: a program error.
|
||||
- Problems from mixing strict and sloppy calls with the `strict` option off, listed under Risks: left to a future reset tool.
|
||||
- The column shift on the final expression's line.
|
||||
|
||||
### Integration
|
||||
|
||||
The `scoped-repl` format connects the evaluator with only the changes the connection requires, without refactoring or reusing existing code, so rebases and merges with other ongoing PTC work stay simple. The program format is `scoped-repl` and the Config field is `replScoped`.
|
||||
|
||||
| Location | Change |
|
||||
|---|---|
|
||||
| `PtcProgramFormat` in `ptc-runtime` | Includes `'scoped-repl'` |
|
||||
| `config.ts` in `ptc-runtime-node` | The boolean field `replScoped`, accepted only with `persistent-in-memory` and rejected at load otherwise; when omitted, `resolveReplScoped` takes the value that `resolvePersistentExecution` writes for function bodies with `persistent-in-memory`, currently true, and false in other modes |
|
||||
| `execution.ts` in `ptc-runtime-node` | In the `persistent-in-memory` branch with `replScoped`, format `scoped-repl`, `retainsState` true, REPL-statement TypeScript erasure, and new model guidance |
|
||||
| `evaluators/index.ts` in `ptc-runtime-node` | The evaluator table maps `scoped-repl` to this evaluator with the default of all three options |
|
||||
| `protocol.ts` in `ptc-runtime-node` | The protocol version is 3, so older bootstraps fail at readiness; the upgrade guide `docs/upgrade-guide/v0.2.2-rc.1/ptc-bootstrap-protocol/` also covers the new `PtcProgramFormat` member |
|
||||
| `host.ts` in workflow-ptc | `scoped-repl` uses the same wrapper as `repl`, `await (async () => {…})()` |
|
||||
| `configuration.ts` in `ptc-runtime-node` (used by CUA to merge settings) | `resolvePersistentExecution` returns `executionMode`, `scriptPersistence`, and `replScoped` to merge together: `script` keeps retained VM Scripts with `replScoped: false`, `scoped-repl` yields `replScoped: true`, and function bodies and REPL statements yield `persistent-in-memory` with the constant `SCOPED_REPL_BY_DEFAULT`, currently true, replacing a stale profile value. That constant also sets the omitted default in `config.ts`, so changing it changes one line; `config.ts` imports this function from `configuration.ts`, which is itself a published package entry, and `configuration.ts` imports only the `Config` type, so the build emits no shared chunk |
|
||||
| Documentation, snapshots, and tests | Package README configuration, behavior, and known limitations; the config catalog; the snapshot scenario `ptc-scoped-repl`; a built-artifact e2e case with `replScoped: true` |
|
||||
|
||||
tool-executor-ptc is unchanged: for every format other than `function-body` it uses its printing guidance. The model guidance states the exact behavior above, especially: output uses `inspect(...)`, in a sentence shared with the other modes; top-level `return` is unsupported; copies happen when a call starts and earlier code is unaffected; shared mutable state belongs in objects; an assignment without declaration changes only the copy, including for earlier `const` names; a `let`, `const`, or `class` redeclaration cannot read the earlier value of its name, so assign to the name or use a new one to compute from it; a `var` redeclaration starts from the earlier value; when a call fails, an unfinished `let`, `const`, `class`, or `var` redeclaration keeps the earlier value; a `var` inside a block is also a top-level name of the call, while other block declarations stay in their block; modules load only with `import()`, whose relative specifiers resolve from the directory of the environment's first call. Tests pin the model guidance verbatim.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Nesting each call in the previous call's scope with direct eval.** Calls can share live bindings, but earlier scopes are never reclaimed, positions require a source map or stack rewriting, and direct eval is unavailable in a context that forbids code generation from strings.
|
||||
|
||||
**Rewriting declarations as `var` or global assignments.** Every call shares one binding, which violates requirement 2, and model code must be rewritten.
|
||||
|
||||
**Compiling each call as an ES module.** Each module receives values through a synthetic module for the previous call and an appended `export`. Modules allow neither `return` nor a final value, and the prelude shifts every reported line; function parameters already pass values without modules.
|
||||
|
||||
**Scope analysis with rewritten references.** Allows finer memory reclamation, but needs scope analysis and code rewriting, which violates requirement 5.
|
||||
|
||||
**Printing only, without a final expression.** workflow-ptc and the `repl` format contract depend on completion values, and it departs from the existing REPL syntax.
|
||||
|
||||
**Results from top-level `return`.** Conflicts with requirement 3, requires the function-body format, and changes the run_code guidance.
|
||||
|
||||
**Awaiting a final Promise.** Matches `vm-script` but not the existing REPL; boxing as Node does keeps the REPL behavior.
|
||||
|
||||
**Results for programs ending with `if`, `for`, or a block.** Only Script top-level code has V8 completion values, which a function body lacks; supporting them requires rewriting more statements. node:repl has no result there either when the code contains `await`.
|
||||
|
||||
**Writing the wrapper as `async function (__$binding_vals, __$binding_getters)`.** `this` is also `undefined`, but the program's `arguments` exposes both internal values and has length 3.
|
||||
|
||||
**An async arrow wrapper.** `this` is `globalThis` and `arguments` does not exist, as in the REPL; ours uses `undefined`, as a strict function call without a receiver does.
|
||||
|
||||
**Returning read functions through a callback.** The prefix would need name strings and protection against repeated calls; pushing into an array that the evaluator maps by index is simpler.
|
||||
|
||||
**Recompiling with V8 for syntax errors.** Yields V8's wording at the cost of another compilation; acorn already provides the position and message.
|
||||
|
||||
**Module rules for strict programs.** Module rules are stricter than an async function body: two `function` declarations of one name, a `var` and a `function` of one name, and `await` as a variable name in a nested ordinary function are rejected, and HTML-like comments are handled differently from V8's Script compilation. They are unusable with strict mode off, because module code is always strict.
|
||||
|
||||
**Scanning inside the same function wrapper as the runtime.** Matches the runtime grammar exactly and accepts top-level `using`, as Node's `processTopLevelAwait` does. It requires a statement walk to find top-level `return`, position mapping for the wrapper, and a guard against an extra `}` in the code closing the wrapper early; top-level `using` is not needed, so script rules are simpler.
|
||||
|
||||
**Copying only assigned `const` names as `let`, with a warning.** This walks the new call's syntax tree and tracks shadowing, which is scope analysis and violates requirement 5. Ours copies every `const` as `let` without a notice.
|
||||
|
||||
**Starting a `var` redeclaration from `undefined` without a copy.** `var count = count + 1` then yields `NaN`, but a `var` not reached before a throw or in a branch that is not taken replaces the earlier value with `undefined`; for example, a failed tool call in `var config = await tools.loadConfig({})` leaves later calls without the earlier `config`. With the copy, `var count = count + 1` adds to the earlier value, as in node:repl, which is accepted for now.
|
||||
|
||||
**Falling back only when a call failed and the value reads `undefined`.** It needs no copy, but cannot tell an unreached declaration from one that ran and assigned `undefined`: a call that assigns `undefined` and then fails falls back by mistake, and an untaken branch or `var x;` without an initializer in a successful call is not covered.
|
||||
|
||||
**Marking in the code whether each `var` ran.** It distinguishes the cases exactly but rewrites model code at declarations, earlier assignments, and loop heads, which violates requirement 5.
|
||||
|
||||
**Skipping copies of strict-mode reserved words in strict calls.** It would keep names such as `package`, declared in a sloppy call, from breaking later strict calls; the problem comes from the model mixing both modes, so it is left to a future reset tool.
|
||||
|
||||
**A process-wide handler for uncaught errors.** It would also swallow errors from the child's own code; the REPL's domain covers only the asynchronous work programs start.
|
||||
|
||||
**Not collecting `var` declarations inside blocks.** A block-level `var` then collides with the copy declarations, so the whole program reports `already declared`, and the name is not carried to later calls; collecting it takes about 15 lines of statement traversal without reference analysis, so the scanner collects it.
|
||||
|
||||
**Showing only `.ptc-repl-<number>.js` in stacks, without the directory.** Without the directory, Node's default loader resolves relative `import()` specifiers from the current directory at import time, unlike calls with an import handler; a `//# sourceURL` comment that shows only the name makes V8 ignore `lineOffset`, so every line number grows by 1; handing imports to a callback would work, because both child launches pass `--experimental-vm-modules` and the callback can start `import()` from the first call's directory, but it requires scoped REPL imports to always go through a callback, which the default path does not do yet. The filename therefore keeps the directory of the first call for now.
|
||||
|
||||
**Providing `require`, `module`, and built-in module globals, as node:repl does.** node:repl's `require` resolves inconsistently, and following directory changes needs a rule for when the base moves; only `import()` remains.
|
||||
|
||||
## Consequences
|
||||
|
||||
Gains:
|
||||
|
||||
- Later calls may declare an earlier name again with any keyword, while earlier functions, closures, and still-running code keep their values.
|
||||
- Syntax, results, errors, and asynchronous failure handling match the existing REPL, and asynchronous program failures do not end the process.
|
||||
- There is no scope analysis, and model code is rewritten only at the three insertion points of the final expression.
|
||||
|
||||
Costs:
|
||||
|
||||
- Copying at call start differs from the REPL: assignments that earlier code, such as timers and callbacks, makes to top-level variables afterwards are invisible to later calls; the model guidance states this.
|
||||
- An assignment without declaration affects only later calls and is invisible to earlier functions; in the REPL both sides change.
|
||||
- Strict mode differs from the REPL: assigning an undeclared name creates a global in the REPL and throws here.
|
||||
- For callers that keep completions, a final Promise without `await` makes the host report a non-JSON completion, as in the existing REPL; `run_code` discards completions and is unaffected.
|
||||
- Programs ending with `if`, `for`, or a block have no result, while node:repl has one when the code contains no `await`.
|
||||
- On the final expression's line, columns after the insertion point shift right by 17.
|
||||
- Syntax error messages use acorn's text, which differs from V8's, for example `Unexpected token` instead of `Unexpected token ';'`.
|
||||
- Scanning always uses script rules, so top-level `using` and `await using` are rejected; node:repl does not support them either.
|
||||
- Top-level `arguments` is an empty arguments object, while it does not exist in node:repl.
|
||||
- The context forbids code generation from strings by default: code relying on `eval` or `new Function` fails, including libraries loaded into this context through an import handler. The limit is no isolation, because copied main-realm functions such as `setTimeout.constructor` still generate code.
|
||||
- There is no `require`: code that relies on it must use `await import()` instead.
|
||||
- Relative `import()` keeps resolving from the directory of the environment's first call, also after the Agent changes directory.
|
||||
- Node documents `domain` as deprecated; node:repl still builds on it, so a removal would change the REPL as well.
|
||||
- An error in the child's own code inside a tool call's asynchronous callbacks is reported as the program's `Uncaught …` failure, and the child keeps running, as with the existing REPL.
|
||||
- Programs that start with `#!` are rejected.
|
||||
- Any occurrence of `__$binding_`, including in strings and comments, is rejected; internal names written with escapes such as `\u005f` are not checked.
|
||||
- acorn must keep pace with V8's syntax, or newer syntax is rejected before it runs.
|
||||
- A `var` redeclaration starts from the earlier value: `var count = count + 1` adds to it, as in node:repl, also when the earlier declaration was `let` or `const`, where node:repl reports `already declared`.
|
||||
- A `let`, `const`, or `class` redeclaration cannot read the earlier value of its name and throws, as in `let count = count + 1`. This is a program error and is not handled.
|
||||
- Every earlier `const` is copied as `let` into later calls, which can assign to it without any notice.
|
||||
- With the `strict` option off, strict and sloppy calls can mix: a sloppy call that assigns an undeclared name creates a global shared by every call and earlier function, and a name declared in a sloppy call with a strict-mode reserved word (`package`, `public`, `private`, `protected`, `interface`, `implements`, `static`, `let`, `yield`, `eval`, `arguments`) makes later strict calls fail to compile. This is not handled and is left to a future reset tool.
|
||||
- When a callback throws a frozen or non-extensible object, Node's domain handling fails and the process ends, losing retained variables, as with node:repl.
|
||||
- Every call generates a copy declaration and a read function for every earlier name: the prototype measured about 10 ms per call with 1000 names.
|
||||
- When a call leaves a surviving function, every value copied into that call stays in memory.
|
||||
|
||||
## Testing
|
||||
|
||||
- The kernel tests pass on their own and cover script-rule parsing and detection of a leading `"use strict"`, every statement in the declaration table, top-level `return` including inside blocks, locating and boxing the final expression, programs that start with `#!`, occurrences of `__$binding_`, the syntax-error layout with both message substitutions, and the generated source; with the `strict` option off, a leading `"use strict"` takes effect both with and without copies; running consecutive programs in a plain vm context, they cover every item under Across calls, including keyword combinations, failed calls, timers, function redefinition, and redeclaration notices.
|
||||
- The evaluator tests use the evaluator and a real vm context and cover every item under Syntax and results, Environment, Errors, and Asynchronous failures and lifecycle, plus cross-call behavior through the evaluator, including that the global object's prototype chain does not reach the main realm's `Function` while copied main-realm functions such as `setTimeout.constructor` still generate code, and that host bindings named `module` or `require` stay untouched.
|
||||
- Every option is tested on and off.
|
||||
- Top-level `this` is `undefined`, `arguments.length` is 0, and a synchronous throw's stack holds only the program's frames.
|
||||
- Asynchronous failures: each listed case is tested after and during an evaluation; the text passed to `reportError` starts with `Uncaught `; the process keeps running and variables are retained. A missing `stack`, a throwing `stack` accessor, and throwing custom inspection report `name: message` or the fixed text without ending the process; a thrown plain object is reported without `domainThrown`.
|
||||
- Imports: both the default loader and an import handler; relative specifiers resolve from the directory at creation, also after the working directory changes.
|
||||
- The built-artifact e2e covers `replScoped: true`.
|
||||
- Tests that mount `persistent-in-memory` set `replScoped` explicitly, and retained-execution tests run both values; one test checks the default when it is omitted.
|
||||
- New files have 100% coverage; existing evaluators and their tests are unchanged.
|
||||
@@ -0,0 +1,524 @@
|
||||
# Agent Note: 保留状态 PTC 的 scoped REPL 求值器
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-09-retained-ptc-scoped-repl.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
`persistent-in-memory` 用 node:repl 在同一个全局作用域里执行同一个 Agent 的每次 `run_code`([尽力保留 PTC 状态](../../implemented/architecture/2026-09-23-best-effort-persistent-ptc.zh.md))。模型在后面的调用里再次声明前面用过的 `const`、`let`、`class` 名字时,V8 在执行前就报 `SyntaxError: Identifier 'x' has already been declared`,整段代码一行都不执行。模型说明只能要求"不要重复声明",但模型在多轮调用里很难记住每个名字。
|
||||
|
||||
把声明改成 `var` 能消除报错,但所有调用共用同一个绑定,后面的声明会改掉前面的函数、闭包和后台任务读到的值:
|
||||
|
||||
```js
|
||||
// call 1
|
||||
var value = 1
|
||||
var read = () => value
|
||||
// call 2
|
||||
var value = 2 // no error
|
||||
// call 3
|
||||
read() // 2: call 2 changed the read written by call 1
|
||||
```
|
||||
|
||||
需要同时满足五条要求:
|
||||
|
||||
1. 后面的调用可以用任意关键字再次声明前面的名字,不报错。
|
||||
2. 再次声明只对这次和之后的调用生效;前面的函数、闭包和仍在运行的代码继续使用它们原来的值。
|
||||
3. 写法与现有 REPL 一致:支持顶层 `await`,最后一个表达式的值是结果,不支持顶层 `return`。
|
||||
4. 异步回调里的错误与现有 REPL 一样处理:进程不退出,变量保留。
|
||||
5. 不做作用域分析,不改写模型代码里的声明和引用;acorn 只用来列出顶层声明、找到最后一条语句。
|
||||
|
||||
## 决策
|
||||
|
||||
scoped REPL 求值器分成内核和求值器两个文件:
|
||||
|
||||
| 文件 | 内容 |
|
||||
|---|---|
|
||||
| `packages/ptc-runtime/ptc-runtime-node/src/evaluators/scoped-repl.ts` | 内核:作用域链 `ScopedReplScopes`,负责状态表、拷贝与退回旧值、重复声明、编译成 Script;以及 acorn 扫描、顶层声明与关键字、顶层 `return` 检查、最后一条表达式、保留名检查、语法错误格式化、拼源码 |
|
||||
| `packages/ptc-runtime/ptc-runtime-node/src/evaluators/scoped-repl-evaluator.ts` | 求值器:导出 `createScopedReplEvaluator(options)`,实现现有的 `ProgramEvaluator` 接口,把内核接入现有模式:执行环境和开关、动态 import 与 import 处理器、由微任务启动程序并取完成值、domain、注入 `reportRedeclarations` |
|
||||
| `packages/ptc-runtime/ptc-runtime-node/tests/scoped-repl.spec.ts` | 只测内核,可以单独运行:扫描、拼源码,以及在普通 vm context 里连续执行多段程序的作用域分层 |
|
||||
| `packages/ptc-runtime/ptc-runtime-node/tests/scoped-repl-evaluator.spec.ts` | 测接入层:完成值、执行环境、模块与 import、报错、异步错误;三个开关打开、关闭两种状态都测 |
|
||||
|
||||
求值器以 `scoped-repl` 格式接入现有执行模式,见"接入"。
|
||||
|
||||
### 精确行为
|
||||
|
||||
#### 写法与结果
|
||||
|
||||
- 程序是一组语句,支持顶层 `await`。
|
||||
- 最后一条非空的顶层语句是表达式语句时,它的值就是结果。值是 Promise 时不等待,Promise 本身就是结果,宿主会报 `program completion must be lossless JSON`,和现有 REPL 一样。这一项由 `finalExpression` 开关控制,默认开;关闭后末尾表达式的值被丢弃,结果只能靠打印,模型说明要随之调整。`run_code` 对 TypeScript 运行时传 `discardCompletion: true`,宿主丢弃完成值,模型用 `inspect(...)` 输出;不丢弃完成值的调用方(例如 workflow-ptc)仍以末尾表达式的值为结果。
|
||||
- 最后一条是声明、`if`、`for`、块等其他语句时,没有结果。
|
||||
- 顶层 `return`,包括写在顶层 `if`、循环和块里的,报 `SyntaxError: Illegal return statement`,这次一行都不执行。经 `run_code` 执行时,宿主的 TypeScript 擦除会先拒绝它,报 `Return statement is not allowed here`,和现有 REPL 相同。
|
||||
- 静态 `import`、`export` 报 acorn 的 `SyntaxError: 'import' and 'export' may appear only with 'sourceType: module'`;顶层 `using`、`await using` 同样被 acorn 拒绝,块里和函数里可以用。
|
||||
- 以 `#!` 开头的程序报 `SyntaxError: Hashbang lines are not supported`,这次不执行:`#!` 只能出现在 Script 的最开头,而生成的前缀排在程序前面。
|
||||
- 输出是 `inspect(...)` 等打印的内容;调用方不丢弃完成值时,再加上末尾表达式的值。
|
||||
|
||||
#### 跨次调用
|
||||
|
||||
- 每次调用开始时,把前面留下的每个顶层名字拷贝成本次的同名声明;对象按引用共享,修改对象内部两边都看得到。
|
||||
- 可以用任意关键字再次声明前面的名字,包括 `var`→`let`→`const`→`class`→`function` 依次换。本次用 `let`、`const`、`class`、`function` 声明的名字不再拷贝,以本次为准;只用 `var` 重新声明的名字照样拷贝,这次的 `var` 从旧值开始。
|
||||
- 不声明、直接给前面的名字赋值,改的是本次的拷贝:之后的调用看到新值,前面定义的函数仍看到旧值。前面用 `const` 声明的名字也一样,它只在声明它的那次调用里不能赋值。
|
||||
- 重定义函数后,前面定义的调用方仍然调用旧函数。
|
||||
- 本次开始以后,前面的代码(定时器、回调)再给顶层变量赋值,之后的调用看不到;需要共享的可变状态放进对象。
|
||||
- 前面定义的函数看不到后面才声明的名字,引用时报 `ReferenceError`。
|
||||
- 一次调用用 `let`、`const`、`class` 重新声明了某个名字,这次调用里的所有引用都指向新声明,包括写在声明之前的,拿不到旧值:在声明执行前读取报原生的 `ReferenceError: Cannot access 'x' before initialization`。这类写法是代码本身的问题,不做处理。用 `var` 重新声明时,这次的 `var` 从旧值开始,直到被赋上新值,例如 `var count = count + 1` 在旧值上加 1;`function` 从调用开始就是新函数。
|
||||
- 一次调用中途出错时:抛错前已经执行完的声明保留;重新声明了已有名字、但没来得及赋值的,之后的调用退回旧值;没来得及赋值的新名字直接丢掉。
|
||||
- 块里(包括 `try`/`catch`/`finally`、`if`/`else`、循环体、`switch`、标签语句)声明的 `var` 会提升到这次调用的顶层,和顶层 `var` 一样算这次的声明:带到之后的调用,也可以和前面任何关键字声明的名字同名。嵌套的函数和类里的 `var` 不算。
|
||||
- 没执行到的 `var` 再次声明,不论在抛错之后还是在没走到的分支里,都保留旧值;不带初始值的 `var x;` 也保留旧值。
|
||||
|
||||
#### 执行环境
|
||||
|
||||
下面是默认行为,前两项各由一个开关控制,见"执行环境与开关"。
|
||||
|
||||
- strict 模式:给未声明的名字赋值报 `ReferenceError`;有意的全局变量写 `globalThis.x`。开关关闭时不强制:程序以 `"use strict"` 开头,这次就按严格执行,否则按宽松执行。
|
||||
- 禁止从字符串生成代码:执行环境自己的 `eval`、`new Function` 报 `EvalError`,从字节编译 WebAssembly 报 `CompileError`。这不构成隔离:从主环境复制进来的函数(例如 `setTimeout.constructor`)仍能在主环境里生成代码。
|
||||
- 只能用动态 `import()` 加载模块,相对路径按这个环境第一次调用时的 working directory 解析,之后的调用换了目录也不变;不提供 `require`、`module` 和 `fs`、`path` 这类内置模块全局。读写文件的相对路径仍跟随当前目录。
|
||||
- 顶层 `this` 是 `undefined`,按宽松执行时是 `globalThis`;顶层 `arguments` 是一个空的 arguments 对象,长度为 0。
|
||||
- 顶层 `var` 和 `function` 不会成为 `globalThis` 的属性。
|
||||
- `setTimeout`、`Buffer`、`fetch`、`URL`、`structuredClone` 等 Node 全局对象可用。
|
||||
|
||||
#### 报错
|
||||
|
||||
- 运行时报错的行号、列号是模型代码里的原生位置,跨次调用定义的函数显示它自己那次的文件名,例如 `<目录>/.ptc-repl-1.js:2:9`。最后一条表达式所在的那一行,插入点之后的列号右移 17,这次不处理。
|
||||
- acorn 发现的语法错误直接报 acorn 的消息和位置,按 V8 的样式排版:文件名加行号、源码行、箭头。有两处消息改用 V8 的说法:顶层 `return` 报 `Illegal return statement`;报错位置在代码末尾时报 `Unexpected end of input`。
|
||||
- acorn 放行、V8 编译时才发现的错误,报 V8 原生的消息;strict 开关打开时,严格模式的语法违规(例如 `with`)也在这一步报出。
|
||||
- 以上是求值器自己的行为。经 `run_code` 执行时,宿主的 TypeScript 擦除先解析整段代码,多数语法错误由它报告,和现有 REPL 相同;只有擦除接受、扫描拒绝的代码(例如保留名、静态 `import`、顶层 `using`)才会看到上面的版式。
|
||||
- 运行时错误报错误名加堆栈,没有 `Uncaught ` 前缀,堆栈里没有求值器自己的帧。
|
||||
|
||||
#### 异步错误与生命周期
|
||||
|
||||
- 程序启动的定时器、回调、Promise 后续步骤和 EventEmitter 抛出的错误,以及未处理的 rejection,不让进程退出,变量保留。例外:回调里 `throw` 的是冻结或不可扩展的对象时,Node 的 domain 在通知监听器之前给它加属性失败,进程结束,和 node:repl 相同;以这样的对象 reject 不受影响。
|
||||
- 当时有执行在运行,就让那次执行以 `Uncaught ` 加错误文本失败;否则丢弃。错误文本取错误的 `stack`,`stack` 不是字符串时用"名字: 消息";不是 Error 的值,先去掉 domain 加上的 `domain`、`domainThrown` 再用 `util.inspect`;转换出错时报 `program threw an unrenderable value`。错误按创建回调时所属的执行归属,前一次留下的定时器在后一次运行时抛错,不会算到后一次头上。
|
||||
- 执行结束后才打印的输出被丢弃;超时、取消和重置沿用现有的宿主逻辑([保留状态的 PTC 中断](../../implemented/architecture/2026-10-29-retained-ptc-interruption.zh.md))。
|
||||
|
||||
### 与 node:repl、vm-script 的对比
|
||||
|
||||
✓ 表示支持,✗ 表示不支持,"—" 表示不适用:不开持久化的 `vm-script` 每次调用都是新进程,没有跨次状态。"持久化 vm-script"指设置 `scriptPersistence: true` 的 vm-script(#7992),"两种 vm-script"指它和不开持久化的 vm-script。"我们"一列是默认行为,括号里写和哪几列一致;执行环境一组把"我们"分成 strict 开启(默认)和关闭两列。跨次调用一组多列一项"预期支持跨次调用能力的一种实现",括号和备注里简称"预期实现"。
|
||||
|
||||
**写法与结果**
|
||||
|
||||
| 能力 | node:repl(现有 REPL 求值器) | 我们 | vm-script(不开持久化) | vm-script(开持久化,#7992) | 备注 |
|
||||
|---|---|---|---|---|---|
|
||||
| 顶层 `return`,包括写在 `if` 块里的 | ✗ | ✗(与 node:repl、vm-script、持久化 vm-script 一致) | ✗ | ✗ | 都报 `Illegal return statement` |
|
||||
| 末尾表达式作为结果 | ✓ | ✓(默认;与 node:repl、vm-script、持久化 vm-script 一致) | ✓ | ✓ | 可以用 `finalExpression` 开关关闭 |
|
||||
| 末尾是 Promise 时等它完成 | ✗ | ✗(与 node:repl 一致) | ✓ | ✓ | 不等时宿主报"不是 JSON" |
|
||||
| 以 `if`、`for`、块结尾时有结果 | ✓ | ✗(与 node:repl、vm-script、持久化 vm-script 含 `await` 时一致) | ✓ | ✓ | node:repl 和两种 vm-script 只在代码不含 `await` 时有结果 |
|
||||
| 顶层 `await` | ✓ | ✓(与 node:repl、vm-script、持久化 vm-script 一致) | ✓ | ✓ | node:repl 和两种 vm-script 靠改写整段代码;我们只改写最后一条表达式 |
|
||||
| 静态 `import` | ✗ | ✗(与 node:repl、vm-script、持久化 vm-script 一致) | ✗ | ✗ | 我们报 acorn 的消息 |
|
||||
| 顶层 `using` | ✗ | ✗(与 node:repl、vm-script、持久化 vm-script 一致) | ✗ | ✗ | 块里和函数里可以用 |
|
||||
| 以 `#!` 开头的程序 | ✓ | ✗(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✓ | ✓ | 我们报 `Hashbang lines are not supported` |
|
||||
| 同一次里两个同名 `function`,或同名的 `var` 和 `function` | ✓ | ✓(与 node:repl、vm-script、持久化 vm-script 一致) | ✓ | ✓ | |
|
||||
| 动态 `import()` | ✓ | ✓(与 node:repl、vm-script、持久化 vm-script 一致) | ✓ | ✓ | |
|
||||
|
||||
**跨次调用**
|
||||
|
||||
| 能力 | node:repl | 预期支持跨次调用能力的一种实现 | 我们 | vm-script | 持久化 vm-script | 备注 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 跨次保留变量 | ✓ | ✓ | ✓(与 node:repl、预期实现、持久化 vm-script 一致) | ✗ | ✓ | node:repl 和持久化 vm-script 共用一个作用域;我们靠拷贝;预期实现也靠拷贝 |
|
||||
| 用任意关键字再次声明 | ✗ | ✓ | ✓(与预期实现一致) | — | ✗ | node:repl 和持久化 vm-script 的 25 种关键字组合里 21 种报 `already declared`,只有 `var`、`function` 之间可以;我们预留重复声明提示,暂不输出;预期实现对 `const` 再次声明为 `const` 多输出一行警告 |
|
||||
| 后面不声明直接赋值,前面定义的函数不受影响 | ✗ | ✓ | ✓(与预期实现一致) | — | ✗ | |
|
||||
| 不重新声明,直接给前面的 `const` 赋值 | ✗ | ✓ | ✓(与预期实现一致) | — | ✗ | node:repl 和持久化 vm-script 报 `TypeError`;我们不提示;预期实现先打印警告 |
|
||||
| 用 `var` 重新声明时读到同名旧值(`var n = n + 1`) | ✓ | ✗ | ✓(与 node:repl、持久化 vm-script 一致) | — | ✓ | node:repl 和持久化 vm-script 只在前面也是 `var` 时成立,前面是 `let` 时报 `already declared`;我们前面是 `let`、`const` 时也成立;预期实现得到 `NaN` |
|
||||
| 重定义函数后,旧的调用方仍调旧函数 | ✗ | ✓ | ✓(与预期实现一致) | — | ✗ | |
|
||||
| 前面的代码之后改的值,后面的调用能看到 | ✓ | ✗ | ✗(与预期实现一致) | — | ✓ | 定时器自增的变量,间隔 30 ms 读两次:node:repl 23、50;持久化 vm-script 22、44;我们 0、0;预期实现 15、15 |
|
||||
| 前面定义的函数能看到后面才声明的名字 | ✓ | ✗ | ✗(与预期实现一致) | — | ✓ | 我们报 `ReferenceError` |
|
||||
| 修改对象内部,两边都看得到 | ✓ | ✓ | ✓(与 node:repl、预期实现、持久化 vm-script 一致) | — | ✓ | |
|
||||
| 初始化抛错后,这个名字还能继续用 | ✗ | ✓ | ✓(与预期实现一致) | — | ✗ | node:repl 和持久化 vm-script 里新名字永久不可用,再次声明报 `already declared`;我们已有名字退回旧值,新名字丢掉 |
|
||||
| 抛错前已经执行完的声明会保留 | ✓ | ✓ | ✓(与 node:repl、预期实现、持久化 vm-script 一致) | — | ✓ | |
|
||||
| 抛错前没执行到的 `var` 再次声明,保留旧值 | ✓ | ✓ | ✓(与 node:repl、预期实现、持久化 vm-script 一致) | — | ✓ | 我们把旧值拷进这次的 `var`,没走到的分支里的 `var` 也保留旧值;预期实现靠插入标记代码做到 |
|
||||
| 块里新声明的 `var` 带到下一次 | ✓ | ✗ | ✓(与 node:repl、持久化 vm-script 一致) | — | ✓ | 我们收集块里的 `var` |
|
||||
| 块里的 `var` 和已有的 `let` 同名时不报错 | ✗ | ✗ | ✓(与 node:repl、预期实现、持久化 vm-script 都不一致) | — | ✗ | node:repl 和持久化 vm-script 报 `already declared`;我们把它算作这次的新声明;预期实现也报 `already declared` |
|
||||
|
||||
**执行环境**
|
||||
|
||||
| 能力 | node:repl | 我们(strict,默认) | 我们(非 strict) | vm-script | 持久化 vm-script | 备注 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| strict 模式 | ✗ | ✓(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✗(与 node:repl、vm-script、持久化 vm-script 一致) | ✗ | ✗ | strict 关闭时,程序自己开头的 `"use strict"` 仍然生效 |
|
||||
| 给未声明的名字赋值会创建全局变量 | ✓ | ✗(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✓(与 node:repl、vm-script、持久化 vm-script 一致) | ✓ | ✓ | 非 strict 时创建的全局变量由所有调用和前面的函数共用,不走拷贝 |
|
||||
| 顶层 `var`、`function` 挂到 `globalThis` | ✓ | ✗(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✗(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✓ | ✓ | |
|
||||
| 顶层 `this` 是 `globalThis` | ✓ | ✗(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✓(与 node:repl、vm-script、持久化 vm-script 一致) | ✓ | ✓ | 我们 strict 时是 `undefined` |
|
||||
| 顶层 `arguments` | ✗ | ✓(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✓(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✗ | ✗ | 我们的是空的 arguments 对象 |
|
||||
| `eval`、`new Function` | ✓ | ✗(默认;与 node:repl、vm-script、持久化 vm-script 都不一致) | ✗(默认;与 node:repl、vm-script、持久化 vm-script 都不一致) | ✓ | ✓ | 可打开;只限执行环境自己的,复制进来的主环境函数仍能生成代码 |
|
||||
| 编译 WebAssembly | ✓ | ✗(默认;与 node:repl、vm-script、持久化 vm-script 都不一致) | ✗(默认;与 node:repl、vm-script、持久化 vm-script 都不一致) | ✓ | ✓ | 与 `eval` 同一个开关 |
|
||||
| Node 全局对象(`setTimeout`、`Buffer`、`fetch`、`URL`、`structuredClone`) | ✓ | ✓(与 node:repl、vm-script、持久化 vm-script 一致) | ✓(与 node:repl、vm-script、持久化 vm-script 一致) | ✓ | ✓ | |
|
||||
| `require`、`module`、`fs` 等内置模块全局 | ✓ | ✗(与 vm-script、持久化 vm-script 一致) | ✗(与 vm-script、持久化 vm-script 一致) | ✗ | ✗ | 只能用 `import()` |
|
||||
| 相对路径的 `import()` 跟随之后的目录变化 | ✓ | ✗(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✗(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✓ | ✓ | 我们固定用这个环境第一次调用时的目录 |
|
||||
| `_`、`_error` | ✓ | ✗(与 vm-script、持久化 vm-script 一致) | ✗(与 vm-script、持久化 vm-script 一致) | ✗ | ✗ | |
|
||||
| 以 `{ a: 1 }` 开头的代码当对象字面量 | ✗ | ✗(与 node:repl、vm-script、持久化 vm-script 一致) | ✗(与 node:repl、vm-script、持久化 vm-script 一致) | ✗ | ✗ | node:repl 原本当对象,现有 REPL 求值器强制改成语句块 |
|
||||
|
||||
**报错**
|
||||
|
||||
| 能力 | node:repl | 我们 | vm-script | 持久化 vm-script | 备注 |
|
||||
|---|---|---|---|---|---|
|
||||
| 报错行列号准确(代码不含 `await`) | ✓ | ✓(与 node:repl、vm-script、持久化 vm-script 一致) | ✓ | ✓ | 我们的例外是最后一条表达式所在行 |
|
||||
| 报错行列号准确(代码含 `await` 时的第 1 行) | ✗ | ✓(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✗ | ✗ | node:repl 和两种 vm-script 把第 22 列报成第 54 列 |
|
||||
| 语法错误带源码行和箭头 | ✓ | ✓(与 node:repl、vm-script、持久化 vm-script 一致) | ✓ | ✓ | 我们按 acorn 给出的位置自己排版;这是求值器层的行为,经 `run_code` 时多数语法错误先由宿主的 TypeScript 擦除报告,与 node:repl 列相同 |
|
||||
| 语法错误消息是 V8 的原文 | ✓ | ✗(与 node:repl、vm-script、持久化 vm-script 都不一致) | ✓ | ✓ | 我们用 acorn 的消息,acorn 放行、V8 拒绝的错误用 V8 原文 |
|
||||
| 代码不完整时有明确提示 | ✗ | ✓(与 vm-script、持久化 vm-script 一致) | ✓ | ✓ | node:repl 的报错消息为空;经 `run_code` 时同样先由宿主擦除报告 |
|
||||
| 报错带错误名和堆栈 | ✓ | ✓(与 node:repl、vm-script、持久化 vm-script 一致) | ✓ | ✓ | |
|
||||
| 同步错误和 `await` 失败带 `Uncaught ` 前缀 | ✓ | ✗(与 vm-script、持久化 vm-script 一致) | ✗ | ✗ | |
|
||||
| 异步回调错误带 `Uncaught ` 前缀 | ✓ | ✓(与 node:repl 一致) | — | — | 两种 vm-script 的进程直接退出,没有这类报告 |
|
||||
|
||||
**异步错误与生命周期**
|
||||
|
||||
| 能力 | node:repl | 我们 | vm-script | 持久化 vm-script | 备注 |
|
||||
|---|---|---|---|---|---|
|
||||
| 执行结束后回调里出错,进程不崩、变量保留 | ✓ | ✓(与 node:repl 一致) | — | ✗ | 持久化 vm-script 的进程退出,下一次调用报 `worker-exit`,变量全部丢失;不开持久化时执行结束后进程已关闭;回调里 `throw` 冻结的对象时,node:repl 和我们的进程都会结束 |
|
||||
| 执行中回调里出错,只让这次失败、变量保留 | ✓ | ✓(与 node:repl 一致) | ✗ | ✗ | 两种 vm-script 的进程都退出,报 `worker-exit`,持久化 vm-script 的变量丢失 |
|
||||
| 执行结束后才打印的输出被丢弃 | ✓ | ✓(与 node:repl、持久化 vm-script 一致) | — | ✓ | |
|
||||
| 超时或取消后变量可以保留 | ✓ | ✓(与 node:repl、持久化 vm-script 一致) | — | ✓ | 前提是程序确认已经停住 |
|
||||
数据来源:node:repl、vm-script 和持久化 vm-script 三列直接调用 master 的求值器实测(Node v24.18.0)。持久化 vm-script 用同一个求值器连续执行;一次调用内的各行与 vm-script 相同,跨次调用各行和两行异步错误单独实测,异步错误经真实子进程。"我们"一列的跨次调用各项由 `scoped-repl.spec.ts` 测试,其余各项由 `scoped-repl-evaluator.spec.ts` 测试。不开持久化的 vm-script 两行异步错误按现有实现推断;node:repl、持久化 vm-script 和我们的超时、取消、重置沿用同一套宿主逻辑。预期实现一列在该实现上实测,每个用例新开一个环境。
|
||||
|
||||
### 执行环境与开关
|
||||
|
||||
一个求值器只有一个执行环境。`codeGeneration` 开关关闭(默认)时这样创建:
|
||||
|
||||
```ts ignore-check
|
||||
const runtimeContext = vm.createContext(Object.create(null), { codeGeneration: { strings: false, wasm: false } })
|
||||
```
|
||||
|
||||
- Node 全局对象按 `vm-script` 的 `createScriptContext` 同样的规则复制进来:主环境 `globalThis` 上、新环境没有的自有属性逐个复制,`global` 指向新环境自身。这几行在求值器文件里单独写一份,不改 [vm-script.ts](../../../../packages/ptc-runtime/ptc-runtime-node/src/evaluators/vm-script.ts)。
|
||||
- `tools`、绑定错误类、`console`、`process` 仍由子进程在每次执行前写到 `evaluator.globals`,也就是 `runtimeContext`。
|
||||
- 沙箱对象没有原型,和 vm-script、AsyncFunction 两个求值器一样,全局查找因此不会经原型链走到主环境的 `Function`,例如 `constructor.constructor("…")()`。但复制进来的主环境值(`setTimeout`、`console`、`process`、工具命名空间等)的 `constructor` 仍是主环境的 `Function`,主环境没有 `codeGeneration` 限制,所以这个开关只限制执行环境自己的 `eval`、`Function` 和 WebAssembly,不构成隔离;隔离靠进程级沙箱。
|
||||
- 只提供动态 `import()`,没有开关:每个 Script 都配 `importModuleDynamically`。不提供 `require`、`module` 和内置模块全局;宿主以这些名字注入的绑定原样可用。静态 `import` 不支持。
|
||||
- 生成的文件名是 `<目录>/.ptc-repl-<序号>.js`,这个路径上没有真实文件,文件名用点开头的隐藏名。目录是求值器创建时的 working directory,也就是这个环境第一次调用时的目录,之后不再改变;默认加载器把文件名当作 `import()` 的引用方,所以相对路径的 import 固定按这个目录解析,目录部分不能省。以后需要跟随目录变化时,另加显式操作(例如 `js_switch_cwd`)。
|
||||
- 绑定和 import 返回的值属于主环境,`instanceof Array` 这类跨环境判断可能为 false,和 REPL、`vm-script` 相同。
|
||||
|
||||
三个开关是求值器的创建参数,不是 Config 字段:
|
||||
|
||||
```ts ignore-check
|
||||
interface ScopedReplEvaluatorOptions extends ProgramEvaluatorOptions {
|
||||
/** Force strict mode for every program; when off, a program's own leading "use strict" decides. Defaults to true. */
|
||||
strict?: boolean
|
||||
/** Allow eval, new Function and WebAssembly compilation inside the program context; defaults to false. */
|
||||
codeGeneration?: boolean
|
||||
/** Use the last top-level expression statement as the completion value; defaults to true. */
|
||||
finalExpression?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
| 开关 | 打开时 | 关闭时 | 需要注意 |
|
||||
|---|---|---|---|
|
||||
| `strict`(默认开) | 整个 Script 以 `"use strict";` 开头,每次调用都按严格执行 | 不强制:程序以 `"use strict"` 指令开头时,在内层函数体最前面补一个 `"use strict";`,拷贝语句不会让它失效;否则按宽松执行 | 两种状态下 acorn 都按 script 规则扫描,严格模式的语法违规由 V8 编译时报出。关闭后,按宽松执行的调用里:给未声明的名字赋值会创建所有调用共用的全局变量,不走拷贝;顶层 `this` 是 `globalThis`;用严格模式保留字(如 `package`)声明的名字会让之后严格的调用编译失败,见"风险" |
|
||||
| `codeGeneration`(默认关) | 创建执行环境时不加 `codeGeneration` 限制 | 按上面的代码创建 | 打开后,`eval` 里新声明的 `var` 不在扫描出的名单里,不会带到下一次;关闭时也只限制执行环境自己的代码生成,不构成隔离 |
|
||||
| `finalExpression`(默认开) | 最后一条表达式改写成 `return { value: (…) }` | 不改写,末尾表达式的值被丢弃,结果为空 | 关闭后输出只能靠打印,模型说明和使用完成值的地方(例如 workflow-ptc)都要相应调整;顶层 `return` 在两种状态下都报错 |
|
||||
|
||||
接入时三个开关都取默认值;非默认值是设计上预留的用法,各自都有测试。
|
||||
|
||||
### 包装
|
||||
|
||||
每次调用编译成一个独立的 `vm.Script`。例如前面留下了 `a`(`const`)、`b`(`let`)、`v`(`var`),这次模型写了:
|
||||
|
||||
```js
|
||||
let c = a + b
|
||||
c * 2
|
||||
```
|
||||
|
||||
实际编译的源码是:
|
||||
|
||||
```js
|
||||
"use strict";(function (__$binding_vals, __$binding_getters) { return async function () { let a = __$binding_vals[0]; let b = __$binding_vals[1]; var v = __$binding_vals[2]; __$binding_getters.push(() => a, () => b, () => v, () => c);
|
||||
let c = a + b
|
||||
return { value: (c * 2) }
|
||||
} })
|
||||
```
|
||||
|
||||
| 部分 | 内容 | 原因 |
|
||||
|---|---|---|
|
||||
| `"use strict";` | `strict` 开关打开时,整个 Script 是 strict 模式;关闭时省略,程序以 `"use strict"` 开头时改为在内层函数体最前面补一个 | 打开时强制严格;关闭时保证前缀里的拷贝语句不会让模型写的指令失效 |
|
||||
| 外层 `function (__$binding_vals, __$binding_getters)` | 接收拷贝进来的值和一个空数组,返回内层函数;由求值器同步调用 | 内部值通过闭包交给程序,不出现在程序的 `arguments` 里;外层在程序运行前就已返回,它的帧不会出现在程序的堆栈里 |
|
||||
| 内层 `async function ()` | 模型程序就是它的函数体 | 支持顶层 `await`;顶层声明是函数的局部变量,每次调用互不冲突;不带接收者、零参数调用,所以 `this` 是 `undefined`(按宽松执行时是 `globalThis`),`arguments` 是一个空的 arguments 对象 |
|
||||
| 拷贝声明 | 只给"前面有、本次没声明或只用 `var` 重新声明"的名字生成,值按下标从 `__$binding_vals` 取 | 下标访问不受 `__proto__` 这类名字影响 |
|
||||
| `__$binding_getters.push(...)` | 在模型代码之前,按求值器生成前缀时的名字顺序,给每个名字(拷贝来的和本次声明的)推入一个读取函数 | 求值器知道第 i 个读取函数对应第 i 个名字,前缀里不用写名字字符串;读取函数可以先建好、以后再读,声明还没初始化完时读取会抛错 |
|
||||
| 最后一条表达式 | 语句前插入 `return `,表达式前插入 `{ value: (`,表达式后插入 `) }` | 和 Node 的 `processTopLevelAwait` 处理最后一条语句的方式相同;外包一层对象,返回的 Promise 不会被 async 函数自动等待 |
|
||||
| 前缀单独占第 0 行 | 编译时 `lineOffset: -1` | 模型代码第 N 行就报告为第 N 行 |
|
||||
| 收尾另起一行 | `} })` | 模型代码最后一行可能以 `//` 注释结尾 |
|
||||
|
||||
拷贝语句在本次没有声明这个名字、或者只用 `var` 重新声明它时生成;本次用 `let`、`const`、`class`、`function`(包括 `const`)声明了它,就没有这行,以本次的声明为准。生成时的关键字:
|
||||
|
||||
| 情况 | 前缀里生成 |
|
||||
|---|---|
|
||||
| 本次没有声明,前面是 `let`、`const`、`function`、`class` | `let` |
|
||||
| 本次没有声明,前面是 `var` | `var` |
|
||||
| 本次只用 `var` 重新声明 | `var` |
|
||||
|
||||
拷贝语句只负责把旧值带进来,写成 `const` 唯一的效果是禁止之后的调用赋值,所以前面的 `const` 也拷成 `let`:之后的调用可以直接给它赋值,改的只是拷贝;声明它的那次调用里它仍是 `const`。不判断这次调用是否真的赋值,所以不需要遍历语法树,也不打印警告。
|
||||
|
||||
只用 `var` 重新声明时也生成拷贝:同一个函数里重复的 `var` 声明是同一个变量,所以这次的 `var` 从旧值开始,没执行到的声明不会把旧值换成 `undefined`,和 node:repl 里全局 `var` 的行为一致。同一次里同名的 `function` 在函数开头就初始化,优先于 `var`,这时不生成拷贝。
|
||||
|
||||
编译与运行:
|
||||
|
||||
```ts ignore-check
|
||||
// Kernel: ScopedReplScopes.compile
|
||||
const factory = new Script(source, { filename, lineOffset: -1, ...importOption }).runInContext(context) as ScopedReplFactory
|
||||
const getters: (() => unknown)[] = []
|
||||
const program = factory(values, getters)
|
||||
this.compiled = { bindings, getters, replaced }
|
||||
return program
|
||||
|
||||
// Evaluator: evaluate
|
||||
const filename = resolve(baseDirectory, `.ptc-repl-${++submissions}.js`) // baseDirectory: process.cwd() at creation
|
||||
const program = scopes.compile(code, { filename, context, finalExpression, ...importOption })
|
||||
const start = Reflect.apply.bind(undefined, program, undefined, []) as ScopedReplProgram
|
||||
void programDomain.run(() => Promise.resolve().then(start)).then(
|
||||
(completion) => { finish({ ok: true, value: completion?.value }) },
|
||||
(error: unknown) => { finish({ ok: false, error }) },
|
||||
)
|
||||
```
|
||||
|
||||
- 内核编译时固定给出 `filename` 和 `lineOffset: -1`;求值器总是传 `importModuleDynamically`:有 import 处理器时用 `VmModuleImportLoader.forFile(filename)`,没有时用 `vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER`。
|
||||
- 程序由微任务直接调用,同步抛错时堆栈里没有求值器的帧。宿主用 `.then` 接结果、不用 `await`,否则 V8 会在堆栈末尾补一帧宿主的 async 帧。
|
||||
- 已实测:`this` 是 `undefined`,`arguments.length` 是 0,同步抛错的堆栈只有程序自己的帧;domain 能接住这样启动的程序里定时器抛出的错误;外包对象里的 Promise 原样返回,不被等待。
|
||||
- 代码里任何位置出现 `__$binding_`(包括字符串和注释),都报 `SyntaxError: Names starting with '__$binding_' are reserved`,位置指向第一次出现的地方,这次不执行。
|
||||
|
||||
### 内核:作用域链、acorn 扫描与编译
|
||||
|
||||
内核文件只依赖 acorn 和 `node:vm`,不依赖子进程和求值器,可以在普通 vm context 里连续编译、执行多段程序,单独测试作用域分层。接口:
|
||||
|
||||
```ts ignore-check
|
||||
class ScopedReplScopes {
|
||||
constructor(options: { strict: boolean; onRedeclaration?: (redeclarations: readonly ScopedReplRedeclaration[]) => void })
|
||||
/** Scan and compile one program against the current bindings; throws SyntaxError and leaves the chain unchanged. */
|
||||
compile(code: string, options: { filename: string; context: Context; finalExpression: boolean; importModuleDynamically?: ScriptOptions['importModuleDynamically'] }): ScopedReplProgram
|
||||
}
|
||||
type ScopedReplProgram = () => Promise<{ value: unknown } | undefined>
|
||||
interface ScopedReplRedeclaration { name: string; previous: ScopedReplBindingKind; current: ScopedReplBindingKind }
|
||||
type ScopedReplBindingKind = 'var' | 'let' | 'const' | 'function' | 'class'
|
||||
interface ScopedReplBinding { name: string; kind: ScopedReplBindingKind }
|
||||
interface ScopedReplScan {
|
||||
/** Top-level bindings declared by the program, in source order. */
|
||||
declarations: ReadonlyMap<string, ScopedReplBindingKind>
|
||||
/** Whether the program starts with a "use strict" directive. */
|
||||
useStrict: boolean
|
||||
/** Last top-level statement when it is an expression statement. */
|
||||
finalExpression?: { statementStart: number; expressionStart: number; expressionEnd: number }
|
||||
}
|
||||
/** Parse once; throws a SyntaxError laid out like V8's for every error acorn reports. */
|
||||
function scanProgram(code: string, options: { filename: string }): ScopedReplScan
|
||||
/** Build the Script source and every binding in the order of the read functions it pushes. */
|
||||
function buildProgramSource(
|
||||
code: string, scan: ScopedReplScan, carried: readonly ScopedReplBinding[], options: { strict: boolean; finalExpression: boolean },
|
||||
): { source: string; bindings: ScopedReplBinding[] }
|
||||
```
|
||||
|
||||
**解析方式**:不论 `strict` 开关,都调用 `parse(code, { ecmaVersion: 'latest', sourceType: 'script', allowAwaitOutsideFunction: true })`。这和 async 函数体的规则基本一致:顶层 `await` 合法,顶层 `return` 是语法错误,同一次里两个同名 `function`、同名的 `var` 和 `function` 都合法。不同的是顶层 `using`、`await using` 和静态 `import`、`export` 会被拒绝。扫描时不判断严格与否:开关打开时由 Script 开头的 `"use strict";` 决定,严格模式的语法违规由 V8 编译时报出。
|
||||
|
||||
解析之前先查两件事:程序以 `#!` 开头,或者代码里任何位置出现 `__$binding_`,就报错,不再解析。
|
||||
|
||||
**一次解析得到四样东西**:
|
||||
|
||||
1. 本次的顶层声明,只看顶层语句:
|
||||
|
||||
| 顶层语句 | 收集的名字 | 记录的关键字 |
|
||||
|---|---|---|
|
||||
| `const`、`let`、`var` 声明,含解构(对象、数组、默认值、剩余元素) | 每个绑定名 | 原关键字 |
|
||||
| `function` 声明,含 async 和 generator | 函数名 | `function`;同一次里同名的 `var` 也记为 `function` |
|
||||
| `class` 声明 | 类名 | `class` |
|
||||
| `for (var …; …; …)`、`for (var … in/of …)` 括号里的 `var` | 每个绑定名 | `var` |
|
||||
| 块、`try`/`catch`/`finally`、`if`/`else`、循环体、`switch`、标签语句、`with` 里的 `var`,包括其中 `for` 括号里的 `var`;不进入函数和类 | 每个绑定名 | `var` |
|
||||
|
||||
2. 有没有顶层 `return`:acorn 直接报 `'return' outside of function`,并给出位置。
|
||||
3. 最后一条语句:从后往前跳过空语句 `;`,找到的第一条如果是表达式语句,记下语句和表达式的起止位置。
|
||||
4. 开头的指令里有没有 `"use strict"`:acorn 在指令语句上标出 `directive`,有就记下 `useStrict`。
|
||||
|
||||
不做的事:不进入嵌套的函数和类;块里只找 `var`,`let`、`const`、`class`、`function` 留在块里;不分析引用,不找赋值;除了最后一条表达式的三处插入,不改写代码。
|
||||
|
||||
**语法错误**:acorn 报错时直接抛 `SyntaxError`,不再交给 V8 编译。`stack` 按 V8 的样式排版:
|
||||
|
||||
```text
|
||||
/work/.ptc-repl-3.js:1
|
||||
const x = ;
|
||||
^
|
||||
|
||||
SyntaxError: Unexpected token
|
||||
```
|
||||
|
||||
- 消息取 acorn 的原文,去掉末尾的 `(行:列)`。
|
||||
- 两处改用 V8 的说法:`'return' outside of function` 报 `Illegal return statement`;报错位置在代码末尾时报 `Unexpected end of input`。
|
||||
- 代码里出现 `__$binding_` 时,按同样的样式报错,位置指向第一次出现的地方。
|
||||
|
||||
### 作用域链状态与每次调用的步骤
|
||||
|
||||
`ScopedReplScopes` 持有:
|
||||
|
||||
- `bindings: Map<string, { kind: ScopedReplBindingKind; read(): unknown }>`:当前可拷贝的名字和它们的读取函数。`kind` 是名字最近一次声明用的关键字;只是被拷贝的名字沿用之前记下的关键字,例如 `function` 拷贝成 `let` 后仍记为 `function`。
|
||||
- `replaced: ReadonlyMap<string, { kind: ScopedReplBindingKind; value: unknown }>`:最近采用的那次程序重新声明的名字,在它编译时的旧值。
|
||||
- `compiled`:最近一次编译的程序的绑定顺序、读取函数数组和它的 `replaced`。
|
||||
|
||||
每次 `compile(code, options)`:
|
||||
|
||||
1. 采用 `compiled`:读取函数数组的长度不少于名字个数,说明那次的前缀已经执行过,`bindings` 换成"第 i 个名字对应第 i 个读取函数",`replaced` 换成它的值。子进程不允许两次执行重叠(`overlapping PTC evaluation`),上一次的前缀总在下一次编译之前的微任务里执行完;只有在一段同步代码里连续编译两次时,前一次才会因前缀还没执行而不被采用。
|
||||
2. 扫描,得到本次声明和最后一条表达式的位置;语法错误直接抛出,状态不再变化。
|
||||
3. 逐个调用 `bindings` 里的读取函数取当前值。读取抛错(声明没初始化完)就用 `replaced` 里的值;两边都没有,就丢掉这个名字。
|
||||
4. 本次没声明、或只用 `var` 重新声明的名字作为拷贝,值放进 `values`;本次声明了的已有名字,把值记进新的 `replaced`,同时记一条重复声明 `{ name, previous, current }`。
|
||||
5. 拼源码,然后编译。`strict` 关闭且 `useStrict` 为 true 时,内层函数体最前面加 `"use strict";`。V8 编译失败时抛出原生的 `SyntaxError`,裁掉错误标题之后的编译器内部帧(和 `vm-script` 做法相同,代码单独写一份),状态不再变化。
|
||||
6. 有重复声明时调用 `onRedeclaration(redeclarations)`。
|
||||
7. 调用外层函数拿到程序,记下 `compiled`,返回程序。
|
||||
|
||||
求值器每次 `evaluate(code, finish)`:
|
||||
|
||||
1. 提交序号加 1(包括有语法错误的提交),文件名是 `<创建时的目录>/.ptc-repl-<序号>.js`。
|
||||
2. 调用 `scopes.compile`;抛错就用这个错误结束本次执行。
|
||||
3. 在 domain 里由微任务启动程序;Promise 完成后调用 `finish`,结果取 `completion?.value`,不再等待。
|
||||
|
||||
读取发生在内核第 3 步,也就是新一次编译的时刻。之后前面的代码再给顶层变量赋值,改的只是它自己那次的变量。
|
||||
|
||||
### 失败的调用
|
||||
|
||||
| 第几次 | 代码 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | `const data = 1` | `data` 是 1 |
|
||||
| 2 | `const data = await tools.fetch()`,`fetch` 抛错 | 失败;第 2 次的 `data` 声明了,但没赋上值 |
|
||||
| 3 | `data` | 1:内核第 3 步读第 2 次的 `data` 时抛错,改用第 2 次编译时记下的旧值 |
|
||||
| 4 | `let fresh = await tools.fetch()`,抛错 | 失败;`fresh` 是新名字 |
|
||||
| 5 | `typeof fresh` | `"undefined"`:没有旧值,名字被丢掉 |
|
||||
|
||||
- 抛错之前已经执行完的声明保留。例如先执行了 `let y = 5` 再抛错,之后的调用里 `y` 仍是 5。
|
||||
- `var` 重新声明从拷贝进来的旧值开始,读取不会抛错,也不需要退回:没执行到的 `var` 保留旧值。例如第 2 次在 `var v = 6` 之前就抛错,或者 `var config = await tools.loadConfig({})` 的工具调用失败,之后的调用读到的仍是之前的值。`function` 在函数开头就是新函数。
|
||||
|
||||
### 异步错误:对齐 REPL
|
||||
|
||||
REPL 用 Node 的 `domain` 接住程序启动的异步工作里抛出的错误。求值器照同样的做法:
|
||||
|
||||
- 创建求值器时调用 `domain.create()` 得到 `programDomain`,每个程序都在 `programDomain.run(...)` 里启动。程序创建的定时器、回调、Promise 后续步骤和 EventEmitter 都归属这个 domain。
|
||||
- `programDomain.on('error', ...)` 把错误写成 `Uncaught ` 加错误文本,交给 `options.reportError`。错误文本取错误的 `stack`,`stack` 不是字符串时用"名字: 消息";不用 `util.inspect` 格式化 Error,是因为 domain 会往错误对象上加 `domain`、`domainThrown`,inspect 会把它们一起打印出来。不是 Error 的值,先删掉这两个属性再用 `util.inspect`,和 node:repl 一样。
|
||||
- 程序能让转换文字这一步抛错(`stack` 访问器、`toString`、自定义 inspect)。监听器里抛错会让 Node 结束进程,所以这一步包在 `try`/`catch` 里,出错时报 `program threw an unrenderable value`,和同步失败时 `prepareException` 的兜底文字相同。`reportError` 放在 `try` 外面,它是子进程自己的代码,出错应该照常暴露。
|
||||
- 回调里 `throw` 冻结或不可扩展的对象时,Node 的 domain 在调用监听器之前给它加属性就失败了,进程结束,和 node:repl 相同。
|
||||
- 子进程处理 `reportError` 的方式不变:当时有执行在运行,就让那次执行以这个错误失败;否则丢弃。错误按创建回调时所属的执行归属(AsyncLocalStorage)。
|
||||
- 不在整个进程上注册 `uncaughtException` 或 `unhandledRejection`,子进程自身代码的错误仍然暴露。
|
||||
- 程序调用工具时,子进程为这次调用创建的异步回调(例如 RPC 回调)也在程序的 domain 里:子进程自身代码在这些回调里的异步错误会被当成程序的 `Uncaught …` 报告,子进程继续运行、不退出。现有 REPL 求值器也是这样。
|
||||
|
||||
已验证的情况:
|
||||
|
||||
- REPL:定时器里抛错、未处理的 Promise rejection,在执行结束后和执行中都被接住;EventEmitter 的 `error` 事件在执行结束后被接住。
|
||||
- 单独的 domain:在主环境里接住了定时器抛错、定时器里产生的 rejection、未被等待的 async 函数失败和 EventEmitter 的 `error` 事件;在带默认 `codeGeneration` 限制的执行环境里,接住了定时器抛错和定时器里产生的 rejection;由微任务启动的程序里,定时器抛错同样被接住。
|
||||
|
||||
### 动态 import
|
||||
|
||||
- 程序里的 `import()` 经过每个 Script 的 `importModuleDynamically`:没有 import 处理器时用 `vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER`,有处理器时用现有的 `VmModuleImportLoader.forFile(filename)`,和 `vm-script` 相同。
|
||||
- 相对路径按求值器创建时的目录解析,也就是这个环境第一次调用时的 working directory;之后的调用换了目录也不变。node:repl 和 `vm-script` 跟随每次调用的目录;Agent 用 `working_directory` 换目录后,需要按新目录 import 时用绝对路径。
|
||||
- 不提供 `require`:node:repl 的 `require` 解析规则不一致(相对路径跟随当前目录,但第一次解析后被缓存;包名固定按启动目录),只保留动态 `import()` 这一种加载方式。
|
||||
|
||||
### 重复声明提示(预留)
|
||||
|
||||
内核在第 6 步调用 `onRedeclaration`;求值器把自己的 `reportRedeclarations(redeclarations: readonly ScopedReplRedeclaration[])` 注入进去,函数体先留空,只加一行 `TODO` 注释,不输出任何内容。以后要提示时,在这里写入程序的 `console`,内容会进入模型可见的输出。
|
||||
|
||||
### 范围外
|
||||
|
||||
- 内存回收:不处理,每次拷贝全部名字。没有闭包存活的旧调用会被回收;某次调用留下了存活的函数时,这个函数会留住这次开始时拷贝进来的所有值。
|
||||
- 跟随 working directory 变化解析 `import()`:以后需要时另加显式操作,例如 `js_switch_cwd`。
|
||||
- `let`、`const`、`class` 重新声明的初始化表达式里读同名旧值(例如 `let count = count + 1`):属于代码问题。
|
||||
- `strict` 关闭时严格和宽松调用混用造成的问题,见"风险":交给以后的重置工具。
|
||||
- 最后一条表达式所在行的列号偏移。
|
||||
|
||||
### 接入
|
||||
|
||||
以 `scoped-repl` 格式接入时只做必需的改动,不顺手重构或复用现有代码,方便和其他进行中的 PTC 改动 rebase、合并。程序格式名是 `scoped-repl`,配置字段名是 `replScoped`。
|
||||
|
||||
| 位置 | 改动 |
|
||||
|---|---|
|
||||
| `ptc-runtime` 的 `PtcProgramFormat` | 包含 `'scoped-repl'` |
|
||||
| `ptc-runtime-node` 的 `config.ts` | 布尔字段 `replScoped`,只能和 `persistent-in-memory` 一起用,否则加载时报错;省略时由 `resolveReplScoped` 取 `resolvePersistentExecution` 对函数体写入的 `persistent-in-memory` 取值(目前为 true),其他模式下为 false |
|
||||
| `ptc-runtime-node` 的 `execution.ts` | `persistent-in-memory` 分支里 `replScoped` 为 true 时:格式 `scoped-repl`,`retainsState` 为 true,TypeScript 按 REPL 语句擦除,配一段新的模型说明 |
|
||||
| `ptc-runtime-node` 的 `evaluators/index.ts` | 求值器表含 `scoped-repl`,三个开关取默认值 |
|
||||
| `ptc-runtime-node` 的 `protocol.ts` | 协议版本是 3,旧 bootstrap 在就绪阶段报错;升级指南 `docs/upgrade-guide/v0.2.2-rc.1/ptc-bootstrap-protocol/` 同时说明 `PtcProgramFormat` 新增的成员 |
|
||||
| workflow-ptc 的 `host.ts` | `scoped-repl` 用和 `repl` 一样的包装 `await (async () => {…})()` |
|
||||
| `ptc-runtime-node` 的 `configuration.ts`(CUA 用它合并配置) | `resolvePersistentExecution` 返回 `executionMode`、`scriptPersistence`、`replScoped` 三个字段,一起合并:`script` 保持持久化的 VM Script,`replScoped: false`;`scoped-repl` 为 `replScoped: true`;函数体和 REPL 语句为 `persistent-in-memory` 加常量 `SCOPED_REPL_BY_DEFAULT`(目前为 true),覆盖 profile 里残留的值。这个常量同时决定 `config.ts` 里省略时的默认值,改它只改一行;`config.ts` 从 `configuration.ts` 导入这个函数,而 `configuration.ts` 本身也是包的发布入口;`configuration.ts` 只导入 `Config` 类型,所以打包不会生成共用的 chunk |
|
||||
| 文档、快照和测试 | 包 README 的配置项、行为说明和"已知限制"、config catalog、快照场景 `ptc-scoped-repl`;构建产物的 e2e 含 `replScoped: true` 用例 |
|
||||
|
||||
tool-executor-ptc 没有改动:格式不是 `function-body` 时,它自动使用"打印输出"那套说明。模型说明写明上面的精确行为,尤其是:输出用 `inspect(...)`,这句和其他模式共用;不支持顶层 `return`;每次调用开始时拷贝、旧代码不受影响;需要共享的可变状态放进对象;不声明直接赋值只改拷贝,前面的 `const` 也可以这样赋值;`let`、`const`、`class` 重新声明的初始化表达式读不到同名旧值,要在旧值上计算就直接赋值或换个名字;`var` 重新声明从旧值开始;调用中途失败时,没执行完的 `let`、`const`、`class`、`var` 重新声明保留旧值;块里的 `var` 也算这次调用的顶层名字,其他块里的声明只属于它的块;只能用 `import()`,相对路径按这个环境第一次调用的目录解析。测试整段逐字比对模型说明。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**用直接 eval 把每次调用嵌进上一次的作用域。** 前后调用能共享活的绑定,但旧作用域永远无法回收,行列号要靠 source map 或改写堆栈才能对上;在禁止从字符串生成代码的执行环境里,直接 eval 也不可用。
|
||||
|
||||
**把声明改写成 `var` 或全局赋值。** 所有调用共用同一个绑定,违反要求 2,而且要改写模型代码。
|
||||
|
||||
**每次调用编译成一个 ES 模块。** 靠合成一个代表上一次调用的模块和追加的 `export` 传值。模块里既不能 `return`,也没有末尾表达式的值,前置代码还会让报错行号整体偏移;函数参数已经能传值,不需要模块。
|
||||
|
||||
**作用域分析加改写引用。** 能做更精细的内存回收,但需要作用域分析并改写代码,违反要求 5。
|
||||
|
||||
**只用打印输出、不支持末尾表达式。** workflow-ptc 和 `repl` 格式的约定都依赖完成值,也偏离现有 REPL 的写法。
|
||||
|
||||
**用顶层 `return` 给出结果。** 与要求 3 冲突,需要换成函数体格式,run_code 的说明也会随之变化。
|
||||
|
||||
**末尾的 Promise 等它完成。** 和 `vm-script` 一致,但和现有 REPL 不一致;按 Node 的写法外包一层对象,保持 REPL 的行为。
|
||||
|
||||
**以 `if`、`for`、块结尾时也给出结果。** 只有在 Script 顶层执行时才有 V8 的完成值,函数体里拿不到;要支持就得改写更多语句。node:repl 在代码含 `await` 时同样没有结果。
|
||||
|
||||
**包装直接写成 `async function (__$binding_vals, __$binding_getters)`。** `this` 同样是 `undefined`,但程序的 `arguments` 里能看到这两个内部值,长度是 3。
|
||||
|
||||
**包装用 async 箭头函数。** `this` 是 `globalThis`、没有 `arguments`,和 REPL 一致;我们选择 strict 模式下不带接收者调用函数时的 `undefined`。
|
||||
|
||||
**用回调交出读取函数。** 前缀里要写名字字符串,还要防止程序重复调用;推入数组、由求值器按下标对应名字更简单。
|
||||
|
||||
**语法错误交给 V8 再编译一次。** 能拿到 V8 原文的消息,但要多编译一次;acorn 已经给出位置和消息,直接报。
|
||||
|
||||
**strict 开启时按 module 规则扫描。** module 规则比 async 函数体严:同一次里两个同名 `function`、同名的 `var` 和 `function`、嵌套的普通函数里把 `await` 当变量名都会被拒绝,HTML 注释的处理也和 V8 编译 Script 时不一致。strict 关闭时也用不了,因为 module 规则一律是 strict 的。
|
||||
|
||||
**套上和运行时一样的函数外壳再扫描。** 和运行时的语法完全一致,顶层 `using` 也能用,Node 的 `processTopLevelAwait` 就是这样做的。但要自己遍历语句查找顶层 `return`,换算外壳带来的位置偏移,还要防止代码里多出的 `}` 提前关闭外壳;我们不需要顶层 `using`,script 规则更简单。
|
||||
|
||||
**只把被赋值的 `const` 拷成 `let`,并打印警告。** 要遍历新调用的语法树并判断遮蔽,属于作用域分析,违反要求 5。我们一律拷成 `let`,不提示。
|
||||
|
||||
**`var` 重新声明不拷贝旧值,从 `undefined` 开始。** `var count = count + 1` 得到 `NaN`;但抛错前没执行到、或在没走到的分支里的 `var` 会把旧值换成 `undefined`,例如工具调用失败的 `var config = await tools.loadConfig({})` 让之后的调用拿不到原来的 `config`。拷贝旧值后,`var count = count + 1` 在旧值上加 1,和 node:repl 一致,暂时接受。
|
||||
|
||||
**只在调用失败、且读到 `undefined` 时退回旧值。** 不用拷贝,但分不出"没执行到"和"执行了、值就是 `undefined`":赋上 `undefined` 之后同一次又失败时,会误退回旧值;成功调用里没走到的分支和不带初始值的 `var x;` 也覆盖不到。
|
||||
|
||||
**在代码里插标记,记录 `var` 是否执行到。** 能准确区分,但要在声明处、声明前的赋值和循环头改写模型代码,违反要求 5。
|
||||
|
||||
**严格的调用里跳过严格模式保留字的拷贝。** 能避免宽松调用里声明的 `package` 这类名字让之后严格的调用编译失败;但这是模型混用两种模式造成的,不处理,交给以后的重置工具。
|
||||
|
||||
**在整个进程上监听未捕获的错误。** 会把子进程自身代码的错误一起吞掉;REPL 用 domain,只接程序启动的异步工作。
|
||||
|
||||
**不收集块里的 `var`。** 块里的 `var` 会和拷贝语句撞名,整段程序报 `already declared`,这个名字也带不到之后的调用;收集它只需约 15 行语句遍历,不分析引用,所以采用收集。
|
||||
|
||||
**堆栈里只显示 `.ptc-repl-<序号>.js`,不带目录。** 文件名去掉目录后,默认加载器按导入那一刻的 cwd 解析相对 `import()`,和有 import 处理器时不一致;用 `//# sourceURL` 只显示短名时,V8 不再应用 `lineOffset`,每个行号多 1;把 import 交给回调函数可以解决:子进程两种启动方式都带 `--experimental-vm-modules`,回调可以在第一次调用的目录下发起 `import()`;但这要求 scoped-repl 总是走回调形式的 import,默认路径现在没有走,所以文件名暂时保留第一次调用的目录。
|
||||
|
||||
**提供 `require`、`module` 和内置模块全局(node:repl 做法)。** node:repl 的 `require` 解析规则不一致,跟随目录又要定义何时换基准;只保留 `import()`。
|
||||
|
||||
## 影响
|
||||
|
||||
得到的:
|
||||
|
||||
- 后面的调用可以用任意关键字再次声明前面的名字,前面的函数、闭包和仍在运行的代码继续使用原来的值。
|
||||
- 写法、结果、报错和异步错误处理与现有 REPL 一致,程序的异步错误不让进程退出。
|
||||
- 不做作用域分析;除了最后一条表达式的三处插入,不改写模型代码。
|
||||
|
||||
付出的:
|
||||
|
||||
- "开始时拷贝"和 REPL 不同:前面的代码(定时器、回调)之后再给顶层变量赋值,后面的调用看不到;模型说明写明了这一点。
|
||||
- 不声明直接赋值只影响之后的调用,前面定义的函数看不到;REPL 里两边会一起变。
|
||||
- strict 模式和 REPL 不同:REPL 里给未声明的名字赋值会创建全局变量,这里会报错。
|
||||
- 调用方不丢弃完成值时,末尾是没写 `await` 的 Promise,宿主报"不是 JSON",和现有 REPL 一样;`run_code` 丢弃完成值,不受影响。
|
||||
- 以 `if`、`for`、块结尾的程序没有结果,node:repl 在代码不含 `await` 时有。
|
||||
- 最后一条表达式所在行,插入点之后的列号右移 17。
|
||||
- 语法错误消息是 acorn 的文字,和 V8 原文不同,例如 `Unexpected token` 而不是 `Unexpected token ';'`。
|
||||
- 扫描统一按 script 规则:顶层 `using`、`await using` 会被拒绝,node:repl 也不支持。
|
||||
- 顶层 `arguments` 是一个空的 arguments 对象,node:repl 里它不存在。
|
||||
- 执行环境默认禁止从字符串生成代码:依赖 `eval` 或 `new Function` 的代码会失败,包括经 import 处理器加载到这个执行环境的库。这个限制不构成隔离,复制进来的主环境函数(例如 `setTimeout.constructor`)仍能生成代码。
|
||||
- 不提供 `require`:依赖 `require` 的代码要改用 `await import()`。
|
||||
- 相对路径的 `import()` 固定按这个环境第一次调用时的目录解析,Agent 换目录后仍按旧目录。
|
||||
- Node 文档把 `domain` 标为已弃用;node:repl 仍建立在它上面,如果 Node 移除它,REPL 也会一起变。
|
||||
- 子进程自身代码在工具调用的异步回调里出错时,会被当成程序的 `Uncaught …` 报告,子进程继续运行,和现有 REPL 相同。
|
||||
- 以 `#!` 开头的程序被拒绝。
|
||||
- 代码里任何位置出现 `__$binding_`(包括字符串、注释)都会被拒绝;用 `\u005f` 这类转义写出的内部名不在检查范围内。
|
||||
- acorn 必须跟上 V8 支持的语法,否则较新的语法会被拒绝执行。
|
||||
- `var` 重新声明从旧值开始:`var count = count + 1` 在旧值上加 1,和 node:repl 一致;即使前面是 `let`、`const` 也是这样,node:repl 在这种情况下报 `already declared`。
|
||||
- `let`、`const`、`class` 重新声明的初始化表达式读不到同名旧值,会报错,例如 `let count = count + 1`。属于代码问题,不处理。
|
||||
- 前面的 `const` 在之后的调用里一律拷成 `let`,可以被赋值,而且没有提示。
|
||||
- `strict` 关闭时,严格和宽松的调用可以混用:宽松调用里给未声明的名字赋值,会创建所有调用和旧函数共用的全局变量;宽松调用里用严格模式保留字(`package`、`public`、`private`、`protected`、`interface`、`implements`、`static`、`let`、`yield`、`eval`、`arguments`)声明的名字,会让之后严格的调用编译失败。不处理,交给以后的重置工具。
|
||||
- 回调里 `throw` 冻结或不可扩展的对象时,Node 的 domain 处理本身出错,进程结束,保留的变量丢失,和 node:repl 相同。
|
||||
- 每次调用都要为前面所有名字生成拷贝声明和读取函数:原型实测 1000 个名字时约 10 ms/次。
|
||||
- 某次调用留下存活的函数时,这次拷贝进来的所有值都会一直留在内存里。
|
||||
|
||||
## 测试
|
||||
|
||||
- 内核测试单独运行即可通过,覆盖:script 规则解析和开头 `"use strict"` 的检测、声明表里的每种语句、顶层 `return`(包括块里的)、最后一条表达式的定位与插入、以 `#!` 开头的程序、代码里出现 `__$binding_`、语法错误的排版和两处消息替换、拼出的源码;`strict` 关闭时,程序开头的 `"use strict"` 在有拷贝和没有拷贝时都生效;并在普通 vm context 里连续执行多段程序,覆盖"跨次调用"里的每一条,包括各种关键字组合、失败的调用、定时器、函数重定义和重复声明通知。
|
||||
- 求值器测试直接使用求值器和真实的 vm 执行环境,覆盖"写法与结果""执行环境""报错""异步错误与生命周期"里的每一条,以及经求值器的跨次调用;包括经全局对象的原型链拿不到主环境的 `Function`、复制进来的主环境函数(例如 `setTimeout.constructor`)仍能生成代码,以及宿主以 `module`、`require` 为名的绑定原样保留。
|
||||
- 三个开关各自在打开和关闭时都有测试。
|
||||
- 顶层 `this` 是 `undefined`、`arguments.length` 是 0,同步抛错的堆栈只有程序自己的帧。
|
||||
- 异步错误:上面列出的每种情况在执行结束后和执行中各测一次,交给 `reportError` 的文本以 `Uncaught ` 开头;进程不退出,变量保留。`stack` 为空、读取 `stack` 抛错、自定义 inspect 抛错时,分别报"名字: 消息"和固定文本,进程不退出;抛出普通对象时报错里没有 `domainThrown`。
|
||||
- import:默认加载器和 import 处理器两条路径,相对路径按创建时的目录解析,之后换了目录也不变。
|
||||
- 构建产物的 e2e 覆盖 `replScoped: true`。
|
||||
- 挂载 `persistent-in-memory` 的测试都显式写 `replScoped`,保留执行的测试两种取值都跑;只有一个测试检查省略时的默认值。
|
||||
- 新文件覆盖率 100%;现有求值器及其测试不变。
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 4b312be894531bee
|
||||
zh: d655648ea5843ef5
|
||||
/agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision:
|
||||
en: a1732e2b1b46cb39
|
||||
zh: ddfc2bdcffa099a5
|
||||
en: 109e418dd92ead24
|
||||
zh: e2564f0f98cc4641
|
||||
/agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/per-program-execution-controls:
|
||||
en: da694641e1d009de
|
||||
zh: 9d7492d39039af66
|
||||
@@ -18,23 +18,23 @@
|
||||
en: 4cd76a34f4712e1a
|
||||
zh: 303ef72a18644d7c
|
||||
/agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/the-run-code-tool-and-the-dispatch-bridge:
|
||||
en: c3d079330748cd24
|
||||
zh: 4e2cc41d00227780
|
||||
en: 69e4683ac3150bdf
|
||||
zh: d8eaa12eaba6d67e
|
||||
/agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/observability-tool-ptc-dispatch:
|
||||
en: 5423da23d4e8d1d9
|
||||
zh: f1198d4f224f3fe6
|
||||
en: 951fb03a27841eea
|
||||
zh: 38928d1ca859eced
|
||||
/agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/the-ptc-runtime-seam:
|
||||
en: 4c423f4e9cef6055
|
||||
zh: 5c23a3637eb420c0
|
||||
/agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/the-node-process-runtime:
|
||||
en: 0b846ed33977e383
|
||||
zh: e45158758d657f9a
|
||||
en: b2a8d7d654d1832b
|
||||
zh: 5852a2a3660e5a2e
|
||||
/agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/trust-posture:
|
||||
en: d62b41a6a5c58e5f
|
||||
zh: ef72c5a9b32b370e
|
||||
/agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/decision/what-the-model-sees:
|
||||
en: 81c12f19f7a967c1
|
||||
zh: 29734ca964acdec2
|
||||
en: 109d4c326bfcf572
|
||||
zh: 9f6be5afc281054d
|
||||
/agent-note-ptc-mode-the-model-writes-typescript-against-the-tool-registry/consequences:
|
||||
en: 761eb6c1aeb7af18
|
||||
zh: 5434456a3d0a1b65
|
||||
|
||||
@@ -22,7 +22,7 @@ Three decisions, each elaborated in its own section below:
|
||||
2. **Code execution is a capability seam** — `packages/ptc-runtime/` contains the Service Definition package `@deepseek-ai/dsh-ptc-runtime`, which owns `ctx.ptcRuntime` ([capability seams](../architecture/2026-06-13-capability-seams.md); Consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop` → `dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another Service Provider package, not a redesign.
|
||||
3. **The shipped implementation is `@deepseek-ai/dsh-ptc-runtime-node`**: each program runs in a fresh managed Node process under the calling Session's resolved sandbox policy. Direct Node APIs remain available within that policy; named host bindings cross a separately validated control channel.
|
||||
|
||||
This note owns PTC presentation, composition and settlement. The [typed tool-return decision](2026-07-20-ptc-typed-tool-returns.md) owns generated outputs, canonical binding values, `ToolCallError` and lossless outer output. The [sandboxed Node decision](../architecture/2026-09-11-sandboxed-node-ptc-runtime.md) supersedes the worker execution, trust posture and budget realization while preserving this note's consumer rules.
|
||||
This note owns PTC presentation, composition and settlement. The [typed tool-return decision](2026-07-20-ptc-typed-tool-returns.md) owns generated outputs, ordinary canonical binding values, `ToolCallError` and lossless runtime output. The [explicit output decision](../architecture/2026-11-09-explicit-ptc-output.md) owns TypeScript completion suppression and text/image selection. The [sandboxed Node decision](../architecture/2026-09-11-sandboxed-node-ptc-runtime.md) supersedes the worker execution, trust posture and budget realization while preserving this note's consumer rules.
|
||||
|
||||
### Per-program execution controls
|
||||
|
||||
@@ -48,9 +48,9 @@ The [Node execution decision](../architecture/2026-09-11-sandboxed-node-ptc-runt
|
||||
|
||||
Under `'ptc'` the registry owns `run_code` as a reserved presentation transport with two required parameters, `{ code: string; description: string }` (the description labels the call in UIs, the bash precedent). It is represented by a normal `ToolDefinition` for dispatch but stays outside the filterable capability layers, so restrictions cannot accidentally remove PTC mode's only entry point. Calls traverse the complete tool pipeline — `tools/pre-execute` → monotonic guards → `tools/execute` around dispatch → `tools/post-execute` → optional definition-owned `finalizeContent` → immutable `tools/result` notification — exactly like native calls; a permission plugin can inspect the program text before it runs, and final-result observers see the normalized outer outcome. Its `execute(args, exec)`:
|
||||
|
||||
1. **Build bindings.** One run-scoped signal follows outer cancellation and is aborted whenever the run settles. Each visible tool binding snapshots lossless-JSON arguments, enters the native-contract dispatch pool (the [live-parallel reference](../../../../packages/core/tools/README.md) owns the scheduling design), executes with a deterministic call id and the outer token as `parent`, defers returned contexts through the outer execution, and logs the `tool/ptc-dispatch-start`/`tool/ptc-dispatch` pair, the settle side carrying the full rendered result content. Success returns the tool's final canonical JSON value; failure becomes the program-visible `ToolCallError`. Every sub-call retains its own immutable execution identity and traverses the full tool pipeline.
|
||||
1. **Build bindings.** One run-scoped signal follows outer cancellation and is aborted whenever the run settles. Each visible tool binding snapshots lossless-JSON arguments, enters the native-contract dispatch pool (the [live-parallel reference](../../../../packages/core/tools/README.md) owns the scheduling design), executes with a deterministic call id and the outer token as `parent`, defers returned contexts through the outer execution, and logs the `tool/ptc-dispatch-start`/`tool/ptc-dispatch` pair, the settle side carrying the full rendered result content. Success returns the consumer-facing value, including program Images for the selected TypeScript bindings; failure becomes the program-visible `ToolCallError`. Every sub-call retains its own immutable execution identity and traverses the full tool pipeline.
|
||||
2. **Runs resolved inputs:** `ctx.ptcRuntime.run(ctx.ptcRuntime.resolve(request))`. The request carries the program, bindings, run-scoped signal, calling Session cwd and applicable resolved file policy. The run-scoped signal connects outer settlement to execution cancellation.
|
||||
3. **Settle after quiescence.** When the runtime settles, the bridge aborts outstanding work and drains the dispatch queue before returning. Success returns captured logs and the completion value as canonical output; the registry renders that value into durable `tool/result.content`, which the result card reads directly. A runtime failure becomes `CodeRunFailedError`; backend rejection uses the registry's normal error boundary. Both produce structured error results, and no sub-call can append after `run_code` settles.
|
||||
3. **Settle after quiescence.** When the runtime settles, the bridge aborts outstanding work and drains the dispatch queue before returning. TypeScript success returns captured logs as canonical output and projects inspected images into durable `tool/result.content`; other language consumers also render their completion value. The result card reads that content directly. A runtime failure becomes `CodeRunFailedError`; backend rejection uses the registry's normal error boundary. Both produce structured error results, and no sub-call can append after `run_code` settles.
|
||||
|
||||
**Sub-call contexts are deferred through the parent.** Injecting inside `run_code` would break parent call/result adjacency, so `ToolRunContext.deferContext()` collects every sub-result `additionalContexts` entry in dispatch order. The registry carries that array even when the program later throws, and the loop appends each entry only after the outer result and every sibling result in the step. An outer post-execute block discards tool-deferred entries and exposes only contexts explicitly attached by the blocking decision.
|
||||
|
||||
@@ -58,13 +58,13 @@ Under `'ptc'` the registry owns `run_code` as a reserved presentation transport
|
||||
|
||||
**Tool-call allowance.** Counting attempts before validation and queuing bounds loops that repeatedly fail or enqueue work. The allowance scales with the resolved timeout and permits bursts; it does not enforce a rolling window. Exhaustion uses the run-scoped abort path so a program cannot catch a binding failure and keep running. Active calls are canceled, queued calls are abandoned, and settlement waits for cleanup. Completed effects remain; a new run receives a fresh allowance. The [executor README](../../../../packages/core/tool-executor-ptc/README.md#use-this-package) owns configuration and failure reporting.
|
||||
|
||||
**Presentation.** `run_code`'s render intent is decided here per the [render-intent Agent Note](../architecture/2026-07-02-tool-render-intent-union.md): `presentCall` creates a `generic` card with `kind: 'execute'`, the program text as its title, and the same program text as `rawInput`; `run_code` intentionally declares no `presentResult`, so the TUI and host/client runtime (Web) complete that card through their generic raw-content fallback using the final durable `tool/result.content`, including captured logs plus the returned value, failure, or post-policy spill preview. This is not a `terminal` card: that card's semantics are "a shell command in a working directory", which a program is not. See the [result-card completeness note](../../archived/bug-fix/2026-07-20-code-mode-result-card-completeness.md).
|
||||
**Presentation.** `run_code` uses a `generic` card with `kind: 'execute'`, the model-authored description as its title and the submitted program as `rawInput`. UI adapters read durable `tool/result.content` for captured text, selected images, failure or the post-policy spill preview. The [explicit output decision](../architecture/2026-11-09-explicit-ptc-output.md) owns selection of that content.
|
||||
|
||||
### Observability: `tool/ptc-dispatch`
|
||||
|
||||
Each sub-dispatch appends a log-only `tool/ptc-dispatch-start` event at pool entry and a `tool/ptc-dispatch` settle event containing parent and child call ids, tool identity, normalized arguments, and the complete rendered `content`/`isError` outcome. It remains outside model history but available to persistence and UIs. Appends occur inside the open `run_code` turn. Direct executions without an agent still run but cannot log the event.
|
||||
|
||||
New sub-calls use `<parent>:ptc:<n>` ids, numbered by tool-call attempt. Invalid arguments consume a number without a dispatch event, so logged ids may have gaps. All call ids are opaque to consumers: migration preserves every historical id byte-for-byte, including `:code:` substrings, so dispatch pairs, spill references, and other correlations remain intact. The bridge attributes forwarded image context to `{ kind: 'plugin', plugin: 'tools-ptc' }`.
|
||||
New sub-calls use `<parent>:ptc:<n>` ids, numbered by tool-call attempt. Invalid arguments consume a number without a dispatch event, so logged ids may have gaps. All call ids are opaque to consumers: migration preserves every historical id byte-for-byte, including `:code:` substrings, so dispatch pairs, spill references, and other correlations remain intact. The [explicit output decision](../architecture/2026-11-09-explicit-ptc-output.md) owns TypeScript image presentation; other language consumers attribute forwarded image contexts to `{ kind: 'ptc-mode' }`.
|
||||
|
||||
The [V2-to-V3 PTC specification](../../../../packages/session/session-format-v2-to-v3/README.md#ptc-vocabulary) owns exact historical tag and attribution conversion; [native V3 admission](../../../../packages/session/session-format-v2-to-v3/README.md#native-v3-admission) owns predecessor-tag refusal. These are not runtime aliases: an opaque extension must not acquire PTC lifecycle meaning merely through a version change.
|
||||
|
||||
@@ -78,7 +78,7 @@ Program outcomes resolve as result fields; caller misuse may reject. Lossless bi
|
||||
|
||||
### The Node process runtime
|
||||
|
||||
The Node provider strips erasable TypeScript, resolves its executable/bootstrap in the configured filesystem/subprocess world, and launches through the shared sandbox and managed process owners. Each child has an empty model environment and fresh program globals. Typed rejection constructors and the capturing console remain program-visible.
|
||||
The Node provider strips erasable TypeScript, resolves its executable/bootstrap in the configured filesystem/subprocess world, and launches through the shared sandbox and managed process owners. Each child has an empty model environment and fresh program globals. Typed rejection constructors and `inspect` are program-visible; optional console support shares the inspection function.
|
||||
|
||||
Binding traffic uses a dedicated control channel separate from stdout/stderr. The host validates frames, call identities and allowed bindings before dispatch. Output, pending calls and channel buffers have configured limits; completion, deadline, cancellation and protocol failure terminate and await the managed range. The provider README and sandboxed Node decision own exact bounds and platform limits.
|
||||
|
||||
@@ -88,7 +88,7 @@ The selected OS backend constrains direct Node effects under the same policy use
|
||||
|
||||
### What the model sees
|
||||
|
||||
The SDK instructs the model to write an async body in the loaded runtime's language (an erasable-TypeScript body by default; a Python `async` body under a Python runtime — see the [language-dispatch note](../../archived/feature/2026-07-31-ptc-language-dispatch.md)), call tools through `await tools.name(args)`, catch rejected tool calls when needed, and return or log only the output that should re-enter context. Both flavors state the same contract in their own primitive: independent read-only calls MAY overlap under `Promise.all` (TypeScript) or `asyncio.gather` (Python), mutating calls run alone in submission order, and dependent work sequences with `await`. The declaration prefix can be as large as native schemas, but remains stable for provider caching.
|
||||
The SDK declares the loaded runtime's source format and visible tool bindings, instructs the model to call tools through `await tools.name(args)` and catch rejected tool calls when needed, and presents language-specific output instructions. TypeScript selects output through `inspect`; the Python flavor retains return and print output. Independent read-only calls may overlap under `Promise.all` or `asyncio.gather`, mutating calls run alone in submission order, and dependent work sequences with `await`. The declaration prefix can be as large as native schemas, but remains stable for provider caching.
|
||||
|
||||
The transport's own `description` and both SDK instruction flavors open by naming `code` and `description` as the call's two required arguments. Prose that describes the call as passing a program leaves the second argument discoverable only through the parameter schema, and a model that emits `{code}` alone loses the whole written program to an `INVALID_ARGS` rejection.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
|
||||
2. **代码执行是一个能力 seam**——`packages/ptc-runtime/` 包含 Service Definition 包 `@deepseek-ai/dsh-ptc-runtime`,拥有 `ctx.ptcRuntime`([能力 seam](../architecture/2026-06-13-capability-seams.zh.md);消费方 = `dsh-tools`,core 消费 seam 的先例见 `agent-loop` → `dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个 Service Provider 包,而非重新设计。
|
||||
3. **交付的实现是 `@deepseek-ai/dsh-ptc-runtime-node`**:每个程序在全新的受管 Node 进程中,按调用 Session 的已解析沙箱策略运行。直接 Node API 在该策略内仍可使用;具名 Host 绑定通过单独验证的控制通道调用。
|
||||
|
||||
本说明负责 PTC 呈现、组合与结算。[类型化工具返回决策](2026-07-20-ptc-typed-tool-returns.zh.md)负责生成输出、规范绑定值、`ToolCallError` 与无损外层输出。[沙箱 Node 决策](../architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md)取代 worker 执行、信任姿态与预算实现,同时保留本说明的消费方规则。
|
||||
本说明负责 PTC 呈现、组合与结算。[类型化工具返回决策](2026-07-20-ptc-typed-tool-returns.zh.md)负责生成输出、普通规范绑定值、`ToolCallError` 与无损运行时输出。[显式输出决策](../architecture/2026-11-09-explicit-ptc-output.zh.md)负责 TypeScript 完成值抑制及文本/图片选择。[沙箱 Node 决策](../architecture/2026-09-11-sandboxed-node-ptc-runtime.zh.md)取代 worker 执行、信任姿态与预算实现,同时保留本说明的消费方规则。
|
||||
|
||||
### 每段程序的执行控制
|
||||
|
||||
@@ -48,9 +48,9 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
|
||||
|
||||
在 `'ptc'` 下,注册表拥有 `run_code` 作为保留的呈现传输通道,带两个必需参数 `{ code: string; description: string }`(description 为 UI 标注该调用,沿用 bash 的先例)。它由一个正常的 `ToolDefinition` 表示以供分发,但位于可过滤的能力层之外,因此限制规则不会意外移除 PTC mode 的唯一入口。调用遍历完整的工具流水线——`tools/pre-execute` → 单调性守卫 → `tools/execute` 包裹分发 → `tools/post-execute` → 由定义拥有的可选 `finalizeContent` → 不可变的 `tools/result` 通知——与原生调用完全一致;权限插件可以在程序运行前检查程序文本,最终结果观察者看到的是规范化的外层结果。其 `execute(args, exec)`:
|
||||
|
||||
1. **构建绑定。** 一个 run 级别的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定都会对无损 JSON 参数创建快照,进入原生约定的分发池(调度设计由[实时并行 参考](../../../../packages/core/tools/README.zh.md) 负责),以确定性的 call id 和外层 token 作为 `parent` 执行,通过外层 execution 延后返回的上下文,并记录 `tool/ptc-dispatch-start`/`tool/ptc-dispatch` 事件对,其中结算侧携带完整渲染后的结果内容。成功时返回工具最终的规范 JSON 值;失败则变为程序可见的 `ToolCallError`。每个子调用保留自己不可变的执行标识,并遍历完整的工具流水线。
|
||||
1. **构建绑定。** 一个 run 级别的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定都会对无损 JSON 参数创建快照,进入原生约定的分发池(调度设计由[实时并行 参考](../../../../packages/core/tools/README.zh.md) 负责),以确定性的 call id 和外层 token 作为 `parent` 执行,通过外层 execution 延后返回的上下文,并记录 `tool/ptc-dispatch-start`/`tool/ptc-dispatch` 事件对,其中结算侧携带完整渲染后的结果内容。成功时返回面向消费方的值,包括指定 TypeScript 绑定的程序 Image;失败则变为程序可见的 `ToolCallError`。每个子调用保留自己不可变的执行标识,并遍历完整的工具流水线。
|
||||
2. **运行已解析输入:** `ctx.ptcRuntime.run(ctx.ptcRuntime.resolve(request))`。请求携带程序、绑定、运行级信号、调用 Session 的 cwd 与适用的已解析文件策略。运行级信号将外层结算与执行取消关联。
|
||||
3. **完全停稳后结算。** 运行时结算后,桥 abort 未完成的工作并排空分发队列后再返回。成功时返回捕获的日志和完成值,将其作为规范输出;注册表再把该值渲染为持久化的 `tool/result.content`,供结果卡片直接读取。运行时失败变为 `CodeRunFailedError`;后端拒绝使用注册表的正常错误边界。两者都产生结构化的错误结果,且 `run_code` 结算后不允许子调用追加。
|
||||
3. **完全停稳后结算。** 运行时结算后,桥接层 abort 未完成的工作并排空分发队列后再返回。TypeScript 成功结果将捕获日志作为规范输出,并将显式展示的图片投影到持久化的 `tool/result.content`;其他语言消费方还会渲染完成值。结果卡片直接读取该内容。运行时失败变为 `CodeRunFailedError`;后端拒绝使用注册表的正常错误边界。两者都产生结构化的错误结果,且 `run_code` 结算后不允许子调用追加。
|
||||
|
||||
**子调用上下文通过父调用延后。** 在 `run_code` 内部注入会破坏父调用/结果的相邻性,因此 `ToolRunContext.deferContext()` 按分发顺序收集每个子结果的 `additionalContexts` 条目。即使程序后来抛出异常,注册表仍携带该数组;循环只在外层结果与步骤中所有兄弟结果之后追加每个条目。外层 post-execute 阻止会丢弃工具延后的条目,只暴露阻止 decision 显式附加的上下文。
|
||||
|
||||
@@ -58,13 +58,13 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
|
||||
|
||||
**工具调用额度。** 在验证和排队之前对尝试计数,可以限制重复失败或持续入队的循环。额度随最终超时时间缩放,允许集中调用,不实施滑动窗口限制。额度耗尽时使用运行级取消路径,使程序无法捕获绑定错误后继续运行。活动调用被取消,排队调用被放弃,结算会等待清理完成。已完成的操作仍然生效;新运行获得完整额度。[执行器 README](../../../../packages/core/tool-executor-ptc/README.zh.md#use-this-package) 负责配置与失败报告。
|
||||
|
||||
**呈现。** `run_code` 的 render intent 按[呈现意图 Agent Note](../architecture/2026-07-02-tool-render-intent-union.zh.md)在此决定:`presentCall` 创建一个 `generic` 卡片,`kind: 'execute'`,以程序文本作为标题,并将同一程序文本作为 `rawInput`;`run_code` 有意不声明 `presentResult`,因此 TUI 和宿主/客户端运行时(Web)会通过通用原始内容回退机制,使用最终持久化的 `tool/result.content` 补全该卡片,其中包括捕获的日志,以及返回值、失败信息或 post-policy spill 预览。这不是 `terminal` 卡片:该卡片的语义是「工作目录中的 shell 命令」,程序不是。参见[结果卡片完整性说明](../../archived/bug-fix/2026-07-20-code-mode-result-card-completeness.md)。
|
||||
**呈现。** `run_code` 使用 `kind: 'execute'` 的 `generic` 卡片,标题为模型填写的描述,`rawInput` 为提交的程序。UI 适配器读取持久化的 `tool/result.content`,展示捕获文本、所选图片、失败或 post-policy spill 预览。[显式输出决策](../architecture/2026-11-09-explicit-ptc-output.zh.md)负责这些内容的选择。
|
||||
|
||||
### 可观测性:`tool/ptc-dispatch`
|
||||
|
||||
每次子分发在进入分发池时追加一个仅日志的 `tool/ptc-dispatch-start` 事件,并以一个 `tool/ptc-dispatch` 结算事件收尾,后者包含父子 call id、工具标识、规范化参数以及完整渲染后的 `content`/`isError` 结果。它不进入模型历史,但可供持久化和 UI 使用。追加发生在开放的 `run_code` 轮次内。没有 agent 的直接执行仍然运行,但无法记录该事件。
|
||||
|
||||
新子调用使用 `<parent>:ptc:<n>` 标识,按工具调用尝试编号。无效参数会占用编号而不产生分发事件,因此日志中的编号可能有空缺。消费者将所有 call id 视为不透明值:迁移逐字节保留每个历史标识,包括 `:code:` 子串,因此分发事件对、spill 引用及其他关联保持完整。桥接层将转发图片上下文的来源标记为 `{ kind: 'plugin', plugin: 'tools-ptc' }`。
|
||||
新子调用使用 `<parent>:ptc:<n>` id,按工具调用尝试编号。无效参数在没有分发事件时仍消耗编号,因此日志中的 id 可能有空缺。所有调用 id 对消费方均不透明:迁移逐字节保留每个历史 id,包括 `:code:` 子串,使分发事件对、spill 引用和其他关联保持完整。[显式输出决策](../architecture/2026-11-09-explicit-ptc-output.zh.md)负责 TypeScript 图片呈现;其他语言消费方将转发的图片上下文归属为 `{ kind: 'ptc-mode' }`。
|
||||
|
||||
[V2 到 V3 PTC 规范](../../../../packages/session/session-format-v2-to-v3/README.zh.md#ptc-vocabulary)负责精确的历史标签与归属转换;[原生 V3 准入](../../../../packages/session/session-format-v2-to-v3/README.zh.md#native-v3-admission)负责前代标签拒绝。这些不是运行时别名:不透明扩展不能仅因版本变化就获得 PTC 生命周期含义。
|
||||
|
||||
@@ -78,7 +78,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
|
||||
|
||||
### Node 进程运行时
|
||||
|
||||
Node 提供方擦除可擦除 TypeScript,在配置的文件系统/子进程世界中解析可执行文件与 bootstrap,并通过共享沙箱与受管进程所有者启动。每个子进程都有空模型环境和全新程序全局对象。类型化拒绝构造器与捕获 console 仍对程序可见。
|
||||
Node 提供方擦除可擦除 TypeScript,在配置的文件系统/子进程世界中解析可执行文件与 bootstrap,并通过共享沙箱与受管进程所有者启动。每个子进程都有空模型环境和全新程序全局对象。类型化拒绝构造器与 `inspect` 对程序可见;可选 console 支持复用展示函数。
|
||||
|
||||
绑定通信使用与 stdout/stderr 分开的专用控制通道。Host 在分派前验证帧、调用身份与允许的绑定。输出、待处理调用与通道缓冲有配置上限;完成、截止、取消和协议失败会终止并等待受管范围。提供方 README 与沙箱 Node 决策负责确切上限和平台限制。
|
||||
|
||||
@@ -88,7 +88,7 @@ Node 提供方擦除可擦除 TypeScript,在配置的文件系统/子进程
|
||||
|
||||
### 模型看到的内容
|
||||
|
||||
SDK 指示模型编写一个所加载运行时语言的异步函数体(默认可擦除 TypeScript;Python 运行时下为 Python `async` 函数体——见[语言分发 note](../../archived/feature/2026-07-31-ptc-language-dispatch.md)),通过 `await tools.name(args)` 调用工具,在需要时捕获被拒绝的工具调用,并仅 return 或 log 应重新进入上下文的输出。两种 flavor 用各自的原语陈述同一约定:相互独立的只读调用可以(MAY)在 `Promise.all`(TypeScript)或 `asyncio.gather`(Python)下重叠,有副作用的调用按提交顺序单独运行,有依赖的工作用 `await` 排序。声明前缀可能与原生 schema 一样大,但对提供方缓存保持稳定。
|
||||
SDK 声明所加载运行时的源码格式和可见工具绑定,指示模型通过 `await tools.name(args)` 调用工具、在需要时捕获被拒绝的工具调用,并提供各语言的输出说明。TypeScript 通过 `inspect` 选择输出;Python flavor 保留 return 和 print 输出。相互独立的只读调用可通过 `Promise.all` 或 `asyncio.gather` 重叠,有副作用的调用按提交顺序单独运行,有依赖的工作用 `await` 排序。声明前缀可能与原生 schema 一样大,但对提供方缓存保持稳定。
|
||||
|
||||
传输自身的 `description` 与两种 flavor 的 SDK 说明都以点名 `code` 和 `description` 这两个必填参数开头。把该调用描述成「传入一个程序」的散文会让第二个参数只能从参数 schema 中发现,而只发出 `{code}` 的模型会因 `INVALID_ARGS` 被拒,连同已写好的整个程序一起丢失。
|
||||
|
||||
|
||||
@@ -9,32 +9,32 @@
|
||||
en: e2403bc358c4e14b
|
||||
zh: dd37923097a3b24c
|
||||
/agent-note-typed-tool-returns-in-ptc-mode/decision:
|
||||
en: 2218fac13ef57c77
|
||||
zh: d4a18daa9debc77c
|
||||
en: 6dd62ee16c69536c
|
||||
zh: aec9fa1c74be28e5
|
||||
/agent-note-typed-tool-returns-in-ptc-mode/decision/generated-sdk:
|
||||
en: 30583e9ac6715952
|
||||
zh: 3a85c9482abf574c
|
||||
/agent-note-typed-tool-returns-in-ptc-mode/decision/binding-values-and-failures:
|
||||
en: ecb95cafa2dc1297
|
||||
zh: ccdf300ca936ca12
|
||||
en: 8b10f08e67ca9323
|
||||
zh: e57adb520049f407
|
||||
/agent-note-typed-tool-returns-in-ptc-mode/decision/outer-result-and-output-ledger:
|
||||
en: d907d0a8205157e1
|
||||
zh: 1ed99168266a7ca4
|
||||
en: d57456d76fed9c6d
|
||||
zh: 453775faad48588c
|
||||
/agent-note-typed-tool-returns-in-ptc-mode/decision/typed-handles-and-lifetime:
|
||||
en: b2b9318c2812e3a4
|
||||
zh: 73cf34bfd487e2f1
|
||||
/agent-note-typed-tool-returns-in-ptc-mode/decision/persistence-metadata-and-spill:
|
||||
en: c388619f862a92ba
|
||||
zh: d557eae40c7b6b7a
|
||||
en: 2c3ad4ec376ed2d5
|
||||
zh: 49e8206a4c17b1b3
|
||||
/agent-note-typed-tool-returns-in-ptc-mode/testing:
|
||||
en: 728513c779f031f8
|
||||
zh: f6d869f18add7242
|
||||
en: 81a4355914c918cf
|
||||
zh: 36aa414cd382ff9c
|
||||
/agent-note-typed-tool-returns-in-ptc-mode/alternatives-considered:
|
||||
en: dedd87b9b35d8fab
|
||||
zh: 78db4c7e8c4e5f61
|
||||
en: 069a1e170c7d1dfb
|
||||
zh: 14ce5a56e602595c
|
||||
/agent-note-typed-tool-returns-in-ptc-mode/consequences:
|
||||
en: f5726b5e7a2fc73f
|
||||
zh: 0b9aa884db5d920f
|
||||
/agent-note-typed-tool-returns-in-ptc-mode/known-limitations-and-deferred-work:
|
||||
en: a42af629ad6872cc
|
||||
zh: 41e5c0bb3de0ae4d
|
||||
en: 19828c2e5796df2d
|
||||
zh: 2ef63df81fca7a72
|
||||
@@ -14,7 +14,7 @@ The [canonical tool-output contract](../../../../packages/core/tools/README.md)
|
||||
|
||||
## Decision
|
||||
|
||||
PTC mode is a typed projection of the visible tool registry. Each successful binding resolves to the final canonical `JsonValue` after post-execute policy, while a failed binding rejects with a real `ToolCallError`. Intermediate values remain inside the run and cross the worker boundary whole. The outer `run_code` logs, completion value, or failure diagnostic enter the configurable output ledger and model-facing spill pipeline; a successfully settled sub-call whose final Native content contains an image additionally defers that complete ordered content through the parent result as logged, source-attributed context.
|
||||
PTC mode is a typed projection of the visible tool registry. Ordinary successful bindings resolve to the final canonical `JsonValue` after post-execute policy, while failed bindings reject with a real `ToolCallError`. Intermediate values remain inside the run and cross the process boundary whole. The [explicit output decision](../architecture/2026-11-09-explicit-ptc-output.md) owns TypeScript Image bindings, completion suppression and selection of visible text and images. Other language consumers retain completion rendering and deferred image-bearing content.
|
||||
|
||||
This note owns the return and failure contract layered on the original [PTC mode foundation](2026-06-15-ptc.md). The unified schema vocabulary is owned by the [JSON-value schema DSL reference](../../../../packages/core/tools/README.md), and Native rendering and policy projection remain owned by the canonical-output note.
|
||||
|
||||
@@ -49,15 +49,15 @@ declare const tools: {
|
||||
|
||||
### Binding values and failures
|
||||
|
||||
Before dispatch the bridge snapshots binding arguments as lossless JSON and snapshots the detached value again for an independent durable summary event. Host-side detachment, immutable execution, and output-schema projection all use iterative traversals rather than nested structured clone or recursive freezing. `undefined`, non-finite numbers, `-0`, sparse arrays, cycles, functions, and exotic objects reject that call before the tool runs. Successful dispatch returns `ToolExecutionResult.value`; Native `content`, metadata, and internal error information do not cross to the program. Image-bearing final content is not a second binding value: the bridge ferries it after the outer result so the next model request can see the durable image, while post-execute block/content replacement remains authoritative and text-only results are not duplicated.
|
||||
Before dispatch the bridge snapshots binding arguments as lossless JSON and snapshots the detached value again for an independent durable summary event. Host-side detachment, immutable execution, and output-schema projection all use iterative traversals rather than nested structured clone or recursive freezing. `undefined`, non-finite numbers, `-0`, sparse arrays, cycles, functions, and exotic objects reject that call before the tool runs. Ordinary successful dispatch returns `ToolExecutionResult.value`; the selected TypeScript image bindings use the final image attachment to construct a program Image. Post-execute blocking and result projection remain authoritative. Native content envelopes, metadata and internal error information stay outside ordinary binding values.
|
||||
|
||||
PTC mode declares its rejection capability on the runtime request as `{ name: "ToolCallError", memberNameProperty: "toolName" }`. The runtime Service Definition treats those names as data: the worker materializes and injects the actual constructor used for `tools` binding failures, so `error instanceof ToolCallError` works without making a generic runtime know about tools. The worker constructs failures and defines their public fields through module-captured error and property-definition intrinsics plus null-prototype descriptors, so model mutations cannot replace the promised rejection with a worker failure. The error has the standard `Error` message plus the exact `toolName`; it deliberately omits `ToolFailure.info`, error codes, and Native content. This is an exception contract for control flow, not a failure union for programmatic classification.
|
||||
|
||||
Binding arguments and resolutions are revalidated as lossless JSON on both sides of the hostile worker protocol and have no byte cap. Before crossing through structured clone, each detached value is encoded as a flat pre-order token stream whose transport nesting is bounded; the receiver rebuilds it iteratively. Valid application nesting therefore has neither a JavaScript call-stack depth cap nor a platform-specific nested structured-clone limit. At module initialization the worker captures its own realm's `Array.prototype` and `Object.prototype` identities, the native function-source intrinsic used only to recognize foreign-realm plain-container prototypes, and every structural and metering intrinsic used by the JSON boundary. Property writes use null-prototype descriptors, while private array and set operations invoke captured methods without consulting mutable global or prototype slots. Model code can therefore replace helpers such as `Object.keys`, `Array.isArray`, collection methods, string methods, or `Buffer.byteLength`, rewrite intrinsic-prototype constructor slots, or add descriptor-shaped fields to `Object.prototype` without changing validation, wire transport, or byte accounting. The foreign-realm native function-source check still rejects user-authored constructors that imitate `Object` or `Array`. The dependency-light runtime Service Definition names its structural equivalent `PtcJsonValue` so it need not depend on the session-owned canonical type; the generated SDK and tool API use `JsonValue`. Intermediate values are not prompt-truncated, context-spilled, or persisted. This preserves full acquired search, workflow, task, filesystem, and MCP values for programmatic filtering while leaving provider and executor acquisition limits truthful.
|
||||
Ordinary binding arguments and JSON resolutions are revalidated as lossless JSON on both sides of the hostile worker protocol and have no byte cap. Before crossing through structured clone, each detached value is encoded as a flat pre-order token stream whose transport nesting is bounded; the receiver rebuilds it iteratively. Valid application nesting therefore has neither a JavaScript call-stack depth cap nor a platform-specific nested structured-clone limit. At module initialization the worker captures its own realm's `Array.prototype` and `Object.prototype` identities, the native function-source intrinsic used only to recognize foreign-realm plain-container prototypes, and every structural and metering intrinsic used by the JSON boundary. Property writes use null-prototype descriptors, while private array and set operations invoke captured methods without consulting mutable global or prototype slots. Model code can therefore replace helpers such as `Object.keys`, `Array.isArray`, collection methods, string methods, or `Buffer.byteLength`, rewrite intrinsic-prototype constructor slots, or add descriptor-shaped fields to `Object.prototype` without changing validation, wire transport, or byte accounting. The foreign-realm native function-source check still rejects user-authored constructors that imitate `Object` or `Array`. The dependency-light runtime Service Definition names its structural equivalent `PtcJsonValue` so it need not depend on the session-owned canonical type; the generated SDK and tool API use `JsonValue`. Intermediate values are not prompt-truncated, context-spilled, or persisted. This preserves full acquired search, workflow, task, filesystem, and MCP values for programmatic filtering while leaving provider and executor acquisition limits truthful.
|
||||
|
||||
### Outer result and output ledger
|
||||
|
||||
The runtime accepts an exact lossless JSON completion of any root. Returning `undefined` omits the completion; returning `null` is an explicit result. `run_code` exposes the canonical outer value `{ logs: string[], result?: JsonValue }`. Its Native renderer emits logs first, renders a string result raw, and renders every other JSON root with an iterative pretty printer. Total indentation is capped at ten characters and deeper subtrees remain compact, preserving the established shallow text while keeping traversal stack-safe and formatted size linear in the canonical JSON size.
|
||||
Runtime consumers that retain completions receive exact lossless JSON: `undefined` omits the completion and `null` is an explicit result. TypeScript `run_code` requests completion discard and returns captured logs plus optional sandbox facts as its canonical value; selected images enter its rendered content separately. Other language consumers also expose `result?: JsonValue`. Their renderer emits logs first, renders a string result raw, and renders every other JSON root with an iterative pretty printer. Total indentation is capped at ten characters and deeper subtrees remain compact, keeping traversal stack-safe and formatted size linear in the canonical JSON size.
|
||||
|
||||
`NodePtcRuntime` replaces the former independent log and value caps with configurable `maxOutputBytes`, defaulting to `67_108_864` bytes. The worker charges captured logs by their exact JSON-string serialization and preflights the detached completion or program exception against the remaining combined budget before posting a terminal message. A giant thrown string or stack therefore crosses the worker port only as the fixed `output-limit` diagnostic. The host repeats the hostile-peer ledger for forged traffic and native pipe writes the worker cannot observe. Fixed `PtcRunResult` field names, braces, the bounded error-kind tag, and later presentation whitespace are deliberately outside this variable-payload ledger. Neither stage materializes an over-limit serialized completion. A result at or below the cap is exact. A completion that cannot survive lossless JSON snapshotting fails as `invalid-output`; a value, diagnostic, or combined outcome over the cap fails as `output-limit` rather than becoming inspected or truncated text.
|
||||
|
||||
@@ -73,13 +73,13 @@ Temporary Cordis Plugins follow the same rule: `cordis_mount` returns `{ id, plu
|
||||
|
||||
### Persistence, metadata, and spill
|
||||
|
||||
Nested dispatch logs the sub-call's full rendered `content`/`isError` on `tool/ptc-dispatch` but does not persist canonical values. `tool/result` continues to persist only rendered content, error, and optional metadata. A successful final content sequence containing an image is also wrapped in a source-attributed user message and deferred through the outer result; the normal session event makes that model-visible input reconstructable. The [PTC mode note](2026-06-15-ptc.md) owns durable event names and historical identity preservation; replay cannot recreate intermediate canonical program values.
|
||||
Nested dispatch logs the sub-call's full rendered `content`/`isError` on `tool/ptc-dispatch` but does not persist canonical values. TypeScript programs select images for the outer `tool/result.content` through explicit inspection. Other language consumers defer successful image-bearing content as source-attributed user messages after the outer result. The normal Session events make these model-visible inputs reconstructable. The [PTC mode note](2026-06-15-ptc.md) owns durable event names and historical identity preservation; replay cannot recreate intermediate canonical program values.
|
||||
|
||||
The opaque `exec.parent` token marks nested calls. Presentation metadata and generic or tool-owned spill projections skip those calls; their canonical values never enter context. The Client can derive [nested terminal cards](../../../../packages/client/ui-tool/README.md) from raw dispatch events without metadata. The outer `run_code` call produces the model-facing result and may spill its final post-policy presentation; `run_code` intentionally declares neither a result presenter nor presentation metadata, so UI adapters complete the card through their generic raw-content fallback using durable `tool/result.content`.
|
||||
|
||||
## Testing
|
||||
|
||||
Compile-time and snapshot tests pin exact `ToolArgsMap`, `ToolOutputMap`, `ToolName`, schema-to-TypeScript coverage, exotic names, and assembled PTC mode image forwarding. Registry and real-worker tests cover scalar, array, object, and null values; raw string rendering; absent `undefined`; consumer-declared real rejection classes, including `ToolCallError`; invalid arguments and completions, including intrinsic-looking forged prototypes; model-mutated JSON-boundary globals, prototype methods, constructor slots, and inherited descriptor fields; typed binding failures after those mutations; large uncapped intermediate bindings; nested spill suppression; generic image-bearing context deferral plus post-execute replacement/block precedence; exact and over-limit 64 MiB accounting; combined logs/value/diagnostic accounting; giant thrown stacks; bounded failure spill; hostile forged traffic; and built-package execution.
|
||||
Compile-time and snapshot tests pin exact `ToolArgsMap`, `ToolOutputMap`, `ToolName`, schema-to-TypeScript coverage, exotic names, and assembled PTC mode image projection. Registry and real-worker tests cover scalar, array, object, and null values; raw string rendering; absent `undefined`; consumer-declared real rejection classes, including `ToolCallError`; invalid arguments and completions, including intrinsic-looking forged prototypes; model-mutated JSON-boundary globals, prototype methods, constructor slots, and inherited descriptor fields; typed binding failures after those mutations; large uncapped intermediate bindings; nested spill suppression; consumer-specific image projection plus post-execute replacement/block precedence; exact and over-limit 64 MiB accounting; combined logs/value/diagnostic accounting; giant thrown stacks; bounded failure spill; hostile forged traffic; and built-package execution.
|
||||
|
||||
Keyless real-worker integration tests pin the two handle workflows that prose results could not safely support. A background bash call returns its job id, the outer run settles, and a later run polls that id to completion; separate cases prove pre-abort creates no task, post-publication call abort preserves the task, foreground execution stays signal-coupled, and `job_kill` owns cancellation. A Cordis program reads an active or pending mount's id and `waitingFor` fields directly, unmounts by that id, and confirms removal without parsing rendered text.
|
||||
|
||||
@@ -93,9 +93,9 @@ Keyless real-worker integration tests pin the two handle workflows that prose re
|
||||
|
||||
**Silently inspect or truncate an oversized completion.** Rejected because changing a JSON value into a string is lossy and type-incorrect. The explicit `output-limit` failure lets the model choose a smaller result, while the retained logs and diagnostic can still use normal outer spill.
|
||||
|
||||
**Require each rich leaf tool to inspect `exec.parent` and defer itself.** Rejected because it couples leaf tools to PTC mode internals, duplicates policy handling, and misses future rich tools. The dispatch bridge owns generic forwarding from the already settled final result.
|
||||
**Require each rich leaf tool to inspect `exec.parent` and defer itself.** Rejected because it couples leaf tools to PTC mode internals and duplicates policy handling. The dispatch bridge owns consumer-specific image projection from the already settled final result.
|
||||
|
||||
**Expose Native rich content as part of every binding's canonical value.** Rejected because a canonical value is lossless JSON and tool-specific; attachment blocks are a model projection with durable lifecycle semantics. Keeping the value and projection separate preserves typed programs without dropping images from later model context.
|
||||
**Expose Native rich content as part of every binding's canonical value.** Rejected because a canonical value is lossless JSON and tool-specific; attachment blocks are a model projection with durable lifecycle semantics. Consumer-owned program Image bindings preserve typed tool values while leaving image selection to the program.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -111,6 +111,6 @@ The worker performs bounded-depth flat-wire transport and lossless validation bu
|
||||
- Intermediate values have no byte cap and can exhaust process or worker memory through retention, flat-wire copies, or structured-clone cost.
|
||||
- The 64 MiB hard cap applies only to the outer variable payloads, excluding fixed result-envelope syntax and presentation whitespace; spill cannot recover bytes rejected beyond that cap.
|
||||
- Provider or executor acquisition limits may already have discarded source data before a canonical value reaches PTC mode.
|
||||
- Unsupported MCP output schemas fall back to `JsonValue`; admitted MCP images use the generic deferred projection, while audio and embedded-resource payloads remain diagnostic-only.
|
||||
- Unsupported MCP output schemas fall back to `JsonValue`; image projection follows the consumer-specific rules above, while audio and embedded-resource payloads remain diagnostic-only.
|
||||
- Nested calls have no separate model-facing result; Client child cards do not add model context.
|
||||
- Code failures expose `ToolCallError` message and tool name only, without a programmatic error-code union.
|
||||
@@ -14,7 +14,7 @@ PTC mode 过去会把每个嵌套工具的结果从 `ContentBlock[]` 重新投
|
||||
|
||||
## 决策
|
||||
|
||||
PTC mode 是可见工具注册表的类型化投影。每个成功的绑定调用都会解析为 post-execute 策略处理后的最终规范 `JsonValue`,失败的绑定调用则会以真正的 `ToolCallError` 拒绝 Promise。中间值只存在于本次运行中,并完整跨越 worker 边界。外层 `run_code` 的日志、完成值或失败诊断会进入可配置的输出账本以及面向模型的输出落盘流水线;如果成功结算的子调用最终 Native 内容包含图片,其完整有序内容还会经父结果延后为写入日志且带来源归属的上下文。
|
||||
PTC mode 是可见工具注册表的类型化投影。普通成功绑定调用解析为 post-execute 策略处理后的最终规范 `JsonValue`,失败的绑定调用则以真正的 `ToolCallError` 拒绝 Promise。中间值保留在运行中,并完整跨越进程边界。[显式输出决策](../architecture/2026-11-09-explicit-ptc-output.zh.md)负责 TypeScript Image 绑定、完成值抑制及可见文本和图片的选择。其他语言消费方保留完成值渲染和含图片内容的延后转发。
|
||||
|
||||
本文档定义叠加在原始 [PTC mode 基础](2026-06-15-ptc.zh.md)之上的返回值与失败约定。统一 schema 词汇由 [JSON 值 schema DSL reference](../../../../packages/core/tools/README.zh.md)负责定义;Native 渲染与策略投影仍由规范输出 Agent Note 负责定义。
|
||||
|
||||
@@ -49,15 +49,15 @@ declare const tools: {
|
||||
|
||||
### 绑定值与失败
|
||||
|
||||
分发前,桥接层会把绑定参数快照为无损 JSON,再对分离后的值生成一次快照,供独立的持久摘要事件使用。宿主侧的值分离、执行数据的不可变处理与输出 schema 投影均采用迭代遍历,而不使用嵌套结构化克隆或递归冻结。`undefined`、非有限数、`-0`、稀疏数组、循环引用、函数和非普通对象都会使该调用在工具运行前被拒绝。成功分发会返回 `ToolExecutionResult.value`;Native `content`、元数据和内部错误信息不会传入程序。含图片的最终内容不是第二份绑定值:桥接层会在外层结果之后转运它,使下一次模型请求可以看到持久图片;post-execute 阻止/内容替换仍具有权威性,纯文本结果不会重复。
|
||||
分发前,桥接层会把绑定参数快照为无损 JSON,再对分离后的值生成一次快照,供独立的持久摘要事件使用。宿主侧的值分离、执行数据的不可变处理与输出 schema 投影均采用迭代遍历,而不使用嵌套结构化克隆或递归冻结。`undefined`、非有限数、`-0`、稀疏数组、循环引用、函数和非普通对象都会使该调用在工具运行前被拒绝。普通成功分发返回 `ToolExecutionResult.value`;指定的 TypeScript 图片绑定使用最终图片附件构造程序 Image。Post-execute 阻止和结果投影仍具有最终决定权。普通绑定值不包含 Native 内容信封、元数据和内部错误信息。
|
||||
|
||||
PTC mode 通过运行时请求中的 `{ name: "ToolCallError", memberNameProperty: "toolName" }` 声明其以异常拒绝 Promise 的能力。运行时 Service Definition 只把这些名称视为数据:worker 会动态生成并注入真正用于 `tools` 绑定失败的构造函数,因此无需让通用运行时了解工具,`error instanceof ToolCallError` 也能成立。worker 使用模块初始化时捕获的 Error 构造函数与属性定义内建方法,配合原型为 null 的属性描述符,构造失败对象并定义其公开字段,因此模型代码的修改不会把约定承诺的 reject 变成 worker 失败。该错误包含标准的 `Error` 消息和确切的 `toolName`,并有意省略 `ToolFailure.info`、错误代码与 Native 内容。这是一项用于控制流的异常约定,而不是供程序分类的失败联合。
|
||||
|
||||
绑定参数与绑定返回值会在不可信 worker 协议的两端重新校验为无损 JSON,且不设字节上限。每个分离后的值在通过结构化克隆跨越边界前,都会编码为扁平的前序 token 流,其传输结构的嵌套深度有界;接收方再以迭代方式重建该值。因此,有效应用数据的嵌套深度既不受 JavaScript 调用栈深度上限限制,也不受特定平台对嵌套结构化克隆施加的上限限制。模块初始化时,worker 会捕获自身 JavaScript 运行域中 `Array.prototype` 和 `Object.prototype` 的引用、仅用于识别其他运行域普通容器原型、可获取原生函数源码的内建函数,以及 JSON 边界用于结构处理和计量的全部内建方法。属性写入使用原型为 null 的属性描述符;内部的数组与集合操作直接调用捕获的方法,不会访问可变的全局或原型槽位。因此,即使模型代码替换 `Object.keys`、`Array.isArray`、集合方法、字符串方法或 `Buffer.byteLength` 等辅助方法,重写内建原型的构造函数槽位,或向 `Object.prototype` 添加形如属性描述符的字段,也不会改变校验、协议传输或字节计量。面向其他运行域的原生函数源码检查仍会拒绝由用户编写、冒充 `Object` 或 `Array` 的构造函数。为保持依赖轻量,运行时 Service Definition 将结构等价类型命名为 `PtcJsonValue`,从而无需依赖会话侧拥有的规范类型;生成的 SDK 和工具 API 则使用 `JsonValue`。这些值不会经过提示词截断、上下文 spill 或持久化。因此,程序可以完整筛选已经采集的搜索、工作流、任务、文件系统与 MCP 值,同时提供方和执行器的采集上限仍会实际生效。
|
||||
普通绑定参数与 JSON 返回值会在不可信 worker 协议的两端重新校验为无损 JSON,且不设字节上限。每个分离后的值在通过结构化克隆跨越边界前,都会编码为扁平的前序 token 流,其传输结构的嵌套深度有界;接收方再以迭代方式重建该值。因此,有效应用数据的嵌套深度既不受 JavaScript 调用栈深度上限限制,也不受特定平台对嵌套结构化克隆施加的上限限制。模块初始化时,worker 会捕获自身 JavaScript 运行域中 `Array.prototype` 和 `Object.prototype` 的引用、仅用于识别其他运行域普通容器原型、可获取原生函数源码的内建函数,以及 JSON 边界用于结构处理和计量的全部内建方法。属性写入使用原型为 null 的属性描述符;内部的数组与集合操作直接调用捕获的方法,不会访问可变的全局或原型槽位。因此,即使模型代码替换 `Object.keys`、`Array.isArray`、集合方法、字符串方法或 `Buffer.byteLength` 等辅助方法,重写内建原型的构造函数槽位,或向 `Object.prototype` 添加形如属性描述符的字段,也不会改变校验、协议传输或字节计量。面向其他运行域的原生函数源码检查仍会拒绝由用户编写、冒充 `Object` 或 `Array` 的构造函数。为保持依赖轻量,运行时 Service Definition 将结构等价类型命名为 `PtcJsonValue`,从而无需依赖会话侧拥有的规范类型;生成的 SDK 和工具 API 则使用 `JsonValue`。这些值不会经过提示词截断、上下文 spill 或持久化。因此,程序可以完整筛选已经采集的搜索、工作流、任务、文件系统与 MCP 值,同时提供方和执行器的采集上限仍会实际生效。
|
||||
|
||||
### 外层结果与输出账本
|
||||
|
||||
运行时接受以任意 JSON 类型为根的精确无损完成值。返回 `undefined` 表示省略完成值;返回 `null` 则是显式结果。`run_code` 暴露规范外层值 `{ logs: string[], result?: JsonValue }`。其 Native 渲染器先输出日志;字符串结果保持原文,其他所有 JSON 根值则使用迭代式美化渲染器。总缩进长度上限为 10 个字符,更深的子树保持紧凑格式,既保留既有的浅层文本,又确保遍历不受调用栈深度限制,且格式化输出大小与规范 JSON 大小呈线性关系。
|
||||
保留完成值的运行时消费方接收精确无损 JSON:`undefined` 省略完成值,`null` 是显式结果。TypeScript `run_code` 请求丢弃完成值,将捕获日志和可选沙箱信息作为规范值返回;所选图片单独进入渲染内容。其他语言消费方还提供 `result?: JsonValue`。其渲染器先输出日志,字符串结果保持原文,其他 JSON 根值使用迭代式美化渲染器。总缩进长度上限为十个字符,更深子树保持紧凑格式,使遍历不受调用栈深度限制,且格式化大小与规范 JSON 大小呈线性关系。
|
||||
|
||||
`NodePtcRuntime` 以可配置的 `maxOutputBytes` 取代彼此独立的日志与值上限,默认值为 `67_108_864` 字节。worker 会将已捕获日志序列化为 JSON 字符串后的精确字节数计入账本,并在发送终态消息前,根据组合账本的剩余额度预检分离后的完成值或程序异常。因此,即使抛出的字符串或堆栈极大,通过 worker 端口的也只会是固定的 `output-limit` 诊断。宿主侧会针对伪造流量以及 worker 无法观察的原生管道写入,重复执行这套面向不可信对端的账本校验。固定的 `PtcRunResult` 字段名、花括号、有界的错误类型标签及后续展示空白有意不计入这份可变负载账本。这两个阶段都不会实际生成超出上限的完成值序列化结果。结果不超过上限时会保持精确。完成值无法通过无损 JSON 快照时,以 `invalid-output` 失败;值、诊断或包含日志的组合结果超过上限时,以 `output-limit` 失败,而不会变成检查格式化后或截断的文本。
|
||||
|
||||
@@ -73,13 +73,13 @@ PTC mode 通过运行时请求中的 `{ name: "ToolCallError", memberNamePropert
|
||||
|
||||
### 持久化、元数据与 spill
|
||||
|
||||
嵌套分发在 `tool/ptc-dispatch` 上记录子调用完整渲染后的 `content`/`isError`,但不会持久化规范值。`tool/result` 继续只持久化渲染后的内容、错误和可选元数据。包含图片的成功最终内容序列还会包装成带来源归属的用户消息,并经外层结果延后;普通会话事件使该模型可见输入可以重建。[PTC mode note](2026-06-15-ptc.zh.md) 负责持久事件名称与历史标识保留规则;回放无法重建程序的规范中间值。
|
||||
嵌套分发在 `tool/ptc-dispatch` 上记录子调用完整渲染后的 `content`/`isError`,但不会持久化规范值。TypeScript 程序通过显式展示为外层 `tool/result.content` 选择图片。其他语言消费方将成功的含图片内容作为带来源归属的用户消息,延后到外层结果之后。普通 Session 事件使这些模型可见输入可以重建。[PTC mode note](2026-06-15-ptc.zh.md)负责持久事件名称与历史标识保留规则;回放无法重建程序的规范中间值。
|
||||
|
||||
不透明的 `exec.parent` token 用于标识嵌套调用。展示元数据以及通用或工具自有的 spill 投影都会跳过这些调用;它们的规范值永远不会进入上下文。Client 可以从原始分发事件派生[嵌套 terminal 卡片](../../../../packages/client/ui-tool/README.zh.md),无需元数据。外层 `run_code` 调用产生面向模型的结果,并且可能对 post-policy 处理后的最终展示执行 spill;`run_code` 有意既不声明结果展示器,也不声明展示元数据,因此 UI 适配器会通过通用的原始内容回退机制,使用持久化的 `tool/result.content` 补全该卡片。
|
||||
|
||||
## 测试
|
||||
|
||||
编译期测试与快照测试锁定了精确的 `ToolArgsMap`、`ToolOutputMap`、`ToolName`、schema 到 TypeScript 的覆盖范围、特殊名称,以及组装后的 PTC mode 图片转发。注册表与真实 worker 测试覆盖标量、数组、对象和 null 值;字符串原文渲染;缺席的 `undefined`;消费方声明、实际用于拒绝 Promise 的异常类,包括 `ToolCallError`;无效参数与完成值,包括伪装为内建原型的伪造原型;模型代码修改过的 JSON 边界全局对象、原型方法、构造函数槽位,以及继承而来的属性描述符字段;上述修改后的类型化绑定失败;不设上限的大型中间绑定值;嵌套输出落盘抑制;通用含图片上下文延后以及 post-execute 替换/阻止优先级;64 MiB 上限内外的精确计量;日志、值与诊断的组合计量;抛出的超大堆栈;有界失败的输出落盘;不可信对端伪造的流量;以及构建后包的执行。
|
||||
编译期测试与快照测试锁定了精确的 `ToolArgsMap`、`ToolOutputMap`、`ToolName`、schema 到 TypeScript 的覆盖范围、特殊名称,以及组装后的 PTC mode 图片投影。注册表与真实 worker 测试覆盖标量、数组、对象和 null 值;字符串原文渲染;缺席的 `undefined`;消费方声明、实际用于拒绝 Promise 的异常类,包括 `ToolCallError`;无效参数与完成值,包括伪装为内建原型的伪造原型;模型代码修改过的 JSON 边界全局对象、原型方法、构造函数槽位,以及继承而来的属性描述符字段;上述修改后的类型化绑定失败;不设上限的大型中间绑定值;嵌套输出落盘抑制;按消费方规则投影图片以及 post-execute 替换/阻止优先级;64 MiB 上限内外的精确计量;日志、值与诊断的组合计量;抛出的超大堆栈;有界失败的输出落盘;不可信对端伪造的流量;以及构建后包的执行。
|
||||
|
||||
无密钥的真实 worker 集成测试锁定了自然语言结果无法安全支持的两种句柄工作流。后台 bash 调用返回 job id,外层运行结束,之后的运行再根据该 id 轮询直至任务完成;其他用例分别证明,预先中止不会创建任务、发布后的调用取消会保留任务、前台执行仍与信号耦合,并且由 `job_kill` 负责取消。Cordis 程序会直接读取 active 或 pending 挂载的 id 和 `waitingFor` 字段,按该 id 卸载,并在不解析渲染文本的情况下确认挂载已移除。
|
||||
|
||||
@@ -93,9 +93,9 @@ PTC mode 通过运行时请求中的 `{ name: "ToolCallError", memberNamePropert
|
||||
|
||||
**静默检查格式化或截断过大的完成值:**不予采纳。把 JSON 值改成字符串既有损又违反类型。显式的 `output-limit` 失败让模型可以选择返回更小的结果,而保留的日志和诊断仍可使用普通的外层 spill 机制。
|
||||
|
||||
**要求每个丰富叶子工具检查 `exec.parent` 并自行延后。** 不予采用,因为这会把叶子工具与 PTC mode 内部机制耦合、重复策略处理,并遗漏未来丰富工具。分发桥接层负责从已经结算的最终结果通用转发。
|
||||
**要求每个丰富叶子工具检查 `exec.parent` 并自行延后。** 不予采用,因为这会把叶子工具与 PTC mode 内部机制耦合,并重复策略处理。分发桥接层负责按消费方规则,从已经结算的最终结果投影图片。
|
||||
|
||||
**把 Native 丰富内容暴露为每个绑定规范值的一部分。** 不予采用,因为规范值是无损 JSON 且由工具定义;附件块是具有持久生命周期语义的模型投影。保持值与投影分离,既能保留类型化程序,也不会从后续模型上下文中丢弃图片。
|
||||
**把 Native 丰富内容暴露为每个绑定规范值的一部分。** 不予采用,因为规范值是无损 JSON 且由工具定义;附件块是具有持久生命周期语义的模型投影。消费方拥有的程序 Image 绑定保留类型化工具值,并将图片选择交给程序。
|
||||
|
||||
## 后果
|
||||
|
||||
@@ -111,6 +111,6 @@ worker 会以嵌套深度有界的扁平协议格式传输数据并执行无损
|
||||
- 中间值没有字节上限,可能因值的保留、扁平协议格式副本或结构化克隆开销而耗尽进程或 worker 内存。
|
||||
- 64 MiB 硬上限只适用于外层可变负载,不计固定的结果封装语法与展示空白;spill 无法恢复超出该上限后被拒绝的字节。
|
||||
- 提供方或执行器的采集上限可能在规范值到达 PTC mode 前就已丢弃部分源数据。
|
||||
- 不支持的 MCP 输出 schema 会回退为 `JsonValue`;已准入的 MCP 图片使用通用延后投影,而音频和嵌入资源载荷仍只提供诊断。
|
||||
- 不支持的 MCP 输出 schema 回退为 `JsonValue`;图片投影遵循上述消费方规则,音频和嵌入资源负载仍仅用于诊断。
|
||||
- 嵌套调用没有独立的面向模型的结果;Client 子调用卡片不会增加模型上下文。
|
||||
- PTC mode 失败只暴露 `ToolCallError` 的消息与工具名,不提供程序可用的错误代码联合。
|
||||
@@ -0,0 +1,107 @@
|
||||
name: Package CUA helper · Windows · x64
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
outputs:
|
||||
artifact-id:
|
||||
description: '包含 win-x64 helper ZIP 的产物编号'
|
||||
value: ${{ jobs.package.outputs.artifact-id }}
|
||||
run-id:
|
||||
description: '产物所属的构建运行编号'
|
||||
value: ${{ jobs.package.outputs.run-id }}
|
||||
build-key:
|
||||
description: '提交 SHA、.NET SDK 版本和 win-x64 RID 的 SHA-256 摘要'
|
||||
value: ${{ jobs.package.outputs.build-key }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: pwsh
|
||||
outputs:
|
||||
artifact-id: ${{ steps.upload.outputs.artifact-id }}
|
||||
run-id: ${{ github.run_id }}
|
||||
build-key: ${{ steps.metadata.outputs.build-key }}
|
||||
steps:
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
- name: Restore locked Windows helper and run protocol checks
|
||||
run: |
|
||||
dotnet restore packages/experimental/cua-windows/native/cua-windows.slnx --locked-mode
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
dotnet run --project packages/experimental/cua-windows/native/tests/Protocol.Tests.csproj --configuration Release --no-restore
|
||||
- name: Publish Windows CUA helper
|
||||
run: >-
|
||||
dotnet publish packages/experimental/cua-windows/native/entry/CuaWindows.csproj
|
||||
--configuration Release
|
||||
--runtime win-x64
|
||||
--self-contained true
|
||||
--no-restore
|
||||
--output dist/cua-helper-win-x64
|
||||
# Standalone helper zip contract: flat root with cua-helper.json,
|
||||
# dsh-cua-windows.exe, and its runtime files. Installed providers place
|
||||
# the same payload under native/bin/win-x64.
|
||||
- name: Add helper metadata
|
||||
id: metadata
|
||||
env:
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$directory = 'dist/cua-helper-win-x64'
|
||||
$exe = Join-Path $directory 'dsh-cua-windows.exe'
|
||||
if (-not (Test-Path $exe -PathType Leaf)) { throw "helper executable is missing: $exe" }
|
||||
$version = (Get-Content package.json -Raw | ConvertFrom-Json).version
|
||||
$commit = $env:GITHUB_SHA
|
||||
$sdk = (& dotnet --version).Trim()
|
||||
$keyBytes = [Text.Encoding]::UTF8.GetBytes("$commit`n$sdk`nwin-x64")
|
||||
$buildKey = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($keyBytes)).ToLowerInvariant()
|
||||
$files = [ordered]@{}
|
||||
Get-ChildItem $directory -Recurse -File | Sort-Object FullName | ForEach-Object {
|
||||
$relative = [IO.Path]::GetRelativePath($directory, $_.FullName) -replace '\\', '/'
|
||||
$files[$relative] = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
# sha256 is the executable alias of files[dsh-cua-windows.exe].
|
||||
# signing is an enum: unsigned or authenticode.
|
||||
$metadata = [ordered]@{
|
||||
schemaVersion = 1
|
||||
platform = 'windows'
|
||||
buildKey = $buildKey
|
||||
version = $version
|
||||
commit = $commit
|
||||
architecture = 'x64'
|
||||
executable = 'dsh-cua-windows.exe'
|
||||
sha256 = $files['dsh-cua-windows.exe']
|
||||
signing = 'unsigned'
|
||||
files = $files
|
||||
}
|
||||
$json = $metadata | ConvertTo-Json -Depth 5
|
||||
Set-Content -Path (Join-Path $directory 'cua-helper.json') -Value "$json`n" -NoNewline -Encoding utf8NoBOM
|
||||
Add-Content -Path $env:GITHUB_OUTPUT -Value "build-key=$buildKey`n" -NoNewline -Encoding utf8
|
||||
- name: Archive publish output
|
||||
run: |
|
||||
$zipPath = 'dist/cua-helper-win-x64.zip'
|
||||
Compress-Archive -Path dist/cua-helper-win-x64/* -DestinationPath $zipPath
|
||||
$zip = [IO.Compression.ZipFile]::OpenRead((Resolve-Path $zipPath).Path)
|
||||
try {
|
||||
foreach ($name in 'dsh-cua-windows.exe', 'cua-helper.json') {
|
||||
if (-not $zip.GetEntry($name)) { throw "Missing root entry in helper ZIP: $name" }
|
||||
}
|
||||
} finally { $zip.Dispose() }
|
||||
- uses: actions/[email protected]
|
||||
id: upload
|
||||
with:
|
||||
name: cua-helper-${{ github.run_id }}-${{ github.run_attempt }}-${{ steps.metadata.outputs.build-key }}
|
||||
path: dist/cua-helper-win-x64.zip
|
||||
if-no-files-found: error
|
||||
compression-level: 0
|
||||
retention-days: 14
|
||||
@@ -189,7 +189,7 @@ describe.skipIf(!existsSync(join(root, 'apps/cli/lib/bin.js')))('Official option
|
||||
expect(names(minimal.agent)).toContain('session_search')
|
||||
await turn(coded.agent, 'Read this session lineage through run_code.', [
|
||||
toolCallResponse('ptc-search', 'run_code', {
|
||||
code: 'console.log(await tools.session_trace({}));',
|
||||
code: 'inspect(await tools.session_trace({}));',
|
||||
description: 'Read the current session lineage',
|
||||
}), textResponse('PTC_SEARCH_DONE'),
|
||||
])
|
||||
@@ -340,7 +340,7 @@ describe.skipIf(!existsSync(join(root, 'apps/cli/lib/bin.js')))('Official option
|
||||
if (coded === undefined) throw new Error('the PTC Agent was not created')
|
||||
await turn(coded, 'Read this session lineage and load the badge through one program.', [
|
||||
toolCallResponse('combined-program', 'run_code', {
|
||||
code: 'console.log(await tools.session_trace({})); console.log(await tools.skill({ name: "dsh-badge" }));',
|
||||
code: 'inspect(await tools.session_trace({})); inspect(await tools.skill({ name: "dsh-badge" }));',
|
||||
description: 'Use optional session search and badge together',
|
||||
}), textResponse('COMBINED_DONE'),
|
||||
])
|
||||
|
||||
@@ -38,7 +38,7 @@ import * as CordisInspectProviders from '@deepseek-ai/dsh-tool-cordis/host'
|
||||
*/
|
||||
|
||||
const PERSONA = 'You are a coding agent. You work by writing TypeScript programs for run_code: '
|
||||
+ 'batch related tool work into one program and print or return ONLY the findings that matter.'
|
||||
+ 'batch related tool work into one program and inspect ONLY the findings that matter.'
|
||||
const WORKSPACE_PROBE = 'dragonfruit-8675309'
|
||||
|
||||
let ctx: Context | undefined
|
||||
@@ -106,14 +106,17 @@ function runCode(
|
||||
})
|
||||
}
|
||||
|
||||
/** Read the optional completion from a successful canonical `run_code` value. */
|
||||
/** Read a JSON value explicitly inspected by the program. */
|
||||
function completion(result: ToolExecutionResult): unknown {
|
||||
if (result.isError) {
|
||||
throw new Error(result.content.filter(block => block.type === 'text').map(block => block.text).join('\n'))
|
||||
}
|
||||
const value = result.value
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('invalid run_code result')
|
||||
return value.result
|
||||
if (!Array.isArray(value.logs)) throw new Error('run_code result has no captured output')
|
||||
const text = value.logs.at(-1)
|
||||
if (typeof text !== 'string') throw new Error('run_code did not inspect a JSON value')
|
||||
return JSON.parse(text)
|
||||
}
|
||||
|
||||
async function mountRuntime(harness: Context): Promise<void> {
|
||||
@@ -184,7 +187,7 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
|
||||
exposesInfo: 'info' in error,
|
||||
};
|
||||
}
|
||||
return { length: large.length, failure };
|
||||
inspect(JSON.stringify({ length: large.length, failure }));
|
||||
`))
|
||||
|
||||
expect(value).toEqual({
|
||||
@@ -211,12 +214,12 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
|
||||
description: 'Run completion marker in background',
|
||||
run_in_background: true,
|
||||
});
|
||||
return started.jobId;
|
||||
inspect(JSON.stringify(started.jobId));
|
||||
`))
|
||||
expect(jobId).toMatch(/^bash-[0-9a-f]{16}$/)
|
||||
|
||||
const polled = completion(await runCode(ctx, `
|
||||
return await tools.job_output({ job_id: ${JSON.stringify(jobId)}, wait: true, timeout_ms: 5000 });
|
||||
inspect(JSON.stringify(await tools.job_output({ job_id: ${JSON.stringify(jobId)}, wait: true, timeout_ms: 5000 })));
|
||||
`))
|
||||
if (typeof polled !== 'object' || polled === null || Array.isArray(polled)) throw new Error('invalid job_output completion')
|
||||
const taskOutput = polled as Record<string, unknown>
|
||||
@@ -231,7 +234,7 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
|
||||
const pre = new AbortController()
|
||||
pre.abort('pre-aborted')
|
||||
const preResult = await runCode(ctx, `
|
||||
return await tools.bash({ command: 'sleep 10', description: 'Must never start', run_in_background: true });
|
||||
inspect(JSON.stringify(await tools.bash({ command: 'sleep 10', description: 'Must never start', run_in_background: true })));
|
||||
`, pre.signal)
|
||||
expect(preResult.isError).toBe(true)
|
||||
expect(ctx.jobs.list()).toEqual([])
|
||||
@@ -246,7 +249,7 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
|
||||
const afterPublication = new AbortController()
|
||||
const running = runCode(ctx, `
|
||||
const started = await tools.bash({ command: 'sleep 10', description: 'Wait for explicit task kill', run_in_background: true });
|
||||
console.log(started.jobId);
|
||||
inspect(started.jobId);
|
||||
await new Promise(() => {});
|
||||
`, afterPublication.signal)
|
||||
await Promise.race([
|
||||
@@ -265,11 +268,11 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
|
||||
expect(ctx.jobs.list()[0]).toMatchObject({ id: job!.id, status: 'running' })
|
||||
|
||||
const killed = completion(await runCode(ctx, `
|
||||
return await tools.job_kill({ job_id: ${JSON.stringify(job!.id)}, reason: 'test owns cancellation' });
|
||||
inspect(JSON.stringify(await tools.job_kill({ job_id: ${JSON.stringify(job!.id)}, reason: 'test owns cancellation' })));
|
||||
`))
|
||||
expect(killed).toMatchObject({ outcome: 'cancellation-requested', job: { id: job!.id } })
|
||||
const settled = completion(await runCode(ctx, `
|
||||
return await tools.job_output({ job_id: ${JSON.stringify(job!.id)}, wait: true, timeout_ms: 5000 });
|
||||
inspect(JSON.stringify(await tools.job_output({ job_id: ${JSON.stringify(job!.id)}, wait: true, timeout_ms: 5000 })));
|
||||
`))
|
||||
expect(settled).toMatchObject({ job: { id: job!.id, status: 'killed' } })
|
||||
})
|
||||
@@ -280,7 +283,7 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
|
||||
const controller = new AbortController()
|
||||
const startedAt = Date.now()
|
||||
const pending = runCode(ctx, `
|
||||
return await tools.bash({ command: 'sleep 10', description: 'Run cancellable foreground command' });
|
||||
inspect(JSON.stringify(await tools.bash({ command: 'sleep 10', description: 'Run cancellable foreground command' })));
|
||||
`, controller.signal)
|
||||
setTimeout(() => { controller.abort('stop-foreground') }, 200)
|
||||
const result = await pending
|
||||
@@ -306,7 +309,7 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
|
||||
const inspected = await tools.cordis_inspect_query({
|
||||
platform: provider.platform, provider: provider.id, method: provider.methods[0].name,
|
||||
});
|
||||
return { provider: provider.id, names: inspected.data.tools.map(tool => tool.name) };
|
||||
inspect(JSON.stringify({ provider: provider.id, names: inspected.data.tools.map(tool => tool.name) }));
|
||||
`, testToolSignal, agent))
|
||||
|
||||
expect(value).toEqual({ provider: 'Tool', names: ['cordis_inspect_list', 'cordis_inspect_query'] })
|
||||
@@ -335,7 +338,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('PTC mode: real model writes a pr
|
||||
type: 'text',
|
||||
text: 'Using one run_code program: run `echo alpha-7` with the bash tool, run `echo beta-9` with the bash tool, '
|
||||
+ 'then write both outputs joined by a plus sign into combined.txt (bash heredoc or redirect), '
|
||||
+ 'and return only the joined string.',
|
||||
+ 'and inspect the joined string. Reply with that string only.',
|
||||
}], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
const events: readonly SessionEvent[] = agent.session.snapshotEvents()
|
||||
|
||||
@@ -46,7 +46,7 @@ it.for(['standard', 'ptc'] as const)('keeps %s plugin changes pending until the
|
||||
} catch (error) { fixtureError = error }
|
||||
const call = mode === 'ptc'
|
||||
? { type: 'tool_use', id: `manage-${requests.length}`, name: 'run_code', input: {
|
||||
code: `return await tools.plugin_manager(${JSON.stringify(args)});`, description: 'Disable web tools after restart',
|
||||
code: `inspect(await tools.plugin_manager(${JSON.stringify(args)}));`, description: 'Disable web tools after restart',
|
||||
} }
|
||||
: { type: 'tool_use', id: `manage-${requests.length}`, name: 'plugin_manager', input: args }
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
||||
|
||||
@@ -10,11 +10,12 @@ import { pnpmCommand } from '../../../scripts/release/process.ts'
|
||||
|
||||
const repository = resolve(import.meta.dirname, '../../..')
|
||||
const providerName = '@deepseek-ai/dsh-experimental-cua-macos'
|
||||
const windowsProvider = '@deepseek-ai/dsh-cua-windows'
|
||||
const bundleName = '@deepseek-ai/dsh-cua-bundle'
|
||||
const readManifest = async (path: string): Promise<Record<string, unknown>> =>
|
||||
JSON.parse(await readFile(join(repository, path, 'package.json'), 'utf8')) as Record<string, unknown>
|
||||
|
||||
it('fetches the macOS optional tarball only for Darwin, with an empty cache for every target', async () => {
|
||||
it('fetches the platform-declared CUA provider tarballs with an empty cache for every target', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'cua-cold-install-'))
|
||||
onTestFinished(() => rm(root, { recursive: true, force: true }))
|
||||
const userconfig = join(root, 'npmrc')
|
||||
@@ -37,17 +38,22 @@ it('fetches the macOS optional tarball only for Darwin, with an empty cache for
|
||||
const cli = await readManifest('apps/cli')
|
||||
const bundle = await readManifest('packages/experimental/cua-bundle')
|
||||
const native = await readManifest('packages/experimental/cua-macos')
|
||||
const windows = await readManifest('packages/experimental/cua-windows')
|
||||
expect(cli.dependencies).toHaveProperty(bundleName)
|
||||
expect(bundle.dependencies).not.toHaveProperty(providerName)
|
||||
expect(bundle.optionalDependencies).toEqual({ [providerName]: 'workspace:*' })
|
||||
expect(bundle.dependencies).not.toHaveProperty(windowsProvider)
|
||||
expect(bundle.optionalDependencies).toEqual({ [providerName]: 'workspace:*', [windowsProvider]: 'workspace:*' })
|
||||
expect(windows.os).toEqual(['win32', 'linux'])
|
||||
expect(native.os).toEqual(['darwin'])
|
||||
const version = '1.0.0'
|
||||
const bundleDependencies = Object.fromEntries(Object.keys(bundle.dependencies as Record<string, string>).map(name => [name, version]))
|
||||
const manifests = [
|
||||
{ name: cli.name, version, dependencies: { [bundleName]: version } },
|
||||
{ name: bundleName, version, dependencies: bundleDependencies, optionalDependencies: { [providerName]: version } },
|
||||
{ name: bundleName, version, dependencies: bundleDependencies,
|
||||
optionalDependencies: { [providerName]: version, [windowsProvider]: version } },
|
||||
...Object.keys(bundleDependencies).map(name => ({ name, version })),
|
||||
{ name: providerName, version, os: native.os, files: native.files },
|
||||
{ name: windowsProvider, version, os: windows.os, files: windows.files },
|
||||
]
|
||||
const generation = 'a'.repeat(64)
|
||||
const nativePayload = `native/bin/darwin-arm64/${generation}/DSH Computer Use.app/Contents/MacOS/dsh-cua-native`
|
||||
@@ -121,5 +127,7 @@ it('fetches the macOS optional tarball only for Darwin, with an empty cache for
|
||||
expect(requests).toContain('/@deepseek-ai/dsh-cua.tgz')
|
||||
expect(requests.includes(`/${providerName}.tgz`), `${target}: ${JSON.stringify(requests)}\n${output}`)
|
||||
.toBe(target === 'darwin' || unrestricted)
|
||||
expect(requests.includes(`/${windowsProvider}.tgz`), `${target}: ${JSON.stringify(requests)}\n${output}`)
|
||||
.toBe(target !== 'darwin')
|
||||
}
|
||||
}, 120_000)
|
||||
@@ -21,7 +21,7 @@ it('disables unsupported bundles, shows the Host restriction, and refuses direct
|
||||
await writeFile(join(root, 'package.json'), JSON.stringify({ name, version: '1.0.0', dsh: { bundle: {
|
||||
patch: './cordis.patch.yml', platforms: [process.platform === 'darwin' ? 'win32' : 'darwin'],
|
||||
} } }))
|
||||
await writeFile(join(root, 'cordis.patch.yml'), '- id: ptc-runtime\n config:\n executionMode: persistent-in-memory\n')
|
||||
await writeFile(join(root, 'cordis.patch.yml'), '- id: ptc-runtime\n config:\n executionMode: persistent-in-memory\n replScoped: false\n')
|
||||
const scaffold = owned.scaffold = await launchWebScaffold({ profile: { packages: [{ dir: root }] } })
|
||||
const browser = owned.browser = await chromium.launch()
|
||||
const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
@@ -44,8 +44,9 @@ it('disables unsupported bundles, shows the Host restriction, and refuses direct
|
||||
const names = [name]
|
||||
await page.locator('[data-plugin-panel]').getByRole('button', { name: '更多', exact: true }).click()
|
||||
const cua = page.locator('[data-plugin-package="@deepseek-ai/dsh-cua-bundle"]')
|
||||
expect(await cua.count()).toBe(process.platform === 'darwin' ? 1 : 0)
|
||||
if (process.platform !== 'darwin') {
|
||||
const cuaSupported = process.platform === 'darwin' || process.platform === 'win32'
|
||||
expect(await cua.count()).toBe(cuaSupported ? 1 : 0)
|
||||
if (!cuaSupported) {
|
||||
expect((await scaffold.ctx.pluginManager.listBundles()).find(bundle => bundle.name === '@deepseek-ai/dsh-cua-bundle'))
|
||||
.toMatchObject({ listedByDefault: false, enabled: false, error: { code: 'unsupported-platform' } })
|
||||
names.push('@deepseek-ai/dsh-cua-bundle')
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
webSnapshotMode,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { conversationContextKey, expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
import { conversationContextKey, expandOwningTurnProcess, loadEarlierChatPage, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const MODE = webSnapshotMode()
|
||||
const SESSION_ID = 'chat-long-interactions-e2e'
|
||||
@@ -219,10 +219,9 @@ describe('web e2e: long Chat interaction contract', () => {
|
||||
))
|
||||
expect(await markPitch()).toBe(10)
|
||||
|
||||
const loadEarlier = page.getByRole('button', { name: 'Load earlier', exact: true })
|
||||
const loadedRows = page.locator('[data-chat-flow-key]')
|
||||
const loadedBefore = await loadedRows.count()
|
||||
await loadEarlier.click()
|
||||
await loadEarlierChatPage(page)
|
||||
await expect.poll(() => loadedRows.count(), { timeout: 15_000 }).toBeGreaterThan(loadedBefore)
|
||||
await railScroller.hover()
|
||||
await page.mouse.wheel(0, -FIXTURE_TURNS * 10)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** CUA follows PTC Agent scopes and changes the default only from its detail page. */
|
||||
/** CUA selects each Agent's tool form and changes the default only from its detail page. */
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -13,7 +13,7 @@ import { expect, it, vi } from 'vitest'
|
||||
import { captureStableAria, compareOrRefreshGolden, launchWebScaffold, webSnapshotMode, type WebScaffold } from './scaffold.ts'
|
||||
import { connectFreshWorkspaceZh, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
// The shipped bundle admits only macOS Hosts; unsupported Hosts are exercised in bundle-platform.e2e.ts.
|
||||
// This fixture exercises the macOS provider’s permission setup.
|
||||
it.skipIf(process.platform !== 'darwin').each([
|
||||
{ selectedDefault: undefined, codingTools: false, script: false },
|
||||
{ selectedDefault: undefined, codingTools: true, script: false },
|
||||
@@ -110,8 +110,8 @@ it.skipIf(process.platform !== 'darwin').each([
|
||||
expect(profileManifest().dependencies).toEqual(dependencies)
|
||||
expect(await card.getByText('实验性', { exact: true }).count()).toBe(1)
|
||||
expect(await card.textContent()).not.toContain('将 PTC 模式改为可持久化')
|
||||
expect(await card.textContent()).toContain('请切换到 PTC 模式')
|
||||
expect(await card.textContent()).toContain('Windows 支持正在开发中')
|
||||
expect(await card.textContent()).not.toContain('请切换到 PTC 模式')
|
||||
expect(await card.textContent()).toContain('通过 npm 安装 DSH 的 Windows 用户需要单独配置 helper')
|
||||
expect(await card.textContent()).not.toContain('Swift')
|
||||
const description = await card.getByRole('button', { name: '查看 Computer Use', exact: true }).evaluate((button) => {
|
||||
const element = document.getElementById(button.getAttribute('aria-describedby')!)!
|
||||
@@ -126,11 +126,14 @@ it.skipIf(process.platform !== 'darwin').each([
|
||||
expect(scaffold.ctx.agents.get(ptcAgent.id) === ptcAgent).toBe(true)
|
||||
expect(scaffold.ctx.agentPresets.composedPreset(originalAgent.ctx)).toBe(initialDefault)
|
||||
expect(scaffold.ctx.tools.schemas(originalAgent).some(tool => tool.name === 'cua')).toBe(false)
|
||||
expect(scaffold.ctx.tools.schemas(originalAgent).some(tool => tool.name === 'cua_run_code')).toBe(true)
|
||||
expect(scaffold.ctx.tools.schemas(ptcAgent).some(tool => tool.name === 'cua_run_code')).toBe(false)
|
||||
expect(scaffold.ctx.tools.schemas(ptcAgent).some(tool => tool.name === 'cua')).toBe(true)
|
||||
expect((await scaffold.ctx.systemPrompt.assemble({ scope: ptcAgent })).tools.map(tool => tool.name)).toEqual(['run_code'])
|
||||
for (const preset of ['ptc', 'standard', 'ptc', initialDefault]) {
|
||||
await scaffold.ctx.agentPresets.select(originalAgent, preset)
|
||||
expect(scaffold.ctx.tools.schemas(originalAgent).some(tool => tool.name === 'cua')).toBe(preset === 'ptc')
|
||||
expect(scaffold.ctx.tools.schemas(originalAgent).some(tool => tool.name === 'cua_run_code')).toBe(preset !== 'ptc')
|
||||
}
|
||||
const child = await scaffold.ctx.agents.create({
|
||||
sessionId: SessionId('cua-child'),
|
||||
@@ -163,6 +166,7 @@ it.skipIf(process.platform !== 'darwin').each([
|
||||
})).agent
|
||||
expect(scaffold.ctx.agentPresets.composedPreset(freshDefault.ctx)).toBe(initialDefault)
|
||||
expect(scaffold.ctx.tools.schemas(freshDefault).some(tool => tool.name === 'cua')).toBe(false)
|
||||
expect(scaffold.ctx.tools.schemas(freshDefault).some(tool => tool.name === 'cua_run_code')).toBe(true)
|
||||
const fresh = (await scaffold.ctx.agents.create({
|
||||
sessionId: SessionId('cua-new-ptc'),
|
||||
setup: async (ctx) => { await scaffold.ctx.agentPresets.mount(ctx, 'ptc') },
|
||||
@@ -170,17 +174,40 @@ it.skipIf(process.platform !== 'darwin').each([
|
||||
expect(scaffold.ctx.agentPresets.composedPreset(fresh.ctx)).toBe('ptc')
|
||||
expect(scaffold.ctx.tools.schemas(fresh).some(tool => tool.name === 'cua')).toBe(true)
|
||||
await card.getByRole('button', { name: '查看 Computer Use', exact: true }).click()
|
||||
await plugins.locator('[data-detail-document]').getByText('helperPath', { exact: true }).waitFor()
|
||||
toggle = page.locator('[data-plugin-panel]').getByRole('switch', { name: '启用 Computer Use', exact: true })
|
||||
const persistent = page.getByRole('group', { name: 'PTC 执行方式', exact: true })
|
||||
if (!codingTools) {
|
||||
const panel = page.locator('[data-cua-settings]')
|
||||
await expect.poll(() => panel.getAttribute('aria-busy')).toBe('false')
|
||||
expect(await panel.textContent()).toContain('标准、创造和 PTC 模式均可使用 Computer Use')
|
||||
expect(await persistent.textContent()).toContain('其他模式始终单独保留 Computer Use 状态')
|
||||
for (const width of [1680, 390]) {
|
||||
await page.setViewportSize({ width, height: 1000 })
|
||||
for (const colorScheme of ['light', 'dark'] as const) {
|
||||
await page.emulateMedia({ colorScheme })
|
||||
await expect.poll(() => page.evaluate(() => document.body.hasAttribute('data-ds-dark-theme'))).toBe(colorScheme === 'dark')
|
||||
await expect.poll(() => panel.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true)
|
||||
if (webSnapshotMode() === 'refresh') {
|
||||
const directory = fileURLToPath(new URL('../../../.playwright-mcp/cua-run-code-ui', import.meta.url))
|
||||
await mkdir(directory, { recursive: true })
|
||||
await panel.screenshot({ path: join(directory, `details-zh-${width}-${colorScheme}.png`), animations: 'disabled' })
|
||||
}
|
||||
}
|
||||
}
|
||||
await page.emulateMedia({ colorScheme: null })
|
||||
await page.setViewportSize({ width: 1680, height: 1000 })
|
||||
}
|
||||
const beforePersistent = scaffold.ctx.ptcRuntime
|
||||
await persistent.getByRole('button', { name: '设为持久化', exact: true }).click()
|
||||
await persistent.getByRole('button', { name: '已设为持久化', exact: true }).waitFor()
|
||||
expect(scaffold.ctx.ptcRuntime.programFormat).toBe(script ? 'script' : 'repl')
|
||||
expect(scaffold.ctx.ptcRuntime.programFormat).toBe(script ? 'script' : 'scoped-repl')
|
||||
expect(scaffold.ctx.ptcRuntime.retainsState).toBe(true)
|
||||
expect(scaffold.ctx.ptcRuntime === beforePersistent).toBe(false)
|
||||
saved = await readFile(settingsPath, 'utf8')
|
||||
expect(saved).toContain(`executionMode: ${script ? 'vm-script' : 'persistent-in-memory'}`)
|
||||
expect(saved).toContain(`scriptPersistence: ${script}`)
|
||||
expect(saved).toContain(`replScoped: ${!script}`)
|
||||
const runtime = scaffold.ctx.ptcRuntime
|
||||
const persistentRuntimeFiber = runtimeFiber()
|
||||
const owner = {}
|
||||
@@ -192,6 +219,9 @@ it.skipIf(process.platform !== 'darwin').each([
|
||||
expect(runtimeFiber() === persistentRuntimeFiber).toBe(true)
|
||||
expect(scaffold.ctx.agentPresets.defaultId).toBe(initialDefault)
|
||||
expect(scaffold.ctx.agents.get(ptcAgent.id) === ptcAgent).toBe(true)
|
||||
for (const agent of [originalAgent, freshDefault, ptcAgent, fresh]) {
|
||||
expect(scaffold.ctx.tools.schemas(agent).some(tool => tool.name === 'cua_run_code')).toBe(false)
|
||||
}
|
||||
for (const agent of [ptcAgent, fresh]) {
|
||||
expect(scaffold.ctx.tools.schemas(agent).some(tool => tool.name === 'cua')).toBe(false)
|
||||
expect((await scaffold.ctx.systemPrompt.assemble({ scope: agent })).tools.map(tool => tool.name)).toEqual(['run_code'])
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/** Standard-mode CUA programs retain code cards, nested calls, images, and source inspection. */
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium, type Browser } from 'playwright'
|
||||
import { expect, it, onTestFailed, onTestFinished } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-tool-executor-ptc/types'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, selectedSessionFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const fixture = fileURLToPath(new URL('../../../snapshots/session/cua-run-code/session.v4.jsonl', import.meta.url))
|
||||
const snapshotDirectory = fileURLToPath(new URL('../../../snapshots/web/cua-run-code', import.meta.url))
|
||||
const mode = webSnapshotMode()
|
||||
|
||||
it.skipIf(mode === 'record')('shows standard CUA programs and their recorded source through the Web composition', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'cua-run-code-web-'))
|
||||
const resources: { scaffold?: WebScaffold; browser?: Browser } = {}
|
||||
onTestFinished(async () => {
|
||||
try { await resources.browser?.close() } finally {
|
||||
try { await resources.scaffold?.close() } finally { await rm(directory, { recursive: true, force: true }) }
|
||||
}
|
||||
})
|
||||
const patch = join(directory, 'cua-fixture.yml')
|
||||
await writeFile(patch, JSON.stringify([{ id: 'agent-default-model', config: {
|
||||
provider: 'deepseek-official', model: 'deepseek-v4-flash-vision-exp',
|
||||
} }, { insert: [
|
||||
{ id: 'computer-use', name: '@deepseek-ai/dsh-computer-use' },
|
||||
{ id: 'cua-run-code-fixture', name: fileURLToPath(new URL('../../../snapshots/session/cua-run-code/provider.mjs', import.meta.url)),
|
||||
config: { allowedApps: [{ id: 'a'.repeat(64), name: 'Fixture', executablePath: '/fixture/app' }] } },
|
||||
] }]))
|
||||
const scaffold = resources.scaffold = await launchWebScaffold({
|
||||
replayFixture: fixture, compareReplaySession: false, extraOverlayPath: patch,
|
||||
})
|
||||
const events: SessionEvent[] = []
|
||||
scaffold.ctx.on('session/event', (_session, event) => { events.push(event) })
|
||||
const browser = resources.browser = await chromium.launch()
|
||||
const page = await newEnglishPage(browser), tripwire = watchConsole(page)
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-cua-run-code'))
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
const agent = scaffold.ctx.agents.list()[0]!
|
||||
expect(scaffold.ctx.agentPresets.composedPreset(agent.ctx)).toBe('standard')
|
||||
expect(scaffold.ctx.tools.schemas(agent).map(tool => tool.name)).toContain('cua_run_code')
|
||||
expect(scaffold.ctx.tools.schemas(agent).map(tool => tool.name)).not.toContain('cua')
|
||||
const prompts = fixtureUserPrompts(await readFile(await selectedSessionFixture(fixture), 'utf8'))
|
||||
expect(prompts).toHaveLength(1)
|
||||
const prompt = prompts.at(0)
|
||||
if (prompt === undefined) throw new Error('The CUA recording must contain a user prompt')
|
||||
const settled = scaffold.whenTurnSettled()
|
||||
const input = page.locator('[data-composer-input]').first()
|
||||
await input.fill(prompt)
|
||||
await input.press('Enter')
|
||||
await settled
|
||||
const results = events.filter(event => event.type === 'tool/result')
|
||||
expect(results).toHaveLength(2)
|
||||
expect(results.every(event => !event.data.message.isError)).toBe(true)
|
||||
expect(events.filter(event => event.type === 'tool/ptc-dispatch')).toHaveLength(8)
|
||||
const calls = page.locator('[data-tool="cua_run_code"]')
|
||||
const description = 'Select and capture the fixture window'
|
||||
const turnControl = page.locator('[data-turn-process="1"]')
|
||||
await turnControl.waitFor({ state: 'visible' })
|
||||
await expect.poll(() => turnControl.getAttribute('aria-expanded')).toBe('false')
|
||||
await turnControl.click()
|
||||
await expect.poll(() => turnControl.getAttribute('aria-expanded')).toBe('true')
|
||||
const group = calls.first().locator('xpath=ancestor::*[@data-chat-group-key][1]')
|
||||
const groupControl = group.locator('[data-process-activity]').first()
|
||||
await groupControl.waitFor({ state: 'visible' })
|
||||
if (await groupControl.getAttribute('aria-expanded') === 'false') await groupControl.click()
|
||||
await expect.poll(() => groupControl.getAttribute('aria-expanded')).toBe('true')
|
||||
await expect.poll(() => calls.count()).toBe(2)
|
||||
await calls.first().getByRole('button', { name: `Code ${description}`, exact: true }).click()
|
||||
const source = calls.first().locator('pre.shiki')
|
||||
await source.waitFor()
|
||||
expect(await source.textContent()).toContain('var app = await cua.getApp("test.fixture")')
|
||||
expect(await page.locator('[data-subcalls] [data-tool="cua"]').count()).toBe(8)
|
||||
expect(events.filter(event => event.type === 'tool/ptc-dispatch' && event.data.content.some(block => block.type === 'image'))).toHaveLength(1)
|
||||
expect(events.filter(event => event.type === 'tool/result'
|
||||
&& event.data.message.content.some(block => block.type === 'image'))).toHaveLength(1)
|
||||
expect(await calls.first().textContent()).toContain('(1x1px, 90 bytes)')
|
||||
expect(await calls.first().textContent()).not.toContain('"type": "image"')
|
||||
await compareOrRefreshGolden(join(snapshotDirectory, 'ui.expected.md'),
|
||||
await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd), mode)
|
||||
if (mode === 'refresh') {
|
||||
const evidence = fileURLToPath(new URL('../../../.playwright-mcp/cua-run-code-ui', import.meta.url))
|
||||
await mkdir(evidence, { recursive: true })
|
||||
await page.screenshot({ path: join(evidence, 'standard-code-cards.png'), animations: 'disabled' })
|
||||
const viewport = page.viewportSize()
|
||||
await page.setViewportSize({ width: 800, height: 900 })
|
||||
await page.screenshot({ path: join(evidence, 'standard-code-cards-narrow.png'), animations: 'disabled' })
|
||||
if (viewport !== null) await page.setViewportSize(viewport)
|
||||
}
|
||||
await page.getByRole('tab', { name: 'Trajectory', exact: true }).click()
|
||||
const row = page.getByRole('row', { name: new RegExp(`cua_run_code ${description}`) })
|
||||
await row.click()
|
||||
await page.getByRole('tab', { name: 'Code', exact: true }).click()
|
||||
expect(await page.getByRole('tabpanel').textContent()).toContain('var app = await cua.getApp("test.fixture")')
|
||||
await page.getByRole('button', { name: 'Wrap lines', exact: true }).click()
|
||||
await compareOrRefreshGolden(join(snapshotDirectory, 'code.expected.md'),
|
||||
await captureStableAria(page, '[role="tabpanel"]', scaffold.workspaceCwd), mode)
|
||||
if (mode === 'refresh') {
|
||||
await page.screenshot({ path: fileURLToPath(new URL('../../../.playwright-mcp/cua-run-code-ui/source-inspector.png', import.meta.url)), animations: 'disabled' })
|
||||
}
|
||||
const screenshot = page.locator('tr[data-kind="subtool"]').filter({ hasText: '"method":"screenshot"' })
|
||||
await screenshot.click()
|
||||
await page.getByRole('tab', { name: 'Result', exact: true }).click()
|
||||
const image = page.getByRole('tabpanel').locator('img').first()
|
||||
await image.waitFor({ state: 'visible' })
|
||||
await expect.poll(() => image.evaluate((element: HTMLImageElement) => element.naturalWidth)).toBe(1)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
await assertFixtureInventory(snapshotDirectory, ['ui.expected.md', 'code.expected.md'])
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
- group "新会话默认模式":
|
||||
- strong: 新会话默认模式
|
||||
- paragraph: Computer Use 仅在 PTC 模式中可用。
|
||||
- paragraph: 标准、创造和 PTC 模式均可使用 Computer Use。在此选择 PTC,可将其设为新会话的默认模式
|
||||
- button "设为 PTC"
|
||||
@@ -2,11 +2,11 @@
|
||||
- heading "Authorize computer use" [level=3]
|
||||
- group "Default mode for new sessions":
|
||||
- strong: Default mode for new sessions
|
||||
- paragraph: Computer use is available only in PTC mode.
|
||||
- paragraph: Computer use is available in Standard, Creator, and PTC modes. Choose PTC here to make it the default for new sessions.
|
||||
- button "Set to PTC"
|
||||
- group "PTC execution mode":
|
||||
- strong: PTC execution mode
|
||||
- paragraph: Keep variables and application objects across tool calls. Switching restarts the PTC runtime.
|
||||
- paragraph: Keep variables and application objects across PTC tool calls. Switching restarts the PTC runtime. Other modes always keep their computer use state separately.
|
||||
- button "Use persistent mode"
|
||||
- group "Accessibility":
|
||||
- strong: Accessibility
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
webSnapshotMode,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
import { expandOwningTurnProcess, loadEarlierChatPage, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/minimal-preset', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.v3.jsonl')
|
||||
@@ -485,7 +485,7 @@ describe('minimal image reading', () => {
|
||||
page = await openSession(pagedId)
|
||||
await page.getByText('Later image-history message 53', { exact: true }).waitFor()
|
||||
expect(await page.locator('[data-tool="read_image"]').count()).toBe(0)
|
||||
await page.getByRole('button', { name: 'Load earlier', exact: true }).click()
|
||||
await loadEarlierChatPage(page)
|
||||
card = await imageCard(page)
|
||||
await expect.poll(() => card.locator('img').first().evaluate(img => img instanceof HTMLImageElement && img.naturalWidth)).toBe(1)
|
||||
|
||||
|
||||
@@ -796,7 +796,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
|
||||
const callId = ToolCallId('long-ptc-result')
|
||||
const call = agent.session.append('tool/call', {
|
||||
turn: 3, step: 1, callId, name: 'run_code',
|
||||
arguments: JSON.stringify({ code: 'console.log("PTC output")', description: 'Long program output' }),
|
||||
arguments: JSON.stringify({ code: 'inspect("PTC output")', description: 'Long program output' }),
|
||||
})
|
||||
await page.getByRole('tab', { name: 'Trajectory' }).click()
|
||||
await page.locator('tr[data-kind="tool"]').filter({ hasText: 'Long program output' }).click()
|
||||
|
||||
@@ -141,7 +141,7 @@ describe.skipIf(MODE === 'record')('web snapshot: Official native subagent bundl
|
||||
const firstTurnEvents = agent.session.snapshotEvents()
|
||||
const toolResults = firstTurnEvents.filter(event => event.type === 'tool/result').map(event => event.data)
|
||||
expect(toolResults, `PTC execution must start the native child before waiting for its result: ${JSON.stringify(toolResults)}`)
|
||||
.toMatchObject([{ message: { isError: false, content: [{ type: 'text', text: 'agent-1' }] } }])
|
||||
.toMatchObject([{ message: { isError: false, content: [{ type: 'text', text: 'agent-1\n' }] } }])
|
||||
const dispatches = firstTurnEvents.filter(event => event.type === 'tool/ptc-dispatch').map(event => event.data)
|
||||
expect(dispatches).toMatchObject([{ name: 'subagent_claude_code', isError: false }])
|
||||
const catalog = firstTurnEvents.filter(event => event.type === 'subagent/catalog').map(event => event.data)
|
||||
|
||||
@@ -857,7 +857,7 @@ describe('web e2e: plugin manager', () => {
|
||||
&& bundle.name !== '@deepseek-ai/dsh-experimental-liveshare'
|
||||
&& bundle.official && (bundle.listedByDefault !== false || bundle.enabled || bundle.installed)).length)
|
||||
// Computer Use ships with the installation and is listed only on the macOS Hosts it supports.
|
||||
const cuaSupported = process.platform === 'darwin'
|
||||
const cuaSupported = process.platform === 'darwin' || process.platform === 'win32'
|
||||
expect(bundles.find(bundle => bundle.name === '@deepseek-ai/dsh-cua-bundle'))
|
||||
.toMatchObject({ enabled: false, optional: true, availability: 'installation', ...cuaSupported ? {} : { listedByDefault: false } })
|
||||
expect(await panel.locator('[data-plugin-group="more"] [data-plugin-package="@deepseek-ai/dsh-cua-bundle"]').count())
|
||||
|
||||
@@ -57,7 +57,8 @@ fs.appendFileSync(${JSON.stringify(openLog)}, JSON.stringify({ path, action, con
|
||||
openInAppEnvironment: createLaunchEnvironmentSnapshot([{ source: 'process', values: { SSH_CONNECTION: '10.0.0.2 55000 10.0.0.9 22' } }]),
|
||||
extraOverlayPath: fileURLToPath(new URL('./present.overlay.yml', import.meta.url)),
|
||||
agentPresets: { default: 'ptc' }, compareReplaySession: true,
|
||||
...(MODE === 'record' ? {} : { replayFixture: FIXTURE }),
|
||||
// The recorded UI includes decode throughput from an incremental stream.
|
||||
...(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 }),
|
||||
})
|
||||
// File associations belong to the desktop rather than the recorded Session.
|
||||
const controller = scaffold.ctx.get('sessionController')
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-user-approval'
|
||||
import {
|
||||
assertFinalWorkspaceSnapshot, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
launchWebScaffold, recordFixture, selectedSessionFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
@@ -19,7 +19,7 @@ const UI_EXPECTED = join(SNAPSHOT_DIR, 'approval.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const PROMPT = 'Use run_code with timeoutMs 120000 and direct Node filesystem access to create approved.txt in the working directory containing exactly "approved\\n". '
|
||||
+ 'Use await import("node:fs/promises") and writeFile; do not call nested tools. First attempt the write under the current read-only sandbox without escalation. '
|
||||
+ 'In that first program, catch only filesystem errors with code EPERM, EACCES or EROFS and return exactly "EXPECTED_SANDBOX_DENIAL"; rethrow any other error. '
|
||||
+ 'In that first program, catch only filesystem errors with code EPERM, EACCES or EROFS and use `inspect(...)` to display exactly "EXPECTED_SANDBOX_DENIAL"; rethrow any other error. '
|
||||
+ 'If the sandbox denies it, explicitly retry the program with sandbox_permissions "workspace-write" and justification "Create the file requested by the user". '
|
||||
+ 'I will answer the approval prompt. After the file is written, reply DONE and stop.'
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('web e2e: PTC program sandbox escalation', () => {
|
||||
|
||||
it('keeps the file absent until approval and records the granted program', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-ptc-escalation'))
|
||||
if (MODE !== 'record') expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT])
|
||||
if (MODE !== 'record') expect(fixtureUserPrompts(await readFile(await selectedSessionFixture(FIXTURE), 'utf8'))).toEqual([PROMPT])
|
||||
const input = page.locator('[data-composer-input]').first()
|
||||
await input.waitFor({ timeout: 10_000 })
|
||||
await page.locator('[aria-label^="Access mode"]').click()
|
||||
|
||||
@@ -9,7 +9,7 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
acknowledgeReloadConnectionLoss, captureExpandedTurnProcessAria, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
launchWebScaffold, recordFixture, selectedSessionFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, openSettings, saveFailureShot } from './support.ts'
|
||||
|
||||
@@ -23,7 +23,7 @@ const MODE = webSnapshotMode()
|
||||
|
||||
// Elicits the successful and failed sub-rows this scenario asserts.
|
||||
const PROMPT = 'Using ONE run_code program: run bash `echo CODE_ROUND_OK`, then read the file missing.txt '
|
||||
+ 'catching its error in the program. Return an object with both outcomes. Then reply DONE and stop.'
|
||||
+ 'catching its error in the program. Use `inspect(...)` to display an object with both outcomes. Then reply DONE and stop.'
|
||||
|
||||
describe('web e2e: PTC mode round renders nested sub-calls', () => {
|
||||
let scaffold: WebScaffold
|
||||
@@ -58,7 +58,7 @@ describe('web e2e: PTC mode round renders nested sub-calls', () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-ptc-drive'))
|
||||
const requestStates: string[] = []
|
||||
if (MODE !== 'record') {
|
||||
expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT])
|
||||
expect(fixtureUserPrompts(await readFile(await selectedSessionFixture(FIXTURE), 'utf8'))).toEqual([PROMPT])
|
||||
}
|
||||
const input = page.locator('[data-composer-input]').first()
|
||||
await input.waitFor({ timeout: 10_000 })
|
||||
|
||||
@@ -1045,9 +1045,11 @@ describe('web e2e: shipped right Sidebar', () => {
|
||||
await proseChip(page).click()
|
||||
await expect.poll(async () => await tabTitles(first)).toEqual(['Files', SAMPLE_NAME])
|
||||
const order = await tabTitles(first)
|
||||
// The insertion index is measured against chip midpoints, not strip width.
|
||||
await dragTo(page, first.locator('[data-dockkit-tab]').last(),
|
||||
await pointIn(first.locator('[data-dockkit-tab]').first(), 0.25, 0.5),
|
||||
const tabs = first.locator('[data-dockkit-tab]')
|
||||
const firstTab = tabs.first()
|
||||
// Drag coordinates are viewport-relative. Wait for the strip to settle before measuring the first chip.
|
||||
await firstTab.hover({ trial: true })
|
||||
await dragTo(page, tabs.last(), await pointIn(firstTab, 0.25, 0.5),
|
||||
strip.locator('[data-dockkit-caret="0"]'))
|
||||
await expect.poll(async () => await tabTitles(first)).toEqual([...order].reverse())
|
||||
|
||||
|
||||
@@ -233,6 +233,17 @@ export async function scrollIntoView(target: Locator): Promise<void> {
|
||||
}), { timeout: 10_000 }).toBe(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Request one earlier page without scrolling the transcript to its automatic pagination threshold.
|
||||
* @param page - Page showing the English Chat history control.
|
||||
*/
|
||||
export async function loadEarlierChatPage(page: Page): Promise<void> {
|
||||
await page.getByRole('button', { name: 'Load earlier', exact: true }).evaluate((button) => {
|
||||
button.focus({ preventScroll: true })
|
||||
})
|
||||
await page.keyboard.press('Enter')
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand every eligible Turn process and secondary group so a Tool-focused
|
||||
* scenario can exercise the original row contract beneath product-default
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"tests/cua-optin.e2e.ts",
|
||||
"tests/bundle-platform.e2e.ts",
|
||||
"tests/cua-permissions.e2e.ts",
|
||||
"tests/cua-run-code.e2e.ts",
|
||||
"tests/schedule-header.e2e.ts",
|
||||
"tests/schedule-phone.e2e.ts",
|
||||
"tests/schedule-pickers.e2e.ts",
|
||||
|
||||
+28
-3
@@ -886,6 +886,26 @@ export interface Config {
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-credentials-local -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-cua-windows -->
|
||||
<a id="deepseek-aidsh-cua-windows"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-cua-windows`
|
||||
|
||||
- `inject`: `computerUse` · `tools` · `systemPrompt` · `subprocess` · `agents`
|
||||
- `refs`: [`CuaConfig`](../packages/experimental/cua/src/index.ts)
|
||||
- `source`: [`packages/experimental/cua-windows/src/index.ts:32`](../packages/experimental/cua-windows/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Windows native waiting policy, alongside shared CUA resource limits. */
|
||||
export interface Config extends CuaConfig {
|
||||
/** Windows PowerShell executable used to query WSL interoperation and the Windows architecture. */
|
||||
windowsPowerShellPath: string
|
||||
/** Milliseconds between application-readiness and clipboard-release observations. */
|
||||
pollIntervalMs: number
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-cua-windows -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-deepseek-account -->
|
||||
<a id="deepseek-aidsh-deepseek-account"></a>
|
||||
|
||||
@@ -1606,7 +1626,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-file-reference-local`
|
||||
|
||||
- `inject`: `agents` · `workingDirectory`
|
||||
- `source`: [`packages/context/file-reference-local/src/index.ts:35`](../packages/context/file-reference-local/src/index.ts)
|
||||
- `source`: [`packages/context/file-reference-local/src/index.ts:27`](../packages/context/file-reference-local/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Local file-reference discovery configuration. */
|
||||
@@ -2405,7 +2425,7 @@ export type Config = Readonly<Record<string, never>>
|
||||
## `@deepseek-ai/dsh-lsp-stdio`
|
||||
|
||||
- `inject`: `fs` · `lsp` · `subprocess`
|
||||
- `source`: [`packages/lsp/lsp-stdio/src/index.ts:83`](../packages/lsp/lsp-stdio/src/index.ts)
|
||||
- `source`: [`packages/lsp/lsp-stdio/src/index.ts:70`](../packages/lsp/lsp-stdio/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin configuration: provider id → local language-server configuration. */
|
||||
@@ -2858,7 +2878,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-ptc-runtime-node`
|
||||
|
||||
- `inject`: `fs` · `subprocess` · `sandbox` · `sandboxPolicy`
|
||||
- `source`: [`packages/ptc-runtime/ptc-runtime-node/src/config.ts:7`](../packages/ptc-runtime/ptc-runtime-node/src/config.ts)
|
||||
- `source`: [`packages/ptc-runtime/ptc-runtime-node/src/config.ts:8`](../packages/ptc-runtime/ptc-runtime-node/src/config.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Deployment-varying runtime bounds and launch choices. */
|
||||
@@ -2869,6 +2889,11 @@ export interface Config {
|
||||
executionMode?: 'per-call' | 'persistent-in-memory' | 'vm-script'
|
||||
/** Retain a Script process and context per stateOwner; only supported with executionMode: vm-script. */
|
||||
scriptPersistence?: boolean
|
||||
/**
|
||||
* Run persistent-in-memory programs as scoped REPL statements whose later calls receive copies of earlier top-level bindings;
|
||||
* omitted, it is `SCOPED_REPL_BY_DEFAULT` from configuration.ts, currently true, with persistent-in-memory and false otherwise.
|
||||
*/
|
||||
replScoped?: boolean
|
||||
/** Default elapsed deadline, including nested tool and approval waits. */
|
||||
timeoutMs?: number
|
||||
/** Maximum numeric elapsed budget accepted by resolve. */
|
||||
|
||||
@@ -888,6 +888,26 @@ export interface Config {
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-credentials-local -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-cua-windows -->
|
||||
<a id="deepseek-aidsh-cua-windows"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-cua-windows`
|
||||
|
||||
- `inject`: `computerUse` · `tools` · `systemPrompt` · `subprocess` · `agents`
|
||||
- `refs`: [`CuaConfig`](../packages/experimental/cua/src/index.ts)
|
||||
- `source`: [`packages/experimental/cua-windows/src/index.ts:32`](../packages/experimental/cua-windows/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Windows native waiting policy, alongside shared CUA resource limits. */
|
||||
export interface Config extends CuaConfig {
|
||||
/** Windows PowerShell executable used to query WSL interoperation and the Windows architecture. */
|
||||
windowsPowerShellPath: string
|
||||
/** Milliseconds between application-readiness and clipboard-release observations. */
|
||||
pollIntervalMs: number
|
||||
}
|
||||
```
|
||||
<!-- END GENERATED config-catalog:@deepseek-ai/dsh-cua-windows -->
|
||||
|
||||
<!-- BEGIN GENERATED config-catalog:@deepseek-ai/dsh-deepseek-account -->
|
||||
<a id="deepseek-aidsh-deepseek-account"></a>
|
||||
|
||||
@@ -1608,7 +1628,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-file-reference-local`
|
||||
|
||||
- `inject`: `agents` · `workingDirectory`
|
||||
- `source`: [`packages/context/file-reference-local/src/index.ts:35`](../packages/context/file-reference-local/src/index.ts)
|
||||
- `source`: [`packages/context/file-reference-local/src/index.ts:27`](../packages/context/file-reference-local/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Local file-reference discovery configuration. */
|
||||
@@ -2407,7 +2427,7 @@ export type Config = Readonly<Record<string, never>>
|
||||
## `@deepseek-ai/dsh-lsp-stdio`
|
||||
|
||||
- `inject`: `fs` · `lsp` · `subprocess`
|
||||
- `source`: [`packages/lsp/lsp-stdio/src/index.ts:83`](../packages/lsp/lsp-stdio/src/index.ts)
|
||||
- `source`: [`packages/lsp/lsp-stdio/src/index.ts:70`](../packages/lsp/lsp-stdio/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Plugin configuration: provider id → local language-server configuration. */
|
||||
@@ -2860,7 +2880,7 @@ export interface Config {
|
||||
## `@deepseek-ai/dsh-ptc-runtime-node`
|
||||
|
||||
- `inject`: `fs` · `subprocess` · `sandbox` · `sandboxPolicy`
|
||||
- `source`: [`packages/ptc-runtime/ptc-runtime-node/src/config.ts:7`](../packages/ptc-runtime/ptc-runtime-node/src/config.ts)
|
||||
- `source`: [`packages/ptc-runtime/ptc-runtime-node/src/config.ts:8`](../packages/ptc-runtime/ptc-runtime-node/src/config.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Deployment-varying runtime bounds and launch choices. */
|
||||
@@ -2871,6 +2891,11 @@ export interface Config {
|
||||
executionMode?: 'per-call' | 'persistent-in-memory' | 'vm-script'
|
||||
/** Retain a Script process and context per stateOwner; only supported with executionMode: vm-script. */
|
||||
scriptPersistence?: boolean
|
||||
/**
|
||||
* Run persistent-in-memory programs as scoped REPL statements whose later calls receive copies of earlier top-level bindings;
|
||||
* omitted, it is `SCOPED_REPL_BY_DEFAULT` from configuration.ts, currently true, with persistent-in-memory and false otherwise.
|
||||
*/
|
||||
replScoped?: boolean
|
||||
/** Default elapsed deadline, including nested tool and approval waits. */
|
||||
timeoutMs?: number
|
||||
/** Maximum numeric elapsed budget accepted by resolve. */
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
en: bf79c6acf35c90a5
|
||||
zh: 9f2ce6c7f6493b17
|
||||
/package-surface-rules/package-json-exports-uses-the-standard-key-set:
|
||||
en: 692ded0ff903609d
|
||||
zh: c871ab58ca92ad70
|
||||
en: 37d7728d42231610
|
||||
zh: ba27dd3935f50572
|
||||
/package-surface-rules/dependency-declarations-and-version-consistency-are-enforced-by-the-dependency-scan:
|
||||
en: e68bfcde70f845b4
|
||||
zh: 73e5c4fb067c4fa1
|
||||
|
||||
@@ -79,6 +79,7 @@ A `package.json` `exports` map uses only the standard key set, in which every ke
|
||||
- Standard keys, each with one meaning repo-wide, across `packages/*/*/package.json` and `apps/*/package.json`: `.` (the package entry), `./package.json` (toolchain access, not a semantic export), `./src/*` (source passthrough for tests and tools, whose documented status is still open), `./client` (the browser entry, paired with the `dsh.client` declaration), `./types` (the type entry), `./typert` (the typert type runtime), `./remote` (the `@Remote` call surface), `./locale/*.json` (locale resources), `./cordis.patch.yml` (the bundle patch layer declared by `dsh.bundle`). Counts are not listed here because they rot on the next package; the recompute command below prints them.
|
||||
- Keys outside that set (`./brand` in 5 packages, `./worker` in 4, `./startup`, `./runner`, and `./protocol` in 2 each, plus many one-off keys) have no documented meaning; they are pending review, not a pattern to copy.
|
||||
- `dsh-tools` additionally exposes `./pipeline` for reusable invocation stages and batch scheduling. Its main and pipeline bundles share runtime chunks to preserve error-class identity; workspace constraints check the export targets and publication files.
|
||||
- `dsh-tool-executor-ptc` exposes `./run-code` for the shared program-tool factory, explicit runtime and call limits, and private callable tool definitions. Workspace constraints check its export targets and publication files.
|
||||
- Judged condition: does a change add an export key outside the standard set?
|
||||
- Exception: none by default; a new key changes the owning document and the check first, with the domain owner's confirmation.
|
||||
- Recompute the distribution:
|
||||
|
||||
@@ -79,6 +79,7 @@ import { isSurfaceEligibleType } from '../../core/session/src/surface.ts'
|
||||
- 标准键,每个键在全仓只有一个含义,统计口径是 `packages/*/*/package.json` 与 `apps/*/package.json`:`.`(包主入口)、`./package.json`(工具链访问,不是语义导出)、`./src/*`(测试与工具的源码透传,文档状态仍待定)、`./client`(浏览器入口,与 `dsh.client` 声明配对)、`./types`(类型入口)、`./typert`(typert 类型运行时)、`./remote`(`@Remote` 调用面)、`./locale/*.json`(本地化资源)、`./cordis.patch.yml`(`dsh.bundle` 声明的 bundle 补丁层)。这里不列各键的包数,因为下一个包就会让它过期;下面的复算命令会打印当前数字。
|
||||
- 集合之外的键(`./brand` 5 个包、`./worker` 4 个、`./startup`、`./runner`、`./protocol` 各 2 个,另有大量一次性键)没有文档含义,属待核项,不是可照抄的先例。
|
||||
- `dsh-tools` 另行提供 `./pipeline`,用于复用调用阶段与批量调度。主入口与 pipeline bundle 共享运行时 chunk,以保持错误类的身份一致;工作区检查验证导出目标与发布文件。
|
||||
- `dsh-tool-executor-ptc` 提供 `./run-code`,用于共享程序工具工厂、显式指定运行时及调用上限,并传入私有可调用工具定义。工作区检查验证其导出目标与发布文件。
|
||||
- 判定条件:改动是否新增了标准集合以外的 export 键?
|
||||
- 例外:无默认例外;新增键须先改正家文档与检查,并取得对应领域专家确认。
|
||||
- 复算分布:
|
||||
|
||||
@@ -56,7 +56,7 @@ Application listeners are outside this package matrix. Desktop Host subscribes t
|
||||
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
|
||||
| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:40`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
|
||||
| `llm-pi-ai/request-headers` | `waterfall` | [`packages/llm/llm-pi-ai/src/index.ts:114`](../packages/llm/llm-pi-ai/src/index.ts) | [`llm-pi-ai`](../packages/llm/llm-pi-ai) (`waterfall`) | - |
|
||||
| `llm-pi-ai/request-headers` | `waterfall` | [`packages/llm/llm-pi-ai/src/index.ts:113`](../packages/llm/llm-pi-ai/src/index.ts) | [`llm-pi-ai`](../packages/llm/llm-pi-ai) (`waterfall`) | - |
|
||||
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:24`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `llm/models-updated` | `emit` | [`packages/llm/llm/src/types.ts:30`](../packages/llm/llm/src/types.ts) | [`llm-deepseek`](../packages/llm/llm-deepseek) (`emit`) | `remotes` |
|
||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:76`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
|
||||
| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:40`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
|
||||
| `llm-pi-ai/request-headers` | `waterfall` | [`packages/llm/llm-pi-ai/src/index.ts:114`](../packages/llm/llm-pi-ai/src/index.ts) | [`llm-pi-ai`](../packages/llm/llm-pi-ai) (`waterfall`) | - |
|
||||
| `llm-pi-ai/request-headers` | `waterfall` | [`packages/llm/llm-pi-ai/src/index.ts:113`](../packages/llm/llm-pi-ai/src/index.ts) | [`llm-pi-ai`](../packages/llm/llm-pi-ai) (`waterfall`) | - |
|
||||
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:24`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `llm/models-updated` | `emit` | [`packages/llm/llm/src/types.ts:30`](../packages/llm/llm/src/types.ts) | [`llm-deepseek`](../packages/llm/llm-deepseek) (`emit`) | `remotes` |
|
||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:76`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
||||
|
||||
+17
-6
@@ -264,6 +264,7 @@ flowchart TD
|
||||
pkg_creation_recommendations["creation-recommendations"]
|
||||
pkg_cua["cua"]
|
||||
pkg_cua_bundle["cua-bundle"]
|
||||
pkg_cua_windows["cua-windows"]
|
||||
pkg_experimental_api_speech_to_text["experimental-api-speech-to-text"]
|
||||
pkg_experimental_auto_review["experimental-auto-review"]
|
||||
pkg_experimental_badge_skill_bundle["experimental-badge-skill-bundle"]
|
||||
@@ -586,6 +587,7 @@ flowchart TD
|
||||
pkg_skill_badge --> pkg_skill
|
||||
pkg_host_product_telemetry_otel --> pkg_otel
|
||||
pkg_persona --> pkg_system_prompt
|
||||
pkg_ptc_runtime --> pkg_attachment
|
||||
pkg_ptc_runtime --> pkg_sandbox
|
||||
pkg_sandbox_local --> pkg_llm
|
||||
pkg_sandbox_local --> pkg_sandbox
|
||||
@@ -958,10 +960,12 @@ flowchart TD
|
||||
pkg_creation_recommendations --> pkg_session
|
||||
pkg_creation_recommendations --> pkg_skill
|
||||
pkg_creation_recommendations --> pkg_tools
|
||||
pkg_cua --> pkg_attachment
|
||||
pkg_cua --> pkg_ptc_runtime_node
|
||||
pkg_cua --> pkg_scope
|
||||
pkg_cua --> pkg_user_questions
|
||||
pkg_cua_windows --> pkg_agent
|
||||
pkg_cua_windows --> pkg_computer_use
|
||||
pkg_cua_windows --> pkg_subprocess
|
||||
pkg_cua_windows --> pkg_system_prompt
|
||||
pkg_cua_windows --> pkg_tools
|
||||
pkg_cua_windows --> pkg_user_questions
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_agent
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_browser_use
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_system_prompt
|
||||
@@ -1129,6 +1133,12 @@ flowchart TD
|
||||
pkg_api_settings_controller --> pkg_settings
|
||||
pkg_api_settings_controller --> pkg_typert_protocol
|
||||
pkg_web_app --> pkg_shell_env
|
||||
pkg_cua --> pkg_attachment
|
||||
pkg_cua --> pkg_error
|
||||
pkg_cua --> pkg_ptc_runtime_node
|
||||
pkg_cua --> pkg_scope
|
||||
pkg_cua --> pkg_tool_executor_ptc
|
||||
pkg_cua --> pkg_user_questions
|
||||
pkg_experimental_auto_review --> pkg_agent
|
||||
pkg_experimental_auto_review --> pkg_agent_instructions
|
||||
pkg_experimental_auto_review --> pkg_llm
|
||||
@@ -1751,7 +1761,7 @@ flowchart TD
|
||||
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
|
||||
| [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | `host` | [`otel`](../packages/telemetry/otel) |
|
||||
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | `ptc-runtime` | [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | `ptc-runtime` | [`attachment`](../packages/attachment/attachment), [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
|
||||
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`skill`](../packages/skill/skill), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
|
||||
@@ -1831,7 +1841,7 @@ flowchart TD
|
||||
| [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
|
||||
| [`action-cards`](../packages/experimental/action-cards) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`creation-recommendations`](../packages/experimental/creation-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
|
||||
| [`cua`](../packages/experimental/cua) | `experimental` | [`attachment`](../packages/attachment/attachment), [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), [`scope`](../packages/core/scope), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`cua-windows`](../packages/experimental/cua-windows) | `experimental` | [`agent`](../packages/core/agent), [`computer-use`](../packages/computer-use/computer-use), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
@@ -1865,6 +1875,7 @@ flowchart TD
|
||||
| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) |
|
||||
| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env) |
|
||||
| [`cua`](../packages/experimental/cua) | `experimental` | [`attachment`](../packages/attachment/attachment), [`error`](../packages/util/error), [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), [`scope`](../packages/core/scope), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-forge-session`](../packages/experimental/forge-session) | `experimental` | [`experimental-forge`](../packages/experimental/forge), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`tools`](../packages/core/tools) |
|
||||
| [`webhook`](../packages/experimental/webhook) | `experimental` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
|
||||
+17
-6
@@ -266,6 +266,7 @@ flowchart TD
|
||||
pkg_creation_recommendations["creation-recommendations"]
|
||||
pkg_cua["cua"]
|
||||
pkg_cua_bundle["cua-bundle"]
|
||||
pkg_cua_windows["cua-windows"]
|
||||
pkg_experimental_api_speech_to_text["experimental-api-speech-to-text"]
|
||||
pkg_experimental_auto_review["experimental-auto-review"]
|
||||
pkg_experimental_badge_skill_bundle["experimental-badge-skill-bundle"]
|
||||
@@ -588,6 +589,7 @@ flowchart TD
|
||||
pkg_skill_badge --> pkg_skill
|
||||
pkg_host_product_telemetry_otel --> pkg_otel
|
||||
pkg_persona --> pkg_system_prompt
|
||||
pkg_ptc_runtime --> pkg_attachment
|
||||
pkg_ptc_runtime --> pkg_sandbox
|
||||
pkg_sandbox_local --> pkg_llm
|
||||
pkg_sandbox_local --> pkg_sandbox
|
||||
@@ -960,10 +962,12 @@ flowchart TD
|
||||
pkg_creation_recommendations --> pkg_session
|
||||
pkg_creation_recommendations --> pkg_skill
|
||||
pkg_creation_recommendations --> pkg_tools
|
||||
pkg_cua --> pkg_attachment
|
||||
pkg_cua --> pkg_ptc_runtime_node
|
||||
pkg_cua --> pkg_scope
|
||||
pkg_cua --> pkg_user_questions
|
||||
pkg_cua_windows --> pkg_agent
|
||||
pkg_cua_windows --> pkg_computer_use
|
||||
pkg_cua_windows --> pkg_subprocess
|
||||
pkg_cua_windows --> pkg_system_prompt
|
||||
pkg_cua_windows --> pkg_tools
|
||||
pkg_cua_windows --> pkg_user_questions
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_agent
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_browser_use
|
||||
pkg_experimental_browser_use_chrome_devtools_mcp --> pkg_system_prompt
|
||||
@@ -1131,6 +1135,12 @@ flowchart TD
|
||||
pkg_api_settings_controller --> pkg_settings
|
||||
pkg_api_settings_controller --> pkg_typert_protocol
|
||||
pkg_web_app --> pkg_shell_env
|
||||
pkg_cua --> pkg_attachment
|
||||
pkg_cua --> pkg_error
|
||||
pkg_cua --> pkg_ptc_runtime_node
|
||||
pkg_cua --> pkg_scope
|
||||
pkg_cua --> pkg_tool_executor_ptc
|
||||
pkg_cua --> pkg_user_questions
|
||||
pkg_experimental_auto_review --> pkg_agent
|
||||
pkg_experimental_auto_review --> pkg_agent_instructions
|
||||
pkg_experimental_auto_review --> pkg_llm
|
||||
@@ -1753,7 +1763,7 @@ flowchart TD
|
||||
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
|
||||
| [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | `host` | [`otel`](../packages/telemetry/otel) |
|
||||
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | `ptc-runtime` | [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | `ptc-runtime` | [`attachment`](../packages/attachment/attachment), [`sandbox`](../packages/sandbox/sandbox) |
|
||||
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
|
||||
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`skill`](../packages/skill/skill), [`subprocess`](../packages/subprocess/subprocess) |
|
||||
| [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) |
|
||||
@@ -1833,7 +1843,7 @@ flowchart TD
|
||||
| [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) |
|
||||
| [`action-cards`](../packages/experimental/action-cards) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`creation-recommendations`](../packages/experimental/creation-recommendations) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) |
|
||||
| [`cua`](../packages/experimental/cua) | `experimental` | [`attachment`](../packages/attachment/attachment), [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), [`scope`](../packages/core/scope), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`cua-windows`](../packages/experimental/cua-windows) | `experimental` | [`agent`](../packages/core/agent), [`computer-use`](../packages/computer-use/computer-use), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-browser-use-playwright-mcp`](../packages/experimental/browser-use-playwright-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-browser-use-stagehand-native`](../packages/experimental/browser-use-stagehand-native) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
@@ -1867,6 +1877,7 @@ flowchart TD
|
||||
| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) |
|
||||
| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env) |
|
||||
| [`cua`](../packages/experimental/cua) | `experimental` | [`attachment`](../packages/attachment/attachment), [`error`](../packages/util/error), [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node), [`scope`](../packages/core/scope), [`tool-executor-ptc`](../packages/core/tool-executor-ptc), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
|
||||
| [`experimental-forge-session`](../packages/experimental/forge-session) | `experimental` | [`experimental-forge`](../packages/experimental/forge), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`tools`](../packages/core/tools) |
|
||||
| [`webhook`](../packages/experimental/webhook) | `experimental` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/computer-use.md
|
||||
/computer-use:
|
||||
en: e02a2e4cc9970cba
|
||||
zh: 29aac6c0530a582f
|
||||
en: bf7dcc9124e8bce2
|
||||
zh: 4199c51e21dc9b0a
|
||||
/computer-use/choose-a-provider:
|
||||
en: fa2e0d7b41c0450b
|
||||
zh: af9a799711f901e6
|
||||
|
||||
@@ -14,7 +14,7 @@ The CUA provider owns `ctx.cuaAccess` for system-permission status, explicit sys
|
||||
|---|---|
|
||||
| `CuaApplicationId`, `CuaApplicationGrant` | A platform-provided application identity and its displayed saved decision. |
|
||||
| `CuaSystemPermission` | Accessibility or Screen Recording, selected by the human. |
|
||||
| `CuaPermissionProcess`, `CuaSystemPermissionState` | Native access observations tagged `darwin` or `win32`, with helper process identity. macOS includes permission checks, parent identity and responsible-application metadata when available (falling back to the helper’s own bundle); Windows includes desktop lock, capture availability and elevation status. |
|
||||
| `CuaPermissionProcess`, `CuaSystemPermissionState` | Native access observations tagged `darwin` or `windows`, with the helper’s name and path when available. macOS includes permission checks, numeric helper and parent PIDs, and responsible-application metadata when available (falling back to the helper’s own bundle); Windows includes desktop lock, capture availability and elevation status, and omits the helper PID. |
|
||||
| `CuaApplicationState` | Saved grants, refused applications, and whether persistence is available. Session-only grants remain internal to the provider. System observations and application icons are queried separately so the page can display each result as it arrives. |
|
||||
|
||||
The [shared object API](../../packages/experimental/cua/README.md) uses `connectCua` to select an application, then `app.getWindows()` and `app.getWindow(windowId)` to select a window for screenshots and pointer input. Accessibility operations use element IDs from the latest tree for that application handle. Providers declare keyboard scopes and application-level accessibility in `getCapabilities()`.
|
||||
|
||||
@@ -14,7 +14,7 @@ CUA provider 通过 `ctx.cuaAccess` 提供系统权限状态、用户主动申
|
||||
|---|---|
|
||||
| `CuaApplicationId`、`CuaApplicationGrant` | 平台提供的应用身份,以及显示给用户的已保存决定。 |
|
||||
| `CuaSystemPermission` | 用户选择申请的辅助功能或屏幕录制权限。 |
|
||||
| `CuaPermissionProcess`、`CuaSystemPermissionState` | 以 `darwin` 或 `win32` 标记的原生访问状态和 helper 进程身份。macOS 包含权限检查、父进程身份和责任应用信息(系统无法识别时使用 helper 自身的应用包);Windows 包含桌面锁定、截图可用性和提权状态。 |
|
||||
| `CuaPermissionProcess`、`CuaSystemPermissionState` | 以 `darwin` 或 `windows` 标记的原生访问状态,包含可获取的 helper 名称和路径。macOS 包含权限检查、helper 和父进程的数字 PID,以及责任应用信息(系统无法识别时使用 helper 自身的应用包);Windows 包含桌面锁定、截图可用性和提权状态,省略 helper 的 PID。 |
|
||||
| `CuaApplicationState` | 持久授权、拒绝记录,以及能否持久保存。会话授权只保留在 provider 内部。系统权限和应用图标分别查询,页面在各项结果返回后分别显示。 |
|
||||
|
||||
调用[共享对象 API](../../packages/experimental/cua/README.zh.md) 时,通过 `connectCua` 连接,选择应用后用 `app.getWindows()` 和 `app.getWindow(windowId)` 选择要截图或进行鼠标操作的窗口。辅助功能操作使用该应用句柄最近一次读取的树中的元素 ID。提供方通过 `getCapabilities()` 声明键盘输入支持的范围,以及是否支持应用级辅助功能。
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
en: 337a6580d7571274
|
||||
zh: f4ae98899120d122
|
||||
/ptc-runtime/the-run-request-in-result-out:
|
||||
en: 6dfa7da8b13a4f17
|
||||
zh: 09a3c8c4922239ef
|
||||
en: 94ba725bef6684b7
|
||||
zh: f113745a4a07952a
|
||||
/ptc-runtime/bindings-host-functions-as-program-globals:
|
||||
en: 182147ddf388c600
|
||||
zh: c0a7d77602c78bcd
|
||||
/ptc-runtime/captured-output-and-the-failure-taxonomy:
|
||||
en: 8eb1607888ab6efc
|
||||
zh: 580c852d17b9413f
|
||||
en: ef553620e77bd638
|
||||
zh: 40ff2f5ba3de5ed5
|
||||
/ptc-runtime/the-service:
|
||||
en: a82f828fa006c7e9
|
||||
zh: 2279d36867335b57
|
||||
@@ -19,13 +19,15 @@ interface PtcRunRequest {
|
||||
/**
|
||||
* The program source, in the runtime's {@link ../index.ts | language}. It
|
||||
* follows the provider's programFormat: an async function body supports
|
||||
* top-level `await` and `return`; REPL and Script programs support top-level
|
||||
* top-level `await` and `return`; REPL (`repl`, `scoped-repl`) and Script programs support top-level
|
||||
* declarations, `await`, and a final expression, but not top-level `return`.
|
||||
* The Script provider lowers top-level await; its executionInstructions and
|
||||
* documentation define retained-binding semantics. The completion value
|
||||
* becomes {@link PtcRunResult.value}.
|
||||
*/
|
||||
program: string
|
||||
/** Discard the completion without JSON validation when true; omission preserves the completion value. */
|
||||
discardCompletion?: boolean
|
||||
/**
|
||||
* Object-identity owner for optional in-memory state. Omission requests a fresh
|
||||
* environment. Consumers must await runtime.release(owner) when the owner ends.
|
||||
@@ -93,11 +95,12 @@ interface PtcRunResult {
|
||||
*/
|
||||
value?: PtcJsonValue
|
||||
/**
|
||||
* Captured text. Each source channel preserves emission order; interleaving
|
||||
* Captured text fragments retain their emitted line breaks. Concatenate adjacent text without separators.
|
||||
* Each source channel preserves emission order; interleaving
|
||||
* across independent channels is backend-dependent. Bounded only as part of
|
||||
* the outer result.
|
||||
*/
|
||||
logs: string[]
|
||||
output: PtcOutput[]
|
||||
/** Present iff the run failed; see {@link PtcRunFailure} for the taxonomy. */
|
||||
error?: PtcRunFailure
|
||||
}
|
||||
@@ -105,10 +108,10 @@ interface PtcRunResult {
|
||||
|
||||
```ts type-equiv
|
||||
/** Source evaluation form, independent of retained program state. */
|
||||
type PtcProgramFormat = 'function-body' | 'repl' | 'script'
|
||||
type PtcProgramFormat = 'function-body' | 'repl' | 'script' | 'scoped-repl'
|
||||
```
|
||||
|
||||
`programFormat` distinguishes async function bodies, REPL statements, and scripts. REPL and Script programs accept top-level `await`, declarations, and a final expression; top-level `return` is not supported. The Node Script provider lowers top-level await and awaits the completion value; its [declaration limitations](../../packages/ptc-runtime/ptc-runtime-node/README.md#known-limitations-and-deferred-work) apply to transformed programs. `retainsState` reports memory retention independently of source syntax. `stateOwner` is an in-process object identity for optional retained memory; distinct objects remain isolated. Consumers await `release(owner)` when that owner ends; cleanup failures reject that call. Omitted owners are one-shot, and stateless providers ignore ownership. The [persistent Node provider](../../packages/ptc-runtime/ptc-runtime-node/README.md#persistent-repl) defines reset and recovery behavior; no retained object enters the durable Session format.
|
||||
`programFormat` distinguishes async function bodies, REPL statements, scoped REPL statements, and scripts. REPL, scoped REPL, and Script programs accept top-level `await`, declarations, and a final expression; top-level `return` is not supported. Retained REPL calls share one scope, while each scoped REPL call starts with copies of the top-level bindings that earlier calls left, so it can declare those names again without changing earlier code. The Node Script provider lowers top-level await and awaits the completion value; its [declaration limitations](../../packages/ptc-runtime/ptc-runtime-node/README.md#known-limitations-and-deferred-work) apply to transformed programs. `retainsState` reports memory retention independently of source syntax. `stateOwner` is an in-process object identity for optional retained memory; distinct objects remain isolated. Consumers await `release(owner)` when that owner ends; cleanup failures reject that call. Omitted owners are one-shot, and stateless providers ignore ownership. The [persistent Node provider](../../packages/ptc-runtime/ptc-runtime-node/README.md#persistent-repl) defines reset and recovery behavior; no retained object enters the durable Session format.
|
||||
|
||||
## Bindings: host functions as program globals
|
||||
|
||||
@@ -150,7 +153,7 @@ interface PtcBindingNamespace {
|
||||
* words, so the same namespace list works against every backend regardless
|
||||
* of `language` — a JS-only spelling like `$tools` is rejected by design,
|
||||
* not just by the Python backend. Names that satisfy the identifier rule but
|
||||
* name a backend-owned slot (`RESERVED_BINDING_GLOBALS`, e.g. `console`,
|
||||
* name a backend-owned slot (`RESERVED_BINDING_GLOBALS`, e.g. `inspect`, `console`, `Image`,
|
||||
* `__dsh_main__`) are also refused everywhere; see its declaration for the
|
||||
* exact set and why each entry is reserved.
|
||||
*/
|
||||
@@ -171,7 +174,7 @@ type PtcJsonValue = null | boolean | number | string | PtcJsonValue[] | { [key:
|
||||
/**
|
||||
* One host-side function exposed to the program as an async callable. The
|
||||
* runtime bridges calls to it (possibly across a serialization boundary), so
|
||||
* `args` and the resolution value MUST be lossless JSON. A runtime rejects a
|
||||
* `args` must be lossless JSON; resolutions also support host-issued PtcImage values. A runtime rejects a
|
||||
* lossy or non-cloneable value with a descriptive error rather than corrupting
|
||||
* the run. No seam-level byte cap applies to a binding resolution. A rejection
|
||||
* of this function surfaces inside the program as a rejection of the
|
||||
@@ -182,7 +185,29 @@ type PtcBindingFunction = (args: unknown) => Promise<PtcJsonValue>
|
||||
|
||||
## Captured output and the failure taxonomy
|
||||
|
||||
Logs are plain strings. Each source channel preserves emission order, while interleaving across independent channels is backend-dependent because channel metadata is not part of the seam. The runtime captures the program's console and stream output, and consumers render only the text. Implementations cap the serialized outer log-array plus completion-value or failure-message payload; fixed result-envelope syntax and consumer presentation whitespace are not part of that variable-payload ledger. Overflow is an explicit failure rather than in-band value substitution.
|
||||
`output` contains text and image entries in emission order: `{ type: 'text', text }` or `{ type: 'image', attachment }`. Text entries retain emitted newline characters; concatenate adjacent text without separators. Each source channel preserves emission order; interleaving across independent channels is backend-dependent. Host bindings return `PtcImage` descriptors containing the attachment reference and a readable execution-world `path`; the program receives an `Image` with that path and the stored pixel dimensions. The Node provider caps serialized text, image descriptors, and completion or diagnostic payloads together; overflow is an explicit failure.
|
||||
|
||||
```ts public-api
|
||||
/** A stored image readable from the program's execution environment. */
|
||||
declare class PtcImage {
|
||||
readonly [key: string]: PtcJsonValue;
|
||||
/** Absolute attachment file path readable in the execution world. */
|
||||
readonly path: string;
|
||||
/** Stored image selected when the program inspects this value. */
|
||||
readonly attachment: Readonly<ImageAttachmentRef>;
|
||||
/**
|
||||
* Describe one immutable image file in the execution world.
|
||||
* @param path - Absolute path to the stored image.
|
||||
* @param attachment - Reference to the immutable stored image.
|
||||
*/
|
||||
constructor(path: string, attachment: ImageAttachmentRef);
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Raw text fragments and inspected images in emission order; concatenate adjacent text without separators. */
|
||||
type PtcOutput = { type: 'text'; text: string } | { type: 'image'; attachment: ImageAttachmentRef }
|
||||
```
|
||||
|
||||
Failure kinds are **orthogonal outcomes reported independently** (per [defensive-patterns](../defensive-patterns/general.md)): a budget expiry is not an exception, an abort is not a timeout, and a substrate death (e.g. OOM) is neither:
|
||||
|
||||
@@ -237,6 +262,21 @@ Registers one `ctx.ptcRuntime` implementation. Program, budget, abort, and subst
|
||||
*/
|
||||
release(owner: object): Promise<void>
|
||||
|
||||
/**
|
||||
* Register source evaluated once when each execution environment is created.
|
||||
* Registration must precede the first run admission; only one initializer may
|
||||
* be registered. Current bindings are installed before initialization. Its
|
||||
* completion is discarded; logs, timeout, cancellation and output bounds share
|
||||
* the first run. Initialization failure discards the environment without
|
||||
* executing the user program or rolling back external effects.
|
||||
* @param program - Initialization source in the provider's language and format.
|
||||
* @returns An idempotent async disposer that stops admission, cancels and drains
|
||||
* admitted runs, and releases environments. Failed cleanup blocks execution
|
||||
* until the disposer succeeds on retry. The caller owns this disposer.
|
||||
* @throws When unsupported, registered twice, registered after admission, or invalid.
|
||||
*/
|
||||
registerEnvironmentInitializer(program: string): () => Promise<void>
|
||||
|
||||
/**
|
||||
* Resolve supported options and provider defaults before execution.
|
||||
* @param request - Program, bindings, cancellation and optional execution choices.
|
||||
|
||||
@@ -19,13 +19,15 @@ interface PtcRunRequest {
|
||||
/**
|
||||
* The program source, in the runtime's {@link ../index.ts | language}. It
|
||||
* follows the provider's programFormat: an async function body supports
|
||||
* top-level `await` and `return`; REPL and Script programs support top-level
|
||||
* top-level `await` and `return`; REPL (`repl`, `scoped-repl`) and Script programs support top-level
|
||||
* declarations, `await`, and a final expression, but not top-level `return`.
|
||||
* The Script provider lowers top-level await; its executionInstructions and
|
||||
* documentation define retained-binding semantics. The completion value
|
||||
* becomes {@link PtcRunResult.value}.
|
||||
*/
|
||||
program: string
|
||||
/** Discard the completion without JSON validation when true; omission preserves the completion value. */
|
||||
discardCompletion?: boolean
|
||||
/**
|
||||
* Object-identity owner for optional in-memory state. Omission requests a fresh
|
||||
* environment. Consumers must await runtime.release(owner) when the owner ends.
|
||||
@@ -93,11 +95,12 @@ interface PtcRunResult {
|
||||
*/
|
||||
value?: PtcJsonValue
|
||||
/**
|
||||
* Captured text. Each source channel preserves emission order; interleaving
|
||||
* Captured text fragments retain their emitted line breaks. Concatenate adjacent text without separators.
|
||||
* Each source channel preserves emission order; interleaving
|
||||
* across independent channels is backend-dependent. Bounded only as part of
|
||||
* the outer result.
|
||||
*/
|
||||
logs: string[]
|
||||
output: PtcOutput[]
|
||||
/** Present iff the run failed; see {@link PtcRunFailure} for the taxonomy. */
|
||||
error?: PtcRunFailure
|
||||
}
|
||||
@@ -105,10 +108,10 @@ interface PtcRunResult {
|
||||
|
||||
```ts type-equiv
|
||||
/** Source evaluation form, independent of retained program state. */
|
||||
type PtcProgramFormat = 'function-body' | 'repl' | 'script'
|
||||
type PtcProgramFormat = 'function-body' | 'repl' | 'script' | 'scoped-repl'
|
||||
```
|
||||
|
||||
`programFormat` 区分异步函数体、REPL 语句与脚本。REPL 与 Script 程序接受顶层 `await`、声明和末尾表达式,不支持顶层 `return`。Node Script 提供方转换顶层 await 并等待完成值;其[声明限制](../../packages/ptc-runtime/ptc-runtime-node/README.zh.md#known-limitations-and-deferred-work)适用于转换后的程序。`retainsState` 独立于源码语法报告是否保留内存。`stateOwner` 是标识可选保留内存的进程内对象身份;不同对象相互隔离。消费者在所有者结束时等待 `release(owner)`;清理失败时该调用拒绝。省略所有者时仅执行一次,无状态提供方忽略所有权。[持久 Node 提供方](../../packages/ptc-runtime/ptc-runtime-node/README.zh.md#persistent-repl)定义重置和恢复行为;保留对象不会进入持久 Session 格式。
|
||||
`programFormat` 区分异步函数体、REPL 语句、scoped REPL 语句与脚本。REPL、scoped REPL 与 Script 程序接受顶层 `await`、声明和末尾表达式,不支持顶层 `return`。保留状态的 REPL 各次调用共用一个作用域;scoped REPL 每次调用开始时拿到之前调用留下的顶层绑定的拷贝,因此可以再次声明这些名字,而不改变之前的代码。Node Script 提供方转换顶层 await 并等待完成值;其[声明限制](../../packages/ptc-runtime/ptc-runtime-node/README.zh.md#known-limitations-and-deferred-work)适用于转换后的程序。`retainsState` 独立于源码语法报告是否保留内存。`stateOwner` 是标识可选保留内存的进程内对象身份;不同对象相互隔离。消费者在所有者结束时等待 `release(owner)`;清理失败时该调用拒绝。省略所有者时仅执行一次,无状态提供方忽略所有权。[持久 Node 提供方](../../packages/ptc-runtime/ptc-runtime-node/README.zh.md#persistent-repl)定义重置和恢复行为;保留对象不会进入持久 Session 格式。
|
||||
|
||||
## 绑定:宿主函数作为程序全局变量
|
||||
|
||||
@@ -150,7 +153,7 @@ interface PtcBindingNamespace {
|
||||
* words, so the same namespace list works against every backend regardless
|
||||
* of `language` — a JS-only spelling like `$tools` is rejected by design,
|
||||
* not just by the Python backend. Names that satisfy the identifier rule but
|
||||
* name a backend-owned slot (`RESERVED_BINDING_GLOBALS`, e.g. `console`,
|
||||
* name a backend-owned slot (`RESERVED_BINDING_GLOBALS`, e.g. `inspect`, `console`, `Image`,
|
||||
* `__dsh_main__`) are also refused everywhere; see its declaration for the
|
||||
* exact set and why each entry is reserved.
|
||||
*/
|
||||
@@ -171,7 +174,7 @@ type PtcJsonValue = null | boolean | number | string | PtcJsonValue[] | { [key:
|
||||
/**
|
||||
* One host-side function exposed to the program as an async callable. The
|
||||
* runtime bridges calls to it (possibly across a serialization boundary), so
|
||||
* `args` and the resolution value MUST be lossless JSON. A runtime rejects a
|
||||
* `args` must be lossless JSON; resolutions also support host-issued PtcImage values. A runtime rejects a
|
||||
* lossy or non-cloneable value with a descriptive error rather than corrupting
|
||||
* the run. No seam-level byte cap applies to a binding resolution. A rejection
|
||||
* of this function surfaces inside the program as a rejection of the
|
||||
@@ -182,7 +185,29 @@ type PtcBindingFunction = (args: unknown) => Promise<PtcJsonValue>
|
||||
|
||||
## 捕获的输出与失败分类体系
|
||||
|
||||
日志是纯字符串。每个来源通道保留自身的发出顺序;由于通道元数据不属于 seam,相互独立的通道如何交错由后端决定。运行时捕获程序的 console 与流输出,Consumer 只渲染文本。实现会对序列化后的外层日志数组,以及完成值或失败消息的组合载荷设置上限;固定的结果封装语法与 Consumer 展示空白不计入这份可变载荷计量。超限会显式失败,而不会在值中插入替代内容。
|
||||
`output` 按发出顺序保存文本和图片条目:文本使用 `{ type: 'text', text }`,图片使用 `{ type: 'image', attachment }`。文本条目保留实际输出的换行,相邻文本直接拼接,不额外插入分隔符。每个来源通道都保留发出顺序;独立通道之间的交错顺序由后端决定。宿主绑定返回 `PtcImage` 描述符,包含附件引用和可在执行环境读取的 `path`;程序收到带有该路径和存储像素尺寸的 `Image`。Node 提供方对序列化文本、图片描述符、完成值或诊断一起计量;超限会显式失败。
|
||||
|
||||
```ts public-api
|
||||
/** A stored image readable from the program's execution environment. */
|
||||
declare class PtcImage {
|
||||
readonly [key: string]: PtcJsonValue;
|
||||
/** Absolute attachment file path readable in the execution world. */
|
||||
readonly path: string;
|
||||
/** Stored image selected when the program inspects this value. */
|
||||
readonly attachment: Readonly<ImageAttachmentRef>;
|
||||
/**
|
||||
* Describe one immutable image file in the execution world.
|
||||
* @param path - Absolute path to the stored image.
|
||||
* @param attachment - Reference to the immutable stored image.
|
||||
*/
|
||||
constructor(path: string, attachment: ImageAttachmentRef);
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Raw text fragments and inspected images in emission order; concatenate adjacent text without separators. */
|
||||
type PtcOutput = { type: 'text'; text: string } | { type: 'image'; attachment: ImageAttachmentRef }
|
||||
```
|
||||
|
||||
失败类型是**正交的结果,独立报告**(见 [defensive-patterns](../defensive-patterns/general.zh.md)):预算耗尽不是异常,中止不是超时,基底崩溃(如 OOM)也不是二者中的任何一个:
|
||||
|
||||
@@ -237,6 +262,21 @@ Registers one `ctx.ptcRuntime` implementation. Program, budget, abort, and subst
|
||||
*/
|
||||
release(owner: object): Promise<void>
|
||||
|
||||
/**
|
||||
* Register source evaluated once when each execution environment is created.
|
||||
* Registration must precede the first run admission; only one initializer may
|
||||
* be registered. Current bindings are installed before initialization. Its
|
||||
* completion is discarded; logs, timeout, cancellation and output bounds share
|
||||
* the first run. Initialization failure discards the environment without
|
||||
* executing the user program or rolling back external effects.
|
||||
* @param program - Initialization source in the provider's language and format.
|
||||
* @returns An idempotent async disposer that stops admission, cancels and drains
|
||||
* admitted runs, and releases environments. Failed cleanup blocks execution
|
||||
* until the disposer succeeds on retry. The caller owns this disposer.
|
||||
* @throws When unsupported, registered twice, registered after admission, or invalid.
|
||||
*/
|
||||
registerEnvironmentInitializer(program: string): () => Promise<void>
|
||||
|
||||
/**
|
||||
* Resolve supported options and provider defaults before execution.
|
||||
* @param request - Program, bindings, cancellation and optional execution choices.
|
||||
|
||||
@@ -72,8 +72,8 @@
|
||||
en: 8c8643ef46fd075f
|
||||
zh: 8c8643ef46fd075f
|
||||
/tool-schema-catalog/deepseek-ai-dsh-tool-executor-ptc/run-code:
|
||||
en: 7917e16cbc2c366f
|
||||
zh: c39eb0420544f04a
|
||||
en: 2c7a3166dfd9751c
|
||||
zh: 1b52ae2d0f33b9a4
|
||||
/tool-schema-catalog/deepseek-ai-dsh-plan-mode:
|
||||
en: b500b530845e26a9
|
||||
zh: b500b530845e26a9
|
||||
|
||||
@@ -623,7 +623,7 @@ ask_user_question keeps the original blocking behavior by default; set `mode: ti
|
||||
|
||||
### `run_code`
|
||||
|
||||
Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return is program output — curate it. Image-bearing subtool results are attached after the run. The program is the body of an async TypeScript function; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level await and return are supported. Emit output with console.log(...) or return a lossless JSON value. Only printed values and the returned value become program output. Each call runs in a fresh Node process. Node APIs are available through await import(...). Relative paths use the supplied working directory; process.env starts empty. Direct filesystem access follows this execution's sandbox policy. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.
|
||||
Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Show selected text and images with inspect(...values). The program is the body of an async TypeScript function; only erasable syntax is accepted, with no enum or namespaces. Type annotations are advisory. Top-level await and return are supported. Use inspect(...values) to display output. Do not use console. Each call runs in a fresh Node process. Node APIs are available through await import(...). Relative paths use the supplied working directory; process.env starts empty. Direct filesystem access follows this execution's sandbox policy. Each program starts in the Session's current directory. Running programs keep their initial directory. A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -627,7 +627,7 @@ ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才
|
||||
|
||||
### `run_code`
|
||||
|
||||
针对可用工具执行 TypeScript 程序。接受两个必填参数:`code`,异步函数的函数体(仅限可擦除语法,支持顶层 `await` 和 `return`);以及 `description`,简要说明程序做什么。请按系统提示词中的声明,以 `await tools.name(args)` 调用工具。只有打印或返回的内容构成程序输出,请筛选这些内容。含图片的子工具结果会在运行结束后附加。程序是异步 TypeScript 函数的函数体,只接受可擦除语法,不支持 enum 或 namespace。类型标注只作提示。支持顶层 await 和 return。使用 console.log(...) 或返回无损 JSON 值来输出。只有打印值和返回值成为程序输出。每次调用在新的 Node 进程中运行,可通过 await import(...) 使用 Node API。相对路径以传入的工作目录为准;process.env 初始为空。直接文件系统访问遵循本次执行的沙箱策略。每个程序从 Session 的当前目录启动,运行中保留初始目录。沙箱提权只为本次完整程序执行授权。嵌套工具保留自身策略与审批。只有在获得拒绝证据后才请求更宽访问。先前效果可能已经发生,请在显式重试前检查。程序不会自动重放。
|
||||
针对可用工具执行 TypeScript 程序。接受两个必填参数:`code`,异步函数的函数体(仅限可擦除语法,支持顶层 `await` 和 `return`);以及 `description`,简要说明程序做什么。请按系统提示词中的声明,以 `await tools.name(args)` 调用工具。使用 inspect(...values) 展示选定的文本和图片。程序是异步 TypeScript 函数的函数体,只接受可擦除语法,不支持 enum 或 namespace。类型标注只作提示。支持顶层 await 和 return。使用 inspect(...values) 展示输出。不要使用 console。每次调用在新的 Node 进程中运行,可通过 await import(...) 使用 Node API。相对路径以传入的工作目录为准;process.env 初始为空。直接文件系统访问遵循本次执行的沙箱策略。每个程序从 Session 的当前目录启动,运行中保留初始目录。沙箱提权只为本次完整程序执行授权。嵌套工具保留自身策略与审批。只有在获得拒绝证据后才请求更宽访问。先前效果可能已经发生,请在显式重试前检查。程序不会自动重放。
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 207c515390c5e529
|
||||
zh: bd4d90881efb011d
|
||||
/native-cua-application-and-window-api/change:
|
||||
en: e5d1027faa55adba
|
||||
zh: 4b88ad6a70977fb3
|
||||
en: 43ace3409e7976b8
|
||||
zh: da185b705dfe600a
|
||||
/native-cua-application-and-window-api/migration:
|
||||
en: 071b67613c512b84
|
||||
zh: 7ee32d8dbcf7a0f2
|
||||
en: 33bdef11cad75c53
|
||||
zh: 0d576c6160278b7b
|
||||
@@ -9,15 +9,15 @@ English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
CUA scripts use an explicit window for screenshots and pointer input. App and Window objects share the latest accessibility snapshot for their application handle. Their identifiers are `appHandle` and `windowId` respectively. Application discovery still reports the PID of a running match.
|
||||
Screenshots and pointer input require explicit windows. App (`appHandle`) and Window (`windowId`) objects share their application handle's latest accessibility snapshot. Window IDs are opaque strings. macOS retains running application PIDs and permission `helper.pid` and `parent.pid`. Windows permission metadata omits PIDs.
|
||||
|
||||
Providers supply startup capabilities alongside their platform guidance. The native helper reports runtime permissions and session lock state. The Host combines both in `getCapabilities()`. Human-facing permission responses carry a `darwin` or `win32` platform tag.
|
||||
Providers supply startup capabilities and implement `getGuidance(kind)` instead of supplying a `guidance` string. `raw` selects instructions for the raw `cua` tool; `standalone` selects instructions for `cua_run_code`. The native helper reports runtime permissions and session lock state; the Host combines these with startup capabilities in `getCapabilities()`. Human-facing permission responses carry a `darwin` or `windows` platform tag.
|
||||
|
||||
## Migration
|
||||
|
||||
1. After `cua.getApp(selector)`, select the target from `app.getWindows()` with `app.getWindow(windowId)`. Use `window.getScreenshot()`, `window.click()`, `window.scroll()`, and `window.drag()`. Replace `hover` with `window.mouseMove()`. On macOS, `window.click(id)` sends mouse events at the element center. Switch to `invoke(id)` for the default accessibility action, or `performAction(id, "AXShowMenu")` to request an advertised menu action.
|
||||
2. Replace `getAXState()` with `getAccessibilityTree()`. Replace `getAXStateAndScreenshot()` with separate tree and screenshot reads. Application and window selection does not read AX. Before using element IDs, call `getAccessibilityTree()` explicitly; use `{ emit: false }` to retain the snapshot without displaying it. A screenshot does not require a successful AX read. Before coordinate input, including relative coordinates, obtain a successful AX observation covering the window or a screenshot; absolute coordinates require a screenshot. See [macOS AX read limits](../cua-ax-read-limits/guide.md) for changes to traversal defaults and configuration.
|
||||
1. Pass a current `app.getWindows()` ID unchanged to `app.getWindow(windowId)`. Rediscover saved numeric IDs. Use `window.getScreenshot()`, `window.click()`, `window.scroll()`, and `window.drag()`. Replace `hover` with `window.mouseMove()`. On macOS, `window.click(id)` sends mouse events at the element center. Switch to `invoke(id)` for the default accessibility action, or `performAction(id, "AXShowMenu")` to request an advertised menu action.
|
||||
2. Replace `getAXState()` with `getAccessibilityTree()`. Replace `getAXStateAndScreenshot()` with separate tree and screenshot reads. Application and window selection does not read AX. Before using element IDs, call `getAccessibilityTree()` explicitly. To display the tree in TypeScript PTC, pass its return value to `inspect`. A screenshot does not require a successful AX read. Before coordinate input, including relative coordinates, obtain a successful AX observation covering the window or a screenshot; absolute coordinates require a screenshot. See [macOS AX read limits](../cua-ax-read-limits/guide.md) for changes to traversal defaults and configuration.
|
||||
3. Pass the opaque string ID from the latest tree wherever you passed a numeric element index. In raw requests use `elementId` with `snapshotId`. Replace `performSecondaryAction` with `listActions(id)` and `performAction(id, exactName, argument?)`, or use `invoke(id)` for the default action. `setValue` accepts the provider’s supported JSON values. On macOS, `performAction(id, 'AXRaise')` performs the advertised raise action. Call `app.activate()` first when the application must become active.
|
||||
4. Replace a packed chord such as `pressKey('shift+Tab')` with `pressKey('Tab', ['shift'])`. Check `(await cua.getCapabilities()).methods` before using `sendKeyEvent`. On macOS, call it inside one `run_code` to hold a key across tool calls. To request one repeat, send another down for a non-modifier key held by that execution. Returning, throwing or pausing releases any remaining keys. If your script handles unknown macOS key names as `NATIVE_ERROR`, handle `INVALID_ARGUMENT` instead. Correct the key name before retrying.
|
||||
5. Read `dispatched` from action results, then observe the application to verify the outcome. `invoke` returns `dispatched: false` when the element has no default action. Remove reads of the old `verified` field and of `pid` on selected objects or window records. Use `listApps()` or running `findApps()` matches when you need a PID.
|
||||
6. For a custom provider, replace `CuaBackend.description` with `capabilities` and pass `guidance` and `permissionParameters` to `installCua`. Rebuild any custom `helperPath` against the [operation schema](../../../../packages/experimental/cua/src/protocol.ts) and [tagged permission response](../../../../packages/experimental/cua/src/access-types.ts). Restart the provider, inspect its advertised methods and keyboard scopes, and open the Computer Use permission page to verify its platform controls.
|
||||
4. Replace a packed chord such as `pressKey('shift+Tab')` with `pressKey('Tab', ['shift'])`. Check `(await cua.getCapabilities()).methods` before using `sendKeyEvent`. On macOS, call it inside one `run_code` or `cua_run_code` to hold a key across tool calls. Repeat a held non-modifier with another down. Returning, throwing or pausing releases any remaining keys. Replace `NATIVE_ERROR` with `INVALID_ARGUMENT` for unknown macOS keys and correct the key before retrying.
|
||||
5. Read `dispatched`, then observe the outcome. `invoke` returns `dispatched: false` when the element has no default action. Remove reads of the old `verified` field and of `pid` on selected objects or window records. Pass a Windows application ID unchanged. For a specific macOS instance, use `getApp(String(row.pid))`. Update permission readers that test for `win32` to use `windows`.
|
||||
6. Custom providers replace `CuaBackend.description` with `capabilities` and `guidance` with `getGuidance(kind)`. Supply `permissionParameters`. Set `acceptsPidSelector: true` if `getApp` accepts process IDs. Rebuild custom `helperPath` against the [operation schema](../../../../packages/experimental/cua/src/protocol.ts) and [tagged permission response](../../../../packages/experimental/cua/src/access-types.ts). Restart. Verify both tool forms’ guidance, advertised methods, keyboard scopes and permission controls.
|
||||
@@ -9,15 +9,15 @@ description: "迁移 CUA 脚本和自定义原生提供方,使用显式窗口
|
||||
|
||||
## 变更
|
||||
|
||||
CUA 脚本在指定窗口上截图和操作鼠标。App 和 Window 对象共享同一应用句柄最近读取的辅助功能快照。App 用 `appHandle` 标识,Window 用 `windowId` 标识。查找应用时,运行中的匹配项仍提供 PID。
|
||||
截图和指针输入要求显式选择窗口。App(`appHandle`)和 Window(`windowId`)对象共享所属应用句柄的最新辅助功能快照。窗口 ID 是不透明字符串,macOS 应用发现保留运行进程的 PID,权限诊断保留 `helper.pid` 和 `parent.pid`。Windows 权限诊断不提供 PID。
|
||||
|
||||
提供方在安装时声明固定的平台能力并提供操作指引。原生 helper 返回实际的权限和会话锁定状态。Host 合并两者,作为 `getCapabilities()` 的结果。供权限页面使用的响应带有 `darwin` 或 `win32` 平台标记。
|
||||
提供方声明启动能力,并实现 `getGuidance(kind)`,不再传入 `guidance` 字符串。`raw` 选择原始 `cua` 工具的指引,`standalone` 选择 `cua_run_code` 的指引。原生 helper 返回运行时权限和会话锁定状态,Host 将其与启动能力合并到 `getCapabilities()` 中。供权限页面使用的响应带有 `darwin` 或 `windows` 平台标记。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 调用 `cua.getApp(selector)` 后,从 `app.getWindows()` 返回的列表选定目标,再调用 `app.getWindow(windowId)`。在窗口上调用 `getScreenshot()`、`click()`、`scroll()` 和 `drag()`,将原有的 `hover` 调用改为 `window.mouseMove()`。在 macOS 上,`window.click(id)` 在元素中心发送鼠标事件。需要执行默认辅助功能动作时,改用 `invoke(id)`。需要打开控件菜单且控件声明了 `AXShowMenu` 时,调用 `performAction(id, "AXShowMenu")`。
|
||||
2. 将 `getAXState()` 改为 `getAccessibilityTree()`,将 `getAXStateAndScreenshot()` 拆成读取树和截图两次调用。选择应用或窗口不读取 AX。使用元素 ID 前须显式调用 `getAccessibilityTree()`;使用 `{ emit: false }` 可保留快照但不显示树。截图不要求 AX 读取成功。所有坐标输入(包括相对坐标)之前都须成功读取覆盖该窗口的 AX 或取得截图;绝对坐标要求截图。[读取上限的迁移说明](../cua-ax-read-limits/guide.zh.md)列出了 macOS 默认值和配置含义的变化。
|
||||
1. 从 `app.getWindows()` 返回的列表选定目标,把原始字符串传给 `app.getWindow(windowId)`。曾以数字保存的窗口 ID 需要重新获取。在窗口上调用 `getScreenshot()`、`click()`、`scroll()` 和 `drag()`,将原有的 `hover` 调用改为 `window.mouseMove()`。在 macOS 上,`window.click(id)` 在元素中心发送鼠标事件。需要执行默认辅助功能动作时,改用 `invoke(id)`。需要打开控件菜单且控件声明了 `AXShowMenu` 时,调用 `performAction(id, "AXShowMenu")`。
|
||||
2. 将 `getAXState()` 改为 `getAccessibilityTree()`,将 `getAXStateAndScreenshot()` 拆成读取树和截图两次调用。选择应用或窗口不读取 AX。使用元素 ID 前须显式调用 `getAccessibilityTree()`。在 TypeScript PTC 中,将返回值传给 `inspect` 即可展示这棵树。截图不要求 AX 读取成功。所有坐标输入(包括相对坐标)之前都须成功读取覆盖该窗口的 AX 或取得截图;绝对坐标要求截图。[读取上限的迁移说明](../cua-ax-read-limits/guide.zh.md)列出了 macOS 默认值和配置含义的变化。
|
||||
3. 将数字元素索引改为从最新树中取得的字符串 ID。直接发送请求时使用 `elementId` 和 `snapshotId`。原先调用 `performSecondaryAction` 的地方,先用 `listActions(id)` 取得完整动作名,再调用 `performAction(id, exactName, argument?)`。要执行默认动作,可调用 `invoke(id)`。`setValue` 接受提供方支持的 JSON 值。 在 macOS 上,`performAction(id, 'AXRaise')` 只执行元素声明的抬起窗口动作。需要把应用切到前台时先调用 `app.activate()`。
|
||||
4. 将 `pressKey('shift+Tab')` 之类的组合键写法改为 `pressKey('Tab', ['shift'])`。调用 `sendKeyEvent` 前检查 `(await cua.getCapabilities()).methods`。在 macOS 上,同一次 `run_code` 中可调用此方法跨工具调用保持按键。如果需要一次重复按下,可在这次执行中对已按住的普通键再发一次 `down`。执行返回、异常退出或用户暂停时会释放仍按着的键。如果脚本用 `NATIVE_ERROR` 识别 macOS 上的未知键名,请改为处理 `INVALID_ARGUMENT`,并在重试前修正键名。
|
||||
5. 从动作结果读取 `dispatched`,再观察应用确认效果。元素没有默认动作时,`invoke` 返回 `dispatched: false`。移除读取旧字段 `verified` 的代码,以及对已选对象和窗口记录中 `pid` 的读取。需要 PID 时,使用 `listApps()` 或 `findApps()` 返回的运行中应用记录。
|
||||
6. 自定义提供方需将 `CuaBackend.description` 改为 `capabilities`,并向 `installCua` 传入 `guidance` 和 `permissionParameters`。如配置了 `helperPath`,请按[操作协议](../../../../packages/experimental/cua/src/protocol.ts)和[带平台标记的权限响应](../../../../packages/experimental/cua/src/access-types.ts)重新构建 helper。重启提供方,检查它声明的方法和键盘输入范围,再打开 Computer Use 权限页确认平台控件。
|
||||
4. 将 `pressKey('shift+Tab')` 之类的组合键写法改为 `pressKey('Tab', ['shift'])`。调用 `sendKeyEvent` 前检查 `(await cua.getCapabilities()).methods`。在 macOS 上,同一次 `run_code` 或 `cua_run_code` 中可调用此方法跨工具调用保持按键。如果需要一次重复按下,可在这次执行中对已按住的普通键再发一次 `down`。执行返回、异常退出或用户暂停时会释放仍按着的键。如果脚本用 `NATIVE_ERROR` 识别 macOS 上的未知键名,请改为处理 `INVALID_ARGUMENT`,并在重试前修正键名。
|
||||
5. 从动作结果读取 `dispatched`,再观察应用确认效果。元素没有默认动作时,`invoke` 返回 `dispatched: false`。移除读取旧字段 `verified` 的代码,以及对已选对象和窗口记录中 `pid` 的读取。Windows 应用 ID 原样传递。需要选择某个 macOS 进程时,使用 `getApp(String(row.pid))`。读取权限状态时,将原先判断 `win32` 的代码改为判断 `windows`。
|
||||
6. 自定义提供方将 `CuaBackend.description` 改为 `capabilities`,将 `guidance` 改为 `getGuidance(kind)`,并向 `installCua` 传入 `permissionParameters`。若 `getApp` 接受进程 ID,设置 `acceptsPidSelector: true`。按[操作协议](../../../../packages/experimental/cua/src/protocol.ts)和[带平台标记的权限响应](../../../../packages/experimental/cua/src/access-types.ts)重新构建自定义 `helperPath` 对应的 helper。重启后验证两种工具形态的指引、声明的方法、键盘输入范围及 Computer Use 权限页的平台控件。
|
||||
@@ -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.2-rc.1/package-export-cleanup/guide.md
|
||||
/:
|
||||
en: 5048dc7fc046e7ab
|
||||
zh: 7845cb3ba5a543d4
|
||||
/replace-internal-plugin-root-imports:
|
||||
en: 65af277d1f4ed1a0
|
||||
zh: 9d2b980fb6fabe89
|
||||
/replace-internal-plugin-root-imports/change:
|
||||
en: fcb1af4db4d0edd5
|
||||
zh: 6cc6ed79d0100eac
|
||||
/replace-internal-plugin-root-imports/migration:
|
||||
en: 01c24ef3fa9273f6
|
||||
zh: cd5ae3b6f0218aba
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: Internal helper values are removed from three published plugin-root entries.
|
||||
---
|
||||
|
||||
# Replace internal plugin-root imports
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
After upgrading from v0.2.2-rc.1, the following plugin-root values are no longer exported. Their implementations remain inside the packages. Consumers importing these values from a package root must change their imports or use the owning service.
|
||||
|
||||
| Package suffix (`@deepseek-ai/dsh-`) | Removed root helpers |
|
||||
|---|---|
|
||||
| `file-reference-local` | `DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES`, `DEFAULT_FILE_SEARCH_MAX_ENTRIES`, `DEFAULT_FILE_SEARCH_MAX_RESULTS`, `WorkspaceFileSearch`, `FILE_REFERENCE_PROMPT`, `activeAtToken`, `formatFileMention` |
|
||||
| `llm-pi-ai` | `recordKeyFor` |
|
||||
| `lsp-stdio` | `LspConnection`, `LspInstance`, `MessageDecoder`, `canonicalizeWorkspace`, `encodeMessage`, `negotiatePositionEncoding`, `normalizeHover`, `normalizeLocations`, `readHostSource`, `requestMethod`, `supportsOperation`, `supportsTransientOpen` |
|
||||
|
||||
Brand constructors, package subpaths, persisted values and Session formats are unchanged.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Stop importing the listed helper values from plugin roots. Package-owned tests can use relative imports from the corresponding internal modules.
|
||||
2. Application code should use the injected `fileReferences` or `lsp` service instead of provider implementation classes or protocol helpers. File-reference grammar remains at `@deepseek-ai/dsh-file-reference/grammar`; `FILE_REFERENCE_PROMPT` remains at `@deepseek-ai/dsh-file-reference`. `recordKeyFor` has no replacement public entry.
|
||||
3. Compile the application and run its tests. Load any plain-JavaScript consumers under Node as well: TypeScript checking alone does not cover missing ESM named exports in `.mjs` files.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: 三个已发布插件的根入口移除内部辅助实体的值导出。
|
||||
---
|
||||
|
||||
# 替换内部插件根导入
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
从 v0.2.2-rc.1 升级后,以下插件根入口不再导出这些值。对应实现在包内保留。从包根入口导入这些值的消费方需要更改导入或改用所属服务。
|
||||
|
||||
| 包名后缀(`@deepseek-ai/dsh-`) | 移除的根辅助实体 |
|
||||
|---|---|
|
||||
| `file-reference-local` | `DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES`、`DEFAULT_FILE_SEARCH_MAX_ENTRIES`、`DEFAULT_FILE_SEARCH_MAX_RESULTS`、`WorkspaceFileSearch`、`FILE_REFERENCE_PROMPT`、`activeAtToken`、`formatFileMention` |
|
||||
| `llm-pi-ai` | `recordKeyFor` |
|
||||
| `lsp-stdio` | `LspConnection`、`LspInstance`、`MessageDecoder`、`canonicalizeWorkspace`、`encodeMessage`、`negotiatePositionEncoding`、`normalizeHover`、`normalizeLocations`、`readHostSource`、`requestMethod`、`supportsOperation`、`supportsTransientOpen` |
|
||||
|
||||
品牌构造器、包子路径、持久化值和 Session 格式不变。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 停止从插件根入口导入上表的辅助实体值。包内测试可以相对导入对应内部模块。
|
||||
2. 应用代码应使用注入的 `fileReferences` 或 `lsp` 服务,而不是提供方的实现类或协议辅助函数。文件引用语法仍在 `@deepseek-ai/dsh-file-reference/grammar`;`FILE_REFERENCE_PROMPT` 仍在 `@deepseek-ai/dsh-file-reference`。`recordKeyFor` 没有替代的公开入口。
|
||||
3. 编译应用并运行其测试,同时用 Node 加载纯 JavaScript 消费方:仅检查 TypeScript 无法覆盖 `.mjs` 文件中缺少 ESM 命名导出的错误。
|
||||
@@ -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.2-rc.1/ptc-bootstrap-protocol/guide.md
|
||||
/:
|
||||
en: cee454924a9ab1f8
|
||||
zh: 2d7cbdcc398fa914
|
||||
/node-ptc-bootstrap-protocol-version-3-and-the-scoped-repl-format:
|
||||
en: 541c4f16c0107c90
|
||||
zh: edda22e2ebf01992
|
||||
/node-ptc-bootstrap-protocol-version-3-and-the-scoped-repl-format/change:
|
||||
en: f9b071385a270a1e
|
||||
zh: e664c70b769579e9
|
||||
/node-ptc-bootstrap-protocol-version-3-and-the-scoped-repl-format/migration:
|
||||
en: 350de9ad88b43420
|
||||
zh: 8b8bfe380cbf6fe8
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "Update custom Node PTC bootstraps to protocol version 3 before running them with the upgraded Host, and handle the new scoped-repl program format."
|
||||
---
|
||||
|
||||
# Node PTC bootstrap protocol version 3 and the scoped-repl format
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
The Node PTC Host requires bootstrap protocol version 3 instead of version 2. It rejects an older bootstrap before dispatching a program, with `bootstrap protocol mismatch; upgrade launch.bootstrapPath with the host`. This affects custom `launch.bootstrapPath` deployments in every execution mode, including `@deepseek-ai/dsh-ssh` hosts with a `node-script` launch that set `launch.bootstrapPath` and `launch.bootstrapHash`. The bootstrap bundled with DSH updates with the Host and needs no configuration change.
|
||||
|
||||
Version 3 carries optional `ProgramBootData.initialization` source for each new environment. The bootstrap evaluates it separately after installing current bindings, then awaits the Host's `initialized()` RPC acceptance and rechecks cancellation before running the model program. Initialization failure discards the environment and prevents model execution. The Node provider supports initializer registration only in `persistent-in-memory` mode.
|
||||
|
||||
Version 3 also carries `ProgramBootData.discardCompletion` to discard completion values before JSON validation and serialization. The bootstrap provides `inspect` and sends selected images through `HostMethods.image(id)` in output order. Custom bootstraps must support these fields and methods alongside environment initialization.
|
||||
|
||||
Version 3 also adds the `scoped-repl` program format. `PtcProgramFormat` in `@deepseek-ai/dsh-ptc-runtime` now includes `'scoped-repl'`, which `@deepseek-ai/dsh-ptc-runtime-node` reports as `PtcRuntime.programFormat` when `replScoped: true` is set with `executionMode: persistent-in-memory`. Code that matches the union exhaustively stops compiling until it handles the new value.
|
||||
|
||||
## Migration
|
||||
|
||||
1. If your Node PTC configuration sets `launch.bootstrapPath`, deploy the matching version of `@deepseek-ai/dsh-ptc-runtime-node` in its execution environment. Use the package's built `lib/process.js` or REPL-default `lib/persistent-process.js` with its dependencies. Keep the Host and bootstrap versions aligned; changing only the advertised version is insufficient.
|
||||
2. Update `launch.bootstrapPath` to the matching entry and restart the provider. For `@deepseek-ai/dsh-ssh` with a `node-script` launch, copy the new bootstrap to the remote `launch.bootstrapPath` and set `launch.bootstrapHash` to its lowercase SHA-256, for example from `sha256sum`. For local deployments that need no custom bootstrap, remove that override to use the installed default. The [Node runtime documentation](../../../../packages/ptc-runtime/ptc-runtime-node/README.md#persistent-repl) describes retained execution and remote launch.
|
||||
3. Handle `'scoped-repl'` wherever code switches over `PtcProgramFormat` or keys a record by it. A `scoped-repl` program is REPL statements whose final expression is the result; top-level `return` is not supported, and a final Promise is not awaited.
|
||||
4. Run a program in each configured execution mode and confirm that it completes without the protocol error. Consumers using `registerEnvironmentInitializer(program)` must also verify that initialization runs once per new environment and that an initialization error prevents the model program's effects. Typecheck the code that uses `PtcProgramFormat`.
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "自定义 Node PTC bootstrap 必须升级到协议版本 3,才能与新版 Host 一起运行;还要处理新增的 scoped-repl 程序格式。"
|
||||
---
|
||||
|
||||
# Node PTC bootstrap 协议版本 3 与 scoped-repl 格式
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
Node PTC Host 要求 bootstrap 协议版本从 2 升至 3。它会在派发程序前拒绝旧 bootstrap,并报告 `bootstrap protocol mismatch; upgrade launch.bootstrapPath with the host`。这影响所有执行模式下自定义 `launch.bootstrapPath` 的部署,包括使用 `node-script` 启动、设置了 `launch.bootstrapPath` 和 `launch.bootstrapHash` 的 `@deepseek-ai/dsh-ssh` 宿主。DSH 内置 bootstrap 随 Host 更新,无需修改配置。
|
||||
|
||||
版本 3 为每个新环境携带可选的 `ProgramBootData.initialization` 源码。bootstrap 安装本次绑定后独立执行初始化,随后等待 Host 对 `initialized()` RPC 的确认,再次检查取消状态后才执行模型程序。初始化失败会丢弃环境并阻止模型代码执行。Node 提供方仅在 `persistent-in-memory` 模式下支持注册初始化程序。
|
||||
|
||||
版本 3 还携带 `ProgramBootData.discardCompletion`,用于在 JSON 校验和序列化前丢弃完成值。bootstrap 提供 `inspect`,并通过 `HostMethods.image(id)` 按输出顺序发送选中的图片。自定义 bootstrap 必须同时支持这些字段、方法和环境初始化。
|
||||
|
||||
版本 3 还新增了 `scoped-repl` 程序格式。`@deepseek-ai/dsh-ptc-runtime` 的 `PtcProgramFormat` 新增 `'scoped-repl'`;`@deepseek-ai/dsh-ptc-runtime-node` 在 `executionMode: persistent-in-memory` 下设置 `replScoped: true` 时,`PtcRuntime.programFormat` 报告这个值。对这个联合类型做穷尽匹配的代码,处理新值之前无法通过编译。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 如果 Node PTC 配置设置了 `launch.bootstrapPath`,请在其执行环境中部署匹配版本的 `@deepseek-ai/dsh-ptc-runtime-node`。使用包内构建产物 `lib/process.js`,或默认采用 REPL 的 `lib/persistent-process.js`,并保留其依赖。Host 与 bootstrap 版本必须匹配;只修改声明的版本号并不够。
|
||||
2. 将 `launch.bootstrapPath` 指向匹配的入口并重启提供方。使用 `node-script` 启动的 `@deepseek-ai/dsh-ssh`:把新的 bootstrap 复制到远端的 `launch.bootstrapPath`,并把 `launch.bootstrapHash` 设为它的小写 SHA-256,例如用 `sha256sum` 计算。本地部署若不需要自定义 bootstrap,可以移除此覆盖项以使用安装默认值。[Node 运行时文档](../../../../packages/ptc-runtime/ptc-runtime-node/README.zh.md#persistent-repl)说明状态保留和远程启动方式。
|
||||
3. 在按 `PtcProgramFormat` 分支或以它为键的地方处理 `'scoped-repl'`。`scoped-repl` 程序是 REPL 语句,最后一条表达式就是结果;不支持顶层 `return`,末尾的 Promise 不会被等待。
|
||||
4. 在每种已配置的执行模式中运行一个程序,确认正常完成且没有协议错误。使用 `registerEnvironmentInitializer(program)` 的消费方还须验证:初始化在每个新环境中只执行一次,且初始化出错时模型程序不会产生效果。再对使用 `PtcProgramFormat` 的代码做类型检查。
|
||||
@@ -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.2-rc.1/ptc-inspect-output/guide.md
|
||||
/:
|
||||
en: aa6195ec0952307e
|
||||
zh: 1d80177284309ac6
|
||||
/explicit-output-in-typescript-ptc-programs:
|
||||
en: 064ec0081b5c2946
|
||||
zh: 4b67a513ea949220
|
||||
/explicit-output-in-typescript-ptc-programs/change:
|
||||
en: a4b3d449a4eedd9f
|
||||
zh: ae9b0ba7f9f4acbd
|
||||
/explicit-output-in-typescript-ptc-programs/migration:
|
||||
en: 0953d3ac2f2f01e3
|
||||
zh: b3614bf85d3d2fcf
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "TypeScript PTC programs display selected values through inspect and return ordered text and image output."
|
||||
---
|
||||
|
||||
# Explicit output in TypeScript PTC programs
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
TypeScript `run_code` programs display selected values through `inspect(...values)`. Function return values and final expressions previously appeared in the result; they are now omitted. Each inspected argument is rendered independently: strings appear directly, and other values use Node's object formatting without depth, array-length or string-length preview limits. Undefined properties, Error objects and circular references remain visible.
|
||||
|
||||
CUA observations previously produced output automatically. `getCapabilities()`, `listApps()`, `findApps(query)`, `getState()`, `app.getWindows()` and app or window `getAccessibilityTree()` now return their results to the program. Use `inspect` to display the values you need.
|
||||
|
||||
Only CUA screenshots and `read_image` return ordinary `Image` values in TypeScript programs. Their `path` points to an attachment file readable in the execution environment; `width` and `height` describe its stored pixels. Pass selected images to `inspect` alongside explanatory text, including images in arrays, Map entries, Set values and enumerable object properties. Nested image selection follows Node's custom formatting. Read CUA window IDs from the window object’s `windowId`.
|
||||
|
||||
Other image-producing tools retain their declared program values and automatically attach their pictures to the model context. Image adaptation requires a readable attachment path in the program filesystem. `fs-ssh` does not provide this mapping, so `read_image` and CUA screenshot bindings fail in that composition. Use a local filesystem composition for program Images.
|
||||
|
||||
Formatting selected values can require more memory than the retained output; see the [Node formatting memory limit](../../../../packages/ptc-runtime/ptc-runtime-node/README.md#known-limitations-and-deferred-work).
|
||||
|
||||
## Migration
|
||||
|
||||
1. Update programs that rely on completion values to explicitly inspect the values they need:
|
||||
|
||||
```js
|
||||
const result = await tools.bash({ command: 'pwd', description: 'Show current directory' })
|
||||
inspect(result)
|
||||
```
|
||||
|
||||
2. Remove output callbacks from `connectCua` and `createCuaApi`, the `emit` observation option and the unused `disableDiffing` AX option. Pass observations you want to display to `inspect`, for example `inspect(await app.getWindows())`. A custom dispatcher must return a program Image for screenshot operations.
|
||||
|
||||
```js
|
||||
const { connectCua } = await import("<host-provided bridge file URL>")
|
||||
const cua = connectCua(() => tools.cua)
|
||||
const app = await cua.getApp("TextEdit")
|
||||
const windows = await app.getWindows()
|
||||
const win = await app.getWindow(windows[0].windowId)
|
||||
const before = await win.getScreenshot()
|
||||
await app.pressKey("Tab")
|
||||
const after = await win.getScreenshot()
|
||||
inspect("before", { windowId: win.windowId }, before, "after", after)
|
||||
```
|
||||
|
||||
3. Direct runtime consumers can set `discardCompletion: true` to select the same explicit-output behavior.
|
||||
4. Third-party `PtcRuntime` providers replace returned `logs` arrays with `output: PtcOutput[]`; direct runtime consumers read `result.output`. Text entries are `{ type: 'text', text }`, image entries are `{ type: 'image', attachment }`, and array order is display order. Preserve emitted line breaks and concatenate adjacent text entries without separators.
|
||||
5. For custom `launch.bootstrapPath` deployments, follow the [Node PTC bootstrap upgrade guide](../ptc-bootstrap-protocol/guide.md).
|
||||
6. Run an updated program and verify that inspected values appear once and its final expression adds no output.
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "TypeScript PTC 程序通过 inspect 展示选定的值,并返回有序的文本与图片输出。"
|
||||
---
|
||||
|
||||
# TypeScript PTC 程序的显式输出
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
TypeScript `run_code` 程序通过 `inspect(...values)` 展示选定的值。此前函数返回值和末尾表达式的值会进入结果,现在会被省略。各个 inspect 参数独立渲染:字符串直接展示,其他值使用 Node 的对象格式,不限制预览深度、数组项数或字符串长度。undefined 属性、Error 对象和循环引用均会展示。
|
||||
|
||||
此前,CUA 观察方法默认会输出结果。现在,`getCapabilities()`、`listApps()`、`findApps(query)`、`getState()`、`app.getWindows()` 以及应用或窗口的 `getAccessibilityTree()` 都将结果返回给程序。需要展示哪些值,就把它们传给 `inspect`。
|
||||
|
||||
只有 CUA 截图与 `read_image` 在 TypeScript 程序中返回普通 `Image`,其 `path` 指向执行环境可读取的附件文件,`width` 与 `height` 为该文件的像素尺寸。使用 `inspect` 将选中的图片与说明一起展示;图片可嵌入数组、Map 条目、Set 的值以及可枚举的对象属性。嵌套图片的选择遵循 Node 的自定义格式化。CUA 窗口 ID 从窗口对象的 `windowId` 读取。
|
||||
|
||||
其他图片工具保留其声明的程序返回值,并将图片自动附加到模型上下文。图片适配要求附件在程序文件系统中有可读路径。`fs-ssh` 未提供这项映射,因此该组合中的 `read_image` 与 CUA 截图绑定会失败。需要程序 Image 时,使用本地文件系统组合。
|
||||
|
||||
格式化选定的值所需内存可能大于保留的输出;参见 [Node 格式化的内存限制](../../../../packages/ptc-runtime/ptc-runtime-node/README.zh.md#known-limitations-and-deferred-work)。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 将依赖完成值的程序,改为显式 inspect 所需的值:
|
||||
|
||||
```js
|
||||
const result = await tools.bash({ command: 'pwd', description: 'Show current directory' })
|
||||
inspect(result)
|
||||
```
|
||||
|
||||
2. 删除 `connectCua` 和 `createCuaApi` 的输出回调、观察选项 `emit` 和已无作用的 AX 选项 `disableDiffing`。将要展示的观察结果传给 `inspect`,例如 `inspect(await app.getWindows())`。自定义分派器须为截图操作直接返回程序可用的 `Image`。
|
||||
|
||||
```js
|
||||
const { connectCua } = await import("<host-provided bridge file URL>")
|
||||
const cua = connectCua(() => tools.cua)
|
||||
const app = await cua.getApp("TextEdit")
|
||||
const windows = await app.getWindows()
|
||||
const win = await app.getWindow(windows[0].windowId)
|
||||
const before = await win.getScreenshot()
|
||||
await app.pressKey("Tab")
|
||||
const after = await win.getScreenshot()
|
||||
inspect("before", { windowId: win.windowId }, before, "after", after)
|
||||
```
|
||||
|
||||
3. 直接调用运行时的消费方可设置 `discardCompletion: true`,选择相同的显式输出行为。
|
||||
4. 第三方 `PtcRuntime` 提供方将返回的 `logs` 数组替换为 `output: PtcOutput[]`;直接消费运行时结果的调用方改为读取 `result.output`。文本条目为 `{ type: 'text', text }`,图片条目为 `{ type: 'image', attachment }`,数组顺序就是展示顺序。保留实际输出的换行,相邻文本条目直接拼接,不额外插入分隔符。
|
||||
5. 使用自定义 `launch.bootstrapPath` 的部署,请按 [Node PTC bootstrap 升级指南](../ptc-bootstrap-protocol/guide.zh.md)更新。
|
||||
6. 运行更新后的程序,确认 inspect 的值只出现一次,且末尾表达式不额外输出。
|
||||
@@ -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.2-rc.1/ptc-scoped-repl-default/guide.md
|
||||
/:
|
||||
en: cff145a993d30f0a
|
||||
zh: 784a2ce78146a563
|
||||
/retained-node-ptc-runs-the-scoped-repl-by-default:
|
||||
en: 60119ea304ce7bad
|
||||
zh: c0724f1da0e4590c
|
||||
/retained-node-ptc-runs-the-scoped-repl-by-default/change:
|
||||
en: 1ca349eed9a54b9b
|
||||
zh: 5dc5ba17ec953f0e
|
||||
/retained-node-ptc-runs-the-scoped-repl-by-default/migration:
|
||||
en: 5d1e1f1bf7ce7821
|
||||
zh: aafa439b6080acb4
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "Node PTC persistent-in-memory execution runs the scoped REPL unless replScoped is false."
|
||||
---
|
||||
|
||||
# Retained Node PTC runs the scoped REPL by default
|
||||
|
||||
English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
`@deepseek-ai/dsh-ptc-runtime-node` previously ran `executionMode: persistent-in-memory` programs in Node's REPL evaluator when `replScoped` was omitted. It now runs them as scoped REPL statements and reports program format `scoped-repl`. Each call is a separate strict-mode script that starts with copies of the top-level bindings left by earlier calls, so a later call may declare a `let`, `const`, or `class` name again. `require`, `_`, `_error`, and globals created by assigning an undeclared name are unavailable. The model guidance describes this behavior; the package README lists its [known limitations](../../../../packages/ptc-runtime/ptc-runtime-node/README.md#known-limitations-and-deferred-work).
|
||||
|
||||
This affects:
|
||||
|
||||
- profiles whose `ptc-runtime` entry sets `executionMode: persistent-in-memory` without `replScoped`, including workflow-ptc scripts that run on that runtime;
|
||||
- persistent PTC enabled from the Computer Use detail page, which now writes `replScoped: true`;
|
||||
- the Computer Use `cua_run_code` tool, whose dedicated runtime omits `replScoped` unless `standaloneRunCode.runtime.replScoped` is set;
|
||||
- profile editors that merge `resolvePersistentExecution` from `@deepseek-ai/dsh-ptc-runtime-node/configuration`, which now returns `replScoped: true` for function bodies and REPL statements.
|
||||
|
||||
`per-call` and `vm-script` execution, including the default profile, are unchanged, and entries that set `replScoped: false` keep Node's REPL evaluator.
|
||||
|
||||
## Migration
|
||||
|
||||
1. To keep the scoped REPL, replace REPL-only code: use `await import(...)` instead of `require`, assign `globalThis.name` for an intentional global, and use a new name instead of reading a name's earlier value in its own `let` or `const` initializer.
|
||||
2. To restore Node's REPL evaluator, set `replScoped: false` in `$DSH_HOME/profiles/<profile>/cordis.patch.yml`; to move an entry that sets `false` to the scoped REPL, delete the key or set `true`. Each patch below replaces that entry's whole `config`, so copy the entry's existing fields into it, for the `cua` entry including `agentPreset`, `allowedApps`, and the other `standaloneRunCode` fields:
|
||||
|
||||
```yaml
|
||||
- id: ptc-runtime
|
||||
config:
|
||||
executionMode: persistent-in-memory
|
||||
replScoped: false
|
||||
```
|
||||
|
||||
For `cua_run_code`, set it on the Computer Use provider entry:
|
||||
|
||||
```yaml
|
||||
- id: cua
|
||||
config:
|
||||
standaloneRunCode:
|
||||
runtime:
|
||||
replScoped: false
|
||||
```
|
||||
|
||||
3. Restart the profile and run `const probe = 1` in two consecutive calls. The scoped REPL accepts both calls; Node's REPL evaluator reports that `probe` has already been declared in the second.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "Node PTC 的 persistent-in-memory 执行默认使用 scoped REPL,replScoped 为 false 时除外。"
|
||||
---
|
||||
|
||||
# 保留状态的 Node PTC 默认使用 scoped REPL
|
||||
|
||||
[English](guide.md) | 中文
|
||||
|
||||
## 变更
|
||||
|
||||
`@deepseek-ai/dsh-ptc-runtime-node` 在 `executionMode: persistent-in-memory` 下省略 `replScoped` 时,原先用 Node 的 REPL 求值器运行程序;现在按 scoped REPL 语句运行,并报告程序格式 `scoped-repl`。每次调用是独立的严格模式脚本,开始时拿到之前调用留下的顶层绑定的拷贝,因此之后的调用可以再次声明同名的 `let`、`const` 或 `class`。`require`、`_`、`_error`,以及给未声明的名字赋值而产生的全局变量都不可用。模型说明描述了这些行为;包 README 列出了它的[已知限制](../../../../packages/ptc-runtime/ptc-runtime-node/README.zh.md#known-limitations-and-deferred-work)。
|
||||
|
||||
受影响的有:
|
||||
|
||||
- `ptc-runtime` 条目设置了 `executionMode: persistent-in-memory` 但没写 `replScoped` 的 profile,包括在该运行时上运行的 workflow-ptc 脚本;
|
||||
- 在 Computer Use 详情页打开的持久化 PTC,现在会写入 `replScoped: true`;
|
||||
- Computer Use 的 `cua_run_code` 工具,其专用运行时在没有设置 `standaloneRunCode.runtime.replScoped` 时省略 `replScoped`;
|
||||
- 合并 `@deepseek-ai/dsh-ptc-runtime-node/configuration` 中 `resolvePersistentExecution` 结果的 profile 编辑器,该函数现在对函数体和 REPL 语句返回 `replScoped: true`。
|
||||
|
||||
`per-call` 和 `vm-script` 执行(包括默认 profile)不变,设置了 `replScoped: false` 的条目继续使用 Node 的 REPL 求值器。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 如果保留 scoped REPL,替换只在 REPL 下可用的代码:用 `await import(...)` 代替 `require`,有意的全局变量赋给 `globalThis.name`,不要在 `let` 或 `const` 的初始化表达式里读取同名的旧值,改用新名字。
|
||||
2. 如果要恢复 Node 的 REPL 求值器,在 `$DSH_HOME/profiles/<profile>/cordis.patch.yml` 里设置 `replScoped: false`;要让设为 `false` 的条目改用 scoped REPL,删除这个键或改为 `true`。下面每个 patch 都会替换该条目的整个 `config`,所以要把条目已有的字段一并写进去,`cua` 条目还包括 `agentPreset`、`allowedApps` 和 `standaloneRunCode` 的其他字段:
|
||||
|
||||
```yaml
|
||||
- id: ptc-runtime
|
||||
config:
|
||||
executionMode: persistent-in-memory
|
||||
replScoped: false
|
||||
```
|
||||
|
||||
对 `cua_run_code`,在 Computer Use 提供方条目上设置:
|
||||
|
||||
```yaml
|
||||
- id: cua
|
||||
config:
|
||||
standaloneRunCode:
|
||||
runtime:
|
||||
replScoped: false
|
||||
```
|
||||
|
||||
3. 重启 profile,在连续两次调用里运行 `const probe = 1`。scoped REPL 两次都接受;Node 的 REPL 求值器在第二次报告 `probe` 已经声明过。
|
||||
@@ -301,6 +301,7 @@
|
||||
"@deepseek-ai/dsh-experimental-worktree": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-tool-worktree": "workspace:*",
|
||||
"@deepseek-ai/dsh-cua-bundle": "workspace:*",
|
||||
"@deepseek-ai/dsh-cua-windows": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-subagent-oar": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-tool-subagent-oar": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-cua-macos": "workspace:*",
|
||||
|
||||
@@ -32,7 +32,7 @@ function llmRuntime(overrides: Partial<LlmRuntime> = {}): LlmRuntime {
|
||||
},
|
||||
}),
|
||||
...overrides,
|
||||
} as unknown as LlmRuntime
|
||||
} as LlmRuntime
|
||||
}
|
||||
|
||||
async function createControl(runtime: LlmRuntime, initial?: ModelSelection) {
|
||||
|
||||
@@ -80,7 +80,7 @@ it('skips an unsupported saved bundle before composing any runtime overrides and
|
||||
const unsupported = process.platform === 'darwin' ? 'win32' : 'darwin'
|
||||
const anchor = stageInstallation({
|
||||
base: { patch: '- insert:\n - id: ptc-runtime\n name: runtime\n config: { executionMode: isolated }\n' },
|
||||
restricted: { platforms: [unsupported], patch: '- id: ptc-runtime\n config: { executionMode: persistent-in-memory }\n' },
|
||||
restricted: { platforms: [unsupported], patch: '- id: ptc-runtime\n config: { executionMode: persistent-in-memory, replScoped: false }\n' },
|
||||
})
|
||||
const dir = join(tmp(), 'profile')
|
||||
initProfile(dir, ['base', 'restricted'])
|
||||
|
||||
@@ -54,11 +54,11 @@
|
||||
en: 131811a8a12c9d81
|
||||
zh: f8a8c1e02a5787f8
|
||||
/chat-conversation-node-rules/activity-summaries/tool-name-classification:
|
||||
en: a962c232f916bfff
|
||||
zh: 2e80754623c3f14d
|
||||
en: 28760f1caab0763b
|
||||
zh: 2d0eb175509096e7
|
||||
/chat-conversation-node-rules/activity-summaries/subagents-background-jobs-and-nested-calls:
|
||||
en: e7796e7ba825c3d6
|
||||
zh: c377d9eab0a8869f
|
||||
en: da0c896564c0d78e
|
||||
zh: 6ff7b8a3b2f2de8c
|
||||
/chat-conversation-node-rules/activity-summaries/live-activity-and-detail:
|
||||
en: e0ca09c305690be7
|
||||
zh: fb35d0238b1a827b
|
||||
@@ -269,7 +269,7 @@ Classification uses the recorded tool name exactly: no case folding, namespace s
|
||||
| `write` | `write` |
|
||||
| `edit` | `edit`, `apply_patch` |
|
||||
| `commands` | `bash`, `pwsh`, `exec_command`, `write_stdin`, or prefix `terminal_` |
|
||||
| `code` | `run_code` |
|
||||
| `code` | `run_code`, `cua_run_code` |
|
||||
| `webSearch` | `web_search` |
|
||||
| `webFetch` | `web_fetch` |
|
||||
| `subagents` | `subagent` or prefix `subagent_` |
|
||||
@@ -290,7 +290,7 @@ A subagent tool follows ordinary group boundaries: it does not create a group me
|
||||
| Background subagent startup has returned its tool result, while the child is still running | Keep its category count, but remove that call from running-activity candidates. The child does not keep the parent's group in the subagent activity state. |
|
||||
| Tool calls recorded only in a child Session | Do not include them in the parent group's counts or live detail. Only calls actually attached to a member's `subCalls` participate recursively. |
|
||||
|
||||
A `run_code` root counts once as `code`; its recorded PTC subcalls each count in their own categories. The wrapper is not replaced by leaf-call counts, and children are not all classified as `code`. With distinct call ids, `run_code → [read, bash, subagent_codex]` produces `code=1, read=1, commands=1, subagents=1`, in that tie order. A subcall can supply the live category while the outer `run_code` remains running.
|
||||
A `run_code` or `cua_run_code` root counts once as `code`; its recorded PTC subcalls each count in their own categories. The wrapper is not replaced by leaf-call counts, and children are not all classified as `code`. With distinct call ids, `run_code → [read, bash, subagent_codex]` produces `code=1, read=1, commands=1, subagents=1`, in that tie order. A subcall can supply the live category while the outer `run_code` remains running.
|
||||
|
||||
Exact-name rules also mean that a recorded name such as `functions.read`, `mcp.read`, or `Read` falls into `tools`, while `browser_inspect` falls into `search`. A `terminal_*` name follows the command rule even when its operation only inspects or closes a terminal, unless the earlier `_inspect` rule matches.
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ description: "Chat 过程分组与活动摘要的明细业务规则。"
|
||||
| `write` | `write` |
|
||||
| `edit` | `edit`、`apply_patch` |
|
||||
| `commands` | `bash`、`pwsh`、`exec_command`、`write_stdin`,或以 `terminal_` 开头 |
|
||||
| `code` | `run_code` |
|
||||
| `code` | `run_code`、`cua_run_code` |
|
||||
| `webSearch` | `web_search` |
|
||||
| `webFetch` | `web_fetch` |
|
||||
| `subagents` | `subagent`,或以 `subagent_` 开头 |
|
||||
@@ -290,7 +290,7 @@ Subagent 工具遵循普通分组边界,不会仅因委派任务就单独成
|
||||
| 后台 subagent 的启动调用已返回工具结果,但子代理仍在运行 | 保留类别次数,但该调用不再参与运行中活动选择;子代理的执行不会维持父组的 subagent 活动状态。 |
|
||||
| 工具调用只记录在子 Session 中 | 不纳入父组计数或实时详情;只有实际附在本组成员 `subCalls` 中的调用才递归参与。 |
|
||||
|
||||
`run_code` 根调用自身计一次 `code`,已记录的每个 PTC 子调用另按自己的类别计数。不会用叶子调用替代包装调用,也不会把子调用统一归为 `code`。各调用 id 不同时,`run_code → [read, bash, subagent_codex]` 得到 `code=1, read=1, commands=1, subagents=1`,同次数时按此顺序排列。外层 `run_code` 仍在运行时,组的实时类别可以由子调用决定。
|
||||
`run_code` 或 `cua_run_code` 根调用自身计一次 `code`,已记录的每个 PTC 子调用另按自己的类别计数。不会用叶子调用替代包装调用,也不会把子调用统一归为 `code`。各调用 id 不同时,`run_code → [read, bash, subagent_codex]` 得到 `code=1, read=1, commands=1, subagents=1`,同次数时按此顺序排列。外层 `run_code` 仍在运行时,组的实时类别可以由子调用决定。
|
||||
|
||||
精确名称规则也意味着:记录为 `functions.read`、`mcp.read` 或 `Read` 的名称归 `tools`,`browser_inspect` 归 `search`。`terminal_*` 即使只检查或关闭终端,也按命令规则分类,除非先命中 `_inspect` 规则。
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ function activity(name: string): RunningProcessActivity {
|
||||
if (name === 'write') return 'write'
|
||||
if (name === 'edit' || name === 'apply_patch') return 'edit'
|
||||
if (['bash', 'pwsh', 'exec_command', 'write_stdin'].includes(name) || name.startsWith('terminal_')) return 'commands'
|
||||
if (name === 'run_code') return 'code'
|
||||
if (name === 'run_code' || name === 'cua_run_code') return 'code'
|
||||
if (name === 'web_search') return 'webSearch'
|
||||
if (name === 'web_fetch') return 'webFetch'
|
||||
if (name === 'subagent' || name.startsWith('subagent_')) return 'subagents'
|
||||
|
||||
@@ -211,6 +211,7 @@ describe('Definition-owned Chat process groups', () => {
|
||||
|
||||
it.each([
|
||||
['read', 'read'], ['read_image', 'readImage'], ['write', 'write'], ['edit', 'edit'], ['apply_patch', 'edit'],
|
||||
['run_code', 'code'], ['cua_run_code', 'code'],
|
||||
['todo_write', 'plan'], ['create_goal', 'plan'], ['update_goal', 'plan'], ['get_goal', 'plan'],
|
||||
['list_mcp_resources', 'tools'], ['list_mcp_resource_templates', 'tools'], ['read_mcp_resource', 'tools'],
|
||||
] as const)('%s keeps its activity category through preparation, dispatch, and result', (name, kind) => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
ConversationViewNode, TurnLocation,
|
||||
} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
import type { GlobalStandardProps, SessionStandardProps } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { apply as applyLocale, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type { ChatFileMentions, TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client'
|
||||
import { makeTranslate, stubConfigForm } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
@@ -40,6 +41,38 @@ import { en, zh } from '../src/client/locales.ts'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
|
||||
const unusedStandardHook = (): never => { throw new Error('isolated deliverables fixture does not consume this framework hook') }
|
||||
const standardProps: GlobalStandardProps & Omit<SessionStandardProps, 'sessionId'> = {
|
||||
useSession: unusedStandardHook,
|
||||
useProjection: unusedStandardHook,
|
||||
useAccess: unusedStandardHook,
|
||||
useRemoteAccess: unusedStandardHook,
|
||||
useSessionAccess: unusedStandardHook,
|
||||
useWorkspaceAccess: unusedStandardHook,
|
||||
useConversation: unusedStandardHook,
|
||||
useInput: unusedStandardHook,
|
||||
useChat: unusedStandardHook,
|
||||
useTrajectory: unusedStandardHook,
|
||||
usePanelInfo: unusedStandardHook,
|
||||
useSessions: unusedStandardHook,
|
||||
useSessionStatus: unusedStandardHook,
|
||||
useSessionRetainInfo: unusedStandardHook,
|
||||
useResource: unusedStandardHook,
|
||||
useWorkspaces: unusedStandardHook,
|
||||
useContextMenu: contextMenuHook(),
|
||||
inputActions: {
|
||||
insertQuote: () => false,
|
||||
captureInsertion: unusedStandardHook,
|
||||
insertText: unusedStandardHook,
|
||||
setDraft: unusedStandardHook,
|
||||
persistDraft: unusedStandardHook,
|
||||
addAttachments: unusedStandardHook,
|
||||
removeAttachment: unusedStandardHook,
|
||||
pruneAttachments: unusedStandardHook,
|
||||
submit: unusedStandardHook,
|
||||
},
|
||||
}
|
||||
|
||||
function openProps(controller = new PresentedOpenController(), summaries = new ChangesSummaryStore()) {
|
||||
controller.host.set({ name: 'desktop', available: true, fileManager: 'finder' })
|
||||
const diffs = new ChangesDiffStore()
|
||||
@@ -926,7 +959,7 @@ describe('plugin registration', () => {
|
||||
}
|
||||
expect(preview.mock.calls).toEqual(Array.from({ length: 4 }, () => ['out/report.docx']))
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
const face = entry!.inject!(SessionId('child-session') as never) as unknown as DeliverablesInjected
|
||||
const face: DeliverablesInjected = entry!.inject!(SessionId('child-session') as never) as Record<string, unknown> & DeliverablesInjected
|
||||
fetcher.mockResolvedValueOnce(Response.json({ name: 'desktop', available: true, fileManager: 'finder' }))
|
||||
await face.reloadPresentedHost()
|
||||
expect(face.hooks.presentedHost.getSnapshot()).toMatchObject({ name: 'desktop' })
|
||||
@@ -944,7 +977,7 @@ describe('plugin registration', () => {
|
||||
face.openChangesReview({ sessionId: SessionId('child-session'), seq: 5, turn: 3 }, 1)
|
||||
expect(openResource).toHaveBeenCalledWith('dsh-resource://changes-review/session/child-session/5/3', { params: { index: 1 } })
|
||||
expect((registered as { title(address: string): string }).title('dsh-resource://changes-review/session/child-session/5/3')).toBe('Review · turn 3')
|
||||
const tabFace = tabEntry!.inject!(SessionId('child-session') as never) as unknown as ReviewInjected
|
||||
const tabFace: ReviewInjected = tabEntry!.inject!(SessionId('child-session') as never) as Record<string, unknown> & ReviewInjected
|
||||
fetcher.mockResolvedValueOnce(Response.json({ turn: 3, files: [], total: 0, added: 0, deleted: 0 }))
|
||||
await tabFace.loadChangesSummary(SessionId('child-session'), 6)
|
||||
expect(tabFace.hooks.changesSummary.getSnapshot()['api/changes.summary?sessionId=child-session&seq=6']).toEqual({ turn: 3, files: [], total: 0, added: 0, deleted: 0 })
|
||||
@@ -1146,7 +1179,13 @@ it('loads desktop information once the tail renders and not again while it is kn
|
||||
|
||||
it('contributes file artifacts to the tail list only for turns with deliveries', () => {
|
||||
const owner = tailOwner(undefined, 3)
|
||||
const props = { ...openProps(), ...owner, sessionId: SessionId('session'), t: makeTranslate(en) } as unknown as Parameters<typeof DeliverablesTail>[0]
|
||||
const props: Parameters<typeof DeliverablesTail>[0] = {
|
||||
...standardProps,
|
||||
...openProps(),
|
||||
...owner,
|
||||
sessionId: SessionId('session'),
|
||||
t: makeTranslate(en),
|
||||
}
|
||||
const view = render(<DeliverablesTail {...props} />)
|
||||
expect(view.container.innerHTML).toBe('')
|
||||
const withFile = tailOwner({ produced: [], presented: [{ path: 'report.md', seq: 2, index: 0 }] }, 3)
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 4d8cb38024095705
|
||||
zh: 1baf6fa42f3fb80f
|
||||
/deepseek-ai-dsh-client-ui-tool/use-this-package/registering-a-business-tool-view:
|
||||
en: e458aff61e634190
|
||||
zh: 650e417f450b3edf
|
||||
en: 3dd6bd71dfc5527d
|
||||
zh: 9fe3c2135014bb7b
|
||||
/deepseek-ai-dsh-client-ui-tool/use-this-package/built-in-views:
|
||||
en: 33f41ad70d0728f7
|
||||
zh: b7c65108c5921134
|
||||
|
||||
@@ -47,7 +47,7 @@ ctx.slots.inject('tool.call.toolview', () =>
|
||||
}, BusinessToolRow))
|
||||
```
|
||||
|
||||
The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the `phase` discriminant and its stage-specific `block`, optional `cwd` and `home`, the Gateway-authorized `loadImage` loader (for a view whose result carries durable images), and plain `openFile`/`inspect` callbacks. A PTC dispatch block retains its event's `parentCallId`; a root Session call has no such field, so descendants route through the same keyed dispatch — a registered view such as `read_image` renders its card there, and unregistered descendants keep the generic flattened form. Path summaries relativize to the Session cwd first, then replace a leftover POSIX Host home with `~`; `filePath` and Host open prefer the operation-time absolute result path while displayed labels keep the authored argument. Successful older root results retain their argument fallback; pending relative paths wait for a result. The registration receives the normal Session slot runtime share but no React node or Runtime service.
|
||||
The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the `phase` discriminant and its stage-specific `block`, optional `cwd` and `home`, the Gateway-authorized `loadImage` loader (for a view whose result carries durable images), and plain `openFile`/`inspect` callbacks. A PTC dispatch block retains its event's `parentCallId`; a root Session call has no such field, so descendants route through the same keyed dispatch — a registered view such as `read_image` renders its card there, and unregistered descendants keep the generic flattened form. Path summaries relativize to the Session cwd first, then replace a leftover POSIX Host home with `~`; `filePath` and Host open prefer the operation-time absolute result path while displayed labels keep the authored argument. Successful older root results retain their argument fallback; pending relative paths wait for a result. The registration receives the normal Session slot runtime share but no React node or Runtime service. Generic result output preserves block order and displays image attachment names or IDs with dimensions and byte counts.
|
||||
|
||||
### Built-in views
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ ctx.slots.inject('tool.call.toolview', () =>
|
||||
}, BusinessToolRow))
|
||||
```
|
||||
|
||||
owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、`phase` 判别字段及对应阶段的 `block`、可选 `cwd` 与 `home`、经 Gateway 授权的 `loadImage` loader(供结果携带持久图像的视图使用),以及普通的 `openFile`/`inspect` 回调。PTC dispatch 块保留事件的 `parentCallId`;根会话调用没有该字段,因此后代调用都走同一条按 key 分发路径:已注册视图的调用(如 `read_image`)也会在嵌套处渲染对应卡片,未注册的后代调用则保持通用压平形式。路径摘要先相对会话 cwd 缩短,再把剩余的 POSIX Host home 写成 `~`;`filePath` 与 Host 打开优先使用操作发生时记录的绝对结果路径,显示标签仍保留原始参数。成功的旧根调用结果继续回退到原参数;运行中的相对路径等待结果。注册项会收到常规的会话 slot 运行时共享数据,但不会收到 React 节点或运行时服务。
|
||||
owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、`phase` 判别字段及对应阶段的 `block`、可选 `cwd` 与 `home`、经 Gateway 授权的 `loadImage` loader(供结果携带持久图像的视图使用),以及普通的 `openFile`/`inspect` 回调。PTC dispatch 块保留事件的 `parentCallId`;根会话调用没有该字段,因此后代调用都走同一条按 key 分发路径:已注册视图的调用(如 `read_image`)也会在嵌套处渲染对应卡片,未注册的后代调用则保持通用压平形式。路径摘要先相对会话 cwd 缩短,再把剩余的 POSIX Host home 写成 `~`;`filePath` 与 Host 打开优先使用操作发生时记录的绝对结果路径,显示标签仍保留原始参数。成功的旧根调用结果继续回退到原参数;运行中的相对路径等待结果。注册项会收到常规的会话 slot 运行时共享数据,但不会收到 React 节点或运行时服务。 通用结果输出保留块的顺序,并以附件名称或 ID、尺寸和字节数展示图片引用。
|
||||
|
||||
### 内置视图
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ const TOOL_VARIANTS: Record<string, ToolRowVariant> = {
|
||||
edit: 'edit',
|
||||
apply_patch: 'edit',
|
||||
run_code: 'code',
|
||||
cua_run_code: 'code',
|
||||
cordis_package_inspect: 'read',
|
||||
cordis_runtime_inspect: 'read',
|
||||
// The three run-control verbs take one package id and produce a receipt, so
|
||||
@@ -202,21 +203,26 @@ function deriveAutoReviewDenial(block: ToolCallBlock): AutoReviewDenial | null {
|
||||
|
||||
/**
|
||||
* Flatten a settled result's content blocks to display text: text blocks
|
||||
* verbatim, other block shapes as pretty JSON. Empty content on a failed call
|
||||
* falls back to the structured error's `name: code` line.
|
||||
* verbatim, images through the supplied formatter, and other blocks
|
||||
* as pretty JSON. Empty failure content falls back to the structured error's
|
||||
* `name: code` line.
|
||||
* @param node - the settled result node.
|
||||
* @param formatImage - Optional formatter for unvalidated image attachments; omission or undefined preserves image JSON.
|
||||
* @returns the flattened result text (may be empty).
|
||||
*/
|
||||
export function resultText(node: ToolResultNode): string {
|
||||
export function resultText(node: ToolResultNode, formatImage?: (image: unknown) => string | undefined): string {
|
||||
const parts: string[] = []
|
||||
for (const block of node.content) {
|
||||
if (block.type === 'text') parts.push(block.text)
|
||||
else parts.push(JSON.stringify(block, null, 2))
|
||||
else {
|
||||
const image = block.type === 'image' ? formatImage?.(block.attachment) : undefined
|
||||
parts.push(image ?? JSON.stringify(block, null, 2))
|
||||
}
|
||||
}
|
||||
if (parts.length === 0 && node.error !== undefined) {
|
||||
parts.push(`${node.error.name}: ${node.error.code}`)
|
||||
}
|
||||
return parts.join('\n')
|
||||
return parts.reduce((text, part, index) => text + (index === 0 || text.endsWith('\n') ? '' : '\n') + part, '')
|
||||
}
|
||||
|
||||
function parseArgs(argsRaw: string): unknown {
|
||||
@@ -367,9 +373,13 @@ export function formatToolBody(variant: ToolRowVariant, argsRaw: string): string
|
||||
* @param block - preparing call, dispatched call, or result from the snapshot.
|
||||
* @param cwd - session workspace root; workspace-rooted path summaries display relative to it.
|
||||
* @param home - host account home; a leftover POSIX home path displays as `~`.
|
||||
* @param formatImage - Optional image formatter; omission preserves image JSON.
|
||||
* @returns the row model.
|
||||
*/
|
||||
export function toolRowModel(toolName: string, block: ToolCallBlock, cwd?: string, home?: string): ToolRowModel {
|
||||
export function toolRowModel(
|
||||
toolName: string, block: ToolCallBlock, cwd?: string, home?: string,
|
||||
formatImage?: (image: unknown) => string | undefined,
|
||||
): ToolRowModel {
|
||||
const variant = classifyTool(toolName)
|
||||
const titleKey = toolTitleKey(toolName)
|
||||
const done = 'kind' in block
|
||||
@@ -403,7 +413,7 @@ export function toolRowModel(toolName: string, block: ToolCallBlock, cwd?: strin
|
||||
// The empty string is "no text" for both derived result fields: a settled
|
||||
// call with blank content has nothing to expand, and a blank first line
|
||||
// would erase the collapsed error row's summary slot.
|
||||
const output = done ? (resultText(block) || null) : null
|
||||
const output = done ? (resultText(block, formatImage) || null) : null
|
||||
const errorSummary = state === 'error' && output !== null ? firstLine(output) : null
|
||||
const bodyRaw = argsRaw === '' ? null : argsRaw
|
||||
return {
|
||||
|
||||
@@ -36,7 +36,14 @@ export type GenericToolCardProps = ToolCallOwnerProps & {
|
||||
export function GenericToolCard({
|
||||
toolName, block, cwd, home, openFile, getFileTriggerProps, inspect, useDisclosure, t,
|
||||
}: GenericToolCardProps) {
|
||||
const model = toolRowModel(toolName, block, cwd, home)
|
||||
const model = toolRowModel(toolName, block, cwd, home, (image) => {
|
||||
if (typeof image !== 'object' || image === null || Array.isArray(image)) return undefined
|
||||
const { attachmentId, name, width, height, bytes } = image as Record<string, unknown>
|
||||
if (typeof attachmentId !== 'string' || attachmentId === ''
|
||||
|| name !== undefined && typeof name !== 'string'
|
||||
|| ![width, height, bytes].every(value => typeof value === 'number' && Number.isInteger(value) && value > 0)) return undefined
|
||||
return `${name ?? attachmentId} (${t('row.imageSize', { width, height, bytes })})`
|
||||
})
|
||||
const autoReview = model.autoReviewDenial === null
|
||||
? null
|
||||
: localizeAutoReviewDenial(model.autoReviewDenial, t)
|
||||
|
||||
@@ -45,17 +45,17 @@ beforeEach(() => {
|
||||
const PROGRAM = 'const listing = await tools.bash({ command: "ls notes", description: "List notes" })\nreturn listing'
|
||||
const RUN_CODE_ARGS = JSON.stringify({ code: PROGRAM, description: 'List the notes directory' })
|
||||
|
||||
const codeResult = (seq: number, callId: string): ToolResultNode => ({
|
||||
const codeResult = (seq: number, callId: string, name = 'run_code', argsRaw = RUN_CODE_ARGS): ToolResultNode => ({
|
||||
kind: 'tool-result', seq, time: seq * 1_000, callId,
|
||||
name: 'run_code', args: PartialArguments.fromText(RUN_CODE_ARGS),
|
||||
call: { name: 'run_code', argsRaw: RUN_CODE_ARGS },
|
||||
name, args: PartialArguments.fromText(argsRaw),
|
||||
call: { name, argsRaw },
|
||||
callTime: seq * 1_000 - 500,
|
||||
content: [{ type: 'text', text: 'demo.txt' }], isError: false,
|
||||
subCalls: [],
|
||||
})
|
||||
|
||||
const runningCode = (callId: string): StartedToolCall => ({
|
||||
phase: 'start' as const, args: PartialArguments.fromText(RUN_CODE_ARGS), callId, name: 'run_code', argsRaw: RUN_CODE_ARGS, turn: 9, step: 0, time: 9_000,
|
||||
const runningCode = (callId: string, name = 'run_code', argsRaw = RUN_CODE_ARGS): StartedToolCall => ({
|
||||
phase: 'start' as const, args: PartialArguments.fromText(argsRaw), callId, name, argsRaw, turn: 9, step: 0, time: 9_000,
|
||||
subCalls: [],
|
||||
})
|
||||
|
||||
@@ -155,7 +155,24 @@ function mountApp(runtime: SlotTestRuntime) {
|
||||
return runtime.renderRoot()
|
||||
}
|
||||
|
||||
describe('run_code sub-calls through the real chat machinery', () => {
|
||||
describe('program sub-calls through the real chat machinery', () => {
|
||||
it.each([false, true])('renders the CUA program and its nested calls while running=%s', async (running) => {
|
||||
const parent = 'cua-program'
|
||||
const program = "var app = await cua.getApp('Calculator')\nconsole.log(app)"
|
||||
const argsRaw = JSON.stringify({ code: program, description: 'Select Calculator' })
|
||||
const child = subCall(11, parent, 1, 'cua', { method: 'getApp', params: { selector: 'Calculator' } }, 'Calculator')
|
||||
const b = await bench(snapshotWith(
|
||||
running ? [] : [codeResult(10, parent, 'cua_run_code', argsRaw)], [child],
|
||||
running ? [runningCode(parent, 'cua_run_code', argsRaw)] : [],
|
||||
))
|
||||
const view = mountApp(b.runtime)
|
||||
expect(view.getByText('Code')).toBeTruthy()
|
||||
expect(view.getByText('Select Calculator')).toBeTruthy()
|
||||
expect(view.container.querySelector('[data-subcalls] [data-tool="cua"]')).not.toBeNull()
|
||||
fireEvent.click(view.getByText('Select Calculator'))
|
||||
expect(view.container.querySelector('pre.shiki')?.textContent).toBe(program)
|
||||
})
|
||||
|
||||
it('renders the code-variant parent row with the description summary and nested sub-rows', async () => {
|
||||
const parent = 'call-64'
|
||||
const subCalls = [
|
||||
|
||||
@@ -62,7 +62,7 @@ async function executeShell(text: string, nested: boolean, name = 'bash', maxInl
|
||||
if (tool === undefined) throw new Error('missing fixture binding')
|
||||
const blocks = await tool(shellArgs)
|
||||
expect(blocks).toEqual([{ type: 'text', text }])
|
||||
return { logs: [], value: true }
|
||||
return { output: [{ type: 'text', text: 'true' }] }
|
||||
}
|
||||
}
|
||||
await ctx.plugin(BindingRuntime)
|
||||
@@ -86,7 +86,7 @@ async function executeShell(text: string, nested: boolean, name = 'bash', maxInl
|
||||
signal: new AbortController().signal,
|
||||
name: nested ? 'run_code' : name,
|
||||
arguments: nested ? {
|
||||
code: `const blocks = await tools.${name}(${JSON.stringify(shellArgs)}); return blocks[0].text === ${JSON.stringify(text)};`,
|
||||
code: `const blocks = await tools.${name}(${JSON.stringify(shellArgs)}); inspect(blocks[0].text === ${JSON.stringify(text)});`,
|
||||
description: 'Dispatch shell fixture through the tool pipeline',
|
||||
} : shellArgs,
|
||||
})
|
||||
@@ -95,7 +95,7 @@ async function executeShell(text: string, nested: boolean, name = 'bash', maxInl
|
||||
|
||||
let block: ToolResultNode
|
||||
if (nested) {
|
||||
expect(result.value).toMatchObject({ result: true })
|
||||
expect(result.value).toMatchObject({ logs: ['true'] })
|
||||
const dispatches = session.snapshotEvents().filter(event => event.type === 'tool/ptc-dispatch')
|
||||
expect(dispatches).toHaveLength(1)
|
||||
const event = dispatches[0]!
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { AttachmentId } from '@deepseek-ai/dsh-attachment'
|
||||
// @vitest-environment jsdom
|
||||
import { contextMenuHook } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
} from '../src/client/tool/models/tool-call-model.ts'
|
||||
import { ToolRow } from '../src/client/tool/components/ToolRow.tsx'
|
||||
import { GenericToolCard, type GenericToolCardProps } from '../src/client/tool/toolviews/GenericToolCard.tsx'
|
||||
import { zh } from '@deepseek-ai/dsh-client-ui-conversation/src/client/locales.ts'
|
||||
import { en, zh } from '@deepseek-ai/dsh-client-ui-conversation/src/client/locales.ts'
|
||||
import { PartialArguments } from '@deepseek-ai/dsh-util-values'
|
||||
|
||||
afterEach(() => {
|
||||
@@ -53,6 +54,9 @@ describe('tool-call-model', () => {
|
||||
expect(classifyTool('grep')).toBe('search')
|
||||
expect(classifyTool('write')).toBe('write')
|
||||
expect(classifyTool('edit')).toBe('edit')
|
||||
expect(classifyTool('run_code')).toBe('code')
|
||||
expect(classifyTool('cua_run_code')).toBe('code')
|
||||
expect(classifyTool('cua_run_code_extra')).toBe('others')
|
||||
expect(classifyTool('cordis_runtime_inspect')).toBe('read')
|
||||
// The v3 run-control verbs: `others` is the decided intent, not an
|
||||
// unclassified default (there is no program to show and no file to open).
|
||||
@@ -235,21 +239,50 @@ describe('tool-call-model', () => {
|
||||
.toBe('{\n "code": ""\n}')
|
||||
})
|
||||
|
||||
it('resultText flattens text blocks verbatim, other shapes as JSON, empty error content to name: code', () => {
|
||||
it('resultText retains text and image JSON and uses the structured failure when content is empty', () => {
|
||||
expect(resultText(result({ content: [{ type: 'text', text: 'a\nb' }] }))).toBe('a\nb')
|
||||
expect(resultText(result({ content: [{ type: 'text', text: 'a' }, { type: 'image', data: 'x' } as never] })))
|
||||
.toBe(`a\n${JSON.stringify({ type: 'image', data: 'x' }, null, 2)}`)
|
||||
const image = { type: 'image' as const, attachment: { attachmentId: AttachmentId('sha256:image'), mediaType: 'image/png' as const, width: 8, height: 4, bytes: 64 } }
|
||||
expect(resultText(result({ content: [image] }))).toBe(JSON.stringify(image, null, 2))
|
||||
const reasoning = { type: 'reasoning' as const, text: 'detail' }
|
||||
expect(resultText(result({ content: [reasoning] }))).toBe(JSON.stringify(reasoning, null, 2))
|
||||
expect(resultText(result({ content: [], isError: true, error: { name: 'ToolError', code: 'denied' } })))
|
||||
.toBe('ToolError: denied')
|
||||
expect(resultText(result({ content: [] }))).toBe('')
|
||||
})
|
||||
|
||||
it('preserves image JSON when the formatter declines', () => {
|
||||
for (const content of ['[{"type":"image"}]', '[{"type":"image","attachment":null}]',
|
||||
'[{"type":"image","attachment":{"width":"8"}}]']) {
|
||||
const blocks = JSON.parse(content) as ToolResultNode['content']
|
||||
expect(resultText(result({ content: blocks }), () => undefined)).toBe(JSON.stringify(blocks[0], null, 2))
|
||||
}
|
||||
})
|
||||
|
||||
it('separates image references without duplicating an emitted newline', () => {
|
||||
const attachment = { attachmentId: AttachmentId('sha256:image'), mediaType: 'image/png' as const, width: 8, height: 4, bytes: 64 }
|
||||
for (const text of ['a', 'a\n']) {
|
||||
expect(resultText(result({ content: [{ type: 'text', text }, { type: 'image', attachment }] }), () => 'sha256:image'))
|
||||
.toBe('a\nsha256:image')
|
||||
}
|
||||
expect(resultText(result({ content: [{ type: 'text', text: 'a\n\n' }, { type: 'image', attachment }] }), () => 'sha256:image'))
|
||||
.toBe('a\n\nsha256:image')
|
||||
})
|
||||
|
||||
it('derives output from the settled result and null while running or blank', () => {
|
||||
expect(toolRowModel('bash', result({ content: [{ type: 'text', text: 'out' }] })).output).toBe('out')
|
||||
expect(toolRowModel('bash', running()).output).toBeNull()
|
||||
expect(toolRowModel('bash', result({ content: [] })).output).toBeNull()
|
||||
})
|
||||
|
||||
it.each([false, true])('shares formatted image output with its error summary, failure=%s', (isError) => {
|
||||
const image = { type: 'image' as const, attachment: { attachmentId: AttachmentId('sha256:image'), mediaType: 'image/png' as const, width: 8, height: 4, bytes: 64 } }
|
||||
const formatImage = vi.fn(() => 'selected image')
|
||||
const model = toolRowModel('run_code', result({ content: [image], isError }), undefined, undefined, formatImage)
|
||||
expect(model.output).toBe('selected image')
|
||||
expect(model.errorSummary).toBe(isError ? 'selected image' : null)
|
||||
expect(formatImage).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('derives errorSummary as the first output line on error rows only', () => {
|
||||
const failed = result({ content: [{ type: 'text', text: 'boom\ndetail' }], isError: true })
|
||||
expect(toolRowModel('bash', failed).errorSummary).toBe('boom')
|
||||
@@ -588,6 +621,33 @@ describe('GenericToolCard', () => {
|
||||
useDisclosure, callId: 'c1', toolName, ...('kind' in block ? { phase: 'result' as const, block: block } : { phase: block.phase, block: block }), openFile: vi.fn(), t,
|
||||
})
|
||||
|
||||
it.each([
|
||||
[t, 'sha256:image (8x4px,64 字节)'],
|
||||
[makeTranslate(en), 'sha256:image (8x4px, 64 bytes)'],
|
||||
] as const)('localizes compact image references in the generic card', (translate, expected) => {
|
||||
const attachment = { attachmentId: AttachmentId('sha256:image'), mediaType: 'image/png' as const, width: 8, height: 4, bytes: 64 }
|
||||
const block = result({ content: [{ type: 'text', text: 'before\n' }, { type: 'image', attachment }] })
|
||||
const view = render(<GenericToolCard {...props('run_code', block)} t={translate} />)
|
||||
fireEvent.click(view.getByRole('button'))
|
||||
expect(view.container.textContent).toContain(expected)
|
||||
expect(view.container.querySelector('[class*="output"]')?.textContent ?? view.container.textContent).not.toContain('before\n\nsha256:')
|
||||
})
|
||||
|
||||
it.each([
|
||||
'{"type":"image"}',
|
||||
'{"type":"image","attachment":null}',
|
||||
'{"type":"image","attachment":[]}',
|
||||
'{"type":"image","attachment":{"attachmentId":"image","width":"8","height":4,"bytes":64}}',
|
||||
'{"type":"image","attachment":{"attachmentId":"image","width":8,"height":4,"bytes":0}}',
|
||||
'{"type":"image","attachment":{"attachmentId":"image","name":12,"width":8,"height":4,"bytes":64}}',
|
||||
'{"type":"image","attachment":{"width":8,"height":4,"bytes":64}}',
|
||||
])('renders malformed image references as JSON: %s', (json) => {
|
||||
const content = JSON.parse(`[${json}]`) as ToolResultNode['content']
|
||||
const view = render(<GenericToolCard {...props('run_code', result({ content }))} />)
|
||||
fireEvent.click(view.getByRole('button'))
|
||||
expect(view.container.textContent).toContain(JSON.stringify(content[0], null, 2))
|
||||
})
|
||||
|
||||
it('renders the classified variant row from the frozen slice', () => {
|
||||
const view = render(<GenericToolCard {...props('bash', result())}
|
||||
getFileTriggerProps={() => ({})} />)
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 4bd17d05b8385675
|
||||
zh: 5174e27f754f2a03
|
||||
/deepseek-ai-dsh-client-ui-trajectory/use-this-package/inspecting-records:
|
||||
en: 4bf16077921c643c
|
||||
zh: 08ded05f8f0afded
|
||||
en: dc86f133fbc74633
|
||||
zh: 289fa88898ac3e55
|
||||
/deepseek-ai-dsh-client-ui-trajectory/use-this-package/the-timing-overview:
|
||||
en: 0f4ee2189324ac06
|
||||
zh: 12e76fed22c09bc1
|
||||
|
||||
@@ -31,7 +31,7 @@ Open the Trajectory tab in the conversation's view ring to inspect agent activit
|
||||
|
||||
The toolbar folds turns, calls, and subcalls independently. Subcalls fold into a summary beneath their parent tool; click the summary or press Enter or Space to expand it. Search includes folded subcalls, and cross-view inspection expands the requested call. Toolbar controls and search wrap onto additional lines when the pane is narrow.
|
||||
|
||||
Calls are identified by their recorded tool name. For `run_code`, the row shows the program description and the inspector opens numbered, highlighted source. The Code tab provides wrapping, a `{}` toggle for the original JSON arguments, and exact source copying, including trailing newlines. Copying the original arguments retains their recorded JSON whitespace. Each newly opened code view takes the last wrapping choice; changing it leaves other open views as they are. Output preserves the recorded text, using a tree for complete JSON objects or arrays. Highlighting reads the leading `Execute a program in` language declaration and the legacy `code` parameter description from the recorded tool schema. Only TypeScript and Python are supported. Missing or conflicting hints, or an explicit declaration of another language, leave plain source; later examples in the tool description do not affect language selection.
|
||||
Calls are identified by their recorded tool name. For `run_code` and `cua_run_code`, the row shows the program description and the inspector opens numbered, highlighted source. The Code tab provides wrapping, a `{}` toggle for the original JSON arguments, and exact source copying, including trailing newlines. Copying the original arguments retains their recorded JSON whitespace. Each newly opened code view takes the last wrapping choice; changing it leaves other open views as they are. Output preserves the recorded text, using a tree for complete JSON objects or arrays. Highlighting reads the leading `Execute a program in` language declaration and the legacy `code` parameter description from the recorded tool schema. Only TypeScript and Python are supported. Missing or conflicting hints, or an explicit declaration of another language, leave plain source; later examples in the tool description do not affect language selection.
|
||||
|
||||
Selection, timeline navigation, folding, and search cover the React-visible window. Request numbers and cumulative usage cover the complete resident snapshot. Selecting a record opens a local inspector for token usage, duration, Input, Output, Timing, and durable images. Image URLs use the Conversation-owned per-session cache, so Chat and Trajectory share one authorized read per attachment. The Trajectory view declares `conversation.trajectory.images`; ledger children call this Slot through the framework `renderSlot` seat, passing image references, display options, and the authorized loader explicitly. Its official `MessageImages` entry calls the shared `conversation.images.content` Factory. A user record shows both nonzero image and ordinary-file counts beside its text, including attachment-only records. A standalone compaction request appears chronologically in its own `Between turns` section, while a numbered compaction remains inside its owning turn.
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间
|
||||
|
||||
工具栏可以分别折叠轮次、调用和子调用。子调用收起后,在父工具下保留一条摘要;点击摘要或按 Enter、空格键即可展开。搜索包含已折叠的子调用,从其他视图检查调用时会自动展开目标调用。 面板较窄时,工具栏按钮与搜索框会自动换行。
|
||||
|
||||
调用按已记录的工具名识别。对于 `run_code`,记录行展示程序说明,检查器显示带行号和语法高亮的源码。“代码”页提供换行、查看原始 JSON 参数的 `{}` 切换按钮,以及保留末尾换行的源码原样复制。复制原始参数时保留记录中 JSON 的空白。每个新打开的代码视图采用上次选择的换行状态;切换时,其他已打开视图保持原状。输出保留记录中的文本,完整 JSON 对象或数组使用树形展示。高亮从已记录的工具 Schema 中读取开头的 `Execute a program in` 语言声明及旧式 `code` 参数说明,仅支持 TypeScript 和 Python。提示缺失或冲突,或明确声明了其他语言时,显示纯文本源码;工具说明后文的示例不影响语言选择。
|
||||
调用按已记录的工具名识别。对于 `run_code` 和 `cua_run_code`,记录行展示程序说明,检查器显示带行号和语法高亮的源码。“代码”页提供换行、查看原始 JSON 参数的 `{}` 切换按钮,以及保留末尾换行的源码原样复制。复制原始参数时保留记录中 JSON 的空白。每个新打开的代码视图采用上次选择的换行状态;切换时,其他已打开视图保持原状。输出保留记录中的文本,完整 JSON 对象或数组使用树形展示。高亮从已记录的工具 Schema 中读取开头的 `Execute a program in` 语言声明及旧式 `code` 参数说明,仅支持 TypeScript 和 Python。提示缺失或冲突,或明确声明了其他语言时,显示纯文本源码;工具说明后文的示例不影响语言选择。
|
||||
|
||||
选择、时间线导航、折叠与搜索只覆盖 React 可见窗口。请求编号与累计用量覆盖完整的驻留快照。选择记录会打开局部检查器,查看 token 用量、耗时、输入、输出、计时与持久保留的图片。图片 URL 使用 Conversation 拥有的逐会话缓存,因此 Chat 与 Trajectory 对每个附件共享一次已授权读取。Trajectory 视图声明 `conversation.trajectory.images`;账本子组件通过框架 `renderSlot` 席位调用该 Slot,显式传入图片引用、显示参数及授权 loader。官方 `MessageImages` 条目调用共享的 `conversation.images.content` Factory。用户记录会在文本旁同时显示非零的图片与普通文件数量,仅含附件的记录也显示这些数量。独立运行的压缩请求会按时间顺序显示在自己的 `Between turns` 区段中,而带编号的压缩仍位于其所属轮次内。
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import type { TrajectoryTurnModel } from './layout.ts'
|
||||
import { trajectoryPreviewText } from './trajectory-preview.ts'
|
||||
import type { TrajectoryKey, TrajectoryTranslate } from './locales.ts'
|
||||
import { COMPACTION_INTERRUPTED_ERROR } from './copy-codes.ts'
|
||||
import { codeProgram, PTC_TOOL_NAME, type CodeProgram } from './code-program.ts'
|
||||
import { codeProgram, isProgramTool, type CodeProgram } from './code-program.ts'
|
||||
import css from './TrajectoryTable.module.css'
|
||||
|
||||
const BOTTOM_FOLLOW_THRESHOLD_PX = 2
|
||||
@@ -1030,7 +1030,7 @@ function detailTabs(record: TableRecord): readonly DetailTabItem[] {
|
||||
function recordDisplayText(cell: TrajectoryCellProps, t: TrajectoryTranslate): string {
|
||||
if (isToolCallOnly(cell, t)) return ''
|
||||
const program = codeProgram(cell)
|
||||
if (program !== undefined) return `${PTC_TOOL_NAME} · ${program.description.replace(/\s+/g, ' ')}`
|
||||
if (program !== undefined) return `${cell.toolName} · ${program.description.replace(/\s+/g, ' ')}`
|
||||
if (cell.previewMarkdown !== undefined) {
|
||||
const preview = trajectoryPreviewText(cell.previewMarkdown)
|
||||
if (cell.text === '') return preview
|
||||
@@ -1056,7 +1056,7 @@ function toolCallTextParts(
|
||||
text: string,
|
||||
): ToolCallTextParts | undefined {
|
||||
if (cell.kind !== 'tool' && cell.kind !== 'subtool') return undefined
|
||||
const program = cell.toolName === PTC_TOOL_NAME
|
||||
const program = isProgramTool(cell.toolName)
|
||||
const separator = text.indexOf(' · ')
|
||||
if (separator === -1) return { name: text, program }
|
||||
return {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
/** Read replayable PTC source and language hints from recorded tool arguments and schemas. */
|
||||
/** Read program source and language hints from recorded tool arguments and schemas. */
|
||||
import type { TrajectoryCellProps } from './trajectory-record.ts'
|
||||
|
||||
/** Recorded name of the programmatic tool-calling entry point. */
|
||||
export const PTC_TOOL_NAME = 'run_code'
|
||||
/**
|
||||
* Identify tools supported by the trajectory code inspector.
|
||||
* @param name - Recorded tool name, absent when the call is outside the loaded window.
|
||||
* @returns Whether this tool executes a program shown by the code inspector.
|
||||
*/
|
||||
export function isProgramTool(name: string | undefined): boolean {
|
||||
return name === 'run_code' || name === 'cua_run_code'
|
||||
}
|
||||
|
||||
/** Validated source and original arguments for one recorded run_code call. */
|
||||
/** Validated source and original arguments for one recorded program call. */
|
||||
export interface CodeProgram {
|
||||
rawInput: string
|
||||
source: string
|
||||
@@ -46,14 +52,14 @@ function recordedLanguage(schemaRaw: string | undefined): CodeProgram['language'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a PTC program without guessing its language from source or current runtime settings.
|
||||
* Resolve a program without guessing its language from source or current runtime settings.
|
||||
* Unknown explicit languages or conflicting recorded hints leave the source unhighlighted.
|
||||
* @param cell - Recorded tool arguments and the schema visible at call time.
|
||||
* @returns The program, or undefined for another tool or unsupported arguments.
|
||||
*/
|
||||
export function codeProgram(cell: TrajectoryCellProps): CodeProgram | undefined {
|
||||
if (cell.kind !== 'tool' && cell.kind !== 'subtool') return undefined
|
||||
if (cell.toolName !== PTC_TOOL_NAME || cell.inputDetail === undefined) return undefined
|
||||
if (!isProgramTool(cell.toolName) || cell.inputDetail === undefined) return undefined
|
||||
const args = parseRecord(cell.inputDetail)
|
||||
if (typeof args?.code !== 'string') return undefined
|
||||
if (args.description !== undefined && typeof args.description !== 'string') return undefined
|
||||
|
||||
@@ -143,10 +143,10 @@ describe('PTC trajectory inspector', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('shows the description, numbered source, original arguments, and verbatim output', () => {
|
||||
const view = render(<Harness />)
|
||||
it.each(['run_code', 'cua_run_code'])('shows the %s description, numbered source, original arguments, and verbatim output', (toolName) => {
|
||||
const view = render(<Harness cell={{ ...CELL, toolName, text: `${toolName} · ${ARGS}` }} />)
|
||||
const ui = within(view.container)
|
||||
fireEvent.click(ui.getByRole('row', { name: /run_code Read the working directory/ }))
|
||||
fireEvent.click(ui.getByRole('row', { name: new RegExp(`${toolName} Read the working directory`) }))
|
||||
expect(ui.getAllByRole('tab').map(tab => tab.textContent)).toEqual(['Summary', 'Code', 'Result', 'Schema', 'Timing'])
|
||||
expect(ui.getByRole('tabpanel').textContent).toContain('Read the working directory')
|
||||
expect(view.container.querySelector('[data-line-numbers] pre')?.textContent).toBe(SOURCE.trimEnd())
|
||||
|
||||
@@ -9,8 +9,8 @@ const CELL: TrajectoryCellProps = {
|
||||
}
|
||||
|
||||
describe('recorded code programs', () => {
|
||||
it('keeps the program and arguments verbatim', () => {
|
||||
expect(codeProgram(CELL)).toEqual({
|
||||
it.each(['run_code', 'cua_run_code'])('keeps the %s program and arguments verbatim', (toolName) => {
|
||||
expect(codeProgram({ ...CELL, toolName })).toEqual({
|
||||
rawInput: CELL.inputDetail,
|
||||
source: 'return 42\n', description: 'Compute the answer',
|
||||
arguments: { code: 'return 42\n', description: 'Compute the answer' }, language: undefined,
|
||||
@@ -122,6 +122,7 @@ describe('recorded code programs', () => {
|
||||
it('does not specialize other tools or non-tool records', () => {
|
||||
expect(codeProgram({ ...CELL, kind: 'message' })).toBeUndefined()
|
||||
expect(codeProgram({ ...CELL, toolName: 'run_code_extra' })).toBeUndefined()
|
||||
expect(codeProgram({ ...CELL, toolName: 'cua_run_code_extra' })).toBeUndefined()
|
||||
expect(codeProgram({ ...CELL, kind: 'subtool' })?.source).toBe('return 42\n')
|
||||
})
|
||||
})
|
||||
@@ -21,15 +21,7 @@ import {
|
||||
type FileSearchConfig,
|
||||
} from './search.ts'
|
||||
|
||||
export {
|
||||
DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES,
|
||||
DEFAULT_FILE_SEARCH_MAX_ENTRIES,
|
||||
DEFAULT_FILE_SEARCH_MAX_RESULTS,
|
||||
WorkspaceFileSearch,
|
||||
} from './search.ts'
|
||||
export type { FileSearchConfig } from './search.ts'
|
||||
export { FILE_REFERENCE_PROMPT } from '@deepseek-ai/dsh-file-reference'
|
||||
export { activeAtToken, formatFileMention } from '@deepseek-ai/dsh-file-reference/grammar'
|
||||
|
||||
/** Local file-reference discovery configuration. */
|
||||
export interface Config {
|
||||
|
||||
Loaded 100 of 528 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user