@datalayer/agent-runtimes 1.3.81 → 1.3.83

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 (72) hide show
  1. package/lib/chat/assistant/SpeechBalloon.js +16 -1
  2. package/lib/chat/base/ChatBase.js +40 -4
  3. package/lib/chat/indicators/McpStatusIndicator.d.ts +1 -1
  4. package/lib/chat/indicators/McpStatusIndicator.js +13 -6
  5. package/lib/chat/indicators/SkillsStatusIndicator.d.ts +1 -1
  6. package/lib/chat/indicators/SkillsStatusIndicator.js +7 -3
  7. package/lib/chat/prompt/InputPrompt.d.ts +4 -1
  8. package/lib/chat/prompt/InputPrompt.js +2 -2
  9. package/lib/chat/prompt/InputPromptBase.d.ts +4 -1
  10. package/lib/chat/prompt/InputPromptBase.js +2 -2
  11. package/lib/chat/prompt/InputPromptLexical.d.ts +7 -1
  12. package/lib/chat/prompt/InputPromptLexical.js +5 -3
  13. package/lib/components/a2ui/datalayer/Form.js +19 -2
  14. package/lib/components/teams/index.d.ts +1 -0
  15. package/lib/components/teams/index.js +1 -0
  16. package/lib/components/teams/usePeerSandbox.d.ts +13 -0
  17. package/lib/components/teams/usePeerSandbox.js +59 -0
  18. package/lib/examples/AgentA2ATeamExample.js +7 -2
  19. package/lib/loop/apps/AppComposer.d.ts +31 -0
  20. package/lib/loop/apps/AppComposer.js +67 -0
  21. package/lib/loop/apps/AppRenderer.d.ts +16 -5
  22. package/lib/loop/apps/AppRenderer.js +49 -16
  23. package/lib/loop/apps/appspec.d.ts +8 -1
  24. package/lib/loop/apps/appspec.js +88 -23
  25. package/lib/loop/apps/checks.js +125 -8
  26. package/lib/loop/apps/composer.d.ts +37 -0
  27. package/lib/loop/apps/composer.js +61 -0
  28. package/lib/loop/apps/index.d.ts +2 -0
  29. package/lib/loop/apps/index.js +2 -0
  30. package/lib/loop/core/index.d.ts +31 -0
  31. package/lib/loop/core/index.js +22 -0
  32. package/lib/loop/embed/AppEmbed.d.ts +46 -7
  33. package/lib/loop/embed/AppEmbed.js +108 -26
  34. package/lib/loop/embed/LoopEmbed.js +2 -0
  35. package/lib/loop/embed/element.js +19 -5
  36. package/lib/loop/embed/embedConfig.d.ts +10 -1
  37. package/lib/loop/embed/embedConfig.js +12 -1
  38. package/lib/loop/embed/embedSession.d.ts +79 -0
  39. package/lib/loop/embed/embedSession.js +136 -0
  40. package/lib/loop/embed/embedTheme.d.ts +12 -9
  41. package/lib/loop/embed/embedTheme.js +32 -22
  42. package/lib/loop/index.d.ts +4 -3
  43. package/lib/loop/index.js +4 -3
  44. package/lib/loop/plugins/agent-capacity/index.d.ts +3 -1
  45. package/lib/loop/plugins/agent-capacity/index.js +2 -1
  46. package/lib/loop/plugins/app-page/appPageModel.d.ts +14 -5
  47. package/lib/loop/plugins/app-page/appPageModel.js +80 -88
  48. package/lib/loop/plugins/chat/ChatView.js +31 -4
  49. package/lib/loop/plugins/chat/index.d.ts +6 -1
  50. package/lib/loop/presets.d.ts +3 -1
  51. package/lib/loop/presets.js +2 -1
  52. package/lib/loop/shell/LoopWorkspace.js +3 -0
  53. package/lib/protocols/BrowserAgentAdapter.d.ts +18 -0
  54. package/lib/protocols/BrowserAgentAdapter.js +35 -2
  55. package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +8 -0
  56. package/lib/runtimes/browser/a2aPeer.d.ts +11 -0
  57. package/lib/runtimes/browser/a2aPeer.js +15 -1
  58. package/lib/runtimes/browser/protocol.d.ts +3 -0
  59. package/lib/runtimes/browser/protocol.js +2 -1
  60. package/lib/specs/agents/agents.js +6 -3
  61. package/lib/specs/apps.js +216 -176
  62. package/lib/specs/appspecSchema.js +205 -84
  63. package/lib/specs/cogs.js +3 -2
  64. package/lib/types/agentspecs.d.ts +92 -7
  65. package/lib/types/chat.d.ts +29 -1
  66. package/package.json +1 -1
  67. package/scripts/codegen/generate_agents.py +4 -2
  68. package/scripts/codegen/generate_apps.py +0 -1
  69. package/scripts/codegen/generate_backend_tools.py +4 -12
  70. package/scripts/codegen/generate_cogs.py +3 -1
  71. package/scripts/codegen/generate_ui_plugins.py +22 -9
  72. package/scripts/voice/transcribe.mjs +29 -21
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /*
3
3
  * Copyright (c) 2025-2026 Datalayer, Inc.
4
4
  * Distributed under the terms of the Modified BSD License.
@@ -33,13 +33,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
33
33
  * host may override with the face and the mode (D-11) — around the
34
34
  * conversation and inside it, where the chat sets its theme again.
35
35
  *
36
+ * On the host's server with the visit's embed token, its session is kept
37
+ * in the host's storage and picked up again after the page reloads (D-13,
38
+ * `useEmbedSession`): its conversation drawn again, and gone on with.
39
+ *
36
40
  * @module loop/embed/AppEmbed
37
41
  */
