@datalayer/agent-runtimes 1.3.87 → 1.3.89
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/components/a2ui/datalayer/Download.d.ts +21 -0
- package/lib/components/a2ui/datalayer/Download.js +66 -0
- package/lib/components/a2ui/datalayer/drawn.d.ts +1 -1
- package/lib/components/a2ui/datalayer/drawn.js +1 -0
- package/lib/components/a2ui/datalayer/index.d.ts +2 -1
- package/lib/components/a2ui/datalayer/index.js +3 -0
- package/lib/components/inspector/a2aSpans.d.ts +9 -0
- package/lib/components/inspector/a2aSpans.js +11 -1
- package/lib/components/teams/AnswerSurfaces.d.ts +56 -0
- package/lib/components/teams/AnswerSurfaces.js +27 -0
- package/lib/components/teams/AnswerSurfacesView.d.ts +14 -0
- package/lib/components/teams/AnswerSurfacesView.js +30 -0
- package/lib/components/teams/index.d.ts +1 -0
- package/lib/components/teams/index.js +1 -0
- package/lib/components/teams/useA2ATeam.d.ts +56 -2
- package/lib/components/teams/useA2ATeam.js +122 -3
- package/lib/hooks/useAgentRuntimes.d.ts +13 -0
- package/lib/hooks/useAgentRuntimes.js +11 -1
- package/lib/loop/apps/AppFeedback.js +11 -2
- package/lib/loop/apps/AppRenderer.d.ts +13 -1
- package/lib/loop/apps/AppRenderer.js +34 -1
- package/lib/loop/apps/AppRuntimePlugins.d.ts +8 -0
- package/lib/loop/apps/AppRuntimePlugins.js +87 -0
- package/lib/loop/apps/feedback.d.ts +7 -0
- package/lib/loop/apps/feedback.js +3 -0
- package/lib/loop/apps/pluginPair.d.ts +60 -0
- package/lib/loop/apps/pluginPair.js +44 -0
- package/lib/loop/apps/threads.d.ts +35 -0
- package/lib/loop/apps/threads.js +79 -0
- package/lib/loop/embed/LoopEmbed.d.ts +4 -3
- package/lib/loop/embed/hostBridge.d.ts +27 -0
- package/lib/loop/embed/hostBridge.js +47 -0
- package/lib/loop/index.d.ts +1 -1
- package/lib/loop/index.js +1 -1
- package/lib/loop/plugins/agent-capacity/index.d.ts +7 -0
- package/lib/loop/plugins/agent-capacity/index.js +3 -2
- package/lib/loop/plugins/agents/DatalayerAgentBridge.js +11 -0
- package/lib/loop/plugins/agents/appLaunch.d.ts +15 -0
- package/lib/loop/plugins/agents/appLaunch.js +28 -0
- package/lib/loop/plugins/chat/ChatView.js +177 -109
- package/lib/loop/shell/LoopWorkspace.d.ts +3 -3
- package/lib/loop/shell/LoopWorkspace.js +1 -1
- package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +10 -2
- package/lib/runtimes/actions.js +23 -0
- package/lib/runtimes/apis.d.ts +18 -0
- package/lib/runtimes/browser/a2aPeer.d.ts +19 -2
- package/lib/runtimes/browser/a2aPeer.js +13 -7
- package/lib/specs/apps.js +102 -30
- package/lib/specs/envvars.d.ts +2 -0
- package/lib/specs/envvars.js +22 -0
- package/lib/specs/outputs.d.ts +1 -0
- package/lib/specs/outputs.js +12 -0
- package/lib/specs/sceneSchema.js +12 -1
- package/lib/specs/scenes.js +238 -121
- package/lib/specs/teams/teams.js +23 -7
- package/lib/specs/uiPlugins.js +55 -0
- package/lib/stores/agentRuntimeStore.js +4 -0
- package/lib/types/chat.d.ts +6 -0
- package/lib/types/scenes.d.ts +7 -1
- package/package.json +1 -1
- package/scripts/codegen/generate_envvars.py +0 -1
- package/scripts/codegen/generate_mcp_servers.py +11 -7
|
@@ -362,7 +362,7 @@ function toAgentRuntimeData(raw) {
|
|
|
362
362
|
* ```
|
|
363
363
|
*/
|
|
364
364
|
export function useAgentRuntimes(options = {}) {
|
|
365
|
-
const { agentSpecId, agentConfig, autoCreateAgent = true, autoStart = false, agentSpec, variant: variantOption, runtimeCreationTarget, runtimeCreationBaseUrl, runtimeConnection, } = options;
|
|
365
|
+
const { agentSpecId, agentConfig, autoCreateAgent = true, autoStart = false, agentSpec, variant: variantOption, runtimeCreationTarget, runtimeCreationBaseUrl, runtimeConnection, appLaunch, } = options;
|
|
366
366
|
// Both vocabularies funnel into one value here, so no branch below has to
|
|
367
367
|
// remember that the old names exist.
|
|
368
368
|
const variant = toAgentRuntimeVariant(variantOption ?? runtimeCreationTarget);
|
|
@@ -484,12 +484,14 @@ export function useAgentRuntimes(options = {}) {
|
|
|
484
484
|
const conn = await storeLaunchAgent(runtimeOptions
|
|
485
485
|
? {
|
|
486
486
|
...runtimeOptions,
|
|
487
|
+
...appLaunch,
|
|
487
488
|
runtimesUrl: resolvedRuntimeCreationBaseUrl,
|
|
488
489
|
}
|
|
489
490
|
: {
|
|
490
491
|
environmentName: 'ai-agents-env',
|
|
491
492
|
creditsLimit: 10,
|
|
492
493
|
givenName: safeName,
|
|
494
|
+
...appLaunch,
|
|
493
495
|
runtimesUrl: resolvedRuntimeCreationBaseUrl,
|
|
494
496
|
});
|
|
495
497
|
setLifecycleStatus('ready');
|
|
@@ -515,6 +517,7 @@ export function useAgentRuntimes(options = {}) {
|
|
|
515
517
|
agentConfig?.name,
|
|
516
518
|
agentSpec?.name,
|
|
517
519
|
agentSpecId,
|
|
520
|
+
appLaunch,
|
|
518
521
|
hasSpec,
|
|
519
522
|
resolvedRuntimeCreationBaseUrl,
|
|
520
523
|
storeLaunchAgent,
|
|
@@ -654,6 +657,12 @@ export function useAgentRuntimes(options = {}) {
|
|
|
654
657
|
if (!hasSpec || runtime || lifecycleStatus !== 'idle') {
|
|
655
658
|
return;
|
|
656
659
|
}
|
|
660
|
+
if (appLaunch) {
|
|
661
|
+
// An application's runtime is launched for it (R-19): a running one
|
|
662
|
+
// was not given its connections' secrets, so none is looked for.
|
|
663
|
+
setLookedForExisting(true);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
657
666
|
let cancelled = false;
|
|
658
667
|
const bootstrap = async () => {
|
|
659
668
|
try {
|
|
@@ -736,6 +745,7 @@ export function useAgentRuntimes(options = {}) {
|
|
|
736
745
|
lifecycleStatus,
|
|
737
746
|
getAuthHeaders,
|
|
738
747
|
agentSpecId,
|
|
748
|
+
appLaunch,
|
|
739
749
|
storeConnectAgent,
|
|
740
750
|
]);
|
|
741
751
|
// Reset agent creation tracking on disconnect
|
|
@@ -6,13 +6,17 @@ import { contribution, definePlugin, signal } from '@datalayer/reactor';
|
|
|
6
6
|
import { useContributions, useSignalValue } from '@datalayer/reactor/react';
|
|
7
7
|
import { useIAMStore } from '../../state';
|
|
8
8
|
import { LoopChatTurn, LoopPromptPanel, } from '../core';
|
|
9
|
-
import { FEEDBACK_WORDS, sendFeedback } from './feedback';
|
|
9
|
+
import { answerIdOf, FEEDBACK_WORDS, sendFeedback } from './feedback';
|
|
10
10
|
export const APP_FEEDBACK_PLUGIN_NAME = '@datalayer/loop-plugin-app-feedback';
|
|
11
11
|
/* A signal to read when no chat contributed a turn: the hook needs one. */
|
|
12
12
|
const NO_TURN = signal({ id: 0, status: 'idle' });
|
|
13
|
+
const NO_CONVERSATION = signal([]);
|
|
13
14
|
export function AppFeedback({ app, workspace, }) {
|
|
14
15
|
const turns = useContributions(LoopChatTurn);
|
|
15
16
|
const turn = useSignalValue(turns[0]?.value.turn ?? NO_TURN);
|
|
17
|
+
// Which answer it is about (P-24): its place among the conversation's answers.
|
|
18
|
+
const conversation = useSignalValue(turns[0]?.value.conversation ?? NO_CONVERSATION);
|
|
19
|
+
const answers = conversation.filter(entry => entry.role === 'assistant').length;
|
|
16
20
|
const token = useIAMStore(state => state.token);
|
|
17
21
|
const [liked, setLiked] = useState(null);
|
|
18
22
|
const [comment, setComment] = useState('');
|
|
@@ -31,7 +35,12 @@ export function AppFeedback({ app, workspace, }) {
|
|
|
31
35
|
}
|
|
32
36
|
setSaid({ turn: turn.id, state: 'sending', says: '' });
|
|
33
37
|
try {
|
|
34
|
-
await sendFeedback({
|
|
38
|
+
await sendFeedback({
|
|
39
|
+
session: thread,
|
|
40
|
+
liked: choice,
|
|
41
|
+
comment,
|
|
42
|
+
message: answerIdOf(answers),
|
|
43
|
+
}, { agentBaseUrl, token });
|
|
35
44
|
setSaid({
|
|
36
45
|
turn: turn.id,
|
|
37
46
|
state: 'kept',
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* @module loop/apps/AppRenderer
|
|
46
46
|
*/
|
|
47
47
|
import { type ComponentType } from 'react';
|
|
48
|
-
import type
|
|
48
|
+
import { type PluginRef, type ReactorExtension, type ReactorPlugin } from '@datalayer/reactor';
|
|
49
49
|
import type { AppSpec } from '../../types/agentspecs';
|
|
50
50
|
import type { ThemeOverrides } from '../../types/chat';
|
|
51
51
|
import { type AppHostPageProps } from '../plugins/app-page';
|
|
@@ -58,6 +58,11 @@ export declare const agentIdOf: (app: Pick<AppSpec, "agent" | "team">) => string
|
|
|
58
58
|
/**
|
|
59
59
|
* The plugin that makes a LOOP workspace run an application.
|
|
60
60
|
*
|
|
61
|
+
* Its page plugin of the pair (LOOP F-15): named `loop-app-<id>`, as its
|
|
62
|
+
* runtime's plugin is, and requiring it — so that it stands down while its
|
|
63
|
+
* runtime does not hold the application, and comes back when it does
|
|
64
|
+
* (`AppRuntimePlugins` follows the runtime).
|
|
65
|
+
*
|
|
61
66
|
* The agent is created with the application's own document (`app_spec`) in
|
|
62
67
|
* its payload; a runtime that knows applications configures the agent from
|
|
63
68
|
* it, and one that does not creates the agent alone — the application is
|
|
@@ -68,6 +73,13 @@ export declare const agentIdOf: (app: Pick<AppSpec, "agent" | "team">) => string
|
|
|
68
73
|
* is not an agent spec, and is refused here rather than launched as one.
|
|
69
74
|
*/
|
|
70
75
|
export declare function defineAppPlugin(app: AppSpec, shown?: AppSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
76
|
+
/**
|
|
77
|
+
* The application on the page: its plugins delivered as one extension named
|
|
78
|
+
* as its plugin pair is, `loop-app-<id>` — the extension its runtime's plugin
|
|
79
|
+
* says too, so that Reactor's manager ("Delivered by") and graph show the
|
|
80
|
+
* page's plugins and the runtime's as one application (LOOP F-15).
|
|
81
|
+
*/
|
|
82
|
+
export declare function appExtension(app: AppSpec, shown: AppSpec, plugins: PluginRef[]): ReactorExtension;
|
|
71
83
|
/** Which instance of an application runs, as the platform knows it (LOOP R-07). */
|
|
72
84
|
export type AppInstance = {
|
|
73
85
|
/** Its `app` item. */
|
|
@@ -50,6 +50,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
50
50
|
* @module loop/apps/AppRenderer
|
|
51
51
|
*/
|
|
52
52
|
import { useMemo } from 'react';
|
|
53
|
+
import { defineExtension, } from '@datalayer/reactor';
|
|
53
54
|
import { loopAccentStyles, useSystemColorMode, useThemeStore, } from '@datalayer/primer-addons';
|
|
54
55
|
import { defineAgentCapacityPlugin } from '../plugins/agent-capacity';
|
|
55
56
|
import { APP_PAGE_SURFACE, defineAppHostPagePlugin, defineAppPagePlugin, hasAppPage, } from '../plugins/app-page';
|
|
@@ -69,6 +70,8 @@ import { defineAppKeptPlugin } from './AppKept';
|
|
|
69
70
|
import { keepsFeedback } from './feedback';
|
|
70
71
|
import { keptBeforeFirstMessage } from './kept';
|
|
71
72
|
import { ChatLanguage } from '../../chat/ChatLanguage';
|
|
73
|
+
import { defineAppRuntimePlugin } from './AppRuntimePlugins';
|
|
74
|
+
import { appPluginPair } from './pluginPair';
|
|
72
75
|
/** The id of an agent or a Cog, without its version. */
|
|
73
76
|
export const agentIdOf = (app) => {
|
|
74
77
|
const reference = app.agent || app.team;
|
|
@@ -80,6 +83,11 @@ export const agentIdOf = (app) => {
|
|
|
80
83
|
/**
|
|
81
84
|
* The plugin that makes a LOOP workspace run an application.
|
|
82
85
|
*
|
|
86
|
+
* Its page plugin of the pair (LOOP F-15): named `loop-app-<id>`, as its
|
|
87
|
+
* runtime's plugin is, and requiring it — so that it stands down while its
|
|
88
|
+
* runtime does not hold the application, and comes back when it does
|
|
89
|
+
* (`AppRuntimePlugins` follows the runtime).
|
|
90
|
+
*
|
|
83
91
|
* The agent is created with the application's own document (`app_spec`) in
|
|
84
92
|
* its payload; a runtime that knows applications configures the agent from
|
|
85
93
|
* it, and one that does not creates the agent alone — the application is
|
|
@@ -93,8 +101,11 @@ export function defineAppPlugin(app, shown = app) {
|
|
|
93
101
|
if (!app.agent) {
|
|
94
102
|
throw new Error(`“${app.name}” is run by a team, which the workspace does not run as an application yet.`);
|
|
95
103
|
}
|
|
104
|
+
const pair = appPluginPair(app.id);
|
|
96
105
|
return defineAgentCapacityPlugin({
|
|
97
106
|
key: `app-${app.id}`,
|
|
107
|
+
name: pair.page.name,
|
|
108
|
+
requiredBackendPlugins: pair.page.requiredBackendPlugins,
|
|
98
109
|
// What a person reads is in their language (P-26); what it is created
|
|
99
110
|
// with is its own Appspec.
|
|
100
111
|
displayName: shown.name,
|
|
@@ -128,6 +139,22 @@ export function defineAppPlugin(app, shown = app) {
|
|
|
128
139
|
: {}),
|
|
129
140
|
});
|
|
130
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The application on the page: its plugins delivered as one extension named
|
|
144
|
+
* as its plugin pair is, `loop-app-<id>` — the extension its runtime's plugin
|
|
145
|
+
* says too, so that Reactor's manager ("Delivered by") and graph show the
|
|
146
|
+
* page's plugins and the runtime's as one application (LOOP F-15).
|
|
147
|
+
*/
|
|
148
|
+
export function appExtension(app, shown, plugins) {
|
|
149
|
+
return defineExtension({
|
|
150
|
+
name: appPluginPair(app.id).extension,
|
|
151
|
+
displayName: shown.name,
|
|
152
|
+
description: shown.description || `The ${shown.name} application`,
|
|
153
|
+
emoji: app.emoji,
|
|
154
|
+
version: app.version,
|
|
155
|
+
plugins,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
131
158
|
/**
|
|
132
159
|
* The workspace an application's layout asks for (LOOP T-07).
|
|
133
160
|
*
|
|
@@ -205,6 +232,8 @@ export function appPreset(app, options = {}) {
|
|
|
205
232
|
return {
|
|
206
233
|
plugins: [
|
|
207
234
|
defineAppPlugin(app, shown),
|
|
235
|
+
// What its runtime holds, told to the page (F-15).
|
|
236
|
+
defineAppRuntimePlugin(app.id),
|
|
208
237
|
...(withPage ? [defineAppPagePlugin(shown), ...blocks] : []),
|
|
209
238
|
// The components its developer wrote (LOOP P-17): blocks of its page
|
|
210
239
|
// and renderers of its page, its elements and its answers — its own.
|
|
@@ -353,7 +382,11 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
|
|
|
353
382
|
deployed,
|
|
354
383
|
visitor,
|
|
355
384
|
]);
|
|
356
|
-
const allPlugins = useMemo(() =>
|
|
385
|
+
const allPlugins = useMemo(() => 'problem' in preset
|
|
386
|
+
? plugins
|
|
387
|
+
: [appExtension(app, shown, preset.plugins), ...plugins],
|
|
388
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
389
|
+
[preset, plugins]);
|
|
357
390
|
/*
|
|
358
391
|
* On Datalayer, the application runs on a runtime: allocated with a plain
|
|
359
392
|
* agentspec, its agent created there with the application's spec — so that
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ReactorPlugin } from '@datalayer/reactor';
|
|
2
|
+
export declare const APP_RUNTIME_PLUGIN_NAME = "@datalayer/loop-plugin-app-runtime";
|
|
3
|
+
/** Follows the application's runtime, and tells the page what it holds. */
|
|
4
|
+
export declare function AppRuntimePlugins({ appId }: {
|
|
5
|
+
appId: string;
|
|
6
|
+
}): null;
|
|
7
|
+
/** The plugin that follows an application's runtime for its page. */
|
|
8
|
+
export declare function defineAppRuntimePlugin(appId: string): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The page told which of an application's plugins its runtime holds (LOOP
|
|
8
|
+
* F-15).
|
|
9
|
+
*
|
|
10
|
+
* The application's page plugin requires the runtime's plugin of the same
|
|
11
|
+
* name (`requiredBackendPlugins`). Reactor stands such a plugin down while
|
|
12
|
+
* the plugin it requires is not running, and brings it back when it is — but
|
|
13
|
+
* only a host that says what runs can make that true. Every place an
|
|
14
|
+
* application appears draws it with `AppRenderer`, so this is said once, for
|
|
15
|
+
* all of them — the Studio's Preview, its address, an embed, the browser
|
|
16
|
+
* alone:
|
|
17
|
+
*
|
|
18
|
+
* - on a runtime, Reactor's `useBackendPluginStream` follows the runtime's
|
|
19
|
+
* `/api/v1/apps/<id>/plugins/state` (and its stream): the page's plugin
|
|
20
|
+
* stands down when the runtime no longer holds the application, and comes
|
|
21
|
+
* back when it does;
|
|
22
|
+
* - with no runtime — the browser alone, or one still starting — the page is
|
|
23
|
+
* its own runtime, and the pair is whole: nothing stands down, nothing is
|
|
24
|
+
* blank.
|
|
25
|
+
*
|
|
26
|
+
* A plugin of its own, with no backend requirement, so that it keeps
|
|
27
|
+
* listening while the application's plugin is stood down. It renders nothing.
|
|
28
|
+
*
|
|
29
|
+
* @module loop/apps/AppRuntimePlugins
|
|
30
|
+
*/
|
|
31
|
+
import { useCallback, useEffect } from 'react';
|
|
32
|
+
import { definePlugin } from '@datalayer/reactor';
|
|
33
|
+
import { registerReactor, useBackendPluginStream, useReactorPlatform, useSignalValue, } from '@datalayer/reactor/react';
|
|
34
|
+
import { IDLE_SANDBOX_SNAPSHOT_SIGNAL, IDLE_SANDBOX_TARGET_SIGNAL, LoopSlots, } from '../core';
|
|
35
|
+
import { useOptionalSandboxService } from '../plugins/agents/useSandboxService';
|
|
36
|
+
import { appPluginName, appPluginsBase, appRuntimeOf } from './pluginPair';
|
|
37
|
+
export const APP_RUNTIME_PLUGIN_NAME = '@datalayer/loop-plugin-app-runtime';
|
|
38
|
+
/** A host's answer to "is this runtime plugin running?", from a list. */
|
|
39
|
+
const runningOf = (names) => (name) => names.includes(name);
|
|
40
|
+
/** Follows the application's runtime, and tells the page what it holds. */
|
|
41
|
+
export function AppRuntimePlugins({ appId }) {
|
|
42
|
+
const reactor = useReactorPlatform();
|
|
43
|
+
const service = useOptionalSandboxService();
|
|
44
|
+
const target = useSignalValue(service?.target ?? IDLE_SANDBOX_TARGET_SIGNAL);
|
|
45
|
+
const snapshot = useSignalValue(service?.snapshot ?? IDLE_SANDBOX_SNAPSHOT_SIGNAL);
|
|
46
|
+
const runtime = appRuntimeOf({
|
|
47
|
+
target,
|
|
48
|
+
snapshot,
|
|
49
|
+
serverUrl: service?.serverUrl,
|
|
50
|
+
});
|
|
51
|
+
const name = appPluginName(appId);
|
|
52
|
+
// No runtime: the page is its own, and the pair is whole.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (runtime) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
registerReactor(reactor, runningOf([name]));
|
|
58
|
+
void reactor.setBackendPlugins([name]);
|
|
59
|
+
}, [reactor, runtime, name]);
|
|
60
|
+
// On one: what it says, for the slots gated on it too.
|
|
61
|
+
const onState = useCallback((state) => registerReactor(reactor, runningOf(state.plugins
|
|
62
|
+
.filter(plugin => plugin.enabled)
|
|
63
|
+
.map(plugin => plugin.name))), [reactor]);
|
|
64
|
+
useBackendPluginStream(runtime ? appPluginsBase(runtime, appId) : undefined, {
|
|
65
|
+
onState,
|
|
66
|
+
});
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/** The plugin that follows an application's runtime for its page. */
|
|
70
|
+
export function defineAppRuntimePlugin(appId) {
|
|
71
|
+
const Follow = () => _jsx(AppRuntimePlugins, { appId: appId });
|
|
72
|
+
return definePlugin({
|
|
73
|
+
name: APP_RUNTIME_PLUGIN_NAME,
|
|
74
|
+
displayName: 'Its runtime',
|
|
75
|
+
description: 'Tells the application’s page which of its plugins its runtime holds.',
|
|
76
|
+
octicon: 'server',
|
|
77
|
+
build: () => ({
|
|
78
|
+
components: [
|
|
79
|
+
{
|
|
80
|
+
slot: LoopSlots.status,
|
|
81
|
+
id: 'app-runtime-plugins',
|
|
82
|
+
Component: Follow,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -21,7 +21,14 @@ export type Feedback = {
|
|
|
21
21
|
session: string;
|
|
22
22
|
liked: boolean;
|
|
23
23
|
comment: string;
|
|
24
|
+
/**
|
|
25
|
+
* The answer it is about (LOOP P-24): `answer-<n>`, its place among the
|
|
26
|
+
* conversation's answers; none for the conversation as a whole.
|
|
27
|
+
*/
|
|
28
|
+
message?: string;
|
|
24
29
|
};
|
|
30
|
+
/** How an answer is named in feedback: its place among the conversation's answers, from 1. */
|
|
31
|
+
export declare const answerIdOf: (answers: number) => string | undefined;
|
|
25
32
|
/** What the feedback strip says, in a person's words. */
|
|
26
33
|
export declare const FEEDBACK_WORDS: {
|
|
27
34
|
readonly ask: "Was this answer useful?";
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/** The longest comment kept, as the runtime holds it (`COMMENT_LIMIT`). */
|
|
6
6
|
export const FEEDBACK_COMMENT_LIMIT = 2000;
|
|
7
|
+
/** How an answer is named in feedback: its place among the conversation's answers, from 1. */
|
|
8
|
+
export const answerIdOf = (answers) => answers > 0 ? `answer-${answers}` : undefined;
|
|
7
9
|
/** What the feedback strip says, in a person's words. */
|
|
8
10
|
export const FEEDBACK_WORDS = {
|
|
9
11
|
ask: 'Was this answer useful?',
|
|
@@ -50,6 +52,7 @@ export async function sendFeedback(feedback, { agentBaseUrl, token, fetcher = fe
|
|
|
50
52
|
session: feedback.session,
|
|
51
53
|
liked: feedback.liked,
|
|
52
54
|
comment: feedback.comment.trim(),
|
|
55
|
+
...(feedback.message ? { message: feedback.message } : {}),
|
|
53
56
|
}),
|
|
54
57
|
});
|
|
55
58
|
const body = (await response.json().catch(() => ({})));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An application's two Reactor plugins, named once (LOOP F-15).
|
|
3
|
+
*
|
|
4
|
+
* Whichever editor wrote it — a spec, the Canvas, an `app.py` — an
|
|
5
|
+
* application is a pair of plugins of one name, `loop-app-<id>`: the page's
|
|
6
|
+
* (`defineAppPlugin`) and the runtime's (`manifest_of` in
|
|
7
|
+
* `agent_runtimes.loop.apps.plugins`, which says the same name). Each
|
|
8
|
+
* declares the other: the page's `requiredBackendPlugins`, the runtime's
|
|
9
|
+
* `frontend_dependencies`; and both are delivered as one extension of that
|
|
10
|
+
* name, which is how Reactor's manager and graph show them as one
|
|
11
|
+
* application.
|
|
12
|
+
*
|
|
13
|
+
* The page follows what its runtime holds with Reactor's
|
|
14
|
+
* `useBackendPluginStream`, on `appPluginsBase` (`AppRuntimePlugins`).
|
|
15
|
+
*
|
|
16
|
+
* @module loop/apps/pluginPair
|
|
17
|
+
*/
|
|
18
|
+
import type { SandboxSnapshot } from '../core';
|
|
19
|
+
import type { SandboxTarget } from '../plugins/agents/switchable';
|
|
20
|
+
/** The one name of an application's two plugins, the page's and the runtime's. */
|
|
21
|
+
export declare const appPluginName: (appId: string) => string;
|
|
22
|
+
/** What each tier of an application's pair says. */
|
|
23
|
+
export type AppPluginPair = {
|
|
24
|
+
/** The page's plugin, and the runtime plugin it cannot work without. */
|
|
25
|
+
page: {
|
|
26
|
+
name: string;
|
|
27
|
+
requiredBackendPlugins: string[];
|
|
28
|
+
};
|
|
29
|
+
/** The runtime's plugin, and the page plugin it cannot be used without. */
|
|
30
|
+
server: {
|
|
31
|
+
name: string;
|
|
32
|
+
frontendDependencies: string[];
|
|
33
|
+
};
|
|
34
|
+
/** The extension that delivers both: the application. */
|
|
35
|
+
extension: string;
|
|
36
|
+
};
|
|
37
|
+
/** The pair an application of this id is, on both tiers. */
|
|
38
|
+
export declare function appPluginPair(appId: string): AppPluginPair;
|
|
39
|
+
/**
|
|
40
|
+
* Where Reactor's `useBackendPluginStream` reads an application's plugins on
|
|
41
|
+
* its runtime: `<base>/plugins/state`, `<base>/events/stream` — about that
|
|
42
|
+
* application only, so that a runtime several share says nothing of the
|
|
43
|
+
* others.
|
|
44
|
+
*/
|
|
45
|
+
export declare const appPluginsBase: (runtimeUrl: string, appId: string) => string;
|
|
46
|
+
/**
|
|
47
|
+
* The runtime an application's page follows, or `undefined` when it runs on
|
|
48
|
+
* none yet.
|
|
49
|
+
*
|
|
50
|
+
* - **In the browser alone**: none — the page is its own runtime.
|
|
51
|
+
* - **On Datalayer**: the runtime its agent was made on, once reported
|
|
52
|
+
* (`agent_base_url`) — at its address, embedded, on the visitors' runtime.
|
|
53
|
+
* - **Here** (`local`, `jupyter`): the workspace's server, once it runs —
|
|
54
|
+
* after the agent was made there with the application, never before.
|
|
55
|
+
*/
|
|
56
|
+
export declare function appRuntimeOf({ target, snapshot, serverUrl, }: {
|
|
57
|
+
target: SandboxTarget | undefined;
|
|
58
|
+
snapshot: Pick<SandboxSnapshot, 'state' | 'agentBaseUrl'>;
|
|
59
|
+
serverUrl: string | undefined;
|
|
60
|
+
}): string | undefined;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The one name of an application's two plugins, the page's and the runtime's. */
|
|
6
|
+
export const appPluginName = (appId) => `loop-app-${appId}`;
|
|
7
|
+
/** The pair an application of this id is, on both tiers. */
|
|
8
|
+
export function appPluginPair(appId) {
|
|
9
|
+
const name = appPluginName(appId);
|
|
10
|
+
return {
|
|
11
|
+
page: { name, requiredBackendPlugins: [name] },
|
|
12
|
+
server: { name, frontendDependencies: [name] },
|
|
13
|
+
extension: name,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Where Reactor's `useBackendPluginStream` reads an application's plugins on
|
|
18
|
+
* its runtime: `<base>/plugins/state`, `<base>/events/stream` — about that
|
|
19
|
+
* application only, so that a runtime several share says nothing of the
|
|
20
|
+
* others.
|
|
21
|
+
*/
|
|
22
|
+
export const appPluginsBase = (runtimeUrl, appId) => `${runtimeUrl.replace(/\/+$/, '')}/api/v1/apps/${encodeURIComponent(appId)}`;
|
|
23
|
+
/**
|
|
24
|
+
* The runtime an application's page follows, or `undefined` when it runs on
|
|
25
|
+
* none yet.
|
|
26
|
+
*
|
|
27
|
+
* - **In the browser alone**: none — the page is its own runtime.
|
|
28
|
+
* - **On Datalayer**: the runtime its agent was made on, once reported
|
|
29
|
+
* (`agent_base_url`) — at its address, embedded, on the visitors' runtime.
|
|
30
|
+
* - **Here** (`local`, `jupyter`): the workspace's server, once it runs —
|
|
31
|
+
* after the agent was made there with the application, never before.
|
|
32
|
+
*/
|
|
33
|
+
export function appRuntimeOf({ target, snapshot, serverUrl, }) {
|
|
34
|
+
if (!target || target === 'browser') {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (snapshot.agentBaseUrl) {
|
|
38
|
+
return snapshot.agentBaseUrl;
|
|
39
|
+
}
|
|
40
|
+
if (target === 'datalayer') {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return snapshot.state === 'running' && serverUrl ? serverUrl : undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A thread opened from a person's history, to go on with it (LOOP P-24).
|
|
3
|
+
*
|
|
4
|
+
* A thread is a session of an application: its uid is the conversation's
|
|
5
|
+
* AG-UI thread. Opened again, its conversation is read from the runtime the
|
|
6
|
+
* chat runs on — the session's messages while the runtime holds it
|
|
7
|
+
* (`GET …/sessions/{uid}/messages`, D-13), else the session resumed from its
|
|
8
|
+
* record (`POST …/sessions/{uid}/resume` with the agent, R-04), whose
|
|
9
|
+
* stream's `MESSAGES_SNAPSHOT` says what it holds — and the chat draws it
|
|
10
|
+
* and goes on with it on the same thread.
|
|
11
|
+
*
|
|
12
|
+
* Pure: the network is the `fetch` it is handed.
|
|
13
|
+
*
|
|
14
|
+
* @module loop/apps/threads
|
|
15
|
+
*/
|
|
16
|
+
import type { ChatMessage } from '../../types/messages';
|
|
17
|
+
/** What opening a thread says, in a person's words. */
|
|
18
|
+
export declare const THREAD_WORDS: {
|
|
19
|
+
readonly opening: "Opening your conversation…";
|
|
20
|
+
readonly notOpened: (detail: string) => string;
|
|
21
|
+
};
|
|
22
|
+
/** The AG-UI messages a session-API stream's `MESSAGES_SNAPSHOT` carries, if one does. */
|
|
23
|
+
export declare function snapshotOfStream(text: string): unknown[] | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* A thread's conversation, read from the runtime its application runs on:
|
|
26
|
+
* held there, or resumed from its record. Throws the runtime's sentence when
|
|
27
|
+
* neither answers.
|
|
28
|
+
*/
|
|
29
|
+
export declare function openThread({ agentBaseUrl, agentId, uid, token, fetcher, }: {
|
|
30
|
+
agentBaseUrl: string;
|
|
31
|
+
agentId: string;
|
|
32
|
+
uid: string;
|
|
33
|
+
token?: string | null;
|
|
34
|
+
fetcher?: typeof fetch;
|
|
35
|
+
}): Promise<ChatMessage[]>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { messagesOfThread } from '../embed/embedSession';
|
|
6
|
+
/** What opening a thread says, in a person's words. */
|
|
7
|
+
export const THREAD_WORDS = {
|
|
8
|
+
opening: 'Opening your conversation…',
|
|
9
|
+
notOpened: (detail) => `Your conversation could not be opened, so this is a new one: ${detail}`,
|
|
10
|
+
};
|
|
11
|
+
/** The AG-UI messages a session-API stream's `MESSAGES_SNAPSHOT` carries, if one does. */
|
|
12
|
+
export function snapshotOfStream(text) {
|
|
13
|
+
let found;
|
|
14
|
+
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
15
|
+
const data = block
|
|
16
|
+
.split(/\r?\n/)
|
|
17
|
+
.filter(line => line.startsWith('data:'))
|
|
18
|
+
.map(line => line.slice('data:'.length).trim())
|
|
19
|
+
.join('');
|
|
20
|
+
if (!data) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const event = JSON.parse(data);
|
|
25
|
+
if (event.type === 'MESSAGES_SNAPSHOT' && Array.isArray(event.messages)) {
|
|
26
|
+
found = event.messages;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Not an event: skipped.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return found;
|
|
34
|
+
}
|
|
35
|
+
async function detailOf(response) {
|
|
36
|
+
try {
|
|
37
|
+
const body = (await response.json());
|
|
38
|
+
if (typeof body.detail === 'string' && body.detail) {
|
|
39
|
+
return body.detail;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// No sentence: the status says it.
|
|
44
|
+
}
|
|
45
|
+
return `${response.status}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A thread's conversation, read from the runtime its application runs on:
|
|
49
|
+
* held there, or resumed from its record. Throws the runtime's sentence when
|
|
50
|
+
* neither answers.
|
|
51
|
+
*/
|
|
52
|
+
export async function openThread({ agentBaseUrl, agentId, uid, token, fetcher = fetch, }) {
|
|
53
|
+
const base = `${agentBaseUrl.replace(/\/+$/, '')}/api/v1/apps/sessions/${encodeURIComponent(uid)}`;
|
|
54
|
+
const authorization = token
|
|
55
|
+
? { Authorization: `Bearer ${token}` }
|
|
56
|
+
: {};
|
|
57
|
+
const held = await fetcher(`${base}/messages`, { headers: authorization });
|
|
58
|
+
if (held.ok) {
|
|
59
|
+
const body = (await held.json());
|
|
60
|
+
return messagesOfThread(body.messages);
|
|
61
|
+
}
|
|
62
|
+
if (held.status !== 404) {
|
|
63
|
+
throw new Error(await detailOf(held));
|
|
64
|
+
}
|
|
65
|
+
// Not held here: resumed from its record, under the same uid.
|
|
66
|
+
const resumed = await fetcher(`${base}/resume`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { 'Content-Type': 'application/json', ...authorization },
|
|
69
|
+
body: JSON.stringify({ agent: agentId }),
|
|
70
|
+
});
|
|
71
|
+
if (!resumed.ok) {
|
|
72
|
+
throw new Error(await detailOf(resumed));
|
|
73
|
+
}
|
|
74
|
+
const snapshot = snapshotOfStream(await resumed.text());
|
|
75
|
+
if (!snapshot) {
|
|
76
|
+
throw new Error(`The runtime said nothing of session ${uid}.`);
|
|
77
|
+
}
|
|
78
|
+
return messagesOfThread(snapshot);
|
|
79
|
+
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* @module loop/embed/LoopEmbed
|
|
19
19
|
*/
|
|
20
20
|
import { type ReactNode } from 'react';
|
|
21
|
-
import type {
|
|
21
|
+
import type { PlatformInput } from '@datalayer/reactor';
|
|
22
22
|
import { type LoopPresetOptions } from '../presets';
|
|
23
23
|
export type LoopEmbedProps = LoopPresetOptions & {
|
|
24
24
|
/** Which agent the conversation is with. */
|
|
@@ -59,9 +59,10 @@ export type LoopEmbedProps = LoopPresetOptions & {
|
|
|
59
59
|
* How a page adds something to Loop rather than around it — a button in the
|
|
60
60
|
* window's title bar, a panel of its own, a command. Without this a host
|
|
61
61
|
* would have to rebuild the plugin list to add one thing to it, which is the
|
|
62
|
-
* assembly this component exists to save.
|
|
62
|
+
* assembly this component exists to save. An extension groups some of
|
|
63
|
+
* them, as an application's own plugins are (`AppRenderer`, LOOP F-15).
|
|
63
64
|
*/
|
|
64
|
-
plugins?:
|
|
65
|
+
plugins?: PlatformInput[];
|
|
65
66
|
};
|
|
66
67
|
export declare function LoopEmbed({ agentId, showHeader, chatHeaderActions, headerActions, headerContainer, frameTitle, frameHeight, sidebarRail, plugins, ...preset }: LoopEmbedProps): React.JSX.Element;
|
|
67
68
|
export default LoopEmbed;
|
|
@@ -124,3 +124,30 @@ export declare function hostFrontendTools(app: Pick<AppSpec, 'connections' | 'ru
|
|
|
124
124
|
* the page says.
|
|
125
125
|
*/
|
|
126
126
|
export declare function hostUserRunProps(app: Pick<AppSpec, 'deployment'>, host: () => AppEmbedHost): RunPropsContribution | undefined;
|
|
127
|
+
/** A user token ai-agents signed, and when it ends (seconds since the epoch). */
|
|
128
|
+
export type SignedUserToken = {
|
|
129
|
+
token: string;
|
|
130
|
+
exp: number;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Asks ai-agents to sign the person signed in to Datalayer as a deployment's
|
|
134
|
+
* user (LOOP D-21; STUDIO A-18 to A-20), with their own token: what a
|
|
135
|
+
* surface that has no host server holding the deployment's secret — the
|
|
136
|
+
* VS Code extension, Jupyter AI Agents, Datalayer Desktop — sends as
|
|
137
|
+
* `forwardedProps.loop.user_token` with the runs of an application that
|
|
138
|
+
* says `user: signed`. HS256 with the deployment's secret, `sub` the
|
|
139
|
+
* person's uid, `name` theirs, fifteen minutes; refused (`403`) for an
|
|
140
|
+
* application's principal or a narrowed token, and for a person the
|
|
141
|
+
* deployment does not let in. Those surfaces share no code, so they copy
|
|
142
|
+
* it.
|
|
143
|
+
*
|
|
144
|
+
* @param aiAgentsUrl - The ai-agents service's base URL.
|
|
145
|
+
* @param deploymentUid - The deployment talked to.
|
|
146
|
+
* @param token - The person's Datalayer token.
|
|
147
|
+
* @param fetcher - What asks; `fetch` unless given.
|
|
148
|
+
*
|
|
149
|
+
* @returns The user token and when it ends.
|
|
150
|
+
*
|
|
151
|
+
* @throws With ai-agents' sentence when it refuses.
|
|
152
|
+
*/
|
|
153
|
+
export declare function fetchUserToken(aiAgentsUrl: string, deploymentUid: string, token: string, fetcher?: (url: string, init: RequestInit) => Promise<Response>): Promise<SignedUserToken>;
|
|
@@ -122,3 +122,50 @@ export function hostUserRunProps(app, host) {
|
|
|
122
122
|
},
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Asks ai-agents to sign the person signed in to Datalayer as a deployment's
|
|
127
|
+
* user (LOOP D-21; STUDIO A-18 to A-20), with their own token: what a
|
|
128
|
+
* surface that has no host server holding the deployment's secret — the
|
|
129
|
+
* VS Code extension, Jupyter AI Agents, Datalayer Desktop — sends as
|
|
130
|
+
* `forwardedProps.loop.user_token` with the runs of an application that
|
|
131
|
+
* says `user: signed`. HS256 with the deployment's secret, `sub` the
|
|
132
|
+
* person's uid, `name` theirs, fifteen minutes; refused (`403`) for an
|
|
133
|
+
* application's principal or a narrowed token, and for a person the
|
|
134
|
+
* deployment does not let in. Those surfaces share no code, so they copy
|
|
135
|
+
* it.
|
|
136
|
+
*
|
|
137
|
+
* @param aiAgentsUrl - The ai-agents service's base URL.
|
|
138
|
+
* @param deploymentUid - The deployment talked to.
|
|
139
|
+
* @param token - The person's Datalayer token.
|
|
140
|
+
* @param fetcher - What asks; `fetch` unless given.
|
|
141
|
+
*
|
|
142
|
+
* @returns The user token and when it ends.
|
|
143
|
+
*
|
|
144
|
+
* @throws With ai-agents' sentence when it refuses.
|
|
145
|
+
*/
|
|
146
|
+
export async function fetchUserToken(aiAgentsUrl, deploymentUid, token, fetcher = fetch) {
|
|
147
|
+
const url = `${aiAgentsUrl.replace(/\/+$/, '')}/api/ai-agents/v1/apps/deployments/${encodeURIComponent(deploymentUid)}/user-token`;
|
|
148
|
+
const response = await fetcher(url, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
151
|
+
});
|
|
152
|
+
let body = {};
|
|
153
|
+
try {
|
|
154
|
+
const read = await response.json();
|
|
155
|
+
body =
|
|
156
|
+
read && typeof read === 'object' ? read : {};
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// The status says it.
|
|
160
|
+
}
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
const detail = typeof body.detail === 'string' ? body.detail : '';
|
|
163
|
+
throw new Error(detail || `ai-agents refused to sign you (${response.status}).`);
|
|
164
|
+
}
|
|
165
|
+
const signed = typeof body.user_token === 'string' ? body.user_token : '';
|
|
166
|
+
const exp = Number(body.exp);
|
|
167
|
+
if (!signed || !Number.isFinite(exp)) {
|
|
168
|
+
throw new Error('ai-agents answered no user token.');
|
|
169
|
+
}
|
|
170
|
+
return { token: signed, exp };
|
|
171
|
+
}
|