@datalayer/agent-runtimes 1.3.80 → 1.3.82

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 (97) hide show
  1. package/lib/chat/assistant/AssistantStage.d.ts +9 -4
  2. package/lib/chat/assistant/AssistantStage.js +18 -6
  3. package/lib/chat/assistant/SpeechBalloon.js +16 -1
  4. package/lib/chat/assistant/SpriteCharacter.d.ts +25 -3
  5. package/lib/chat/assistant/SpriteCharacter.js +68 -3
  6. package/lib/chat/assistant/formats/acf.d.ts +56 -0
  7. package/lib/chat/assistant/formats/acf.js +317 -0
  8. package/lib/chat/assistant/formats/acs.d.ts +85 -7
  9. package/lib/chat/assistant/formats/acs.js +111 -39
  10. package/lib/chat/assistant/formats/files.d.ts +6 -2
  11. package/lib/chat/assistant/formats/files.js +18 -4
  12. package/lib/chat/assistant/formats/index.d.ts +6 -3
  13. package/lib/chat/assistant/formats/index.js +4 -2
  14. package/lib/chat/assistant/formats/types.d.ts +14 -0
  15. package/lib/chat/assistant/state.d.ts +2 -0
  16. package/lib/chat/assistant/state.js +9 -0
  17. package/lib/chat/base/ChatBase.js +47 -4
  18. package/lib/chat/base/loopStep.d.ts +40 -0
  19. package/lib/chat/base/loopStep.js +73 -0
  20. package/lib/chat/indicators/McpStatusIndicator.d.ts +1 -1
  21. package/lib/chat/indicators/McpStatusIndicator.js +13 -6
  22. package/lib/chat/indicators/SkillsStatusIndicator.d.ts +1 -1
  23. package/lib/chat/indicators/SkillsStatusIndicator.js +7 -3
  24. package/lib/chat/prompt/InputPrompt.d.ts +4 -1
  25. package/lib/chat/prompt/InputPrompt.js +2 -2
  26. package/lib/chat/prompt/InputPromptBase.d.ts +4 -1
  27. package/lib/chat/prompt/InputPromptBase.js +2 -2
  28. package/lib/chat/prompt/InputPromptLexical.d.ts +7 -1
  29. package/lib/chat/prompt/InputPromptLexical.js +5 -3
  30. package/lib/components/teams/index.d.ts +1 -0
  31. package/lib/components/teams/index.js +1 -0
  32. package/lib/components/teams/usePeerSandbox.d.ts +13 -0
  33. package/lib/components/teams/usePeerSandbox.js +59 -0
  34. package/lib/examples/AgentA2ATeamExample.js +7 -2
  35. package/lib/loop/apps/AppComposer.d.ts +31 -0
  36. package/lib/loop/apps/AppComposer.js +67 -0
  37. package/lib/loop/apps/AppRenderer.d.ts +16 -5
  38. package/lib/loop/apps/AppRenderer.js +49 -16
  39. package/lib/loop/apps/appspec.d.ts +8 -1
  40. package/lib/loop/apps/appspec.js +119 -2
  41. package/lib/loop/apps/checks.js +161 -1
  42. package/lib/loop/apps/composer.d.ts +37 -0
  43. package/lib/loop/apps/composer.js +61 -0
  44. package/lib/loop/apps/hostTools.d.ts +17 -0
  45. package/lib/loop/apps/hostTools.js +20 -0
  46. package/lib/loop/apps/index.d.ts +2 -0
  47. package/lib/loop/apps/index.js +2 -0
  48. package/lib/loop/apps/rules.d.ts +22 -1
  49. package/lib/loop/apps/rules.js +49 -1
  50. package/lib/loop/core/index.d.ts +31 -0
  51. package/lib/loop/core/index.js +22 -0
  52. package/lib/loop/embed/AppEmbed.d.ts +100 -6
  53. package/lib/loop/embed/AppEmbed.js +172 -27
  54. package/lib/loop/embed/LoopEmbed.js +2 -0
  55. package/lib/loop/embed/element.js +58 -5
  56. package/lib/loop/embed/embedConfig.d.ts +10 -1
  57. package/lib/loop/embed/embedConfig.js +12 -1
  58. package/lib/loop/embed/embedSession.d.ts +79 -0
  59. package/lib/loop/embed/embedSession.js +136 -0
  60. package/lib/loop/embed/embedTheme.d.ts +12 -9
  61. package/lib/loop/embed/embedTheme.js +32 -22
  62. package/lib/loop/embed/hostBridge.d.ts +79 -0
  63. package/lib/loop/embed/hostBridge.js +104 -0
  64. package/lib/loop/index.d.ts +5 -3
  65. package/lib/loop/index.js +6 -3
  66. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.d.ts +5 -2
  67. package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +7 -1
  68. package/lib/loop/plugins/a2ui-surface/index.d.ts +1 -1
  69. package/lib/loop/plugins/a2ui-surface/index.js +4 -4
  70. package/lib/loop/plugins/a2ui-surface/toolResult.d.ts +27 -0
  71. package/lib/loop/plugins/a2ui-surface/toolResult.js +22 -0
  72. package/lib/loop/plugins/agent-capacity/index.d.ts +3 -1
  73. package/lib/loop/plugins/agent-capacity/index.js +2 -1
  74. package/lib/loop/plugins/chat/ChatView.js +31 -4
  75. package/lib/loop/plugins/chat/index.d.ts +6 -1
  76. package/lib/loop/presets.d.ts +3 -1
  77. package/lib/loop/presets.js +2 -1
  78. package/lib/loop/shell/LoopWorkspace.js +3 -0
  79. package/lib/protocols/BrowserAgentAdapter.d.ts +18 -0
  80. package/lib/protocols/BrowserAgentAdapter.js +35 -2
  81. package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +8 -0
  82. package/lib/runtimes/browser/a2aPeer.d.ts +11 -0
  83. package/lib/runtimes/browser/a2aPeer.js +15 -1
  84. package/lib/runtimes/browser/protocol.d.ts +3 -0
  85. package/lib/runtimes/browser/protocol.js +2 -1
  86. package/lib/specs/agents/agents.js +6 -25
  87. package/lib/specs/apps.js +50 -8
  88. package/lib/specs/appspecSchema.js +258 -0
  89. package/lib/types/agentspecs.d.ts +86 -0
  90. package/lib/types/chat.d.ts +29 -1
  91. package/package.json +1 -1
  92. package/scripts/codegen/generate_agents.py +4 -2
  93. package/scripts/codegen/generate_apps.py +7 -0
  94. package/scripts/codegen/generate_backend_tools.py +4 -12
  95. package/scripts/codegen/generate_cogs.py +3 -1
  96. package/scripts/codegen/generate_ui_plugins.py +22 -9
  97. package/scripts/voice/transcribe.mjs +29 -21