38
42
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
39
43
  import { Text, registerPortalRoot } from '@primer/react';
40
44
  import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
41
45
  import { FluentEmoji } from '@datalayer/core/lib/components/emoji';
42
- import { DatalayerThemeProvider, loopTheme } from '@datalayer/primer-addons';
46
+ import { DatalayerThemeProvider, getThemeConfig, } from '@datalayer/primer-addons';
47
+ import { generateMessageId } from '../../types/messages';
43
48
  import { ChatFloating } from '../../chat/ChatFloating';
44
49
  import { AppRenderer, } from '../apps/AppRenderer';
45
50
  import { AssistantCharactersPlugin, assistantCharacterFor, assistantCharactersFrom, } from '../plugins/assistant-characters';
@@ -47,6 +52,78 @@ import { floatingViewOf } from './embedConfig';
47
52
  import { createChatExtrasPlugin } from '../plugins/chat-extras';
48
53
  import { hostFrontendTools } from './hostBridge';
49
54
  import { embedThemeOverrides, embedThemeStyles } from './embedTheme';
55
+ import { embedSessionKey, reattachSession, sessionGoneSentence, sessionKeeper, visitOfToken, } from './embedSession';
56
+ /**
57
+ * The visit's session, kept and picked up again (LOOP D-13): on the host's
58
+ * server with an embed token that names its visit, the session the chat
59
+ * opens is kept under the application and the visit — in the host's
60
+ * storage unless `resume` is off, in memory always — and, when the
61
+ * application is drawn again (the page reloaded, or a token renewed for the
62
+ * visit), its conversation is asked for and drawn again. One that is gone
63
+ * or refused is forgotten, said once, and a new one starts. Elsewhere —
64
+ * on a Datalayer runtime, without a token — nothing is kept.
65
+ */
66
+ export function useEmbedSession({ app, serverUrl, embedToken, resume = true, }) {
67
+ const visit = serverUrl && embedToken ? visitOfToken(embedToken) : undefined;
68
+ const key = visit ? embedSessionKey(app.id, visit) : undefined;
69
+ const keeper = useMemo(() => sessionKeeper({ persist: resume }), [resume]);
70
+ // What was found, for the token it was found with: a renewed token asks again.
71
+ const [found, setFound] = useState();
72
+ useEffect(() => {
73
+ if (!key || !serverUrl || !embedToken) {
74
+ return undefined;
75
+ }
76
+ let cancelled = false;
77
+ const onStarted = (uid) => {
78
+ keeper.keep(key, uid);
79
+ setFound(before => before?.said ? { ...before, said: undefined } : before);
80
+ };
81
+ const fresh = (said) => setFound({
82
+ token: embedToken,
83
+ thread: { id: generateMessageId(), messages: [], onStarted },
84
+ ...(said ? { said } : {}),
85
+ });
86
+ const kept = keeper.read(key);
87
+ if (!kept) {
88
+ fresh();
89
+ return undefined;
90
+ }
91
+ void reattachSession({ serverUrl, uid: kept, token: embedToken }).then(reattached => {
92
+ if (cancelled) {
93
+ return;
94
+ }
95
+ if (reattached.kind === 'resumed') {
96
+ setFound({
97
+ token: embedToken,
98
+ thread: { id: kept, messages: reattached.messages, onStarted },
99
+ });
100
+ return;
101
+ }
102
+ keeper.forget(key);
103
+ fresh(sessionGoneSentence(app.name));
104
+ });
105
+ return () => {
106
+ cancelled = true;
107
+ };
108
+ // The application's name is only said.
109
+ // eslint-disable-next-line react-hooks/exhaustive-deps
110
+ }, [key, serverUrl, embedToken, keeper]);
111
+ if (!key) {
112
+ return { ready: true };
113
+ }
114
+ if (!found || found.token !== embedToken) {
115
+ return { ready: false };
116
+ }
117
+ return {
118
+ ready: true,
119
+ thread: found.thread,
120
+ ...(found.said ? { said: found.said } : {}),
121
+ };
122
+ }
123
+ /** What the embed says once of its session, above the conversation. */
124
+ function SessionSaid({ said }) {
125
+ return said ? (_jsx(Text, { as: "p", role: "status", sx: { px: 3, py: 2, m: 0, fontSize: 1 }, children: said })) : null;
126
+ }
50
127
  /** No plugins for the renderer: one array, so that it never reads as a change. */
