dsh-codex-style-computer-use 1.3.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -10
- package/README.zh.md +26 -9
- package/lib/audit.js +7 -0
- package/lib/batch.js +14 -19
- package/lib/bin/attention-worker.exe +0 -0
- package/lib/bin/attention-worker.exe.json +1 -1
- package/lib/compact-feedback.js +175 -0
- package/lib/continuation.js +1 -1
- package/lib/core/bin/dsh-computer-use-worker.exe +0 -0
- package/lib/core/bin/dsh-computer-use-worker.exe.json +2 -2
- package/lib/core/index.js +372 -136
- package/lib/core/native-runtime.js +17 -0
- package/lib/core/worker.cs +1779 -218
- package/lib/feedback-routes.json +11 -0
- package/lib/feedback.js +49 -16
- package/lib/image-output.js +59 -11
- package/lib/index.js +25 -7
- package/lib/key-command.js +37 -0
- package/lib/media-contract.js +43 -0
- package/lib/observations.js +267 -13
- package/lib/policy.js +7 -7
- package/lib/tool-contract.js +56 -0
- package/lib/tools.js +134 -49
- package/lib/verification-region.js +17 -0
- package/mcp/package.json +2 -1
- package/mcp/src/execution.js +8 -5
- package/mcp/src/index.js +16 -267
- package/mcp/src/tools.js +271 -0
- package/package.json +2 -1
- package/scripts/check-behavior.mjs +83 -0
- package/scripts/measure-tool-tokens.mjs +21 -0
- package/skills/computer-use/SKILL.md +20 -8
- package/skills/computer-use/references/lifecycle.md +11 -7
- package/skills/computer-use/references/maintenance.md +9 -5
- package/skills/computer-use/references/precision.md +39 -14
- package/skills/computer-use/references/progressive-control.md +10 -4
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md) · [Install](#install-and-update-in-dsh-market)
|
|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
- **See the target. Take the action.** Combine screenshots, UI Automation and numbered targets with mouse, keyboard, drag, scroll and window tools.
|
|
12
|
-
- **Take over. Let the agent wait. Hand back and continue.** Version
|
|
12
|
+
- **Take over. Let the agent wait. Hand back and continue.** Version 2.0.0 shows a 1-second notice before input, or 0.1 second when the previous notice was less than 3 minutes ago. After handback, the agent re-observes and resumes the original task.
|
|
13
13
|
- **Fewer round trips. More efficient token use.** Incremental control observations, cropped or downsampled screenshots and batches of known actions reduce repeated context and model calls.
|
|
14
14
|
|
|
15
15
|

|
|
@@ -18,12 +18,12 @@ English | [中文](README.zh.md) · [Install](#install-and-update-in-dsh-market)
|
|
|
18
18
|
|
|
19
19
|
These are **1.3.0 feature illustrations**. Actual token use depends on the task, model and observation strategy. Approvals remain enabled by default; explicit pause, cancellation and exit remain authoritative.
|
|
20
20
|
|
|
21
|
-
**Ready to install.**
|
|
21
|
+
**Ready to install.** 19 desktop tools, with native helpers included. No SDK or install-time compilation. Requires Windows, Node.js ≥ 22 and the .NET Framework 4.x included with Windows. An independent community plugin; Codex describes the interaction style.
|
|
22
22
|
|
|
23
23
|
<details>
|
|
24
24
|
<summary>Current release and compatibility</summary>
|
|
25
25
|
|
|
26
|
-
**
|
|
26
|
+
**2.0.0** is the new major release, initially distributed on npm `next` while the 24-hour release gate runs. See [2.0.0 changes and migration](docs/RELEASE-2.0.0.md). Check registry tags for current stable availability.
|
|
27
27
|
|
|
28
28
|
Version 1.3.1 reads text from the screen by name, notices the Windows secure desktop and resumes on its own, and keeps every colour on the same DPI-derived edge rail with a visible breathing floor. Version 1.3.0 releases desktop control when work ends while the chat continues.
|
|
29
29
|
|
|
@@ -33,6 +33,12 @@ Earlier fixes: [1.2.3 approval attribution](docs/RELEASE-1.2.3.md) · [1.2.1 nat
|
|
|
33
33
|
|
|
34
34
|
## What it does
|
|
35
35
|
|
|
36
|
+
Version 2.0.0 also uses a compact blue kite pointer during AI operation and scene hold. Its tilted tip marks the click point, with small text/resize symbols inside the kite. A breathing kite means the page and pointer position should stay in place; free mode restores the application's normal cursor. The user's Windows cursor theme is preserved.
|
|
37
|
+
|
|
38
|
+
Version 2.0.0 bounds UIA traversal and defaults to compact post-action text. Existing image feedback remains available; `feedback: compact|full|none` controls detail, and click `verificationRegion: "x,y,w,h"` names the expected effect area independently of the input point. Compact text normally adds at most 6 KiB to the original action receipt. Complete point evidence can be retained in a bounded, expiring local cache and retrieved on demand. Disk failure falls back to bounded memory; if no retrievable snapshot can be retained, the current evidence is returned inline in full, outside that compact budget, without another observation or input.
|
|
39
|
+
|
|
40
|
+
Filtered UIA queries require `hwnd`. Defaults are depth 6, 120 nodes and a 1200 ms soft budget. A query deeper than 6 or larger than 300 nodes requires a measured `anchor:"x,y"` in that window; an exact `id` does not bypass this rule. A provider call can exceed the soft budget, and partial results cannot establish absence or uniqueness. After a window-management shortcut, inspect `computer_state {windows:true}`, activate the intended window successfully, then observe it before more keyboard input.
|
|
41
|
+
|
|
36
42
|
- **Observe** — window list, screenshots (region / JPEG / PNG / downscaled), a Set-of-Marks map of
|
|
37
43
|
clickable elements, and the UI Automation tree with exact `AutomationId`s.
|
|
38
44
|
- **Act** — click, move, drag, scroll, select a text range, type, press keys, and drive controls
|
|
@@ -44,9 +50,10 @@ Earlier fixes: [1.2.3 approval attribution](docs/RELEASE-1.2.3.md) · [1.2.1 nat
|
|
|
44
50
|
- **A human brake that reaches the model** — a stop is pushed into the running session, so the agent
|
|
45
51
|
learns it was interrupted instead of discovering it on the next turn.
|
|
46
52
|
|
|
47
|
-
The tools
|
|
48
|
-
|
|
49
|
-
under `$DSH_HOME/data/computer-use/`.
|
|
53
|
+
The tools observe the desktop rather than crawl user documents. The plugin manages native helpers in
|
|
54
|
+
`%LOCALAPPDATA%\dsh-computer-use\worker\`, and audit logs, screenshots and its own bounded
|
|
55
|
+
`observation-cache` under `$DSH_HOME/data/computer-use/`. `computer_ctrl {action:"clear_cache"}` clears
|
|
56
|
+
this Agent's observation cache; it does not delete screenshots, attachments or user documents.
|
|
50
57
|
|
|
51
58
|
## Install and update in dsh-market
|
|
52
59
|
|
|
@@ -112,9 +119,9 @@ The configured GLM 5.3 Max, GLM 5.3 Flash and DeepSeek Flash routes passed a con
|
|
|
112
119
|
|
|
113
120
|
## On-demand controls in 1.1
|
|
114
121
|
|
|
115
|
-
- **Per-Agent tools:** idle Agents see `computer_use_activate`, `computer_ctrl` and `computer_ask`. A successful computer-use skill load, receipt or activation exposes the
|
|
122
|
+
- **Per-Agent tools:** idle Agents see `computer_use_activate`, `computer_ctrl` and `computer_ask`. A successful computer-use skill load, receipt or activation exposes the 19 desktop tools plus the activation entry to that Agent. Other Agents and children activate independently. Native calls and Node `run_code` are supported. Activation does not start control or release a human brake.
|
|
116
123
|
- **Observed UIA targets:** a scoped name/id query returns an opaque target handle. Use it for pattern actions; opt-in `rebind:true` allows unique identity or semantic recovery after replacement in the same window. Changed context, ambiguity and incomplete scans refuse. Broad queries skip recovery handles unless `targets:true`; creating witnesses for a large list costs extra native work.
|
|
117
|
-
- **Incremental observations:** pass the previous observation ID as `since` with the same query. Deltas preserve additions, changes, removals and ordering.
|
|
124
|
+
- **Incremental observations:** pass the previous observation ID as `since` with the same query. Deltas preserve additions, changes, removals and ordering. Snapshots are isolated by Agent, with at most 64 indexed observations and 4 MiB of retained JSON per store. `snapshot` retrieves a retained historical result; `offset` and `limit` page its arrays. Missing bases, changed scope, incomplete evidence or a larger delta fall back to full output.
|
|
118
125
|
|
|
119
126
|
See [the detailed protocol](skills/computer-use/references/progressive-control.md) for cache limits and recovery boundaries. UIA snapshots preserve provider-returned fields within existing provider limits; historical retrieval is not fresh outcome verification.
|
|
120
127
|
|
|
@@ -123,7 +130,7 @@ See [the detailed protocol](skills/computer-use/references/progressive-control.m
|
|
|
123
130
|
| Group | Tools |
|
|
124
131
|
|---|---|
|
|
125
132
|
| Activate | `computer_use_activate` |
|
|
126
|
-
| Observe | `computer_state` `computer_shot` `computer_marks` `computer_uia` |
|
|
133
|
+
| Observe | `computer_state` `computer_shot` `computer_ocr` `computer_marks` `computer_uia` |
|
|
127
134
|
| Act | `computer_click` `computer_move` `computer_drag` `computer_scroll` `computer_select` `computer_key` `computer_type` `computer_uia_act` `computer_window` `computer_clip` |
|
|
128
135
|
| Flow | `computer_wait` `computer_batch` |
|
|
129
136
|
| Meta | `computer_ask` `computer_ctrl` |
|
|
@@ -166,7 +173,9 @@ fields are detected through UI Automation's `IsPassword` and refused by default.
|
|
|
166
173
|
| Key | Default | Meaning |
|
|
167
174
|
|---|---:|---|
|
|
168
175
|
| `automationMode` | `standard` | approval mode (above) |
|
|
169
|
-
| `progressiveTools` | `true` | per-Agent activation; `false`
|
|
176
|
+
| `progressiveTools` | `true` | per-Agent activation; `false` exposes the 19 desktop tools globally |
|
|
177
|
+
| `actionFeedback` | `true` | automatically observe after direct actions and batches |
|
|
178
|
+
| `actionFeedbackDetail` | `compact` | `compact`, `full` or `none`; a call's `feedback` overrides this default; uncached evidence falls back to full inline output |
|
|
170
179
|
| `dryRun` | `false` | simulate actuations, log only |
|
|
171
180
|
| `maxActionsPerMinute` | `60` | core-level actuation rate limit |
|
|
172
181
|
| `annotateMarks` | `true` | draw Set-of-Marks boxes on screenshots |
|
|
@@ -182,6 +191,19 @@ than Cordis:
|
|
|
182
191
|
node mcp/src/index.js
|
|
183
192
|
```
|
|
184
193
|
|
|
194
|
+
The embedded server reports the version of this root package. It starts with observation only.
|
|
195
|
+
The launching host may set `DSH_CU_ALLOW_INPUT=1` when it owns authorization for desktop actions;
|
|
196
|
+
MCP tool calls cannot enable input. The compatibility tool `cu_approve` only reports this rule and
|
|
197
|
+
never grants consent. DSH's `automationMode` policy belongs to the Cordis adapter and is not an
|
|
198
|
+
MCP approval service. `DSH_CU_DRY_RUN=1` simulates input that the host has already enabled.
|
|
199
|
+
|
|
200
|
+
Both adapters use the same key-command grammar, batch failure and continuation rules, and image
|
|
201
|
+
coordinate mapping. MCP returns structured capture metadata and one latest PNG/JPEG image, without
|
|
202
|
+
serializing image bytes into JSON text. `cu_hold` / `cu_free` reserve or release the scene between
|
|
203
|
+
actions; `cu_release` ends the desktop cycle. `cu_drag` accepts `expectCursor` and `hwnd` checks.
|
|
204
|
+
MCP exposes a smaller tool surface; DSH-specific human-question and host-lifecycle integration are
|
|
205
|
+
provided by the Cordis adapter.
|
|
206
|
+
|
|
185
207
|
## Development
|
|
186
208
|
|
|
187
209
|
Publisher release checks use npm's official registry by default. If that origin is unreachable on your network, `CU_REGISTRY` selects an explicit mirror for the read-only `check` command; the JSON result includes `registry` and `authoritative`. Example in PowerShell (this changes only the current shell):
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
- **看得准,也点得动。** 截图、控件树与编号定位配合使用,支持点击、输入、拖拽、滚动和窗口操作。
|
|
12
|
-
- **你接手,AI 等待,再接着做。**
|
|
12
|
+
- **你接手,AI 等待,再接着做。** 2.0.0首次操作预告倒计时 1 秒;距上次提示不足 3 分钟时为 0.1 秒。交回后重新观察并接续原任务。
|
|
13
13
|
- **更少往返,更省 Token。** 控件增量观察、区域与降采样截图,加上连续动作批量执行,减少重复观察和模型往返。
|
|
14
14
|
|
|
15
15
|

|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
|
|
19
19
|
上图为 **1.3.0 功能示意**。实际 Token 用量取决于任务、模型和观察方式。默认保留操作审批;明确暂停、取消和退出始终有效。
|
|
20
20
|
|
|
21
|
-
**安装即用。**
|
|
21
|
+
**安装即用。** 19 个桌面工具,原生组件随包提供,无需 SDK 或安装时编译。支持 Windows、Node.js ≥ 22 和系统自带的 .NET Framework 4.x。独立社区插件;Codex 用于说明交互风格。
|
|
22
22
|
|
|
23
23
|
<details>
|
|
24
24
|
<summary>当前版本与兼容性</summary>
|
|
25
25
|
|
|
26
|
-
**
|
|
26
|
+
**2.0.0** 为本次大版本,先发布至 npm `next`,通过仓库规定的 24 小时成熟期后再升稳定频道。详见 [2.0.0 变更与迁移](docs/RELEASE-2.0.0.md),稳定版以 registry 标签为准。
|
|
27
27
|
|
|
28
28
|
1.3.1 可直接从屏幕读字定位控件,察觉 Windows 安全桌面并在其结束后自动续接;所有颜色共用同一条按 DPI 换算的边缘光环,呼吸谷值保持可见。1.3.0 起工具用完可主动释放,聊天继续;正常收尾自动退出。
|
|
29
29
|
|
|
@@ -33,6 +33,12 @@
|
|
|
33
33
|
|
|
34
34
|
## 它能做什么
|
|
35
35
|
|
|
36
|
+
2.0.0在 AI 操作和保留现场时使用小巧的蓝色风筝光标:倾斜的尖端就是点击位置,文本、缩放等提示收在风筝中央。呼吸状态表示仍需保持页面与鼠标位置;放开现场后恢复应用原来的鼠标。Windows 鼠标主题保持原样。
|
|
37
|
+
|
|
38
|
+
2.0.0进一步收紧 UIA 查询范围,并默认精简动作后的文字反馈;视觉模型仍保留原有图片检查。调用可用 `feedback: compact|full|none`,点击可用 `verificationRegion: "x,y,w,h"` 指定真正的结果区域。正常精简反馈在原动作回执之外最多增加 6 KiB 文字。完整点查证据可留在有容量与期限限制的本地缓存,按需读快照。磁盘失败先回退有界内存;如果无法保留可回读的快照,则直接完整返回已有证据,此时不受精简文字预算限制,不重复查询或操作。
|
|
39
|
+
|
|
40
|
+
带过滤条件的 UIA 查询必须指定 `hwnd`,默认深度 6、最多 120 节点和 1200 毫秒软预算。深度大于 6 或节点上限大于 300 时,须提供该窗口内已测量的 `anchor:"x,y"`;精确 `id` 也不豁免。单次提供器调用可能超过软预算,部分结果不能证明目标不存在或唯一。窗口管理快捷键后,先用 `computer_state {windows:true}` 查看窗口,再成功激活目标并重新观察,随后才能继续键盘输入。
|
|
41
|
+
|
|
36
42
|
- **观察** —— 窗口列表、截图(区域 / JPEG / PNG / 降采样)、可点元素的 Set-of-Marks 编号图,
|
|
37
43
|
以及带精确 `AutomationId` 的 UI Automation 树。
|
|
38
44
|
- **操作** —— 点击、移动、拖拽、滚动、选中文本区间、键入、按键,并可走控件**自身的 UI
|
|
@@ -42,8 +48,7 @@
|
|
|
42
48
|
- **能传到模型的人为刹车** —— 停止事件被推入正在运行的会话,agent 当场知道自己被打断,
|
|
43
49
|
而不是等到下一轮才发现。
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
(`%LOCALAPPDATA%\dsh-computer-use\worker\`)、审计日志与截图(`$DSH_HOME/data/computer-use/`)。
|
|
51
|
+
工具观察桌面,不遍历用户文档。插件管理原生组件(`%LOCALAPPDATA%\dsh-computer-use\worker\`),以及 `$DSH_HOME/data/computer-use/` 下的审计日志、截图和自有 `observation-cache` 有界缓存。`computer_ctrl {action:"clear_cache"}` 只清理本 Agent 的观察缓存,不删除截图、附件或用户文档。
|
|
47
52
|
|
|
48
53
|
## 在 dsh-market 安装和更新
|
|
49
54
|
|
|
@@ -108,9 +113,9 @@ $CuDshInstall = Join-Path $env:LOCALAPPDATA 'Programs\DeepSeek Harness'
|
|
|
108
113
|
|
|
109
114
|
## 1.1 的按需控制
|
|
110
115
|
|
|
111
|
-
- **按 Agent 加载:**未激活时暴露 `computer_use_activate`、`computer_ctrl`、`computer_ask`。成功读取 computer-use skill、确认规程或调用激活入口后,该 Agent
|
|
116
|
+
- **按 Agent 加载:**未激活时暴露 `computer_use_activate`、`computer_ctrl`、`computer_ask`。成功读取 computer-use skill、确认规程或调用激活入口后,该 Agent 获得 19 个桌面工具,加入口共 20 个。其他 Agent 和子 Agent 独立启用,支持原生调用和 Node `run_code`。激活本身不启动控制,也不释放人为刹车。
|
|
112
117
|
- **已观察的 UIA 目标:**指定窗口和 name/id 的聚焦查询默认生成 target 句柄。控件重建时,显式 `rebind:true` 才允许在同一窗口内进行唯一身份或语义匹配。上下文变化、歧义、扫描不完整时拒绝动作。宽查询需 `targets:true` 才生成恢复句柄,避免普通观察承担额外开销。
|
|
113
|
-
- **增量观察:**重复相同查询并传入上一 observation 的 `since
|
|
118
|
+
- **增量观察:**重复相同查询并传入上一 observation 的 `since`,返回新增、变化、删除及顺序信息。快照按 Agent 隔离,每个 store 总计最多索引 64 份观察、在内存保留 4 MiB JSON。`snapshot` 可取回仍保留的历史结果,`offset` 和 `limit` 可分页读取数组。缺少基线、范围变化、证据不完整或增量更大时自动回退全量。
|
|
114
119
|
|
|
115
120
|
保留的是提供器实际返回的完整字段,沿用原有提供器限制;历史快照不代替动作后的新验证。缓存上限和恢复边界见[详细协议](skills/computer-use/references/progressive-control.md)。
|
|
116
121
|
|
|
@@ -119,7 +124,7 @@ $CuDshInstall = Join-Path $env:LOCALAPPDATA 'Programs\DeepSeek Harness'
|
|
|
119
124
|
| 分组 | 工具 |
|
|
120
125
|
|---|---|
|
|
121
126
|
| 激活 | `computer_use_activate` |
|
|
122
|
-
| 观察 | `computer_state` `computer_shot` `computer_marks` `computer_uia` |
|
|
127
|
+
| 观察 | `computer_state` `computer_shot` `computer_ocr` `computer_marks` `computer_uia` |
|
|
123
128
|
| 操作 | `computer_click` `computer_move` `computer_drag` `computer_scroll` `computer_select` `computer_key` `computer_type` `computer_uia_act` `computer_window` `computer_clip` |
|
|
124
129
|
| 流程 | `computer_wait` `computer_batch` |
|
|
125
130
|
| 元 | `computer_ask` `computer_ctrl` |
|
|
@@ -157,7 +162,9 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
|
|
|
157
162
|
| 键 | 默认 | 含义 |
|
|
158
163
|
|---|---:|---|
|
|
159
164
|
| `automationMode` | `standard` | 审批模式(见上) |
|
|
160
|
-
| `progressiveTools` | `true` | 按 Agent 激活;设为 `false`
|
|
165
|
+
| `progressiveTools` | `true` | 按 Agent 激活;设为 `false` 全局暴露 19 个桌面工具 |
|
|
166
|
+
| `actionFeedback` | `true` | 直接动作及批处理后自动观察 |
|
|
167
|
+
| `actionFeedbackDetail` | `compact` | `compact`、`full` 或 `none`;单次调用的 `feedback` 可覆盖默认值;无法缓存时完整内联返回证据 |
|
|
161
168
|
| `dryRun` | `false` | 只模拟操作并记录 |
|
|
162
169
|
| `maxActionsPerMinute` | `60` | 核心层操作限速 |
|
|
163
170
|
| `annotateMarks` | `true` | 在截图上绘制 Set-of-Marks 方框 |
|
|
@@ -172,6 +179,16 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
|
|
|
172
179
|
node mcp/src/index.js
|
|
173
180
|
```
|
|
174
181
|
|
|
182
|
+
该入口随根包发布,运行时报告根包版本;默认只能观察桌面。启动它的宿主在负责桌面操作授权时,
|
|
183
|
+
可以设置 `DSH_CU_ALLOW_INPUT=1`。模型调用工具不能开启输入:兼容工具 `cu_approve` 只说明授权来源,
|
|
184
|
+
不会授予同意。DSH 的 `automationMode` 属于 Cordis 入口,不是 MCP 的审批服务。
|
|
185
|
+
`DSH_CU_DRY_RUN=1` 只模拟已由宿主启用的输入。
|
|
186
|
+
|
|
187
|
+
两个入口共享按键规范化、批次失败与接管后续行语义,以及图像坐标换算。MCP 保留截图时间、光标、
|
|
188
|
+
稳定性和区域元数据,只附最后一张 PNG/JPEG,不把图像字节重复塞进 JSON。`cu_hold` / `cu_free`
|
|
189
|
+
用于操作间保护或放开现场,`cu_release` 结束桌面周期;`cu_drag` 支持 `expectCursor` 与 `hwnd` 校验。
|
|
190
|
+
MCP 工具范围较小,DSH 的人类提问和宿主生命周期集成仍由 Cordis 入口提供。
|
|
191
|
+
|
|
175
192
|
## 开发
|
|
176
193
|
|
|
177
194
|
发布者检查默认查询 npm 官方源。若本地无法访问,可用 `CU_REGISTRY` 为只读的 `check` 命令指定镜像;JSON 输出会注明 `registry` 和 `authoritative`。PowerShell 示例(只影响当前终端):
|
package/lib/audit.js
CHANGED
|
@@ -28,3 +28,10 @@ export function appendAudit (dataDir, rec) {
|
|
|
28
28
|
appendFileSync(join(dataDir.root, 'audit.jsonl'), JSON.stringify(rec) + '\n')
|
|
29
29
|
} catch { /* audit must never break the action */ }
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
export function installAudit (cu, dataDir) {
|
|
33
|
+
const events = ['action', 'action:settled', 'handoff-wait']
|
|
34
|
+
const write = record => appendAudit(dataDir, record)
|
|
35
|
+
for (const event of events) cu.on(event, write)
|
|
36
|
+
return () => { for (const event of events) cu.off(event, write) }
|
|
37
|
+
}
|
package/lib/batch.js
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
1
|
// Serial execution and observation selection; no desktop or lifecycle side effects here.
|
|
2
2
|
import { handoffResult } from './continuation.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const isReadback = (step) => ['computer_state', 'computer_uia', 'computer_marks', 'computer_shot'].includes(step.tool) ||
|
|
6
|
-
(step.tool === 'computer_clip' && step.args?.text === undefined)
|
|
7
|
-
|
|
8
|
-
export function actionRefusal (step, result) {
|
|
9
|
-
if (result?.ok === false || result?.success === false) return result.error || 'action reported failure'
|
|
10
|
-
if (step.tool === 'computer_window' && (step.args?.op || 'activate') === 'activate' && result?.activated !== true) {
|
|
11
|
-
return 'target window activation was not confirmed; subsequent actions were not dispatched'
|
|
12
|
-
}
|
|
13
|
-
if (step.tool === 'computer_ctrl' && step.args?.action === 'recover' && result?.recovered !== true) return result.reason || 'temporary pause recovery refused'
|
|
14
|
-
if (step.tool === 'computer_ctrl' && step.args?.action === 'release' && !result?.released && !result?.alreadyReleased) return result.reason || 'cycle release refused'
|
|
15
|
-
return null
|
|
16
|
-
}
|
|
3
|
+
import { actionRefusal, actionError, isReadStep as isRead, isReadbackStep as isReadback, endsBatch } from './tool-contract.js'
|
|
4
|
+
export { actionRefusal } from './tool-contract.js'
|
|
17
5
|
|
|
18
6
|
export async function runBatch ({ steps, handlers, signal, stopped, screenshot, shot = 'auto', exec, assertOwnership }) {
|
|
19
7
|
if (!Array.isArray(steps) || !['auto', 'always', 'never'].includes(shot)) throw new Error('invalid batch actions or shot mode')
|
|
20
8
|
const actions = []
|
|
21
9
|
let failed = false
|
|
22
10
|
let continuation = null
|
|
11
|
+
let endedBy = null
|
|
23
12
|
for (const step of steps) {
|
|
24
13
|
const name = step?.tool
|
|
25
14
|
if (failed || signal?.aborted) {
|
|
26
15
|
actions.push({ tool: name || '', ok: false, outcome: 'not-dispatched',
|
|
27
16
|
...(continuation && !signal?.aborted
|
|
28
17
|
? { status: 'deferred', message: 'Resume this unfinished step after fresh target evidence; the original task must continue.' }
|
|
29
|
-
: { error: signal?.aborted ? 'cancelled' : 'earlier action failed' }) })
|
|
18
|
+
: { error: signal?.aborted ? 'cancelled' : endedBy ? 'batch ended by control action' : 'earlier action failed' }) })
|
|
30
19
|
failed = true
|
|
31
20
|
continue
|
|
32
21
|
}
|
|
@@ -38,22 +27,28 @@ export async function runBatch ({ steps, handlers, signal, stopped, screenshot,
|
|
|
38
27
|
const result = await fn(step.args || {}, exec)
|
|
39
28
|
const refusal = actionRefusal(step, result)
|
|
40
29
|
actions.push({ tool: name, ok: !refusal, result, ...(refusal ? { error: refusal } : {}) })
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
if (result?.taskStatus === 'continuing' && result.continuation?.required && !signal?.aborted && !stopped()) {
|
|
31
|
+
const nestedPath = result.continuation.resumeActionPath || [result.continuation.resumeFromActionIndex]
|
|
32
|
+
continuation = { ...result.continuation, resumeFromActionIndex: actions.length - 1,
|
|
33
|
+
resumeActionPath: [actions.length - 1, ...nestedPath.filter(Number.isInteger)] }
|
|
34
|
+
}
|
|
35
|
+
if (endsBatch(step)) endedBy = { tool: name, action: step.args?.action || name.replace(/^cu_/, '') }
|
|
36
|
+
else if (result?.endedBy) endedBy = result.endedBy
|
|
37
|
+
failed = !!refusal || !!continuation || (!stoppedBefore && stopped()) || !!endedBy
|
|
43
38
|
} catch (error) {
|
|
44
39
|
const checkpoint = !signal?.aborted && !stopped() ? handoffResult(error) : null
|
|
45
40
|
if (checkpoint) {
|
|
46
41
|
continuation = { ...checkpoint.continuation, resumeFromActionIndex: actions.length }
|
|
47
42
|
actions.push({ tool: name, ...checkpoint, continuation })
|
|
48
43
|
} else {
|
|
49
|
-
actions.push({ tool: name,
|
|
50
|
-
code: error.code || 'action-error', outcome: error.outcome || 'unknown' })
|
|
44
|
+
actions.push({ tool: name, ...actionError(error) })
|
|
51
45
|
}
|
|
52
46
|
failed = true
|
|
53
47
|
}
|
|
54
48
|
}
|
|
55
49
|
const needsShot = shot === 'always' || (shot === 'auto' && steps.some(s => !isRead(s)) && !isReadback(steps.at(-1) || {}))
|
|
56
50
|
const result = { actions, attachedShot: null }
|
|
51
|
+
if (endedBy) result.endedBy = endedBy
|
|
57
52
|
if (continuation) Object.assign(result, {
|
|
58
53
|
status: 'continuation-required', taskStatus: 'continuing', transient: true, continuation,
|
|
59
54
|
})
|
|
Binary file
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"platform": "win32",
|
|
4
4
|
"architecture": "x64",
|
|
5
5
|
"sourceSha256": "6e17308fc1cb0de1ece6f3164866b6de03ecdf74a42a6e78c0a8e6d02912c3bf",
|
|
6
|
-
"binarySha256": "
|
|
6
|
+
"binarySha256": "897c8dfd1ef2c0722105311d4775241999fab42d77a0ed1ecfc95ae290bf456f"
|
|
7
7
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Small post-action evidence, shared by host/MCP adapters. Never dispatches input,
|
|
2
|
+
// retries a read, inventories a UIA tree, or changes the original action receipt.
|
|
3
|
+
export const COMPACT_FEEDBACK_BYTES = 6144
|
|
4
|
+
export const COMPACT_OBSERVATION_MS = 6000
|
|
5
|
+
export const COMPACT_POINT_MS = 5000
|
|
6
|
+
|
|
7
|
+
const finite = value => typeof value === 'number' && Number.isFinite(value)
|
|
8
|
+
const bytes = value => Buffer.byteLength(JSON.stringify(value), 'utf8')
|
|
9
|
+
const rectKeys = ['x', 'y', 'width', 'height']
|
|
10
|
+
const windowKeys = ['hwnd', 'pid', 'process', 'title', 'class', 'active', 'minimized']
|
|
11
|
+
const elementKeys = ['name', 'role', 'controlType', 'automationId', 'className', 'value', 'text', 'enabled', 'isEnabled', 'offscreen', 'isOffscreen', 'editable', 'focused', 'hasKeyboardFocus', 'selected', 'checked', 'toggleState', 'expanded', 'expandCollapseState', 'hwnd', 'pid']
|
|
12
|
+
|
|
13
|
+
function point (value) {
|
|
14
|
+
if (typeof value === 'string') {
|
|
15
|
+
const parts = value.trim().split(/[,\s]+/)
|
|
16
|
+
if (parts.length !== 2 || parts.some(p => !p)) return null
|
|
17
|
+
value = { x: Number(parts[0]), y: Number(parts[1]) }
|
|
18
|
+
}
|
|
19
|
+
return value && finite(value.x) && finite(value.y) ? { x: Math.round(value.x), y: Math.round(value.y) } : null
|
|
20
|
+
}
|
|
21
|
+
export function feedbackRegion (value) {
|
|
22
|
+
if (typeof value === 'string') {
|
|
23
|
+
const parts = value.trim().split(/[,\s]+/)
|
|
24
|
+
if (parts.length !== 4 || parts.some(p => !p)) return null
|
|
25
|
+
value = Object.fromEntries(rectKeys.map((k, i) => [k, Number(parts[i])]))
|
|
26
|
+
}
|
|
27
|
+
if (!value || !rectKeys.every(k => finite(value[k])) || value.width <= 0 || value.height <= 0) return null
|
|
28
|
+
return Object.fromEntries(rectKeys.map(k => [k, value[k]]))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A native default verification crop is NOT authorization to narrow vision.
|
|
32
|
+
// Only an explicitly requested region changes the automatic image viewport.
|
|
33
|
+
export function feedbackViewport (args = {}, receipt = {}) {
|
|
34
|
+
args ||= {}; receipt ||= {}
|
|
35
|
+
const requested = (receipt.verificationSource === 'requested-region' ? feedbackRegion(receipt.verificationRegion) : null) ||
|
|
36
|
+
feedbackRegion(args.verificationRegion)
|
|
37
|
+
if (requested) return { region: rectKeys.map(k => requested[k]).join(','),
|
|
38
|
+
point: { x: Math.floor(requested.x + requested.width / 2), y: Math.floor(requested.y + requested.height / 2) }, source: 'requested-region' }
|
|
39
|
+
const actual = point(receipt)
|
|
40
|
+
if (actual) return { point: actual, source: 'action-receipt' }
|
|
41
|
+
const target = point(args.to) || point(args.at)
|
|
42
|
+
if (target) return { point: { x: target.x + (finite(args.dx) ? args.dx : 0), y: target.y + (finite(args.dy) ? args.dy : 0) }, source: 'requested-point' }
|
|
43
|
+
return { source: 'no-point' }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function clipped (value, limit, cuts, path) {
|
|
47
|
+
const text = String(value), raw = Buffer.from(text, 'utf8')
|
|
48
|
+
if (raw.length <= limit) return text
|
|
49
|
+
let end = limit - 3
|
|
50
|
+
while (end > 0 && (raw[end] & 0xc0) === 0x80) end--
|
|
51
|
+
cuts.push(path)
|
|
52
|
+
return raw.subarray(0, end).toString('utf8') + '…'
|
|
53
|
+
}
|
|
54
|
+
function brief (value, keys, cuts, path, limit = 768) {
|
|
55
|
+
if (!value || typeof value !== 'object') return null
|
|
56
|
+
const result = {}
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
const item = value[key]
|
|
59
|
+
if (typeof item === 'string') result[key] = clipped(item, limit, cuts, path + '.' + key)
|
|
60
|
+
else if (typeof item === 'boolean' || finite(item) || item === null) result[key] = item
|
|
61
|
+
}
|
|
62
|
+
const rect = feedbackRegion(value.rect || value.bounds)
|
|
63
|
+
if (rect) result.rect = rect
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Budget applies to the ADDED observation envelope, never to an input receipt.
|
|
68
|
+
// Whole fields are omitted if needed; a truncated field is never silently complete.
|
|
69
|
+
export function limitCompactFeedback (envelope, maxBytes = COMPACT_FEEDBACK_BYTES) {
|
|
70
|
+
const limit = Number.isInteger(maxBytes) && maxBytes >= 1024 ? Math.min(maxBytes, COMPACT_FEEDBACK_BYTES) : COMPACT_FEEDBACK_BYTES
|
|
71
|
+
const result = JSON.parse(JSON.stringify(envelope))
|
|
72
|
+
const omitted = []
|
|
73
|
+
const existing = result.observation?.truncatedFields || []
|
|
74
|
+
const budget = result.budget = { maxBytes: limit, returnedBytes: 0, truncated: existing.length > 0, omittedFields: omitted }
|
|
75
|
+
const fit = () => {
|
|
76
|
+
// returnedBytes is part of its own JSON; stabilize decimal width.
|
|
77
|
+
for (let i = 0; i < 4; i++) budget.returnedBytes = bytes(result)
|
|
78
|
+
return budget.returnedBytes <= limit
|
|
79
|
+
}
|
|
80
|
+
if (fit()) return result
|
|
81
|
+
for (const field of ['ancestors', 'focused', 'element', 'receiver', 'window', 'contextFromAction']) {
|
|
82
|
+
if (result.observation && Object.hasOwn(result.observation, field)) {
|
|
83
|
+
delete result.observation[field]; omitted.push('observation.' + field); budget.truncated = true
|
|
84
|
+
if (fit()) return result
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Unknown adapter metadata is not allowed to evade the byte contract. Retain
|
|
88
|
+
// the truth that this observation could not fit, rather than cut JSON or paths.
|
|
89
|
+
result.observation = { scope: 'compact', incomplete: true, reason: 'observation exceeded compact byte budget; request feedback full for complete readback' }
|
|
90
|
+
omitted.push('observation'); budget.truncated = true
|
|
91
|
+
if (result.feedback) result.feedback = { kind: result.feedback.kind, detail: 'compact', reason: 'bounded post-action observation' }
|
|
92
|
+
if (!fit()) {
|
|
93
|
+
for (const key of Object.keys(result)) if (!['observation', 'feedback', 'verification', 'budget'].includes(key)) delete result[key]
|
|
94
|
+
if (typeof result.verification === 'string') result.verification = clipped(result.verification, 512, omitted, 'verification')
|
|
95
|
+
fit()
|
|
96
|
+
}
|
|
97
|
+
return result
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function collectCompactState (cu, { args = {}, receipt = {}, checkAllowed = () => {}, archive, now = Date.now } = {}) {
|
|
101
|
+
args ||= {}; receipt ||= {}
|
|
102
|
+
const began = now(), deadline = began + COMPACT_OBSERVATION_MS, cuts = [], failures = []
|
|
103
|
+
const observation = { scope: 'local-post-action', ts: began, elementAvailable: false }
|
|
104
|
+
const raw = { scope: 'local-post-action', ts: began }
|
|
105
|
+
let viewport = feedbackViewport(args, receipt), calls = 0
|
|
106
|
+
const read = async (op, params, cap) => {
|
|
107
|
+
checkAllowed()
|
|
108
|
+
const remaining = Math.floor(deadline - now())
|
|
109
|
+
if (remaining <= 0) throw new Error('compact observation time budget exhausted')
|
|
110
|
+
calls++
|
|
111
|
+
return cu.call(op, params, Math.min(cap, remaining))
|
|
112
|
+
}
|
|
113
|
+
if (!viewport.point) {
|
|
114
|
+
try {
|
|
115
|
+
raw.cursor = await read('cursor', {}, 1000)
|
|
116
|
+
const current = point(raw.cursor)
|
|
117
|
+
if (current) viewport = { point: current, source: 'current-cursor' }
|
|
118
|
+
else failures.push({ channel: 'cursor', error: 'cursor position unavailable' })
|
|
119
|
+
} catch (error) { failures.push({ channel: 'cursor', error: clipped(error.message || error, 320, cuts, 'errors.cursor') }) }
|
|
120
|
+
}
|
|
121
|
+
if (viewport.point) {
|
|
122
|
+
observation.point = { ...viewport.point, source: viewport.source }
|
|
123
|
+
raw.point = observation.point
|
|
124
|
+
if (viewport.source === 'action-receipt' || viewport.source === 'current-cursor') observation.cursor = { ...viewport.point, source: viewport.source }
|
|
125
|
+
try {
|
|
126
|
+
const probe = await read('uiaFromPoint', viewport.point, COMPACT_POINT_MS)
|
|
127
|
+
raw.probe = probe
|
|
128
|
+
observation.window = brief(probe.root, windowKeys, cuts, 'window', 320)
|
|
129
|
+
observation.receiver = brief(probe.receives, windowKeys, cuts, 'receiver', 320)
|
|
130
|
+
observation.element = brief(probe.element, elementKeys, cuts, 'element')
|
|
131
|
+
observation.focused = brief(probe.focused, elementKeys, cuts, 'focused', 320)
|
|
132
|
+
if (typeof probe.typingWouldReach === 'boolean') observation.typingWouldReach = probe.typingWouldReach
|
|
133
|
+
const ancestors = probe.ancestors || probe.element?.ancestors || []
|
|
134
|
+
observation.ancestors = Array.isArray(ancestors) ? ancestors.slice(0, 3).map((a, i) => brief(a, elementKeys, cuts, `ancestors.${i}`, 160)) : []
|
|
135
|
+
if (Array.isArray(ancestors) && ancestors.length > 3) cuts.push('ancestors')
|
|
136
|
+
observation.elementAvailable = !!probe.element
|
|
137
|
+
if (!probe.element) failures.push({ channel: 'uiaFromPoint', error: 'no UIA element at this point; absence does not prove task failure' })
|
|
138
|
+
} catch (error) { failures.push({ channel: 'uiaFromPoint', error: clipped(error.message || error, 320, cuts, 'errors.uiaFromPoint') }) }
|
|
139
|
+
}
|
|
140
|
+
if (!viewport.point) observation.reason = 'No action, witness-region or current-cursor point was available; no UIA tree was read.'
|
|
141
|
+
if (receipt.receiver || receipt.focus || receipt.cursor) observation.contextFromAction = {
|
|
142
|
+
source: 'action-receipt, not an independent post-action read',
|
|
143
|
+
receiver: brief(receipt.receiver, windowKeys, cuts, 'context.receiver', 160),
|
|
144
|
+
focus: brief(receipt.focus, windowKeys, cuts, 'context.focus', 160),
|
|
145
|
+
cursor: point(receipt.cursor),
|
|
146
|
+
}
|
|
147
|
+
if (failures.length) observation.errors = failures
|
|
148
|
+
if (archive && (raw.probe || raw.cursor)) {
|
|
149
|
+
try {
|
|
150
|
+
const archived = await archive({ ...raw, ...(failures.length ? { errors: failures } : {}) })
|
|
151
|
+
const snapshot = typeof archived === 'string' ? archived : archived?.snapshot || archived?.observation
|
|
152
|
+
if (typeof snapshot === 'string' && /^O[\w-]{1,127}$/.test(snapshot)) observation.snapshot = snapshot
|
|
153
|
+
const cache = archived?.cache || archived?.archive
|
|
154
|
+
if (cache && (cache.status === 'memory-only' || cache.reason || cache.fallback)) {
|
|
155
|
+
observation.cache = brief(cache, ['status', 'reason', 'fallback'], cuts, 'cache', 160)
|
|
156
|
+
}
|
|
157
|
+
if (!observation.snapshot) {
|
|
158
|
+
// Once neither disk nor memory retains the evidence, truncation would
|
|
159
|
+
// make the omitted fields irretrievable. Return the existing raw value
|
|
160
|
+
// inline without another probe, input replay, clone or disk attempt.
|
|
161
|
+
observation.cache = { status: 'uncached', reason: cache?.reason || 'cache-unavailable', fallback: 'inline' }
|
|
162
|
+
observation.inlineEvidence = raw
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
observation.archiveError = clipped(error.message || error, 240, cuts, 'archiveError')
|
|
166
|
+
observation.cache = { status: 'uncached', reason: 'cache-unavailable', fallback: 'inline' }
|
|
167
|
+
observation.inlineEvidence = raw
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (cuts.length) observation.truncatedFields = [...new Set(cuts)]
|
|
171
|
+
observation.reads = calls
|
|
172
|
+
observation.elapsedMs = Math.max(0, now() - began)
|
|
173
|
+
observation.limitations = 'A local point/focus read may miss remote or visual changes. It does not verify the task effect; use explicit UIA/visual or application readback when needed.'
|
|
174
|
+
return observation
|
|
175
|
+
}
|
package/lib/continuation.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Temporary ownership handoff is a task checkpoint, not a failed/cancelled task.
|
|
2
2
|
export function handoffResult (error) {
|
|
3
|
-
if (error?.code
|
|
3
|
+
if (!['HUMAN_REOBSERVE', 'WORKER_REOBSERVE'].includes(error?.code) || !error.continuation?.required) return null
|
|
4
4
|
return {
|
|
5
5
|
ok: false,
|
|
6
6
|
status: 'continuation-required',
|
|
Binary file
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"format": 1,
|
|
3
3
|
"platform": "win32",
|
|
4
4
|
"architecture": "x64",
|
|
5
|
-
"sourceSha256": "
|
|
6
|
-
"binarySha256": "
|
|
5
|
+
"sourceSha256": "a7496b49b57ca8a505ecf12abad049e9b1b5d4b70f3ae574a5b55cb3fa76bd42",
|
|
6
|
+
"binarySha256": "60bbcd1bd6638b1088e65e7b4a6e68deeecc83cb470967499ed4d9911d98c99a"
|
|
7
7
|
}
|