@datalayer/agent-runtimes 1.3.29 → 1.3.30

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 (52) hide show
  1. package/lib/AgentNode.js +14 -33
  2. package/lib/chat/base/ChatBase.js +41 -32
  3. package/lib/chat/base/modelChoice.d.ts +13 -0
  4. package/lib/chat/base/modelChoice.js +13 -0
  5. package/lib/loop/apps/AppRenderer.d.ts +33 -1
  6. package/lib/loop/apps/AppRenderer.js +96 -29
  7. package/lib/loop/apps/agent.d.ts +15 -0
  8. package/lib/loop/apps/agent.js +42 -0
  9. package/lib/loop/apps/index.d.ts +1 -0
  10. package/lib/loop/apps/index.js +1 -0
  11. package/lib/loop/core/index.d.ts +8 -0
  12. package/lib/loop/embed/AppEmbed.d.ts +42 -0
  13. package/lib/loop/embed/AppEmbed.js +205 -0
  14. package/lib/loop/embed/datalayer-app.d.ts +1 -0
  15. package/lib/loop/embed/datalayer-app.js +16 -0
  16. package/lib/loop/embed/element.d.ts +57 -0
  17. package/lib/loop/embed/element.js +405 -0
  18. package/lib/loop/embed/embedConfig.d.ts +120 -0
  19. package/lib/loop/embed/embedConfig.js +148 -0
  20. package/lib/loop/embed/embedTheme.d.ts +24 -0
  21. package/lib/loop/embed/embedTheme.js +83 -0
  22. package/lib/loop/index.d.ts +6 -2
  23. package/lib/loop/index.js +8 -2
  24. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +21 -3
  25. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +34 -6
  26. package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
  27. package/lib/loop/plugins/agent-capacity/index.js +3 -1
  28. package/lib/loop/plugins/agents/switchable.d.ts +7 -0
  29. package/lib/loop/plugins/agents/switchable.js +91 -88
  30. package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
  31. package/lib/loop/plugins/app-page/AppPage.js +53 -0
  32. package/lib/loop/plugins/app-page/appPage.d.ts +180 -0
  33. package/lib/loop/plugins/app-page/appPage.js +443 -0
  34. package/lib/loop/plugins/app-page/index.d.ts +8 -0
  35. package/lib/loop/plugins/app-page/index.js +34 -0
  36. package/lib/loop/plugins/chat/ChatView.js +47 -25
  37. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
  38. package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
  39. package/lib/loop/plugins/page-layout/index.d.ts +13 -0
  40. package/lib/loop/plugins/page-layout/index.js +10 -1
  41. package/lib/loop/presets.d.ts +12 -0
  42. package/lib/loop/presets.js +5 -3
  43. package/lib/specs/actions.js +8 -0
  44. package/lib/specs/agents/agents.js +4 -0
  45. package/lib/specs/apps.d.ts +10 -1
  46. package/lib/specs/apps.js +1792 -178
  47. package/lib/specs/appspecSchema.js +1 -1
  48. package/lib/types/agentspecs.d.ts +11 -0
  49. package/lib/types/config.d.ts +12 -0
  50. package/package.json +2 -1
  51. package/scripts/codegen/generate_agents.py +50 -2
  52. package/scripts/codegen/generate_apps.py +36 -6
package/lib/AgentNode.js CHANGED
@@ -345,7 +345,8 @@ export function AgentNode() {
345
345
  ]);
346
346
  const [inferenceProvider, setInferenceProvider] = useState('datalayer');
347
347
  const [inferenceModels, setInferenceModels] = useState([]);
348
- const [inferenceDefaultModel, setInferenceDefaultModel] = useState(null);
348
+ // The runtime's sentence about its models, or why it gave none.
349
+ const [inferenceModelsNote, setInferenceModelsNote] = useState(null);
349
350
  const [configurationLoaded, setConfigurationLoaded] = useState(false);
350
351
  const [isActiveAgentRunning, setIsActiveAgentRunning] = useState(false);
351
352
  // The protocol the active agent actually speaks, as reported by the node.