51
128
  const NO_HOST_PLUGINS = [];
52
129
  /**
@@ -122,7 +199,7 @@ function Face({ emoji }) {
122
199
  * The workspace tells the chrome what the application is doing and what it
123
200
  * last said (`onPresence`, `onSaying`).
124
201
  */
125
- export function AppFloating({ app, view, colorMode, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, }) {
202
+ export function AppFloating({ app, view, colorMode, themeVariant, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, session, }) {
126
203
  const [presence, setPresence] = useState('idle');
127
204
  const [said, setSaid] = useState({ answering: false });
128
205
  const bridge = useHostBridge(app, host);
@@ -143,14 +220,14 @@ export function AppFloating({ app, view, colorMode, themeOverrides, character, s
143
220
  return (_jsx(ChatFloating, { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}), ...(app.interface.balloon
144
221
  ? { balloonDisplay: app.interface.balloon }
145
222
  : {}), title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, colorMode: colorMode, useStore: false, conversation: {
146
- body: signedOut ? (_jsx(Text, { as: "p", role: "status", sx: { p: 3, m: 0, fontSize: 1 }, children: signedOutSentence(app) })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
147
- // The host's accent and face over the application's own, inside
148
- // its conversation too, in the embed's mode.
149
- themeOverrides: themeOverrides, colorMode: colorMode,
150
- // The window draws its face, its name and close.
151
- hideChatHeader: true,
152
- // Mounted closed: the caret goes in when the window opens.
153
- autoFocusPrompt: false, onPresence: onPresence, onSaying: onSaying, plugins: bridge.plugins, ...renderer })),
223
+ body: signedOut ? (_jsx(Text, { as: "p", role: "status", sx: { p: 3, m: 0, fontSize: 1 }, children: signedOutSentence(app) })) : session && !session.ready ? null : (_jsxs(_Fragment, { children: [_jsx(SessionSaid, { said: session?.said }), _jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
224
+ // The host's accent and face over the application's own, inside
225
+ // its conversation too, in the embed's mode.
226
+ themeOverrides: themeOverrides, colorMode: colorMode, ...(themeVariant ? { themeVariant } : {}),
227
+ // The window draws its face, its name and close.
228
+ hideChatHeader: true,
229
+ // Mounted closed: the caret goes in when the window opens.
230
+ autoFocusPrompt: false, onPresence: onPresence, onSaying: onSaying, plugins: bridge.plugins, ...(session?.thread ? { thread: session.thread } : {}), ...renderer })] })),
154
231
  presence,
155
232
  saying: said.saying,
156
233
  answering: said.answering,
@@ -187,9 +264,9 @@ function useSystemMode() {
187
264
  }, [query]);
188
265
  return dark ? 'dark' : 'light';
189
266
  }
