@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.
Files changed (62) hide show
  1. package/lib/components/a2ui/datalayer/Download.d.ts +21 -0
  2. package/lib/components/a2ui/datalayer/Download.js +66 -0
  3. package/lib/components/a2ui/datalayer/drawn.d.ts +1 -1
  4. package/lib/components/a2ui/datalayer/drawn.js +1 -0
  5. package/lib/components/a2ui/datalayer/index.d.ts +2 -1
  6. package/lib/components/a2ui/datalayer/index.js +3 -0
  7. package/lib/components/inspector/a2aSpans.d.ts +9 -0
  8. package/lib/components/inspector/a2aSpans.js +11 -1
  9. package/lib/components/teams/AnswerSurfaces.d.ts +56 -0
  10. package/lib/components/teams/AnswerSurfaces.js +27 -0
  11. package/lib/components/teams/AnswerSurfacesView.d.ts +14 -0
  12. package/lib/components/teams/AnswerSurfacesView.js +30 -0
  13. package/lib/components/teams/index.d.ts +1 -0
  14. package/lib/components/teams/index.js +1 -0
  15. package/lib/components/teams/useA2ATeam.d.ts +56 -2
  16. package/lib/components/teams/useA2ATeam.js +122 -3
  17. package/lib/hooks/useAgentRuntimes.d.ts +13 -0
  18. package/lib/hooks/useAgentRuntimes.js +11 -1
  19. package/lib/loop/apps/AppFeedback.js +11 -2
  20. package/lib/loop/apps/AppRenderer.d.ts +13 -1
  21. package/lib/loop/apps/AppRenderer.js +34 -1
  22. package/lib/loop/apps/AppRuntimePlugins.d.ts +8 -0
  23. package/lib/loop/apps/AppRuntimePlugins.js +87 -0
  24. package/lib/loop/apps/feedback.d.ts +7 -0
  25. package/lib/loop/apps/feedback.js +3 -0
  26. package/lib/loop/apps/pluginPair.d.ts +60 -0
  27. package/lib/loop/apps/pluginPair.js +44 -0
  28. package/lib/loop/apps/threads.d.ts +35 -0
  29. package/lib/loop/apps/threads.js +79 -0
  30. package/lib/loop/embed/LoopEmbed.d.ts +4 -3
  31. package/lib/loop/embed/hostBridge.d.ts +27 -0
  32. package/lib/loop/embed/hostBridge.js +47 -0
  33. package/lib/loop/index.d.ts +1 -1
  34. package/lib/loop/index.js +1 -1
  35. package/lib/loop/plugins/agent-capacity/index.d.ts +7 -0
  36. package/lib/loop/plugins/agent-capacity/index.js +3 -2
  37. package/lib/loop/plugins/agents/DatalayerAgentBridge.js +11 -0
  38. package/lib/loop/plugins/agents/appLaunch.d.ts +15 -0
  39. package/lib/loop/plugins/agents/appLaunch.js +28 -0
  40. package/lib/loop/plugins/chat/ChatView.js +177 -109
  41. package/lib/loop/shell/LoopWorkspace.d.ts +3 -3
  42. package/lib/loop/shell/LoopWorkspace.js +1 -1
  43. package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +10 -2
  44. package/lib/runtimes/actions.js +23 -0
  45. package/lib/runtimes/apis.d.ts +18 -0
  46. package/lib/runtimes/browser/a2aPeer.d.ts +19 -2
  47. package/lib/runtimes/browser/a2aPeer.js +13 -7
  48. package/lib/specs/apps.js +102 -30
  49. package/lib/specs/envvars.d.ts +2 -0
  50. package/lib/specs/envvars.js +22 -0
  51. package/lib/specs/outputs.d.ts +1 -0
  52. package/lib/specs/outputs.js +12 -0
  53. package/lib/specs/sceneSchema.js +12 -1
  54. package/lib/specs/scenes.js +238 -121
  55. package/lib/specs/teams/teams.js +23 -7
  56. package/lib/specs/uiPlugins.js +55 -0
  57. package/lib/stores/agentRuntimeStore.js +4 -0
  58. package/lib/types/chat.d.ts +6 -0
  59. package/lib/types/scenes.d.ts +7 -1
  60. package/package.json +1 -1
  61. package/scripts/codegen/generate_envvars.py +0 -1
  62. 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];
@@ -17,4 +17,5 @@ export const DRAWN_OWN_COMPONENTS = [
17
17
  'Chat',
18
18
  'Evidence',
19
19
  'Form',
20
+ 'Download',
20
21
  ];
@@ -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 Form — made as A2UI makes its own, so that the decision
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: asker,
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;
@@ -17,3 +17,4 @@ export * from './sceneTranscript';
17
17
  export * from './SceneTranscript';
18
18
  export * from './SceneView';
19
19
  export * from './sceneStage';
20
+ export * from './AnswerSurfaces';
@@ -21,3 +21,4 @@ export * from './sceneTranscript';
21
21
  export * from './SceneTranscript';
22
22
  export * from './SceneView';
23
23
  export * from './sceneStage';
24
+ export * from './AnswerSurfaces';
@@ -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
- /** One turn of the conversation with the entry. */
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.name);
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
  */