@@ -712,49 +713,26 @@ export function AgentNode() {
712
713
  useEffect(() => {
713
714
  if (inferenceProvider !== 'datalayer') {
714
715
  setInferenceModels([]);
715
- setInferenceDefaultModel(null);
716
+ setInferenceModelsNote(null);
716
717
  return;
717
718
  }
719
+ // The runtime's answer, as it had it from ai-inference: no list of its
720
+ // own when that answer is empty or did not come — the sentence says why.
718
721
  const loadInferenceModels = async () => {
719
722
  try {
720
723
  const response = await fetch(`${AGENT_RUNTIMES_BASE_URL}/api/v1/configure/inference/models`);
721
724
  if (!response.ok) {
722
725
  setInferenceModels([]);
723
- setInferenceDefaultModel(null);
726
+ setInferenceModelsNote(`The runtime did not list its models (${response.status}).`);
724
727
  return;
725
728
  }
726
729
  const payload = await response.json();
727
- const fromModels = Array.isArray(payload.models)
728
- ? payload.models.filter(Boolean)
729
- : [];
730
- const fromBedrock = Array.isArray(payload.bedrock_anthropic_models)
731
- ? payload.bedrock_anthropic_models.filter(Boolean)
732
- : [];
733
- const fallback = [
734
- 'bedrock/us.anthropic.claude-3-5-sonnet-20240620-v1:0',
735
- 'bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0',
736
- 'bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0',
737
- ];
738
- const models = fromModels.length > 0
739
- ? fromModels
740
- : fromBedrock.length > 0
741
- ? fromBedrock
742
- : fallback;
743
- setInferenceModels(models);
744
- const specDefault = Array.isArray(payload.bedrock_anthropic_model_specs)
745
- ? payload.bedrock_anthropic_model_specs.find(s => s?.default)?.id
746
- : undefined;
747
- const selected = (specDefault && models.includes(specDefault) ? specDefault : null) ||
748
- (payload.default_model && models.includes(payload.default_model)
749
- ? payload.default_model
750
- : null) ||
751
- models[0] ||
752
- null;
753
- setInferenceDefaultModel(selected);
730
+ setInferenceModels(Array.isArray(payload.models) ? payload.models.filter(Boolean) : []);
731
+ setInferenceModelsNote(payload.note ?? null);
754
732
  }
755
- catch {
733
+ catch (error) {
756
734
  setInferenceModels([]);
757
- setInferenceDefaultModel(null);
735
+ setInferenceModelsNote(`The runtime did not list its models (${String(error)}).`);
758
736
  }
759
737
  };
760
738
  loadInferenceModels();
@@ -1280,7 +1258,10 @@ export function AgentNode() {
1280
1258
  display: 'flex',
1281
1259
  flexDirection: 'column',
1282
1260
  gap: 1,
1283
- }, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Bedrock Anthropic model" }), inferenceModels.length === 0 ? (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "No model list available." })) : (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { children: inferenceDefaultModel || inferenceModels[0] }), _jsx(ActionMenu.Overlay, { width: "large", children: _jsx(ActionList, { selectionVariant: "single", children: inferenceModels.map(model => (_jsx(ActionList.Item, { selected: inferenceDefaultModel === model, inactiveText: "Selection is locked", children: model }, model))) }) })] }))] }))] }), iamUser ? (_jsxs(Box, { sx: {
1261
+ }, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Models ai-inference serves" }), inferenceModels.length === 0 ? (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: inferenceModelsNote ??
1262
+ 'The runtime has not said which models ai-inference serves.' })) : (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { children: inferenceModels.length === 1
1263
+ ? inferenceModels[0]
1264
+ : `${inferenceModels.length} models` }), _jsx(ActionMenu.Overlay, { width: "large", children: _jsx(ActionList, { children: inferenceModels.map(model => (_jsx(ActionList.Item, { inactiveText: "Selection is locked", children: model }, model))) }) })] }))] }))] }), iamUser ? (_jsxs(Box, { sx: {
1284
1265
  border: '1px solid',
1285
1266
  borderColor: 'border.default',
1286
1267
  borderRadius: 2,
@@ -48,7 +48,7 @@ import { InputPrompt } from '../prompt/InputPrompt';
48
48
  import { isFloatingChatViewMode } from '../viewModes';
49
49
  import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
50
50
  import { EphemeralNotebook } from '../notebook/EphemeralNotebook';
51
- import { initialModelId, isOffered, usableModels } from './modelChoice';
51
+ import { initialModelId, isOffered, offeredModels as offeredModelsFor, } from './modelChoice';
52
52
  // EphemeralDocument statically imports `@datalayer/jupyter-lexical` (which
53
53
  // initialises Lumino-backed nodes on load). Lazy-load it so notebook-only chats
54
54
  // never pull lexical into the bundle or trigger its module side effects.
@@ -1380,37 +1380,46 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
1380
1380
  : undefined;
1381
1381
  const offeredModels = useMemo(() => {
1382
1382
  // Only what can actually be called — see `modelChoice` for the rule.
1383
- const fromConfig = availableModels || configQuery.data?.models;
1384
- if (fromConfig?.length) {
1385
- return usableModels(fromConfig);
1386
- }
1387
- /*
1388
- * The catalogue, filtered to what is worth offering.
1389
- *
1390
- * A server tells the chat which models it can reach and that answer wins.
1391
- * Without one — an in-page agent, or a server that has not answered yet —
1392
- * this used to offer the single model the protocol was built with, which
1393
- * is a menu with nothing to choose.
1394
- *
1395
- * `available` is the filter, not the whole catalogue: twenty-six models,
1396
- * most of them superseded, is a list nobody reads. The specs say which
1397
- * four are current.
1398
- */
1399
- const catalogued = Object.values(AI_MODEL_CATALOGUE)
1400
- // A typed-judgment model answers typed questions, not a chat.
1401
- .filter(model => model.available && isChatModel(model))
1402
- .map(model => ({
1403
- id: model.id,
1404
- name: model.name,
1405
- provider: model.provider,
1406
- }));
1407
- if (catalogued.length > 0) {
1408
- return usableModels(catalogued);
1409
- }
1410
- return browserModel
1411
- ? [{ id: browserModel, name: browserModel, provider: 'inference' }]
1412
- : [];
1413
- }, [availableModels, configQuery.data?.models, browserModel]);
1383
+ // The runtime's answer, when it gave one, is the whole list.
1384
+ // A runtime that was asked and did not answer offers nothing: the
1385
+ // agentspecs catalogue below is not its answer.
1386
+ const fromConfig = availableModels ||
1387
+ configQuery.data?.models ||
1388
+ (configQuery.isError ? [] : undefined);
1389
+ return offeredModelsFor(fromConfig, () => {
1390
+ /*
1391
+ * The catalogue, filtered to what is worth offering.
1392
+ *
1393
+ * A server tells the chat which models it can reach and that answer wins.
1394
+ * Without one — an in-page agent, or a server that has not answered yet —
1395
+ * this used to offer the single model the protocol was built with, which
1396
+ * is a menu with nothing to choose.
1397
+ *
1398
+ * `available` is the filter, not the whole catalogue: twenty-six models,
1399
+ * most of them superseded, is a list nobody reads. The specs say which
1400
+ * four are current.
1401
+ */
1402
+ const catalogued = Object.values(AI_MODEL_CATALOGUE)
1403
+ // A typed-judgment model answers typed questions, not a chat.
1404
+ .filter(model => model.available && isChatModel(model))
1405
+ .map(model => ({
1406
+ id: model.id,
1407
+ name: model.name,
1408
+ provider: model.provider,
1409
+ }));
1410
+ if (catalogued.length > 0) {
1411
+ return catalogued;
1412
+ }
1413
+ return browserModel
1414
+ ? [{ id: browserModel, name: browserModel, provider: 'inference' }]
1415
+ : [];
1416
+ });
1417
+ }, [
1418
+ availableModels,
1419
+ configQuery.data?.models,
1420
+ configQuery.isError,
1421
+ browserModel,
1422
+ ]);
1414
1423
  /*
1415
1424
  * Who `@` may address.
1416
1425
  *
@@ -26,6 +26,17 @@ export declare const isUsable: (model: ModelConfig) => boolean;
26
26
  * while the rows and their reasons say which key is missing.
27
27
  */
28
28
  export declare function usableModels(models: ModelConfig[]): ModelConfig[];
29
+ /**
30
+ * The models a chat offers, given what its runtime answered.
31
+ *
32
+ * The runtime's answer is the list: an agent's own models (its `model` and
33
+ * `model_additionals`), each flagged with whether its inference serves it.
34
+ * When the runtime answered, nothing else is added — not even when its list
35
+ * is empty, which says the agent has nothing to switch to. `fallback` is
36
+ * asked only when there is no runtime to answer (an in-page agent) or it
37
+ * has not answered yet.
38
+ */
39
+ export declare function offeredModels(fromRuntime: ModelConfig[] | undefined, fallback: () => ModelConfig[]): ModelConfig[];
29
40
  /** Whether `id` is on offer and usable — the selection may stand. */
30
41
  export declare const isOffered: (offered: ModelConfig[], id?: string) => boolean;
31
42
  /**
@@ -59,4 +70,6 @@ export type ServerCatalogueModel = {
59
70
  */
60
71
  export declare function readServerCatalogue(payload: {
61
72
  models?: ServerCatalogueModel[];
73
+ source?: string;
74
+ note?: string;
62
75
  } | null | undefined): ModelConfig[];
@@ -14,6 +14,19 @@ export function usableModels(models) {
14
14
  const usable = models.filter(isUsable);
15
15
  return usable.length > 0 ? usable : models;
16
16
  }
17
+ /**
18
+ * The models a chat offers, given what its runtime answered.
19
+ *
20
+ * The runtime's answer is the list: an agent's own models (its `model` and
21
+ * `model_additionals`), each flagged with whether its inference serves it.
22
+ * When the runtime answered, nothing else is added — not even when its list
23
+ * is empty, which says the agent has nothing to switch to. `fallback` is
24
+ * asked only when there is no runtime to answer (an in-page agent) or it
25
+ * has not answered yet.
26
+ */
27
+ export function offeredModels(fromRuntime, fallback) {
28
+ return usableModels(fromRuntime ?? fallback());
29
+ }
17
30
  /** Whether `id` is on offer and usable — the selection may stand. */
18
31
  export const isOffered = (offered, id) => Boolean(id) && offered.some(m => m.id === id && isUsable(m));
19
32
  /**
@@ -1,6 +1,7 @@
1
1
  import type { ReactorPlugin } from '@datalayer/reactor';
2
2
  import type { AppSpec } from '../../types/agentspecs';
3
3
  import { type LoopEmbedProps } from '../embed/LoopEmbed';
4
+ import type { LoopPresetOptions } from '../presets';
4
5
  import type { PresenceState } from '../../chat/presence/presenceStatus';
5
6
  /** The id of an agent or a Cog, without its version. */
6
7
  export declare const agentIdOf: (app: Pick<AppSpec, "agent" | "team">) => string;
@@ -37,7 +38,38 @@ export type AppRendererProps = Omit<LoopEmbedProps, 'agentId'> & {
37
38
  * stable function, such as a state setter.
38
39
  */
39
40
  onPresence?: (state: PresenceState) => void;
41
+ /**
42
+ * Draw the application in a window (the `window-frame` plugin), its title
43
+ * the application's face and name. A host's own `frameTitle` wins.
44
+ */
45
+ frame?: boolean;
40
46
  };
47
+ /** What `interface.layout` sets on the workspace. */
48
+ export type AppLayoutOptions = Pick<LoopPresetOptions, 'editors' | 'showViewSelector' | 'defaultEditor' | 'pageLayout' | 'pageLayoutArrangement' | 'pageLayoutPrompt' | 'pageLayoutPromptAnchor' | 'pageLayoutTurnPanelFooter'>;
49
+ /**
50
+ * The workspace an application's layout asks for (LOOP T-07).
51
+ *
52
+ * - `chat`: the conversation alone — no editor, no page, no strip.
53
+ * - `page`: the page layout — its page (or, without one, the editors) on the
54
+ * sheet; the composer a card over it, starting at the bottom, whose display
55
+ * modes put it in the conversation panel beside, over or in the corner of
56
+ * the page; the current turn under the composer, its copy and dismiss only.
57
+ * - `split`: the page layout's split — the conversation on the left with its
58
+ * composer, the page on the right, a hairline between them to drag.
59
+ *
60
+ * With a page, the page is the one editor, opened, without a strip to swap
61
+ * it; without one (a decision, whose page is the Studio's), the editors stay.
62
+ */
63
+ export declare function appLayoutOptions(app: Pick<AppSpec, 'kind' | 'interface' | 'agent'>): AppLayoutOptions;
64
+ /**
65
+ * What an application's agent is created with on a Datalayer runtime: under
66
+ * the application's id, over AG-UI, with the application's own document —
67
+ * so that the runtime registers it and decides every tool call by its rules
68
+ * (LOOP R-03, R-05), as `loop apps run --cloud` does. One payload for every
69
+ * place it runs: the workspace (`AppRenderer`) and the embed's floating
70
+ * modes (`AppEmbed`).
71
+ */
72
+ export declare function appDatalayerCreatePayload(app: AppSpec, instance?: AppInstance): Record<string, unknown>;
41
73
  /**
42
74
  * An application in a LOOP workspace: its agent, its starters, its layout.
43
75
  *
@@ -47,5 +79,5 @@ export type AppRendererProps = Omit<LoopEmbedProps, 'agentId'> & {
47
79
  /** No host plugins: one array, so that it never reads as a change. */
48
80
  /** The agentspec a runtime is allocated with before an application's agent is created on it. */
49
81
  export declare const DATALAYER_BOOTSTRAP_AGENTSPEC = "example-simple";
50
- export declare function AppRenderer({ app, plugins, instance, onPresence, ...embed }: AppRendererProps): React.JSX.Element;
82
+ export declare function AppRenderer({ app, plugins, instance, onPresence, frame, ...embed }: AppRendererProps): React.JSX.Element;
51
83
  export default AppRenderer;
@@ -16,13 +16,26 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
16
16
  * created with the application in its payload — so the runtime reaches only
17
17
  * what the application connects to and enforces its rules before every tool
18
18
  * call — and the application's starters as the openers of the empty chat;
19
- * - the **layout of its kind**: a chat is the conversation alone, with the
20
- * A2UI surfaces its answers draw; the other layouts keep the editors.
19
+ * - its **layout**, as `interface.layout` says (`appLayoutOptions`, LOOP
20
+ * T-07), on the workspace's own `page-layout` plugin: `chat` is the
21
+ * conversation alone, with the A2UI surfaces its answers draw; `page` is
22
+ * the page on a sheet with the composer over it and the conversation in a
23
+ * panel the composer's display modes open; `split` is the conversation and
24
+ * the page side by side, a hairline between them to drag;
25
+ * - its **page**, when it has one (`hasAppPage`: a chat, a widget or a worker
26
+ * with a `page` or `split` layout): its A2UI surface drawn by the
27
+ * `app-page` plugin, in place of the notebook and the document, fed from
28
+ * the conversation and answered through it (`APP_KIND_PATHS`). A
29
+ * decision's page stays the Studio's run page (R-02); its layout keeps the
30
+ * editors;
31
+ * - its **frame**, when the host asks (`frame`): the `window-frame` plugin's
32
+ * window, its title the application's face and name.
21
33
  *
22
34
  * @module loop/apps/AppRenderer
23
35
  */
24
36
  import { useMemo } from 'react';
25
37
  import { defineAgentCapacityPlugin } from '../plugins/agent-capacity';
38
+ import { APP_PAGE_SURFACE, defineAppPagePlugin, hasAppPage, } from '../plugins/app-page';
26
39
  import { LoopEmbed } from '../embed/LoopEmbed';
27
40
  import { dumpAppspec } from './appspec';
28
41
  /** The id of an agent or a Cog, without its version. */
@@ -64,8 +77,75 @@ export function defineAppPlugin(app) {
64
77
  text: starter.label,
65
78
  message: starter.message,
66
79
  })),