190
- /** The `loop` theme, with the application's (or the host's) accent and face. */
191
- export function EmbedThemed({ accent, colorMode, font, children, style, }) {
192
- const themeStyles = useMemo(() => embedThemeStyles({ accent, font }), [accent, font]);
267
+ /** The application's theme (`loop` unless it names one), with its (or the host's) accent and face. */
268
+ export function EmbedThemed({ accent, colorMode, font, variant, children, style, }) {
269
+ const themeStyles = useMemo(() => embedThemeStyles({ accent, font, variant }), [accent, font, variant]);
193
270
  return (
194
271
  /*
195
272
  * Marked as themed above the provider, so that the provider counts
@@ -197,16 +274,21 @@ export function EmbedThemed({ accent, colorMode, font, children, style, }) {
197
274
  * alone: it would otherwise write the theme's tokens, colour and face on
198
275
  * the host's page (T-13). A wrapper of no box.
199
276
  */
200
- _jsx("div", { "data-color-mode": colorMode, style: { display: 'contents' }, children: _jsx(DatalayerThemeProvider, { colorMode: colorMode, theme: loopTheme, themeStyles: themeStyles, baseStyles: style, children: children }) }));
277
+ _jsx("div", { "data-color-mode": colorMode, style: { display: 'contents' }, children: _jsx(DatalayerThemeProvider, { colorMode: colorMode, theme: getThemeConfig(variant).primerTheme, themeStyles: themeStyles, baseStyles: style, children: children }) }));
201
278
  }
