@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.
@@ -8,7 +8,6 @@ const nodeAppVersion = typeof nodePackage.version === 'string' ? nodePackage.ver
8
8
  const runtimeCredentialCapability = () => ({
9
9
  apiVersion: 1,
10
10
  websocket: true,
11
- fastMode: true,
12
11
  serviceTiers: ['auto', 'default', 'flex', 'priority', 'fast', 'ultrafast'],
13
12
  namespacedTools: true,
14
13
  responsesOptionalParameters: true,
@@ -0,0 +1,8 @@
1
+ export class ComputerUseError extends Error {
2
+ code;
3
+ constructor(code, message, options = {}) {
4
+ super(message, options);
5
+ this.name = 'ComputerUseError';
6
+ this.code = code;
7
+ }
8
+ }
@@ -3,7 +3,8 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { resolve } from 'node:path';
5
5
  import { createInterface } from 'node:readline';
6
- import { terminateProcessTree } from '../../process-tree.js';
6
+ import { terminateProcessTree } from '../process-tree.js';
7
+ import { ComputerUseError } from './contracts.js';
7
8
  const START_TIMEOUT_MS = 5_000;
8
9
  const STOP_TIMEOUT_MS = 2_000;
9
10
  /** A non-activating, pointer-transparent primary-display control marker. */
@@ -11,11 +12,14 @@ export class ComputerUseControlIndicator {
11
12
  child;
12
13
  directory;
13
14
  #closed = false;
15
+ #closePromise;
14
16
  #lostError;
17
+ #exited;
15
18
  lost;
16
19
  constructor(child, directory) {
17
20
  this.child = child;
18
21
  this.directory = directory;
22
+ this.#exited = new Promise((resolveExit) => child.once('close', () => resolveExit()));
19
23
  this.lost = new Promise((resolveLost) => {
20
24
  child.once('exit', (code, signal) => {
21
25
  if (this.#closed)
@@ -28,28 +32,53 @@ export class ComputerUseControlIndicator {
28
32
  static async show(runtimeExecutable) {
29
33
  const directory = await mkdtemp(resolve(tmpdir(), 'mar-computer-use-indicator-'));
30
34
  const script = resolve(directory, 'indicator.py');
31
- await writeFile(script, PYTHON_INDICATOR_SOURCE, { encoding: 'utf8', mode: 0o600 });
32
- const child = spawn(runtimeExecutable, ['-u', script], {
33
- cwd: directory,
34
- env: indicatorEnvironment(),
35
- stdio: ['pipe', 'pipe', 'pipe'],
36
- windowsHide: true,
37
- detached: process.platform !== 'win32'
38
- });
35
+ let child;
36
+ try {
37
+ await writeFile(script, PYTHON_INDICATOR_SOURCE, { encoding: 'utf8', mode: 0o600 });
38
+ child = spawn(runtimeExecutable, ['-u', script], {
39
+ cwd: directory,
40
+ env: indicatorEnvironment(),
41
+ stdio: ['pipe', 'pipe', 'pipe'],
42
+ windowsHide: true,
43
+ detached: process.platform !== 'win32'
44
+ });
45
+ }
46
+ catch (error) {
47
+ try {
48
+ await rm(directory, { recursive: true, force: true });
49
+ }
50
+ catch (cleanup) {
51
+ throw new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use indicator startup cleanup failed.', {
52
+ cause: new AggregateError([error, cleanup])
53
+ });
54
+ }
55
+ throw error;
56
+ }
39
57
  const indicator = new ComputerUseControlIndicator(child, directory);
40
58
  try {
41
59
  await waitUntilVisible(child);
42
60
  return indicator;
43
61
  }
44
62
  catch (error) {
45
- await indicator.close();
63
+ try {
64
+ await indicator.close();
65
+ }
66
+ catch (cleanup) {
67
+ throw new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use indicator startup and cleanup failed.', { cause: new AggregateError([error, cleanup]) });
68
+ }
46
69
  throw error;
47
70
  }
48
71
  }
49
- async close() {
50
- if (this.#closed)
51
- return;
72
+ close() {
73
+ if (this.#closePromise !== undefined)
74
+ return this.#closePromise;
52
75
  this.#closed = true;
76
+ this.#closePromise = this.#close().catch((error) => {
77
+ throw new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use indicator cleanup failed.', { cause: error });
78
+ });
79
+ return this.#closePromise;
80
+ }
81
+ async #close() {
53
82
  this.child.stdin.end();
54
83
  if (this.child.exitCode === null && this.child.signalCode === null) {
55
84
  await terminateProcessTree(this.child).catch(() => {
@@ -60,17 +89,27 @@ export class ComputerUseControlIndicator {
60
89
  // The process may have exited between the status check and termination.
61
90
  }
62
91
  });
63
- if (!(await waitForExit(this.child, STOP_TIMEOUT_MS))) {
64
- try {
65
- this.child.kill('SIGKILL');
66
- }
67
- catch {
68
- // The process may have exited before the fallback signal.
69
- }
70
- await waitForExit(this.child, STOP_TIMEOUT_MS);
92
+ }
93
+ if (!(await this.#waitForClose(STOP_TIMEOUT_MS))) {
94
+ try {
95
+ this.child.kill('SIGKILL');
71
96
  }
97
+ catch {
98
+ // The process may have exited before the fallback signal.
99
+ }
100
+ if (!(await this.#waitForClose(STOP_TIMEOUT_MS)))
101
+ throw new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use indicator did not exit.');
72
102
  }
73
- await rm(this.directory, { recursive: true, force: true }).catch(() => undefined);
103
+ await rm(this.directory, { recursive: true, force: true });
104
+ }
105
+ #waitForClose(timeoutMs) {
106
+ return new Promise((resolveWait) => {
107
+ const timer = setTimeout(() => resolveWait(false), timeoutMs);
108
+ void this.#exited.then(() => {
109
+ clearTimeout(timer);
110
+ resolveWait(true);
111
+ });
112
+ });
74
113
  }
75
114
  assertActive() {
76
115
  if (this.#lostError !== undefined)
@@ -79,22 +118,6 @@ export class ComputerUseControlIndicator {
79
118
  throw new Error('Computer Use control indicator is no longer active.');
80
119
  }
81
120
  }
82
- async function waitForExit(child, timeoutMs) {
83
- if (child.exitCode !== null || child.signalCode !== null)
84
- return true;
85
- return await new Promise((resolveExit) => {
86
- const timer = setTimeout(() => finish(false), timeoutMs);
87
- const exit = () => finish(true);
88
- const finish = (exited) => {
89
- clearTimeout(timer);
90
- child.off('exit', exit);
91
- resolveExit(exited);
92
- };
93
- child.once('exit', exit);
94
- if (child.exitCode !== null || child.signalCode !== null)
95
- finish(true);
96
- });
97
- }
98
121
  async function waitUntilVisible(child) {
99
122
  const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
100
123
  try {
@@ -0,0 +1,95 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { access } from 'node:fs/promises';
3
+ import { terminateProcessTree } from '../process-tree.js';
4
+ import { PYTHON_PROBE_SOURCE } from './probe-source.js';
5
+ export async function probeComputerUse(configuration) {
6
+ const platform = platformName();
7
+ const base = {
8
+ platform,
9
+ backend: 'PYAUTOGUI',
10
+ runtimeExecutable: configuration.runtimeExecutable,
11
+ supportsScreenshot: false,
12
+ supportsInput: false,
13
+ supportsUnicodePaste: false,
14
+ supportsPrimaryDisplay: false
15
+ };
16
+ if (!configuration.enabled)
17
+ return { ...base, available: false, reasonCode: 'COMPUTER_USE_DISABLED' };
18
+ if (process.platform === 'linux' && (!process.env['DISPLAY'] || process.env['WAYLAND_DISPLAY']))
19
+ return { ...base, available: false, reasonCode: 'COMPUTER_DESKTOP_UNSUPPORTED' };
20
+ try {
21
+ await access(configuration.runtimeExecutable);
22
+ const result = await runProbe(configuration.runtimeExecutable);
23
+ if (result === undefined)
24
+ return { ...base, available: false, reasonCode: 'COMPUTER_RUNTIME_UNAVAILABLE' };
25
+ return { ...base, ...result, available: result.reasonCode === null };
26
+ }
27
+ catch {
28
+ return { ...base, available: false, reasonCode: 'COMPUTER_RUNTIME_UNAVAILABLE' };
29
+ }
30
+ }
31
+ async function runProbe(executable) {
32
+ const child = spawn(executable, ['-c', PYTHON_PROBE_SOURCE], {
33
+ stdio: ['ignore', 'pipe', 'pipe'],
34
+ windowsHide: true
35
+ });
36
+ let stdout = '';
37
+ let overflow = false;
38
+ child.stdout.on('data', (chunk) => {
39
+ if (stdout.length + chunk.length > 4096)
40
+ overflow = true;
41
+ if (!overflow)
42
+ stdout += chunk.toString('utf8');
43
+ });
44
+ child.stderr.resume();
45
+ return await new Promise((resolveProbe) => {
46
+ const timeout = setTimeout(() => {
47
+ void terminateProcessTree(child).finally(() => resolveProbe(undefined));
48
+ }, 10_000);
49
+ child.once('close', (code) => {
50
+ clearTimeout(timeout);
51
+ if (code !== 0 || overflow)
52
+ return resolveProbe(undefined);
53
+ try {
54
+ const value = JSON.parse(stdout);
55
+ if (value === null ||
56
+ typeof value !== 'object' ||
57
+ typeof value.supportsScreenshot !== 'boolean' ||
58
+ typeof value.supportsInput !== 'boolean' ||
59
+ typeof value.supportsUnicodePaste !== 'boolean' ||
60
+ typeof value.supportsPrimaryDisplay !== 'boolean')
61
+ return resolveProbe(undefined);
62
+ const reasonCode = value.reasonCode === 'COMPUTER_DESKTOP_UNSUPPORTED'
63
+ ? value.reasonCode
64
+ : !value.supportsScreenshot || !value.supportsPrimaryDisplay
65
+ ? 'COMPUTER_RUNTIME_UNAVAILABLE'
66
+ : !value.supportsInput
67
+ ? 'COMPUTER_INPUT_PERMISSION_DENIED'
68
+ : !value.supportsUnicodePaste
69
+ ? 'COMPUTER_RUNTIME_UNAVAILABLE'
70
+ : null;
71
+ resolveProbe({
72
+ supportsScreenshot: value.supportsScreenshot,
73
+ supportsInput: value.supportsInput,
74
+ supportsUnicodePaste: value.supportsUnicodePaste,
75
+ supportsPrimaryDisplay: value.supportsPrimaryDisplay,
76
+ reasonCode
77
+ });
78
+ }
79
+ catch {
80
+ resolveProbe(undefined);
81
+ }
82
+ });
83
+ child.once('error', () => {
84
+ clearTimeout(timeout);
85
+ resolveProbe(undefined);
86
+ });
87
+ });
88
+ }
89
+ function platformName() {
90
+ if (process.platform === 'win32')
91
+ return 'windows';
92
+ if (process.platform === 'darwin')
93
+ return 'macos';
94
+ return 'linux';
95
+ }
@@ -0,0 +1,159 @@
1
+ import { ComputerUseError } from './contracts.js';
2
+ import { ComputerUseControlIndicator } from './indicator.js';
3
+ import { probeComputerUse } from './probe.js';
4
+ import { ComputerUseDesktopLeaseService, ComputerUseSessionResources } from './session.js';
5
+ export class ComputerUseProvider {
6
+ configuration;
7
+ #lease;
8
+ #probe;
9
+ #showControlIndicator;
10
+ #sessions = new Map();
11
+ #pendingBinds = new Map();
12
+ #supportKey;
13
+ #supportPromise;
14
+ #disposed = false;
15
+ #resetPromise;
16
+ #resetFailure;
17
+ constructor(configuration, options = {}) {
18
+ this.configuration = configuration;
19
+ this.#lease = options.lease ?? new ComputerUseDesktopLeaseService();
20
+ this.#probe = options.probe ?? probeComputerUse;
21
+ this.#showControlIndicator = options.showControlIndicator ?? ComputerUseControlIndicator.show;
22
+ }
23
+ support() {
24
+ const configuration = this.#configuration();
25
+ return configuration === undefined
26
+ ? probeComputerUse({ enabled: false, runtimeExecutable: '' })
27
+ : this.#support(configuration);
28
+ }
29
+ async bind(input) {
30
+ if (this.#disposed || this.#resetPromise !== undefined || this.#resetFailure !== undefined)
31
+ throw new ComputerUseError('COMPUTER_UNAVAILABLE', 'Computer Use provider is closed.');
32
+ const token = { released: false };
33
+ const pending = this.#pendingBinds.get(input.sessionId) ?? new Set();
34
+ pending.add(token);
35
+ this.#pendingBinds.set(input.sessionId, pending);
36
+ try {
37
+ const configuration = this.#configuration();
38
+ if (configuration?.enabled !== true ||
39
+ !input.enabled ||
40
+ input.mode !== 'run' ||
41
+ !input.modelSupportsImages)
42
+ return undefined;
43
+ const support = await this.#support(configuration);
44
+ if (!support.available)
45
+ return undefined;
46
+ this.#assertBindingAvailable(token);
47
+ let retained = this.#sessions.get(input.sessionId);
48
+ if (retained !== undefined &&
49
+ retained.runtimeExecutable !== configuration.runtimeExecutable) {
50
+ await this.#destroySession(input.sessionId, retained);
51
+ this.#assertBindingAvailable(token);
52
+ retained = undefined;
53
+ }
54
+ const entry = retained ?? {
55
+ runtimeExecutable: configuration.runtimeExecutable,
56
+ session: new ComputerUseSessionResources(configuration.runtimeExecutable, this.#lease, this.#showControlIndicator, async () => {
57
+ const current = await this.#probe(configuration);
58
+ if (!current.available)
59
+ throw new ComputerUseError(current.reasonCode ?? 'COMPUTER_UNAVAILABLE', 'The interactive desktop is no longer available.');
60
+ })
61
+ };
62
+ if (retained === undefined)
63
+ this.#sessions.set(input.sessionId, entry);
64
+ entry.session.attach(input.runId, input.signal);
65
+ let closed = false;
66
+ let closing;
67
+ const close = () => {
68
+ if (closing !== undefined)
69
+ return closing;
70
+ closed = true;
71
+ closing = entry.session.detach(input.runId);
72
+ return closing;
73
+ };
74
+ entry.session.setBindingClose(close);
75
+ return {
76
+ call: (executionInput, signal) => {
77
+ if (closed || this.#sessions.get(input.sessionId) !== entry)
78
+ return Promise.reject(new ComputerUseError('COMPUTER_UNAVAILABLE', 'Computer Use binding is closed.'));
79
+ return entry.session.call(executionInput, signal);
80
+ },
81
+ wait: (waitInput, signal) => {
82
+ if (closed || this.#sessions.get(input.sessionId) !== entry)
83
+ return Promise.reject(new ComputerUseError('COMPUTER_UNAVAILABLE', 'Computer Use binding is closed.'));
84
+ return entry.session.wait(waitInput, signal);
85
+ },
86
+ close
87
+ };
88
+ }
89
+ finally {
90
+ pending.delete(token);
91
+ if (pending.size === 0)
92
+ this.#pendingBinds.delete(input.sessionId);
93
+ }
94
+ }
95
+ async releaseSession(sessionId) {
96
+ for (const token of this.#pendingBinds.get(sessionId) ?? [])
97
+ token.released = true;
98
+ const entry = this.#sessions.get(sessionId);
99
+ if (entry !== undefined)
100
+ await this.#destroySession(sessionId, entry);
101
+ }
102
+ reset() {
103
+ if (this.#resetPromise !== undefined)
104
+ return this.#resetPromise;
105
+ if (this.#resetFailure !== undefined)
106
+ return Promise.reject(this.#resetFailure);
107
+ this.#supportKey = undefined;
108
+ this.#supportPromise = undefined;
109
+ for (const pending of this.#pendingBinds.values())
110
+ for (const token of pending)
111
+ token.released = true;
112
+ const reset = (async () => {
113
+ const results = await Promise.allSettled([...this.#sessions.entries()].map(([sessionId, entry]) => this.#destroySession(sessionId, entry)));
114
+ const failures = results.filter((result) => result.status === 'rejected');
115
+ if (failures.length > 0)
116
+ throw new ComputerUseError('COMPUTER_CLEANUP_FAILED', 'Computer Use cleanup failed.', {
117
+ cause: new AggregateError(failures.map((failure) => failure.reason))
118
+ });
119
+ })();
120
+ this.#resetPromise = reset;
121
+ void reset.then(() => {
122
+ this.#resetPromise = undefined;
123
+ }, (error) => {
124
+ this.#resetFailure =
125
+ error instanceof Error
126
+ ? error
127
+ : new Error('Computer Use cleanup failed.', { cause: error });
128
+ this.#resetPromise = undefined;
129
+ });
130
+ return reset;
131
+ }
132
+ async dispose() {
133
+ this.#disposed = true;
134
+ await this.reset();
135
+ }
136
+ #configuration() {
137
+ return typeof this.configuration === 'function' ? this.configuration() : this.configuration;
138
+ }
139
+ #assertBindingAvailable(token) {
140
+ if (!this.#disposed && !token.released)
141
+ return;
142
+ throw new ComputerUseError('COMPUTER_UNAVAILABLE', 'Computer Use Session resources were released while binding.');
143
+ }
144
+ #support(configuration) {
145
+ const key = `${configuration.enabled}:${configuration.runtimeExecutable}`;
146
+ if (this.#supportKey !== key) {
147
+ this.#supportKey = key;
148
+ this.#supportPromise = this.#probe(configuration);
149
+ }
150
+ return this.#supportPromise;
151
+ }
152
+ async #destroySession(sessionId, entry) {
153
+ if (this.#sessions.get(sessionId) !== entry)
154
+ return;
155
+ await entry.session.close();
156
+ if (this.#sessions.get(sessionId) === entry)
157
+ this.#sessions.delete(sessionId);
158
+ }
159
+ }