@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.
Files changed (62) hide show
  1. package/lib/components/a2ui/datalayer/Download.d.ts +21 -0
  2. package/lib/components/a2ui/datalayer/Download.js +66 -0
  3. package/lib/components/a2ui/datalayer/drawn.d.ts +1 -1
  4. package/lib/components/a2ui/datalayer/drawn.js +1 -0
  5. package/lib/components/a2ui/datalayer/index.d.ts +2 -1
  6. package/lib/components/a2ui/datalayer/index.js +3 -0
  7. package/lib/components/inspector/a2aSpans.d.ts +9 -0
  8. package/lib/components/inspector/a2aSpans.js +11 -1
  9. package/lib/components/teams/AnswerSurfaces.d.ts +56 -0
  10. package/lib/components/teams/AnswerSurfaces.js +27 -0
  11. package/lib/components/teams/AnswerSurfacesView.d.ts +14 -0
  12. package/lib/components/teams/AnswerSurfacesView.js +30 -0
  13. package/lib/components/teams/index.d.ts +1 -0
  14. package/lib/components/teams/index.js +1 -0
  15. package/lib/components/teams/useA2ATeam.d.ts +56 -2
  16. package/lib/components/teams/useA2ATeam.js +122 -3
  17. package/lib/hooks/useAgentRuntimes.d.ts +13 -0
  18. package/lib/hooks/useAgentRuntimes.js +11 -1
  19. package/lib/loop/apps/AppFeedback.js +11 -2
  20. package/lib/loop/apps/AppRenderer.d.ts +13 -1
  21. package/lib/loop/apps/AppRenderer.js +34 -1
  22. package/lib/loop/apps/AppRuntimePlugins.d.ts +8 -0
  23. package/lib/loop/apps/AppRuntimePlugins.js +87 -0
  24. package/lib/loop/apps/feedback.d.ts +7 -0
  25. package/lib/loop/apps/feedback.js +3 -0
  26. package/lib/loop/apps/pluginPair.d.ts +60 -0
  27. package/lib/loop/apps/pluginPair.js +44 -0
  28. package/lib/loop/apps/threads.d.ts +35 -0
  29. package/lib/loop/apps/threads.js +79 -0
  30. package/lib/loop/embed/LoopEmbed.d.ts +4 -3
  31. package/lib/loop/embed/hostBridge.d.ts +27 -0
  32. package/lib/loop/embed/hostBridge.js +47 -0
  33. package/lib/loop/index.d.ts +1 -1
  34. package/lib/loop/index.js +1 -1
  35. package/lib/loop/plugins/agent-capacity/index.d.ts +7 -0
  36. package/lib/loop/plugins/agent-capacity/index.js +3 -2
  37. package/lib/loop/plugins/agents/DatalayerAgentBridge.js +11 -0
  38. package/lib/loop/plugins/agents/appLaunch.d.ts +15 -0
  39. package/lib/loop/plugins/agents/appLaunch.js +28 -0
  40. package/lib/loop/plugins/chat/ChatView.js +177 -109
  41. package/lib/loop/shell/LoopWorkspace.d.ts +3 -3
  42. package/lib/loop/shell/LoopWorkspace.js +1 -1
  43. package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +10 -2
  44. package/lib/runtimes/actions.js +23 -0
  45. package/lib/runtimes/apis.d.ts +18 -0
  46. package/lib/runtimes/browser/a2aPeer.d.ts +19 -2
  47. package/lib/runtimes/browser/a2aPeer.js +13 -7
  48. package/lib/specs/apps.js +102 -30
  49. package/lib/specs/envvars.d.ts +2 -0
  50. package/lib/specs/envvars.js +22 -0
  51. package/lib/specs/outputs.d.ts +1 -0
  52. package/lib/specs/outputs.js +12 -0
  53. package/lib/specs/sceneSchema.js +12 -1
  54. package/lib/specs/scenes.js +238 -121
  55. package/lib/specs/teams/teams.js +23 -7
  56. package/lib/specs/uiPlugins.js +55 -0
  57. package/lib/stores/agentRuntimeStore.js +4 -0
  58. package/lib/types/chat.d.ts +6 -0
  59. package/lib/types/scenes.d.ts +7 -1
  60. package/package.json +1 -1
  61. package/scripts/codegen/generate_envvars.py +0 -1
  62. 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({ session: thread, liked: choice, comment }, { agentBaseUrl, token });
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 { PluginRef, ReactorPlugin } from '@datalayer/reactor';
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(() => ('problem' in preset ? plugins : [...preset.plugins, ...plugins]), [preset, plugins]);
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 { PluginRef } from '@datalayer/reactor';
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?: PluginRef[];
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
+ }