@datalayer/agent-runtimes 1.3.81 → 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 (66) 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/teams/index.d.ts +1 -0
  14. package/lib/components/teams/index.js +1 -0
  15. package/lib/components/teams/usePeerSandbox.d.ts +13 -0
  16. package/lib/components/teams/usePeerSandbox.js +59 -0
  17. package/lib/examples/AgentA2ATeamExample.js +7 -2
  18. package/lib/loop/apps/AppComposer.d.ts +31 -0
  19. package/lib/loop/apps/AppComposer.js +67 -0
  20. package/lib/loop/apps/AppRenderer.d.ts +16 -5
  21. package/lib/loop/apps/AppRenderer.js +49 -16
  22. package/lib/loop/apps/appspec.d.ts +8 -1
  23. package/lib/loop/apps/appspec.js +87 -0
  24. package/lib/loop/apps/checks.js +108 -1
  25. package/lib/loop/apps/composer.d.ts +37 -0
  26. package/lib/loop/apps/composer.js +61 -0
  27. package/lib/loop/apps/index.d.ts +2 -0
  28. package/lib/loop/apps/index.js +2 -0
  29. package/lib/loop/core/index.d.ts +31 -0
  30. package/lib/loop/core/index.js +22 -0
  31. package/lib/loop/embed/AppEmbed.d.ts +46 -7
  32. package/lib/loop/embed/AppEmbed.js +108 -26
  33. package/lib/loop/embed/LoopEmbed.js +2 -0
  34. package/lib/loop/embed/element.js +19 -5
  35. package/lib/loop/embed/embedConfig.d.ts +10 -1
  36. package/lib/loop/embed/embedConfig.js +12 -1
  37. package/lib/loop/embed/embedSession.d.ts +79 -0
  38. package/lib/loop/embed/embedSession.js +136 -0
  39. package/lib/loop/embed/embedTheme.d.ts +12 -9
  40. package/lib/loop/embed/embedTheme.js +32 -22
  41. package/lib/loop/index.d.ts +4 -3
  42. package/lib/loop/index.js +4 -3
  43. package/lib/loop/plugins/agent-capacity/index.d.ts +3 -1
  44. package/lib/loop/plugins/agent-capacity/index.js +2 -1
  45. package/lib/loop/plugins/chat/ChatView.js +31 -4
  46. package/lib/loop/plugins/chat/index.d.ts +6 -1
  47. package/lib/loop/presets.d.ts +3 -1
  48. package/lib/loop/presets.js +2 -1
  49. package/lib/loop/shell/LoopWorkspace.js +3 -0
  50. package/lib/protocols/BrowserAgentAdapter.d.ts +18 -0
  51. package/lib/protocols/BrowserAgentAdapter.js +35 -2
  52. package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +8 -0
  53. package/lib/runtimes/browser/a2aPeer.d.ts +11 -0
  54. package/lib/runtimes/browser/a2aPeer.js +15 -1
  55. package/lib/runtimes/browser/protocol.d.ts +3 -0
  56. package/lib/runtimes/browser/protocol.js +2 -1
  57. package/lib/specs/apps.js +28 -0
  58. package/lib/specs/appspecSchema.js +191 -0
  59. package/lib/types/agentspecs.d.ts +58 -0
  60. package/lib/types/chat.d.ts +29 -1
  61. package/package.json +1 -1
  62. package/scripts/codegen/generate_agents.py +4 -2
  63. package/scripts/codegen/generate_backend_tools.py +4 -12
  64. package/scripts/codegen/generate_cogs.py +3 -1
  65. package/scripts/codegen/generate_ui_plugins.py +22 -9
  66. package/scripts/voice/transcribe.mjs +29 -21
@@ -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
  }
@@ -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,10 +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, AppFloating, embedAssistantCharacter, type AppEmbedProps, type AppFloatingProps, useHostBridge, } 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';
71
72
  export { HOST_CONTEXT_TOOL, hostFrontendTools, hostNotAllowed, hostRefused, hostTool, hostToolsOf, type AppEmbedHost, type HostEvent, type HostFunction, type HostQuestion, } from './embed/hostBridge';
