@datalayer/agent-runtimes 1.5.89 → 1.5.90

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.
@@ -72,7 +72,7 @@ import { AppEmbed, embedAssistantCharacter } from './AppEmbed.js';
72
72
  import { WINDOW_NOT_YET } from './embedSession.js';
73
73
  import { EMBED_HOST_VARIABLES, EMBED_OBSERVED_ATTRIBUTES, EMBED_TAG, EMBED_TOKEN_NAMES_NO_DEPLOYMENT, EmbedAttributeError, adoptPropertiesSetEarly, embedLookOf, embedTokenDeployment, inlineHeightOf, languageOf, resumeOf, runnable, } from './embedConfig.js';
74
74
  import { embedShadowCss } from './embedTheme.js';
75
- import { DEFAULT_API, readVisitorApp } from './visitorApp.js';
75
+ import { DEFAULT_API, DEFAULT_INFERENCE, readVisitorApp } from './visitorApp.js';
76
76
  export { runnable };
77
77
  /** What the framed run page and the element say to each other. */
78
78
  export const EMBED_MESSAGES = {
@@ -383,7 +383,11 @@ export function defineDatalayerAppElement(options = {}) {
383
383
  // A public application, as a visitor without an account (D-07):
384
384
  // an example kept warm or an address open to visitors, run on the
385
385
  // visitors' runtime; a decision framed; a refusal said.
386
- const visitor = await readVisitorApp({ api: this.agents(), app });
386
+ const visitor = await readVisitorApp({
387
+ api: this.agents(),
388
+ inference: this.inference(),
389
+ app,
390
+ });
387
391
  if (visitor.kind === 'decision') {
388
392
  return { kind: 'frame', src: frameSrcOf(this.origin(), visitor.app) };
389
393
  }
@@ -410,15 +414,26 @@ export function defineDatalayerAppElement(options = {}) {
410
414
  }
411
415
  /**
412
416
  * ai-agents, where a visitor's address is read and beside which the
413
- * visitors' runtime and ai-inference answer: the `api` attribute, else
414
- * the platform's, else r1's. Not Spacer's address (`api()`), which lives
415
- * on another plane.
417
+ * visitors' runtime answers: the `api` attribute, else the platform's,
418
+ * else the AI plane's (`DEFAULT_API`). Not Spacer's address (`api()`),
419
+ * which lives on another plane.
416
420
  */
417
421
  agents() {
418
422
  return (this.getAttribute('api') ||
419
423
  coreStore.getState().configuration?.aiAgentsUrl ||
420
424
  DEFAULT_API).replace(/\/+$/, '');
421
425
  }
426
+ /**
427
+ * ai-inference, which mints a visitor's token: the `api` attribute, else
428
+ * the platform's, else the AI plane's (`DEFAULT_INFERENCE`) — never the
429
+ * control plane, where no ai-inference answers (28 October 2026: a bare
430
+ * `<datalayer-app app="…">` asked prod1 and got "Failed to fetch").
431
+ */
432
+ inference() {
433
+ return (this.getAttribute('api') ||
434
+ coreStore.getState().configuration?.aiInferenceUrl ||
435
+ DEFAULT_INFERENCE).replace(/\/+$/, '');
436
+ }
422
437
  /** Spacer, where an application is read with its embed token: the `api` attribute, else the platform's, else prod1's. */
423
438
  api() {
424
439
  return (this.getAttribute('api') ||
@@ -1,39 +1,3 @@
1
- /**
2
- * A public application in the element, for a visitor without an account
3
- * (D-07, R-30).
4
- *
5
- * `<datalayer-app app="…">` with no token names a public application. The
6
- * element reads what it is without anybody's credentials, as the landing's
7
- * hosted page does for whoever is signed out:
8
- *
9
- * - **an example Datalayer keeps warm for visitors** (`web-research`,
10
- * `customer-interview`): its Appspec from the catalogue this bundle
11
- * carries; the visitors' runtime answers its id;
12
- * - **an application at its address** (`demo-accounting`): ai-agents'
13
- * `GET /apps/deployments/at/<slug>` without a token says what its page is
14
- * drawn from — the Appspec of the version it runs — and whether somebody
15
- * signed out may talk to it (`session.opens`); the visitors' runtime
16
- * answers `at:<slug>`, making the address's agent when first talked to —
17
- * or, for a version with code of its own (`runs_code`), the runtime its
18
- * deployment is kept on (`kept.url`), which runs that code;
19
- * - **a decision**, named by its id (`01M…`): framed from Datalayer's run
20
- * page, as before.
21
- *
22
- * The conversation then runs as the landing's signed-out one does: the chat
23
- * mints the visitor's token itself from ai-inference (`useVisitorToken`,
24
- * `POST /anonymous/token {app}`), minted again before it runs out, and talks
25
- * to the visitors' runtime with it — in every mode, inline or floating.
26
- *
27
- * What a visitor may not have is said in one sentence, before anything is
28
- * drawn (fail fast): a private address (ai-agents answers *No application is
29
- * at this address*), one that acts through its owner's connections or
30
- * documents (ai-agents' own reason), an example the visitors' runtime does
31
- * not keep. Each says the embed token is what an embed of it takes.
32
- *
33
- * Pure but for the one request, given or `fetch`.
34
- *
35
- * @module apps/embed/visitorApp
36
- */
37
1
  import type { AppSpec } from '../../types/agentspecs';
38
2
  import type { DatalayerVisitors } from '../apps/visitorToken';
39
3
  /** Where the visitors' runtime is published, on ai-agents' host (its chart). */
@@ -45,12 +9,21 @@ export declare const AT_PREFIX = "at:";
45
9
  /**
46
10
  * Datalayer's services for a visitor, where the element names none (`api`)
47
11
  * and the platform's configuration says none: ai-agents, where an address is
48
- * read, and beside it the visitors' runtime and ai-inference, which mints the
49
- * visitor's token. Not Spacer's address, which reads an application with its
50
- * embed token: the two live on different planes (ai-agents on r1, Spacer on
51
- * prod1), found 15 October 2026 on datalayer.ai's embed.
12
+ * read, and beside it the visitors' runtime — the AI plane, as core's plane
13
+ * table says it, never a literal of this bundle's own. Not Spacer's address,
14
+ * which reads an application with its embed token: the two live on different
15
+ * planes (ai-agents on r1, Spacer on prod1), found 15 October 2026 on
16
+ * datalayer.ai's embed; a bundle whose literal still said prod1 failed to
17
+ * mint a visitor's token there on 28 October 2026 ("Failed to fetch"),
18
+ * ai-inference being on r1 with the agents that call it.
19
+ */
20
+ export declare const DEFAULT_API: string;
21
+ /**
22
+ * ai-inference, which mints a visitor's token, where the element names none
23
+ * (`api`) and the platform's configuration says none: the AI plane, as core's
24
+ * plane table says it.
52
25
  */
53
- export declare const DEFAULT_API = "https://r1.datalayer.run";
26
+ export declare const DEFAULT_INFERENCE: string;
54
27
  /** An application's id on Datalayer: a ULID. */
55
28
  export declare const isAppUid: (value: string) => boolean;
56
29
  /** What `app` with no token resolves to. */
@@ -74,9 +47,10 @@ export type VisitorApp =
74
47
  /**
75
48
  * Where a visitor's conversation with `app` runs: the visitors' runtime beside
76
49
  * ai-agents — or `runtime`, the one a deployment with code of its own is
77
- * kept on (R-30) — its token from ai-inference.
50
+ * kept on (R-30) — its token from ai-inference, at `inference` when it is
51
+ * named, else beside ai-agents.
78
52
  */
79
- export declare function visitorsAt(api: string, app: string, runtime?: string): DatalayerVisitors;
53
+ export declare function visitorsAt(api: string, app: string, runtime?: string, inference?: string): DatalayerVisitors;
80
54
  /** What an embed without a token is told it takes. */
81
55
  export declare const TOKEN_HINT = "Embedded for a visitor it refuses, it takes its embed token (the \"token\" attribute).";
82
56
  /** The sentence for an example the visitors' runtime does not keep. */
@@ -87,13 +61,17 @@ export declare const refusedSentence: (reason: string) => string;
87
61
  * Read what `app` is for a visitor without an account: a decision to frame,
88
62
  * an example or an address to run on the visitors' runtime, or a sentence.
89
63
  *
90
- * @param api - Datalayer's services (ai-agents, ai-inference): the `api` attribute.
64
+ * @param api - Datalayer's services (ai-agents, and ai-inference unless
65
+ * `inference` names it): the `api` attribute; the AI plane unless given.
66
+ * @param inference - ai-inference, which mints the visitor's token: `api`
67
+ * when that is given, else the AI plane.
91
68
  * @param app - The `app` attribute: a decision's id, an example's id or an address.
92
69
  * @param fetcher - What asks ai-agents; `fetch` unless given.
93
70
  * @param catalogue - The examples; the bundle's catalogue unless given.
94
71
  */
95
- export declare function readVisitorApp({ api, app, fetcher, catalogue, }: {
72
+ export declare function readVisitorApp({ api, inference, app, fetcher, catalogue, }: {
96
73
  api?: string;
74
+ inference?: string;
97
75
  app: string;
98
76
  fetcher?: typeof fetch;
99
77
  catalogue?: (ref: string) => AppSpec | undefined;
@@ -2,6 +2,43 @@
2
2
  * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ /**
6
+ * A public application in the element, for a visitor without an account
7
+ * (D-07, R-30).
8
+ *
9
+ * `<datalayer-app app="…">` with no token names a public application. The
10
+ * element reads what it is without anybody's credentials, as the landing's
11
+ * hosted page does for whoever is signed out:
12
+ *
13
+ * - **an example Datalayer keeps warm for visitors** (`web-research`,
14
+ * `customer-interview`): its Appspec from the catalogue this bundle
15
+ * carries; the visitors' runtime answers its id;
16
+ * - **an application at its address** (`demo-accounting`): ai-agents'
17
+ * `GET /apps/deployments/at/<slug>` without a token says what its page is
18
+ * drawn from — the Appspec of the version it runs — and whether somebody
19
+ * signed out may talk to it (`session.opens`); the visitors' runtime
20
+ * answers `at:<slug>`, making the address's agent when first talked to —
21
+ * or, for a version with code of its own (`runs_code`), the runtime its
22
+ * deployment is kept on (`kept.url`), which runs that code;
23
+ * - **a decision**, named by its id (`01M…`): framed from Datalayer's run
24
+ * page, as before.
25
+ *
26
+ * The conversation then runs as the landing's signed-out one does: the chat
27
+ * mints the visitor's token itself from ai-inference (`useVisitorToken`,
28
+ * `POST /anonymous/token {app}`), minted again before it runs out, and talks
29
+ * to the visitors' runtime with it — in every mode, inline or floating.
30
+ *
31
+ * What a visitor may not have is said in one sentence, before anything is
32
+ * drawn (fail fast): a private address (ai-agents answers *No application is
33
+ * at this address*), one that acts through its owner's connections or
34
+ * documents (ai-agents' own reason), an example the visitors' runtime does
35
+ * not keep. Each says the embed token is what an embed of it takes.
36
+ *
37
+ * Pure but for the one request, given or `fetch`.
38
+ *
39
+ * @module apps/embed/visitorApp
40
+ */
41
+ import { DEFAULT_PLANE_URLS } from '@datalayer/core/lib/config/planes.js';
5
42
  import { getApp } from '../../specs/apps.js';
6
43
  import { parseAppspec } from '../apps/appspec.js';
7
44
  import { runnable } from './embedConfig.js';
@@ -17,12 +54,21 @@ export const AT_PREFIX = 'at:';
17
54
  /**
18
55
  * Datalayer's services for a visitor, where the element names none (`api`)
19
56
  * and the platform's configuration says none: ai-agents, where an address is
20
- * read, and beside it the visitors' runtime and ai-inference, which mints the
21
- * visitor's token. Not Spacer's address, which reads an application with its
22
- * embed token: the two live on different planes (ai-agents on r1, Spacer on
23
- * prod1), found 15 October 2026 on datalayer.ai's embed.
57
+ * read, and beside it the visitors' runtime — the AI plane, as core's plane
58
+ * table says it, never a literal of this bundle's own. Not Spacer's address,
59
+ * which reads an application with its embed token: the two live on different
60
+ * planes (ai-agents on r1, Spacer on prod1), found 15 October 2026 on
61
+ * datalayer.ai's embed; a bundle whose literal still said prod1 failed to
62
+ * mint a visitor's token there on 28 October 2026 ("Failed to fetch"),
63
+ * ai-inference being on r1 with the agents that call it.
64
+ */
65
+ export const DEFAULT_API = DEFAULT_PLANE_URLS.aiAgentsUrl;
66
+ /**
67
+ * ai-inference, which mints a visitor's token, where the element names none
68
+ * (`api`) and the platform's configuration says none: the AI plane, as core's
69
+ * plane table says it.
24
70
  */
25
- export const DEFAULT_API = 'https://r1.datalayer.run';
71
+ export const DEFAULT_INFERENCE = DEFAULT_PLANE_URLS.aiInferenceUrl;
26
72
  /** An application's id on Datalayer: a ULID. */
27
73
  export const isAppUid = (value) => /^[0-9A-HJKMNP-TV-Z]{26}$/.test(value);
28
74
  /** An address, as ai-agents spells one. */
@@ -30,14 +76,15 @@ const SLUG = /^[a-z0-9][a-z0-9-]{0,62}$/;
30
76
  /**
31
77
  * Where a visitor's conversation with `app` runs: the visitors' runtime beside
32
78
  * ai-agents — or `runtime`, the one a deployment with code of its own is
33
- * kept on (R-30) — its token from ai-inference.
79
+ * kept on (R-30) — its token from ai-inference, at `inference` when it is
80
+ * named, else beside ai-agents.
34
81
  */
35
- export function visitorsAt(api, app, runtime) {
82
+ export function visitorsAt(api, app, runtime, inference) {
36
83
  const base = api.replace(/\/+$/, '');
37
84
  return {
38
85
  url: runtime ? runtime.replace(/\/+$/, '') : `${base}${VISITORS_PATH}`,
39
86
  app,
40
- inferenceUrl: base,
87
+ inferenceUrl: (inference || base).replace(/\/+$/, ''),
41
88
  };
42
89
  }
43
90
  /** What an embed without a token is told it takes. */
@@ -50,12 +97,17 @@ export const refusedSentence = (reason) => `datalayer-app: ${reason.trim().repla
50
97
  * Read what `app` is for a visitor without an account: a decision to frame,
51
98
  * an example or an address to run on the visitors' runtime, or a sentence.
52
99
  *
53
- * @param api - Datalayer's services (ai-agents, ai-inference): the `api` attribute.
100
+ * @param api - Datalayer's services (ai-agents, and ai-inference unless
101
+ * `inference` names it): the `api` attribute; the AI plane unless given.
102
+ * @param inference - ai-inference, which mints the visitor's token: `api`
103
+ * when that is given, else the AI plane.
54
104
  * @param app - The `app` attribute: a decision's id, an example's id or an address.
55
105
  * @param fetcher - What asks ai-agents; `fetch` unless given.
56
106
  * @param catalogue - The examples; the bundle's catalogue unless given.
57
107
  */
58
- export async function readVisitorApp({ api = DEFAULT_API, app, fetcher = fetch, catalogue = getApp, }) {
108
+ export async function readVisitorApp({ api, inference, app, fetcher = fetch, catalogue = getApp, }) {
109
+ const agents = api || DEFAULT_API;
110
+ const mints = inference || api || DEFAULT_INFERENCE;
59
111
  const named = app.trim();
60
112
  if (isAppUid(named)) {
61
113
  return { kind: 'decision', app: named };
@@ -71,7 +123,7 @@ export async function readVisitorApp({ api = DEFAULT_API, app, fetcher = fetch,
71
123
  return {
72
124
  kind: 'visitors',
73
125
  app: runnable({ app: example, problems: [] }),
74
- visitors: visitorsAt(api, example.id),
126
+ visitors: visitorsAt(agents, example.id, undefined, mints),
75
127
  };
76
128
  }
77
129
  if (!SLUG.test(named)) {
@@ -82,7 +134,7 @@ export async function readVisitorApp({ api = DEFAULT_API, app, fetcher = fetch,
82
134
  }
83
135
  let response;
84
136
  try {
85
- response = await fetcher(`${api.replace(/\/+$/, '')}/api/ai-agents/v1/apps/deployments/at/${encodeURIComponent(named)}`);
137
+ response = await fetcher(`${agents.replace(/\/+$/, '')}/api/ai-agents/v1/apps/deployments/at/${encodeURIComponent(named)}`);
86
138
  }
87
139
  catch (error) {
88
140
  return {
@@ -117,7 +169,7 @@ export async function readVisitorApp({ api = DEFAULT_API, app, fetcher = fetch,
117
169
  return {
118
170
  kind: 'visitors',
119
171
  app: runnable({ app: { ...parsed.app, name }, problems: parsed.problems }),
120
- visitors: visitorsAt(api, `${AT_PREFIX}${named}`, runtime.url),
172
+ visitors: visitorsAt(agents, `${AT_PREFIX}${named}`, runtime.url, mints),
121
173
  };
122
174
  }
123
175
  /**
@@ -21,10 +21,12 @@
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
- * (`SpecMark`), linked to it by an edge of its own. That edge is still until
25
- * the member calls one of the connection's tools: then it flows toward the
26
- * connection for as long as the call runs ({@link A2ATeamCall}), with the
27
- * tool's name on it; under reduced motion, the arrow and the words alone.
24
+ * (`SpecMark`), linked to it by an edge of its own, `MCP`, drawn as the A2A
25
+ * link is: still until the member calls one of the connection's tools, then
26
+ * flowing toward the connection for as long as the call runs
27
+ * ({@link A2ATeamCall}), `MCP · list invoices`, and back with its result for
28
+ * a moment, `MCP · Odoo answers` (a screen reader hears who calls whom);
29
+ * under reduced motion, the arrow and the words alone.
28
30
  *
29
31
  * The graph is a picture, not an editor: nothing is dragged, panned or
30
32
  * zoomed, and the wheel scrolls the page. Its viewport is worked out from its
@@ -232,13 +234,32 @@ export declare function arrowPath(source: {
232
234
  y: number;
233
235
  }, forward: boolean): string;
234
236
  type CallData = {
237
+ /** Which way it carries a call: out with the call, back with its result. */
238
+ flow: A2ATeamFlow;
235
239
  /** The tool being called, in words (`list invoices`); empty while none is. */
236
240
  calling: string;
241
+ /** What it says: `MCP`, and what it carries while it carries something. */
242
+ label: string;
237
243
  };
238
- /** A member's link to a connection: still, or flowing toward it while a call runs. */
244
+ /**
245
+ * A member's link to a connection, drawn as the A2A link is: still, flowing
246
+ * toward the connection while a call runs, and back with its result.
247
+ */
239
248
  export declare const CallEdge: import("react").NamedExoticComponent<EdgeProps<Edge<CallData>>>;
240
- /** What a member's call says, on its edge and to a screen reader. */
241
- export declare function callWords(member: string, connection: A2ATeamConnection, tool: string): string;
249
+ /** What a link to a connection says at rest, as `A2A` says the members' link. */
250
+ export declare const MCP_LABEL = "MCP";
251
+ /**
252
+ * What a link to a connection says while it carries a call, short enough
253
+ * for the room beside the member: the tool going out, `MCP · list invoices`,
254
+ * then `MCP · Odoo answers` as its result comes back.
255
+ */
256
+ export declare function callEdgeWords(connection: A2ATeamConnection, tool: string, flow: A2ATeamFlow): string;
257
+ /**
258
+ * What a member's call says, on its edge and to a screen reader: the call
259
+ * going out, `Accounting calls Odoo · list invoices`, then its result
260
+ * coming back, `Odoo answers Accounting · list invoices`.
261
+ */
262
+ export declare function callWords(member: string, connection: A2ATeamConnection, tool: string, flow?: A2ATeamFlow): string;
242
263
  /** The viewport that shows a graph `drawn` wide whole in a box `width` wide, never larger than drawn. */
243
264
  export declare function teamViewport(width: number, drawn?: number): Viewport;
244
265
  /** Where a member is laid out: its column and row, from the positions or the default. */
@@ -8,7 +8,7 @@ import { AssistantStage, } from '../../chat/assistant/AssistantStage.js';
8
8
  import { useSignalValue } from '@datalayer/reactor/react';
9
9
  import { teamNotebookKernel } from './teamNotebookKernel.js';
10
10
  import { SpecMark } from '../../chat/marks/SpecMark.js';
11
- import { toolWords, } from './a2aTeamFlow.js';
11
+ import { callFlow, toolWords, } from './a2aTeamFlow.js';
12
12
  import { NotebookPreview } from './NotebookPreview.js';
13
13
  import { notebookBalloonVisual } from './TeamNotebook.js';
14
14
  import { useBalloonRoom } from './useBalloonRoom.js';
@@ -26,8 +26,11 @@ const WIDTH = NODE_WIDTH * 2 + GAP;
26
26
  const ROW_GAP = 48;
27
27
  /** A connection's node: half a member's. */
28
28
  const CONNECTION_WIDTH = NODE_WIDTH / 2;
29
- /** Between a member and its connections, to its right. */
30
- const CONNECTION_GAP = 44;
29
+ /**
30
+ * Between a member and its connections, to its right: room for the MCP
31
+ * edge's words, as the A2A edge has room for its own.
32
+ */
33
+ const CONNECTION_GAP = 140;
31
34
  const CONNECTIONS_APART = 16;
32
35
  /** Where a member's connections start: right of it. */
33
36
  const CONNECTION_X = NODE_WIDTH + CONNECTION_GAP;
@@ -304,7 +307,7 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
304
307
  if (!connection) {
305
308
  return null;
306
309
  }
307
- return (_jsxs(Box, { className: "nodrag nopan nowheel", width: CONNECTION_WIDTH, display: "flex", flexDirection: "column", alignItems: "center", gap: "2px", cursor: "default", pointerEvents: "all", 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) => {
310
+ return (_jsxs(Box, { className: "nodrag nopan nowheel", width: CONNECTION_WIDTH, display: "flex", flexDirection: "column", alignItems: "center", gap: "2px", cursor: "default", pointerEvents: "all", 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: (CONNECTION_WIDTH - size) / 2 }, 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) => {
308
311
  if (event.key === 'Escape' && told) {
309
312
  event.stopPropagation();
310
313
  setTold(false);
@@ -367,18 +370,20 @@ const LinkEdge = memo(function LinkEdge({ id, sourceX, sourceY, targetX, targetY
367
370
  opacity: flow === 'still' ? 0.6 : 0.25,
368
371
  } }), flow !== 'still' && (_jsxs("g", { "data-a2a-edge-flow": flow, children: [_jsx("path", { d: path, fill: "none", className: `a2a-team-flow a2a-team-flow-${flow}`, stroke: "var(--fgColor-accent)", strokeWidth: 3, strokeLinecap: "round", strokeDasharray: "10 14" }), _jsx("path", { d: arrow, fill: "var(--fgColor-accent)" })] })), _jsx(EdgeLabelRenderer, { children: _jsx(Box, { className: "nodrag nopan", position: "absolute", transform: `translate(-50%, 12px) translate(${(sourceX + targetX) / 2}px, ${(sourceY + targetY) / 2}px)`, pointerEvents: "none", textAlign: "center", children: _jsx(Label, { variant: data?.connected ? 'accent' : 'secondary', children: data?.label }) }) })] }));
369
372
  });
370
- /** A member's link to a connection: still, or flowing toward it while a call runs. */
373
+ /**
374
+ * A member's link to a connection, drawn as the A2A link is: still, flowing
375
+ * toward the connection while a call runs, and back with its result.
376
+ */
371
377
  export const CallEdge = memo(function CallEdge({ id, sourceX, sourceY, targetX, targetY, data, }) {
372
- const calling = Boolean(data?.calling);
378
+ const flow = data?.flow ?? 'still';
373
379
  const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
374
- const arrow = `M ${targetX},${targetY} L ${targetX - ARROW / 1.6},${targetY - ARROW} L ${targetX + ARROW / 1.6},${targetY - ARROW} Z`;
380
+ // The arrow at the end the call or its result goes to, along the edge.
381
+ const arrow = arrowPath({ x: sourceX, y: sourceY }, { x: targetX, y: targetY }, flow === 'asking');
375
382
  return (_jsxs(_Fragment, { children: [_jsx(BaseEdge, { id: id, path: path, style: {
376
- stroke: 'var(--borderColor-default)',
383
+ stroke: 'var(--borderColor-accent-emphasis)',
377
384
  strokeWidth: 2,
378
- opacity: calling ? 0.3 : 0.8,
379
- } }), calling && (_jsxs("g", { "data-mcp-edge-call": data?.calling, children: [_jsx("path", { d: path, fill: "none", className: "a2a-team-flow a2a-team-call", stroke: "var(--fgColor-accent)", strokeWidth: 3, strokeLinecap: "round", strokeDasharray: "6 8" }), _jsx("path", { d: arrow, fill: "var(--fgColor-accent)" })] })), calling && (_jsx(EdgeLabelRenderer, { children: _jsx(Box, { className: "nodrag nopan", position: "absolute",
380
- // Beside the edge, toward the middle of the graph.
381
- transform: `translate(calc(-100% - 10px), -50%) translate(${(sourceX + targetX) / 2}px, ${(sourceY + targetY) / 2}px)`, pointerEvents: "none", whiteSpace: "nowrap", children: _jsx(Label, { variant: "accent", children: data?.calling }) }) }))] }));
385
+ opacity: flow === 'still' ? 0.6 : 0.25,
386
+ } }), flow !== 'still' && (_jsxs("g", { "data-mcp-edge-call": data?.calling, "data-mcp-edge-flow": flow, children: [_jsx("path", { d: path, fill: "none", className: `a2a-team-flow a2a-team-flow-${flow}`, stroke: "var(--fgColor-accent)", strokeWidth: 3, strokeLinecap: "round", strokeDasharray: "10 14" }), _jsx("path", { d: arrow, fill: "var(--fgColor-accent)" })] })), _jsx(EdgeLabelRenderer, { children: _jsx(Box, { className: "nodrag nopan", position: "absolute", transform: `translate(-50%, 12px) translate(${(sourceX + targetX) / 2}px, ${(sourceY + targetY) / 2}px)`, pointerEvents: "none", textAlign: "center", whiteSpace: "nowrap", "data-mcp-edge-label": "", children: _jsx(Label, { variant: "accent", children: data?.label }) }) })] }));
382
387
  });
383
388
  const NODE_TYPES = { member: MemberNode, connection: ConnectionNode };
384
389
  const EDGE_TYPES = { a2a: LinkEdge, mcp: CallEdge };
@@ -397,9 +402,30 @@ function connectionsReach(size, count, connectionHeight) {
397
402
  ? size / 4 + count * connectionHeight + (count - 1) * CONNECTIONS_APART
398
403
  : 0;
399
404
  }
400
- /** What a member's call says, on its edge and to a screen reader. */
401
- export function callWords(member, connection, tool) {
402
- return `${member} calls ${connection.label} · ${toolWords(tool, connection)}`;
405
+ /** What a link to a connection says at rest, as `A2A` says the members' link. */
406
+ export const MCP_LABEL = 'MCP';
407
+ /**
408
+ * What a link to a connection says while it carries a call, short enough
409
+ * for the room beside the member: the tool going out, `MCP · list invoices`,
410
+ * then `MCP · Odoo answers` as its result comes back.
411
+ */
412
+ export function callEdgeWords(connection, tool, flow) {
413
+ return flow === 'asking'
414
+ ? `${MCP_LABEL} · ${toolWords(tool, connection)}`
415
+ : flow === 'answering'
416
+ ? `${MCP_LABEL} · ${connection.label} answers`
417
+ : MCP_LABEL;
418
+ }
419
+ /**
420
+ * What a member's call says, on its edge and to a screen reader: the call
421
+ * going out, `Accounting calls Odoo · list invoices`, then its result
422
+ * coming back, `Odoo answers Accounting · list invoices`.
423
+ */
424
+ export function callWords(member, connection, tool, flow = 'asking') {
425
+ const words = toolWords(tool, connection);
426
+ return flow === 'answering'
427
+ ? `${connection.label} answers ${member} · ${words}`
428
+ : `${member} calls ${connection.label} · ${words}`;
403
429
  }
404
430
  /** The viewport that shows a graph `drawn` wide whole in a box `width` wide, never larger than drawn. */
405
431
  export function teamViewport(width, drawn = WIDTH) {
@@ -621,21 +647,34 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
621
647
  ? flowWords(flowOf(link.to), entry.name, memberOf(link.to)?.name ?? link.to)
622
648
  : '';
623
649
  const words = edgesWanted.map(wordsOf).filter(Boolean);
624
- // The tool each connection is answering now, in words, by its node's id.
650
+ // The tool each connection is answering now, in words, by its node's id,
651
+ // and which way its edge carries it: a call still running before one
652
+ // whose result is coming back.
625
653
  const busy = useMemo(() => {
626
654
  const byNode = {};
655
+ const edgeByNode = {};
627
656
  const said = [];
628
- for (const call of calls) {
657
+ const running = [
658
+ ...calls.filter(call => callFlow(call) === 'asking'),
659
+ ...calls.filter(call => callFlow(call) === 'answering'),
660
+ ];
661
+ for (const call of running) {
629
662
  const member = members.find(one => one.id === call.member);
630
663
  const connection = member?.connections?.find(known => known.id === call.connection);
631
664
  if (!member || !connection) {
632
665
  continue;
633
666
  }
634
667
  const node = connectionNodeId(member.id, connection.id);
668
+ const flow = callFlow(call);
669
+ const words = callWords(member.name, connection, call.tool, flow);
635
670
  byNode[node] ??= toolWords(call.tool, connection);
636
- said.push(callWords(member.name, connection, call.tool));
671
+ edgeByNode[node] ??= {
672
+ flow,
673
+ label: callEdgeWords(connection, call.tool, flow),
674
+ };
675
+ said.push(words);
637
676
  }
638
- return { byNode, said: [...new Set(said)] };
677
+ return { byNode, edgeByNode, said: [...new Set(said)] };
639
678
  }, [calls, members]);
640
679
  const linkKey = JSON.stringify(edgesWanted.map(link => ({
641
680
  ...link,
@@ -661,6 +700,7 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
661
700
  ...JSON.parse(placedKey).flatMap(({ member, connections }) => connections.map(connection => {
662
701
  const node = connectionNodeId(member, connection);
663
702
  const calling = busy.byNode[node] ?? '';
703
+ const carried = busy.edgeByNode[node];
664
704
  return {
665
705
  id: `${member}->${node}`,
666
706
  source: member,
@@ -669,7 +709,11 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
669
709
  type: 'mcp',
670
710
  selectable: false,
671
711
  focusable: false,
672
- data: { calling },
712
+ data: {
713
+ flow: carried?.flow ?? 'still',
714
+ calling,
715
+ label: carried?.label ?? MCP_LABEL,
716
+ },
673
717
  };
674
718
  })),
675
719
  ], [linkKey, placedKey, busy]);
@@ -686,19 +730,12 @@ export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows,
686
730
  '& .a2a-team-flow-answering': {
687
731
  animation: 'a2aTeamFlow 0.6s linear infinite reverse',
688
732
  },
689
- '@keyframes a2aTeamCall': {
690
- from: { strokeDashoffset: 14 },
691
- to: { strokeDashoffset: 0 },
692
- },
693
733
  '@keyframes a2aTeamPulse': {
694
734
  '0%, 100%': { boxShadow: '0 0 0 0 transparent' },
695
735
  '50%': {
696
736
  boxShadow: '0 0 0 4px var(--bgColor-accent-muted)',
697
737
  },
698
738
  },
699
- '& .a2a-team-call': {
700
- animation: 'a2aTeamCall 0.5s linear infinite',
701
- },
702
739
  '& .a2a-team-connection-busy': {
703
740
  animation: 'a2aTeamPulse 1s ease-in-out infinite',
704
741
  },
@@ -65,11 +65,20 @@ export type A2ATeamCall = {
65
65
  id?: string;
66
66
  /** When it started, in milliseconds. */
67
67
  started: number;
68
- /** Ended, but shown until it has been shown {@link CALL_SHOWN_MS}. */
69
- ended?: boolean;
68
+ /**
69
+ * When it ended: its result is shown coming back from the connection
70
+ * until {@link RESULT_SHOWN_MS} after.
71
+ */
72
+ ended?: number;
70
73
  };
71
- /** The least time a call is shown, in milliseconds: a quick one still reads. */
72
- export declare const CALL_SHOWN_MS = 900;
74
+ /**
75
+ * How long a call's result is shown coming back to the member, in
76
+ * milliseconds: the link to a connection flows like the A2A link, out with
77
+ * the call, back with its result.
78
+ */
79
+ export declare const RESULT_SHOWN_MS = 1200;
80
+ /** Which way a member's link to a connection carries a call now. */
81
+ export declare function callFlow(call: A2ATeamCall): A2ATeamFlow;
73
82
  /**
74
83
  * The connection a tool is of: one whose tools name it (a toolset's prefix
75
84
  * before the name is allowed, as a host may add one), else one whose
@@ -85,8 +94,8 @@ export declare function toolWords(tool: string, connection?: A2ATeamConnection):
85
94
  export declare function toolOwnName(tool: string, connections: A2ATeamConnection[]): string;
86
95
  /**
87
96
  * The calls running after an event of the peer's (`member`): a call it
88
- * starts to one of its connections is added; its end marks it ended, and
89
- * drops it once it has been shown long enough (else `holdMs` says when to
97
+ * starts to one of its connections is added; its end marks it ended, its
98
+ * result coming back, for {@link RESULT_SHOWN_MS} (`holdMs` says when to
90
99
  * look again, with {@link pruneCalls}). A new request, an answer or a
91
100
  * failure ends them all.
92
101
  */
@@ -94,5 +103,5 @@ export declare function callsAfter(calls: A2ATeamCall[], event: A2APeerEvent, me
94
103
  calls: A2ATeamCall[];
95
104
  holdMs?: number;
96
105
  };
97
- /** The calls without those ended and shown long enough. */
106
+ /** The calls without those whose result has been shown long enough. */
98
107
  export declare function pruneCalls(calls: A2ATeamCall[], now: number): A2ATeamCall[];
@@ -30,8 +30,16 @@ export function flowEnds(flow, entry, peer) {
30
30
  }
31
31
  return undefined;
32
32
  }
33
- /** The least time a call is shown, in milliseconds: a quick one still reads. */
34
- export const CALL_SHOWN_MS = 900;
33
+ /**
34
+ * How long a call's result is shown coming back to the member, in
35
+ * milliseconds: the link to a connection flows like the A2A link, out with
36
+ * the call, back with its result.
37
+ */
38
+ export const RESULT_SHOWN_MS = 1200;
39
+ /** Which way a member's link to a connection carries a call now. */
40
+ export function callFlow(call) {
41
+ return call.ended === undefined ? 'asking' : 'answering';
42
+ }
35
43
  /**
36
44
  * The connection a tool is of: one whose tools name it (a toolset's prefix
37
45
  * before the name is allowed, as a host may add one), else one whose
@@ -65,8 +73,8 @@ export function toolOwnName(tool, connections) {
65
73
  }
66
74
  /**
67
75
  * The calls running after an event of the peer's (`member`): a call it
68
- * starts to one of its connections is added; its end marks it ended, and
69
- * drops it once it has been shown long enough (else `holdMs` says when to
76
+ * starts to one of its connections is added; its end marks it ended, its
77
+ * result coming back, for {@link RESULT_SHOWN_MS} (`holdMs` says when to
70
78
  * look again, with {@link pruneCalls}). A new request, an answer or a
71
79
  * failure ends them all.
72
80
  */
@@ -96,24 +104,19 @@ export function callsAfter(calls, event, member, connections, now) {
96
104
  ],
97
105
  };
98
106
  }
99
- const index = calls.findIndex(call => !call.ended &&
107
+ const index = calls.findIndex(call => call.ended === undefined &&
100
108
  call.connection === connection.id &&
101
109
  (step.id && call.id ? call.id === step.id : call.tool === step.name));
102
110
  if (index < 0) {
103
111
  return { calls };
104
112
  }
105
- const call = calls[index];
106
- const left = call.started + CALL_SHOWN_MS - now;
107
- if (left <= 0) {
108
- return { calls: calls.filter((_, at) => at !== index) };
109
- }
110
113
  return {
111
- calls: calls.map((c, at) => (at === index ? { ...c, ended: true } : c)),
112
- holdMs: left,
114
+ calls: calls.map((c, at) => (at === index ? { ...c, ended: now } : c)),
115
+ holdMs: RESULT_SHOWN_MS,
113
116
  };
114
117
  }
115
- /** The calls without those ended and shown long enough. */
118
+ /** The calls without those whose result has been shown long enough. */
116
119
  export function pruneCalls(calls, now) {
117
- const kept = calls.filter(call => !call.ended || call.started + CALL_SHOWN_MS > now);
120
+ const kept = calls.filter(call => call.ended === undefined || call.ended + RESULT_SHOWN_MS > now);
118
121
  return kept.length === calls.length ? calls : kept;
119
122
  }
@@ -20,6 +20,12 @@ export type HostTurnRequest<P> = Readonly<{
20
20
  * another application): a new session's uid is derived with it.
21
21
  */
22
22
  sessionGeneration?: number;
23
+ /**
24
+ * The host user never linked: exchanged as a visitor, which ai-agents
25
+ * answers only when the installation's owner opened it to visitors and the
26
+ * deployment opens to somebody signed out (WHATSAPP WA-03).
27
+ */
28
+ visitor?: boolean;
23
29
  }>;
24
30
  export type TurnStream = Readonly<{
25
31
  /**
@@ -111,6 +117,8 @@ export declare class HostKeyCredentials<P> implements HostCredentials<P> {
111
117
  hostKey: string;
112
118
  /** Where the turn is asked from: Datalayer refuses a denied channel at once. */
113
119
  place?: Place;
120
+ /** As a visitor: the host user never linked (WHATSAPP WA-03). */
121
+ visitor?: boolean;
114
122
  }): Promise<HostExchange>;
115
123
  forTurn(request: HostTurnRequest<P>): Promise<HostSession>;
116
124
  }