@datalayer/agent-runtimes 1.3.29 → 1.3.32

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 (107) hide show
  1. package/lib/AgentNode.js +14 -33
  2. package/lib/chat/ChatFloating.js +40 -8
  3. package/lib/chat/assistant/AssistantStage.d.ts +26 -7
  4. package/lib/chat/assistant/AssistantStage.js +115 -8
  5. package/lib/chat/assistant/SpeechBalloon.js +7 -3
  6. package/lib/chat/assistant/characters.d.ts +32 -1
  7. package/lib/chat/assistant/characters.js +180 -15
  8. package/lib/chat/assistant/state.d.ts +28 -0
  9. package/lib/chat/assistant/state.js +31 -0
  10. package/lib/chat/base/ChatBase.js +74 -50
  11. package/lib/chat/base/modelChoice.d.ts +40 -1
  12. package/lib/chat/base/modelChoice.js +42 -15
  13. package/lib/chat/prompt/InputPrompt.d.ts +7 -1
  14. package/lib/chat/prompt/InputPrompt.js +2 -2
  15. package/lib/chat/prompt/InputPromptBase.js +1 -1
  16. package/lib/chat/prompt/menus/ModelSelector.d.ts +13 -1
  17. package/lib/chat/prompt/menus/ModelSelector.js +17 -5
  18. package/lib/chat/useViewportDrag.d.ts +9 -1
  19. package/lib/chat/useViewportDrag.js +4 -4
  20. package/lib/components/a2ui/index.d.ts +2 -0
  21. package/lib/components/a2ui/index.js +2 -0
  22. package/lib/components/a2ui/visibility.d.ts +15 -0
  23. package/lib/components/a2ui/visibility.js +24 -0
  24. package/lib/components/a2ui/visibleWhen.d.ts +17 -0
  25. package/lib/components/a2ui/visibleWhen.js +54 -0
  26. package/lib/examples/ChatAssistantExample.d.ts +4 -0
  27. package/lib/examples/ChatAssistantExample.js +26 -5
  28. package/lib/examples/utils/owlCharacterPlugin.d.ts +7 -0
  29. package/lib/examples/utils/owlCharacterPlugin.js +44 -0
  30. package/lib/loop/apps/AppFeedback.d.ts +27 -0
  31. package/lib/loop/apps/AppFeedback.js +78 -0
  32. package/lib/loop/apps/AppRenderer.d.ts +33 -1
  33. package/lib/loop/apps/AppRenderer.js +111 -29
  34. package/lib/loop/apps/agent.d.ts +15 -0
  35. package/lib/loop/apps/agent.js +42 -0
  36. package/lib/loop/apps/appspec.js +6 -3
  37. package/lib/loop/apps/checks.d.ts +10 -2
  38. package/lib/loop/apps/checks.js +21 -10
  39. package/lib/loop/apps/feedback.d.ts +51 -0
  40. package/lib/loop/apps/feedback.js +61 -0
  41. package/lib/loop/apps/index.d.ts +3 -0
  42. package/lib/loop/apps/index.js +3 -0
  43. package/lib/loop/apps/rules.d.ts +14 -0
  44. package/lib/loop/apps/rules.js +24 -0
  45. package/lib/loop/core/canvasBlocks.d.ts +16 -0
  46. package/lib/loop/core/canvasBlocks.js +13 -0
  47. package/lib/loop/core/index.d.ts +15 -0
  48. package/lib/loop/core/index.js +3 -0
  49. package/lib/loop/embed/AppEmbed.d.ts +42 -0
  50. package/lib/loop/embed/AppEmbed.js +205 -0
  51. package/lib/loop/embed/datalayer-app.d.ts +1 -0
  52. package/lib/loop/embed/datalayer-app.js +16 -0
  53. package/lib/loop/embed/element.d.ts +57 -0
  54. package/lib/loop/embed/element.js +405 -0
  55. package/lib/loop/embed/embedConfig.d.ts +120 -0
  56. package/lib/loop/embed/embedConfig.js +148 -0
  57. package/lib/loop/embed/embedTheme.d.ts +24 -0
  58. package/lib/loop/embed/embedTheme.js +83 -0
  59. package/lib/loop/index.d.ts +6 -2
  60. package/lib/loop/index.js +8 -2
  61. package/lib/loop/plugins/a2ui/A2uiView.js +2 -3
  62. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +23 -3
  63. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +36 -9
  64. package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
  65. package/lib/loop/plugins/agent-capacity/index.js +3 -1
  66. package/lib/loop/plugins/agents/switchable.d.ts +7 -0
  67. package/lib/loop/plugins/agents/switchable.js +91 -88
  68. package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
  69. package/lib/loop/plugins/app-page/AppPage.js +53 -0
  70. package/lib/loop/plugins/app-page/appPageModel.d.ts +180 -0
  71. package/lib/loop/plugins/app-page/appPageModel.js +443 -0
  72. package/lib/loop/plugins/app-page/index.d.ts +8 -0
  73. package/lib/loop/plugins/app-page/index.js +34 -0
  74. package/lib/loop/plugins/canvas-blocks/index.d.ts +63 -0
  75. package/lib/loop/plugins/canvas-blocks/index.js +118 -0
  76. package/lib/loop/plugins/chat/ChatView.js +52 -26
  77. package/lib/loop/plugins/chat/turnState.d.ts +5 -2
  78. package/lib/loop/plugins/chat/turnState.js +2 -1
  79. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
  80. package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
  81. package/lib/loop/plugins/page-layout/index.d.ts +13 -0
  82. package/lib/loop/plugins/page-layout/index.js +10 -1
  83. package/lib/loop/presets.d.ts +12 -0
  84. package/lib/loop/presets.js +5 -3
  85. package/lib/specs/actions.js +9 -0
  86. package/lib/specs/agents/agents.js +20 -4
  87. package/lib/specs/apps.d.ts +10 -1
  88. package/lib/specs/apps.js +1779 -154
  89. package/lib/specs/appspecSchema.js +14 -8
  90. package/lib/specs/models.d.ts +1 -1
  91. package/lib/specs/models.js +8 -8
  92. package/lib/specs/tools.d.ts +1 -0
  93. package/lib/specs/tools.js +19 -0
  94. package/lib/stories/loop/LoopReference.stories.d.ts +21 -0
  95. package/lib/stories/loop/LoopReference.stories.js +31 -0
  96. package/lib/stories/loop/ReferenceScreens.d.ts +53 -0
  97. package/lib/stories/loop/ReferenceScreens.js +256 -0
  98. package/lib/stories/loop/pictures-main.d.ts +1 -0
  99. package/lib/stories/loop/pictures-main.js +64 -0
  100. package/lib/types/agentspecs.d.ts +17 -1
  101. package/lib/types/chat.d.ts +5 -0
  102. package/lib/types/config.d.ts +20 -0
  103. package/package.json +10 -6
  104. package/scripts/check-cloudflare-models.py +2 -2
  105. package/scripts/codegen/generate_agents.py +50 -2
  106. package/scripts/codegen/generate_apps.py +38 -7
  107. package/scripts/codegen/generate_models.py +4 -4
