@frockbot/plugin-computer 0.2.4 → 0.3.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/frockbot.json +0 -1
- package/package.json +12 -12
- package/src/agent.ts +74 -70
- package/src/backend.ts +4 -4
- package/src/bot.test.ts +566 -3
- package/src/bot.ts +410 -10
- package/src/capture.ts +107 -0
- package/src/client/ComputerCard.test.ts +24 -4
- package/src/client/ComputerCard.vue +108 -16
- package/src/client/ComputerViewerOverlay.vue +67 -10
- package/src/client/application.test.ts +93 -15
- package/src/client/application.ts +50 -12
- package/src/client/index.ts +0 -6
- package/src/client/progress.test.ts +148 -0
- package/src/client/progress.ts +171 -0
- package/src/client/state-machine.test.ts +1 -1
- package/src/client/styles.css +75 -101
- package/src/protocol.test.ts +37 -0
- package/src/protocol.ts +111 -1
- package/src/screenshot.test.ts +100 -3
- package/src/workspace-fixture.ts +4 -0
- package/src/client/ComputerStrip.test.ts +0 -54
- package/src/client/ComputerStrip.vue +0 -55
package/frockbot.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-computer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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.
|
|
33
|
-
"@frockbot/client-ui": "0.
|
|
34
|
-
"@frockbot/computer-core": "0.
|
|
35
|
-
"@frockbot/computer-host-runtime": "0.
|
|
36
|
-
"@frockbot/kernel-agent-loop": "0.
|
|
37
|
-
"@frockbot/kernel-contracts": "0.
|
|
38
|
-
"@frockbot/plugin-prompt": "0.
|
|
39
|
-
"@frockbot/plugin-shell": "0.
|
|
40
|
-
"@frockbot/plugin-tools": "0.
|
|
32
|
+
"@frockbot/client-core": "0.3.0",
|
|
33
|
+
"@frockbot/client-ui": "0.3.0",
|
|
34
|
+
"@frockbot/computer-core": "0.3.0",
|
|
35
|
+
"@frockbot/computer-host-runtime": "0.3.0",
|
|
36
|
+
"@frockbot/kernel-agent-loop": "0.3.0",
|
|
37
|
+
"@frockbot/kernel-contracts": "0.3.0",
|
|
38
|
+
"@frockbot/plugin-prompt": "0.3.0",
|
|
39
|
+
"@frockbot/plugin-shell": "0.3.0",
|
|
40
|
+
"@frockbot/plugin-tools": "0.3.0",
|
|
41
41
|
"cordis": "4.0.0-rc.8",
|
|
42
42
|
"vue": "3.5.41"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@frockbot/plugin-models": "0.
|
|
46
|
-
"@frockbot/plugin-testkit": "0.
|
|
45
|
+
"@frockbot/plugin-models": "0.3.0",
|
|
46
|
+
"@frockbot/plugin-testkit": "0.3.0",
|
|
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.ts
CHANGED
|
@@ -71,8 +71,12 @@ import {
|
|
|
71
71
|
import {
|
|
72
72
|
COMPUTER_DOCTOR_ROOT_ID,
|
|
73
73
|
COMPUTER_SCREENSHOTS_ROOT_ID,
|
|
74
|
-
COMPUTER_SCREENSHOT_RETENTION,
|
|
75
74
|
} from "./roots.js";
|
|
75
|
+
import {
|
|
76
|
+
fileComputerScreenshotV1,
|
|
77
|
+
type ComputerProjectionFileInvalidationV1,
|
|
78
|
+
type ComputerProjectionFileKindV1,
|
|
79
|
+
} from "./capture.js";
|
|
76
80
|
import {
|
|
77
81
|
COMPUTER_CONTROL_RECORD_KEY,
|
|
78
82
|
decodeStoredComputerControlV1,
|
|
@@ -86,6 +90,10 @@ export {
|
|
|
86
90
|
} from "./roots.js";
|
|
87
91
|
|
|
88
92
|
export type { ComputerProcessStorageV1 };
|
|
93
|
+
export type {
|
|
94
|
+
ComputerProjectionFileInvalidationV1,
|
|
95
|
+
ComputerProjectionFileKindV1,
|
|
96
|
+
} from "./capture.js";
|
|
89
97
|
|
|
90
98
|
/**
|
|
91
99
|
* The Session and Turn a durable Workspace write records as its writer.
|
|
@@ -122,6 +130,8 @@ export interface ComputerAgentPluginConfig {
|
|
|
122
130
|
get<T>(key: string): Promise<T | undefined>;
|
|
123
131
|
now?(): Date;
|
|
124
132
|
};
|
|
133
|
+
/** Drops resident projection caches after this Turn's Workspace sync. */
|
|
134
|
+
projectionFiles?: ComputerProjectionFileInvalidationV1;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
export const HUMAN_CONTROL_PROMPT_LINE =
|
|
@@ -413,46 +423,6 @@ class ComputerTurnSync {
|
|
|
413
423
|
}
|
|
414
424
|
}
|
|
415
425
|
|
|
416
|
-
/**
|
|
417
|
-
* Keeps the newest {@link COMPUTER_SCREENSHOT_RETENTION} captures for one Bot.
|
|
418
|
-
*
|
|
419
|
-
* A screenshot of a logged-in page is User-scoped content that outlives its
|
|
420
|
-
* Turn, so the root is bounded rather than allowed to grow for the life of the
|
|
421
|
-
* Computer. Pruning is a best effort: a capture that was recorded is never
|
|
422
|
-
* failed because an older one could not be removed.
|
|
423
|
-
*/
|
|
424
|
-
async function prune(
|
|
425
|
-
workspace: NonNullable<ComputerHandle["workspace"]>,
|
|
426
|
-
root: WorkspaceRootV1,
|
|
427
|
-
botKey: string,
|
|
428
|
-
writer: WorkspaceWriterV1,
|
|
429
|
-
): Promise<void> {
|
|
430
|
-
const listed = await workspace.list({
|
|
431
|
-
root,
|
|
432
|
-
prefix: botKey,
|
|
433
|
-
limit: COMPUTER_SCREENSHOT_RETENTION * 4,
|
|
434
|
-
});
|
|
435
|
-
if (listed.status !== "ok") return;
|
|
436
|
-
// Ordered by when each capture was written, not by its path: a path names
|
|
437
|
-
// the Turn and the ordinal within it, and neither sorts chronologically
|
|
438
|
-
// across Turns. The generation is the record of when, so it decides.
|
|
439
|
-
const sorted = [...listed.entries].sort((left, right) => {
|
|
440
|
-
const order = left.generation.writtenAt.localeCompare(
|
|
441
|
-
right.generation.writtenAt,
|
|
442
|
-
);
|
|
443
|
-
return order !== 0 ? order : left.path.path.localeCompare(right.path.path);
|
|
444
|
-
});
|
|
445
|
-
const excess = sorted.length - COMPUTER_SCREENSHOT_RETENTION;
|
|
446
|
-
for (let index = 0; index < excess; index += 1) {
|
|
447
|
-
const entry = sorted[index]!;
|
|
448
|
-
await workspace.delete({
|
|
449
|
-
path: entry.path,
|
|
450
|
-
writer,
|
|
451
|
-
expectedGenerationId: entry.generation.generationId,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
426
|
async function useComputer<T>(
|
|
457
427
|
computer: ComputerHandle,
|
|
458
428
|
run: (computer: ComputerHandle) => Promise<T>,
|
|
@@ -486,6 +456,16 @@ export function createComputerAgentPlugin(
|
|
|
486
456
|
// Agent loop knows it; a tool context does not, so it is caught where the
|
|
487
457
|
// loop already announces it.
|
|
488
458
|
let currentTurn = 1;
|
|
459
|
+
const projectionWrites = new Set<ComputerProjectionFileKindV1>();
|
|
460
|
+
const noteProjectionWrite = (kind: ComputerProjectionFileKindV1): void => {
|
|
461
|
+
projectionWrites.add(kind);
|
|
462
|
+
};
|
|
463
|
+
const invalidateProjectionWrites = (botId: string): void => {
|
|
464
|
+
for (const kind of projectionWrites) {
|
|
465
|
+
config.projectionFiles?.invalidate(botId, kind);
|
|
466
|
+
}
|
|
467
|
+
projectionWrites.clear();
|
|
468
|
+
};
|
|
489
469
|
const turnOf = (_context: ToolExecutionContext): number => currentTurn;
|
|
490
470
|
const attach = async (botId: string, signal: AbortSignal) => {
|
|
491
471
|
if (!ctx.computers.assignment(identity)) {
|
|
@@ -953,12 +933,6 @@ export function createComputerAgentPlugin(
|
|
|
953
933
|
return await useComputer(
|
|
954
934
|
await open(context.botId, context.sessionId, context.signal),
|
|
955
935
|
async (computer) => {
|
|
956
|
-
if (!computer.screenshot) {
|
|
957
|
-
throw new ComputerError(
|
|
958
|
-
"capability-unavailable",
|
|
959
|
-
"The selected Computer does not support screenshots",
|
|
960
|
-
);
|
|
961
|
-
}
|
|
962
936
|
const workspace = computer.workspace;
|
|
963
937
|
if (!workspace) {
|
|
964
938
|
throw new ComputerError(
|
|
@@ -966,10 +940,6 @@ export function createComputerAgentPlugin(
|
|
|
966
940
|
"The selected Computer exposes no Workspace to file a screenshot in",
|
|
967
941
|
);
|
|
968
942
|
}
|
|
969
|
-
const captured = await computer.screenshot.capture({
|
|
970
|
-
signal: context.signal,
|
|
971
|
-
effectId: context.effectId,
|
|
972
|
-
});
|
|
973
943
|
const root: WorkspaceRootV1 = {
|
|
974
944
|
kind: "package-declared",
|
|
975
945
|
userId,
|
|
@@ -989,27 +959,23 @@ export function createComputerAgentPlugin(
|
|
|
989
959
|
turnId: writer.turnId,
|
|
990
960
|
runId: writer.runId,
|
|
991
961
|
};
|
|
992
|
-
const
|
|
962
|
+
const filed = await fileComputerScreenshotV1({
|
|
963
|
+
computer,
|
|
964
|
+
workspace,
|
|
993
965
|
path,
|
|
994
|
-
bytes: captured.bytes,
|
|
995
966
|
writer: botWriter,
|
|
996
|
-
|
|
997
|
-
|
|
967
|
+
botKey,
|
|
968
|
+
signal: context.signal,
|
|
969
|
+
effectId: context.effectId,
|
|
998
970
|
});
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
content: `The screenshot could not be filed: ${written.status}: ${written.reason}`,
|
|
1002
|
-
isError: true,
|
|
1003
|
-
};
|
|
1004
|
-
}
|
|
1005
|
-
await prune(workspace, root, botKey, botWriter);
|
|
1006
|
-
const dimensions = pngDimensionsV1(captured.bytes);
|
|
971
|
+
noteProjectionWrite("screenshots");
|
|
972
|
+
const dimensions = pngDimensionsV1(filed.captured.bytes);
|
|
1007
973
|
const attachment: ToolAttachmentV1 = {
|
|
1008
974
|
kind: "image",
|
|
1009
|
-
mediaType: captured.mediaType,
|
|
975
|
+
mediaType: filed.captured.mediaType,
|
|
1010
976
|
workspacePath: path,
|
|
1011
|
-
contentHash:
|
|
1012
|
-
bytes:
|
|
977
|
+
contentHash: filed.generation.contentHash,
|
|
978
|
+
bytes: filed.generation.size,
|
|
1013
979
|
};
|
|
1014
980
|
// The bytes are offered to the resident Session so this Turn's
|
|
1015
981
|
// next model request can show them. They are never recorded:
|
|
@@ -1019,7 +985,7 @@ export function createComputerAgentPlugin(
|
|
|
1019
985
|
.get(context.sessionId)
|
|
1020
986
|
?.offerAttachmentBytes(
|
|
1021
987
|
attachment.contentHash,
|
|
1022
|
-
base64Of(captured.bytes),
|
|
988
|
+
base64Of(filed.captured.bytes),
|
|
1023
989
|
);
|
|
1024
990
|
return {
|
|
1025
991
|
content: JSON.stringify({
|
|
@@ -1028,8 +994,8 @@ export function createComputerAgentPlugin(
|
|
|
1028
994
|
contentHash: attachment.contentHash,
|
|
1029
995
|
bytes: attachment.bytes,
|
|
1030
996
|
...(dimensions ?? {}),
|
|
1031
|
-
display: captured.display,
|
|
1032
|
-
capturedAt: captured.capturedAt,
|
|
997
|
+
display: filed.captured.display,
|
|
998
|
+
capturedAt: filed.captured.capturedAt,
|
|
1033
999
|
}),
|
|
1034
1000
|
isError: false,
|
|
1035
1001
|
attachments: [attachment],
|
|
@@ -1081,7 +1047,9 @@ export function createComputerAgentPlugin(
|
|
|
1081
1047
|
: null,
|
|
1082
1048
|
mediaType: "application/json",
|
|
1083
1049
|
});
|
|
1084
|
-
|
|
1050
|
+
if (written.status !== "ok") return undefined;
|
|
1051
|
+
noteProjectionWrite("doctor");
|
|
1052
|
+
return path;
|
|
1085
1053
|
};
|
|
1086
1054
|
|
|
1087
1055
|
/**
|
|
@@ -1368,6 +1336,7 @@ export function createComputerAgentPlugin(
|
|
|
1368
1336
|
// A Turn's first step is where the Turn's sync state begins; a Turn that
|
|
1369
1337
|
// never touches the Computer never syncs and never wakes one.
|
|
1370
1338
|
ctx.on("agent/pre-step", async (agent, _inputs, turn, _step, next) => {
|
|
1339
|
+
if (turn !== currentTurn) projectionWrites.clear();
|
|
1371
1340
|
currentTurn = turn;
|
|
1372
1341
|
turnSync.beginTurn(turn);
|
|
1373
1342
|
if (controlPrompt?.loadedTurn() !== turn) {
|
|
@@ -1385,13 +1354,48 @@ export function createComputerAgentPlugin(
|
|
|
1385
1354
|
computer = await attach(agent.botId, new AbortController().signal);
|
|
1386
1355
|
} catch (error) {
|
|
1387
1356
|
await turnSync.unavailable(agent.session.id, error);
|
|
1357
|
+
invalidateProjectionWrites(agent.botId);
|
|
1388
1358
|
return;
|
|
1389
1359
|
}
|
|
1390
1360
|
try {
|
|
1361
|
+
if (writer && computer.workspace) {
|
|
1362
|
+
const root: WorkspaceRootV1 = {
|
|
1363
|
+
kind: "package-declared",
|
|
1364
|
+
userId,
|
|
1365
|
+
packageId: "computer",
|
|
1366
|
+
rootId: COMPUTER_SCREENSHOTS_ROOT_ID,
|
|
1367
|
+
};
|
|
1368
|
+
const botKey = computerBotPathKeyV1(agent.botId);
|
|
1369
|
+
captureSequence += 1;
|
|
1370
|
+
try {
|
|
1371
|
+
await fileComputerScreenshotV1({
|
|
1372
|
+
computer,
|
|
1373
|
+
workspace: computer.workspace,
|
|
1374
|
+
path: {
|
|
1375
|
+
root,
|
|
1376
|
+
path: `${botKey}/${writer.turnId}-${captureSequence}.png`,
|
|
1377
|
+
},
|
|
1378
|
+
writer: {
|
|
1379
|
+
kind: "bot",
|
|
1380
|
+
botId: agent.botId,
|
|
1381
|
+
sessionId: writer.sessionId,
|
|
1382
|
+
turnId: writer.turnId,
|
|
1383
|
+
runId: writer.runId,
|
|
1384
|
+
},
|
|
1385
|
+
botKey,
|
|
1386
|
+
effectId: `computer:${writer.runId}:turn-end-screenshot`,
|
|
1387
|
+
});
|
|
1388
|
+
noteProjectionWrite("screenshots");
|
|
1389
|
+
} catch {
|
|
1390
|
+
// Opportunistic capture never changes the Turn outcome. The
|
|
1391
|
+
// provider's human-control refusal is deliberately preserved.
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1391
1394
|
await turnSync.afterTurn(computer, agent.session.id);
|
|
1392
1395
|
} catch (error) {
|
|
1393
1396
|
await turnSync.unavailable(agent.session.id, error);
|
|
1394
1397
|
} finally {
|
|
1398
|
+
invalidateProjectionWrites(agent.botId);
|
|
1395
1399
|
await computer.close();
|
|
1396
1400
|
}
|
|
1397
1401
|
}),
|
package/src/backend.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import type { Plugin } from "cordis";
|
|
5
5
|
import {
|
|
6
6
|
ComputerProtocolDecodeError,
|
|
7
|
-
|
|
7
|
+
decodeComputerCommandResponse,
|
|
8
8
|
decodeComputerCommandV1,
|
|
9
9
|
decodeComputerProjectionV1,
|
|
10
|
-
type
|
|
10
|
+
type ComputerCommandResponse,
|
|
11
11
|
type ComputerCommandV1,
|
|
12
12
|
type ComputerProjectionV1,
|
|
13
13
|
} from "./protocol.js";
|
|
@@ -18,7 +18,7 @@ export interface ComputerGatewayHost {
|
|
|
18
18
|
userId: string,
|
|
19
19
|
botId: string,
|
|
20
20
|
command: ComputerCommandV1,
|
|
21
|
-
): Promise<
|
|
21
|
+
): Promise<ComputerCommandResponse>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface ComputerBackendRouteContribution {
|
|
@@ -144,7 +144,7 @@ export function createComputerBackendContribution(
|
|
|
144
144
|
);
|
|
145
145
|
}
|
|
146
146
|
return Response.json(
|
|
147
|
-
|
|
147
|
+
decodeComputerCommandResponse(
|
|
148
148
|
await host.executeComputerCommand(context.userId, botId, decoded),
|
|
149
149
|
),
|
|
150
150
|
);
|