@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.
- package/lib/AgentNode.js +14 -33
- package/lib/chat/base/ChatBase.js +41 -32
- package/lib/chat/base/modelChoice.d.ts +13 -0
- package/lib/chat/base/modelChoice.js +13 -0
- package/lib/loop/apps/AppRenderer.d.ts +33 -1
- package/lib/loop/apps/AppRenderer.js +96 -29
- package/lib/loop/apps/agent.d.ts +15 -0
- package/lib/loop/apps/agent.js +42 -0
- package/lib/loop/apps/index.d.ts +1 -0
- package/lib/loop/apps/index.js +1 -0
- package/lib/loop/core/index.d.ts +8 -0
- package/lib/loop/embed/AppEmbed.d.ts +42 -0
- package/lib/loop/embed/AppEmbed.js +205 -0
- package/lib/loop/embed/datalayer-app.d.ts +1 -0
- package/lib/loop/embed/datalayer-app.js +16 -0
- package/lib/loop/embed/element.d.ts +57 -0
- package/lib/loop/embed/element.js +405 -0
- package/lib/loop/embed/embedConfig.d.ts +120 -0
- package/lib/loop/embed/embedConfig.js +148 -0
- package/lib/loop/embed/embedTheme.d.ts +24 -0
- package/lib/loop/embed/embedTheme.js +83 -0
- package/lib/loop/index.d.ts +6 -2
- package/lib/loop/index.js +8 -2
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +21 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +34 -6
- package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
- package/lib/loop/plugins/agent-capacity/index.js +3 -1
- package/lib/loop/plugins/agents/switchable.d.ts +7 -0
- package/lib/loop/plugins/agents/switchable.js +91 -88
- package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
- package/lib/loop/plugins/app-page/AppPage.js +53 -0
- package/lib/loop/plugins/app-page/appPage.d.ts +180 -0
- package/lib/loop/plugins/app-page/appPage.js +443 -0
- package/lib/loop/plugins/app-page/index.d.ts +8 -0
- package/lib/loop/plugins/app-page/index.js +34 -0
- package/lib/loop/plugins/chat/ChatView.js +47 -25
- package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
- package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
- package/lib/loop/plugins/page-layout/index.d.ts +13 -0
- package/lib/loop/plugins/page-layout/index.js +10 -1
- package/lib/loop/presets.d.ts +12 -0
- package/lib/loop/presets.js +5 -3
- package/lib/specs/actions.js +8 -0
- package/lib/specs/agents/agents.js +4 -0
- package/lib/specs/apps.d.ts +10 -1
- package/lib/specs/apps.js +1792 -178
- package/lib/specs/appspecSchema.js +1 -1
- package/lib/types/agentspecs.d.ts +11 -0
- package/lib/types/config.d.ts +12 -0
- package/package.json +2 -1
- package/scripts/codegen/generate_agents.py +50 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
726
|
+
setInferenceModelsNote(`The runtime did not list its models (${response.status}).`);
|
|
724
727
|
return;
|
|
725
728
|
}
|
|
726
729
|
const payload = await response.json();
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
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: "
|
|
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,
|
|
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
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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
|
-
* -
|
|
20
|
-
*
|
|
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
|
|
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,
|
|
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
|
+
}
|
package/lib/loop/apps/index.d.ts
CHANGED
package/lib/loop/apps/index.js
CHANGED
package/lib/loop/core/index.d.ts
CHANGED
|
@@ -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;
|