@myagentroam/node 0.11.38 → 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.
- package/dist/computer-use/contracts.js +8 -0
- package/dist/{runner/mar-agent/computer-use-indicator.js → computer-use/indicator.js} +61 -38
- package/dist/computer-use/probe.js +95 -0
- package/dist/computer-use/provider.js +159 -0
- package/dist/computer-use/session.js +334 -0
- package/dist/computer-use/worker-source.js +135 -0
- package/dist/computer-use/worker.js +321 -0
- package/dist/connector.js +5 -1
- package/dist/mcp/naming.js +6 -1
- package/dist/runner/mar-agent/computer-use-skill.js +11 -2
- package/dist/runner/mar-agent/computer-use.js +104 -728
- package/dist/runner/mar-agent/conversation-normalizer.js +10 -4
- package/dist/runner/mar-agent/managed-run-controller.js +8 -5
- package/dist/service/computer-use-enablement-service.js +13 -7
- package/dist/service/conversation-hosting-enablement-service.js +2 -2
- package/dist/service/node-config-mutations.js +27 -0
- package/package.json +6 -5
- /package/dist/{runner/mar-agent/computer-use-probe.js → computer-use/probe-source.js} +0 -0
|
@@ -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 '
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
if (this.#
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 })
|
|
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
|
+
}
|