@@ -0,0 +1,24 @@
1
+ import { type ThemeStyles } from '@datalayer/primer-addons';
2
+ import type { AppAccent } from '../../types/agentspecs';
3
+ /**
4
+ * The `loop` theme as the embed wears it: the application's accent over it
5
+ * in both modes (T-05), and the host's face when it named one.
6
+ */
7
+ export declare function embedThemeStyles({ accent, font, }: {
8
+ accent: AppAccent;
9
+ font?: string;
10
+ }): ThemeStyles;
11
+ /**
12
+ * The element's own stylesheet, written into its shadow root.
13
+ *
14
+ * `all: initial` on the host: nothing the host page sets — a font, a colour,
15
+ * a line height, a text alignment — is inherited by the application. The
16
+ * theme's custom properties are set again inside, on the provider's element,
17
+ * so a host's variable of the same name never reaches what the application
18
+ * draws. A floating mode takes no room in the page: what it draws is fixed to
19
+ * the viewport.
20
+ *
21
+ * With the theme's own stylesheet, scoped to the provider's element in this
22
+ * root.
23
+ */
24
+ export declare function embedShadowCss(themeCss?: string): string;
@@ -0,0 +1,83 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { loopAccentStyles, loopFontFamily, loopThemeStyles, scopeThemeCss, THEME_SCOPE_ATTRIBUTE, } from '@datalayer/primer-addons';
6
+ /** One mode's properties, with the theme's face replaced by the host's. */
7
+ function withFace(styles, font) {
8
+ if (!font) {
9
+ return styles;
10
+ }
11
+ return Object.fromEntries(Object.entries(styles).map(([name, value]) => [
12
+ name,
13
+ typeof value === 'string'
14
+ ? value.split(loopFontFamily).join(font)
15
+ : value,
16
+ ]));
17
+ }
18
+ /**
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.
21
+ */
22
+ export function embedThemeStyles({ accent, font = '', }) {
23
+ const mode = (which) => withFace({
24
+ ...loopThemeStyles[which],
25
+ ...loopAccentStyles(accent, which),
26
+ }, font);
27
+ return {
28
+ ...loopThemeStyles,
29
+ light: mode('light'),
30
+ dark: mode('dark'),
31
+ };
32
+ }
33
+ /**
34
+ * The element's own stylesheet, written into its shadow root.
35
+ *
36
+ * `all: initial` on the host: nothing the host page sets — a font, a colour,
37
+ * a line height, a text alignment — is inherited by the application. The
38
+ * theme's custom properties are set again inside, on the provider's element,
39
+ * so a host's variable of the same name never reaches what the application
40
+ * draws. A floating mode takes no room in the page: what it draws is fixed to
41
+ * the viewport.
42
+ *
43
+ * With the theme's own stylesheet, scoped to the provider's element in this
44
+ * root.
45
+ */
46
+ export function embedShadowCss(themeCss) {
47
+ const own = `
48
+ :host {
49
+ all: initial;
50
+ display: block;
51
+ position: relative;
52
+ width: 100%;
53
+ }
54
+ :host([hidden]) {
55
+ display: none;
56
+ }
57
+ :host(:not([data-embed-mode="inline"])) {
58
+ width: 0;
59
+ height: 0;
60
+ }
61
+ .datalayer-app-root,
62
+ .datalayer-app-portal {
63
+ font-family: var(--fontStack-sansSerif, system-ui, sans-serif);
64
+ }
65
+ .datalayer-app-said {
66
+ font: 14px/1.5 system-ui, sans-serif;
67
+ padding: 12px 16px;
68
+ border: 1px solid #d0d7de;
69
+ border-radius: 12px;
70
+ color: #1f2328;
71
+ background: #ffffff;
72
+ }
73
+ iframe.datalayer-app-frame {
74
+ display: block;
75
+ width: 100%;
76
+ border: 0;
77
+ min-height: 240px;
78
+ }
79
+ `;
80
+ return themeCss
81
+ ? `${own}\n${scopeThemeCss(themeCss, [`[${THEME_SCOPE_ATTRIBUTE}]`])}`
82
+ : own;
83
+ }
@@ -16,7 +16,7 @@ export { ChatHeaderPlugin, CHAT_HEADER_PLUGIN_NAME, } from './plugins/chat-heade
16
16
  export { InputPromptPlugin, INPUT_PROMPT_PLUGIN_NAME, type InputPromptPluginConfig, } from './plugins/input-prompt';