80
+ // In the page, the agent is told what a runtime tells it: its own
81
+ // spec's prompt, then the application's instructions; on the
82
+ // application's model when it names one (`loop/apps/agent`).
83
+ instructions: app.instructions,
84
+ model: app.model,
67
85
  });
68
86
  }
87
+ /**
88
+ * The workspace an application's layout asks for (LOOP T-07).
89
+ *
90
+ * - `chat`: the conversation alone — no editor, no page, no strip.
91
+ * - `page`: the page layout — its page (or, without one, the editors) on the
92
+ * sheet; the composer a card over it, starting at the bottom, whose display
93
+ * modes put it in the conversation panel beside, over or in the corner of
94
+ * the page; the current turn under the composer, its copy and dismiss only.
95
+ * - `split`: the page layout's split — the conversation on the left with its
96
+ * composer, the page on the right, a hairline between them to drag.
97
+ *
98
+ * With a page, the page is the one editor, opened, without a strip to swap
99
+ * it; without one (a decision, whose page is the Studio's), the editors stay.
100
+ */
101
+ export function appLayoutOptions(app) {
102
+ const layout = app.interface.layout;
103
+ if (layout === 'chat') {
104
+ return { editors: false, showViewSelector: false };
105
+ }
106
+ const withPage = hasAppPage(app);
107
+ return {
108
+ editors: !withPage,
109
+ showViewSelector: !withPage,
110
+ ...(withPage ? { defaultEditor: APP_PAGE_SURFACE } : {}),
111
+ pageLayout: true,
112
+ pageLayoutArrangement: layout === 'split' ? 'split' : 'page',
113
+ pageLayoutPrompt: 'floating',
114
+ pageLayoutPromptAnchor: 'bottom',
115
+ pageLayoutTurnPanelFooter: 'actions',
116
+ };
117
+ }
118
+ /**
119
+ * What an application's agent is created with on a Datalayer runtime: under
120
+ * the application's id, over AG-UI, with the application's own document —
121
+ * so that the runtime registers it and decides every tool call by its rules
122
+ * (LOOP R-03, R-05), as `loop apps run --cloud` does. One payload for every
123
+ * place it runs: the workspace (`AppRenderer`) and the embed's floating
124
+ * modes (`AppEmbed`).
125
+ */
126
+ export function appDatalayerCreatePayload(app, instance) {
127
+ return {
128
+ // Under the application's id, over AG-UI: what the workspace's
129
+ // chat addresses (`AppRenderer` gives it as `agentId`).
130
+ name: app.id,
131
+ transport: 'ag-ui',
132
+ agent_spec_id: agentIdOf(app),
133
+ app_spec: dumpAppspec(app),
134
+ // Code Mode calls every tool through `execute_code`, which an
135
+ // application without a shell is refused: its tools are called
136
+ // one by one instead, each decided by its rules.
137
+ enable_codemode: Boolean(app.permissions?.computer?.shell),
138
+ ...(instance
139
+ ? {
140
+ app_instance: {
141
+ app_uid: instance.appUid ?? '',
142
+ deployment_uid: instance.deploymentUid ?? '',
143
+ version: instance.version ?? 0,
144
+ },
145
+ }
146
+ : {}),
147
+ };
148
+ }
69
149
  /**
70
150
  * An application in a LOOP workspace: its agent, its starters, its layout.
71
151
  *
@@ -76,8 +156,7 @@ export function defineAppPlugin(app) {
76
156
  /** The agentspec a runtime is allocated with before an application's agent is created on it. */
