@anionex/dsh-computer-use 0.1.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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +307 -0
- package/README.zh.md +307 -0
- package/assets/computer-use-fixture.png +0 -0
- package/assets/cursor.png +0 -0
- package/cordis.patch.yml +13 -0
- package/docs/interaction-policy.i18n.yaml +4 -0
- package/docs/interaction-policy.md +140 -0
- package/docs/interaction-policy.zh.md +140 -0
- package/lib/approval-policy.js +12 -0
- package/lib/approval-policy.js.map +1 -0
- package/lib/artifacts.js +79 -0
- package/lib/artifacts.js.map +1 -0
- package/lib/backend.js +3 -0
- package/lib/backend.js.map +1 -0
- package/lib/client.js +349 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +120 -0
- package/lib/config.js.map +1 -0
- package/lib/confirmations.js +99 -0
- package/lib/confirmations.js.map +1 -0
- package/lib/diff.js +58 -0
- package/lib/diff.js.map +1 -0
- package/lib/errors.js +22 -0
- package/lib/errors.js.map +1 -0
- package/lib/exposure.js +231 -0
- package/lib/exposure.js.map +1 -0
- package/lib/index.js +56 -0
- package/lib/index.js.map +1 -0
- package/lib/leases.js +259 -0
- package/lib/leases.js.map +1 -0
- package/lib/providers/macos.js +148 -0
- package/lib/providers/macos.js.map +1 -0
- package/lib/providers/native-helper.js +356 -0
- package/lib/providers/native-helper.js.map +1 -0
- package/lib/service.js +480 -0
- package/lib/service.js.map +1 -0
- package/lib/skill.js +119 -0
- package/lib/skill.js.map +1 -0
- package/lib/target-resolver.js +180 -0
- package/lib/target-resolver.js.map +1 -0
- package/lib/tools.js +493 -0
- package/lib/tools.js.map +1 -0
- package/lib/types/approval-policy.d.ts +13 -0
- package/lib/types/approval-policy.d.ts.map +1 -0
- package/lib/types/artifacts.d.ts +10 -0
- package/lib/types/artifacts.d.ts.map +1 -0
- package/lib/types/backend.d.ts +94 -0
- package/lib/types/backend.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +78 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/config.d.ts +75 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/confirmations.d.ts +23 -0
- package/lib/types/confirmations.d.ts.map +1 -0
- package/lib/types/diff.d.ts +5 -0
- package/lib/types/diff.d.ts.map +1 -0
- package/lib/types/errors.d.ts +15 -0
- package/lib/types/errors.d.ts.map +1 -0
- package/lib/types/exposure.d.ts +30 -0
- package/lib/types/exposure.d.ts.map +1 -0
- package/lib/types/index.d.ts +21 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/leases.d.ts +80 -0
- package/lib/types/leases.d.ts.map +1 -0
- package/lib/types/providers/macos.d.ts +15 -0
- package/lib/types/providers/macos.d.ts.map +1 -0
- package/lib/types/providers/native-helper.d.ts +36 -0
- package/lib/types/providers/native-helper.d.ts.map +1 -0
- package/lib/types/service.d.ts +52 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/skill.d.ts +9 -0
- package/lib/types/skill.d.ts.map +1 -0
- package/lib/types/target-resolver.d.ts +38 -0
- package/lib/types/target-resolver.d.ts.map +1 -0
- package/lib/types/tools.d.ts +6 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/types.d.ts +246 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/web.d.ts +32 -0
- package/lib/types/web.d.ts.map +1 -0
- package/lib/types.js +9 -0
- package/lib/types.js.map +1 -0
- package/lib/web.js +163 -0
- package/lib/web.js.map +1 -0
- package/native/macos/Sources/Fixture/main.swift +366 -0
- package/native/macos/Sources/Helper/CursorImage.swift +11 -0
- package/native/macos/Sources/Helper/CursorOverlay.swift +488 -0
- package/native/macos/Sources/Helper/TargetedPointer.swift +314 -0
- package/native/macos/Sources/Helper/main.swift +1140 -0
- package/native/macos/Sources/Monitor/main.swift +139 -0
- package/native/macos/bin/dsh-computer-use-helper +0 -0
- package/native/macos/manifest.json +14 -0
- package/package.json +128 -0
- package/scripts/build-client.mjs +24 -0
- package/scripts/build-native.mjs +158 -0
- package/scripts/check-native.mjs +92 -0
- package/scripts/model-e2e.mjs +291 -0
- package/scripts/session-transcript.mjs +117 -0
- package/scripts/validate.mjs +256 -0
- package/src/approval-policy.ts +16 -0
- package/src/artifacts.ts +93 -0
- package/src/backend.ts +103 -0
- package/src/client/index.tsx +539 -0
- package/src/config.ts +196 -0
- package/src/confirmations.ts +128 -0
- package/src/diff.ts +52 -0
- package/src/errors.ts +39 -0
- package/src/exposure.ts +236 -0
- package/src/index.ts +66 -0
- package/src/leases.ts +337 -0
- package/src/providers/macos.ts +185 -0
- package/src/providers/native-helper.ts +393 -0
- package/src/service.ts +583 -0
- package/src/skill.ts +123 -0
- package/src/target-resolver.ts +237 -0
- package/src/tools.ts +528 -0
- package/src/types.ts +299 -0
- package/src/web.ts +180 -0
package/src/service.ts
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/** Provider-independent Computer Use Service: leases, observations, staleness, confirmations, and fresh post-action state. */
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
5
|
+
import { Context, Service } from '@deepseek-ai/cordis'
|
|
6
|
+
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
7
|
+
import type { BackendCursorAction, BackendObservation, ComputerUseBackend } from './backend.ts'
|
|
8
|
+
import { allocateScreenshotPath, describeScreenshot } from './artifacts.ts'
|
|
9
|
+
import type { ResolvedComputerUseConfig } from './config.ts'
|
|
10
|
+
import { ComputerConfirmationManager } from './confirmations.ts'
|
|
11
|
+
import { diffElements } from './diff.ts'
|
|
12
|
+
import { ComputerUseError, computerUseError } from './errors.ts'
|
|
13
|
+
import { ComputerLeaseManager } from './leases.ts'
|
|
14
|
+
import {
|
|
15
|
+
describeComputerTarget,
|
|
16
|
+
resolveComputerTarget,
|
|
17
|
+
type ComputerTargetDescriptor,
|
|
18
|
+
} from './target-resolver.ts'
|
|
19
|
+
import {
|
|
20
|
+
ComputerObservationId,
|
|
21
|
+
ComputerTargetHandle,
|
|
22
|
+
type ComputerActionRequest,
|
|
23
|
+
type ComputerActionResult,
|
|
24
|
+
type ComputerAppIdentity,
|
|
25
|
+
type ComputerAppSummary,
|
|
26
|
+
type ComputerConfirmRequest,
|
|
27
|
+
type ComputerConfirmation,
|
|
28
|
+
type ComputerElement,
|
|
29
|
+
type ComputerObservation,
|
|
30
|
+
type ComputerObserveRequest,
|
|
31
|
+
type ComputerTargetResolutionResult,
|
|
32
|
+
type ComputerUseContext,
|
|
33
|
+
type ComputerUseStatus,
|
|
34
|
+
} from './types.ts'
|
|
35
|
+
|
|
36
|
+
declare module '@deepseek-ai/cordis' {
|
|
37
|
+
interface Context {
|
|
38
|
+
computerUse: ComputerUseService
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface StoredObservation {
|
|
43
|
+
public: ComputerObservation
|
|
44
|
+
backend: BackendObservation
|
|
45
|
+
targets: Map<ComputerTargetHandle, ComputerTargetDescriptor>
|
|
46
|
+
generation: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface AgentState {
|
|
50
|
+
observations: Map<ComputerObservationId, StoredObservation>
|
|
51
|
+
latestByApp: Map<string, ComputerObservationId>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function publicElements(observation: BackendObservation): {
|
|
55
|
+
elements: ComputerElement[]
|
|
56
|
+
targets: Map<ComputerTargetHandle, ComputerTargetDescriptor>
|
|
57
|
+
} {
|
|
58
|
+
const targets = new Map<ComputerTargetHandle, ComputerTargetDescriptor>()
|
|
59
|
+
const elements = observation.elements.map((backendElement) => {
|
|
60
|
+
const { locator: _locator, nativeIdentifier: _nativeIdentifier, ...element } = backendElement
|
|
61
|
+
const targetHandle = ComputerTargetHandle(randomUUID())
|
|
62
|
+
targets.set(targetHandle, describeComputerTarget(backendElement, observation))
|
|
63
|
+
return { ...element, targetHandle }
|
|
64
|
+
})
|
|
65
|
+
return { elements, targets }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function matchesWait(observation: BackendObservation, action: Extract<ComputerActionRequest, { kind: 'wait' }>): boolean {
|
|
69
|
+
const condition = action.condition
|
|
70
|
+
if (condition.text !== undefined && !observation.treeText.toLocaleLowerCase().includes(condition.text.toLocaleLowerCase())) return false
|
|
71
|
+
if (condition.elementRole !== undefined && !observation.elements.some(element => element.role === condition.elementRole)) return false
|
|
72
|
+
if (condition.elementTitle !== undefined && !observation.elements.some(element => element.title === condition.elementTitle || element.label === condition.elementTitle)) return false
|
|
73
|
+
return condition.text !== undefined || condition.elementRole !== undefined || condition.elementTitle !== undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function targetIndex(action: ComputerActionRequest): number | undefined {
|
|
77
|
+
switch (action.kind) {
|
|
78
|
+
case 'click':
|
|
79
|
+
case 'scroll': return action.elementIndex
|
|
80
|
+
case 'set-value':
|
|
81
|
+
case 'perform-action': return action.elementIndex
|
|
82
|
+
case 'type-text':
|
|
83
|
+
case 'press-key':
|
|
84
|
+
case 'drag':
|
|
85
|
+
case 'wait': return undefined
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function targetHandle(action: ComputerActionRequest): ComputerTargetHandle | undefined {
|
|
90
|
+
switch (action.kind) {
|
|
91
|
+
case 'click':
|
|
92
|
+
case 'scroll':
|
|
93
|
+
case 'set-value':
|
|
94
|
+
case 'perform-action': return action.targetHandle
|
|
95
|
+
case 'type-text':
|
|
96
|
+
case 'press-key':
|
|
97
|
+
case 'drag':
|
|
98
|
+
case 'wait': return undefined
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function allowsTargetRebind(action: ComputerActionRequest): boolean {
|
|
103
|
+
switch (action.kind) {
|
|
104
|
+
case 'click':
|
|
105
|
+
case 'scroll':
|
|
106
|
+
case 'set-value':
|
|
107
|
+
case 'perform-action': return action.allowRebind === true
|
|
108
|
+
case 'type-text':
|
|
109
|
+
case 'press-key':
|
|
110
|
+
case 'drag':
|
|
111
|
+
case 'wait': return false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function requiresElement(action: ComputerActionRequest): boolean {
|
|
116
|
+
return action.kind === 'set-value' || action.kind === 'perform-action'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function requiresPointerInput(
|
|
120
|
+
action: Exclude<ComputerActionRequest, { kind: 'wait' }>,
|
|
121
|
+
element: BackendObservation['elements'][number] | undefined,
|
|
122
|
+
): boolean {
|
|
123
|
+
switch (action.kind) {
|
|
124
|
+
case 'click':
|
|
125
|
+
if (action.x !== undefined || action.y !== undefined) return true
|
|
126
|
+
return element !== undefined
|
|
127
|
+
&& !element.actions.includes('AXPress')
|
|
128
|
+
&& action.allowCoordinateFallback === true
|
|
129
|
+
case 'scroll':
|
|
130
|
+
case 'drag': return true
|
|
131
|
+
case 'set-value':
|
|
132
|
+
case 'type-text':
|
|
133
|
+
case 'press-key':
|
|
134
|
+
case 'perform-action': return false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function requiresForegroundPermission(action: Exclude<ComputerActionRequest, { kind: 'wait' }>): boolean {
|
|
139
|
+
return action.kind === 'perform-action' && action.action === 'AXRaise'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function cursorAction(
|
|
143
|
+
action: Exclude<ComputerActionRequest, { kind: 'wait' }>,
|
|
144
|
+
element: BackendObservation['elements'][number] | undefined,
|
|
145
|
+
window: BackendObservation['window'] | undefined,
|
|
146
|
+
app: BackendObservation['app'],
|
|
147
|
+
): BackendCursorAction | undefined {
|
|
148
|
+
if (window?.id === undefined) return undefined
|
|
149
|
+
const target = {
|
|
150
|
+
targetPid: app.pid,
|
|
151
|
+
targetWindowNumber: window.id,
|
|
152
|
+
targetWindowFrame: { ...window.frame },
|
|
153
|
+
}
|
|
154
|
+
const elementPoint = element?.frame === undefined
|
|
155
|
+
? undefined
|
|
156
|
+
: {
|
|
157
|
+
x: element.frame.x + element.frame.width / 2,
|
|
158
|
+
y: element.frame.y + element.frame.height / 2,
|
|
159
|
+
}
|
|
160
|
+
const coordinateSpace = action.kind === 'click' || action.kind === 'scroll' || action.kind === 'drag'
|
|
161
|
+
? action.coordinateSpace
|
|
162
|
+
: undefined
|
|
163
|
+
const windowPoint = (x: number | undefined, y: number | undefined): { x: number; y: number } | undefined => {
|
|
164
|
+
if (x === undefined || y === undefined || window === undefined) return undefined
|
|
165
|
+
return coordinateSpace === 'screen' ? { x, y } : { x: window.frame.x + x, y: window.frame.y + y }
|
|
166
|
+
}
|
|
167
|
+
switch (action.kind) {
|
|
168
|
+
case 'click':
|
|
169
|
+
case 'scroll': {
|
|
170
|
+
const point = elementPoint ?? windowPoint(action.x, action.y)
|
|
171
|
+
return point === undefined ? undefined : { kind: action.kind, to: point, ...target }
|
|
172
|
+
}
|
|
173
|
+
case 'drag': {
|
|
174
|
+
const from = windowPoint(action.fromX, action.fromY)
|
|
175
|
+
const to = windowPoint(action.toX, action.toY)
|
|
176
|
+
return from === undefined || to === undefined ? undefined : { kind: 'drag', from, to, ...target }
|
|
177
|
+
}
|
|
178
|
+
case 'set-value':
|
|
179
|
+
case 'type-text':
|
|
180
|
+
case 'press-key':
|
|
181
|
+
case 'perform-action': return undefined
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Complete Service Definition plus provider-independent implementation. */
|
|
186
|
+
export class ComputerUseService extends Service {
|
|
187
|
+
private backend: ComputerUseBackend
|
|
188
|
+
private config: ResolvedComputerUseConfig
|
|
189
|
+
private generation = 1
|
|
190
|
+
private readonly agents = new Map<Agent, AgentState>()
|
|
191
|
+
private readonly leases: ComputerLeaseManager
|
|
192
|
+
private readonly confirmations: ComputerConfirmationManager
|
|
193
|
+
private readonly lifecycle = new AbortController()
|
|
194
|
+
private healthState: Omit<ComputerUseStatus, 'platform' | 'provider' | 'generation' | 'helperPath'> = {
|
|
195
|
+
ready: false,
|
|
196
|
+
accessibility: 'unavailable',
|
|
197
|
+
screenRecording: 'unavailable',
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Register `ctx.computerUse` using one validated backend and configuration generation. */
|
|
201
|
+
constructor(ctx: Context, backend: ComputerUseBackend, config: ResolvedComputerUseConfig) {
|
|
202
|
+
super(ctx, 'computerUse')
|
|
203
|
+
this.backend = backend
|
|
204
|
+
this.config = config
|
|
205
|
+
this.leases = new ComputerLeaseManager(ctx, () => this.config)
|
|
206
|
+
this.confirmations = new ComputerConfirmationManager(ctx, () => this.config)
|
|
207
|
+
ctx.effect(() => async () => {
|
|
208
|
+
this.lifecycle.abort()
|
|
209
|
+
this.clearState()
|
|
210
|
+
await this.backend.dispose()
|
|
211
|
+
}, 'dsh-computer-use: service lifecycle')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Verify the active backend before consumers become injectable. */
|
|
215
|
+
protected async initialize(): Promise<void> {
|
|
216
|
+
try {
|
|
217
|
+
await this.leases.initialize()
|
|
218
|
+
const health = await this.backend.health(this.lifecycle.signal)
|
|
219
|
+
this.healthState = { ready: true, ...health }
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const failure = computerUseError(error, 'Computer Use provider initialization failed')
|
|
222
|
+
this.healthState = {
|
|
223
|
+
ready: false,
|
|
224
|
+
accessibility: 'unavailable',
|
|
225
|
+
screenRecording: 'unavailable',
|
|
226
|
+
lastError: failure.message,
|
|
227
|
+
}
|
|
228
|
+
throw failure
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Replace the backend/config generation after a validated live Settings update. */
|
|
233
|
+
protected async reconfigure(backend: ComputerUseBackend, config: ResolvedComputerUseConfig): Promise<void> {
|
|
234
|
+
const health = await backend.health(this.lifecycle.signal)
|
|
235
|
+
const previous = this.backend
|
|
236
|
+
this.backend = backend
|
|
237
|
+
this.config = config
|
|
238
|
+
this.generation += 1
|
|
239
|
+
this.clearState()
|
|
240
|
+
this.healthState = { ready: true, ...health }
|
|
241
|
+
await previous.dispose()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Current provider and permission diagnostics. */
|
|
245
|
+
status(): ComputerUseStatus {
|
|
246
|
+
return {
|
|
247
|
+
platform: process.platform,
|
|
248
|
+
provider: 'macos-ax',
|
|
249
|
+
generation: this.generation,
|
|
250
|
+
helperPath: this.backend.helperPath,
|
|
251
|
+
...this.healthState,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Re-run non-mutating provider health checks. */
|
|
256
|
+
async health(signal: AbortSignal): Promise<ComputerUseStatus> {
|
|
257
|
+
try {
|
|
258
|
+
const health = await this.backend.health(AbortSignal.any([signal, this.lifecycle.signal]))
|
|
259
|
+
this.healthState = { ready: true, ...health }
|
|
260
|
+
} catch (error) {
|
|
261
|
+
const failure = computerUseError(error, 'Computer Use health check failed')
|
|
262
|
+
this.healthState = { ...this.healthState, ready: false, lastError: failure.message }
|
|
263
|
+
throw failure
|
|
264
|
+
}
|
|
265
|
+
return this.status()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Open the exact macOS privacy pane after an explicit Settings-page action. */
|
|
269
|
+
async openPermissionSettings(kind: 'accessibility' | 'screen-recording', signal: AbortSignal): Promise<void> {
|
|
270
|
+
await this.backend.openSettings(kind, AbortSignal.any([signal, this.lifecycle.signal]))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** List bounded running applications without inspecting their UI contents. */
|
|
274
|
+
async listApps(context: ComputerUseContext): Promise<ComputerAppSummary[]> {
|
|
275
|
+
return await this.backend.listApps(AbortSignal.any([context.signal, this.lifecycle.signal]))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Obtain a fresh, scoped observation after enforcing the app read lease. */
|
|
279
|
+
async observe(request: ComputerObserveRequest, context: ComputerUseContext): Promise<ComputerObservation> {
|
|
280
|
+
const signal = AbortSignal.any([context.signal, this.lifecycle.signal])
|
|
281
|
+
const app = await this.backend.resolveApp(request.app, signal)
|
|
282
|
+
await this.leases.ensure(context.agent, app, 'read', 'computer_observe', context.callId, signal)
|
|
283
|
+
return await this.capture(app, request, context, 'computer_observe')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Ask for a one-use token bound to an exact proposed sensitive action. */
|
|
287
|
+
async confirm(request: ComputerConfirmRequest, context: ComputerUseContext): Promise<ComputerConfirmation> {
|
|
288
|
+
const stored = this.requireObservation(request.action.observationId, context.agent)
|
|
289
|
+
return await this.confirmations.confirm(
|
|
290
|
+
context.agent,
|
|
291
|
+
stored.backend.app,
|
|
292
|
+
request,
|
|
293
|
+
context.callId,
|
|
294
|
+
AbortSignal.any([context.signal, this.lifecycle.signal]),
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Execute one observation-bound action and always return a fresh post-action observation. */
|
|
299
|
+
async act(action: ComputerActionRequest, context: ComputerUseContext): Promise<ComputerActionResult> {
|
|
300
|
+
const signal = AbortSignal.any([context.signal, this.lifecycle.signal])
|
|
301
|
+
const stored = this.requireObservation(action.observationId, context.agent)
|
|
302
|
+
if (action.kind === 'wait') return await this.wait(stored, action, context, signal)
|
|
303
|
+
const index = targetIndex(action)
|
|
304
|
+
const handle = targetHandle(action)
|
|
305
|
+
const originalElement = index === undefined ? undefined : stored.backend.elements.find(candidate => candidate.index === index)
|
|
306
|
+
if (index !== undefined && originalElement === undefined) {
|
|
307
|
+
throw new ComputerUseError('COMPUTER_ELEMENT_UNAVAILABLE', `element ${index} is not part of observation ${String(action.observationId)}`)
|
|
308
|
+
}
|
|
309
|
+
if (allowsTargetRebind(action) && handle === undefined) {
|
|
310
|
+
throw new ComputerUseError('COMPUTER_TARGET_UNAVAILABLE', 'allowRebind requires a targetHandle from the referenced observation')
|
|
311
|
+
}
|
|
312
|
+
const descriptor = handle === undefined ? undefined : stored.targets.get(handle)
|
|
313
|
+
if (handle !== undefined && descriptor === undefined) {
|
|
314
|
+
throw new ComputerUseError('COMPUTER_TARGET_UNAVAILABLE', 'targetHandle is unknown or does not belong to the referenced observation')
|
|
315
|
+
}
|
|
316
|
+
if (descriptor !== undefined && index !== undefined && (descriptor.locator.length !== originalElement?.locator.length
|
|
317
|
+
|| !descriptor.locator.every((part, position) => part === originalElement.locator[position]))) {
|
|
318
|
+
throw new ComputerUseError('COMPUTER_TARGET_UNAVAILABLE', 'elementIndex and targetHandle select different elements')
|
|
319
|
+
}
|
|
320
|
+
const selectedOriginalElement = originalElement ?? (descriptor === undefined
|
|
321
|
+
? undefined
|
|
322
|
+
: stored.backend.elements.find(candidate => candidate.locator.length === descriptor.locator.length
|
|
323
|
+
&& candidate.locator.every((part, position) => part === descriptor.locator[position])))
|
|
324
|
+
if (descriptor !== undefined && selectedOriginalElement === undefined) {
|
|
325
|
+
throw new ComputerUseError('COMPUTER_TARGET_UNAVAILABLE', 'targetHandle no longer has provider evidence in the referenced observation')
|
|
326
|
+
}
|
|
327
|
+
if (requiresElement(action) && selectedOriginalElement === undefined) {
|
|
328
|
+
throw new ComputerUseError('COMPUTER_ELEMENT_UNAVAILABLE', `${action.kind} requires elementIndex or targetHandle`)
|
|
329
|
+
}
|
|
330
|
+
if (requiresPointerInput(action, selectedOriginalElement) && this.config.interaction.pointerInputPolicy === 'deny') {
|
|
331
|
+
throw new ComputerUseError(
|
|
332
|
+
'COMPUTER_ACTION_BLOCKED',
|
|
333
|
+
`${action.kind} requires target-process pointer input, which interaction.pointerInputPolicy denies; use an Accessibility action or enable targeted pointer input in host Settings`,
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
if (requiresForegroundPermission(action) && this.config.interaction.focusPolicy === 'preserve') {
|
|
337
|
+
throw new ComputerUseError(
|
|
338
|
+
'COMPUTER_ACTION_BLOCKED',
|
|
339
|
+
'AXRaise may raise the target window, which interaction.focusPolicy preserve denies; enable explicit activation in host Settings before using this action',
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
await this.leases.ensure(context.agent, stored.backend.app, 'control', `computer_${action.kind}`, context.callId, signal)
|
|
343
|
+
let actionObservation = stored.backend
|
|
344
|
+
let element = selectedOriginalElement
|
|
345
|
+
let resolution: ComputerTargetResolutionResult | undefined = selectedOriginalElement === undefined
|
|
346
|
+
? undefined
|
|
347
|
+
: { mode: 'exact-locator', confidence: 1, candidateCount: 1, targetChanged: false }
|
|
348
|
+
if (descriptor !== undefined) {
|
|
349
|
+
const fresh = await this.backend.observe(stored.backend.app, {
|
|
350
|
+
screenshot: 'none',
|
|
351
|
+
maxNodes: this.config.maxNodes,
|
|
352
|
+
maxDepth: this.config.maxDepth,
|
|
353
|
+
maxTextBytes: this.config.maxTextBytes,
|
|
354
|
+
}, signal)
|
|
355
|
+
const resolved = resolveComputerTarget(stored.backend, fresh, descriptor, allowsTargetRebind(action))
|
|
356
|
+
actionObservation = resolved.observation
|
|
357
|
+
element = resolved.element
|
|
358
|
+
resolution = resolved.resolution
|
|
359
|
+
if (action.sensitive === true && resolution.targetChanged) {
|
|
360
|
+
this.confirmations.invalidate(context.agent, action.confirmationToken)
|
|
361
|
+
throw new ComputerUseError(
|
|
362
|
+
'COMPUTER_TARGET_REBIND_REQUIRES_CONFIRMATION',
|
|
363
|
+
'the sensitive target rebound to a fresh element; observe the current UI and request a new one-use confirmation before acting',
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
this.confirmations.consume(context.agent, stored.backend.app, action)
|
|
368
|
+
const visualization = cursorAction(action, element, actionObservation.window, actionObservation.app)
|
|
369
|
+
let cursorStarted = false
|
|
370
|
+
if (visualization !== undefined && this.config.interaction.cursorVisualization === 'visible') {
|
|
371
|
+
try {
|
|
372
|
+
await this.backend.visualizeCursor(visualization, 'before', signal)
|
|
373
|
+
cursorStarted = true
|
|
374
|
+
} catch {
|
|
375
|
+
// The overlay is presentation-only; native input remains authoritative.
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
let outcome
|
|
379
|
+
try {
|
|
380
|
+
outcome = await this.backend.act({
|
|
381
|
+
action,
|
|
382
|
+
app: actionObservation.app,
|
|
383
|
+
expectedStateHash: actionObservation.stateHash,
|
|
384
|
+
interaction: this.config.interaction,
|
|
385
|
+
...(element === undefined ? {} : { element }),
|
|
386
|
+
...(actionObservation.window === undefined ? {} : { window: actionObservation.window }),
|
|
387
|
+
}, signal)
|
|
388
|
+
} catch (error) {
|
|
389
|
+
throw computerUseError(error, `Computer Use ${action.kind} failed`)
|
|
390
|
+
} finally {
|
|
391
|
+
if (cursorStarted && visualization !== undefined) {
|
|
392
|
+
try {
|
|
393
|
+
await this.backend.visualizeCursor(visualization, 'after', signal)
|
|
394
|
+
} catch {
|
|
395
|
+
// The overlay is presentation-only; native input remains authoritative.
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const started = Date.now()
|
|
400
|
+
let latest: BackendObservation | undefined
|
|
401
|
+
do {
|
|
402
|
+
if (this.config.settleMs > 0) await delay(this.config.settleMs, undefined, { signal })
|
|
403
|
+
latest = await this.backend.observe(stored.backend.app, {
|
|
404
|
+
screenshot: 'none',
|
|
405
|
+
maxNodes: this.config.maxNodes,
|
|
406
|
+
maxDepth: this.config.maxDepth,
|
|
407
|
+
maxTextBytes: this.config.maxTextBytes,
|
|
408
|
+
}, signal)
|
|
409
|
+
if (latest.stateHash !== actionObservation.stateHash) break
|
|
410
|
+
} while (Date.now() - started < this.config.maxSettleMs)
|
|
411
|
+
const observation = await this.capture(
|
|
412
|
+
stored.backend.app,
|
|
413
|
+
{ app: { bundleId: stored.backend.app.bundleId, pid: stored.backend.app.pid }, screenshot: stored.public.screenshot === undefined ? 'none' : 'optional' },
|
|
414
|
+
context,
|
|
415
|
+
'computer_action',
|
|
416
|
+
latest,
|
|
417
|
+
)
|
|
418
|
+
return {
|
|
419
|
+
action: action.kind,
|
|
420
|
+
channel: outcome.channel,
|
|
421
|
+
activation: outcome.activation,
|
|
422
|
+
pointerInput: outcome.pointerInput,
|
|
423
|
+
pointerRouting: outcome.pointerRouting,
|
|
424
|
+
...(resolution === undefined ? {} : { resolution }),
|
|
425
|
+
observation,
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Release all scoped observations and confirmations for one disposed Agent. */
|
|
430
|
+
releaseAgent(agent: Agent): void {
|
|
431
|
+
this.agents.delete(agent)
|
|
432
|
+
this.leases.releaseAgent(agent)
|
|
433
|
+
this.confirmations.releaseAgent(agent)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private state(agent: Agent): AgentState {
|
|
437
|
+
let state = this.agents.get(agent)
|
|
438
|
+
if (state === undefined) {
|
|
439
|
+
state = { observations: new Map(), latestByApp: new Map() }
|
|
440
|
+
this.agents.set(agent, state)
|
|
441
|
+
}
|
|
442
|
+
return state
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private requireObservation(id: ComputerObservationId, agent: Agent): StoredObservation {
|
|
446
|
+
this.prune(agent)
|
|
447
|
+
const stored = this.state(agent).observations.get(id)
|
|
448
|
+
if (stored === undefined || stored.generation !== this.generation) {
|
|
449
|
+
throw new ComputerUseError('COMPUTER_STALE_OBSERVATION', `observation ${String(id)} is unknown, expired, or belongs to another provider generation`)
|
|
450
|
+
}
|
|
451
|
+
return stored
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private prune(agent: Agent): void {
|
|
455
|
+
const state = this.agents.get(agent)
|
|
456
|
+
if (state === undefined) return
|
|
457
|
+
const now = Date.now()
|
|
458
|
+
for (const [id, stored] of state.observations) {
|
|
459
|
+
if (Date.parse(stored.public.expiresAt) <= now || stored.generation !== this.generation) state.observations.delete(id)
|
|
460
|
+
}
|
|
461
|
+
for (const [app, id] of state.latestByApp) {
|
|
462
|
+
if (!state.observations.has(id)) state.latestByApp.delete(app)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private async capture(
|
|
467
|
+
app: ComputerAppIdentity,
|
|
468
|
+
request: ComputerObserveRequest,
|
|
469
|
+
context: ComputerUseContext,
|
|
470
|
+
sourceTool: 'computer_observe' | 'computer_action',
|
|
471
|
+
preObserved?: BackendObservation,
|
|
472
|
+
): Promise<ComputerObservation> {
|
|
473
|
+
const signal = AbortSignal.any([context.signal, this.lifecycle.signal])
|
|
474
|
+
const screenshot = request.screenshot ?? 'optional'
|
|
475
|
+
const screenshotPath = screenshot === 'none'
|
|
476
|
+
? undefined
|
|
477
|
+
: await allocateScreenshotPath(context.workspace, this.config.artifactRoot, context.agent.session.id)
|
|
478
|
+
const backend = preObserved !== undefined && screenshot === 'none'
|
|
479
|
+
? preObserved
|
|
480
|
+
: await this.backend.observe(app, {
|
|
481
|
+
screenshot,
|
|
482
|
+
...(screenshotPath === undefined ? {} : { screenshotPath }),
|
|
483
|
+
maxNodes: this.config.maxNodes,
|
|
484
|
+
maxDepth: this.config.maxDepth,
|
|
485
|
+
maxTextBytes: this.config.maxTextBytes,
|
|
486
|
+
}, signal)
|
|
487
|
+
if (backend.app.bundleId !== app.bundleId || backend.app.pid !== app.pid) {
|
|
488
|
+
throw new ComputerUseError('COMPUTER_STALE_OBSERVATION', 'the selected application restarted or resolved to a different process')
|
|
489
|
+
}
|
|
490
|
+
const state = this.state(context.agent)
|
|
491
|
+
this.prune(context.agent)
|
|
492
|
+
const key = `${app.bundleId}:${app.pid}`
|
|
493
|
+
const previousId = state.latestByApp.get(key)
|
|
494
|
+
const previous = previousId === undefined ? undefined : state.observations.get(previousId)
|
|
495
|
+
const projected = publicElements(backend)
|
|
496
|
+
const elements = projected.elements
|
|
497
|
+
const full = request.full === true || previous === undefined
|
|
498
|
+
const createdAt = Date.now()
|
|
499
|
+
const observationId = ComputerObservationId(randomUUID())
|
|
500
|
+
const artifact = backend.screenshot === undefined
|
|
501
|
+
? undefined
|
|
502
|
+
: await describeScreenshot(
|
|
503
|
+
backend.screenshot.path,
|
|
504
|
+
backend.screenshot.width,
|
|
505
|
+
backend.screenshot.height,
|
|
506
|
+
this.config.maxScreenshotBytes,
|
|
507
|
+
sourceTool,
|
|
508
|
+
)
|
|
509
|
+
const observation: ComputerObservation = {
|
|
510
|
+
observationId,
|
|
511
|
+
app: backend.app,
|
|
512
|
+
createdAt: new Date(createdAt).toISOString(),
|
|
513
|
+
expiresAt: this.config.observationTtlMs === 0
|
|
514
|
+
? '9999-12-31T23:59:59.999Z'
|
|
515
|
+
: new Date(createdAt + this.config.observationTtlMs).toISOString(),
|
|
516
|
+
frontmost: backend.frontmost,
|
|
517
|
+
...(backend.window === undefined ? {} : { window: backend.window }),
|
|
518
|
+
tree: {
|
|
519
|
+
mode: full ? 'full' : 'diff',
|
|
520
|
+
text: full ? backend.treeText : diffElements(previous.public.elements, elements, this.config.maxTextBytes),
|
|
521
|
+
truncated: backend.truncated,
|
|
522
|
+
},
|
|
523
|
+
elements,
|
|
524
|
+
...(artifact === undefined ? {} : { screenshot: artifact }),
|
|
525
|
+
permissions: backend.permissions,
|
|
526
|
+
}
|
|
527
|
+
state.observations.set(observationId, { public: observation, backend, targets: projected.targets, generation: this.generation })
|
|
528
|
+
state.latestByApp.set(key, observationId)
|
|
529
|
+
while (state.observations.size > 64) {
|
|
530
|
+
const oldest = state.observations.keys().next().value as ComputerObservationId | undefined
|
|
531
|
+
if (oldest === undefined) break
|
|
532
|
+
state.observations.delete(oldest)
|
|
533
|
+
}
|
|
534
|
+
return observation
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private async wait(
|
|
538
|
+
stored: StoredObservation,
|
|
539
|
+
action: Extract<ComputerActionRequest, { kind: 'wait' }>,
|
|
540
|
+
context: ComputerUseContext,
|
|
541
|
+
signal: AbortSignal,
|
|
542
|
+
): Promise<ComputerActionResult> {
|
|
543
|
+
await this.leases.ensure(context.agent, stored.backend.app, 'read', 'computer_wait', context.callId, signal)
|
|
544
|
+
const timeoutMs = action.timeoutMs ?? this.config.maxSettleMs
|
|
545
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > this.config.maxSettleMs) {
|
|
546
|
+
throw new ComputerUseError('COMPUTER_TIMEOUT', `wait timeout must be between 100 and ${this.config.maxSettleMs} milliseconds`)
|
|
547
|
+
}
|
|
548
|
+
const deadline = Date.now() + timeoutMs
|
|
549
|
+
let latest = stored.backend
|
|
550
|
+
while (!matchesWait(latest, action)) {
|
|
551
|
+
if (Date.now() >= deadline) throw new ComputerUseError('COMPUTER_TIMEOUT', 'wait condition was not met before the configured deadline')
|
|
552
|
+
await delay(Math.min(this.config.settleMs || 100, Math.max(1, deadline - Date.now())), undefined, { signal })
|
|
553
|
+
latest = await this.backend.observe(stored.backend.app, {
|
|
554
|
+
screenshot: 'none',
|
|
555
|
+
maxNodes: this.config.maxNodes,
|
|
556
|
+
maxDepth: this.config.maxDepth,
|
|
557
|
+
maxTextBytes: this.config.maxTextBytes,
|
|
558
|
+
}, signal)
|
|
559
|
+
}
|
|
560
|
+
const observation = await this.capture(
|
|
561
|
+
stored.backend.app,
|
|
562
|
+
{ app: { bundleId: stored.backend.app.bundleId, pid: stored.backend.app.pid }, screenshot: stored.public.screenshot === undefined ? 'none' : 'optional' },
|
|
563
|
+
context,
|
|
564
|
+
'computer_action',
|
|
565
|
+
latest,
|
|
566
|
+
)
|
|
567
|
+
return {
|
|
568
|
+
action: 'wait',
|
|
569
|
+
channel: 'wait',
|
|
570
|
+
activation: 'not-requested',
|
|
571
|
+
pointerInput: false,
|
|
572
|
+
pointerRouting: 'none',
|
|
573
|
+
observation,
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private clearState(): void {
|
|
578
|
+
this.agents.clear()
|
|
579
|
+
this.confirmations.clear()
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export default ComputerUseService
|