17
17
  export { createChatExtrasPlugin, CHAT_EXTRAS_PLUGIN_NAME, type ChatExtrasHandle, } from './plugins/chat-extras';
18
18
  export { LoopChatExtras, LoopChatLayout, LoopChatTurn, LoopOpeningDocument, LoopOpeningNotebook, LoopPromptPanel, type ChatLayoutContribution, type ChatLayoutParts, type ChatTurnContribution, type ChatTurnSnapshot, type ChatTurnStatus, type LoopChatExtrasValue, type OpeningDocumentContribution, type OpeningNotebookContribution, type PromptPanelContribution, } from './core';
19
- export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen, setPageLayoutChatMode, type LoopPageLayoutConfig, } from './plugins/page-layout';
19
+ export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen, setPageLayoutChatMode, SplitLayout, type LoopPageLayoutConfig, } from './plugins/page-layout';
20
20
  export { useOptionalTeamSelection } from './plugins/agents/useTeamSelection';
21
21
  export type { TeamMember, TeamSelection } from './plugins/agents/team';
22
22
  export { ChatViewPlugin, CHAT_VIEW_PLUGIN_NAME } from './plugins/chat-view';
@@ -50,6 +50,7 @@ export { SANDBOX_CAPACITIES, SandboxCapacityPlugins, } from './plugins/agent-cod
50
50
  export { AgentA2uiPlugin, AGENT_A2UI_PLUGIN_NAME } from './plugins/agent-a2ui';
51
51
  export { AgentA2uiJupyterOutputPlugin, AGENT_A2UI_JUPYTER_OUTPUT_PLUGIN_NAME, } from './plugins/agent-a2ui-jupyter-output';
52
52
  export { defineA2uiScenePlugin, type A2uiSceneOptions, } from './plugins/a2ui-scene';
53
+ export { APP_KIND_PATHS, APP_PAGE_PLUGIN_NAME, APP_PAGE_SURFACE, AppPage, appKindPaths, appPageAction, appPageData, defineAppPagePlugin, hasAppPage, surfaceUnshown, type AppKindPaths, type AppPageAction, type AppPageKind, type AppPagePath, } from './plugins/app-page';
53
54
  export { GraphViewPlugin, GRAPH_PLUGIN_NAME, GRAPH_VIEW_TYPE, } from './plugins/graph';
54
55
  export { PluginsPanelPlugin, PLUGINS_PANEL_PLUGIN_NAME, PluginsPanel, } from './plugins/plugins-panel';
55
56
  export { AGENTS_PLUGIN_NAME, AgentsPlugin, SandboxSelector, browserSource, createBrowserSandboxService, createServerSandboxService, createSwitchableSandboxService, suppliedSource, useOptionalSandboxService, useSandboxService, type AgentsConfig, type AgentsOutput, type SandboxExecution, type SandboxService, type SandboxTarget, type ServiceManagerSource, type SwitchableSandboxService, } from './plugins/agents';
@@ -63,7 +64,10 @@ export { WindowFrame, WindowFramePlugin, WINDOW_ACTIONS_SLOT, WINDOW_CONTROLS_SL
63
64
  export { LoopCommandsPlugin, COMMANDS_PLUGIN_NAME } from './plugins/commands';
64
65
  export { PromptPlugin, PROMPT_PLUGIN_NAME } from './plugins/prompt';
65
66
  export { LoopEmbed, type LoopEmbedProps } from './embed/LoopEmbed';
67
+ export { AppEmbed, type AppEmbedProps } from './embed/AppEmbed';
68
+ export { defineDatalayerAppElement, type DatalayerAppElementOptions, } from './embed/element';
69
+ export { EMBED_ATTRIBUTES, EMBED_HOST_VARIABLES, EMBED_TAG, embedLookOf, embedSnippetOf, floatingViewOf, type EmbedLook, type EmbedSnippetOptions, } from './embed/embedConfig';
66
70
  export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
67
71
  export { loopPlugins, type LoopPresetOptions } from './presets';
68
72
  export { DocumentExtension, NotebookExtension } from './extensions';
69
- export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, type BehaviourOptions, type ToolArguments, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, type AppCheck, defineAppPlugin, type AppRendererProps, type AppInstance, readAppspecYaml, writeAppspecYaml, type ParsedAppspec, } from './apps';
73
+ export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, type BehaviourOptions, type ToolArguments, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, type AppCheck, type CheckContext, defineAppPlugin, type AppRendererProps, type AppInstance, appLayoutOptions, readAppspecYaml, writeAppspecYaml, type ParsedAppspec, } from './apps';
package/lib/loop/index.js CHANGED
@@ -29,7 +29,7 @@ export { LoopChatExtras, LoopChatLayout, LoopChatTurn, LoopOpeningDocument, Loop
29
29
  export { LoopPageLayoutPlugin, LOOP_PAGE_LAYOUT_PLUGIN_NAME, openConversationPanel, pageLayoutSheet, pageLayoutPanelOpen,
30
30
  // How a host moves the composer: `sidebar` stands it in the conversation
31
31
  // panel, under what was said, and opens the panel to show it.
32
- setPageLayoutChatMode, } from './plugins/page-layout';
32
+ setPageLayoutChatMode, SplitLayout, } from './plugins/page-layout';
33
33
  export { useOptionalTeamSelection } from './plugins/agents/useTeamSelection';