77
157
  export const DATALAYER_BOOTSTRAP_AGENTSPEC = 'example-simple';
78
158
  const NO_PLUGINS = [];
79
- export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, ...embed }) {
80
- const chatOnly = app.interface.layout === 'chat';
159
+ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, frame = false, ...embed }) {
81
160
  /*
82
161
  * The application plugin, made once per application. `LoopEmbed` rebuilds
83
162
  * its whole reactor when its plugins change, so a new plugin on every
@@ -88,36 +167,20 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, .
88
167
  const appPlugin = useMemo(() => (app.agent ? defineAppPlugin(app) : null),
89
168
  // eslint-disable-next-line react-hooks/exhaustive-deps
90
169
  [source]);
91
- const allPlugins = useMemo(() => (appPlugin ? [appPlugin, ...plugins] : plugins), [appPlugin, plugins]);
170
+ const withPage = hasAppPage(app);
171
+ const pagePlugin = useMemo(() => (withPage ? defineAppPagePlugin(app) : null),
172
+ // eslint-disable-next-line react-hooks/exhaustive-deps
173
+ [source, withPage]);
174
+ const allPlugins = useMemo(() => appPlugin
175
+ ? [appPlugin, ...(pagePlugin ? [pagePlugin] : []), ...plugins]
176
+ : plugins, [appPlugin, pagePlugin, plugins]);
92
177
  /*
93
178
  * On Datalayer, the application runs on a runtime: allocated with a plain
94
179
  * agentspec, its agent created there with the application's spec — so that
95
180
  * the runtime registers it and decides every tool call by its rules (LOOP
96
181
  * R-03, R-05), as `loop apps run --cloud` does.
97
182
  */
98
- const datalayerCreatePayload = useMemo(() => app.agent
99
- ? {
100
- // Under the application's id, over AG-UI: what the workspace's
101
- // chat addresses (`agentId` below).
102
- name: app.id,
103
- transport: 'ag-ui',
104
- agent_spec_id: agentIdOf(app),
105
- app_spec: dumpAppspec(app),
106
- // Code Mode calls every tool through `execute_code`, which an
107
- // application without a shell is refused: its tools are called
108
- // one by one instead, each decided by its rules.
109
- enable_codemode: Boolean(app.permissions?.computer?.shell),
110
- ...(instance
111
- ? {
112
- app_instance: {
113
- app_uid: instance.appUid ?? '',
114
- deployment_uid: instance.deploymentUid ?? '',
115
- version: instance.version ?? 0,
116
- },
117
- }
118
- : {}),
119
- }
120
- : undefined,
183
+ const datalayerCreatePayload = useMemo(() => (app.agent ? appDatalayerCreatePayload(app, instance) : undefined),
121
184
  // eslint-disable-next-line react-hooks/exhaustive-deps
122
185
  [source, instance?.appUid, instance?.deploymentUid, instance?.version]);
123
186
  if (!appPlugin) {
@@ -129,7 +192,11 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, .
129
192
  , {
130
193
  // The application's own agent: created under its id, so that its
131
194
  // spec is applied to an agent of its own, never to the one it extends.
132
- agentId: app.id, editors: !chatOnly, showViewSelector: !chatOnly, teamPicker: false, showAgentVariants: false, graph: false, pluginsPanel: false, datalayerAgentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
195
+ agentId: app.id, ...appLayoutOptions(app), ...(frame
196
+ ? {
197
+ frameTitle: [app.emoji, app.name].filter(Boolean).join(' '),
198
+ }
199
+ : {}), teamPicker: false, showAgentVariants: false, graph: false, pluginsPanel: false, datalayerAgentSpecId: DATALAYER_BOOTSTRAP_AGENTSPEC,
133
200
  // Where an application runs is decided by its host — the Studio's
134
201
  // Preview, the hosted page — not offered to its user.
135
202
  targetFixed: true,
@@ -0,0 +1,15 @@
1
+ import type { Agentspec } from '../../types/agentspecs';
2
+ /** An agent of the catalogue, or a Cog's agent, by `id` or `id:version`. */
3
+ export declare function resolveAgentspec(reference: string): Agentspec | undefined;
4
+ /**
5
+ * What the agent is told: its spec's system prompt, then what the
6
+ * application tells it on top — as the runtime joins them.
7
+ */
8
+ export declare function agentInstructions(spec: Pick<Agentspec, 'systemPrompt'> | undefined, extra?: string): string | undefined;
9
+ /**
10
+ * Why an agent turned in the page cannot run, or undefined when it can.
11
+ *
12
+ * An agent named by a reference that resolves to nothing would answer with
13
+ * no instructions — bare — which is not the agent that was asked for.
14
+ */
15
+ export declare function inPageAgentRefusal(reference: string | undefined, spec: Agentspec | undefined): string | undefined;
@@ -0,0 +1,42 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The agent an application runs, as the page turns it (LOOP H-01, E-11).
7
+ *
8
+ * On a runtime, an application's agent is its agent's spec — an agent of the
9
+ * catalogue, or a Cog's, which is an agent equipped with Frames, their
10
+ * context already in its system prompt — and the application's own
11
+ * instructions on top of it (`get_library_agent_spec` and the create route
12
+ * in `agent_runtimes/routes/agents.py`). The page turns the same agent in the
13
+ * browser, so it resolves it the same way: an application on a Cog that the
14
+ * page looked up among the agentspecs alone ran with no instructions at all.
15
+ *
16
+ * @module loop/apps/agent
17
+ */
18
+ import { getAgentspecs } from '../../specs/agents';
19
+ import { getCog } from '../../specs/cogs';
20
+ /** An agent of the catalogue, or a Cog's agent, by `id` or `id:version`. */
21
+ export function resolveAgentspec(reference) {
22
+ return getAgentspecs(reference) ?? getCog(reference)?.spec;
23
+ }
24
+ /**
25
+ * What the agent is told: its spec's system prompt, then what the
26
+ * application tells it on top — as the runtime joins them.
27
+ */
28
+ export function agentInstructions(spec, extra) {
29
+ const parts = [spec?.systemPrompt?.trim(), extra?.trim()].filter((part) => Boolean(part));
30
+ return parts.length > 0 ? parts.join('\n\n') : undefined;
31
+ }
32
+ /**
33
+ * Why an agent turned in the page cannot run, or undefined when it can.
34
+ *
35
+ * An agent named by a reference that resolves to nothing would answer with
36
+ * no instructions — bare — which is not the agent that was asked for.
37
+ */
38
+ export function inPageAgentRefusal(reference, spec) {
39
+ return reference && !spec
40
+ ? `There is no agent or Cog named “${reference}” in this page, so it cannot answer here.`
41
+ : undefined;
42
+ }
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * @module loop/apps
5
5
  */
6
+ export * from './agent';
6
7
  export * from './AppRenderer';
7
8
  export * from './appspec';
8
9
  export * from './checks';
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * @module loop/apps
9
9
  */
10
+ export * from './agent';
10
11
  export * from './AppRenderer';
11
12
  export * from './appspec';
12
13
  export * from './checks';
@@ -483,6 +483,14 @@ export type AgentBlueprintContribution = {
483
483
  specId?: string;
484
484
  /** Extra fields merged into the server's create-agent payload. */
485
485
  createPayload?: Record<string, unknown>;
486
+ /**
487
+ * What the agent is told on top of its spec's system prompt — an
488
+ * application's instructions — when the page turns it, as a runtime
489
+ * appends them.
490
+ */
491
+ instructions?: string;
492
+ /** The model it runs on in the page, in place of its spec's. */
493
+ model?: string;
486
494
  };
487
495
  export declare const LoopAgentBlueprint: import("@datalayer/reactor").ContributionPoint<AgentBlueprintContribution>;
488
496
  /** One conversation opener a plugin offers on the empty chat. */
@@ -0,0 +1,42 @@
1
+ import type { CSSProperties, JSX, ReactNode } from 'react';
2
+ import type { AppAccent, AppEmbedMode, AppSpec } from '../../types/agentspecs';
3
+ import { type AppInstance } from '../apps/AppRenderer';
4
+ import { type EmbedColorMode } from './embedConfig';
5
+ export type AppEmbedProps = {
6
+ /** The application. */
7
+ app: AppSpec;
8
+ /** How it sits in the page; the Appspec's `deployment.embedded.mode` by default. */
9
+ mode?: AppEmbedMode;
10
+ /** The host's accent; the application's own by default. */
11
+ accent?: AppAccent;
12
+ /** Light, dark, or the visitor's system. */
13
+ colorMode?: EmbedColorMode;
14
+ /** The host's face, a CSS font family; the theme's by default. */
15
+ font?: string;
16
+ /**
17
+ * An agent-runtimes server the application's agent runs on. Without one,
18
+ * it runs on a Datalayer runtime, launched with the visitor's Datalayer
19
+ * credentials.
20
+ */
21
+ serverUrl?: string;
22
+ /** What its record is kept under, on Datalayer. */
23
+ instance?: AppInstance;
24
+ /** Height of an inline application. */
25
+ height?: number | string;
26
+ /**
27
+ * Draw overlays — menus, dialogs — inside the embed's own root rather than
28
+ * in Primer's, in the host's `<body>`: what the element asks for, since a
29
+ * shadow root's styles stop at its edge. A React host leaves it off.
30
+ */
31
+ ownPortal?: boolean;
32
+ };
33
+ /** The `loop` theme, with the application's (or the host's) accent and face. */
34
+ export declare function EmbedThemed({ accent, colorMode, font, children, style, }: {
35
+ accent: AppAccent;
36
+ colorMode: 'light' | 'dark';
37
+ font?: string;
38
+ children: ReactNode;
39
+ style?: CSSProperties;
40
+ }): JSX.Element;
41
+ export declare function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, height, ownPortal, }: AppEmbedProps): JSX.Element;
42
+ export default AppEmbed;