@myagentroam/node 0.11.37 → 0.11.39

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.
@@ -0,0 +1,334 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ComputerUseError } from './contracts.js';
3
+ import { PythonWorker } from './worker.js';
4
+ export class ComputerUseDesktopLeaseService {
5
+ #holder;
6
+ acquire(runId) {
7
+ if (this.#holder?.runId === runId)
8
+ return this.#holder;
9
+ if (this.#holder !== undefined)
10
+ throw new ComputerUseError('DESKTOP_BUSY', `The desktop is in use by another Run since ${new Date(this.#holder.acquiredAt).toISOString()}.`);
11
+ this.#holder = { runId, leaseId: randomUUID(), acquiredAt: Date.now() };
12
+ return this.#holder;
13
+ }
14
+ release(runId) {
15
+ if (this.#holder?.runId === runId)
16
+ this.#holder = undefined;
17
+ }
18
+ holder() {
19
+ return this.#holder;
20
+ }
21
+ }
22
+ export class ComputerUseSessionResources {
23
+ runtimeExecutable;
24
+ lease;
25
+ showControlIndicator;
26
+ validateDesktop;
27
+ #worker;
28
+ #observationId;
29
+ #runtimeGeneration = 1;
30
+ #runId;
31
+ #closed = false;
32
+ #bindingClose;
33
+ #closePromise;
34
+ #cleanupError;
35
+ #task;
36
+ #bindingStop = new AbortController();
37
+ #unsubscribeExecution;
38
+ constructor(runtimeExecutable, lease, showControlIndicator, validateDesktop) {
39
+ this.runtimeExecutable = runtimeExecutable;
40
+ this.lease = lease;
41
+ this.showControlIndicator = showControlIndicator;
42
+ this.validateDesktop = validateDesktop;
43
+ }
44
+ attach(runId, signal) {
45
+ if (this.#cleanupError !== undefined)
46
+ throw this.#cleanupError;
47
+ if (this.#closed)
48
+ throw new ComputerUseError('COMPUTER_UNAVAILABLE', 'Computer Use resources are closed.');
49
+ if (this.#runId !== undefined)
50
+ throw new ComputerUseError('COMPUTER_SESSION_BUSY', 'Computer Use Session resources are already attached to another Run.');
51
+ this.#runId = runId;
52
+ this.#observationId = undefined;
53
+ this.#task = undefined;
54
+ this.#bindingStop = new AbortController();
55
+ const abort = () => {
56
+ this.#bindingStop.abort();
57
+ if (this.#task?.settled === undefined)
58
+ this.#task?.stop.abort();
59
+ };
60
+ signal?.addEventListener('abort', abort, { once: true });
61
+ this.#unsubscribeExecution = () => signal?.removeEventListener('abort', abort);
62
+ if (signal?.aborted)
63
+ abort();
64
+ }
65
+ async detach(runId) {
66
+ if (this.#runId !== runId)
67
+ return;
68
+ this.#bindingStop.abort();
69
+ this.#unsubscribeExecution?.();
70
+ this.#unsubscribeExecution = undefined;
71
+ if (this.#task?.settled === undefined)
72
+ this.#task?.stop.abort();
73
+ await this.#task?.completion;
74
+ if (this.#cleanupError !== undefined)
75
+ throw this.#cleanupError;
76
+ if (this.#closed)
77
+ return;
78
+ this.#observationId = undefined;
79
+ this.#task = undefined;
80
+ this.lease.release(runId);
81
+ this.#runId = undefined;
82
+ this.#bindingClose = undefined;
83
+ }
84
+ setBindingClose(close) {
85
+ this.#bindingClose = close;
86
+ }
87
+ async call(input, signal) {
88
+ const runId = this.#assertBound();
89
+ if (signal.aborted)
90
+ throw new ComputerUseError('COMPUTER_UNAVAILABLE', 'Computer Use call was cancelled.');
91
+ if (this.#cleanupError !== undefined)
92
+ throw this.#cleanupError;
93
+ if (this.#task !== undefined && this.#task.settled === undefined)
94
+ throw new ComputerUseError('COMPUTER_SESSION_BUSY', 'Computer Use is already executing.');
95
+ if (input.basedOnObservationId !== undefined &&
96
+ input.basedOnObservationId !== this.#observationId)
97
+ throw new ComputerUseError('COMPUTER_OBSERVATION_STALE', 'The desktop observation is stale; observe again before acting.');
98
+ this.lease.acquire(runId);
99
+ this.#observationId = undefined;
100
+ const stop = new AbortController();
101
+ const abort = () => stop.abort();
102
+ signal.addEventListener('abort', abort, { once: true });
103
+ const task = {
104
+ id: `cu_${randomUUID()}`,
105
+ stop,
106
+ // Admission is synchronous; preparation and every failure belong to this slot.
107
+ completion: Promise.resolve()
108
+ .then(() => this.#execute(input, stop.signal, runId))
109
+ .then((result) => (task.settled = { result }), (error) => (task.settled = { error }))
110
+ };
111
+ this.#task = task;
112
+ try {
113
+ return await this.#waitTask(task, input.yieldMs);
114
+ }
115
+ finally {
116
+ signal.removeEventListener('abort', abort);
117
+ }
118
+ }
119
+ async wait(input, signal) {
120
+ this.#assertBound();
121
+ if (signal.aborted)
122
+ throw waitCancelled();
123
+ const task = this.#task;
124
+ if (task === undefined || task.id !== input.taskId)
125
+ throw new ComputerUseError('COMPUTER_TASK_NOT_FOUND', 'The task is not available in this binding.');
126
+ if (input.action === 'cancel' && task.settled === undefined)
127
+ task.stop.abort();
128
+ return this.#waitTask(task, input.yieldMs, signal);
129
+ }
130
+ #assertBound() {
131
+ if (this.#closed || this.#runId === undefined || this.#bindingStop.signal.aborted)
132
+ throw new ComputerUseError('COMPUTER_UNAVAILABLE', 'Computer Use binding is closed.');
133
+ return this.#runId;
134
+ }
135
+ async #waitTask(task, yieldMs, signal) {
136
+ let timer;
137
+ let onAbort;
138
+ try {
139
+ if (task.settled === undefined && yieldMs !== 0) {
140
+ const waits = [task.completion];
141
+ if (yieldMs !== undefined)
142
+ waits.push(new Promise((resolve) => {
143
+ timer = setTimeout(resolve, yieldMs);
144
+ }));
145
+ if (signal !== undefined)
146
+ waits.push(new Promise((_, reject) => {
147
+ onAbort = () => reject(waitCancelled());
148
+ signal.addEventListener('abort', onAbort, { once: true });
149
+ if (signal.aborted)
150
+ onAbort();
151
+ }));
152
+ await Promise.race(waits);
153
+ }
154
+ if (this.#task !== task)
155
+ throw new ComputerUseError('COMPUTER_TASK_NOT_FOUND', 'The task is no longer available in this binding.');
156
+ if (task.settled === undefined)
157
+ return { taskId: task.id, status: task.stop.signal.aborted ? 'STOPPING' : 'RUNNING' };
158
+ if ('error' in task.settled)
159
+ throw task.settled.error;
160
+ return { ...task.settled.result, taskId: task.id, status: 'COMPLETED' };
161
+ }
162
+ finally {
163
+ if (timer !== undefined)
164
+ clearTimeout(timer);
165
+ if (onAbort !== undefined)
166
+ signal?.removeEventListener('abort', onAbort);
167
+ }
168
+ }
169
+ async #execute(input, signal, runId) {
170
+ this.#assertActive(signal, runId);
171
+ await this.validateDesktop();
172
+ this.#assertActive(signal, runId);
173
+ let indicator;
174
+ try {
175
+ indicator = await this.showControlIndicator(this.runtimeExecutable);
176
+ }
177
+ catch (error) {
178
+ if (error instanceof ComputerUseError && error.code === 'COMPUTER_CLEANUP_FAILED') {
179
+ this.#cleanupError = error;
180
+ throw error;
181
+ }
182
+ throw new ComputerUseError('COMPUTER_CONTROL_INDICATOR_UNAVAILABLE', 'The desktop control indicator could not be displayed.', { cause: error });
183
+ }
184
+ let result;
185
+ let primaryError;
186
+ let failed = false;
187
+ try {
188
+ this.#assertActive(signal, runId);
189
+ const worker = await this.#ensureWorker();
190
+ this.#assertActive(signal, runId);
191
+ let executionStarted = false;
192
+ try {
193
+ indicator.assertActive?.();
194
+ executionStarted = true;
195
+ const execution = worker.execute(input.code, input.timeoutMs, signal);
196
+ const executionResult = await (indicator.lost === undefined
197
+ ? execution
198
+ : Promise.race([
199
+ execution,
200
+ indicator.lost.then((error) => {
201
+ throw error;
202
+ })
203
+ ]));
204
+ result = {
205
+ outcome: 'SUCCEEDED',
206
+ observationId: randomUUID(),
207
+ runtimeGeneration: this.#runtimeGeneration,
208
+ logs: executionResult.logs,
209
+ images: executionResult.images
210
+ };
211
+ }
212
+ catch (error) {
213
+ if (executionStarted && worker.startedRequest) {
214
+ try {
215
+ await this.#resetWorker();
216
+ }
217
+ catch {
218
+ // The worker owner records cleanup failure; the sent request remains uncertain.
219
+ }
220
+ this.#observationId = undefined;
221
+ const message = error instanceof Error ? error.message : 'Computer Use execution ended unexpectedly.';
222
+ result = {
223
+ message,
224
+ outcome: 'OUTCOME_UNKNOWN',
225
+ observationId: '',
226
+ runtimeGeneration: this.#runtimeGeneration,
227
+ logs: []
228
+ };
229
+ }
230
+ else
231
+ throw error;
232
+ }
233
+ }
234
+ catch (error) {
235
+ failed = true;
236
+ primaryError = error;
237
+ }
238
+ try {
239
+ await indicator.close();
240
+ }
241
+ catch (error) {
242
+ this.#observationId = undefined;
243
+ this.#cleanupError ??= new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use indicator cleanup failed.', { cause: error });
244
+ }
245
+ if (failed) {
246
+ if (this.#cleanupError !== undefined)
247
+ throw withCleanupFailure(primaryError, this.#cleanupError);
248
+ throw primaryError;
249
+ }
250
+ if (result === undefined)
251
+ throw new Error('Computer Use execution returned no result.');
252
+ if (this.#cleanupError !== undefined)
253
+ return { ...result, observationId: '', cleanupFailed: true };
254
+ this.#observationId = result.observationId || undefined;
255
+ return result;
256
+ }
257
+ close() {
258
+ if (this.#closePromise !== undefined)
259
+ return this.#closePromise;
260
+ this.#closed = true;
261
+ this.#observationId = undefined;
262
+ this.#closePromise = (async () => {
263
+ let failure;
264
+ try {
265
+ await this.#bindingClose?.();
266
+ }
267
+ catch (error) {
268
+ failure = error;
269
+ }
270
+ try {
271
+ await this.#resetWorker();
272
+ }
273
+ catch (error) {
274
+ failure ??= error;
275
+ }
276
+ if (failure !== undefined)
277
+ throw failure;
278
+ if (this.#runId !== undefined)
279
+ this.lease.release(this.#runId);
280
+ this.#runId = undefined;
281
+ this.#bindingClose = undefined;
282
+ this.#task = undefined;
283
+ })();
284
+ return this.#closePromise;
285
+ }
286
+ #assertActive(signal, runId) {
287
+ if (signal.aborted || this.#closed || this.#runId !== runId)
288
+ throw new ComputerUseError('COMPUTER_UNAVAILABLE', 'Computer Use binding is closed.');
289
+ }
290
+ async #ensureWorker() {
291
+ if (this.#worker !== undefined)
292
+ return this.#worker;
293
+ try {
294
+ return await PythonWorker.start(this.runtimeExecutable, (worker) => {
295
+ this.#worker = worker;
296
+ });
297
+ }
298
+ catch (error) {
299
+ if (error instanceof ComputerUseError && error.code === 'COMPUTER_CLEANUP_FAILED')
300
+ this.#cleanupError = error;
301
+ try {
302
+ await this.#resetWorker();
303
+ }
304
+ catch (cleanupError) {
305
+ throw withCleanupFailure(error, cleanupError);
306
+ }
307
+ throw error;
308
+ }
309
+ }
310
+ async #resetWorker() {
311
+ const worker = this.#worker;
312
+ if (worker !== undefined) {
313
+ try {
314
+ await worker.close();
315
+ }
316
+ catch (error) {
317
+ this.#cleanupError = new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use worker cleanup failed.', { cause: error });
318
+ throw this.#cleanupError;
319
+ }
320
+ this.#worker = undefined;
321
+ this.#runtimeGeneration++;
322
+ }
323
+ }
324
+ }
325
+ function waitCancelled() {
326
+ return new ComputerUseError('COMPUTER_WAIT_CANCELLED', 'Waiting was cancelled; use wait to inspect the task.');
327
+ }
328
+ function withCleanupFailure(primary, cleanup) {
329
+ const message = `${primary instanceof Error ? primary.message : 'Computer Use execution failed.'} Computer Use cleanup failed; further use is blocked.`;
330
+ const cause = new AggregateError([primary, cleanup], 'Computer Use execution and cleanup failed');
331
+ return primary instanceof ComputerUseError
332
+ ? new ComputerUseError(primary.code, message, { cause })
333
+ : new Error(message, { cause });
334
+ }
@@ -0,0 +1,135 @@
1
+ export const MAX_IMAGE_BYTES = 16 * 1024 * 1024;
2
+ export const MAX_IMAGES = 4;
3
+ export const PYTHON_WORKER_SOURCE = String.raw `
4
+ import base64
5
+ import contextlib
6
+ import io
7
+ import json
8
+ import sys
9
+ import time
10
+ import traceback
11
+
12
+ import pyautogui
13
+
14
+ namespace = {"pyautogui": pyautogui, "time": time}
15
+ current_logs = []
16
+ current_images = []
17
+
18
+ def _emit(message):
19
+ sys.__stdout__.write(json.dumps(message, ensure_ascii=False, separators=(",", ":")) + "\n")
20
+ sys.__stdout__.flush()
21
+
22
+ def log(value):
23
+ current_logs.append(str(value))
24
+
25
+ def _image_bytes(image):
26
+ if hasattr(image, "save"):
27
+ buffer = io.BytesIO()
28
+ image.save(buffer, format="PNG")
29
+ value = buffer.getvalue()
30
+ if len(value) > ${MAX_IMAGE_BYTES}:
31
+ raise ValueError("display image exceeds the size limit")
32
+ return value
33
+ if isinstance(image, (bytes, bytearray)):
34
+ value = bytes(image)
35
+ if len(value) > ${MAX_IMAGE_BYTES}:
36
+ raise ValueError("display image exceeds the size limit")
37
+ return value
38
+ raise TypeError("display expects a PIL image or PNG bytes")
39
+
40
+ def display(image):
41
+ if len(current_images) >= ${MAX_IMAGES}:
42
+ raise ValueError("too many displayed images")
43
+ current_images.append({
44
+ "mimeType": "image/png",
45
+ "dataBase64": base64.b64encode(_image_bytes(image)).decode("ascii")
46
+ })
47
+
48
+ def paste_text(text):
49
+ value = str(text)
50
+ try:
51
+ import pyperclip
52
+ previous = None
53
+ try:
54
+ previous = pyperclip.paste()
55
+ except Exception:
56
+ pass
57
+ with open("clipboard.json", "w", encoding="utf-8") as clipboard_file:
58
+ json.dump({"value": previous}, clipboard_file, ensure_ascii=False)
59
+ pyperclip.copy(value)
60
+ pyautogui.hotkey("command" if sys.platform == "darwin" else "ctrl", "v")
61
+ time.sleep(0.05)
62
+ if previous is not None:
63
+ pyperclip.copy(previous)
64
+ try:
65
+ import os
66
+ os.remove("clipboard.json")
67
+ except OSError:
68
+ pass
69
+ return
70
+ except Exception as error:
71
+ raise RuntimeError("Unicode clipboard paste is unavailable") from error
72
+
73
+ namespace.update({"display": display, "log": log, "paste_text": paste_text})
74
+ _emit({"type": "ready"})
75
+
76
+ for raw in sys.stdin:
77
+ request = None
78
+ try:
79
+ request = json.loads(raw)
80
+ request_id = request["id"]
81
+ code = request["code"]
82
+ current_logs = []
83
+ current_images = []
84
+ captured_stdout = io.StringIO()
85
+ captured_stderr = io.StringIO()
86
+ with contextlib.redirect_stdout(captured_stdout), contextlib.redirect_stderr(captured_stderr):
87
+ if code:
88
+ exec(compile(code, "<computer-use>", "exec"), namespace, namespace)
89
+ if not current_images:
90
+ display(pyautogui.screenshot())
91
+ stdout_value = captured_stdout.getvalue()
92
+ stderr_value = captured_stderr.getvalue()
93
+ if stdout_value:
94
+ current_logs.append(stdout_value.rstrip())
95
+ if stderr_value:
96
+ current_logs.append(stderr_value.rstrip())
97
+ _emit({
98
+ "type": "result",
99
+ "id": request_id,
100
+ "logs": current_logs,
101
+ "images": current_images
102
+ })
103
+ except BaseException:
104
+ _emit({
105
+ "type": "error",
106
+ "id": request.get("id") if isinstance(request, dict) else "",
107
+ "error": traceback.format_exc(limit=20)
108
+ })
109
+ `;
110
+ export const PYTHON_CLEANUP_SOURCE = String.raw `
111
+ import json
112
+ import os
113
+
114
+ import pyautogui
115
+
116
+ for key in ("ctrl", "shift", "alt", "win", "command"):
117
+ try:
118
+ pyautogui.keyUp(key)
119
+ except Exception:
120
+ pass
121
+ for button in ("left", "middle", "right"):
122
+ try:
123
+ pyautogui.mouseUp(button=button)
124
+ except Exception:
125
+ pass
126
+ try:
127
+ import pyperclip
128
+ if os.path.exists("clipboard.json"):
129
+ with open("clipboard.json", "r", encoding="utf-8") as clipboard_file:
130
+ previous = json.load(clipboard_file).get("value")
131
+ if previous is not None:
132
+ pyperclip.copy(previous)
133
+ except Exception:
134
+ pass
135
+ `;