202
- export function AppEmbed({ app, mode, accent, colorMode = 'auto', font, serverUrl, instance, embedToken, height = 640, ownPortal = false, plugins = NO_PLUGINS, host, }) {
279
+ export function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, embedToken, height = 640, ownPortal = false, plugins = NO_PLUGINS, host, resume = true, }) {
203
280
  const bridge = useHostBridge(app, host);
281
+ const session = useEmbedSession({ app, serverUrl, embedToken, resume });
204
282
  const system = useSystemMode();
205
- const resolvedMode = colorMode === 'auto' ? system : colorMode;
283
+ // The theme the application names (T-30), `loop` when none; its mode
284
+ // the host's, else the application's, else the visitor's system's.
285
+ const variant = app.interface.theme?.variant ?? 'loop';
286
+ const asked = colorMode ?? app.interface.theme?.mode ?? 'auto';
287
+ const resolvedMode = asked === 'auto' ? system : asked;
206
288
  const shownAs = mode ?? app.deployment.embedded?.mode ?? 'inline';
207
289
  const view = floatingViewOf(shownAs);
208
290
  const worn = accent ?? app.interface.accent;
209
- const themeOverrides = useMemo(() => embedThemeOverrides({ accent: worn, font }), [worn, font]);
291
+ const themeOverrides = useMemo(() => embedThemeOverrides({ accent: worn, font, variant }), [worn, font, variant]);
210
292
  const named = app.interface.assistant;
211
293
  const assistant = useMemo(() => view === 'assistant' ? embedAssistantCharacter(app, plugins) : undefined,
212
294
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -215,15 +297,15 @@ export function AppEmbed({ app, mode, accent, colorMode = 'auto', font, serverUr
215
297
  return (_jsx("p", { role: "status", className: "datalayer-app-said", children: assistant.problem }));
216
298
  }
217
299
  const character = assistant?.character;
218
- return (_jsxs(EmbedThemed, { accent: worn, colorMode: resolvedMode, font: font, style: view
300
+ return (_jsxs(EmbedThemed, { accent: worn, colorMode: resolvedMode, font: font, variant: variant, style: view
219
301
  ? // Floating: nothing in the page's flow, nothing painted behind.
220
302
  { backgroundColor: 'transparent' }
221
- : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view ? (_jsx(AppFloating, { app: app, view: view, serverUrl: serverUrl, instance: instance, embedToken: embedToken, colorMode: resolvedMode, themeOverrides: themeOverrides, character: character, host: host })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
222
- // The host's accent and face over the application's own, inside
223
- // its conversation too, in the embed's mode: the host's or its
224
- // visitor's system's, not a Datalayer setting.
225
- themeOverrides: themeOverrides, colorMode: resolvedMode,
226
- // What the page gives it, and what it tells the page (D-10).
227
- plugins: bridge.plugins, onPresence: bridge.onPresence, onSaying: bridge.onSaying }))] }));
303
+ : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view ? (_jsx(AppFloating, { app: app, view: view, serverUrl: serverUrl, instance: instance, embedToken: embedToken, colorMode: resolvedMode, themeVariant: variant, themeOverrides: themeOverrides, character: character, host: host, session: session })) : !session.ready ? null : (_jsxs(_Fragment, { children: [_jsx(SessionSaid, { said: session.said }), _jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
304
+ // The host's accent and face over the application's own, inside
305
+ // its conversation too, in the embed's mode: the host's or its
306
+ // visitor's system's, not a Datalayer setting.
307
+ themeOverrides: themeOverrides, colorMode: resolvedMode, themeVariant: variant,
308
+ // What the page gives it, and what it tells the page (D-10).
309
+ plugins: bridge.plugins, onPresence: bridge.onPresence, onSaying: bridge.onSaying, ...(session.thread ? { thread: session.thread } : {}) })] }))] }));
228
310
  }
229
311
  export default AppEmbed;
@@ -74,6 +74,8 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
74
74
  preset.presence?.welcome,
75
75
  preset.presence?.paused,
76
76
  preset.showTokenUsage,
77
+ // The thread a reloaded embed goes on with, by its id (D-13).
78
+ preset.thread?.id,
77
79
  // By its content: an application's spec written inline is a new object each render.
78
80
  JSON.stringify(preset.datalayerCreatePayload ?? null),
79
81
  JSON.stringify(preset.datalayerVisitors ?? null),
@@ -38,16 +38,23 @@ import { jsx as _jsx } from "react/jsx-runtime";
38
38
  * when it called a function of the page, beside `decision` and
39
39
  * `token-expired`.
40
40
  *
41
+ * On the host's server (`server`) with the visit's embed token, the
42
+ * session the application opens is kept in the page's `localStorage`, under
43
+ * the application and the visit its token names, and picked up again after
44
+ * the page reloads — its conversation drawn again (LOOP D-13); a token
45
+ * renewed for the visit redraws the application on the same session.
46
+ * `resume="false"` keeps nothing in the page's storage.
47
+ *
41
48
  * @module loop/embed/element
42
49
  */
43
50
  import { createRoot } from 'react-dom/client';
44
51
  import { StyleSheetManager } from 'styled-components';
