@datalayer/agent-runtimes 1.3.86 → 1.3.88
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/lib/components/a2ui/datalayer/Download.d.ts +21 -0
- package/lib/components/a2ui/datalayer/Download.js +66 -0
- package/lib/components/a2ui/datalayer/drawn.d.ts +1 -1
- package/lib/components/a2ui/datalayer/drawn.js +1 -0
- package/lib/components/a2ui/datalayer/index.d.ts +2 -1
- package/lib/components/a2ui/datalayer/index.js +3 -0
- package/lib/components/inspector/a2aSpans.d.ts +9 -0
- package/lib/components/inspector/a2aSpans.js +11 -1
- package/lib/components/teams/A2ATeamGraph.d.ts +2 -2
- package/lib/components/teams/A2ATeamGraph.js +39 -22
- package/lib/components/teams/AnswerSurfaces.d.ts +56 -0
- package/lib/components/teams/AnswerSurfaces.js +27 -0
- package/lib/components/teams/AnswerSurfacesView.d.ts +14 -0
- package/lib/components/teams/AnswerSurfacesView.js +30 -0
- package/lib/components/teams/index.d.ts +1 -0
- package/lib/components/teams/index.js +1 -0
- package/lib/components/teams/useA2ATeam.d.ts +56 -2
- package/lib/components/teams/useA2ATeam.js +122 -3
- package/lib/hooks/useAgentRuntimes.d.ts +13 -0
- package/lib/hooks/useAgentRuntimes.js +11 -1
- package/lib/loop/apps/AppFeedback.js +11 -2
- package/lib/loop/apps/AppRenderer.d.ts +13 -1
- package/lib/loop/apps/AppRenderer.js +34 -1
- package/lib/loop/apps/AppRuntimePlugins.d.ts +8 -0
- package/lib/loop/apps/AppRuntimePlugins.js +87 -0
- package/lib/loop/apps/feedback.d.ts +7 -0
- package/lib/loop/apps/feedback.js +3 -0
- package/lib/loop/apps/pluginPair.d.ts +60 -0
- package/lib/loop/apps/pluginPair.js +44 -0
- package/lib/loop/apps/threads.d.ts +35 -0
- package/lib/loop/apps/threads.js +79 -0
- package/lib/loop/embed/LoopEmbed.d.ts +4 -3
- package/lib/loop/embed/hostBridge.d.ts +27 -0
- package/lib/loop/embed/hostBridge.js +47 -0
- package/lib/loop/index.d.ts +1 -1
- package/lib/loop/index.js +1 -1
- package/lib/loop/plugins/agent-capacity/index.d.ts +7 -0
- package/lib/loop/plugins/agent-capacity/index.js +3 -2
- package/lib/loop/plugins/agents/DatalayerAgentBridge.js +11 -0
- package/lib/loop/plugins/agents/appLaunch.d.ts +15 -0
- package/lib/loop/plugins/agents/appLaunch.js +28 -0
- package/lib/loop/plugins/chat/ChatView.js +177 -109
- package/lib/loop/shell/LoopWorkspace.d.ts +3 -3
- package/lib/loop/shell/LoopWorkspace.js +1 -1
- package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +10 -2
- package/lib/runtimes/actions.js +23 -0
- package/lib/runtimes/apis.d.ts +18 -0
- package/lib/runtimes/browser/a2aPeer.d.ts +19 -2
- package/lib/runtimes/browser/a2aPeer.js +13 -7
- package/lib/specs/apps.js +102 -30
- package/lib/specs/envvars.d.ts +2 -0
- package/lib/specs/envvars.js +22 -0
- package/lib/specs/outputs.d.ts +1 -0
- package/lib/specs/outputs.js +12 -0
- package/lib/specs/sceneSchema.d.ts +8 -0
- package/lib/specs/sceneSchema.js +863 -0
- package/lib/specs/scenes.js +238 -121
- package/lib/specs/teams/teams.js +23 -7
- package/lib/specs/uiPlugins.js +55 -0
- package/lib/stores/agentRuntimeStore.js +4 -0
- package/lib/types/chat.d.ts +6 -0
- package/lib/types/scenes.d.ts +7 -1
- package/package.json +1 -1
- package/scripts/codegen/generate_envvars.py +1 -1
- package/scripts/codegen/generate_mcp_servers.py +11 -7
- package/scripts/codegen/generate_scenes.py +43 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type OwnCommon } from './implementation';
|
|
2
|
+
export type DownloadProps = OwnCommon & {
|
|
3
|
+
name?: string;
|
|
4
|
+
url?: string;
|
|
5
|
+
media_type?: string;
|
|
6
|
+
size?: number;
|
|
7
|
+
description?: string;
|
|
8
|
+
};
|
|
9
|
+
/** How large a file is, in words. */
|
|
10
|
+
export declare function sizeInWords(size: number | undefined): string;
|
|
11
|
+
/** The file a block offers, or why it cannot offer it. */
|
|
12
|
+
export declare function downloadOf(props: Pick<DownloadProps, 'name' | 'url'>): {
|
|
13
|
+
name: string;
|
|
14
|
+
url: string;
|
|
15
|
+
} | {
|
|
16
|
+
problem: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function DownloadView({ props }: {
|
|
19
|
+
props: DownloadProps;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
export declare const Download: import("@a2ui/react/v0_9").ReactComponentImplementation;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* A file to download (STUDIO P-04): what an application gives to save — a
|
|
8
|
+
* report, a sheet, an export — by its name and its link, an http(s) link or
|
|
9
|
+
* the file itself as a `data:` URL. Its kind and size are said beside its
|
|
10
|
+
* name, its description under it. Pressing it saves the file under its name
|
|
11
|
+
* and dispatches the block's action (its `download` event), when it has one.
|
|
12
|
+
* A link of any other scheme is not followed: the block says so.
|
|
13
|
+
*
|
|
14
|
+
* @module components/a2ui/datalayer/Download
|
|
15
|
+
*/
|
|
16
|
+
import { useMemo } from 'react';
|
|
17
|
+
import { Link, Text } from '@primer/react';
|
|
18
|
+
import { DownloadIcon } from '@primer/octicons-react';
|
|
19
|
+
import { Box } from '@datalayer/primer-addons';
|
|
20
|
+
import { BlockFrame, Problem } from './parts';
|
|
21
|
+
import { ownImplementation } from './implementation';
|
|
22
|
+
/** The schemes a file is fetched by: the web's, or the file itself. */
|
|
23
|
+
const SCHEMES = /^(https?:\/\/|data:)/i;
|
|
24
|
+
/** How large a file is, in words. */
|
|
25
|
+
export function sizeInWords(size) {
|
|
26
|
+
if (typeof size !== 'number' || !Number.isFinite(size) || size < 0) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
if (size < 1024) {
|
|
30
|
+
return size === 1 ? '1 byte' : `${size} bytes`;
|
|
31
|
+
}
|
|
32
|
+
const units = ['KB', 'MB', 'GB'];
|
|
33
|
+
let value = size / 1024;
|
|
34
|
+
let unit = 0;
|
|
35
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
36
|
+
value /= 1024;
|
|
37
|
+
unit += 1;
|
|
38
|
+
}
|
|
39
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
|
40
|
+
}
|
|
41
|
+
/** The file a block offers, or why it cannot offer it. */
|
|
42
|
+
export function downloadOf(props) {
|
|
43
|
+
const name = typeof props.name === 'string' ? props.name.trim() : '';
|
|
44
|
+
const url = typeof props.url === 'string' ? props.url.trim() : '';
|
|
45
|
+
if (!name) {
|
|
46
|
+
return { problem: 'This file has no name.' };
|
|
47
|
+
}
|
|
48
|
+
if (!url) {
|
|
49
|
+
return { problem: `${name} has no link.` };
|
|
50
|
+
}
|
|
51
|
+
if (!SCHEMES.test(url)) {
|
|
52
|
+
return {
|
|
53
|
+
problem: `${name} is not offered: its link is neither an http(s) link nor a data: URL.`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { name, url };
|
|
57
|
+
}
|
|
58
|
+
export function DownloadView({ props }) {
|
|
59
|
+
const { action } = props;
|
|
60
|
+
const offered = useMemo(() => downloadOf({ name: props.name, url: props.url }), [props.name, props.url]);
|
|
61
|
+
const said = [props.media_type, sizeInWords(props.size)]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(' · ');
|
|
64
|
+
return (_jsx(BlockFrame, { label: "File to download", weight: props.weight, testId: "a2ui-download", children: 'problem' in offered ? (_jsx(Problem, { children: offered.problem })) : (_jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'flex-start' }, children: [_jsx(Box, { sx: { color: 'fg.muted', pt: '2px' }, children: _jsx(DownloadIcon, { size: 16, "aria-hidden": "true" }) }), _jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', minWidth: 0 }, children: [_jsxs(Box, { children: [_jsx(Link, { href: offered.url, download: offered.name, target: "_blank", rel: "noreferrer noopener", onClick: () => action?.(), sx: { fontWeight: 'semibold', wordBreak: 'break-word' }, children: offered.name }), said ? (_jsx(Text, { sx: { color: 'fg.muted', fontSize: 1, ml: 2 }, children: said })) : null] }), props.description ? (_jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: props.description })) : null] })] })) }));
|
|
65
|
+
}
|
|
66
|
+
export const Download = ownImplementation('Download', DownloadView);
|
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module components/a2ui/datalayer/drawn
|
|
8
8
|
*/
|
|
9
|
-
export declare const DRAWN_OWN_COMPONENTS: readonly ["Table", "Chart", "FileUpload", "Chat", "Evidence", "Form"];
|
|
9
|
+
export declare const DRAWN_OWN_COMPONENTS: readonly ["Table", "Chart", "FileUpload", "Chat", "Evidence", "Form", "Download"];
|
|
10
10
|
export type DrawnOwnComponent = (typeof DRAWN_OWN_COMPONENTS)[number];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Datalayer's own components, drawn (LOOP C-18): a renderer for each the
|
|
3
3
|
* catalog has beside A2UI's basic ones — Table, Chart, File upload, Chat,
|
|
4
|
-
* Evidence and
|
|
4
|
+
* Evidence, Form and File to download — made as A2UI makes its own, so that the decision
|
|
5
5
|
* surface, an application's page, the Surface view and the components page
|
|
6
6
|
* draw them as they draw a Button.
|
|
7
7
|
*
|
|
@@ -19,5 +19,6 @@ export { DRAWN_OWN_COMPONENTS, type DrawnOwnComponent } from './drawn';
|
|
|
19
19
|
export { chartOption, chartPoints, chartSeries } from './Chart';
|
|
20
20
|
export { chatItems } from './Chat';
|
|
21
21
|
export { evidenceSources } from './Evidence';
|
|
22
|
+
export { downloadOf, sizeInWords } from './Download';
|
|
22
23
|
export { refusal as fileRefusal, type GivenFile } from './FileUpload';
|
|
23
24
|
export { tableRows } from './Table';
|
|
@@ -10,6 +10,7 @@ import { FileUpload } from './FileUpload';
|
|
|
10
10
|
import { Chat } from './Chat';
|
|
11
11
|
import { Evidence } from './Evidence';
|
|
12
12
|
import { Form } from './Form';
|
|
13
|
+
import { Download } from './Download';
|
|
13
14
|
const RENDERERS = {
|
|
14
15
|
Table,
|
|
15
16
|
Chart,
|
|
@@ -17,6 +18,7 @@ const RENDERERS = {
|
|
|
17
18
|
Chat,
|
|
18
19
|
Evidence,
|
|
19
20
|
Form,
|
|
21
|
+
Download,
|
|
20
22
|
};
|
|
21
23
|
/**
|
|
22
24
|
* A renderer for every component of Datalayer's own the catalog has, in the
|
|
@@ -40,5 +42,6 @@ export { DRAWN_OWN_COMPONENTS } from './drawn';
|
|
|
40
42
|
export { chartOption, chartPoints, chartSeries } from './Chart';
|
|
41
43
|
export { chatItems } from './Chat';
|
|
42
44
|
export { evidenceSources } from './Evidence';
|
|
45
|
+
export { downloadOf, sizeInWords } from './Download';
|
|
43
46
|
export { refusal as fileRefusal } from './FileUpload';
|
|
44
47
|
export { tableRows } from './Table';
|
|
@@ -41,9 +41,18 @@ export type A2ASpansOptions = {
|
|
|
41
41
|
* Without it, a tool the peer's runtime runs.
|
|
42
42
|
*/
|
|
43
43
|
classifyTool?: (name: string, args?: Record<string, unknown>) => ToolClass;
|
|
44
|
+
/**
|
|
45
|
+
* Who presses the buttons of what the peer shows: a request carrying a
|
|
46
|
+
* pressed button's action (`loop.action` in its message's metadata) is
|
|
47
|
+
* theirs, not the asker's — the person (`You`) on a page that draws the
|
|
48
|
+
* surfaces (STUDIO H-02). Unsaid, every request is the asker's.
|
|
49
|
+
*/
|
|
50
|
+
pressedBy?: string;
|
|
44
51
|
};
|
|
45
52
|
/** The text of an A2A 1.0 message's or artifact's parts, as JSON carries them. */
|
|
46
53
|
export declare function partsText(parts: unknown): string;
|
|
54
|
+
/** Whether a message carries a pressed button's action: `metadata.loop.action`. */
|
|
55
|
+
export declare function pressedAction(message: unknown): boolean;
|
|
47
56
|
/**
|
|
48
57
|
* A fetch that records the A2A traffic it carries as spans into
|
|
49
58
|
* `options.tracer`, and hands the SDK what the network gave.
|
|
@@ -53,6 +53,14 @@ function stepOf(message) {
|
|
|
53
53
|
}
|
|
54
54
|
return undefined;
|
|
55
55
|
}
|
|
56
|
+
/** Whether a message carries a pressed button's action: `metadata.loop.action`. */
|
|
57
|
+
export function pressedAction(message) {
|
|
58
|
+
if (!isObject(message) || !isObject(message.metadata)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const loop = message.metadata.loop;
|
|
62
|
+
return isObject(loop) && isObject(loop.action);
|
|
63
|
+
}
|
|
56
64
|
const FAILED_STATES = /FAILED|REJECTED|CANCELED/;
|
|
57
65
|
/** Records what came back for one request onto its span. */
|
|
58
66
|
class ResponseRecorder {
|
|
@@ -288,7 +296,9 @@ export function traceA2AFetch(base, options) {
|
|
|
288
296
|
const params = rpc && isObject(rpc.params) ? rpc.params : undefined;
|
|
289
297
|
const message = params && isObject(params.message) ? params.message : undefined;
|
|
290
298
|
const span = tracer.startSpan(rpc ? `a2a ${method}` : `${httpMethod} ${path}`, {
|
|
291
|
-
serviceName:
|
|
299
|
+
serviceName: options.pressedBy && pressedAction(message)
|
|
300
|
+
? options.pressedBy
|
|
301
|
+
: asker,
|
|
292
302
|
kind: 'CLIENT',
|
|
293
303
|
attributes: {
|
|
294
304
|
'peer.service': peer,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* values the rows, top to bottom.
|
|
21
21
|
*
|
|
22
22
|
* A member's connections — the MCP servers it reaches, Odoo for Accounting —
|
|
23
|
-
*
|
|
23
|
+
* stand to its right, each a node half a member's size with the server's mark
|
|
24
24
|
* (`SpecMark`), linked to it by an edge of its own. That edge is still until
|
|
25
25
|
* the member calls one of the connection's tools: then it flows toward the
|
|
26
26
|
* connection for as long as the call runs ({@link A2ATeamCall}), with the
|
|
@@ -59,7 +59,7 @@ export type A2ATeamGraphMember = {
|
|
|
59
59
|
onToggle?: () => void;
|
|
60
60
|
/** It was sent away, or called back. */
|
|
61
61
|
onAway?: (away: boolean) => void;
|
|
62
|
-
/** The MCP servers it reaches, drawn
|
|
62
|
+
/** The MCP servers it reaches, drawn to its right (`teamConnectionsOf`). */
|
|
63
63
|
connections?: A2ATeamConnection[];
|
|
64
64
|
/**
|
|
65
65
|
* How its balloon shows what it says (LOOP T-23): only what it says or
|
|
@@ -25,9 +25,15 @@ const WIDTH = NODE_WIDTH * 2 + GAP;
|
|
|
25
25
|
const ROW_GAP = 48;
|
|
26
26
|
/** A connection's node: half a member's. */
|
|
27
27
|
const CONNECTION_WIDTH = NODE_WIDTH / 2;
|
|
28
|
-
/** Between a member and its connections. */
|
|
28
|
+
/** Between a member and its connections, to its right. */
|
|
29
29
|
const CONNECTION_GAP = 44;
|
|
30
30
|
const CONNECTIONS_APART = 16;
|
|
31
|
+
/** Where a member's connections start: right of it. */
|
|
32
|
+
const CONNECTION_X = NODE_WIDTH + CONNECTION_GAP;
|
|
33
|
+
/** Below the character's middle, so the A2A edge leaving it passes above. */
|
|
34
|
+
const CONNECTION_DROP = 12;
|
|
35
|
+
/** What a member's connections take beside it. */
|
|
36
|
+
const CONNECTIONS_BESIDE = CONNECTION_GAP + CONNECTION_WIDTH;
|
|
31
37
|
/** Where the edge leaves and reaches a member: the middle of its character. */
|
|
32
38
|
const HANDLE = (size) => ({
|
|
33
39
|
opacity: 0,
|
|
@@ -118,7 +124,7 @@ const Moving = createContext({
|
|
|
118
124
|
move: () => undefined,
|
|
119
125
|
reset: () => undefined,
|
|
120
126
|
});
|
|
121
|
-
/** A connection's node id:
|
|
127
|
+
/** A connection's node id: beside its member. */
|
|
122
128
|
const connectionNodeId = (member, connection) => `${member}/${connection}`;
|
|
123
129
|
const MemberNode = memo(function MemberNode({ data, }) {
|
|
124
130
|
const { id, side, size } = data;
|
|
@@ -282,7 +288,7 @@ const MemberNode = memo(function MemberNode({ data, }) {
|
|
|
282
288
|
stayPut: true,
|
|
283
289
|
// Named as the member, not its character: `Sales (in your
|
|
284
290
|
// browser)`; the character describes it.
|
|
285
|
-
agentName: member.name, label: memberLabel(member) })) }), _jsxs(Text, { sx: { fontWeight: 600, textAlign: 'center' }, children: [member.emoji ? `${member.emoji} ` : '', member.name] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', textAlign: 'center' }, "data-member-where": "", children: member.where }), _jsx(Handle, { id: MEMBER_HANDLES.outRight, type: "source", position: Position.Right, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outLeft, type: "source", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outBottom, type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: size }, isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outTop, type: "source", position: Position.Top, style: { ...HANDLE(size), top: 0 }, isConnectable: false }), member.connections?.length ? (_jsx(Handle, { id: MEMBER_HANDLES.connections, type: "source", position: Position.
|
|
291
|
+
agentName: member.name, label: memberLabel(member) })) }), _jsxs(Text, { sx: { fontWeight: 600, textAlign: 'center' }, children: [member.emoji ? `${member.emoji} ` : '', member.name] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', textAlign: 'center' }, "data-member-where": "", children: member.where }), _jsx(Handle, { id: MEMBER_HANDLES.outRight, type: "source", position: Position.Right, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outLeft, type: "source", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outBottom, type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: size }, isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outTop, type: "source", position: Position.Top, style: { ...HANDLE(size), top: 0 }, isConnectable: false }), member.connections?.length ? (_jsx(Handle, { id: MEMBER_HANDLES.connections, type: "source", position: Position.Right, style: { ...HANDLE(size), left: 'auto', right: 0 }, isConnectable: false })) : null] }));
|
|
286
292
|
});
|
|
287
293
|
/** A member's accessible name: its name, and where it runs. */
|
|
288
294
|
export function memberLabel(member) {
|
|
@@ -314,7 +320,7 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
|
|
|
314
320
|
gap: '2px',
|
|
315
321
|
cursor: 'default',
|
|
316
322
|
pointerEvents: 'all',
|
|
317
|
-
}, title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.
|
|
323
|
+
}, title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.Left, style: { ...HANDLE(size), left: 0 }, isConnectable: false }), _jsx(Box, { as: "button", type: "button", className: tool ? 'a2a-team-connection-busy' : undefined, "aria-label": connectionLabel(connection, owner?.name), "aria-expanded": told, "aria-controls": detailsId, "data-connection-figure": "", onClick: () => setTold(shown => !shown), onKeyDown: (event) => {
|
|
318
324
|
if (event.key === 'Escape' && told) {
|
|
319
325
|
event.stopPropagation();
|
|
320
326
|
setTold(false);
|
|
@@ -419,11 +425,19 @@ export const CallEdge = memo(function CallEdge({ id, sourceX, sourceY, targetX,
|
|
|
419
425
|
});
|
|
420
426
|
const NODE_TYPES = { member: MemberNode, connection: ConnectionNode };
|
|
421
427
|
const EDGE_TYPES = { a2a: LinkEdge, mcp: CallEdge };
|
|
422
|
-
/** Where a member's connections sit:
|
|
423
|
-
function
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
428
|
+
/** Where a member's connections sit: right of it, one under another from its character's middle. */
|
|
429
|
+
function connectionsY(memberY, size, count, connectionHeight) {
|
|
430
|
+
const top = memberY + size / 2 + CONNECTION_DROP;
|
|
431
|
+
return Array.from({ length: count }, (_, at) => top + at * (connectionHeight + CONNECTIONS_APART));
|
|
432
|
+
}
|
|
433
|
+
/** How far down a member's connections reach, from its top; none, 0. */
|
|
434
|
+
function connectionsReach(size, count, connectionHeight) {
|
|
435
|
+
return count
|
|
436
|
+
? size / 2 +
|
|
437
|
+
CONNECTION_DROP +
|
|
438
|
+
count * connectionHeight +
|
|
439
|
+
(count - 1) * CONNECTIONS_APART
|
|
440
|
+
: 0;
|
|
427
441
|
}
|
|
428
442
|
/** What a member's call says, on its edge and to a screen reader. */
|
|
429
443
|
export function callWords(member, connection, tool) {
|
|
@@ -502,7 +516,11 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
|
|
|
502
516
|
// Where the members stand, as columns and rows, and the width that takes.
|
|
503
517
|
const peerIds = peers.map(one => one.id);
|
|
504
518
|
const placing = placeMembers(entry.id, peerIds, positions);
|
|
505
|
-
const
|
|
519
|
+
const hasConnections = members.some(member => member.connections?.length);
|
|
520
|
+
// The last column's connections sit beside it, inside the graph.
|
|
521
|
+
const drawn = Math.max(WIDTH, placing.columns * NODE_WIDTH +
|
|
522
|
+
(placing.columns - 1) * GAP +
|
|
523
|
+
(hasConnections ? CONNECTIONS_BESIDE : 0));
|
|
506
524
|
const viewport = useMemo(() => teamViewport(width || drawn, drawn), [width, drawn]);
|
|
507
525
|
// The members' height: the character, its name and where it runs.
|
|
508
526
|
const memberHeight = size + 56;
|
|
@@ -516,14 +534,13 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
|
|
|
516
534
|
connections: (member.connections ?? []).map(connection => connection.id),
|
|
517
535
|
}));
|
|
518
536
|
const placedKey = JSON.stringify(placed);
|
|
519
|
-
const hasConnections = placed.some(({ connections }) => connections.length);
|
|
520
537
|
// A notebook in a balloon: more room above the members.
|
|
521
538
|
const room = members.some(member => member.persona.notebook)
|
|
522
539
|
? Math.max(NOTEBOOK_BALLOON_ROOM, balloonRoom)
|
|
523
540
|
: balloonRoom;
|
|
524
|
-
//
|
|
525
|
-
const
|
|
526
|
-
const rowHeight = memberHeight
|
|
541
|
+
// Beside a member, its connections may reach below it: the row takes them.
|
|
542
|
+
const reach = Math.max(0, ...placed.map(({ connections }) => connectionsReach(size, connections.length, connectionHeight)));
|
|
543
|
+
const rowHeight = Math.max(memberHeight, reach);
|
|
527
544
|
const total = room + placing.rows * rowHeight + (placing.rows - 1) * ROW_GAP;
|
|
528
545
|
const height = Math.ceil(total * viewport.zoom);
|
|
529
546
|
// Where each member is laid out, before it is moved.
|
|
@@ -537,14 +554,14 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
|
|
|
537
554
|
// Where the members were moved, kept inside the graph's box.
|
|
538
555
|
const boundsOf = (id) => {
|
|
539
556
|
const layout = layoutOf(id);
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
557
|
+
const count = placed.find(({ member }) => member === id)?.connections.length ?? 0;
|
|
558
|
+
const beside = count ? CONNECTIONS_BESIDE : 0;
|
|
559
|
+
const tall = Math.max(memberHeight, connectionsReach(size, count, connectionHeight));
|
|
543
560
|
return {
|
|
544
561
|
minDx: -layout.x,
|
|
545
|
-
maxDx: drawn - NODE_WIDTH - layout.x,
|
|
562
|
+
maxDx: drawn - NODE_WIDTH - beside - layout.x,
|
|
546
563
|
minDy: -layout.y,
|
|
547
|
-
maxDy: total - layout.y -
|
|
564
|
+
maxDy: total - layout.y - tall,
|
|
548
565
|
};
|
|
549
566
|
};
|
|
550
567
|
const [moved, setMoved] = useState(() => Object.fromEntries(members.map(member => [
|
|
@@ -597,13 +614,13 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
|
|
|
597
614
|
},
|
|
598
615
|
// A member's connections move with it.
|
|
599
616
|
...connections.map((connection, at) => {
|
|
600
|
-
const
|
|
617
|
+
const ys = connectionsY(y, size, connections.length, connectionHeight);
|
|
601
618
|
return {
|
|
602
619
|
id: connectionNodeId(id, connection),
|
|
603
620
|
type: 'connection',
|
|
604
621
|
position: {
|
|
605
|
-
x:
|
|
606
|
-
y:
|
|
622
|
+
x: x + CONNECTION_X,
|
|
623
|
+
y: ys[at],
|
|
607
624
|
},
|
|
608
625
|
width: CONNECTION_WIDTH,
|
|
609
626
|
height: connectionHeight,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the members of a team showed with their answers: components of the
|
|
3
|
+
* catalog, drawn under the conversation (STUDIO H-02).
|
|
4
|
+
*
|
|
5
|
+
* A member that gives `application/json+a2ui` shows an A2UI surface beside
|
|
6
|
+
* its words — the sources as cards that open (`Evidence`), a comparison as a
|
|
7
|
+
* table, a series as a chart, a choice as buttons that answer the
|
|
8
|
+
* application — one per artifact ({@link A2ATeamSurface}). Each is drawn
|
|
9
|
+
* with the A2UI surface plugin's `InlineSurface`, Datalayer's catalog, in
|
|
10
|
+
* the page's theme. A button pressed is the reader's answer to the member
|
|
11
|
+
* that showed it: its words as the next request and its action beside them
|
|
12
|
+
* (`pressedOf`, the `loop.action` the runtime reads), so that an option
|
|
13
|
+
* that does more than read is decided by the member's rules — and refused
|
|
14
|
+
* to a visitor in a sentence. What came back is said under the surface.
|
|
15
|
+
*
|
|
16
|
+
* This module is light: A2UI's renderer is in `AnswerSurfacesView`, loaded
|
|
17
|
+
* only once a surface is drawn.
|
|
18
|
+
*
|
|
19
|
+
* @module components/teams/AnswerSurfaces
|
|
20
|
+
*/
|
|
21
|
+
import type { JSX } from 'react';
|
|
22
|
+
import type { A2APeerAction } from '../../runtimes/browser/a2aPeer';
|
|
23
|
+
import type { A2ATeamSurface } from './useA2ATeam';
|
|
24
|
+
/** A button pressed on a surface, as it is sent back: its words, and its action. */
|
|
25
|
+
export type AnswerPressed = {
|
|
26
|
+
/** The reader's turn, in words: the option's label. */
|
|
27
|
+
message: string;
|
|
28
|
+
action: A2APeerAction;
|
|
29
|
+
/** What choosing it does, as the option said: `read`, `send`, `write`… */
|
|
30
|
+
does: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* What a button pressed on an answer's surface sends back, or `null` for an
|
|
34
|
+
* action that is not one of an answer's buttons (a form's submission).
|
|
35
|
+
*/
|
|
36
|
+
export declare function pressedOf(action: {
|
|
37
|
+
name: string;
|
|
38
|
+
surfaceId: string;
|
|
39
|
+
context?: Record<string, unknown>;
|
|
40
|
+
}): AnswerPressed | null;
|
|
41
|
+
export type AnswerSurfacesProps = {
|
|
42
|
+
/** The surfaces shown, in the order they came. */
|
|
43
|
+
surfaces: A2ATeamSurface[];
|
|
44
|
+
/** Each member's name, by its id: who showed a surface. */
|
|
45
|
+
names?: Record<string, string>;
|
|
46
|
+
/**
|
|
47
|
+
* A button pressed on one of them: ask the member that showed it, and
|
|
48
|
+
* answer what it said, in words, to be shown under the surface. A team's
|
|
49
|
+
* `pressAction(surface.giver, pressed)` makes the press a turn of its
|
|
50
|
+
* conversation too. A press that throws is said, and another is taken.
|
|
51
|
+
*/
|
|
52
|
+
onPress?: (surface: A2ATeamSurface, pressed: AnswerPressed) => Promise<string | undefined>;
|
|
53
|
+
};
|
|
54
|
+
/** The surfaces the members showed, each under who showed it; nothing when there is none. */
|
|
55
|
+
export declare function AnswerSurfaces(props: AnswerSurfacesProps): JSX.Element | null;
|
|
56
|
+
export default AnswerSurfaces;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, lazy } from 'react';
|
|
3
|
+
import { Text } from '@primer/react';
|
|
4
|
+
import { answerAction } from '../../loop/plugins/a2ui-surface/toolResult';
|
|
5
|
+
/** The renderer itself: A2UI and the catalog, loaded when a surface is drawn. */
|
|
6
|
+
const AnswerSurfacesView = lazy(() => import('./AnswerSurfacesView'));
|
|
7
|
+
/**
|
|
8
|
+
* What a button pressed on an answer's surface sends back, or `null` for an
|
|
9
|
+
* action that is not one of an answer's buttons (a form's submission).
|
|
10
|
+
*/
|
|
11
|
+
export function pressedOf(action) {
|
|
12
|
+
const pressed = answerAction(action);
|
|
13
|
+
if (!pressed) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const { name, payload } = pressed.forwardedProps.loop.action;
|
|
17
|
+
const does = typeof payload.does === 'string' ? payload.does : 'read';
|
|
18
|
+
return { message: pressed.message, action: { name, payload }, does };
|
|
19
|
+
}
|
|
20
|
+
/** The surfaces the members showed, each under who showed it; nothing when there is none. */
|
|
21
|
+
export function AnswerSurfaces(props) {
|
|
22
|
+
if (!props.surfaces.length) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return (_jsx(Suspense, { fallback: _jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Drawing what it shows\u2026" }), children: _jsx(AnswerSurfacesView, { ...props }) }));
|
|
26
|
+
}
|
|
27
|
+
export default AnswerSurfaces;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The surfaces of `AnswerSurfaces`, drawn: each with `InlineSurface` on
|
|
3
|
+
* Datalayer's catalog, its title and who showed it above it, and under it
|
|
4
|
+
* what was chosen and what the member answered. One choice per surface:
|
|
5
|
+
* once a button is pressed the surface says what was chosen and asks no
|
|
6
|
+
* more — unless the press failed (the member not reached, a turn under
|
|
7
|
+
* way): what failed is said, and another press is taken.
|
|
8
|
+
*
|
|
9
|
+
* @module components/teams/AnswerSurfacesView
|
|
10
|
+
*/
|
|
11
|
+
import type { JSX } from 'react';
|
|
12
|
+
import { type AnswerSurfacesProps } from './AnswerSurfaces';
|
|
13
|
+
export declare function AnswerSurfacesView({ surfaces, names, onPress, }: AnswerSurfacesProps): JSX.Element;
|
|
14
|
+
export default AnswerSurfacesView;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
3
|
+
import { Spinner, Text } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { InlineSurface, SURFACE_CATALOG_ID, } from '../../loop/plugins/a2ui-surface/InlineSurface';
|
|
6
|
+
import { readA2uiToolResult } from '../../loop/plugins/a2ui-surface/toolResult';
|
|
7
|
+
import { pressedOf } from './AnswerSurfaces';
|
|
8
|
+
function OneSurface({ surface, name, onPress, }) {
|
|
9
|
+
const parsed = useMemo(() => readA2uiToolResult(surface.artifact.data, SURFACE_CATALOG_ID), [surface.artifact.data]);
|
|
10
|
+
const [answered, setAnswered] = useState(null);
|
|
11
|
+
const onAction = useCallback((action) => {
|
|
12
|
+
const pressed = pressedOf(action);
|
|
13
|
+
if (!pressed || (answered && !answered.failed) || !onPress) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
setAnswered({ chose: pressed.message, waiting: true });
|
|
17
|
+
onPress(surface, pressed).then(reply => setAnswered({ chose: pressed.message, reply, waiting: false }), (error) => setAnswered({
|
|
18
|
+
chose: pressed.message,
|
|
19
|
+
reply: error instanceof Error ? error.message : String(error),
|
|
20
|
+
waiting: false,
|
|
21
|
+
failed: true,
|
|
22
|
+
}));
|
|
23
|
+
}, [answered, onPress, surface]);
|
|
24
|
+
const title = parsed?.title || surface.artifact.name;
|
|
25
|
+
return (_jsxs(Box, { as: "section", "aria-label": name ? `${name}: ${title}` : title, "data-answer-surface": surface.id, sx: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 }, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 1 }, children: name ? `${name} · ${title}` : title }), parsed?.messages?.length ? (_jsx(InlineSurface, { messages: parsed.messages, onAction: onAction })) : (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "This could not be drawn: it holds no surface." })), answered ? (_jsxs(Box, { role: "status", "data-answer-reply": "", sx: { display: 'flex', flexDirection: 'column', gap: 1, fontSize: 1 }, children: [_jsxs(Text, { sx: { color: 'fg.muted' }, children: ["You chose: ", answered.chose] }), answered.waiting ? (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { color: 'fg.muted' }, children: name ? `Asking ${name}…` : 'Asking…' })] })) : answered.reply ? (_jsx(Text, { children: name ? `${name}: ${answered.reply}` : answered.reply })) : null] })) : null] }));
|
|
26
|
+
}
|
|
27
|
+
export function AnswerSurfacesView({ surfaces, names = {}, onPress, }) {
|
|
28
|
+
return (_jsx(Box, { "data-answer-surfaces": "", sx: { display: 'flex', flexDirection: 'column', gap: 3, minWidth: 0 }, children: surfaces.map(surface => (_jsx(OneSurface, { surface: surface, name: names[surface.giver], onPress: onPress }, surface.id))) }));
|
|
29
|
+
}
|
|
30
|
+
export default AnswerSurfacesView;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AssistantState } from '../../chat/assistant/state';
|
|
2
2
|
import type { DisplayItem } from '../../types/chat';
|
|
3
3
|
import { type BrowserModelOptions } from '../../runtimes/browser/model';
|
|
4
|
-
import { type A2APeer, type A2APeerArtifact } from '../../runtimes/browser/a2aPeer';
|
|
4
|
+
import { type A2APeer, type A2APeerAction, type A2APeerArtifact } from '../../runtimes/browser/a2aPeer';
|
|
5
5
|
import type { AppSpec } from '../../types/agentspecs';
|
|
6
6
|
import type { OtelLiveTracer } from '@datalayer/core/lib/otel/live';
|
|
7
7
|
import { type A2ATeamCall, type A2ATeamConnection, type A2ATeamFlow } from './a2aTeamFlow';
|
|
@@ -25,13 +25,51 @@ export type A2ATeamPersona = {
|
|
|
25
25
|
notebook?: A2APeerArtifact;
|
|
26
26
|
};
|
|
27
27
|
export declare const AT_REST: A2ATeamPersona;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* One turn of the conversation with the entry. A button pressed on what
|
|
30
|
+
* another member showed is a turn of it too: the person's line addressed to
|
|
31
|
+
* that member, and its answer from it (`member`, STUDIO H-02).
|
|
32
|
+
*/
|
|
29
33
|
export type A2ATeamTurn = {
|
|
30
34
|
role: 'user' | 'assistant';
|
|
31
35
|
text: string;
|
|
36
|
+
/**
|
|
37
|
+
* The member other than the entry this turn is with: the one whose
|
|
38
|
+
* button the person pressed, and who answered it.
|
|
39
|
+
*/
|
|
40
|
+
member?: {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* A button pressed on a surface a member showed (`AnswerSurfaces`'
|
|
47
|
+
* `AnswerPressed`): its words, the person's turn, and its action.
|
|
48
|
+
*/
|
|
49
|
+
export type A2ATeamPress = {
|
|
50
|
+
message: string;
|
|
51
|
+
action: A2APeerAction;
|
|
32
52
|
};
|
|
33
53
|
/** What a page that shows notebooks accepts: a notebook, and words in Markdown. */
|
|
34
54
|
export declare const NOTEBOOK_AND_WORDS: readonly string[];
|
|
55
|
+
/**
|
|
56
|
+
* What a page that draws an answer's components accepts besides: the
|
|
57
|
+
* catalog's surfaces, a notebook, and words in Markdown (STUDIO H-02).
|
|
58
|
+
*/
|
|
59
|
+
export declare const COMPONENTS_NOTEBOOK_AND_WORDS: readonly string[];
|
|
60
|
+
/** A surface of components a member showed with an answer: who gave it, and the surface. */
|
|
61
|
+
export type A2ATeamSurface = {
|
|
62
|
+
/** Its surface's id: `answer-…`. */
|
|
63
|
+
id: string;
|
|
64
|
+
/** The member that showed it, by its id. */
|
|
65
|
+
giver: string;
|
|
66
|
+
artifact: A2APeerArtifact;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* The surfaces among what a member gave, each once by its surface's id,
|
|
70
|
+
* after those already kept.
|
|
71
|
+
*/
|
|
72
|
+
export declare function surfacesAfter(kept: A2ATeamSurface[], giver: string, artifacts: A2APeerArtifact[]): A2ATeamSurface[];
|
|
35
73
|
/** What the peer gave, in words: its words, and each of its artifacts. */
|
|
36
74
|
export declare function answeredLine(artifacts: A2APeerArtifact[]): string;
|
|
37
75
|
/** The latest notebook among what a peer gave, or none. */
|
|
@@ -158,6 +196,11 @@ export type A2ATeam = {
|
|
|
158
196
|
artifacts: A2APeerArtifact[];
|
|
159
197
|
/** The latest notebook a peer gave, kept until another one comes. */
|
|
160
198
|
notebook: A2APeerArtifact | null;
|
|
199
|
+
/**
|
|
200
|
+
* Every surface of components the members showed in this conversation,
|
|
201
|
+
* in the order they came, with who showed it (STUDIO H-02).
|
|
202
|
+
*/
|
|
203
|
+
surfaces: A2ATeamSurface[];
|
|
161
204
|
/** Which way the first peer's link carries a message now. */
|
|
162
205
|
flow: A2ATeamFlow;
|
|
163
206
|
/** The tool calls the peers' connections are answering now. */
|
|
@@ -167,6 +210,17 @@ export type A2ATeam = {
|
|
|
167
210
|
/** Whether the entry can be asked: every peer is connected, or the entry is. */
|
|
168
211
|
ready: boolean;
|
|
169
212
|
send: (text: string) => Promise<void>;
|
|
213
|
+
/**
|
|
214
|
+
* A button pressed on what a member showed (its id: the entry's, or a
|
|
215
|
+
* peer's), as a turn of the conversation (STUDIO H-02): the person's line
|
|
216
|
+
* in the entry's history, the member asked directly over A2A with the
|
|
217
|
+
* button's action beside its words (`askA2APeer({ action })`), and its
|
|
218
|
+
* reply landing as an `ask_<peer>` answer does — in its history, its
|
|
219
|
+
* balloon, its flow, the team's report, surfaces and notebook — and in
|
|
220
|
+
* the entry's history as that member's answer. Answers its words; throws
|
|
221
|
+
* when the member is not reached, a turn is under way, or it failed.
|
|
222
|
+
*/
|
|
223
|
+
pressAction: (memberId: string, pressed: A2ATeamPress) => Promise<string>;
|
|
170
224
|
stop: () => void;
|
|
171
225
|
};
|
|
172
226
|
/** Run a team over A2A in the page. */
|