@datalayer/agent-runtimes 1.3.87 → 1.3.89
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/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.js +12 -1
- 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 +0 -1
- package/scripts/codegen/generate_mcp_servers.py +11 -7
|
@@ -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,
|
|
@@ -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. */
|
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
* and the way each link carries a message as its flow ({@link flowAfter}).
|
|
23
23
|
* What a peer gives besides words — the formats the page accepts
|
|
24
24
|
* (`accept`), such as a Jupyter notebook — is kept as its artifacts, and
|
|
25
|
-
* the latest notebook kept until another one comes ({@link A2ATeam.notebook})
|
|
25
|
+
* the latest notebook kept until another one comes ({@link A2ATeam.notebook}),
|
|
26
|
+
* and each surface of components of the catalog a peer shows
|
|
27
|
+
* (`application/json+a2ui`) kept with the peer that gave it, in the order
|
|
28
|
+
* they came ({@link A2ATeam.surfaces}, STUDIO H-02). A button pressed on
|
|
29
|
+
* one is a turn of the conversation ({@link A2ATeam.pressAction}): the
|
|
30
|
+
* person's line to that member, its answer from it, in the entry's history.
|
|
26
31
|
* The tools a peer calls on its connections (its MCP servers), told over
|
|
27
32
|
* A2A as it calls them, are kept as the calls running now ({@link callsAfter}).
|
|
28
33
|
*
|
|
@@ -39,9 +44,10 @@
|
|
|
39
44
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
40
45
|
import { stepCountIs, ToolLoopAgent } from 'ai';
|
|
41
46
|
import { createBrowserModel, } from '../../runtimes/browser/model';
|
|
42
|
-
import { a2aPeerTool, askA2APeer, NOTEBOOK_MEDIA_TYPE, } from '../../runtimes/browser/a2aPeer';
|
|
47
|
+
import { a2aPeerTool, askA2APeer, A2UI_MEDIA_TYPE, NOTEBOOK_MEDIA_TYPE, } from '../../runtimes/browser/a2aPeer';
|
|
43
48
|
import { endAgentTurn, endToolCall, startAgentTurn, startToolCall, } from '../inspector/agentSpans';
|
|
44
49
|
import { callsAfter, flowAfter, toolOwnName, pruneCalls, } from './a2aTeamFlow';
|
|
50
|
+
import { PERSON } from './sceneTranscript';
|
|
45
51
|
import { toolLineOfStep, } from '../../chat/assistant/toolLine';
|
|
46
52
|
const NO_CONNECTIONS = [];
|
|
47
53
|
const NO_PEERS = [];
|
|
@@ -55,11 +61,42 @@ export const NOTEBOOK_AND_WORDS = [
|
|
|
55
61
|
NOTEBOOK_MEDIA_TYPE,
|
|
56
62
|
'text/markdown',
|
|
57
63
|
];
|
|
64
|
+
/**
|
|
65
|
+
* What a page that draws an answer's components accepts besides: the
|
|
66
|
+
* catalog's surfaces, a notebook, and words in Markdown (STUDIO H-02).
|
|
67
|
+
*/
|
|
68
|
+
export const COMPONENTS_NOTEBOOK_AND_WORDS = [
|
|
69
|
+
A2UI_MEDIA_TYPE,
|
|
70
|
+
...NOTEBOOK_AND_WORDS,
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* The surfaces among what a member gave, each once by its surface's id,
|
|
74
|
+
* after those already kept.
|
|
75
|
+
*/
|
|
76
|
+
export function surfacesAfter(kept, giver, artifacts) {
|
|
77
|
+
const found = artifacts
|
|
78
|
+
.filter(artifact => artifact.mediaType === A2UI_MEDIA_TYPE)
|
|
79
|
+
.map(artifact => {
|
|
80
|
+
const data = artifact.data;
|
|
81
|
+
return {
|
|
82
|
+
id: data && typeof data.surfaceId === 'string'
|
|
83
|
+
? data.surfaceId
|
|
84
|
+
: `${giver}-${kept.length}`,
|
|
85
|
+
giver,
|
|
86
|
+
artifact,
|
|
87
|
+
};
|
|
88
|
+
})
|
|
89
|
+
.filter((surface, index, all) => !kept.some(one => one.id === surface.id) &&
|
|
90
|
+
all.findIndex(one => one.id === surface.id) === index);
|
|
91
|
+
return found.length ? [...kept, ...found] : kept;
|
|
92
|
+
}
|
|
58
93
|
/** What the peer gave, in words: its words, and each of its artifacts. */
|
|
59
94
|
export function answeredLine(artifacts) {
|
|
60
95
|
const besides = artifacts.map(artifact => artifact.mediaType === NOTEBOOK_MEDIA_TYPE
|
|
61
96
|
? `a notebook, ${artifact.name}`
|
|
62
|
-
: artifact.
|
|
97
|
+
: artifact.mediaType === A2UI_MEDIA_TYPE
|
|
98
|
+
? `what it shows, ${artifact.name}`
|
|
99
|
+
: artifact.name);
|
|
63
100
|
return besides.length ? `the report and ${besides.join(', ')}` : 'the report';
|
|
64
101
|
}
|
|
65
102
|
/** The latest notebook among what a peer gave, or none. */
|
|
@@ -221,6 +258,7 @@ export function useA2ATeam(options) {
|
|
|
221
258
|
const [artifacts, setArtifacts] = useState([]);
|
|
222
259
|
const [givens, setGivens] = useState({});
|
|
223
260
|
const [notebook, setNotebook] = useState(null);
|
|
261
|
+
const [surfaces, setSurfaces] = useState([]);
|
|
224
262
|
const [flows, setFlows] = useState({});
|
|
225
263
|
const [calls, setCalls] = useState([]);
|
|
226
264
|
const callsNow = useRef([]);
|
|
@@ -291,6 +329,7 @@ export function useA2ATeam(options) {
|
|
|
291
329
|
setReports(prev => ({ ...prev, [id]: event.answer }));
|
|
292
330
|
setArtifacts(event.artifacts);
|
|
293
331
|
setGivens(prev => ({ ...prev, [id]: event.artifacts }));
|
|
332
|
+
setSurfaces(prev => surfacesAfter(prev, id, event.artifacts));
|
|
294
333
|
const given = notebookAmong(event.artifacts);
|
|
295
334
|
if (given) {
|
|
296
335
|
setNotebook(given);
|
|
@@ -575,6 +614,72 @@ export function useA2ATeam(options) {
|
|
|
575
614
|
setPersona,
|
|
576
615
|
peersAtRest,
|
|
577
616
|
]);
|
|
617
|
+
const pressAction = useCallback(async (memberId, pressed) => {
|
|
618
|
+
const words = pressed.message.trim();
|
|
619
|
+
const isEntry = memberId === entry.id;
|
|
620
|
+
const one = isEntry ? undefined : peerOf(memberId);
|
|
621
|
+
const name = isEntry ? entry.name : (one?.app.name ?? memberId);
|
|
622
|
+
const to = isEntry ? entryPeer : (one?.peer ?? null);
|
|
623
|
+
if (!to) {
|
|
624
|
+
throw new Error(`${name} is not reached: it cannot be asked.`);
|
|
625
|
+
}
|
|
626
|
+
if (busy) {
|
|
627
|
+
throw new Error('Wait for the answer under way, then choose.');
|
|
628
|
+
}
|
|
629
|
+
if (!words) {
|
|
630
|
+
throw new Error('This button says nothing to send.');
|
|
631
|
+
}
|
|
632
|
+
// The entry's own button is a turn with it; another member's is
|
|
633
|
+
// addressed to that member, and answered by it.
|
|
634
|
+
const member = isEntry ? undefined : { id: memberId, name };
|
|
635
|
+
const withMember = member ? { member } : {};
|
|
636
|
+
setBusy(true);
|
|
637
|
+
setTurns(prev => [...prev, { role: 'user', text: words, ...withMember }]);
|
|
638
|
+
abort.current = new AbortController();
|
|
639
|
+
try {
|
|
640
|
+
const { answer } = await askA2APeer(to, words, {
|
|
641
|
+
signal: abort.current.signal,
|
|
642
|
+
action: pressed.action,
|
|
643
|
+
accept,
|
|
644
|
+
onEvent: event => onMemberEvent(memberId, event),
|
|
645
|
+
});
|
|
646
|
+
setTurns(prev => [
|
|
647
|
+
...prev,
|
|
648
|
+
{ role: 'assistant', text: answer, ...withMember },
|
|
649
|
+
]);
|
|
650
|
+
if (!entryPeer) {
|
|
651
|
+
// The entry's model reads it at its next turn: what was chosen,
|
|
652
|
+
// and what the member answered.
|
|
653
|
+
history.current = [
|
|
654
|
+
...history.current,
|
|
655
|
+
{ role: 'user', content: words },
|
|
656
|
+
{
|
|
657
|
+
role: 'assistant',
|
|
658
|
+
content: `${name} answered the person directly: ${answer}`,
|
|
659
|
+
},
|
|
660
|
+
];
|
|
661
|
+
}
|
|
662
|
+
// Answered: back at rest, its words still in its balloon.
|
|
663
|
+
setPersona(memberId, prev => ({ ...prev, state: 'idle' }));
|
|
664
|
+
return answer;
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
668
|
+
setTurns(prev => [
|
|
669
|
+
...prev,
|
|
670
|
+
{
|
|
671
|
+
role: 'assistant',
|
|
672
|
+
text: `Could not answer: ${message}`,
|
|
673
|
+
...withMember,
|
|
674
|
+
},
|
|
675
|
+
]);
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
abort.current = null;
|
|
680
|
+
setBusy(false);
|
|
681
|
+
}
|
|
682
|
+
}, [entry, entryPeer, peerOf, busy, accept, onMemberEvent, setPersona]);
|
|
578
683
|
const stop = useCallback(() => abort.current?.abort(), []);
|
|
579
684
|
useEffect(() => () => {
|
|
580
685
|
abort.current?.abort();
|
|
@@ -594,6 +699,18 @@ export function useA2ATeam(options) {
|
|
|
594
699
|
role: turn.role,
|
|
595
700
|
content: turn.text,
|
|
596
701
|
createdAt: new Date(0),
|
|
702
|
+
// A button pressed on another member's surface: the person's line
|
|
703
|
+
// to it, and its answer, said by it.
|
|
704
|
+
...(turn.member
|
|
705
|
+
? turn.role === 'user'
|
|
706
|
+
? {
|
|
707
|
+
speaker: { id: 'person', name: PERSON },
|
|
708
|
+
directedTo: { id: turn.member.id, name: turn.member.name },
|
|
709
|
+
}
|
|
710
|
+
: {
|
|
711
|
+
speaker: { id: turn.member.id, name: turn.member.name },
|
|
712
|
+
}
|
|
713
|
+
: {}),
|
|
597
714
|
})), [turns]);
|
|
598
715
|
const NONE = useMemo(() => [], []);
|
|
599
716
|
const teamPeers = useMemo(() => peerList.map(one => ({
|
|
@@ -623,11 +740,13 @@ export function useA2ATeam(options) {
|
|
|
623
740
|
report,
|
|
624
741
|
artifacts,
|
|
625
742
|
notebook,
|
|
743
|
+
surfaces,
|
|
626
744
|
flow: first?.flow ?? 'still',
|
|
627
745
|
calls,
|
|
628
746
|
busy,
|
|
629
747
|
ready: entryPeer ? true : agent !== null,
|
|
630
748
|
send,
|
|
749
|
+
pressAction,
|
|
631
750
|
stop,
|
|
632
751
|
};
|
|
633
752
|
}
|
|
@@ -49,7 +49,20 @@ export interface UseAgentOptions {
|
|
|
49
49
|
* connection bootstrap as well as agent creation.
|
|
50
50
|
*/
|
|
51
51
|
runtimeConnection?: AgentRuntimeConnectionOptions;
|
|
52
|
+
/**
|
|
53
|
+
* The application the runtime is launched for (STUDIO R-19): its Appspec,
|
|
54
|
+
* and the saved application and deployment it runs for. Sent with the
|
|
55
|
+
* launch, as `RuntimesClient.create` sends it in Python, so the runtime is
|
|
56
|
+
* given the secrets its connections declare before its agent is made.
|
|
57
|
+
*
|
|
58
|
+
* An application's runtime is always its own: a running runtime is never
|
|
59
|
+
* looked up and reused for it, since one not launched for the application
|
|
60
|
+
* was not given its secrets.
|
|
61
|
+
*/
|
|
62
|
+
appLaunch?: AppLaunch;
|
|
52
63
|
}
|
|
64
|
+
/** What an application's launch carries besides its environment (R-19). */
|
|
65
|
+
export type AppLaunch = Required<Pick<IRuntimeOptions, 'appSpec'>> & Pick<IRuntimeOptions, 'appUid' | 'deploymentUid'>;
|
|
53
66
|
/**
|
|
54
67
|
* Return type for the useAgents hook.
|
|
55
68
|
*/
|