34
34
  export { ChatViewPlugin, CHAT_VIEW_PLUGIN_NAME } from './plugins/chat-view';
35
35
  export { NotebookViewPlugin, NOTEBOOK_VIEW_PLUGIN_NAME, } from './plugins/notebook-view';
@@ -63,6 +63,7 @@ export { SANDBOX_CAPACITIES, SandboxCapacityPlugins, } from './plugins/agent-cod
63
63
  export { AgentA2uiPlugin, AGENT_A2UI_PLUGIN_NAME } from './plugins/agent-a2ui';
64
64
  export { AgentA2uiJupyterOutputPlugin, AGENT_A2UI_JUPYTER_OUTPUT_PLUGIN_NAME, } from './plugins/agent-a2ui-jupyter-output';
65
65
  export { defineA2uiScenePlugin, } from './plugins/a2ui-scene';
66
+ export { APP_KIND_PATHS, APP_PAGE_PLUGIN_NAME, APP_PAGE_SURFACE, AppPage, appKindPaths, appPageAction, appPageData, defineAppPagePlugin, hasAppPage, surfaceUnshown, } from './plugins/app-page';
66
67
  export { GraphViewPlugin, GRAPH_PLUGIN_NAME, GRAPH_VIEW_TYPE, } from './plugins/graph';
67
68
  export { PluginsPanelPlugin, PLUGINS_PANEL_PLUGIN_NAME, PluginsPanel, } from './plugins/plugins-panel';
68
69
  export { AGENTS_PLUGIN_NAME, AgentsPlugin, SandboxSelector, browserSource, createBrowserSandboxService, createServerSandboxService, createSwitchableSandboxService, suppliedSource, useOptionalSandboxService, useSandboxService, } from './plugins/agents';
@@ -81,6 +82,11 @@ export { LoopCommandsPlugin, COMMANDS_PLUGIN_NAME } from './plugins/commands';
81
82
  export { PromptPlugin, PROMPT_PLUGIN_NAME } from './plugins/prompt';
82
83
  // Loop, ready to drop into somebody else's page.
83
84
  export { LoopEmbed } from './embed/LoopEmbed';
85
+ // An application in another product's page: the React component, the
86
+ // `<datalayer-app>` element, and the snippet a host pastes (LOOP D-07 to D-09).
87
+ export { AppEmbed } from './embed/AppEmbed';
88
+ export { defineDatalayerAppElement, } from './embed/element';
89
+ export { EMBED_ATTRIBUTES, EMBED_HOST_VARIABLES, EMBED_TAG, embedLookOf, embedSnippetOf, floatingViewOf, } from './embed/embedConfig';
84
90
  export { SubagentActivityPlugin, SubagentPulse, SUBAGENT_ACTIVITY_PLUGIN_NAME, } from './plugins/subagent-activity';
85
91
  // The set a workspace normally has, so a host starts from a working Loop
86
92
  // rather than assembling one — and never from the example.
@@ -90,4 +96,4 @@ export { loopPlugins } from './presets';
90
96
  export { DocumentExtension, NotebookExtension } from './extensions';
91
97
  // Applications: what one does when its agent calls a tool — the rules a
92
98
  // person wrote, decided on what the tool does.
93
- export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, defineAppPlugin, readAppspecYaml, writeAppspecYaml, } from './apps';
99
+ export { BEHAVIOURS as APP_BEHAVIOUR_ORDER, DEFAULT_BEHAVIOURS as APP_DEFAULT_BEHAVIOURS, behaviourFor, classesOf, isReadOnly, toolBehaviours, toolEscalations, APP_SCHEMA, APPSPEC_KEY_ORDER, DEFAULT_LAYOUTS as APP_DEFAULT_LAYOUTS, dumpAppspec, emptyAppspec, parseAppspec, AppRenderer, checkApp, checkAppspec, defineAppPlugin, appLayoutOptions, readAppspecYaml, writeAppspecYaml, } from './apps';
@@ -1,10 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Box, Text } from '@primer/react';
4
- import { basicCatalog, } from '@a2ui/react/v0_9';
5
4
  import { MessageProcessor, } from '@a2ui/web_core/v0_9';
6
5
  import { useSignalValue } from '@datalayer/reactor/react';
7
- import { A2uiSurfaceComposed } from '../../../components/a2ui';
6
+ import { A2uiSurfaceComposed, datalayerCatalog, } from '../../../components/a2ui';
8
7
  import { useSandboxService } from '../agents';