72
73
  export { defineDatalayerAppElement, type DatalayerAppElementOptions, } from './embed/element';
73
- 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';
74
75
  export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
75
76
  export { loopPlugins, type LoopPresetOptions } from './presets';
76
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,11 +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, AppFloating, embedAssistantCharacter, useHostBridge, } from './embed/AppEmbed';
90
+ export { AppEmbed, AppFloating, embedAssistantCharacter, useEmbedSession, useHostBridge, } from './embed/AppEmbed';
91
+ export { EMBED_SESSION_KEY_PREFIX, embedSessionKey, visitOfToken, } from './embed/embedSession';
91
92
  // The host page and an embedded application, talking (LOOP D-10).
92
93
  export { HOST_CONTEXT_TOOL, hostFrontendTools, hostNotAllowed, hostRefused, hostTool, hostToolsOf, } from './embed/hostBridge';
93
94
  export { defineDatalayerAppElement, } from './embed/element';
94
- 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';
95
96
  export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
96
97
  // The set a workspace normally has, so a host starts from a working Loop
97
98
  // rather than assembling one — and never from the example.
@@ -17,7 +17,7 @@
17
17
  * @module loop/plugins/agent-capacity
18
18
  */
19
19
  import { type ReactorPlugin } from '@datalayer/reactor';
