@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.
- package/dist-embed/datalayer-app-main.js +38 -34
- package/lib/apps/embed/element.js +20 -5
- package/lib/apps/embed/visitorApp.d.ts +23 -45
- package/lib/apps/embed/visitorApp.js +65 -13
- package/lib/components/teams/A2ATeamGraph.d.ts +28 -7
- package/lib/components/teams/A2ATeamGraph.js +64 -27
- package/lib/components/teams/a2aTeamFlow.d.ts +16 -7
- package/lib/components/teams/a2aTeamFlow.js +17 -14
- package/lib/portable/hosts/agentClient.d.ts +8 -0
- package/lib/portable/hosts/agentClient.js +2 -0
- package/lib/portable/hosts/approvalRenderers.d.ts +10 -2
- package/lib/portable/hosts/approvalRenderers.js +81 -2
- package/lib/portable/hosts/contracts.d.ts +1 -0
- package/lib/portable/hosts/contracts.js +2 -0
- package/lib/portable/hosts/contracts.json +56 -2
- package/package.json +1 -1
- package/scripts/codegen/generate_mcp_servers.py +5 -16
|
@@ -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({
|
|
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
|
|
414
|
-
* the
|
|
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
|
|
49
|
-
*
|
|
50
|
-
* embed token: the two live on different
|
|
51
|
-
* prod1), found 15 October 2026 on
|
|
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
|
|
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
|
|
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
|
|
21
|
-
*
|
|
22
|
-
* embed token: the two live on different
|
|
23
|
-
* prod1), found 15 October 2026 on
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(`${
|
|
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(
|
|
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
|
|
25
|
-
* the member calls one of the connection's tools
|
|
26
|
-
* connection for as long as the call runs
|
|
27
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
241
|
-
export declare
|
|
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
|
-
/**
|
|
30
|
-
|
|
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:
|
|
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
|
-
/**
|
|
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
|
|
378
|
+
const flow = data?.flow ?? 'still';
|
|
373
379
|
const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
|
|
374
|
-
|
|
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-
|
|
383
|
+
stroke: 'var(--borderColor-accent-emphasis)',
|
|
377
384
|
strokeWidth: 2,
|
|
378
|
-
opacity:
|
|
379
|
-
} }),
|
|
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
|
|
401
|
-
export
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
/**
|
|
69
|
-
|
|
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
|
-
/**
|
|
72
|
-
|
|
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,
|
|
89
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
34
|
-
|
|
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,
|
|
69
|
-
*
|
|
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 =>
|
|
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:
|
|
112
|
-
holdMs:
|
|
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
|
|
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 =>
|
|
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
|
}
|