9
8
  /**
10
9
  * A2UI's basic catalog draws from inherited `--a2ui-*` custom properties, so a
@@ -27,7 +26,7 @@ export default function A2uiView({ workspace }) {
27
26
  // rather than whatever the sandbox happened to execute last.
28
27
  const sourceRef = useRef('');
29
28
  const renderRef = useRef(null);
30
- const processor = useMemo(() => new MessageProcessor([basicCatalog], action => {
29
+ const processor = useMemo(() => new MessageProcessor([datalayerCatalog], action => {
31
30
  // The round-trip: what the reader did goes back to the code that drew
32
31
  // the surface, and the surface it returns replaces this one. Without
33
32
  // this a surface is a screenshot with buttons on it.
@@ -8,15 +8,35 @@
8
8
  * follows, and what keeps a generated form from being the one light card in
9
9
  * a dark workspace.
10
10
  *
11
+ * The same renderer draws an application's page (`loop/plugins/app-page`):
12
+ * there the surface is fed live `data` by path as the conversation moves, and
13
+ * an action is handed the surface it came from, so that what a block wrote
14
+ * into its data model can be read when a button is pressed. It draws from
15
+ * Datalayer's catalog: the basic one, any block shown only while its
16
+ * `visible_when` holds.
17
+ *
11
18
  * @module loop/plugins/a2ui-surface/InlineSurface
12
19
  */
13
20
  import type { JSX } from 'react';
14
- import { type A2uiClientAction, type A2uiMessage } from '@a2ui/web_core/v0_9';
21
+ import type { ReactComponentImplementation } from '@a2ui/react/v0_9';
22
+ import { type A2uiClientAction, type A2uiMessage, type SurfaceModel } from '@a2ui/web_core/v0_9';
23
+ /** A surface as the renderer holds it: its data model is `dataModel`. */
24
+ export type InlineSurfaceModel = SurfaceModel<ReactComponentImplementation>;
25
+ type Surface = InlineSurfaceModel;
15
26
  /** The catalogue a surface is rewritten to before it is drawn. */
16
27
  export declare const SURFACE_CATALOG_ID: string;