20
- import { type ChatSuggestionItem } from '../../core';
20
+ import { type AgentBlueprintContribution, type ChatSuggestionItem } from '../../core';
21
21
  export type AgentCapacityOptions = {
22
22
  /** Short slug: `hooks`, `codemode`, … names the plugin `…-agent-<key>`. */
23
23
  key: string;
@@ -38,6 +38,8 @@ export type AgentCapacityOptions = {
38
38
  instructions?: string;
39
39
  /** The model it runs on in the page, in place of its spec's. */
40
40
  model?: string;
41
+ /** What its modes tell a run turned in the page (LOOP P-19). */
42
+ modeEffect?: AgentBlueprintContribution['modeEffect'];
41
43
  };
42
44
  export declare function defineAgentCapacityPlugin(options: AgentCapacityOptions): ReactorPlugin<Record<string, never>, unknown, unknown>;
43
45
  export default defineAgentCapacityPlugin;
@@ -23,7 +23,7 @@
23
23
  import { contribution, definePlugin, } from '@datalayer/reactor';
24
24
  import { LoopAgentBlueprint, LoopChatSuggestion, } from '../../core';
25
25
  export function defineAgentCapacityPlugin(options) {
26
- const { key, displayName, description, specId, octicon, emoji, codemode, createPayload, suggestions, instructions, model, } = options;
26
+ const { key, displayName, description, specId, octicon, emoji, codemode, createPayload, suggestions, instructions, model, modeEffect, } = options;
27
27
  return definePlugin({
28
28
  name: `@datalayer/loop-plugin-agent-${key}`,
29
29
  displayName,
@@ -42,6 +42,7 @@ export function defineAgentCapacityPlugin(options) {
42
42
  },
43
43
  ...(instructions ? { instructions } : {}),
44
44
  ...(model ? { model } : {}),
45
+ ...(modeEffect ? { modeEffect } : {}),
45
46
  }, { id: key }),
46
47
  ...(suggestions && suggestions.length > 0
47
48
  ? [
@@ -41,7 +41,7 @@ import { AGENTS_PLUGIN_NAME } from '../agents/plugin';
41
41
  import { useConfig } from '../../../hooks/useConfig';
42
42
  import { useSkills, useSkillActions } from '../../../hooks/useSkills';
43
43
  import { AI_MODEL_CATALOGUE, isChatModel } from '../../../specs/models';
44
- import { LoopAgentBlueprint, LoopAgentGate, LoopChatComposer, LoopChatExtras, LoopChatHeader, LoopChatLayout, LoopChatSuggestion, LoopChatSurface, LoopChatTurn, LoopFrontendTool, LoopSlots, canOpenView, onPromptFocusRequest, onSurfaceRequest, useLoopPromptStore, } from '../../core';
44
+ import { LoopAgentBlueprint, LoopAgentGate, LoopChatComposer, LoopChatExtras, LoopChatHeader, LoopChatLayout, LoopChatSuggestion, LoopChatSurface, LoopChatTurn, LoopCommand, LoopFrontendTool, LoopRunProps, LoopSlots, canOpenView, runForwardedProps, onPromptFocusRequest, onSurfaceRequest, useLoopPromptStore, } from '../../core';
45
45
  import { messageText, turnWritersOf } from './turnState';
46
46
  import { isInactiveSurfaceContribution, orderToolContributions, toolsForChatView, } from './chatViewTools';
47
47
  import { useAgentCommandTools } from '../../../tools/adapters/commands';
@@ -353,6 +353,10 @@ export default function ChatView({ workspace }) {
353
353
  const heldForAdapter = useRef([]);
354
354
  /** Whether the agent is turned in this page: read by `sendNow`, set below. */
355
355
  const inPageRef = useRef(false);
356
+ /** What goes with every run (an application's modes, LOOP P-19), read when it is sent. */
357
+ const runProps = useContributions(LoopRunProps);
358
+ const runPropsRef = useRef(runProps);
359
+ runPropsRef.current = runProps;
356
360
  /*
357
361
  * Every message that goes to the agent goes through here: the composer's
358
362
  * (through the workspace's dispatch and the prompt channel), a host's, and
@@ -362,13 +366,17 @@ export default function ChatView({ workspace }) {
362
366
  * What the page did besides the message goes with it as AG-UI's
363
367
  * `forwardedProps` (`loop`, LOOP R-04): the application's session takes a
364
368
  * file there. An agent turned in this page has no session, and no file is
365
- * handed to it: said, rather than dropped.
369
+ * handed to it: said, rather than dropped. What the workspace's plugins
370
+ * send with every run (`LoopRunProps`: the modes the person is in, LOOP
371
+ * P-19) goes under it — and an agent turned in this page applies the modes
372
+ * itself (`modeEffect`, `BrowserAgentAdapter`).
366
373
  */
367
- const sendNow = useCallback((message, forwardedProps) => {
368
- const loop = forwardedProps?.loop;
374
+ const sendNow = useCallback((message, given) => {
375
+ const loop = given?.loop;
369
376
  if (inPageRef.current && loop?.files && loop.files.length > 0) {
370
377
  return 'A file is given to an application running on a runtime: run it on Datalayer or on your machine to give it one.';
371
378
  }
379
+ const forwardedProps = runForwardedProps(runPropsRef.current.map(entry => entry.value), given);
372
380
  // A new turn: whatever the panel showed is gone, this message is it.
373
381
  turnFeedRef.current?.begin(message, controlsRef.current?.thread());
374
382
  const send = controlsRef.current?.send;
@@ -936,6 +944,18 @@ export default function ChatView({ workspace }) {
936
944
  * silent no-ops. Derived here rather than in the plugin because it changes
937
945
  * with the selected member.
938
946
  */
947
+ /*
948
+ * The commands the composer lists while `/` is typed: those contributed for
949
+ * it, an application's (LOOP P-19). Picking one writes `/<name> `, and the
950
+ * workspace runs it when the message is sent.
951
+ */
952
+ const commands = useContributions(LoopCommand);
953
+ const composerCommands = useMemo(() => commands
954
+ .filter(entry => entry.value.composer)
955
+ .map(entry => ({
956
+ name: entry.value.name,
957
+ description: entry.value.description,
958
+ })), [commands]);
939
959
  const mentionable = useMemo(() => {
940
960
  if (!team || !member) {
941
961
  // No team: the spec's own subagents, which the in-page loop can reach
@@ -1175,6 +1195,10 @@ export default function ChatView({ workspace }) {
1175
1195
  // server's monitoring stream fills, so the side panel and the
1176
1196
  // composer's pulse draw an in-page delegation like any other.
1177
1197
  onSubagentEvent: event => agentRuntimeStore.getState().appendSubagentEvent(event),
1198
+ // An application's modes (LOOP P-19): the options sent with a
1199
+ // run tell it their instructions, on the model one names, as
1200
+ // its session API does on a runtime.
1201
+ modeEffect: blueprintTurn?.modeEffect,
1178
1202
  })
1179
1203
  : {
1180
1204
  type: 'ag-ui',
@@ -1402,6 +1426,7 @@ export default function ChatView({ workspace }) {
1402
1426
  // because focusing an input on mount scrolls the browser to it.
1403
1427
  autoFocus: config?.autoFocusPrompt ?? true,
1404
1428
  mentionableAgents: mentionable,
1429
+ promptCommands: composerCommands,
1405
1430
  headerContent: inpromptMenu.length > 0 ? (_jsx(ReactorSlot, { slot: LoopSlots.inpromptMenu, props: { workspace } })) : undefined,
1406
1431
  footerExtras: promptActions.length > 0 ? (_jsx(ReactorSlot, { slot: LoopSlots.promptAction, props: { workspace } })) : undefined,
1407
1432
  /*
@@ -1531,6 +1556,8 @@ export default function ChatView({ workspace }) {
1531
1556
  , {
1532
1557
  // The theme it wears, when the host names one (an application's).
1533
1558
  themeVariant: config?.themeVariant, themeOverrides: config?.themeOverrides, colorMode: config?.colorMode,
1559
+ // The session an embed goes on with after a reload (LOOP D-13).
1560
+ thread: config?.thread,
1534
1561
  // The header says why, beside the title, for the same reason the
1535
1562
  // placeholder does: a dead control with no explanation is worse
1536
1563
  // than an absent one.
@@ -1,6 +1,6 @@
1
1
  import type { PresenceState } from '../../../chat/presence/presenceStatus';
2
2
  import type { AssistantSaying } from '../../../chat/assistant/state';
3
- import type { ThemeOverrides } from '../../../types/chat';
3
+ import type { ResumedThread, ThemeOverrides } from '../../../types/chat';
4
4
  /** What a host may set on the chat. */
5
5
  /** Who is answering, as the person meets them. */
6
6
  export interface ChatPresence {
@@ -62,6 +62,11 @@ export type ChatPluginConfig = {
62
62
  * application sets its own.
63
63
  */
64
64
  presence?: ChatPresence;
65
+ /**
66
+ * The conversation to go on with (LOOP D-13): an embedded application's
67
+ * session, reattached after its page reloaded, and what it holds.
68
+ */
69
+ thread?: ResumedThread;
65
70
  /**
66
71
  * Whether the counters — tokens, context — are shown under the prompt.
67
72
  * On unless said: a person using an application is not asking.
@@ -23,7 +23,7 @@ import { type PluginRef } from '@datalayer/reactor';
23
23
  import { type AgentsConfig, type SandboxTarget } from './plugins/agents';
24
24
  import { type ChatPluginConfig, type ChatPresence } from './plugins/chat';
25
25
  import { type PageSize } from './plugins/page-layout';
26
- import type { ThemeOverrides } from '../types/chat';
26
+ import type { ResumedThread, ThemeOverrides } from '../types/chat';
27
27
  export type LoopPresetOptions = {
28
28
  /** Where the agent runtimes service is. */
29
29
  serverUrl?: string;
@@ -155,6 +155,8 @@ export type LoopPresetOptions = {
155
155
  presence?: ChatPresence;
156
156
  /** Whether the counters are shown under the prompt; on unless said. */
157
157
  showTokenUsage?: boolean;
158
+ /** The conversation to go on with: an embed's session after a reload (LOOP D-13). */
159
+ thread?: ResumedThread;
158
160
  /**
159
161
  * The agentspec the local agent is created from, by id.
160
162
  *
@@ -52,7 +52,7 @@ import { LoopPageLayoutPlugin } from './plugins/page-layout';
52
52
  * be installed or signed into, and the switches a demonstration wants are off.
53
53
  */
54
54
  export function loopPlugins(options = {}) {
55
- const { serverUrl, target = 'browser', defaultEditor: askedEditor, showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, suggestionLabels = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutArrangement = 'page', pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, datalayerAgentSpecId, datalayerCreatePayload, datalayerVisitors, datalayerKept, embedToken, targetFixed, themeVariant, themeOverrides, colorMode, presence, showTokenUsage, floatingPrompt = false, editorSelector = false, editors = true, conversation = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
55
+ const { serverUrl, target = 'browser', defaultEditor: askedEditor, showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, suggestionLabels = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutArrangement = 'page', pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, datalayerAgentSpecId, datalayerCreatePayload, datalayerVisitors, datalayerKept, embedToken, targetFixed, themeVariant, themeOverrides, colorMode, presence, showTokenUsage, thread, floatingPrompt = false, editorSelector = false, editors = true, conversation = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
56
56
  const defaultEditor = askedEditor ?? (editors ? 'notebook' : 'none');
57
57
  // A workspace without a conversation: the theme, the shell's points, and
58
58
  // the host's chrome; its view is a host's plugin's.
@@ -89,6 +89,7 @@ export function loopPlugins(options = {}) {
89
89
  colorMode,
90
90
  presence,
91
91
  showTokenUsage,
92
+ ...(thread ? { thread } : {}),
92
93
  }),
93
94
  // The composer and the title bar are plugins of their own: the chat
94
95
  // assembles their props, these render them. In the preset by default —
@@ -91,8 +91,11 @@ function WorkspaceBody({ serverUrl, agentId: initialAgentId, conversationId, mod
91
91
  workspace: currentWorkspace.current,
92
92
  argv: command.argv,
93
93
  });
94
+ // What a command sends goes to the agent as if typed: an
95
+ // application's command sends its prompt (LOOP P-19).
94
96
  if (result?.prompt) {
95
97
  onSend?.(result.prompt, currentWorkspace.current);
98
+ prompts.submit(result.prompt);
96
99
  }
97
100
  return {
98
101
  handled: true,
@@ -26,6 +26,7 @@ import { type LanguageModel } from 'ai';
26
26
  import type { ProtocolAdapterConfig } from '../types/protocol';
27
27
  import type { ChatMessage } from '../types/messages';
28
28
  import type { FrontendToolDefinition } from '../types/tools';
29
+ import type { AppModeEffect } from '../types/agentspecs';
29
30
  import type { AgentStreamSubagentPayload } from '../types/stream';
30
31
  import { BaseProtocolAdapter } from './BaseProtocolAdapter';
31
32
  import type { TeamContextSharing } from '../types/teams';
@@ -63,6 +64,13 @@ export interface BrowserAgentAdapterConfig extends ProtocolAdapterConfig, Fronte
63
64
  sharing?: TeamContextSharing;
64
65
  /** Told what a delegated run does, as it does it — see `subagentTools`. */
65
66
  onSubagentEvent?: (event: AgentStreamSubagentPayload) => void;
67
+ /**
68
+ * What the modes a run is sent in (`forwardedProps.loop.modes`, LOOP P-19)
69
+ * tell it, and the model it runs on: an application's, by agentspecs'
70
+ * rules. Unsaid for an agent with no modes, which refuses a run sent in
71
+ * some.
72
+ */
73
+ modeEffect?: (chosen: Record<string, string>) => AppModeEffect;
66
74
  }
67
75
  /**
68
76
  * An agent loop running in the page, speaking the protocol adapter interface.
@@ -111,6 +119,15 @@ export declare class BrowserAgentAdapter extends BaseProtocolAdapter {
111
119
  * `languageModel` is the host's own choice and is not swapped out.
112
120
  */
113
121
  private resolveModel;
122
+ /**
123
+ * What the modes a run is sent in tell it, and the model it runs on (LOOP
124
+ * P-19) — the runtime's session API applies the same, for that run only.
125
+ *
126
+ * Refused, in a sentence, rather than run without them: modes sent to an
127
+ * agent that has none, modes that are not the application's, and a mode
128
+ * naming a model for an agent whose model is the host's own.
129
+ */
130
+ private runModes;
114
131
  /**
115
132
  * Run one turn of the loop and report it as it happens.
116
133
  *
@@ -121,5 +138,6 @@ export declare class BrowserAgentAdapter extends BaseProtocolAdapter {
121
138
  sendMessage(message: ChatMessage, options?: {
122
139
  messages?: ChatMessage[];
123
140
  model?: string;
141
+ forwardedProps?: Record<string, unknown>;
124
142
  }): Promise<void>;
125
143
  }
@@ -140,6 +140,30 @@ export class BrowserAgentAdapter extends BaseProtocolAdapter {
140
140
  }
141
141
  return createBrowserModel({ ...inference, model: requested || model });
142
142
  }
143
+ /**
144
+ * What the modes a run is sent in tell it, and the model it runs on (LOOP
145
+ * P-19) — the runtime's session API applies the same, for that run only.
146
+ *
147
+ * Refused, in a sentence, rather than run without them: modes sent to an
148
+ * agent that has none, modes that are not the application's, and a mode
149
+ * naming a model for an agent whose model is the host's own.
150
+ */
151
+ runModes(forwardedProps) {
152
+ const loop = forwardedProps?.loop;
153
+ const chosen = loop?.modes;
154
+ if (!chosen || Object.keys(chosen).length === 0) {
155
+ return undefined;
156
+ }
157
+ const { modeEffect, languageModel } = this.browserConfig;
158
+ if (!modeEffect) {
159
+ throw new Error('This agent has no modes: the options chosen in the composer cannot be applied to it.');
160
+ }
161
+ const effect = modeEffect(chosen);
162
+ if (effect.model && languageModel) {
163
+ throw new Error(`The mode chosen runs on ${effect.model}, and this agent's model is its host's own: it cannot run on another.`);
164
+ }
165
+ return effect;
166
+ }
143
167
  /**
144
168
  * Run one turn of the loop and report it as it happens.
145
169
  *
@@ -194,9 +218,18 @@ export class BrowserAgentAdapter extends BaseProtocolAdapter {
194
218
  this.emit({ type: 'message', message: assistant, timestamp: new Date() });
195
219
  };
196
220
  try {
221
+ // The modes the person is in, for this run only: their instructions
222
+ // after the agent's, and the model one names over the chat's.
223
+ const modes = this.runModes(options?.forwardedProps);
224
+ const { instructions } = this.browserConfig;
225
+ const system = modes?.instructions
226
+ ? [instructions?.trim(), modes.instructions]
227
+ .filter(Boolean)
228
+ .join('\n\n')
229
+ : instructions;
197
230
  const result = streamText({
198
- model: this.resolveModel(options?.model),
199
- system: this.browserConfig.instructions,
231
+ model: this.resolveModel(modes?.model || options?.model),
232
+ system,
200
233
  messages: history,
201
234
  tools: this.tools,
202
235
  stopWhen: stepCountIs(this.browserConfig.maxSteps ?? DEFAULT_BROWSER_MAX_STEPS),
@@ -38,6 +38,14 @@
38
38
  "pause": "/api/v1/a2a/pause",
39
39
  "steer": "/api/v1/a2a/steer"
40
40
  }
41
+ },
42
+ {
43
+ "uri": "https://datalayer.ai/extensions/face/v1",
44
+ "description": "Its face: an emoji, and an avatar when it has one.",
45
+ "required": false,
46
+ "params": {
47
+ "emoji": "🧾"
48
+ }
41
49
  }
42
50
  ]
43
51
  },
@@ -37,7 +37,18 @@ export type A2APeer = {
37
37
  client: Client;
38
38
  card: AgentCard;
39
39
  skill: AgentSkill;
40
+ /** Its face, when its card carries one (`A2A_FACE_EXTENSION_URI`). */
41
+ face?: A2APeerFace;
40
42
  };
43
+ /** The extension an agent card carries its face in: its name stays plain. */
44
+ export declare const A2A_FACE_EXTENSION_URI = "https://datalayer.ai/extensions/face/v1";
45
+ /** A peer's face: its emoji and, when it has one, its avatar's name. */
46
+ export type A2APeerFace = {
47
+ emoji: string;
48
+ avatar?: string;
49
+ };
50
+ /** The face an agent card carries, if it carries one. */
51
+ export declare function faceOfCard(card: AgentCard): A2APeerFace | undefined;
41
52
  export type ConnectA2APeerOptions = {
42
53
  /** Where the peer is served: `…/api/v1/a2a/agents/<application id>`. */
43
54
  url: string;