45
- import { loopThemeStyles } from '@datalayer/primer-addons';
52
+ import { getThemeConfig } from '@datalayer/primer-addons';
46
53
  import { coreStore } from '@datalayer/core/lib/state/substates/CoreState';
47
54
  import { parseAppspec } from '../apps/appspec';
48
55
  import { readAppspecYaml } from '../apps/yaml';
49
56
  import { AppEmbed, embedAssistantCharacter } from './AppEmbed';
50
- import { EMBED_HOST_VARIABLES, EMBED_OBSERVED_ATTRIBUTES, EMBED_TAG, EmbedAttributeError, embedLookOf, inlineHeightOf, } from './embedConfig';
57
+ import { EMBED_HOST_VARIABLES, EMBED_OBSERVED_ATTRIBUTES, EMBED_TAG, EmbedAttributeError, embedLookOf, inlineHeightOf, resumeOf, } from './embedConfig';
51
58
  import { embedShadowCss } from './embedTheme';
52
59
  /** What the framed run page and the element say to each other. */
53
60
  export const EMBED_MESSAGES = {
@@ -249,7 +256,7 @@ export function defineDatalayerAppElement(options = {}) {
249
256
  return;
250
257
  }
251
258
  // How it looks is drawn again; what it is, read again.
252
- if (['mode', 'accent', 'theme', 'font', 'height'].includes(name)) {
259
+ if (['mode', 'accent', 'theme', 'font', 'height', 'resume'].includes(name)) {
253
260
  this.draw();
254
261
  return;
255
262
  }
@@ -394,8 +401,10 @@ export function defineDatalayerAppElement(options = {}) {
394
401
  return;
395
402
  }
396
403
  let look;
404
+ let resume;
397
405
  try {
398
406
  look = embedLookOf(this.inputs(), source.app);
407
+ resume = resumeOf(this.getAttribute('resume'));
399
408
  }
400
409
  catch (error) {
401
410
  this.say(error.message);
@@ -411,13 +420,18 @@ export function defineDatalayerAppElement(options = {}) {
411
420
  }
412
421
  }
413
422
  this.setAttribute('data-embed-mode', look.mode);
414
- const shadow = this.skeleton(loopThemeStyles.css);
423
+ // The theme's own stylesheet: `loop`'s, or the one the application
424
+ // names (T-30).
425
+ const shadow = this.skeleton(getThemeConfig(look.variant).themeStyles.css);
415
426
  const mount = document.createElement('div');
416
427
  mount.className = 'datalayer-app-root';
417
428
  shadow.appendChild(mount);
418
429
  this.root = createRoot(mount);
419
430
  const server = this.getAttribute('server') || undefined;
420
- this.root.render(_jsx(StyleSheetManager, { target: shadow, children: _jsx(AppEmbed, { app: source.app, mode: look.mode, accent: look.accent, colorMode: look.colorMode, font: look.font, serverUrl: server, ...(server && this.token ? { embedToken: this.token } : {}), height: inlineHeightOf(this.getAttribute('height')), ownPortal: true, host: this.host }) }));
431
+ this.root.render(_jsx(StyleSheetManager, { target: shadow, children: _jsx(AppEmbed, { app: source.app, mode: look.mode, accent: look.accent, colorMode: look.colorMode, font: look.font, serverUrl: server, ...(server && this.token ? { embedToken: this.token } : {}), height: inlineHeightOf(this.getAttribute('height')), ownPortal: true, host: this.host,
432
+ // The visit's session kept in the page's storage, and picked
433
+ // up again after a reload, unless the host says not (D-13).
434
+ resume: resume }) }));
421
435
  }
422
436
  onMessage(event) {
423
437
  if (!this.frame || event.source !== this.frame.contentWindow) {
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @module loop/embed/embedConfig
13
13
  */
14
- import type { AppAccent, AppEmbedMode, AppSpec } from '../../types/agentspecs';
14
+ import type { AppAccent, AppEmbedMode, AppSpec, AppThemeVariant } from '../../types/agentspecs';
15
15
  /** The element's name: the one a host writes. */
16
16
  export declare const EMBED_TAG = "datalayer-app";
17
17
  /** Where the script is served on a Datalayer origin; the stylesheet beside it. */
@@ -33,6 +33,7 @@ export declare const EMBED_ATTRIBUTES: {
33
33
  readonly server: "An agent-runtimes server the application’s agent runs on; a Datalayer runtime by default.";
34
34
  readonly api: "Datalayer’s API, where an application is read with its token; the platform’s by default.";
35
35
  readonly height: "The height of an inline application, in pixels; 640 by default.";
36
+ readonly resume: "true or false: whether a visit’s conversation is picked up again after the page reloads, kept in the page’s storage; true by default.";
36
37
  };
37
38
  export type EmbedAttribute = keyof typeof EMBED_ATTRIBUTES;
38
39
  /** Every attribute the element watches. */
@@ -86,6 +87,8 @@ export type EmbedLook = {
86
87
  colorMode: EmbedColorMode;
87
88
  /** The host's face; empty for the theme's own. */
88
89
  font: string;
90
+ /** The theme the application names (T-30); `loop` when it names none. */
91
+ variant: AppThemeVariant;
89
92
  };
90
93
  /**
91
94
  * What the element shows and how, from what the host wrote and what the
@@ -94,6 +97,12 @@ export type EmbedLook = {
94
97
  * sentence (`EmbedAttributeError`), never quietly replaced.
95
98
  */
96
99
  export declare function embedLookOf(inputs: EmbedInputs, app?: Pick<AppSpec, 'interface' | 'deployment'>): EmbedLook;
100
+ /**
101
+ * Whether a visit's conversation is picked up again after a reload (LOOP
102
+ * D-13): on unless the host writes `resume="false"`. Anything else is
103
+ * refused in a sentence.
104
+ */
105
+ export declare function resumeOf(value: string | null | undefined): boolean;
97
106
  /** The height of an inline application, in pixels. */
98
107
  export declare function inlineHeightOf(value: string | null | undefined): number;
99
108
  /** What a snippet says beside the application's id. */
@@ -24,6 +24,7 @@ export const EMBED_ATTRIBUTES = {
24
24
  server: 'An agent-runtimes server the application’s agent runs on; a Datalayer runtime by default.',
25
25
  api: 'Datalayer’s API, where an application is read with its token; the platform’s by default.',
26
26
  height: 'The height of an inline application, in pixels; 640 by default.',
27
+ resume: 'true or false: whether a visit’s conversation is picked up again after the page reloads, kept in the page’s storage; true by default.',
27
28
  };
28
29
  /** Every attribute the element watches. */
29
30
  export const EMBED_OBSERVED_ATTRIBUTES = Object.keys(EMBED_ATTRIBUTES);
@@ -105,9 +106,19 @@ export function embedLookOf(inputs, app) {
105
106
  'green';
106
107
  const colorMode = oneOf(attributes.theme, EMBED_COLOR_MODES, 'a theme') ??
107
108
  oneOf(variables.theme, EMBED_COLOR_MODES, 'a theme') ??
109
+ app?.interface?.theme?.mode ??
108
110
  'auto';
109
111
  const font = checkedFont(attributes.font) || checkedFont(variables.font);
110
- return { mode, accent, colorMode, font };
112
+ const variant = app?.interface?.theme?.variant ?? 'loop';
113
+ return { mode, accent, colorMode, font, variant };
114
+ }
115
+ /**
116
+ * Whether a visit's conversation is picked up again after a reload (LOOP
117
+ * D-13): on unless the host writes `resume="false"`. Anything else is
118
+ * refused in a sentence.
119
+ */
120
+ export function resumeOf(value) {
121
+ return (oneOf(value, ['true', 'false'], 'a value of resume') !== 'false');
111
122
  }
112
123
  /** The height of an inline application, in pixels. */
113
124
  export function inlineHeightOf(value) {
@@ -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.