17
- export declare function InlineSurface({ messages, onAction, validationError, }: {
28
+ /** Values to publish into a surface, by path. */
29
+ export type InlineSurfaceData = Record<string, unknown>;
30
+ export declare function InlineSurface({ messages, onAction, validationError, data, }: {
18
31
  messages: A2uiMessage[];
19
- onAction: (action: A2uiClientAction) => void;
32
+ /** Told of an action, with the surface it came from. */
33
+ onAction: (action: A2uiClientAction, surface?: Surface) => void;
20
34
  validationError?: string | null;
35
+ /**
36
+ * Values published into every surface's data model, by path, after the
37
+ * messages are processed and whenever one changes. Only a path whose value
38
+ * changed is written: what a person typed elsewhere in the model stays.
39
+ */
40
+ data?: InlineSurfaceData;
21
41
  }): JSX.Element | null;
22
42
  export default InlineSurface;
@@ -1,23 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Box } from '@datalayer/primer-addons';
4
- import { basicCatalog, } from '@a2ui/react/v0_9';
5
4
  import { MessageProcessor, } from '@a2ui/web_core/v0_9';
6
- import { A2UI_RENDER_SCOPE_SX, A2uiMarkdownProvider, A2uiSurfaceComposed, } from '../../../components/a2ui';
5
+ import { A2UI_RENDER_SCOPE_SX, A2uiMarkdownProvider, A2uiSurfaceComposed, datalayerCatalog, } from '../../../components/a2ui';
7
6
  /** The catalogue a surface is rewritten to before it is drawn. */
8
- export const SURFACE_CATALOG_ID = basicCatalog.id;
7
+ export const SURFACE_CATALOG_ID = datalayerCatalog.id;
9
8
  const INHERIT_THEME = {
10
9
  ['--a2ui-color-surface']: 'var(--bgColor-muted)',
11
10
  ['--a2ui-color-on-surface']: 'var(--fgColor-default)',
12
11
  ['--a2ui-color-primary']: 'var(--fgColor-accent)',
13
12
  ['--a2ui-color-outline']: 'var(--borderColor-default)',
14
13
  };
15
- export function InlineSurface({ messages, onAction, validationError, }) {
14
+ export function InlineSurface({ messages, onAction, validationError, data, }) {
16
15
  // Reached through a ref: the processor is built once, and the handler it
17
16
  // was built with must not go stale when the host's does not.
18
17
  const onActionRef = useRef(onAction);
19
18
  onActionRef.current = onAction;
20
- const processor = useMemo(() => new MessageProcessor([basicCatalog], action => onActionRef.current(action)), []);
19
+ const processor = useMemo(() => new MessageProcessor([datalayerCatalog], action => onActionRef.current(action, processorRef.current?.model.getSurface(action.surfaceId))), []);
20
+ const processorRef = useRef(undefined);
21
+ processorRef.current = processor;
21
22
  const [surfaces, setSurfaces] = useState([]);
22
23
  useEffect(() => {
23
24
  const created = processor.onSurfaceCreated(surface => setSurfaces(previous => [...previous, surface]));
@@ -28,15 +29,41 @@ export function InlineSurface({ messages, onAction, validationError, }) {
28
29
  };
29
30
  }, [processor]);
30
31
  // Once: the messages are one tool result, processed when it arrives.
32
+ // A tree the catalog refuses is said, where it would have been drawn,
33
+ // rather than thrown out of an effect and taking the page down with it.
31
34
  const processedRef = useRef(false);
35
+ const [processError, setProcessError] = useState(null);
32
36
  useEffect(() => {
33
37
  if (processedRef.current) {
34
38
  return;
35
39
  }
36
40
  processedRef.current = true;
37
- processor.processMessages(messages);
41
+ try {
42
+ processor.processMessages(messages);
43
+ }
44
+ catch (error) {
45
+ setProcessError(`This surface could not be drawn: ${error instanceof Error ? error.message : String(error)}`);
46
+ }
38
47
  }, [messages, processor]);
39
- if (surfaces.length === 0) {
48
+ // After the messages: the surfaces exist by then, created synchronously.
49
+ const publishedRef = useRef({});
50
+ useEffect(() => {
51
+ if (!data) {
52
+ return;
53
+ }
54
+ const published = publishedRef.current;
55
+ for (const [path, value] of Object.entries(data)) {
56
+ if (Object.is(published[path], value)) {
57
+ continue;
58
+ }
59
+ published[path] = value;
60
+ for (const surface of processor.model.surfacesMap.values()) {
61
+ surface.dataModel.set(path, value);
62
+ }
63
+ }
64
+ }, [data, processor]);
65
+ const notice = validationError || processError;
66
+ if (surfaces.length === 0 && !notice) {
40
67
  return null;
41
68
  }
42
69
  return (_jsx(A2uiMarkdownProvider, { children: _jsxs(Box, { style: INHERIT_THEME, sx: {
@@ -44,7 +71,7 @@ export function InlineSurface({ messages, onAction, validationError, }) {
44
71
  display: 'flex',
45
72
  flexDirection: 'column',
46
73
  gap: 2,
47
- }, children: [validationError ? (_jsx(Box, { role: "alert", sx: {
74
+ }, children: [notice ? (_jsx(Box, { role: "alert", sx: {
48
75
  px: 3,
49
76
  py: 2,
50
77
  borderRadius: 2,
@@ -53,7 +80,7 @@ export function InlineSurface({ messages, onAction, validationError, }) {
53
80
  borderColor: 'danger.muted',
54
81
  color: 'danger.fg',
55
82
  fontSize: 1,
56
- }, children: validationError })) : null, surfaces.map(surface => (_jsx(Box, { sx: {
83
+ }, children: notice })) : null, surfaces.map(surface => (_jsx(Box, { sx: {
57
84
  border: '1px solid',
58
85
  borderColor: 'border.default',
59
86
  borderRadius: 2,
@@ -34,6 +34,10 @@ export type AgentCapacityOptions = {
34
34
  createPayload?: Record<string, unknown>;
35
35
  /** The openers the empty chat offers for this capacity. */
36
36
  suggestions?: ChatSuggestionItem[];
37
+ /** What the agent is told on top of its spec's prompt when the page turns it. */
38
+ instructions?: string;
39
+ /** The model it runs on in the page, in place of its spec's. */
40
+ model?: string;
37
41
  };
38
42
  export declare function defineAgentCapacityPlugin(options: AgentCapacityOptions): ReactorPlugin<Record<string, never>, unknown, unknown>;
39
43
  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, } = options;
26
+ const { key, displayName, description, specId, octicon, emoji, codemode, createPayload, suggestions, instructions, model, } = options;
27
27
  return definePlugin({
28
28
  name: `@datalayer/loop-plugin-agent-${key}`,
29
29
  displayName,
@@ -40,6 +40,8 @@ export function defineAgentCapacityPlugin(options) {
40
40
  enable_codemode: codemode ?? false,
41
41
  ...createPayload,
42
42
  },
43
+ ...(instructions ? { instructions } : {}),
44
+ ...(model ? { model } : {}),
43
45
  }, { id: key }),
44
46
  ...(suggestions && suggestions.length > 0
45
47
  ? [
@@ -105,3 +105,10 @@ export type SwitchableConfig = {
105
105
  kernelSource?: ServiceManagerSource;
106
106
  };
107
107
  export declare function createSwitchableSandboxService({ serverUrl, initialTarget, kernelSource, localAgent, blueprint, }: SwitchableConfig): SwitchableSandboxService;
108
+ /**
109
+ * Create the agent whose per-agent Jupyter sandbox backs Local, on the
110
+ * agent-runtimes server at `serverUrl`, unless it is there already — then
111
+ * only its sandbox is asked for. Exported for a host that addresses the
112
+ * agent without a workspace: the embed's floating modes (`AppEmbed`).
113
+ */
114
+ export declare function ensureServerAgent(serverUrl: string, agentId: string | undefined, createPayload: Record<string, unknown>): Promise<void>;
@@ -197,94 +197,9 @@ export function createSwitchableSandboxService({ serverUrl, initialTarget = 'loc
197
197
  (await detail(restart)));
198
198
  }
199
199
  }
200
- /**
201
- * Create the agent whose per-agent Jupyter sandbox backs Local.
202
- *
203
- * The payload is passed rather than read from the closure: the caller has
204
- * already established that it exists, and TypeScript cannot carry that
205
- * narrowing across a function boundary.
206
- */
207
- async function ensureLocalAgent(createPayload) {
208
- const id = agentId?.trim();
209
- if (!id) {
210
- throw new Error('Local needs an agent id before its runtime can start.');
211
- }
212
- let existing;
213
- try {
214
- existing = await fetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(id)}`);
215
- }
216
- catch (reason) {
217
- throw new Error(`Local needs an agent-runtimes server at ${serverUrl}, and nothing answered there. ` +
218
- `Start one and try again. (${reason instanceof Error ? reason.message : String(reason)})`, { cause: reason });
219
- }
220
- if (existing.ok) {
221
- // The agent is there, which does not mean its sandbox is. Reconfiguring
222
- // or restarting the manager stops the per-agent sandboxes, because they
223
- // were built under the previous settings — so coming back to Local after
224
- // a spell on Jupyter finds the agent alive and nothing behind it. Asking
225
- // for the sandbox is idempotent, so it costs a round trip and nothing
226
- // else when one is already running.
227
- await ensureLocalSandbox(id);
228
- return;
229
- }
230
- if (existing.status !== 404) {
231
- throw new Error(`The server could not inspect local agent ${id} (${existing.status} ${existing.statusText}). ` +
232
- (await detail(existing)));
233
- }
234
- const created = await fetch(`${serverUrl}/api/v1/agents`, {
235
- method: 'POST',
236
- headers: { 'content-type': 'application/json' },
237
- body: JSON.stringify({
238
- // Defaults first, so a blueprint that names its own transport or
239
- // sandbox variant — the sandbox capacity plugins do — wins over
240
- // them; the name last, because Local's agent id is not negotiable.
241
- transport: 'ag-ui',
242
- sandbox_variant: 'jupyter-server',
243
- ...createPayload,
244
- name: id,
245
- }),
246
- });
247
- if (!created.ok && created.status !== 409) {
248
- throw new Error(`The server could not launch local agent ${id} (${created.status} ${created.statusText}). ` +
249
- (await detail(created)));
250
- }
251
- // A 409 means somebody else created it between the two calls above, and
252
- // that agent is as unlikely to have a sandbox as one found by the GET.
253
- if (created.status === 409) {
254
- await ensureLocalSandbox(id);
255
- }
256
- }
257
- /**
258
- * Make sure the agent has somewhere to run.
259
- *
260
- * Creating an agent starts its sandbox, so this is only ever needed for one
261
- * that already existed. Selecting a target is an operation, not a
262
- * preference: when the promise resolves the sandbox is meant to be up, and
263
- * without this the kernel indicator sits empty and the notebook binds to
264
- * nothing.
265
- */
266
- async function ensureLocalSandbox(id) {
267
- const response = await fetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(id)}/sandbox/ensure`, { method: 'POST' });
268
- if (!response.ok) {
269
- throw new Error(`The server could not start a sandbox for local agent ${id} ` +
270
- `(${response.status} ${response.statusText}). ` +
271
- (await detail(response)));
272
- }
273
- }
274
- /**
275
- * What the server said went wrong, when it said anything.
276
- *
277
- * FastAPI puts the reason in `detail`, and a status code on its own sends a
278
- * reader to the network tab for something the response already contained.
279
- */
280
- async function detail(response) {
281
- try {
282
- const body = (await response.json());
283
- return typeof body.detail === 'string' ? body.detail : '';
284
- }
285
- catch {
286
- return '';
287
- }
200
+ /** The agent backing Local, on this workspace's server. */
201
+ function ensureLocalAgent(createPayload) {
202
+ return ensureServerAgent(serverUrl, agentId, createPayload);
288
203
  }
289
204
  function connectActive() {
290
205
  disconnect?.();
@@ -403,3 +318,91 @@ export function createSwitchableSandboxService({ serverUrl, initialTarget = 'loc
403
318
  },
404
319
  };
405
320
  }
321
+ /**
322
+ * Create the agent whose per-agent Jupyter sandbox backs Local, on the
323
+ * agent-runtimes server at `serverUrl`, unless it is there already — then
324
+ * only its sandbox is asked for. Exported for a host that addresses the
325
+ * agent without a workspace: the embed's floating modes (`AppEmbed`).
326
+ */
327
+ export async function ensureServerAgent(serverUrl, agentId, createPayload) {
328
+ const id = agentId?.trim();
329
+ if (!id) {
330
+ throw new Error('Local needs an agent id before its runtime can start.');
331
+ }
332
+ let existing;
333
+ try {
334
+ existing = await fetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(id)}`);
335
+ }
336
+ catch (reason) {
337
+ throw new Error(`Local needs an agent-runtimes server at ${serverUrl}, and nothing answered there. ` +
338
+ `Start one and try again. (${reason instanceof Error ? reason.message : String(reason)})`, { cause: reason });
339
+ }
340
+ if (existing.ok) {
341
+ // The agent is there, which does not mean its sandbox is. Reconfiguring
342
+ // or restarting the manager stops the per-agent sandboxes, because they
343
+ // were built under the previous settings — so coming back to Local after
344
+ // a spell on Jupyter finds the agent alive and nothing behind it. Asking
345
+ // for the sandbox is idempotent, so it costs a round trip and nothing
346
+ // else when one is already running.
347
+ await ensureServerSandbox(serverUrl, id);
348
+ return;
349
+ }
350
+ if (existing.status !== 404) {
351
+ throw new Error(`The server could not inspect local agent ${id} (${existing.status} ${existing.statusText}). ` +
352
+ (await detail(existing)));
353
+ }
354
+ const created = await fetch(`${serverUrl}/api/v1/agents`, {
355
+ method: 'POST',
356
+ headers: { 'content-type': 'application/json' },
357
+ body: JSON.stringify({
358
+ // Defaults first, so a blueprint that names its own transport or
359
+ // sandbox variant — the sandbox capacity plugins do — wins over
360
+ // them; the name last, because Local's agent id is not negotiable.
361
+ transport: 'ag-ui',
362
+ sandbox_variant: 'jupyter-server',
363
+ ...createPayload,
364
+ name: id,
365
+ }),
366
+ });
367
+ if (!created.ok && created.status !== 409) {
368
+ throw new Error(`The server could not launch local agent ${id} (${created.status} ${created.statusText}). ` +
369
+ (await detail(created)));
370
+ }
371
+ // A 409 means somebody else created it between the two calls above, and
372
+ // that agent is as unlikely to have a sandbox as one found by the GET.
373
+ if (created.status === 409) {
374
+ await ensureServerSandbox(serverUrl, id);
375
+ }
376
+ }
377
+ /**
378
+ * Make sure the agent has somewhere to run.
379
+ *
380
+ * Creating an agent starts its sandbox, so this is only ever needed for one
381
+ * that already existed. Selecting a target is an operation, not a
382
+ * preference: when the promise resolves the sandbox is meant to be up, and
383
+ * without this the kernel indicator sits empty and the notebook binds to
384
+ * nothing.
385
+ */
386
+ async function ensureServerSandbox(serverUrl, id) {
387
+ const response = await fetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(id)}/sandbox/ensure`, { method: 'POST' });
388
+ if (!response.ok) {
389
+ throw new Error(`The server could not start a sandbox for local agent ${id} ` +
390
+ `(${response.status} ${response.statusText}). ` +
391
+ (await detail(response)));
392
+ }
393
+ }
394
+ /**
395
+ * What the server said went wrong, when it said anything.
396
+ *
397
+ * FastAPI puts the reason in `detail`, and a status code on its own sends a
398
+ * reader to the network tab for something the response already contained.
399
+ */
400
+ async function detail(response) {
401
+ try {
402
+ const body = (await response.json());
403
+ return typeof body.detail === 'string' ? body.detail : '';
404
+ }
405
+ catch {
406
+ return '';
407
+ }
408
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * An application's page, drawn beside its conversation: its A2UI surface on
3
+ * the workspace's own renderer (`InlineSurface`), fed by the chat's current
4
+ * turn, its buttons answered through the chat's controls.
5
+ *
6
+ * @module loop/plugins/app-page/AppPage
7
+ */
8
+ import type { JSX } from 'react';
9
+ import type { AppSpec } from '../../../types/agentspecs';
10
+ import { type LoopWorkspaceContext } from '../../core';
11
+ export type AppPageProps = {
12
+ app: AppSpec;
13
+ workspace: LoopWorkspaceContext;
14
+ };
15
+ export declare function AppPage({ app, workspace }: AppPageProps): JSX.Element;
16
+ export default AppPage;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, useRef, useState } from 'react';
3
+ import { Box } from '@datalayer/primer-addons';
4
+ import { signal } from '@datalayer/reactor';
5
+ import { useContributions, useSignalValue } from '@datalayer/reactor/react';
6
+ import { LoopChatTurn, } from '../../core';
7
+ import { InlineSurface, SURFACE_CATALOG_ID, } from '../a2ui-surface/InlineSurface';
8
+ import { appPageAction, appPageData, appPageMessages } from './appPageModel';
9
+ /** No chat in the workspace: the page reads a turn that never starts. */
10
+ const NO_TURN = signal({ id: 0, status: 'idle' });
11
+ export function AppPage({ app, workspace }) {
12
+ const entries = useContributions(LoopChatTurn);
13
+ const turn = useSignalValue(entries[0]?.value.turn ?? NO_TURN);
14
+ const messages = useMemo(() => appPageMessages(app, SURFACE_CATALOG_ID), [app]);
15
+ const data = useMemo(() => appPageData(app, turn), [app, turn]);
16
+ const [refusal, setRefusal] = useState(null);
17
+ // The workspace changes as the chat reports itself; the handler reads the latest.
18
+ const workspaceRef = useRef(workspace);
19
+ workspaceRef.current = workspace;
20
+ // The inputs in words as last sent: a chat's message carries them when they change.
21
+ const lastInputs = useRef('');
22
+ const onAction = useCallback((action, surface) => {
23
+ const outcome = appPageAction(app, { name: action.name, context: action.context ?? {} }, path => surface?.dataModel.get(path), lastInputs.current);
24
+ const controls = workspaceRef.current.viewControls;
25
+ if ('refused' in outcome) {
26
+ setRefusal(outcome.refused);
27
+ return;
28
+ }
29
+ setRefusal(null);
30
+ if ('stop' in outcome) {
31
+ controls.stop?.();
32
+ }
33
+ else if ('newChat' in outcome) {
34
+ controls.newChat?.();
35
+ }
36
+ else {
37
+ // The chat's own send, as a surface in the transcript submits; the
38
+ // prompt channel when the chat has not reported itself yet.
39
+ if (controls.send) {
40
+ controls.send(outcome.send);
41
+ }
42
+ else {
43
+ workspaceRef.current.prompts.submit(outcome.send);
44
+ }
45
+ lastInputs.current = outcome.inputs;
46
+ for (const path of outcome.clear) {
47
+ surface?.dataModel.set(path, '');
48
+ }
49
+ }
50
+ }, [app]);
51
+ return (_jsx(Box, { "data-testid": "app-page", sx: { height: '100%', minHeight: 0, overflow: 'auto', p: 3 }, children: _jsx(InlineSurface, { messages: messages, data: data, onAction: onAction, validationError: refusal }) }));
52
+ }
53
+ export default AppPage;