@frockbot/plugin-computer 0.3.20 → 0.3.21
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/package.json +12 -12
- package/src/agent.test.ts +45 -0
- package/src/agent.ts +98 -3
- package/src/processes.test.ts +79 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-computer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -29,21 +29,21 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@cordisjs/client": "0.8.2",
|
|
32
|
-
"@frockbot/client-core": "0.3.
|
|
33
|
-
"@frockbot/client-ui": "0.3.
|
|
34
|
-
"@frockbot/computer-core": "0.3.
|
|
35
|
-
"@frockbot/computer-host-runtime": "0.3.
|
|
36
|
-
"@frockbot/kernel-agent-loop": "0.3.
|
|
37
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
38
|
-
"@frockbot/plugin-prompt": "0.3.
|
|
39
|
-
"@frockbot/plugin-shell": "0.3.
|
|
40
|
-
"@frockbot/plugin-tools": "0.3.
|
|
32
|
+
"@frockbot/client-core": "0.3.21",
|
|
33
|
+
"@frockbot/client-ui": "0.3.21",
|
|
34
|
+
"@frockbot/computer-core": "0.3.21",
|
|
35
|
+
"@frockbot/computer-host-runtime": "0.3.21",
|
|
36
|
+
"@frockbot/kernel-agent-loop": "0.3.21",
|
|
37
|
+
"@frockbot/kernel-contracts": "0.3.21",
|
|
38
|
+
"@frockbot/plugin-prompt": "0.3.21",
|
|
39
|
+
"@frockbot/plugin-shell": "0.3.21",
|
|
40
|
+
"@frockbot/plugin-tools": "0.3.21",
|
|
41
41
|
"cordis": "4.0.0-rc.8",
|
|
42
42
|
"vue": "3.5.41"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@frockbot/plugin-models": "0.3.
|
|
46
|
-
"@frockbot/plugin-testkit": "0.3.
|
|
45
|
+
"@frockbot/plugin-models": "0.3.21",
|
|
46
|
+
"@frockbot/plugin-testkit": "0.3.21",
|
|
47
47
|
"@types/bun": "1.4.0",
|
|
48
48
|
"@types/node": "26.2.0",
|
|
49
49
|
"@vitejs/plugin-vue": "6.0.8",
|
package/src/agent.test.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { SessionStore } from "@frockbot/kernel-contracts";
|
|
|
14
14
|
import manifest from "../frockbot.json" with { type: "json" };
|
|
15
15
|
import packageJson from "../package.json" with { type: "json" };
|
|
16
16
|
import {
|
|
17
|
+
COMPUTER_OVERLOADED_TOOL_MESSAGE_V1,
|
|
17
18
|
createComputerAgentPlugin,
|
|
18
19
|
HUMAN_CONTROL_PROMPT_LINE,
|
|
19
20
|
} from "./agent.js";
|
|
@@ -223,6 +224,50 @@ describe("computer agent contribution", () => {
|
|
|
223
224
|
await harness.dispose();
|
|
224
225
|
});
|
|
225
226
|
|
|
227
|
+
test("computer_exec maps overloaded transport failures to one bounded plain reason", async () => {
|
|
228
|
+
for (const message of [
|
|
229
|
+
"Computer command failed: WebSocket keepalive timeout after 45000ms",
|
|
230
|
+
"The Computer effect was cancelled",
|
|
231
|
+
]) {
|
|
232
|
+
const provider: ComputerProvider = {
|
|
233
|
+
id: "fixture",
|
|
234
|
+
open: async (identity, tenant, assignment) => ({
|
|
235
|
+
assignment,
|
|
236
|
+
identity,
|
|
237
|
+
tenant,
|
|
238
|
+
exec: {
|
|
239
|
+
execute: () => Promise.reject(new Error(message)),
|
|
240
|
+
},
|
|
241
|
+
close: () => Promise.resolve(),
|
|
242
|
+
}),
|
|
243
|
+
};
|
|
244
|
+
const harness = await createPluginHarness([
|
|
245
|
+
ComputerRegistry,
|
|
246
|
+
ToolRegistry,
|
|
247
|
+
SystemPromptRegistry,
|
|
248
|
+
SessionStore,
|
|
249
|
+
]);
|
|
250
|
+
harness.root.computers.register(provider);
|
|
251
|
+
await harness.mount(
|
|
252
|
+
createComputerAgentPlugin({
|
|
253
|
+
userId: "user-1",
|
|
254
|
+
defaultProviderId: "fixture",
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
await expect(
|
|
259
|
+
execute(harness, "computer_exec", { command: "pwd" }),
|
|
260
|
+
).resolves.toEqual({
|
|
261
|
+
content: COMPUTER_OVERLOADED_TOOL_MESSAGE_V1,
|
|
262
|
+
isError: true,
|
|
263
|
+
});
|
|
264
|
+
expect(COMPUTER_OVERLOADED_TOOL_MESSAGE_V1.length).toBeLessThanOrEqual(
|
|
265
|
+
160,
|
|
266
|
+
);
|
|
267
|
+
await harness.dispose();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
226
271
|
test("injects and records the human-control line only while the durable lease is fresh", async () => {
|
|
227
272
|
const records = new Map<string, unknown>([
|
|
228
273
|
[
|
package/src/agent.ts
CHANGED
|
@@ -153,6 +153,48 @@ export interface ComputerAgentPluginConfig {
|
|
|
153
153
|
export const HUMAN_CONTROL_PROMPT_LINE =
|
|
154
154
|
"Your User is currently controlling the Computer; do not use it this Turn.";
|
|
155
155
|
|
|
156
|
+
/** Bounded copy for the two transport failures an overloaded Sprite emits. */
|
|
157
|
+
export const COMPUTER_OVERLOADED_TOOL_MESSAGE_V1 =
|
|
158
|
+
"The Computer is overloaded; a browser tab using too much memory was closed. Try again.";
|
|
159
|
+
|
|
160
|
+
function errorMessage(error: unknown): string {
|
|
161
|
+
return error instanceof Error ? error.message : String(error);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isOverloadedTransportFailure(error: unknown): boolean {
|
|
165
|
+
const message = errorMessage(error).toLowerCase();
|
|
166
|
+
return (
|
|
167
|
+
message.includes("websocket keepalive timeout") ||
|
|
168
|
+
message.includes("computer effect was cancelled")
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A local HTTP origin an Applet preview can own; public sites never qualify. */
|
|
173
|
+
export function localPreviewOriginV1(value: string): string | undefined {
|
|
174
|
+
try {
|
|
175
|
+
const url = new URL(value);
|
|
176
|
+
const local = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
177
|
+
return local && (url.protocol === "http:" || url.protocol === "https:")
|
|
178
|
+
? url.origin
|
|
179
|
+
: undefined;
|
|
180
|
+
} catch {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function localPreviewOriginsV1(value: string): string[] {
|
|
186
|
+
const found = new Set<string>();
|
|
187
|
+
for (const match of value.matchAll(/https?:\/\/[^\s"'<>]+/g)) {
|
|
188
|
+
const origin = localPreviewOriginV1(match[0].replace(/[),.;!?]+$/g, ""));
|
|
189
|
+
if (origin) found.add(origin);
|
|
190
|
+
}
|
|
191
|
+
return [...found];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function runsAppletPreviewV1(command: string): boolean {
|
|
195
|
+
return /(?:^|[\s;&|])(?:[^\s;&|]*\/)?applet\s+dev(?:\s|$)/.test(command);
|
|
196
|
+
}
|
|
197
|
+
|
|
156
198
|
/** The wake-free, Turn-scoped projection shared by prompt render and its log. */
|
|
157
199
|
class ComputerControlPromptProjection {
|
|
158
200
|
#line = "";
|
|
@@ -345,6 +387,9 @@ function decodeBrowser(input: unknown): ComputerBrowserAction | undefined {
|
|
|
345
387
|
}
|
|
346
388
|
|
|
347
389
|
function failure(error: unknown): { content: string; isError: true } {
|
|
390
|
+
if (isOverloadedTransportFailure(error)) {
|
|
391
|
+
return { content: COMPUTER_OVERLOADED_TOOL_MESSAGE_V1, isError: true };
|
|
392
|
+
}
|
|
348
393
|
if (error instanceof ComputerError) {
|
|
349
394
|
if (error.code === "human-control-active") {
|
|
350
395
|
// The holder is named, so a second Bot of the same User — and the User
|
|
@@ -367,7 +412,7 @@ function failure(error: unknown): { content: string; isError: true } {
|
|
|
367
412
|
return { content: error.message, isError: true };
|
|
368
413
|
}
|
|
369
414
|
return {
|
|
370
|
-
content:
|
|
415
|
+
content: errorMessage(error),
|
|
371
416
|
isError: true,
|
|
372
417
|
};
|
|
373
418
|
}
|
|
@@ -621,6 +666,8 @@ export function createComputerAgentPlugin(
|
|
|
621
666
|
// Agent loop knows it; a tool context does not, so it is caught where the
|
|
622
667
|
// loop already announces it.
|
|
623
668
|
let currentTurn = 1;
|
|
669
|
+
/** Local preview origins this Bot navigated to during the current Turn. */
|
|
670
|
+
const previewOrigins = new Set<string>();
|
|
624
671
|
// One cadence per mounted plugin, which is one per Turn: a Turn's first
|
|
625
672
|
// Computer action always gets its capture, and the rest are debounced.
|
|
626
673
|
const progressCadence = createComputerCaptureCadenceV1(
|
|
@@ -660,6 +707,24 @@ export function createComputerAgentPlugin(
|
|
|
660
707
|
await selfCheck(computer, botId, signal);
|
|
661
708
|
return computer;
|
|
662
709
|
};
|
|
710
|
+
const closePreviewTabs = async (
|
|
711
|
+
computer: ComputerHandle,
|
|
712
|
+
origins: readonly string[],
|
|
713
|
+
effectId: string,
|
|
714
|
+
signal?: AbortSignal,
|
|
715
|
+
): Promise<void> => {
|
|
716
|
+
if (!computer.browser) return;
|
|
717
|
+
const unique = [...new Set(origins)];
|
|
718
|
+
for (let offset = 0; offset < unique.length; offset += 16) {
|
|
719
|
+
await computer.browser.perform(
|
|
720
|
+
{ type: "close-origins", origins: unique.slice(offset, offset + 16) },
|
|
721
|
+
{
|
|
722
|
+
...(signal ? { signal } : {}),
|
|
723
|
+
effectId: `${effectId}:${Math.floor(offset / 16)}`,
|
|
724
|
+
},
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
};
|
|
663
728
|
|
|
664
729
|
const execTool: ToolDefinition = {
|
|
665
730
|
name: "computer_exec",
|
|
@@ -1039,6 +1104,22 @@ export function createComputerAgentPlugin(
|
|
|
1039
1104
|
// A mirror that could not be written never withholds an outcome
|
|
1040
1105
|
// that was read.
|
|
1041
1106
|
}
|
|
1107
|
+
if (
|
|
1108
|
+
action === "stop" &&
|
|
1109
|
+
runsAppletPreviewV1(held.command) &&
|
|
1110
|
+
computer.browser
|
|
1111
|
+
) {
|
|
1112
|
+
const origins = localPreviewOriginsV1(observed.logTail);
|
|
1113
|
+
if (origins.length > 0) {
|
|
1114
|
+
await closePreviewTabs(
|
|
1115
|
+
computer,
|
|
1116
|
+
origins,
|
|
1117
|
+
`${context.effectId}:close-preview-tabs`,
|
|
1118
|
+
context.signal,
|
|
1119
|
+
);
|
|
1120
|
+
for (const origin of origins) previewOrigins.delete(origin);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1042
1123
|
if (action === "logs") {
|
|
1043
1124
|
return {
|
|
1044
1125
|
content: observed.logTail || "(no output yet)",
|
|
@@ -1355,7 +1436,7 @@ export function createComputerAgentPlugin(
|
|
|
1355
1436
|
subagentRoles: ["executor", "computerUse"],
|
|
1356
1437
|
},
|
|
1357
1438
|
description:
|
|
1358
|
-
"Run the Computer's self-check and read the report: disk, the shared scratch, the desktop gateway, your display, the browser profile
|
|
1439
|
+
"Run the Computer's self-check and read the report: disk, the shared scratch, the desktop gateway, your display, the browser profile, renderer-watchdog actions, top memory consumers, the durable-root sync and its conflicts, the reference docs, the browser launcher, the clock, and DNS. Read-only; it changes nothing and repairs nothing.",
|
|
1359
1440
|
inputSchema: {
|
|
1360
1441
|
type: "object",
|
|
1361
1442
|
properties: {},
|
|
@@ -1575,6 +1656,10 @@ export function createComputerAgentPlugin(
|
|
|
1575
1656
|
signal: context.signal,
|
|
1576
1657
|
effectId: context.effectId,
|
|
1577
1658
|
});
|
|
1659
|
+
if (action.type === "navigate") {
|
|
1660
|
+
const origin = localPreviewOriginV1(action.url);
|
|
1661
|
+
if (origin) previewOrigins.add(origin);
|
|
1662
|
+
}
|
|
1578
1663
|
await fileProgressCapture(computer, context.botId, context);
|
|
1579
1664
|
return {
|
|
1580
1665
|
content: result.accessibilitySnapshot,
|
|
@@ -1605,6 +1690,7 @@ export function createComputerAgentPlugin(
|
|
|
1605
1690
|
ctx.on("agent/pre-step", async (agent, _inputs, turn, _step, next) => {
|
|
1606
1691
|
if (turn !== currentTurn) {
|
|
1607
1692
|
projectionWrites.clear();
|
|
1693
|
+
previewOrigins.clear();
|
|
1608
1694
|
// Every Turn's first Computer action is worth a capture, however
|
|
1609
1695
|
// soon after the previous Turn's last one it happens.
|
|
1610
1696
|
progressCadence.reset();
|
|
@@ -1630,6 +1716,15 @@ export function createComputerAgentPlugin(
|
|
|
1630
1716
|
return;
|
|
1631
1717
|
}
|
|
1632
1718
|
try {
|
|
1719
|
+
if (computer.browser && previewOrigins.size > 0) {
|
|
1720
|
+
const origins = [...previewOrigins];
|
|
1721
|
+
await closePreviewTabs(
|
|
1722
|
+
computer,
|
|
1723
|
+
origins,
|
|
1724
|
+
`computer:${writer?.runId ?? agent.session.id}:${turn}:close-preview-tabs`,
|
|
1725
|
+
);
|
|
1726
|
+
previewOrigins.clear();
|
|
1727
|
+
}
|
|
1633
1728
|
if (writer && computer.workspace) {
|
|
1634
1729
|
const root: WorkspaceRootV1 = {
|
|
1635
1730
|
kind: "package-declared",
|
|
@@ -1681,7 +1776,7 @@ export function createComputerAgentPlugin(
|
|
|
1681
1776
|
"Use computer_exec to inspect the filesystem before claiming that a path or file exists.",
|
|
1682
1777
|
"Use computer_screenshot to see your own desktop; each capture is filed in your durable screenshots root.",
|
|
1683
1778
|
"For a job that outlasts this Turn, use computer_exec with background:true and check it later with computer_process_check. Do not poll it in a loop.",
|
|
1684
|
-
"Use computer_doctor when the Computer misbehaves; it reports disk, desktop, sync, and network in one read-only call.",
|
|
1779
|
+
"Use computer_doctor when the Computer misbehaves; it reports disk, desktop, renderer-watchdog actions, top memory consumers, sync, and network in one read-only call.",
|
|
1685
1780
|
...(controlPrompt?.current() ? [controlPrompt.current()] : []),
|
|
1686
1781
|
"Never invent a directory listing.",
|
|
1687
1782
|
].join("\n"),
|
package/src/processes.test.ts
CHANGED
|
@@ -92,6 +92,20 @@ function fakeComputer(options: { launchFails?: boolean } = {}): Computer {
|
|
|
92
92
|
},
|
|
93
93
|
generation: () => Promise.resolve(computer.generation),
|
|
94
94
|
},
|
|
95
|
+
browser: {
|
|
96
|
+
perform: (action) => {
|
|
97
|
+
const cleanup = action as {
|
|
98
|
+
type: string;
|
|
99
|
+
origins?: readonly string[];
|
|
100
|
+
};
|
|
101
|
+
calls.push(
|
|
102
|
+
cleanup.type === "close-origins"
|
|
103
|
+
? `browser:close-origins:${cleanup.origins?.join(",") ?? ""}`
|
|
104
|
+
: `browser:${cleanup.type}`,
|
|
105
|
+
);
|
|
106
|
+
return Promise.resolve({ accessibilitySnapshot: "" });
|
|
107
|
+
},
|
|
108
|
+
},
|
|
95
109
|
close: () => Promise.resolve(),
|
|
96
110
|
}),
|
|
97
111
|
},
|
|
@@ -418,4 +432,69 @@ describe("stopping a background process", () => {
|
|
|
418
432
|
]);
|
|
419
433
|
await harness.dispose();
|
|
420
434
|
});
|
|
435
|
+
|
|
436
|
+
test("closes the Bot's tabs on an applet dev origin when its process stops", async () => {
|
|
437
|
+
const computer = fakeComputer();
|
|
438
|
+
const held = storage();
|
|
439
|
+
const harness = await mount(computer, held);
|
|
440
|
+
const launched = JSON.parse(
|
|
441
|
+
(
|
|
442
|
+
await call(harness, "computer_exec", {
|
|
443
|
+
command: "applet dev --port 8787",
|
|
444
|
+
background: true,
|
|
445
|
+
})
|
|
446
|
+
).content,
|
|
447
|
+
) as { processId: string };
|
|
448
|
+
await call(harness, "computer_browser", {
|
|
449
|
+
action: "navigate",
|
|
450
|
+
url: "http://127.0.0.1:8787/",
|
|
451
|
+
});
|
|
452
|
+
computer.state = {
|
|
453
|
+
alive: false,
|
|
454
|
+
exitCode: 143,
|
|
455
|
+
logTail: "http://127.0.0.1:8787/\nterminated",
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const stopped = await call(harness, "computer_process_stop", {
|
|
459
|
+
processId: launched.processId,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
expect(stopped.isError).toBe(false);
|
|
463
|
+
expect(computer.calls).toEqual([
|
|
464
|
+
`launch:${launched.processId}:applet dev --port 8787`,
|
|
465
|
+
"browser:navigate",
|
|
466
|
+
`stop:${launched.processId}`,
|
|
467
|
+
"browser:close-origins:http://127.0.0.1:8787",
|
|
468
|
+
]);
|
|
469
|
+
await harness.dispose();
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe("releasing a Bot's Turn", () => {
|
|
474
|
+
test("closes local preview origins the Bot navigated during the Turn", async () => {
|
|
475
|
+
const computer = fakeComputer();
|
|
476
|
+
const harness = await mount(computer, storage());
|
|
477
|
+
const session = harness.root.sessions.create("session-1");
|
|
478
|
+
const agent = { botId: "bot-1", session };
|
|
479
|
+
await harness.root.waterfall(
|
|
480
|
+
"agent/pre-step",
|
|
481
|
+
agent as never,
|
|
482
|
+
[],
|
|
483
|
+
1,
|
|
484
|
+
1,
|
|
485
|
+
() => Promise.resolve({ kind: "enter" as const, inputs: [] }),
|
|
486
|
+
);
|
|
487
|
+
await call(harness, "computer_browser", {
|
|
488
|
+
action: "navigate",
|
|
489
|
+
url: "http://localhost:8787/preview",
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
await harness.root.serial("agent/turn-stopping", agent as never, 1);
|
|
493
|
+
|
|
494
|
+
expect(computer.calls).toEqual([
|
|
495
|
+
"browser:navigate",
|
|
496
|
+
"browser:close-origins:http://localhost:8787",
|
|
497
|
+
]);
|
|
498
|
+
await harness.dispose();
|
|
499
|
+
});
|
|
421
500
|
});
|