@@ -0,0 +1,79 @@
1
+ /**
2
+ * An embedded application's conversation, picked up again after the host's
3
+ * page reloads (LOOP D-13).
4
+ *
5
+ * The session an embed opened is kept by its uid in the host page's own
6
+ * storage, under the application and the visit its embed token names —
7
+ * the host's server issues the reloaded page a token for the same visit.
8
+ * After the reload, the session is asked for its conversation on the
9
+ * agent-runtimes server that holds it, with that token, and the chat draws
10
+ * it again and goes on with it. A session that is gone, or refused, is
11
+ * forgotten and said once; a new one starts.
12
+ *
13
+ * Kept in memory as well, for as long as the page lives: a token renewed
14
+ * for the visit redraws the application, which goes on with its session
15
+ * even when the host keeps nothing (`resume="false"`).
16
+ *
17
+ * Pure but for the storage and the request, each given or guarded: the page
18
+ * works without storage.
19
+ *
20
+ * @module loop/embed/embedSession
21
+ */
22
+ import type { ChatMessage } from '../../types/messages';
23
+ /** Where a visit's session is kept in the host's storage: `<prefix>:<app>:<visit>`. */
24
+ export declare const EMBED_SESSION_KEY_PREFIX = "datalayer-app:session";
25
+ /**
26
+ * The visit an embed token was issued for: its `visit` claim, read without
27
+ * checking the signature — the runtime checks the token; the page only
28
+ * needs the name to keep the session under.
29
+ */
30
+ export declare function visitOfToken(token: string | undefined): string | undefined;
31
+ /** The key a visit's session is kept under. */
32
+ export declare function embedSessionKey(app: string, visit: string): string;
33
+ /** What keeps a visit's session uid. */
34
+ export type SessionKeeper = {
35
+ read: (key: string) => string | undefined;
36
+ keep: (key: string, uid: string) => void;
37
+ forget: (key: string) => void;
38
+ };
39
+ /**
40
+ * The keeper of a visit's session: in memory, and — unless the host opted
41
+ * out — in its storage too, read first. Every access to the storage is
42
+ * guarded: a page whose storage is blocked, full or absent keeps the
43
+ * session in memory and resumes nothing after a reload.
44
+ */
45
+ export declare function sessionKeeper({ persist, storage, }: {
46
+ persist: boolean;
47
+ storage?: () => Storage | undefined;
48
+ }): SessionKeeper;
49
+ /** Forget what this page holds in memory (a test). */
50
+ export declare function forgetSessionsInMemory(): void;
51
+ /**
52
+ * A session's conversation as the chat draws it, from the AG-UI messages
53
+ * the runtime says (`GET …/sessions/{uid}/messages`): what was said and
54
+ * answered, in words. A page an answer drew is not drawn again here.
55
+ */
56
+ export declare function messagesOfThread(messages: unknown): ChatMessage[];
57
+ /** What asking for a kept session found. */
58
+ export type Reattached = {
59
+ kind: 'resumed';
60
+ uid: string;
61
+ messages: ChatMessage[];
62
+ } | {
63
+ kind: 'gone';
64
+ uid: string;
65
+ };
66
+ /**
67
+ * Ask the server that runs the application for a session's conversation,
68
+ * with the visit's token. Anything but an answer — a session the runtime
69
+ * let go, another visit's, a token refused, a server out of reach — is
70
+ * `gone`: a new session starts.
71
+ */
72
+ export declare function reattachSession({ serverUrl, uid, token, fetcher, }: {
73
+ serverUrl: string;
74
+ uid: string;
75
+ token: string;
76
+ fetcher?: typeof fetch;
77
+ }): Promise<Reattached>;
78
+ /** What the page says, once, when the conversation it kept is gone. */
79
+ export declare const sessionGoneSentence: (name: string) => string;
@@ -0,0 +1,136 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** Where a visit's session is kept in the host's storage: `<prefix>:<app>:<visit>`. */
6
+ export const EMBED_SESSION_KEY_PREFIX = 'datalayer-app:session';
7
+ /**
8
+ * The visit an embed token was issued for: its `visit` claim, read without
9
+ * checking the signature — the runtime checks the token; the page only
10
+ * needs the name to keep the session under.
11
+ */
12
+ export function visitOfToken(token) {
13
+ const payload = token?.split('.')[1];
14
+ if (!payload) {
15
+ return undefined;
16
+ }
17
+ try {
18
+ const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
19
+ const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
20
+ const claims = JSON.parse(atob(padded));
21
+ return typeof claims.visit === 'string' && claims.visit
22
+ ? claims.visit
23
+ : undefined;
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ /** The key a visit's session is kept under. */
30
+ export function embedSessionKey(app, visit) {
31
+ return `${EMBED_SESSION_KEY_PREFIX}:${app}:${visit}`;
32
+ }
33
+ /** The sessions of this page, while it lives. */
34
+ const IN_MEMORY = new Map();
35
+ /** The host's `localStorage`, when there is one the page may use. */
36
+ function hostStorage() {
37
+ try {
38
+ return typeof window !== 'undefined' ? window.localStorage : undefined;
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ /**
45
+ * The keeper of a visit's session: in memory, and — unless the host opted
46
+ * out — in its storage too, read first. Every access to the storage is
47
+ * guarded: a page whose storage is blocked, full or absent keeps the
48
+ * session in memory and resumes nothing after a reload.
49
+ */
50
+ export function sessionKeeper({ persist, storage = hostStorage, }) {
51
+ return {
52
+ read: key => {
53
+ if (persist) {
54
+ try {
55
+ const kept = storage()?.getItem(key);
56
+ if (kept) {
57
+ return kept;
58
+ }
59
+ }
60
+ catch {
61
+ // No storage: what the page holds in memory.
62
+ }
63
+ }
64
+ return IN_MEMORY.get(key);
65
+ },
66
+ keep: (key, uid) => {
67
+ IN_MEMORY.set(key, uid);
68
+ if (persist) {
69
+ try {
70
+ storage()?.setItem(key, uid);
71
+ }
72
+ catch {
73
+ // Kept in memory only.
74
+ }
75
+ }
76
+ },
77
+ forget: key => {
78
+ IN_MEMORY.delete(key);
79
+ try {
80
+ storage()?.removeItem(key);
81
+ }
82
+ catch {
83
+ // Nothing kept there to forget.
84
+ }
85
+ },
86
+ };
87
+ }
88
+ /** Forget what this page holds in memory (a test). */
89
+ export function forgetSessionsInMemory() {
90
+ IN_MEMORY.clear();
91
+ }
92
+ /**
93
+ * A session's conversation as the chat draws it, from the AG-UI messages
94
+ * the runtime says (`GET …/sessions/{uid}/messages`): what was said and
95
+ * answered, in words. A page an answer drew is not drawn again here.
96
+ */
97
+ export function messagesOfThread(messages) {
98
+ if (!Array.isArray(messages)) {
99
+ return [];
100
+ }
101
+ const drawn = [];
102
+ for (const message of messages) {
103
+ if (!message || typeof message !== 'object') {
104
+ continue;
105
+ }
106
+ const { id, role, content } = message;
107
+ if ((role === 'user' || role === 'assistant') &&
108
+ typeof id === 'string' &&
109
+ typeof content === 'string' &&
110
+ content.trim()) {
111
+ drawn.push({ id, role, content, createdAt: new Date() });
112
+ }
113
+ }
114
+ return drawn;
115
+ }
116
+ /**
117
+ * Ask the server that runs the application for a session's conversation,
118
+ * with the visit's token. Anything but an answer — a session the runtime
119
+ * let go, another visit's, a token refused, a server out of reach — is
120
+ * `gone`: a new session starts.
121
+ */
122
+ export async function reattachSession({ serverUrl, uid, token, fetcher = fetch, }) {
123
+ try {
124
+ const response = await fetcher(`${serverUrl.replace(/\/+$/, '')}/api/v1/apps/sessions/${encodeURIComponent(uid)}/messages`, { headers: { Authorization: `Bearer ${token}` } });
125
+ if (!response.ok) {
126
+ return { kind: 'gone', uid };
127
+ }
128
+ const body = (await response.json());
129
+ return { kind: 'resumed', uid, messages: messagesOfThread(body.messages) };
130
+ }
131
+ catch {
132
+ return { kind: 'gone', uid };
133
+ }
134
+ }
135
+ /** What the page says, once, when the conversation it kept is gone. */
136
+ export const sessionGoneSentence = (name) => `Your earlier conversation with ${name} has ended, so this is a new one.`;
@@ -1,24 +1,27 @@
1
1
  import { type ThemeStyles } from '@datalayer/primer-addons';
2
- import type { AppAccent } from '../../types/agentspecs';
2
+ import type { AppAccent, AppThemeVariant } from '../../types/agentspecs';
3
3
  import type { ThemeOverrides } from '../../types/chat';
4
4
  /**
5
- * The `loop` theme as the embed wears it: the application's accent over it
6
- * in both modes (T-05), and the host's face when it named one.
5
+ * The theme as the embed wears it — `loop`, or the one the application
6
+ * names (T-30) — with the application's accent over `loop` in both modes
7
+ * (T-05), and the host's face when it named one.
7
8
  */
8
- export declare function embedThemeStyles({ accent, font, }: {
9
+ export declare function embedThemeStyles({ accent, font, variant, }: {
9
10
  accent: AppAccent;
10
11
  font?: string;
12
+ variant?: AppThemeVariant;
11
13
  }): ThemeStyles;
12
14
  /**
13
15
  * The same accent and face laid over the conversation's own theme (T-05,
14
- * D-11): the chat sets the `loop` theme again inside it, which would put the
15
- * theme's mint and face back over what the embed set around it. The
16
- * accent's properties in each mode, and, when the host named a face, every
17
- * property of the theme that names its own.
16
+ * D-11): the chat sets its theme again inside it, which would put the
17
+ * theme's colours and face back over what the embed set around it. The
18
+ * accent's properties in each mode (over `loop` only), and, when the host
19
+ * named a face, every property of the theme that names its own.
18
20
  */
19
- export declare function embedThemeOverrides({ accent, font, }: {
21
+ export declare function embedThemeOverrides({ accent, font, variant, }: {
20
22
  accent: AppAccent;
21
23
  font?: string;
24
+ variant?: AppThemeVariant;
22
25
  }): ThemeOverrides;
23
26
  /**
24
27
  * The element's own stylesheet, written into its shadow root.
@@ -2,46 +2,56 @@
2
2
  * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- import { loopAccentStyles, loopFontFamily, loopThemeStyles, scopeThemeCss, THEME_SCOPE_ATTRIBUTE, } from '@datalayer/primer-addons';
5
+ import { getThemeConfig, loopAccentStyles, loopFontFamily, scopeThemeCss, THEME_SCOPE_ATTRIBUTE, } from '@datalayer/primer-addons';
6
6
  /** One mode's properties, with the theme's face replaced by the host's. */
7
- function withFace(styles, font) {
8
- if (!font) {
7
+ function withFace(styles, font, own) {
8
+ if (!font || !own) {
9
9
  return styles;
10
10
  }
11
11
  return Object.fromEntries(Object.entries(styles).map(([name, value]) => [
12
12
  name,
13
- typeof value === 'string'
14
- ? value.split(loopFontFamily).join(font)
15
- : value,
13
+ typeof value === 'string' ? value.split(own).join(font) : value,
16
14
  ]));
17
15
  }
16
+ /** The face a theme names in a mode: `loop`'s own, or the theme's `fontFamily`. */
17
+ function faceOf(variant, styles) {
18
+ if (variant === 'loop') {
19
+ return loopFontFamily;
20
+ }
21
+ return typeof styles.fontFamily === 'string' ? styles.fontFamily : '';
22
+ }
23
+ /** The accent over a theme: `loop`'s colours, over `loop` only (T-30). */
24
+ const accentOver = (variant, accent, which) => variant === 'loop' ? loopAccentStyles(accent, which) : {};
18
25
  /**
19
- * The `loop` theme as the embed wears it: the application's accent over it
20
- * in both modes (T-05), and the host's face when it named one.
26
+ * The theme as the embed wears it — `loop`, or the one the application
27
+ * names (T-30) — with the application's accent over `loop` in both modes
28
+ * (T-05), and the host's face when it named one.
21
29
  */
22
- export function embedThemeStyles({ accent, font = '', }) {
23
- const mode = (which) => withFace({
24
- ...loopThemeStyles[which],
25
- ...loopAccentStyles(accent, which),
26
- }, font);
30
+ export function embedThemeStyles({ accent, font = '', variant = 'loop', }) {
31
+ const base = getThemeConfig(variant).themeStyles;
32
+ const mode = (which) => {
33
+ const theme = base[which];
34
+ return withFace({ ...theme, ...accentOver(variant, accent, which) }, font, faceOf(variant, theme));
35
+ };
27
36
  return {
28
- ...loopThemeStyles,
37
+ ...base,
29
38
  light: mode('light'),
30
39
  dark: mode('dark'),
31
40
  };
32
41
  }
33
42
  /**
34
43
  * The same accent and face laid over the conversation's own theme (T-05,
35
- * D-11): the chat sets the `loop` theme again inside it, which would put the
36
- * theme's mint and face back over what the embed set around it. The
37
- * accent's properties in each mode, and, when the host named a face, every
38
- * property of the theme that names its own.
44
+ * D-11): the chat sets its theme again inside it, which would put the
45
+ * theme's colours and face back over what the embed set around it. The
46
+ * accent's properties in each mode (over `loop` only), and, when the host
47
+ * named a face, every property of the theme that names its own.
39
48
  */
40
- export function embedThemeOverrides({ accent, font = '', }) {
49
+ export function embedThemeOverrides({ accent, font = '', variant = 'loop', }) {
50
+ const base = getThemeConfig(variant).themeStyles;
41
51
  const mode = (which) => {
42
- const theme = loopThemeStyles[which];
43
- const faced = Object.fromEntries(Object.entries(withFace(theme, font)).filter(([name, value]) => typeof value === 'string' && value !== theme[name]));
44
- return { ...faced, ...loopAccentStyles(accent, which) };
52
+ const theme = base[which];
53
+ const faced = Object.fromEntries(Object.entries(withFace(theme, font, faceOf(variant, theme))).filter(([name, value]) => typeof value === 'string' && value !== theme[name]));
54
+ return { ...faced, ...accentOver(variant, accent, which) };
45
55
  };
46
56
  return { light: mode('light'), dark: mode('dark') };
47
57
  }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The host page and an embedded application, talking (LOOP D-10).
3
+ *
4
+ * In: the values the page passes — `user`, `page`, or names of its own —
5
+ * read by the application's agent with the tool `host_context`, only those
6
+ * its Appspec names (`deployment.embedded.host.context`).
7
+ *
8
+ * Out: what the application does, as events — `message` when it has said
9
+ * something, `action` when it called a function of the page — beside the
10
+ * `decision` and `token-expired` the element raises already.
11
+ *
12
+ * Offered: the functions of the page its Appspec names
13
+ * (`deployment.embedded.host.functions`), each called by its agent as the
14
+ * tool `host_<name>`. Every one of these tools is decided in the page by
15
+ * the rule that names it (`behaviourFor`), before the page is called: one no
16
+ * rule names, or *leave it to me*, is refused; *ask me first* and *do it if
17
+ * I asked* ask the person on the page (`ask`, else the browser's `confirm`),
18
+ * and a call they do not allow is refused; *do it* is done.
19
+ *
20
+ * Pure: the tools call the page, and ask, through what they are given.
21
+ *
22
+ * @module loop/embed/hostBridge
23
+ */
24
+ import type { AppSpec } from '../../types/agentspecs';
25
+ import type { FrontendToolDefinition } from '../../types/tools';
26
+ import { HOST_CONTEXT_TOOL, HOST_NAME, hostTool, hostToolsOf } from '../apps/hostTools';
27
+ export { HOST_CONTEXT_TOOL, HOST_NAME, hostTool, hostToolsOf };
28
+ /** What the application tells the page it sits in. */
29
+ export type HostEvent = {
30
+ type: 'message';
31
+ /** What it said, as written. */
32
+ detail: {
33
+ id: string;
34
+ text: string;
35
+ };
36
+ } | {
37
+ type: 'action';
38
+ /** A function of the page it called, with what came back or why not. */
39
+ detail: {
40
+ function: string;
41
+ arguments: Record<string, unknown>;
42
+ result?: unknown;
43
+ error?: string;
44
+ };
45
+ };
46
+ /** A function of the page, as the page gives it. */
47
+ export type HostFunction = (args: Record<string, unknown>) => unknown | Promise<unknown>;
48
+ /** What the page gives an embedded application (LOOP D-10). */
49
+ export type AppEmbedHost = {
50
+ /** What the page passes now: its visitor (`user`), itself (`page`), values of its own. */
51
+ context?: () => Record<string, unknown>;
52
+ /** The page's functions, by the names the Appspec gives them. */
53
+ functions?: Record<string, HostFunction>;
54
+ /** Told what the application does: `message`, `action`. */
55
+ onEvent?: (event: HostEvent) => void;
56
+ /**
57
+ * Asks the person on the page whether a call its rule says to ask first
58
+ * may be made; the browser's `confirm` when the page gives none.
59
+ */
60
+ ask?: (question: HostQuestion) => boolean | Promise<boolean>;
61
+ };
62
+ /** What the person is asked before a call of the page is made. */
63
+ export type HostQuestion = {
64
+ /** The function, as the Appspec names it. */
65
+ function: string;
66
+ arguments: Record<string, unknown>;
67
+ /** The question, in a sentence. */
68
+ sentence: string;
69
+ };
70
+ /** The sentence a call is refused with when no rule names its tool. */
71
+ export declare const hostRefused: (tool: string) => string;
72
+ /** The sentence a call is refused with when the person did not allow it. */
73
+ export declare const hostNotAllowed: (tool: string) => string;
74
+ /**
75
+ * The frontend tools its agent is given for the page: `host_context` when
76
+ * the Appspec names values, and `host_<name>` for each function it names.
77
+ * `host` is read at each call, so that what the page passes is the newest.
78
+ */
79
+ export declare function hostFrontendTools(app: Pick<AppSpec, 'connections' | 'rules' | 'deployment'>, host: () => AppEmbedHost): FrontendToolDefinition[];
@@ -0,0 +1,104 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { behaviourFor } from '../apps/rules';
6
+ import { HOST_CONTEXT_TOOL, HOST_NAME, hostTool, hostToolsOf, } from '../apps/hostTools';
7
+ export { HOST_CONTEXT_TOOL, HOST_NAME, hostTool, hostToolsOf };
8
+ /** The sentence a call is refused with when no rule names its tool. */
9
+ export const hostRefused = (tool) => `No rule of this application names ${tool}: it is left to the person, and the page was not called.`;
10
+ /** The sentence a call is refused with when the person did not allow it. */
11
+ export const hostNotAllowed = (tool) => `The person did not allow ${tool}: the page was not called.`;
12
+ /** Asks with the browser's `confirm`, where there is one; refuses where not. */
13
+ const confirmOnPage = (question) => typeof window !== 'undefined' && typeof window.confirm === 'function'
14
+ ? window.confirm(question.sentence)
15
+ : false;
16
+ /** The words of the rule that names the tool, for the question. */
17
+ const actionOf = (app, tool) => app.rules.find(rule => rule.appliesTo.includes(tool))?.action;
18
+ /**
19
+ * The frontend tools its agent is given for the page: `host_context` when
20
+ * the Appspec names values, and `host_<name>` for each function it names.
21
+ * `host` is read at each call, so that what the page passes is the newest.
22
+ */
23
+ export function hostFrontendTools(app, host) {
24
+ const bridge = app.deployment.embedded?.host;
25
+ if (!bridge) {
26
+ return [];
27
+ }
28
+ const behaviour = (tool) => behaviourFor(app, tool);
29
+ const ruled = (tool) => behaviour(tool) !== 'leave_to_me';
30
+ const tools = [];
31
+ if (bridge.context.length > 0) {
32
+ tools.push({
33
+ name: HOST_CONTEXT_TOOL,
34
+ description: `What the page you are embedded in says of its visitor and of itself: ${bridge.context.join(', ')}. Read it before answering what depends on them.`,
35
+ parameters: { type: 'object', properties: {} },
36
+ location: 'frontend',
37
+ handler: async () => {
38
+ if (!ruled(HOST_CONTEXT_TOOL)) {
39
+ return { error: hostRefused(HOST_CONTEXT_TOOL) };
40
+ }
41
+ const passed = host().context?.() ?? {};
42
+ const values = {};
43
+ const unsaid = [];
44
+ for (const name of bridge.context) {
45
+ if (passed[name] === undefined) {
46
+ unsaid.push(name);
47
+ }
48
+ else {
49
+ values[name] = passed[name];
50
+ }
51
+ }
52
+ return { values, ...(unsaid.length > 0 ? { unsaid } : {}) };
53
+ },
54
+ });
55
+ }
56
+ for (const fn of bridge.functions) {
57
+ const tool = hostTool(fn.name);
58
+ tools.push({
59
+ name: tool,
60
+ description: fn.description,
61
+ parameters: fn.parameters,
62
+ location: 'frontend',
63
+ handler: async (args) => {
64
+ const said = (detail) => {
65
+ host().onEvent?.({
66
+ type: 'action',
67
+ detail: { function: fn.name, arguments: args, ...detail },
68
+ });
69
+ return detail;
70
+ };
71
+ if (!ruled(tool)) {
72
+ return said({ error: hostRefused(tool) });
73
+ }
74
+ if (behaviour(tool) !== 'do_it') {
75
+ const action = actionOf(app, tool) ?? fn.description;
76
+ const ask = host().ask ?? confirmOnPage;
77
+ const allowed = await ask({
78
+ function: fn.name,
79
+ arguments: args,
80
+ sentence: `${action}? (${fn.name} with ${JSON.stringify(args)})`,
81
+ });
82
+ if (!allowed) {
83
+ return said({ error: hostNotAllowed(tool) });
84
+ }
85
+ }
86
+ const call = host().functions?.[fn.name];
87
+ if (!call) {
88
+ return said({
89
+ error: `The page offers no function ${fn.name}: it was not called.`,
90
+ });
91
+ }
92
+ try {
93
+ return said({ result: await call(args) });
94
+ }
95
+ catch (error) {
96
+ return said({
97
+ error: `The page's ${fn.name} failed: ${error instanceof Error ? error.message : String(error)}`,
98
+ });
99
+ }
100
+ },
101
+ });
102
+ }
103
+ return tools;
104
+ }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module loop
8
8
  */
9
- export { LoopCommand, LoopDocumentToolbar, LoopMention, LoopNotebookToolbar, LoopSlots, LoopViewType, canOpenView, createPromptChannel, parseCommand, type CommandArgSpec, type CommandContribution, type CommandResult, type EditorToolbarContext, type EditorToolbarContribution, type LoopCommandContext, type LoopViewProps, type LoopWorkspaceContext, type MentionCandidate, type MentionContribution, type PromptChannel, type SandboxSnapshot, type SandboxState, type ViewControls, type ViewTypeContribution, useLoopPromptStore, suggestLoopPrompt, focusPrompt, onPromptFocusRequest, type LoopPromptState, type SuggestedPrompt, } from './core';
9
+ export { LoopCommand, LoopDocumentToolbar, LoopMention, LoopNotebookToolbar, LoopRunProps, LoopSlots, LoopViewType, canOpenView, createPromptChannel, parseCommand, runForwardedProps, type CommandArgSpec, type CommandContribution, type CommandResult, type EditorToolbarContext, type EditorToolbarContribution, type LoopCommandContext, type LoopViewProps, type LoopWorkspaceContext, type MentionCandidate, type MentionContribution, type PromptChannel, type RunPropsContribution, type SandboxSnapshot, type SandboxState, type ViewControls, type ViewTypeContribution, useLoopPromptStore, suggestLoopPrompt, focusPrompt, onPromptFocusRequest, type LoopPromptState, type SuggestedPrompt, } from './core';
10
10
  export { useEditorToolbar } from './core/toolbar';
11
11
  export { LoopWorkspace, PluginToggles, ViewSwitcher, WorkspaceFullScreenAction, buildLoopReactor, useWorkspaceFullScreen, type LoopWorkspaceProps, type PluginTogglesProps, type ViewSwitcherProps, } from './shell';
12
12
  export { A2uiPlugin, A2UI_PLUGIN_NAME } from './plugins/a2ui';
@@ -67,9 +67,11 @@ export { WindowFrame, WindowFramePlugin, WINDOW_ACTIONS_SLOT, WINDOW_CONTROLS_SL
67
67
  export { LoopCommandsPlugin, COMMANDS_PLUGIN_NAME } from './plugins/commands';
68
68
  export { PromptPlugin, PROMPT_PLUGIN_NAME } from './plugins/prompt';
69
69
  export { LoopEmbed, type LoopEmbedProps } from './embed/LoopEmbed';
70
- export { AppEmbed, type AppEmbedProps } from './embed/AppEmbed';
70
+ export { AppEmbed, AppFloating, embedAssistantCharacter, type AppEmbedProps, type AppFloatingProps, type EmbedSession, useEmbedSession, useHostBridge, } from './embed/AppEmbed';
71
+ export { EMBED_SESSION_KEY_PREFIX, embedSessionKey, visitOfToken, } from './embed/embedSession';
72
+ export { HOST_CONTEXT_TOOL, hostFrontendTools, hostNotAllowed, hostRefused, hostTool, hostToolsOf, type AppEmbedHost, type HostEvent, type HostFunction, type HostQuestion, } from './embed/hostBridge';
71
73
  export { defineDatalayerAppElement, type DatalayerAppElementOptions, } from './embed/element';
72
- export { EMBED_ATTRIBUTES, EMBED_HOST_VARIABLES, EMBED_TAG, embedLookOf, embedSnippetOf, floatingViewOf, type EmbedLook, type EmbedSnippetOptions, } from './embed/embedConfig';
74
+ export { EMBED_ATTRIBUTES, EMBED_HOST_VARIABLES, EMBED_TAG, embedLookOf, embedSnippetOf, floatingViewOf, resumeOf, type EmbedLook, type EmbedSnippetOptions, } from './embed/embedConfig';
73
75
  export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
74
76
  export { loopPlugins, type LoopPresetOptions } from './presets';
75
77
  export { DocumentExtension, NotebookExtension } from './extensions';
package/lib/loop/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * @module loop
12
12
  */
13
13
  // The contracts. A plugin depends on these and on the reactor, never on the shell.
14
- export { LoopCommand, LoopDocumentToolbar, LoopMention, LoopNotebookToolbar, LoopSlots, LoopViewType, canOpenView, createPromptChannel, parseCommand, useLoopPromptStore, suggestLoopPrompt, focusPrompt, onPromptFocusRequest, } from './core';
14
+ export { LoopCommand, LoopDocumentToolbar, LoopMention, LoopNotebookToolbar, LoopRunProps, LoopSlots, LoopViewType, canOpenView, createPromptChannel, parseCommand, runForwardedProps, useLoopPromptStore, suggestLoopPrompt, focusPrompt, onPromptFocusRequest, } from './core';
15
15
  export { useEditorToolbar } from './core/toolbar';
16
16
  // The shell.
17
17
  export { LoopWorkspace, PluginToggles, ViewSwitcher, WorkspaceFullScreenAction, buildLoopReactor, useWorkspaceFullScreen, } from './shell';
@@ -87,9 +87,12 @@ export { PromptPlugin, PROMPT_PLUGIN_NAME } from './plugins/prompt';
87
87
  export { LoopEmbed } from './embed/LoopEmbed';
88
88
  // An application in another product's page: the React component, the
89
89
  // `<datalayer-app>` element, and the snippet a host pastes (LOOP D-07 to D-09).
90
- export { AppEmbed } from './embed/AppEmbed';
90
+ export { AppEmbed, AppFloating, embedAssistantCharacter, useEmbedSession, useHostBridge, } from './embed/AppEmbed';
91
+ export { EMBED_SESSION_KEY_PREFIX, embedSessionKey, visitOfToken, } from './embed/embedSession';
92
+ // The host page and an embedded application, talking (LOOP D-10).
93
+ export { HOST_CONTEXT_TOOL, hostFrontendTools, hostNotAllowed, hostRefused, hostTool, hostToolsOf, } from './embed/hostBridge';
91
94
  export { defineDatalayerAppElement, } from './embed/element';
92
- export { EMBED_ATTRIBUTES, EMBED_HOST_VARIABLES, EMBED_TAG, embedLookOf, embedSnippetOf, floatingViewOf, } from './embed/embedConfig';
95
+ export { EMBED_ATTRIBUTES, EMBED_HOST_VARIABLES, EMBED_TAG, embedLookOf, embedSnippetOf, floatingViewOf, resumeOf, } from './embed/embedConfig';
93
96
  export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
94
97
  // The set a workspace normally has, so a host starts from a working Loop
95
98
  // rather than assembling one — and never from the example.
@@ -29,8 +29,11 @@ export type SurfaceSubmitted = {
29
29
  };
30
30
  export type SurfaceToolResultProps = {
31
31
  context: ToolCallRenderContext;
32
- /** Say something as the reader; `false` when the chat cannot take it. */
33
- send: (message: string) => boolean;
32
+ /**
33
+ * Say something as the reader, with what goes with the run besides it;
34
+ * `false` when the chat cannot take it.
35
+ */
36
+ send: (message: string, forwardedProps?: Record<string, unknown>) => boolean;
34
37
  onRendered?: (rendered: SurfaceRendered) => void;
35
38
  onSubmitted?: (submitted: SurfaceSubmitted) => void;
36
39
  };
@@ -4,7 +4,7 @@ import { Spinner, Text } from '@primer/react';
4
4
  import { Box } from '@datalayer/primer-addons';
5
5
  import { encodeFormSubmission } from '../../../chat/messages/formSubmission';
6
6
  import { InlineSurface, SURFACE_CATALOG_ID } from './InlineSurface';
7
- import { readA2uiToolResult, validateA2uiSubmission, } from './toolResult';
7
+ import { answerAction, readA2uiToolResult, validateA2uiSubmission, } from './toolResult';
8
8
  export function SurfaceToolResult({ context, send, onRendered, onSubmitted, }) {
9
9
  const parsed = useMemo(() => context.status === 'complete'
10
10
  ? readA2uiToolResult(context.result, SURFACE_CATALOG_ID)
@@ -27,6 +27,12 @@ export function SurfaceToolResult({ context, send, onRendered, onSubmitted, }) {
27
27
  }, [parsed, context.toolCallId]);
28
28
  const handleAction = useCallback((action) => {
29
29
  if (action.name !== 'submit_a2ui_form') {
30
+ // A button of what an application's answer shows: its action goes to
31
+ // the application, as a button of its page does (LOOP P-04).
32
+ const pressed = answerAction(action);
33
+ if (pressed && !send(pressed.message, pressed.forwardedProps)) {
34
+ setValidationError('The chat is not ready to take it yet.');
35
+ }
30
36
  return;
31
37
  }
32
38
  const values = (action.context ?? {});
@@ -30,5 +30,5 @@ export declare function createA2uiSurfacePlugin(options?: A2uiSurfacePluginOptio
30
30
  /** The instance the preset mounts: no host callbacks. */
31
31
  export declare const A2uiSurfacePlugin: ReactorPlugin<Record<string, never>, unknown, unknown>;
32
32
  export type { SurfaceRendered, SurfaceSubmitted } from './SurfaceToolResult';
33
- export { readA2uiToolResult, validateA2uiField, validateA2uiSubmission, type A2uiFieldRule, type A2uiToolResult, } from './toolResult';
33
+ export { answerAction, readA2uiToolResult, validateA2uiField, validateA2uiSubmission, type A2uiFieldRule, type A2uiToolResult, } from './toolResult';
34
34
  export default A2uiSurfacePlugin;
@@ -38,12 +38,12 @@ let counter = 0;
38
38
  export function createA2uiSurfacePlugin(options = {}) {
39
39
  const id = `a2ui-surface-${(counter += 1)}`;
40
40
  let currentSend;
41
- const send = (message) => {
41
+ const send = (message, forwardedProps) => {
42
42
  if (!currentSend) {
43
43
  return false;
44
44
  }
45
- currentSend(message);
46
- return true;
45
+ // The chat answers why it could not send, when it could not.
46
+ return !currentSend(message, forwardedProps);
47
47
  };
48
48
  function Capture({ workspace, }) {
49
49
  const workspaceSend = workspace?.viewControls.send;
@@ -87,5 +87,5 @@ export function createA2uiSurfacePlugin(options = {}) {
87
87
  }
88
88
  /** The instance the preset mounts: no host callbacks. */
89
89
  export const A2uiSurfacePlugin = createA2uiSurfacePlugin();
90
- export { readA2uiToolResult, validateA2uiField, validateA2uiSubmission, } from './toolResult';
90
+ export { answerAction, readA2uiToolResult, validateA2uiField, validateA2uiSubmission, } from './toolResult';
91
91
  export default A2uiSurfacePlugin;