@datalayer/agent-runtimes 1.3.105 → 1.3.108
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-embed/datalayer-app-main.js +85 -5
- package/lib/apps/apps/appspec.js +17 -0
- package/lib/apps/apps/checks.js +4 -0
- package/lib/apps/apps/computer.d.ts +55 -2
- package/lib/apps/apps/computer.js +82 -4
- package/lib/apps/plugins/app-computer/AppComputer.d.ts +7 -3
- package/lib/apps/plugins/app-computer/AppComputer.js +91 -4
- package/lib/apps/plugins/pap-example/index.d.ts +23 -0
- package/lib/apps/plugins/pap-example/index.js +25 -0
- package/lib/components/inspector/a2aSpans.js +8 -1
- package/lib/components/teams/SceneTranscript.js +8 -3
- package/lib/examples/LoopAppComputerExample.d.ts +4 -3
- package/lib/examples/LoopAppComputerExample.js +11 -6
- package/lib/examples/PapAgentIdentityExample.d.ts +4 -0
- package/lib/examples/PapAgentIdentityExample.js +23 -0
- package/lib/examples/PapAuthorizationBoundaryExample.d.ts +4 -0
- package/lib/examples/PapAuthorizationBoundaryExample.js +23 -0
- package/lib/examples/PapCompanyDiscoveryExample.d.ts +4 -0
- package/lib/examples/PapCompanyDiscoveryExample.js +23 -0
- package/lib/examples/PapDpopProofExample.d.ts +4 -0
- package/lib/examples/PapDpopProofExample.js +23 -0
- package/lib/examples/PapGuidedJourneyExample.d.ts +4 -0
- package/lib/examples/PapGuidedJourneyExample.js +23 -0
- package/lib/examples/PapSessionLifecycleExample.d.ts +4 -0
- package/lib/examples/PapSessionLifecycleExample.js +23 -0
- package/lib/examples/example-selector.js +9 -0
- package/lib/examples/exampleGroups.d.ts +1 -1
- package/lib/examples/exampleGroups.js +3 -0
- package/lib/examples/index.d.ts +6 -0
- package/lib/examples/index.js +6 -0
- package/lib/examples/main.js +9 -0
- package/lib/examples/pap/PapAgentIdentityView.d.ts +4 -0
- package/lib/examples/pap/PapAgentIdentityView.js +55 -0
- package/lib/examples/pap/PapAuthorizationBoundaryView.d.ts +4 -0
- package/lib/examples/pap/PapAuthorizationBoundaryView.js +59 -0
- package/lib/examples/pap/PapCompanyDiscoveryView.d.ts +4 -0
- package/lib/examples/pap/PapCompanyDiscoveryView.js +62 -0
- package/lib/examples/pap/PapDpopProofView.d.ts +4 -0
- package/lib/examples/pap/PapDpopProofView.js +57 -0
- package/lib/examples/pap/PapExamplePage.d.ts +14 -0
- package/lib/examples/pap/PapExamplePage.js +6 -0
- package/lib/examples/pap/PapGuidedJourneyView.d.ts +4 -0
- package/lib/examples/pap/PapGuidedJourneyView.js +448 -0
- package/lib/examples/pap/PapSessionLifecycleView.d.ts +4 -0
- package/lib/examples/pap/PapSessionLifecycleView.js +103 -0
- package/lib/specs/actions.js +22 -0
- package/lib/specs/apps.js +76 -15
- package/lib/specs/appspecSchema.js +27 -0
- package/lib/types/agentspecs.d.ts +14 -0
- package/package.json +2 -2
- package/scripts/codegen/generate_apps.py +9 -0
- package/scripts/validate_pap_examples.py +62 -0
package/lib/apps/apps/appspec.js
CHANGED
|
@@ -694,6 +694,15 @@ export function parseAppspec(document) {
|
|
|
694
694
|
expect: text(testCase.expect),
|
|
695
695
|
// Decided by its code (LOOP P-06), when it names the function.
|
|
696
696
|
...(text(testCase.code) ? { code: text(testCase.code) } : {}),
|
|
697
|
+
// Given files with what it is asked, when it is.
|
|
698
|
+
...(records(testCase.files).length
|
|
699
|
+
? {
|
|
700
|
+
files: records(testCase.files).map(file => ({
|
|
701
|
+
name: text(file.name),
|
|
702
|
+
text: text(file.text),
|
|
703
|
+
})),
|
|
704
|
+
}
|
|
705
|
+
: {}),
|
|
697
706
|
})),
|
|
698
707
|
verified: {
|
|
699
708
|
live: texts(verified.live),
|
|
@@ -1012,6 +1021,14 @@ export function dumpAppspec(app) {
|
|
|
1012
1021
|
ask: testCase.ask,
|
|
1013
1022
|
expect: testCase.expect,
|
|
1014
1023
|
...(testCase.code ? { code: testCase.code } : {}),
|
|
1024
|
+
...(testCase.files?.length
|
|
1025
|
+
? {
|
|
1026
|
+
files: testCase.files.map(file => ({
|
|
1027
|
+
name: file.name,
|
|
1028
|
+
text: file.text,
|
|
1029
|
+
})),
|
|
1030
|
+
}
|
|
1031
|
+
: {}),
|
|
1015
1032
|
})))
|
|
1016
1033
|
.part('verified', new Writer()
|
|
1017
1034
|
.list('live', app.tests.verified.live)
|
package/lib/apps/apps/checks.js
CHANGED
|
@@ -1033,6 +1033,10 @@ export function documentShapeProblems(document) {
|
|
|
1033
1033
|
records(tests.cases, 'tests.cases', (item, where) => {
|
|
1034
1034
|
required(item, 'ask', where);
|
|
1035
1035
|
required(item, 'expect', where);
|
|
1036
|
+
records(item.files, `${where}.files`, (file, at) => {
|
|
1037
|
+
required(file, 'name', at);
|
|
1038
|
+
required(file, 'text', at);
|
|
1039
|
+
});
|
|
1036
1040
|
});
|
|
1037
1041
|
});
|
|
1038
1042
|
mapping(d.record, 'record', record => {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* An application's computer (LOOP R-23, I-01): the sandbox its agent runs on,
|
|
3
3
|
* as the computer view shows it — its three parts, each off until it is
|
|
4
4
|
* turned on; what its agent ran on it, from the conversation's tool calls;
|
|
5
|
-
* its files, read-only; and *Take over* and
|
|
5
|
+
* its files, read-only; its browser's page, live; and *Take over* and
|
|
6
|
+
* *Hand back* — the person then clicks and types in that page — through the
|
|
6
7
|
* runtime's `/api/v1/apps/agents/{agent}/computer` routes.
|
|
7
8
|
*
|
|
8
9
|
* Pure but for `fetch`: no React.
|
|
@@ -25,7 +26,17 @@ export declare const COMPUTER_WORDS: {
|
|
|
25
26
|
readonly off: "off";
|
|
26
27
|
readonly none: "It has no computer: browse, files and shell are all off. Turn one on in its permissions to give it one.";
|
|
27
28
|
readonly offSaid: (parts: string[]) => string;
|
|
28
|
-
readonly
|
|
29
|
+
readonly browser: "Browser";
|
|
30
|
+
readonly noBrowser: "No browser is installed on its computer.";
|
|
31
|
+
readonly noPage: "Its browser has opened no page yet.";
|
|
32
|
+
readonly pageAlt: (title: string) => string;
|
|
33
|
+
readonly clickToAct: "Click on the page to click there; with the page selected, what you type goes to it.";
|
|
34
|
+
readonly typeOnPage: "Text to type on the page";
|
|
35
|
+
readonly type: "Type";
|
|
36
|
+
readonly address: "An address to open";
|
|
37
|
+
readonly open: "Open";
|
|
38
|
+
readonly scrollUp: "Scroll up";
|
|
39
|
+
readonly scrollDown: "Scroll down";
|
|
29
40
|
readonly agentHas: "Its agent has the computer.";
|
|
30
41
|
readonly youHave: "You have the computer: its agent waits for it until you hand it back.";
|
|
31
42
|
readonly someoneHas: "Somebody else has taken this computer over.";
|
|
@@ -84,7 +95,10 @@ export type ComputerState = {
|
|
|
84
95
|
agent: string;
|
|
85
96
|
app: string;
|
|
86
97
|
parts: ComputerParts;
|
|
98
|
+
/** Whether a browser is installed where its agent runs, browse on. */
|
|
87
99
|
browser: boolean;
|
|
100
|
+
/** The page its browser has open; none before its first, or browse off. */
|
|
101
|
+
page?: BrowserPage | null;
|
|
88
102
|
started: boolean;
|
|
89
103
|
held: ComputerHolder | null;
|
|
90
104
|
/** Whether the person asking is who has it. */
|
|
@@ -96,6 +110,11 @@ export type ComputerState = {
|
|
|
96
110
|
*/
|
|
97
111
|
servedFrom?: string;
|
|
98
112
|
};
|
|
113
|
+
/** A page its browser has open. */
|
|
114
|
+
export type BrowserPage = {
|
|
115
|
+
url: string;
|
|
116
|
+
title: string;
|
|
117
|
+
};
|
|
99
118
|
/** One entry of a directory of its computer. */
|
|
100
119
|
export type ComputerFile = {
|
|
101
120
|
name: string;
|
|
@@ -154,5 +173,39 @@ export declare const takeOverComputer: (context: ComputerContext) => Promise<{
|
|
|
154
173
|
export declare const handBackComputer: (context: ComputerContext) => Promise<void>;
|
|
155
174
|
/** Run code on it, as the person who took it over. */
|
|
156
175
|
export declare const runOnComputer: (context: ComputerContext, code: string) => Promise<RunOutput>;
|
|
176
|
+
/** The keys a person presses on its page, as the runtime takes them. */
|
|
177
|
+
export declare const BROWSER_KEYS: readonly ["Enter", "Tab", "Backspace", "Escape", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "PageUp", "PageDown"];
|
|
178
|
+
/**
|
|
179
|
+
* The page its browser shows, as a PNG; undefined before its first page
|
|
180
|
+
* (the runtime answers 404).
|
|
181
|
+
*/
|
|
182
|
+
export declare const readBrowserScreenshot: (context: ComputerContext) => Promise<Blob | undefined>;
|
|
183
|
+
/** Click on its page, at a point of its screenshot, as who took it over. */
|
|
184
|
+
export declare const clickOnBrowser: (context: ComputerContext, x: number, y: number) => Promise<BrowserPage>;
|
|
185
|
+
/** Type where its page's focus is. */
|
|
186
|
+
export declare const typeOnBrowser: (context: ComputerContext, text: string) => Promise<BrowserPage>;
|
|
187
|
+
/** Press one of `BROWSER_KEYS` on its page. */
|
|
188
|
+
export declare const pressOnBrowser: (context: ComputerContext, key: string) => Promise<BrowserPage>;
|
|
189
|
+
/** Scroll its page, down for a positive `dy`. */
|
|
190
|
+
export declare const scrollBrowser: (context: ComputerContext, dy: number) => Promise<BrowserPage>;
|
|
191
|
+
/** Open an address in its browser. */
|
|
192
|
+
export declare const openOnBrowser: (context: ComputerContext, url: string) => Promise<BrowserPage>;
|
|
193
|
+
/**
|
|
194
|
+
* Where a click on a screenshot drawn at another size lands on the page: its
|
|
195
|
+
* pixels are the page's (the runtime takes it at the page's own size).
|
|
196
|
+
*/
|
|
197
|
+
export declare function pointOnPage(clicked: {
|
|
198
|
+
x: number;
|
|
199
|
+
y: number;
|
|
200
|
+
}, drawn: {
|
|
201
|
+
width: number;
|
|
202
|
+
height: number;
|
|
203
|
+
}, natural: {
|
|
204
|
+
width: number;
|
|
205
|
+
height: number;
|
|
206
|
+
}): {
|
|
207
|
+
x: number;
|
|
208
|
+
y: number;
|
|
209
|
+
};
|
|
157
210
|
/** The directory above a path of its working directory. */
|
|
158
211
|
export declare function parentOf(path: string): string;
|
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
export const COMPUTER_PARTS = ['browse', 'files', 'shell'];
|
|
7
7
|
/** The tools of each part, as the runtime gives them (`loop/apps/computer.py`). */
|
|
8
8
|
export const COMPUTER_PART_TOOLS = {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
browse: [
|
|
10
|
+
'open_page',
|
|
11
|
+
'read_page',
|
|
12
|
+
'page_screenshot',
|
|
13
|
+
'click_on_page',
|
|
14
|
+
'type_on_page',
|
|
15
|
+
],
|
|
11
16
|
files: ['list_computer_files', 'read_computer_file', 'write_computer_file'],
|
|
12
17
|
shell: ['execute_code', 'run_skill_script'],
|
|
13
18
|
};
|
|
@@ -23,7 +28,17 @@ export const COMPUTER_WORDS = {
|
|
|
23
28
|
off: 'off',
|
|
24
29
|
none: 'It has no computer: browse, files and shell are all off. Turn one on in its permissions to give it one.',
|
|
25
30
|
offSaid: (parts) => `Off, so its agent is given no tool for it: ${parts.join(', ')}.`,
|
|
26
|
-
|
|
31
|
+
browser: 'Browser',
|
|
32
|
+
noBrowser: 'No browser is installed on its computer.',
|
|
33
|
+
noPage: 'Its browser has opened no page yet.',
|
|
34
|
+
pageAlt: (title) => `The page its browser shows: ${title}`,
|
|
35
|
+
clickToAct: 'Click on the page to click there; with the page selected, what you type goes to it.',
|
|
36
|
+
typeOnPage: 'Text to type on the page',
|
|
37
|
+
type: 'Type',
|
|
38
|
+
address: 'An address to open',
|
|
39
|
+
open: 'Open',
|
|
40
|
+
scrollUp: 'Scroll up',
|
|
41
|
+
scrollDown: 'Scroll down',
|
|
27
42
|
agentHas: 'Its agent has the computer.',
|
|
28
43
|
youHave: 'You have the computer: its agent waits for it until you hand it back.',
|
|
29
44
|
someoneHas: 'Somebody else has taken this computer over.',
|
|
@@ -105,7 +120,7 @@ export function outputOf(value) {
|
|
|
105
120
|
: text;
|
|
106
121
|
}
|
|
107
122
|
function commandOf(args) {
|
|
108
|
-
for (const key of ['code', 'path', 'script_name']) {
|
|
123
|
+
for (const key of ['code', 'path', 'script_name', 'url', 'selector']) {
|
|
109
124
|
const value = args[key];
|
|
110
125
|
if (typeof value === 'string' && value.trim()) {
|
|
111
126
|
return value;
|
|
@@ -225,6 +240,69 @@ export const runOnComputer = async (context, code) => (await asked(context, '/ru
|
|
|
225
240
|
method: 'POST',
|
|
226
241
|
body: JSON.stringify({ code }),
|
|
227
242
|
})).json();
|
|
243
|
+
/** The keys a person presses on its page, as the runtime takes them. */
|
|
244
|
+
export const BROWSER_KEYS = [
|
|
245
|
+
'Enter',
|
|
246
|
+
'Tab',
|
|
247
|
+
'Backspace',
|
|
248
|
+
'Escape',
|
|
249
|
+
'ArrowUp',
|
|
250
|
+
'ArrowDown',
|
|
251
|
+
'ArrowLeft',
|
|
252
|
+
'ArrowRight',
|
|
253
|
+
'PageUp',
|
|
254
|
+
'PageDown',
|
|
255
|
+
];
|
|
256
|
+
/**
|
|
257
|
+
* The page its browser shows, as a PNG; undefined before its first page
|
|
258
|
+
* (the runtime answers 404).
|
|
259
|
+
*/
|
|
260
|
+
export const readBrowserScreenshot = async (context) => {
|
|
261
|
+
const response = await fetch(computerUrl(context, '/browser/screenshot'), {
|
|
262
|
+
headers: context.token ? { Authorization: `Bearer ${context.token}` } : {},
|
|
263
|
+
cache: 'no-store',
|
|
264
|
+
});
|
|
265
|
+
if (response.status === 404) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
let detail = `${response.status}`;
|
|
270
|
+
try {
|
|
271
|
+
const body = (await response.json());
|
|
272
|
+
detail = typeof body.detail === 'string' ? body.detail : detail;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// A refusal without a sentence: its status says it.
|
|
276
|
+
}
|
|
277
|
+
throw new Error(detail);
|
|
278
|
+
}
|
|
279
|
+
return response.blob();
|
|
280
|
+
};
|
|
281
|
+
const onBrowser = async (context, step, body) => (await (await asked(context, `/browser/${step}`, {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
body: JSON.stringify(body),
|
|
284
|
+
})).json()).page;
|
|
285
|
+
/** Click on its page, at a point of its screenshot, as who took it over. */
|
|
286
|
+
export const clickOnBrowser = (context, x, y) => onBrowser(context, 'click', { x, y });
|
|
287
|
+
/** Type where its page's focus is. */
|
|
288
|
+
export const typeOnBrowser = (context, text) => onBrowser(context, 'type', { text });
|
|
289
|
+
/** Press one of `BROWSER_KEYS` on its page. */
|
|
290
|
+
export const pressOnBrowser = (context, key) => onBrowser(context, 'key', { key });
|
|
291
|
+
/** Scroll its page, down for a positive `dy`. */
|
|
292
|
+
export const scrollBrowser = (context, dy) => onBrowser(context, 'scroll', { dy });
|
|
293
|
+
/** Open an address in its browser. */
|
|
294
|
+
export const openOnBrowser = (context, url) => onBrowser(context, 'open', { url });
|
|
295
|
+
/**
|
|
296
|
+
* Where a click on a screenshot drawn at another size lands on the page: its
|
|
297
|
+
* pixels are the page's (the runtime takes it at the page's own size).
|
|
298
|
+
*/
|
|
299
|
+
export function pointOnPage(clicked, drawn, natural) {
|
|
300
|
+
const scale = (value, from, to) => from > 0 ? Math.round((value * to) / from) : value;
|
|
301
|
+
return {
|
|
302
|
+
x: Math.max(0, Math.min(natural.width, scale(clicked.x, drawn.width, natural.width))),
|
|
303
|
+
y: Math.max(0, Math.min(natural.height, scale(clicked.y, drawn.height, natural.height))),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
228
306
|
/** The directory above a path of its working directory. */
|
|
229
307
|
export function parentOf(path) {
|
|
230
308
|
const parts = path.split('/').filter(part => part && part !== '.');
|
|
@@ -8,16 +8,20 @@
|
|
|
8
8
|
* wrote, each with what came back, streamed from the conversation's tool
|
|
9
9
|
* calls;
|
|
10
10
|
* - its **files**, read-only, each to download, read again as each turn ends;
|
|
11
|
-
* - its **browser
|
|
11
|
+
* - its **browser**, when browse is on: the page it has open, live — a
|
|
12
|
+
* screenshot read again every moment;
|
|
12
13
|
* - **Take over**: what runs on it is interrupted and its agent's calls to it
|
|
13
|
-
* wait while the person runs code on it themselves
|
|
14
|
-
*
|
|
14
|
+
* wait while the person runs code on it themselves, and clicks and types
|
|
15
|
+
* in its page — a click on the screenshot is a click there; **Hand back**
|
|
16
|
+
* lets its agent go on.
|
|
15
17
|
*
|
|
16
18
|
* @module apps/plugins/app-computer/AppComputer
|
|
17
19
|
*/
|
|
18
20
|
import type { JSX } from 'react';
|
|
19
21
|
import type { AppSpec } from '../../../types/agentspecs';
|
|
20
22
|
import { type LoopWorkspaceContext } from '../../core';
|
|
23
|
+
/** How often the page its browser shows is read again, in milliseconds. */
|
|
24
|
+
export declare const BROWSER_POLL_MS = 1500;
|
|
21
25
|
export declare function AppComputer({ app, workspace, }: {
|
|
22
26
|
app: AppSpec;
|
|
23
27
|
workspace?: LoopWorkspaceContext;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { Button, Heading, Label, Spinner, Text, Textarea } from '@primer/react';
|
|
3
|
+
import { Button, Heading, Label, Spinner, Text, TextInput, Textarea, } from '@primer/react';
|
|
4
4
|
import { Box } from '@datalayer/primer-addons';
|
|
5
5
|
import { signal } from '@datalayer/reactor';
|
|
6
6
|
import { useContributions, useSignalValue } from '@datalayer/reactor/react';
|
|
7
7
|
import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
|
|
8
8
|
import { useChatWords } from '../../../chat/ChatLanguage';
|
|
9
9
|
import { LoopChatTurn, agentServerOf, noRuntimeSaid, IDLE_SANDBOX_SNAPSHOT, } from '../../core';
|
|
10
|
-
import { COMPUTER_PARTS, COMPUTER_WORDS, computerParts, downloadComputerFile, handBackComputer, hasComputer, listComputerFiles, parentOf, partsOff, readComputer, runOnComputer, takeOverComputer, terminalEntries, } from '../../apps/computer';
|
|
10
|
+
import { BROWSER_KEYS, COMPUTER_PARTS, COMPUTER_WORDS, clickOnBrowser, computerParts, downloadComputerFile, handBackComputer, hasComputer, listComputerFiles, openOnBrowser, parentOf, partsOff, pointOnPage, pressOnBrowser, readBrowserScreenshot, readComputer, runOnComputer, scrollBrowser, takeOverComputer, terminalEntries, typeOnBrowser, } from '../../apps/computer';
|
|
11
11
|
const NO_TURN = signal({ id: 0, status: 'idle' });
|
|
12
12
|
const NO_CONVERSATION = signal([]);
|
|
13
13
|
const MONO_FACE = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace';
|
|
@@ -71,6 +71,93 @@ function Files({ context, state, ended, }) {
|
|
|
71
71
|
: String(failed)));
|
|
72
72
|
}, children: COMPUTER_WORDS.download })] })) }, entry.path)))] }))] }));
|
|
73
73
|
}
|
|
74
|
+
/** How often the page its browser shows is read again, in milliseconds. */
|
|
75
|
+
export const BROWSER_POLL_MS = 1500;
|
|
76
|
+
function BrowserView({ context, state, mine, }) {
|
|
77
|
+
const [shot, setShot] = useState();
|
|
78
|
+
const [none, setNone] = useState(false);
|
|
79
|
+
const [page, setPage] = useState(state.page);
|
|
80
|
+
const [error, setError] = useState('');
|
|
81
|
+
const [text, setText] = useState('');
|
|
82
|
+
const [address, setAddress] = useState('');
|
|
83
|
+
const [asked, setAsked] = useState(0);
|
|
84
|
+
const shown = useRef(undefined);
|
|
85
|
+
useEffect(() => setPage(state.page), [state.page]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!state.browser) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
let cancelled = false;
|
|
91
|
+
const look = () => readBrowserScreenshot(context).then(blob => {
|
|
92
|
+
if (cancelled) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setError('');
|
|
96
|
+
setNone(!blob);
|
|
97
|
+
if (shown.current) {
|
|
98
|
+
URL.revokeObjectURL(shown.current);
|
|
99
|
+
}
|
|
100
|
+
shown.current = blob ? URL.createObjectURL(blob) : undefined;
|
|
101
|
+
setShot(shown.current);
|
|
102
|
+
}, (failed) => !cancelled &&
|
|
103
|
+
setError(failed instanceof Error ? failed.message : String(failed)));
|
|
104
|
+
void look();
|
|
105
|
+
const every = setInterval(look, BROWSER_POLL_MS);
|
|
106
|
+
return () => {
|
|
107
|
+
cancelled = true;
|
|
108
|
+
clearInterval(every);
|
|
109
|
+
};
|
|
110
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
111
|
+
}, [context.serverUrl, context.agentId, context.token, state.browser, asked]);
|
|
112
|
+
useEffect(() => () => {
|
|
113
|
+
if (shown.current) {
|
|
114
|
+
URL.revokeObjectURL(shown.current);
|
|
115
|
+
}
|
|
116
|
+
}, []);
|
|
117
|
+
const act = (step) => step.then(found => {
|
|
118
|
+
setPage(found);
|
|
119
|
+
setError('');
|
|
120
|
+
setAsked(previous => previous + 1);
|
|
121
|
+
}, (failed) => setError(failed instanceof Error ? failed.message : String(failed)));
|
|
122
|
+
const onClick = (event) => {
|
|
123
|
+
const image = event.currentTarget;
|
|
124
|
+
const box = image.getBoundingClientRect();
|
|
125
|
+
const point = pointOnPage({ x: event.clientX - box.left, y: event.clientY - box.top }, { width: box.width, height: box.height }, { width: image.naturalWidth, height: image.naturalHeight });
|
|
126
|
+
image.focus();
|
|
127
|
+
void act(clickOnBrowser(context, point.x, point.y));
|
|
128
|
+
};
|
|
129
|
+
const onKey = (event) => {
|
|
130
|
+
if (event.metaKey || event.ctrlKey || event.altKey) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (BROWSER_KEYS.includes(event.key)) {
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
void act(pressOnBrowser(context, event.key));
|
|
136
|
+
}
|
|
137
|
+
else if (event.key.length === 1) {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
void act(typeOnBrowser(context, event.key));
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
return (_jsxs(Box, { "data-testid": "computer-browser", children: [_jsxs(Heading, { as: "h4", sx: { fontSize: 1, mt: 3, mb: 1 }, children: [COMPUTER_WORDS.browser, page ? (_jsx(Text, { sx: { fontWeight: 'normal', color: 'fg.muted' }, children: ` · ${page.title || page.url}` })) : null] }), !state.browser ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: COMPUTER_WORDS.noBrowser })) : none || !shot ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0 }, children: none ? COMPUTER_WORDS.noPage : null })) : (_jsxs(_Fragment, { children: [page ? (_jsx(Text, { as: "p", sx: {
|
|
143
|
+
fontFamily: MONO_FACE,
|
|
144
|
+
fontSize: 0,
|
|
145
|
+
color: 'fg.muted',
|
|
146
|
+
m: 0,
|
|
147
|
+
mb: 1,
|
|
148
|
+
wordBreak: 'break-all',
|
|
149
|
+
}, children: page.url })) : null, _jsx("img", { src: shot, alt: COMPUTER_WORDS.pageAlt(page?.title || page?.url || ''), "data-testid": "computer-browser-page", tabIndex: mine ? 0 : -1, onClick: mine ? onClick : undefined, onKeyDown: mine ? onKey : undefined, style: {
|
|
150
|
+
width: '100%',
|
|
151
|
+
height: 'auto',
|
|
152
|
+
display: 'block',
|
|
153
|
+
border: '1px solid var(--borderColor-muted, #d0d7de)',
|
|
154
|
+
borderRadius: 6,
|
|
155
|
+
cursor: mine ? 'pointer' : 'default',
|
|
156
|
+
} })] })), error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'danger.fg', m: 0, mt: 1 }, children: error })) : null, mine && state.browser ? (_jsxs(Box, { mt: 2, "data-testid": "computer-browser-controls", children: [_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0, mb: 1 }, children: COMPUTER_WORDS.clickToAct }), _jsxs(Box, { display: "flex", gap: 1, mb: 1, children: [_jsx(TextInput, { size: "small", "aria-label": COMPUTER_WORDS.typeOnPage, placeholder: COMPUTER_WORDS.typeOnPage, value: text, onChange: event => setText(event.target.value), sx: { flex: 1 } }), _jsx(Button, { size: "small", disabled: !text, onClick: () => {
|
|
157
|
+
void act(typeOnBrowser(context, text));
|
|
158
|
+
setText('');
|
|
159
|
+
}, children: COMPUTER_WORDS.type }), _jsx(Button, { size: "small", onClick: () => void act(pressOnBrowser(context, 'Enter')), children: "Enter" })] }), _jsxs(Box, { display: "flex", gap: 1, mb: 1, children: [_jsx(TextInput, { size: "small", "aria-label": COMPUTER_WORDS.address, placeholder: COMPUTER_WORDS.address, value: address, onChange: event => setAddress(event.target.value), sx: { flex: 1 } }), _jsx(Button, { size: "small", disabled: !address.trim(), onClick: () => void act(openOnBrowser(context, address.trim())), children: COMPUTER_WORDS.open })] }), _jsxs(Box, { display: "flex", gap: 1, children: [_jsx(Button, { size: "small", onClick: () => void act(scrollBrowser(context, -400)), children: COMPUTER_WORDS.scrollUp }), _jsx(Button, { size: "small", onClick: () => void act(scrollBrowser(context, 400)), children: COMPUTER_WORDS.scrollDown })] })] })) : null] }));
|
|
160
|
+
}
|
|
74
161
|
function YourTerminal({ context }) {
|
|
75
162
|
const [code, setCode] = useState('');
|
|
76
163
|
const [running, setRunning] = useState(false);
|
|
@@ -193,10 +280,10 @@ export function AppComputer({ app, workspace, }) {
|
|
|
193
280
|
});
|
|
194
281
|
};
|
|
195
282
|
const me = Boolean(state?.yours);
|
|
196
|
-
return (_jsxs(Box, { "data-testid": "app-computer", children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: COMPUTER_WORDS.title }), _jsx(Parts, { app: app }), !used ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.none })) : (_jsxs(_Fragment, { children: [
|
|
283
|
+
return (_jsxs(Box, { "data-testid": "app-computer", children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: COMPUTER_WORDS.title }), _jsx(Parts, { app: app }), !used ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.none })) : (_jsxs(_Fragment, { children: [noRuntime ? (_jsx(Text, { as: "p", role: "status", sx: { fontSize: 1, color: 'fg.default', m: 0, mt: 2 }, children: noRuntime })) : null, error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'danger.fg', m: 0, mt: 2 }, children: error })) : null, state && context ? (_jsxs(Box, { mt: 2, "data-testid": "computer-holder", children: [_jsx(Text, { as: "p", sx: { fontSize: 1, m: 0, mb: 1 }, children: !state.held
|
|
197
284
|
? COMPUTER_WORDS.agentHas
|
|
198
285
|
: me
|
|
199
286
|
? COMPUTER_WORDS.youHave
|
|
200
|
-
: COMPUTER_WORDS.someoneHas }), !state.held ? (_jsx(Button, { size: "small", disabled: busy, onClick: () => act(takeOverComputer), children: COMPUTER_WORDS.takeOver })) : me ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "small", variant: "primary", disabled: busy, onClick: () => act(handBackComputer), children: COMPUTER_WORDS.handBack }), _jsx(YourTerminal, { context: context })] })) : null] })) : null, _jsx(Terminal, { conversation: conversation }), state && context ? (_jsx(Files, { context: context, state: state, ended: ended })) : null] }))] }));
|
|
287
|
+
: COMPUTER_WORDS.someoneHas }), !state.held ? (_jsx(Button, { size: "small", disabled: busy, onClick: () => act(takeOverComputer), children: COMPUTER_WORDS.takeOver })) : me ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "small", variant: "primary", disabled: busy, onClick: () => act(handBackComputer), children: COMPUTER_WORDS.handBack }), _jsx(YourTerminal, { context: context })] })) : null] })) : null, parts.browse && state && context ? (_jsx(BrowserView, { context: context, state: state, mine: me })) : null, _jsx(Terminal, { conversation: conversation }), state && context ? (_jsx(Files, { context: context, state: state, ended: ended })) : null] }))] }));
|
|
201
288
|
}
|
|
202
289
|
export default AppComputer;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mould for Personal Agent Protocol gallery examples.
|
|
3
|
+
*
|
|
4
|
+
* Each example is a Reactor plugin contributing one lazy workspace view. PAP
|
|
5
|
+
* therefore enters the Loop through the same extension boundary as A2A,
|
|
6
|
+
* A2UI, editors and runtime capacities—not through a page-specific switch.
|
|
7
|
+
*
|
|
8
|
+
* @module apps/plugins/pap-example
|
|
9
|
+
*/
|
|
10
|
+
import type { ComponentType } from 'react';
|
|
11
|
+
import { type ReactorPlugin } from '@datalayer/reactor';
|
|
12
|
+
import { type LoopViewProps } from '../../core';
|
|
13
|
+
export type PapExamplePluginOptions = {
|
|
14
|
+
key: string;
|
|
15
|
+
title: string;
|
|
16
|
+
description: string;
|
|
17
|
+
load: () => Promise<{
|
|
18
|
+
default: ComponentType<LoopViewProps>;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
/** Define one PAP workspace view as a Reactor plugin. */
|
|
22
|
+
export declare function definePapExamplePlugin(options: PapExamplePluginOptions): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
23
|
+
export default definePapExamplePlugin;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { contribution, definePlugin, } from '@datalayer/reactor';
|
|
6
|
+
import { LoopViewType } from '../../core';
|
|
7
|
+
/** Define one PAP workspace view as a Reactor plugin. */
|
|
8
|
+
export function definePapExamplePlugin(options) {
|
|
9
|
+
return definePlugin({
|
|
10
|
+
name: `@datalayer/loop-plugin-pap-example-${options.key}`,
|
|
11
|
+
displayName: options.title,
|
|
12
|
+
description: options.description,
|
|
13
|
+
octicon: 'shield-lock',
|
|
14
|
+
emoji: '🔐',
|
|
15
|
+
contributes: [
|
|
16
|
+
contribution(LoopViewType, {
|
|
17
|
+
viewType: `pap-example-${options.key}`,
|
|
18
|
+
title: options.title,
|
|
19
|
+
order: -10,
|
|
20
|
+
load: options.load,
|
|
21
|
+
}, { id: `pap-example-${options.key}`, order: -10 }),
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export default definePapExamplePlugin;
|
|
@@ -63,6 +63,11 @@ export function pressedAction(message) {
|
|
|
63
63
|
}
|
|
64
64
|
const FAILED_STATES = /FAILED|REJECTED|CANCELED/;
|
|
65
65
|
/** Records what came back for one request onto its span. */
|
|
66
|
+
/** A text cut at `length` characters, its line breaks kept. */
|
|
67
|
+
function clipped(text, length) {
|
|
68
|
+
const trimmed = text.trim();
|
|
69
|
+
return trimmed.length > length ? `${trimmed.slice(0, length - 1)}…` : trimmed;
|
|
70
|
+
}
|
|
66
71
|
class ResponseRecorder {
|
|
67
72
|
options;
|
|
68
73
|
span;
|
|
@@ -217,7 +222,9 @@ class ResponseRecorder {
|
|
|
217
222
|
this.span.addEvent('a2a.artifact_update', {
|
|
218
223
|
...chunk.attributes,
|
|
219
224
|
'a2a.artifact.chunks': chunk.events.length,
|
|
220
|
-
|
|
225
|
+
// Its words as they came — a table's rows, a list's lines — cut at
|
|
226
|
+
// 2000 characters, not run into one line: the transcript draws them.
|
|
227
|
+
'a2a.message.text': chunk.text ? clipped(chunk.text, 2000) : undefined,
|
|
221
228
|
'a2a.event': jsonAttribute(chunk.events.length === 1 ? chunk.events[0] : chunk.events),
|
|
222
229
|
});
|
|
223
230
|
}
|
|
@@ -3,6 +3,7 @@ import { useMemo, useState } from 'react';
|
|
|
3
3
|
import { Button, Text } from '@primer/react';
|
|
4
4
|
import { Box } from '@datalayer/primer-addons';
|
|
5
5
|
import { useOtelLiveSpans } from '@datalayer/core/lib/otel/live';
|
|
6
|
+
import { ChatMarkdown } from '../../chat/messages/ChatMarkdown';
|
|
6
7
|
import { lineHeading, timeText, transcriptOfSpans, transcriptText, } from './sceneTranscript';
|
|
7
8
|
/** The transcript of a tracer's spans, kept current as they change. */
|
|
8
9
|
export function useSceneTranscript(tracer, members) {
|
|
@@ -32,9 +33,13 @@ export function SceneTranscript({ lines, emptyText = 'What the scene says and do
|
|
|
32
33
|
fontSize: 0,
|
|
33
34
|
pt: '2px',
|
|
34
35
|
whiteSpace: 'nowrap',
|
|
35
|
-
}, dateTime: new Date(line.at).toISOString(), children: timeText(line.at) }), _jsxs(Box, { minWidth: 0, overflowWrap: "anywhere", children: [_jsxs(Text, { sx: { fontWeight: 600 }, children: [lineHeading(line), ": "] }), _jsxs(Text, { sx: {
|
|
36
|
-
fontFamily:
|
|
36
|
+
}, dateTime: new Date(line.at).toISOString(), children: timeText(line.at) }), line.kind === 'called' ? (_jsxs(Box, { minWidth: 0, overflowWrap: "anywhere", children: [_jsxs(Text, { sx: { fontWeight: 600 }, children: [lineHeading(line), ": "] }), _jsxs(Text, { sx: {
|
|
37
|
+
fontFamily: 'mono',
|
|
37
38
|
color: line.failed ? 'danger.fg' : undefined,
|
|
38
|
-
}, children: [line.text, line.open ? '…' : '', line.failed ? ' (failed)' : ''] })] })
|
|
39
|
+
}, children: [line.text, line.open ? '…' : '', line.failed ? ' (failed)' : ''] })] })) : (
|
|
40
|
+
// Words, drawn as the chat draws a message (`ChatMarkdown`):
|
|
41
|
+
// a table, a list or a code block an answer gives reads as
|
|
42
|
+
// one, not as its markdown run into one line (2026-10-10).
|
|
43
|
+
_jsxs(Box, { minWidth: 0, overflowWrap: "anywhere", children: [_jsxs(Text, { sx: { fontWeight: 600 }, children: [lineHeading(line), ": "] }), _jsx(Box, { sx: { color: line.failed ? 'danger.fg' : undefined }, children: _jsx(ChatMarkdown, { text: `${line.text}${line.open ? '…' : ''}${line.failed ? ' (failed)' : ''}`, density: "compact" }) })] }))] }, line.key))) })), copy && lines.length > 0 && (_jsx(Box, { mt: 2, children: _jsx(Button, { size: "small", onClick: () => void onCopy(), "data-transcript-copy": "", children: copied ? 'Copied' : 'Copy as text' }) }))] }));
|
|
39
44
|
}
|
|
40
45
|
export default SceneTranscript;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* An application's computer, beside its page (LOOP R-23).
|
|
3
3
|
*
|
|
4
|
-
* An application whose files and shell are on, drawn as the Studio's Preview
|
|
4
|
+
* An application whose browse, files and shell are on, drawn as the Studio's Preview
|
|
5
5
|
* draws it — `AppRenderer` with its `sidebar` — on the Local target: its
|
|
6
6
|
* agent runs on the local agent-runtimes server, and the computer view beside
|
|
7
|
-
* it shows what ran on its sandbox, its
|
|
8
|
-
*
|
|
7
|
+
* it shows the page its browser has open, what ran on its sandbox, its
|
|
8
|
+
* files, and *Take over* — clicks and typing then go to its page — and
|
|
9
|
+
* *Hand back*.
|
|
9
10
|
*
|
|
10
11
|
* @module examples/LoopAppComputerExample
|
|
11
12
|
*/
|
|
@@ -6,11 +6,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
6
6
|
/**
|
|
7
7
|
* An application's computer, beside its page (LOOP R-23).
|
|
8
8
|
*
|
|
9
|
-
* An application whose files and shell are on, drawn as the Studio's Preview
|
|
9
|
+
* An application whose browse, files and shell are on, drawn as the Studio's Preview
|
|
10
10
|
* draws it — `AppRenderer` with its `sidebar` — on the Local target: its
|
|
11
11
|
* agent runs on the local agent-runtimes server, and the computer view beside
|
|
12
|
-
* it shows what ran on its sandbox, its
|
|
13
|
-
*
|
|
12
|
+
* it shows the page its browser has open, what ran on its sandbox, its
|
|
13
|
+
* files, and *Take over* — clicks and typing then go to its page — and
|
|
14
|
+
* *Hand back*.
|
|
14
15
|
*
|
|
15
16
|
* @module examples/LoopAppComputerExample
|
|
16
17
|
*/
|
|
@@ -29,14 +30,18 @@ function computerDesk() {
|
|
|
29
30
|
id: 'computer-desk',
|
|
30
31
|
name: 'Computer Desk',
|
|
31
32
|
emoji: '🖥️',
|
|
32
|
-
description: 'Works on its own computer: runs code, reads and writes files.',
|
|
33
|
+
description: 'Works on its own computer: opens web pages, runs code, reads and writes files.',
|
|
33
34
|
connections: [],
|
|
34
35
|
rules: [],
|
|
35
|
-
instructions: 'You have a computer: run Python on it with your tools, and keep what you make in files there.',
|
|
36
|
+
instructions: 'You have a computer: open web pages in its browser, run Python on it with your tools, and keep what you make in files there.',
|
|
36
37
|
interface: {
|
|
37
38
|
...base.interface,
|
|
38
39
|
welcome: 'I work on my own computer. Ask me to compute something.',
|
|
39
40
|
starters: [
|
|
41
|
+
{
|
|
42
|
+
label: 'Open a page',
|
|
43
|
+
message: 'Open https://example.com in your browser, tell me its title and take a screenshot.',
|
|
44
|
+
},
|
|
40
45
|
{
|
|
41
46
|
label: 'Write a file',
|
|
42
47
|
message: 'Run Python that writes the squares of 1 to 5 in squares.txt, then print the file.',
|
|
@@ -45,7 +50,7 @@ function computerDesk() {
|
|
|
45
50
|
},
|
|
46
51
|
permissions: {
|
|
47
52
|
...base.permissions,
|
|
48
|
-
computer: { browse:
|
|
53
|
+
computer: { browse: true, files: true, shell: true },
|
|
49
54
|
},
|
|
50
55
|
};
|
|
51
56
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/** PAP personal-agent identity mounted as a Reactor workspace plugin. */
|
|
7
|
+
import { useMemo } from 'react';
|
|
8
|
+
import { Box, setupPrimerPortals } from '@datalayer/primer-addons';
|
|
9
|
+
import { LoopEmbed } from '../apps';
|
|
10
|
+
import { definePapExamplePlugin } from '../apps/plugins/pap-example';
|
|
11
|
+
import { ThemedProvider } from './utils/themedProvider';
|
|
12
|
+
setupPrimerPortals();
|
|
13
|
+
const PapIdentityPlugin = definePapExamplePlugin({
|
|
14
|
+
key: 'agent-identity',
|
|
15
|
+
title: 'PAP Agent Identity',
|
|
16
|
+
description: 'Verified client and signing policy without JWK coordinates.',
|
|
17
|
+
load: () => import('./pap/PapAgentIdentityView'),
|
|
18
|
+
});
|
|
19
|
+
const PapAgentIdentityExample = () => {
|
|
20
|
+
const plugins = useMemo(() => [PapIdentityPlugin], []);
|
|
21
|
+
return (_jsx(ThemedProvider, { children: _jsx(Box, { height: "100vh", minHeight: 0, children: _jsx(LoopEmbed, { target: "browser", agentId: "loop-shell", defaultEditor: "none", showHeader: true, plugins: plugins }) }) }));
|
|
22
|
+
};
|
|
23
|
+
export default PapAgentIdentityExample;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/** PAP Session and Direct Sign-In boundaries as a Reactor workspace plugin. */
|
|
7
|
+
import { useMemo } from 'react';
|
|
8
|
+
import { Box, setupPrimerPortals } from '@datalayer/primer-addons';
|
|
9
|
+
import { LoopEmbed } from '../apps';
|
|
10
|
+
import { definePapExamplePlugin } from '../apps/plugins/pap-example';
|
|
11
|
+
import { ThemedProvider } from './utils/themedProvider';
|
|
12
|
+
setupPrimerPortals();
|
|
13
|
+
const PapAuthorizationPlugin = definePapExamplePlugin({
|
|
14
|
+
key: 'authorization-boundary',
|
|
15
|
+
title: 'PAP Authorization Boundary',
|
|
16
|
+
description: 'Real Session and PKCE material kept outside model context.',
|
|
17
|
+
load: () => import('./pap/PapAuthorizationBoundaryView'),
|
|
18
|
+
});
|
|
19
|
+
const PapAuthorizationBoundaryExample = () => {
|
|
20
|
+
const plugins = useMemo(() => [PapAuthorizationPlugin], []);
|
|
21
|
+
return (_jsx(ThemedProvider, { children: _jsx(Box, { height: "100vh", minHeight: 0, children: _jsx(LoopEmbed, { target: "browser", agentId: "loop-shell", defaultEditor: "none", showHeader: true, plugins: plugins }) }) }));
|
|
22
|
+
};
|
|
23
|
+
export default PapAuthorizationBoundaryExample;
|