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/lib/tools.js
CHANGED
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import { runBatch, actionRefusal } from './batch.js'
|
|
19
19
|
import { saveOwnedShot } from './artifacts.js'
|
|
20
|
-
import { attachScreenshot, renderScreenshot, selectBatchScreenshot } from './image-output.js'
|
|
20
|
+
import { attachScreenshot, renderScreenshot, selectBatchScreenshot, screenshotMetadata } from './image-output.js'
|
|
21
21
|
import { ObservationStore } from './observations.js'
|
|
22
|
-
import {
|
|
22
|
+
import { feedbackPolicy, feedbackActions, feedbackVerification, selectFeedbackDetail } from './feedback.js'
|
|
23
|
+
import { collectCompactState, limitCompactFeedback, feedbackViewport } from './compact-feedback.js'
|
|
23
24
|
import { handoffResult } from './continuation.js'
|
|
25
|
+
import { join } from 'node:path'
|
|
24
26
|
|
|
25
27
|
const P = (type, description, extra = {}) => ({ type, description, ...extra })
|
|
26
28
|
const cooperativeTools = new Set(['computer_click', 'computer_move', 'computer_drag', 'computer_scroll',
|
|
@@ -29,6 +31,11 @@ const usesInput = (name, args = {}) => name === 'computer_clip' ? args.text !==
|
|
|
29
31
|
name === 'computer_batch' ? (args.actions || []).some(step => usesInput(step.tool, step.args)) :
|
|
30
32
|
cooperativeTools.has(name)
|
|
31
33
|
|
|
34
|
+
export const isCycleStateOnly = (name, args = {}) => name === 'computer_ctrl'
|
|
35
|
+
? ['hold', 'free', 'release', 'clear_cache'].includes(args?.action)
|
|
36
|
+
: name === 'computer_batch' && Array.isArray(args?.actions) && args.actions.length > 0 &&
|
|
37
|
+
args.actions.every(step => isCycleStateOnly(step?.tool, step?.args))
|
|
38
|
+
|
|
32
39
|
/** "M3" | "x,y" | "x y" -> {mark:'M3'} | {x,y}. One parameter instead of four. */
|
|
33
40
|
const pt = (v) => {
|
|
34
41
|
if (v === undefined || v === null) return null
|
|
@@ -49,7 +56,18 @@ const rect = (v) => {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, defineTool, hooks, ensureCycle, register }) {
|
|
52
|
-
const observations = new ObservationStore()
|
|
59
|
+
const observations = new ObservationStore({ archiveDir: typeof dataDir?.root === 'string' ? join(dataDir.root, 'observation-cache') : undefined })
|
|
60
|
+
let observationEpoch
|
|
61
|
+
const syncObservationEpoch = () => {
|
|
62
|
+
const epoch = JSON.stringify([cu.workerGeneration ?? null, cu.cooperation?.revision ?? null, cu.stateSeq ?? 0])
|
|
63
|
+
if (observationEpoch !== undefined && observationEpoch !== epoch) observations.invalidate('worker-or-human-state-changed')
|
|
64
|
+
observationEpoch = epoch
|
|
65
|
+
}
|
|
66
|
+
const archiveFeedback = (exec, raw) => {
|
|
67
|
+
syncObservationEpoch()
|
|
68
|
+
const saved = observations.publish(exec, 'state', ['post-action', raw.point?.x, raw.point?.y], raw, undefined, { archive: true })
|
|
69
|
+
return { snapshot: saved.observation, archive: saved.archive }
|
|
70
|
+
}
|
|
53
71
|
const saveShot = (buf, ext = 'jpg') => {
|
|
54
72
|
const p = snapshotPath(dataDir).replace(/\.jpg$/, `.${ext}`)
|
|
55
73
|
return saveOwnedShot(dataDir, p, buf)
|
|
@@ -64,9 +82,8 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
64
82
|
maxWidth: r && r.width <= 900 ? r.width : 1280,
|
|
65
83
|
})
|
|
66
84
|
const path = saveShot(shot.image, shot.mime === 'image/png' ? 'png' : 'jpg')
|
|
67
|
-
const res = { path, mime: shot.mime, region: shot.region }
|
|
68
|
-
|
|
69
|
-
return attachScreenshot(ctx, exec, res, shot.image)
|
|
85
|
+
const res = { path, mime: shot.mime, region: shot.region, ...screenshotMetadata(shot) }
|
|
86
|
+
return attachScreenshot(ctx, exec, res, shot.image, { rules: config.feedbackRules })
|
|
70
87
|
}
|
|
71
88
|
|
|
72
89
|
const clickTarget = async (args) => {
|
|
@@ -81,26 +98,30 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
const queryUia = async (args, exec) => {
|
|
84
|
-
if (args.snapshot) return observations.get(exec, args.snapshot, 'uia')
|
|
101
|
+
if (args.snapshot) return observations.get(exec, args.snapshot, 'uia', { offset: args.offset, limit: args.limit })
|
|
85
102
|
const p = pt(args.at)
|
|
86
103
|
if (p && p.x !== undefined) return cu.call('uiaFromPoint', { x: p.x, y: p.y })
|
|
87
104
|
// Identity witnesses help an intended action, but are expensive for a broad inventory.
|
|
88
105
|
const targets = !!args.hwnd && (args.targets ?? !!(args.id || args.name))
|
|
89
|
-
const res = await cu.call('uia', { hwnd: args.hwnd, depth: args.depth ??
|
|
106
|
+
const res = await cu.call('uia', { hwnd: args.hwnd, depth: args.depth ?? 6, maxNodes: args.maxNodes ?? 120,
|
|
107
|
+
budgetMs: args.budgetMs ?? 1200, anchor: args.anchor,
|
|
90
108
|
query: true, targets, role: args.role, nameContains: args.name, automationId: args.id, limit: args.limit ?? 80 })
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
syncObservationEpoch()
|
|
110
|
+
return observations.publish(exec, 'uia', [args.hwnd ?? null, args.depth ?? 6, args.maxNodes ?? 120, args.budgetMs ?? 1200, args.anchor ?? null, args.role ?? null, args.name ?? null, args.id ?? null, args.limit ?? 80, targets],
|
|
111
|
+
{ count: res.count, elements: res.flat || [], scanned: res.scanned, truncated: res.truncated, budget: res.budget, hwnd: res.hwnd ?? args.hwnd, pid: res.pid }, args.since)
|
|
93
112
|
}
|
|
94
113
|
const actUia = (args, exec) => {
|
|
95
114
|
if (args.target && !observations.ownsTarget(exec, args.target)) {
|
|
96
115
|
throw Object.assign(new Error('TARGET_NOT_OBSERVED: obtain a fresh computer_uia target in this Agent before acting'), { code: 'TARGET_NOT_OBSERVED', outcome: 'not-dispatched' })
|
|
97
116
|
}
|
|
98
117
|
return cu.uiaAct({ hwnd: args.hwnd, nameContains: args.name, automationId: args.id, target: args.target, allowRebind: args.rebind === true,
|
|
99
|
-
role: args.role, action: args.action, value: args.value, requireUnique: true,
|
|
118
|
+
role: args.role, action: args.action, value: args.value, requireUnique: true,
|
|
119
|
+
depth: args.depth ?? 6, maxNodes: 300, anchor: args.anchor, budgetMs: args.budgetMs ?? 1200 })
|
|
100
120
|
}
|
|
101
121
|
const observeState = async (args, exec) => {
|
|
102
|
-
if (args.snapshot) return observations.get(exec, args.snapshot, 'state')
|
|
122
|
+
if (args.snapshot) return observations.get(exec, args.snapshot, 'state', { offset: args.offset, limit: args.limit })
|
|
103
123
|
const result = args.windows ? await cu.call('windows', {}) : await cu.screenState({ uiaDepth: args.depth })
|
|
124
|
+
syncObservationEpoch()
|
|
104
125
|
return observations.publish(exec, 'state', [!!args.windows, args.depth ?? 6], result, args.since)
|
|
105
126
|
}
|
|
106
127
|
|
|
@@ -128,28 +149,46 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
128
149
|
if (exec?.signal?.aborted || cu.stopped || cu.exited || cu.cooperation?.active) return true
|
|
129
150
|
try { return !!hooks?.askBlocked?.('computer_shot') } catch { return true }
|
|
130
151
|
}
|
|
131
|
-
const captureFeedback = async (exec,
|
|
152
|
+
const captureFeedback = async (exec, policy, context = {}) => {
|
|
132
153
|
// Route resolution is async: the human can brake while it is pending.
|
|
133
154
|
if (feedbackBlocked(exec)) return { observationSkipped: 'cancelled-or-paused' }
|
|
134
|
-
const
|
|
135
|
-
|
|
155
|
+
const compact = policy.detail === 'compact'
|
|
156
|
+
const checkAllowed = () => {
|
|
157
|
+
if (feedbackBlocked(exec)) throw new Error('compact observation cancelled-or-paused')
|
|
158
|
+
hooks?.assertOwnership?.(exec)
|
|
159
|
+
}
|
|
160
|
+
const observation = policy.kind === 'state'
|
|
161
|
+
? compact ? await collectCompactState(cu, { ...context, checkAllowed,
|
|
162
|
+
archive: raw => hooks?.archiveFeedback ? hooks.archiveFeedback(exec, raw) : archiveFeedback(exec, raw) }) : await observeState({}, exec)
|
|
163
|
+
: await takeScreenshot(compact ? { region: feedbackViewport(context.args, context.receipt).region } : {}, exec)
|
|
164
|
+
const envelope = { observation, feedback: policy, verification: feedbackVerification }
|
|
165
|
+
if (compact && policy.kind === 'state' && observation.inlineEvidence) {
|
|
166
|
+
// Resource fallback deliberately favors recoverable evidence over the
|
|
167
|
+
// compact byte cap. This is the already-read local result, never a retry.
|
|
168
|
+
envelope.feedback = { ...policy, detail: 'full', reason: 'cache unavailable; complete local evidence returned inline' }
|
|
169
|
+
return envelope
|
|
170
|
+
}
|
|
171
|
+
// Images retain their existing attachment/coordinate contract. The compact
|
|
172
|
+
// text budget bounds the extra local UIA envelope, never the native receipt.
|
|
173
|
+
return compact && policy.kind === 'state' ? limitCompactFeedback(envelope) : envelope
|
|
136
174
|
}
|
|
137
175
|
|
|
138
176
|
const renderFeedback = (original, args, value) => {
|
|
139
177
|
if (!value?.feedback && !value?.observationError && !value?.observationSkipped) return original(args, value)
|
|
140
|
-
const { observation, feedback, ...receipt } = value
|
|
178
|
+
const { observation, feedback, budget, ...receipt } = value
|
|
141
179
|
return [
|
|
142
180
|
...original(args, receipt),
|
|
143
181
|
...(observation ? feedback?.kind === 'image' ? renderScreenshot(observation)
|
|
144
|
-
: [{ type: 'text', text: JSON.stringify({ feedback, observation }) }] : []),
|
|
182
|
+
: [{ type: 'text', text: JSON.stringify({ feedback, observation, ...(budget ? { budget } : {}) }) }] : []),
|
|
145
183
|
]
|
|
146
184
|
}
|
|
147
185
|
|
|
148
186
|
const reg = (def) => {
|
|
149
187
|
const inner = def.execute
|
|
150
|
-
const hasFeedback = feedbackActions.has(def.name)
|
|
188
|
+
const hasFeedback = feedbackActions.has(def.name)
|
|
189
|
+
if (hasFeedback || def.name === 'computer_batch') def.parameters.feedback = P('string', 'compact (default) | full | none', { enum: ['compact', 'full', 'none'] })
|
|
151
190
|
if (hasFeedback) {
|
|
152
|
-
def.description += ' Returns a route-specific checkpoint.'
|
|
191
|
+
def.description += ' Returns a route-specific checkpoint; input success does not verify the task effect.'
|
|
153
192
|
const original = def.output.render
|
|
154
193
|
def.output.render = (args, value) => renderFeedback(original, args, value)
|
|
155
194
|
// Input plus the existing bounded screenshot/state call share one deadline.
|
|
@@ -159,7 +198,10 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
159
198
|
// human wait may be long; the owning tool's cancellation still releases it immediately.
|
|
160
199
|
if (cooperativeTools.has(def.name)) def.timeoutMs = Math.max(def.timeoutMs, 3_600_000)
|
|
161
200
|
def.execute = async (args, exec) => {
|
|
201
|
+
if (def.name === 'computer_ctrl' && args?.action === 'clear_cache')
|
|
202
|
+
return lossless({ ...observations.clear(exec), desktopInput: false })
|
|
162
203
|
const releaseOnly = def.name === 'computer_ctrl' && args?.action === 'release'
|
|
204
|
+
const stateOnly = isCycleStateOnly(def.name, args)
|
|
163
205
|
// hooks drive the "the agent stopped driving" signal: the activity border is cut the
|
|
164
206
|
// instant the last computer-use call of a turn finishes.
|
|
165
207
|
if (hooks && hooks.onStart) { try { hooks.onStart(exec) } catch { /* never break a tool */ } }
|
|
@@ -203,7 +245,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
203
245
|
err.dshCycleRefusal = true
|
|
204
246
|
throw err
|
|
205
247
|
}
|
|
206
|
-
if (ensureCycle && !
|
|
248
|
+
if (ensureCycle && !stateOnly) {
|
|
207
249
|
let opened = null
|
|
208
250
|
try { opened = await ensureCycle(exec) } catch (e) {
|
|
209
251
|
// An OWNERSHIP refusal is a VERDICT, not a hiccup: another session owns this machine
|
|
@@ -244,27 +286,36 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
244
286
|
// registration-only probe does exactly that), and the earlier `hooks.onDispatch` form threw
|
|
245
287
|
// `Cannot read properties of undefined` straight into the dispatch path.
|
|
246
288
|
const invoke = async () => {
|
|
289
|
+
syncObservationEpoch()
|
|
247
290
|
if (usesInput(def.name, args) && cu.waitInputReady) await cu.waitInputReady()
|
|
248
291
|
if (!releaseOnly && hooks?.assertOwnership) hooks.assertOwnership(exec)
|
|
249
292
|
if (hooks && hooks.onDispatch) { try { hooks.onDispatch(exec) } catch { /* never break a dispatch for a log */ } }
|
|
250
293
|
return inner(args, exec)
|
|
251
294
|
}
|
|
252
|
-
const inputContext = { ...exec, assertOwnership: () => hooks?.assertOwnership?.(exec) }
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
295
|
+
const inputContext = { ...exec, ...hooks?.contextOf?.(exec), assertOwnership: () => hooks?.assertOwnership?.(exec) }
|
|
296
|
+
const executeWithFeedback = async () => {
|
|
297
|
+
const detail = hasFeedback ? selectFeedbackDetail(args?.feedback,
|
|
298
|
+
config.actionFeedback === false ? 'none' : config.actionFeedbackDetail ?? 'compact') : null
|
|
299
|
+
const out = await invoke()
|
|
300
|
+
if (out === undefined || out === null) return out
|
|
301
|
+
let observed = out
|
|
302
|
+
if (hasFeedback && detail !== 'none' && !actionRefusal({ tool: def.name, args }, out) && !feedbackBlocked(exec)) {
|
|
303
|
+
try {
|
|
304
|
+
const policy = await feedbackPolicy(ctx, exec, true, { rules: config.feedbackRules, detail })
|
|
305
|
+
if (policy.kind) {
|
|
306
|
+
hooks?.assertOwnership?.(exec)
|
|
307
|
+
const captured = await captureFeedback(exec, policy, { args, receipt: out, tool: def.name })
|
|
308
|
+
observed = { ...out, ...captured, verification: out.verification ?? captured.verification }
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
// Optional feedback cannot erase an input receipt or invite replay.
|
|
312
|
+
observed = { ...out, observationError: String(error.message || error), verification: out.verification ?? feedbackVerification }
|
|
313
|
+
}
|
|
264
314
|
}
|
|
315
|
+
const safe = lossless(observed)
|
|
316
|
+
return safe === undefined ? null : safe
|
|
265
317
|
}
|
|
266
|
-
|
|
267
|
-
return safe === undefined ? null : safe
|
|
318
|
+
return cu.withInputContext ? await cu.withInputContext(inputContext, executeWithFeedback) : await executeWithFeedback()
|
|
268
319
|
} catch (error) {
|
|
269
320
|
// The native/local wait has already finished. Return an explicit continuation
|
|
270
321
|
// checkpoint through normal tool output so the host does not end the task as an error.
|
|
@@ -346,6 +397,8 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
346
397
|
depth: P('number', 'tree depth (default 6)'),
|
|
347
398
|
since: P('string', 'base observation ID for changes; invalid base returns full'),
|
|
348
399
|
snapshot: P('string', 'retrieve this full observation without capturing again'),
|
|
400
|
+
offset: P('number', 'snapshot only: first row, zero-based'),
|
|
401
|
+
limit: P('number', 'snapshot only: rows per array, 1–80'),
|
|
349
402
|
},
|
|
350
403
|
output: outObservation(),
|
|
351
404
|
isConcurrencySafe: () => false,
|
|
@@ -388,15 +441,19 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
388
441
|
description: "Query hwnd plus role/name/exact id; returns controls and patterns. Scoped name/id queries also return target handles. since gives fresh changes; omit for full. snapshot retrieves a retained full result. at hit-tests. truncated:true is partial evidence.",
|
|
389
442
|
parameters: {
|
|
390
443
|
at: P('string', 'hit test at "x,y"'),
|
|
391
|
-
hwnd: P('number', 'window;
|
|
444
|
+
hwnd: P('number', 'required window scope for a tree query; at needs no tree'),
|
|
392
445
|
role: P('string', 'Button | Edit | Hyperlink | TabItem | ...'),
|
|
393
446
|
name: P('string', 'name substring'),
|
|
394
447
|
id: P('string', 'exact AutomationId from UIA'),
|
|
395
448
|
targets: P('boolean', 'recovery handles; default on for hwnd + name/id, off for broad queries'),
|
|
396
449
|
limit: P('number', 'maximum matches, 1–80 (default 80)'),
|
|
397
|
-
depth: P('number', 'tree depth,
|
|
450
|
+
depth: P('number', 'tree depth, default 6; broader traversal needs anchor'),
|
|
451
|
+
anchor: P('string', 'observed "x,y" inside hwnd; search only this element subtree'),
|
|
452
|
+
maxNodes: P('number', 'scanned node cap, default 120; above 300 needs anchor'),
|
|
453
|
+
budgetMs: P('number', 'soft traversal budget 100–5000 ms (default 1200); one provider call can exceed it'),
|
|
398
454
|
since: P('string', 'base observation ID; repeat the same query'),
|
|
399
455
|
snapshot: P('string', 'retrieve a retained full observation ID'),
|
|
456
|
+
offset: P('number', 'snapshot only: first row, zero-based; limit bounds the page'),
|
|
400
457
|
},
|
|
401
458
|
output: outObservation(),
|
|
402
459
|
isConcurrencySafe: () => false,
|
|
@@ -408,13 +465,15 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
408
465
|
name: 'computer_uia_act',
|
|
409
466
|
description: "Act through UIA. Prefer an observed target handle; rebind:true permits unique identity/semantic recovery inside the same window. Or use hwnd + exact id. Ambiguous, changed or incomplete evidence refuses; verify readback.",
|
|
410
467
|
parameters: {
|
|
411
|
-
hwnd: P('number', 'window
|
|
468
|
+
hwnd: P('number', 'required window scope unless using an observed target'),
|
|
412
469
|
name: P('string', 'element name substring'),
|
|
413
470
|
id: P('string', 'exact AutomationId from UIA; scope with hwnd'),
|
|
414
471
|
role: P('string', 'TabItem | Button | ListItem | ComboBox | Edit | MenuItem'),
|
|
415
472
|
action: P('string', 'invoke (default) | select | expand | collapse | toggle | setValue | focus | scrollIntoView'),
|
|
416
473
|
value: P('string', 'value for setValue'),
|
|
417
|
-
depth: P('number', 'tree depth,
|
|
474
|
+
depth: P('number', 'tree depth, default 6; broader traversal needs anchor'),
|
|
475
|
+
anchor: P('string', 'observed "x,y" inside hwnd; narrow selector search'),
|
|
476
|
+
budgetMs: P('number', 'soft traversal budget 100–5000 ms (default 1200)'),
|
|
418
477
|
target: P('string', 'opaque target from computer_uia; do not combine with selectors'),
|
|
419
478
|
rebind: P('boolean', 'allow unique recovery if the observed native element was replaced'),
|
|
420
479
|
},
|
|
@@ -436,6 +495,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
436
495
|
button: P('string', 'left (default) | right | middle'),
|
|
437
496
|
clicks: P('number', '1 | 2 (double) | 3 (triple)'),
|
|
438
497
|
shift: P('boolean', 'hold SHIFT (range selection)'),
|
|
498
|
+
verificationRegion: P('string', 'expected effect region "x,y,w,h" in screen pixels; may differ from click point; a change alone does not prove task success'),
|
|
439
499
|
},
|
|
440
500
|
output: outJSON(),
|
|
441
501
|
isConcurrencySafe: () => false,
|
|
@@ -445,7 +505,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
445
505
|
|
|
446
506
|
reg({
|
|
447
507
|
name: 'computer_move',
|
|
448
|
-
description: "
|
|
508
|
+
description: "Hover without clicking; returns Windows standard cursor evidence. Browser col-resize/row-resize or custom cursors may be unrecognized; unknown proves neither a handle hit nor a miss.",
|
|
449
509
|
parameters: { at: P('string', '"x,y"', { required: true }) },
|
|
450
510
|
output: outJSON(),
|
|
451
511
|
isConcurrencySafe: () => false,
|
|
@@ -459,16 +519,18 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
459
519
|
|
|
460
520
|
reg({
|
|
461
521
|
name: 'computer_drag',
|
|
462
|
-
description: "Drag
|
|
522
|
+
description: "Drag measured screen points. Optional expectCursor checks a recognized Windows standard shape before button-down; browser/custom cursors may be unverifiable. Ground those handles independently. cursorCheck says matched or not-requested; ok is delivery, not layout success. Verify application geometry. Prefer computer_select for text.",
|
|
463
523
|
parameters: {
|
|
464
524
|
from: P('string', '"M3" or "x,y"', { required: true }),
|
|
465
525
|
to: P('string', '"M7" or "x,y"', { required: true }),
|
|
526
|
+
expectCursor: P('string', 'Optional standard shape: resize | resize-ew | resize-ns | resize-nesw | resize-nwse | move | hand. Unknown refuses this check; omit only with independently verified target geometry/hit-test.'),
|
|
527
|
+
hwnd: P('number', 'expected window at the drag start'),
|
|
466
528
|
},
|
|
467
529
|
output: outJSON(),
|
|
468
530
|
isConcurrencySafe: () => false,
|
|
469
531
|
timeoutMs: 15_000,
|
|
470
532
|
async execute (args) {
|
|
471
|
-
return cu.drag(await cu.resolveTarget(pt(args.from)), await cu.resolveTarget(pt(args.to)))
|
|
533
|
+
return cu.drag(await cu.resolveTarget(pt(args.from)), await cu.resolveTarget(pt(args.to)), { expectCursor: args.expectCursor, expectHwnd: args.hwnd })
|
|
472
534
|
},
|
|
473
535
|
})
|
|
474
536
|
|
|
@@ -637,15 +699,32 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
637
699
|
isConcurrencySafe: () => false,
|
|
638
700
|
timeoutMs: 120_000,
|
|
639
701
|
async execute (args, exec) {
|
|
640
|
-
let automatic, feedback
|
|
641
|
-
const
|
|
702
|
+
let automatic, feedback, lastContext = {}
|
|
703
|
+
const detail = selectFeedbackDetail(args.feedback,
|
|
704
|
+
config.actionFeedback === false ? 'none' : config.actionFeedbackDetail ?? 'compact')
|
|
705
|
+
// Batch still observes once at its final checkpoint. Capture the actual
|
|
706
|
+
// result coordinates without re-resolving potentially stale mark ids.
|
|
707
|
+
const feedbackHandlers = Object.fromEntries(Object.entries(HANDLERS).map(([name, handler]) => [name,
|
|
708
|
+
async (input, context) => {
|
|
709
|
+
const receipt = await handler(input, context)
|
|
710
|
+
lastContext = { args: input, receipt, tool: name }
|
|
711
|
+
return receipt
|
|
712
|
+
}]))
|
|
713
|
+
const result = await runBatch({ steps: args.actions, handlers: feedbackHandlers, signal: exec && exec.signal,
|
|
642
714
|
stopped: () => cu.stopped || cu.exited, shot: args.shot, exec,
|
|
643
715
|
assertOwnership: () => hooks?.assertOwnership?.(exec),
|
|
644
716
|
screenshot: async () => {
|
|
645
|
-
const
|
|
717
|
+
const policy = args.shot === 'always' ? { kind: 'image', detail: 'full', reason: 'explicit batch screenshot' }
|
|
718
|
+
: await feedbackPolicy(ctx, exec, config.actionFeedback !== false || args.feedback !== undefined, { rules: config.feedbackRules, detail })
|
|
646
719
|
if (feedbackBlocked(exec)) { feedback = { observationSkipped: 'cancelled-or-paused' }; return null }
|
|
647
|
-
if (
|
|
648
|
-
|
|
720
|
+
if (!policy.kind) { feedback = { feedback: policy, observationSkipped: policy.reason || 'disabled' }; return null }
|
|
721
|
+
hooks?.assertOwnership?.(exec)
|
|
722
|
+
if (policy.kind === 'state') { feedback = await captureFeedback(exec, policy, lastContext); return null }
|
|
723
|
+
const captured = await captureFeedback(exec, policy, lastContext)
|
|
724
|
+
const { observation, ...details } = captured
|
|
725
|
+
automatic = observation
|
|
726
|
+
feedback = details
|
|
727
|
+
if (!automatic) return null
|
|
649
728
|
return automatic.path
|
|
650
729
|
} })
|
|
651
730
|
return { ...selectBatchScreenshot(result, feedback?.observation ?? automatic, feedback?.feedback?.kind), ...feedback }
|
|
@@ -655,9 +734,9 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
655
734
|
// ---------------- meta ----------------
|
|
656
735
|
reg({
|
|
657
736
|
name: 'computer_ctrl',
|
|
658
|
-
description: '
|
|
737
|
+
description: 'hold requests stable page/pointer state between actions (breathing blue); free allows human use while observing (cyan). Neither blocks human takeover. After final verification, release ends desktop control while the conversation continues. stop raises a manual brake, released by human Ctrl+Alt+R; recover checks a temporary diagnostic pause; exit aborts. selftest/calibrate diagnose input; indicator controls the border; acknowledge accepts the skill receipt.',
|
|
659
738
|
parameters: {
|
|
660
|
-
action: P('string', 'release | selftest | calibrate | stop | exit | indicator | acknowledge | recover', { required: true }),
|
|
739
|
+
action: P('string', 'hold | free | release | clear_cache | selftest | calibrate | stop | exit | indicator | acknowledge | recover', { required: true }),
|
|
661
740
|
why: P('string', 'reason recorded with a stop'),
|
|
662
741
|
temporary: P('boolean', 'stop: explicitly agent-owned diagnostic pause, eligible for checked recovery; ordinary waits use computer_wait'),
|
|
663
742
|
pauseId: P('string', 'recover: credential returned by an explicit temporary stop'),
|
|
@@ -671,6 +750,9 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
671
750
|
timeoutMs: 30_000,
|
|
672
751
|
async execute (args, exec) {
|
|
673
752
|
switch (args.action) {
|
|
753
|
+
case 'clear_cache': return { ...observations.clear(exec), desktopInput: false }
|
|
754
|
+
case 'hold': return cu.indicator({ hold: true })
|
|
755
|
+
case 'free': return cu.indicator({ hold: false })
|
|
674
756
|
case 'release': return hooks.releaseCycle(exec, args.why)
|
|
675
757
|
case 'stop': return args.temporary ? hooks.temporaryStop(exec, args.why) : cu.panic(args.why)
|
|
676
758
|
case 'recover': return hooks.recover(exec, args.pauseId)
|
|
@@ -706,7 +788,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
706
788
|
const HANDLERS = {
|
|
707
789
|
computer_click: clickTarget,
|
|
708
790
|
computer_move: (a) => { const p = pt(a.at); return cu.move(p.x, p.y) },
|
|
709
|
-
computer_drag: async (a) => cu.drag(await cu.resolveTarget(pt(a.from)), await cu.resolveTarget(pt(a.to))),
|
|
791
|
+
computer_drag: async (a) => cu.drag(await cu.resolveTarget(pt(a.from)), await cu.resolveTarget(pt(a.to)), { expectCursor: a.expectCursor, expectHwnd: a.hwnd }),
|
|
710
792
|
computer_scroll: (a) => cu.scroll(a.dir, a.lines || 3, pt(a.at) ? { at: pt(a.at) } : {}),
|
|
711
793
|
computer_select: async (a) => {
|
|
712
794
|
const f = await cu.resolveTarget(pt(a.from)); const t = await cu.resolveTarget(pt(a.to))
|
|
@@ -749,6 +831,9 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
|
|
|
749
831
|
computer_uia: queryUia,
|
|
750
832
|
computer_ctrl: (a, exec) => {
|
|
751
833
|
switch (a.action) {
|
|
834
|
+
case 'clear_cache': return { ...observations.clear(exec), desktopInput: false }
|
|
835
|
+
case 'hold': return cu.indicator({ hold: true })
|
|
836
|
+
case 'free': return cu.indicator({ hold: false })
|
|
752
837
|
case 'release': return hooks.releaseCycle(exec, a.why)
|
|
753
838
|
case 'stop': return a.temporary ? hooks.temporaryStop(exec, a.why) : cu.panic(a.why)
|
|
754
839
|
case 'recover': return hooks.recover(exec, a.pauseId)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// A witness rectangle describes where the caller expects an effect, independently
|
|
2
|
+
// of the input point. Validation happens before focus, pointer or button effects.
|
|
3
|
+
export function verificationRegion (value) {
|
|
4
|
+
if (value === undefined) return undefined
|
|
5
|
+
const tokens = typeof value === 'string' ? value.trim().split(/[,\s]+/) : null
|
|
6
|
+
const parts = tokens ? (tokens.some(token => !token) ? [] : tokens.map(Number))
|
|
7
|
+
: value && typeof value === 'object' && !Array.isArray(value)
|
|
8
|
+
? [value.x, value.y, value.width, value.height] : []
|
|
9
|
+
const [x, y, width, height] = parts
|
|
10
|
+
if (parts.length !== 4 || !parts.every(n => Number.isInteger(n) && n > -2147483648 && n <= 2147483647) ||
|
|
11
|
+
width <= 0 || height <= 0 || width * height > 16777216 ||
|
|
12
|
+
x + width > 2147483647 || y + height > 2147483647) {
|
|
13
|
+
throw Object.assign(new Error('verificationRegion must be "x,y,width,height" in integer screen pixels with positive size (maximum 16M pixels). No input was dispatched.'),
|
|
14
|
+
{ code: 'INVALID_VERIFICATION_REGION', outcome: 'not-dispatched' })
|
|
15
|
+
}
|
|
16
|
+
return { x, y, width, height }
|
|
17
|
+
}
|
package/mcp/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh/computer-use-mcp",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Embedded MCP stdio adapter for the root dsh-computer-use package; not a standalone package. Zero npm dependencies.",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "src/index.js",
|
|
7
8
|
"bin": { "dsh-computer-use-mcp": "src/index.js" },
|
package/mcp/src/execution.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Serialize whole MCP tool calls, including batches and human handoff waits.
|
|
2
2
|
// The native desktop lease also excludes other worker processes.
|
|
3
|
+
import { handoffResult } from '../../lib/continuation.js'
|
|
4
|
+
|
|
3
5
|
export function createToolQueue () {
|
|
4
6
|
let tail = Promise.resolve()
|
|
5
7
|
return (run, signal) => {
|
|
@@ -14,9 +16,10 @@ export function createToolQueue () {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export function continuationResult (error, describe) {
|
|
17
|
-
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const shared = handoffResult(error)
|
|
20
|
+
if (shared) return shared
|
|
21
|
+
// Compatibility for a bare human-handoff error from an older core. Worker
|
|
22
|
+
// recovery must retain its own description, never invent a human handback.
|
|
23
|
+
if (error?.code !== 'HUMAN_REOBSERVE' || !describe) return null
|
|
24
|
+
return handoffResult({ ...error, continuation: describe(error) })
|
|
22
25
|
}
|