@datalayer/agent-runtimes 1.0.18 → 1.0.19

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.
@@ -21,7 +21,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
21
21
  */
22
22
  import { useEffect, useMemo, useRef, useState } from 'react';
23
23
  import { ServerConnection, ServiceManager } from '@jupyterlab/services';
24
- import { Box, DatalayerThemeProvider, getThemeConfig, useSystemColorMode, useThemeStore, } from '@datalayer/primer-addons';
24
+ import { DatalayerThemeProvider, getThemeConfig, useSystemColorMode, useThemeStore, } from '@datalayer/primer-addons';
25
+ import { Box } from '@primer/react';
25
26
  import { Notebook, NotebookToolbar, CellSidebarExtension, CellSidebarButton, JupyterReactTheme, notebookStore, disposeServiceManager, } from '@datalayer/jupyter-react';
26
27
  import { useAgentsRuntimes } from '../../hooks/useAgentRuntimes';
27
28
  import { registerSandboxServiceManager } from '../../services/sandboxServiceManagers';
@@ -54,7 +55,7 @@ const EPHEMERAL_NOTEBOOK_CONTENT = {
54
55
  /**
55
56
  * Renders an in-memory notebook backed by a sandbox kernel.
56
57
  */
57
- export function EphemeralNotebook({ notebookId, runtimePodName, cellSidebarMargin = 120, nbformat, onNbformatChange, toolbarComponent, }) {
58
+ export function EphemeralNotebook({ notebookId, runtimePodName, runtimeOverride, cellSidebarMargin = 120, nbformat, onNbformatChange, toolbarComponent, collaborationProvider, }) {
58
59
  // The `nbformat` passed to the `Notebook` component MUST stay a stable
59
60
  // reference for the lifetime of a given `notebookId`: the underlying
60
61
  // `useNotebookModel` hook rebuilds the notebook model (and its context /
@@ -77,18 +78,36 @@ export function EphemeralNotebook({ notebookId, runtimePodName, cellSidebarMargi
77
78
  // notebook must bind to exactly the runtime assigned to this agent, or to
78
79
  // none at all (straight path).
79
80
  const { runtimes, refetchRuntimes } = useAgentsRuntimes();
80
- const selectedRuntime = useMemo(() => {
81
+ const podResolvedRuntime = useMemo(() => {
81
82
  const preferredPod = String(runtimePodName || '').trim();
82
83
  if (!preferredPod) {
83
84
  return undefined;
84
85
  }
85
86
  return runtimes.find(rt => String(rt?.pod_name || '') === preferredPod);
86
87
  }, [runtimePodName, runtimes]);
88
+ // An explicit endpoint override wins over the pod lookup: it lets a SaaS
89
+ // browser bind the kernel through the runtimes tunnel proxy (the node's
90
+ // Jupyter server is not directly reachable, so no pod exists in the list).
91
+ const selectedRuntime = useMemo(() => {
92
+ const overrideBaseUrl = String(runtimeOverride?.baseUrl || '').trim();
93
+ if (overrideBaseUrl) {
94
+ return {
95
+ url: overrideBaseUrl,
96
+ wsUrl: String(runtimeOverride?.wsUrl || '').trim() || undefined,
97
+ token: String(runtimeOverride?.token || '').trim(),
98
+ pod_name: String(runtimeOverride?.podName || '').trim() || 'agent-node-proxy',
99
+ };
100
+ }
101
+ return podResolvedRuntime;
102
+ }, [runtimeOverride, podResolvedRuntime]);
87
103
  // While the assigned pod has not yet appeared in the runtimes list, poll the
88
104
  // list quickly instead of waiting for the default (10s) refresh interval.
89
105
  // This is what makes the "Starting notebook…" state clear promptly once the
90
- // agent runtime is ready, rather than lingering for several seconds.
91
- const needsRuntimeLookup = Boolean(String(runtimePodName || '').trim() && !selectedRuntime);
106
+ // agent runtime is ready, rather than lingering for several seconds. The
107
+ // override path binds immediately, so no polling is needed there.
108
+ const needsRuntimeLookup = Boolean(!runtimeOverride?.baseUrl &&
109
+ String(runtimePodName || '').trim() &&
110
+ !selectedRuntime);
92
111
  useEffect(() => {
93
112
  if (!needsRuntimeLookup) {
94
113
  return;
@@ -120,9 +139,10 @@ export function EphemeralNotebook({ notebookId, runtimePodName, cellSidebarMargi
120
139
  }
121
140
  try {
122
141
  const token = String(selectedRuntime?.token || '').trim();
142
+ const wsUrl = String(selectedRuntime?.wsUrl || '').trim() || baseUrl.replace(/^http/, 'ws');
123
143
  const serverSettings = ServerConnection.makeSettings({
124
144
  baseUrl,
125
- wsUrl: baseUrl.replace(/^http/, 'ws'),
145
+ wsUrl,
126
146
  token,
127
147
  appendToken: true,
128
148
  });
@@ -155,7 +175,12 @@ export function EphemeralNotebook({ notebookId, runtimePodName, cellSidebarMargi
155
175
  disposeServiceManager(manager);
156
176
  }
157
177
  };
158
- }, [selectedRuntime?.pod_name, selectedRuntime?.url, selectedRuntime?.token]);
178
+ }, [
179
+ selectedRuntime?.pod_name,
180
+ selectedRuntime?.url,
181
+ selectedRuntime?.token,
182
+ selectedRuntime?.wsUrl,
183
+ ]);
159
184
  // Bind strictly to the agent runtime sandbox; there is NO local fallback
160
185
  // kernel. The notebook executes on the agent's runtime or shows a waiting
161
186
  // state until the runtime is ready.
@@ -163,9 +188,17 @@ export function EphemeralNotebook({ notebookId, runtimePodName, cellSidebarMargi
163
188
  const activeKernelId = runtimeKernelId;
164
189
  const activeStartDefaultKernel = runtimeStartDefaultKernel;
165
190
  const ToolbarComponent = toolbarComponent || NotebookToolbar;
166
- const isRuntimeStarting = Boolean(String(runtimePodName || '').trim() && !activeServiceManager);
191
+ const isRuntimeStarting = Boolean((String(runtimePodName || '').trim() ||
192
+ String(runtimeOverride?.baseUrl || '').trim()) &&
193
+ !activeServiceManager);
167
194
  useProgressTask(`ephemeral-notebook-start-${notebookId}`, isRuntimeStarting);
168
195
  useEffect(() => {
196
+ // When a collaboration provider is active the shared ydoc is the single
197
+ // source of truth and is synced remotely; the local in-memory persistence
198
+ // poll would fight it, so it is disabled in that mode.
199
+ if (collaborationProvider) {
200
+ return;
201
+ }
169
202
  // Read the CURRENT live notebook model straight from the notebook store.
170
203
  // The `NotebookAdapter` exposes `notebook` (the widget, whose `.model` is
171
204
  // the `INotebookModel`) and `panel` (`panel.content.model`). There is NO
@@ -247,6 +280,6 @@ export function EphemeralNotebook({ notebookId, runtimePodName, cellSidebarMargi
247
280
  flex: '1 1 auto',
248
281
  minHeight: 0,
249
282
  },
250
- }, children: _jsx(Notebook, { nbformat: initialNbformat, id: notebookId, serviceManager: activeServiceManager, startDefaultKernel: activeStartDefaultKernel, kernelId: activeKernelId, height: "100%", cellSidebarMargin: cellSidebarMargin, extensions: extensions, Toolbar: ToolbarComponent }) }) }) })) : null }));
283
+ }, children: _jsx(Notebook, { nbformat: initialNbformat, id: notebookId, serviceManager: activeServiceManager, startDefaultKernel: activeStartDefaultKernel, kernelId: activeKernelId, height: "100%", cellSidebarMargin: cellSidebarMargin, extensions: extensions, Toolbar: ToolbarComponent, collaborationProvider: collaborationProvider }) }) }) })) : null }));
251
284
  }
252
285
  export default EphemeralNotebook;
@@ -201,11 +201,10 @@ export function IdentityCard({ identity, providerConfig, showExpirationDetails =
201
201
  justifyContent: 'center',
202
202
  backgroundColor: display.color,
203
203
  color: 'white',
204
- flexShrink: 0,
205
204
  }, children: _jsx(display.icon, { size: 16 }) }), _jsxs(Box, { sx: { flex: 1, display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Text, { sx: { fontWeight: 'semibold', fontSize: 1 }, children: display.name }), _jsx(Label, { size: "small", variant: "secondary", children: identity.authType === 'token' ? 'API Key' : 'OAuth' })] }), tokenStatus.isExpired ? (_jsx(Label, { variant: "danger", size: "small", children: _jsxs(Box, { sx: { display: 'inline-flex', alignItems: 'center', gap: 1 }, children: [_jsx(AlertIcon, { size: 12 }), " Expired"] }) })) : tokenStatus.isExpiringSoon ? (_jsx(Label, { variant: "attention", size: "small", children: _jsxs(Box, { sx: { display: 'inline-flex', alignItems: 'center', gap: 1 }, children: [_jsx(ClockIcon, { size: 12 }), " Expiring Soon"] }) })) : (_jsx(Label, { variant: "success", size: "small", children: _jsxs(Box, { sx: { display: 'inline-flex', alignItems: 'center', gap: 1 }, children: [_jsx(CheckCircleFillIcon, { size: 12 }), " Connected"] }) }))] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [identity.userInfo?.avatarUrl && (_jsx("a", { href: identity.userInfo.profileUrl ||
206
205
  (provider === 'github'
207
206
  ? `https://github.com/${identity.userInfo.username}`
208
- : undefined), target: "_blank", rel: "noopener noreferrer", style: { display: 'block', lineHeight: 0, flexShrink: 0 }, children: _jsx(Avatar, { src: identity.userInfo.avatarUrl, size: 32, alt: identity.userInfo.name ||
207
+ : undefined), target: "_blank", rel: "noopener noreferrer", style: { display: 'block', lineHeight: 0 }, children: _jsx(Avatar, { src: identity.userInfo.avatarUrl, size: 32, alt: identity.userInfo.name ||
209
208
  identity.userInfo.username ||
210
209
  display.name, sx: { cursor: 'pointer' } }) })), _jsxs(Box, { sx: { flex: 1, minWidth: 0 }, children: [identity.userInfo?.name && (_jsx(Text, { sx: { fontWeight: 'semibold', fontSize: 1, display: 'block' }, children: identity.userInfo.name })), (identity.userInfo?.username || identity.userInfo?.email) && (_jsx("a", { href: identity.userInfo.profileUrl ||
211
210
  (provider === 'github'
@@ -0,0 +1 @@
1
+ export { Box as default, Box } from '@primer/react';
@@ -0,0 +1,7 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ // Runtime shim: force the primer-addons Box export to use Primer's Box.
6
+ // This prevents `sx` from being forwarded to raw DOM nodes as an attribute.
7
+ export { Box as default, Box } from '@primer/react';
@@ -4,6 +4,7 @@
4
4
  * @module types/chat
5
5
  */
6
6
  import type { ComponentType, ReactNode } from 'react';
7
+ import type { ICollaborationProvider } from '@datalayer/jupyter-react';
7
8
  import type { ChatMessage, MessageHandler } from './messages';
8
9
  import type { Protocol, ProtocolConfig } from './protocol';
9
10
  import type { McpServerSelection } from './inference';
@@ -12,6 +13,8 @@ import type { AgentRuntimeConfig } from './config';
12
13
  import type { SandboxWsStatus } from './sandbox';
13
14
  import type { FrontendToolDefinition } from './tools';
14
15
  import type { PoweredByTagProps } from '../chat/display/PoweredByTag';
16
+ import type { EphemeralRuntimeOverride } from '../chat/notebook/EphemeralNotebook';
17
+ import type { EphemeralDocumentCollaboration } from '../chat/document/EphemeralDocument';
15
18
  /**
16
19
  * Context passed to tool-call pre-hooks.
17
20
  * Fires when a tool call starts executing (backend or frontend).
@@ -366,6 +369,34 @@ export interface ChatCommonProps {
366
369
  * Defaults to the toolbar from `@datalayer/jupyter-react` when omitted.
367
370
  */
368
371
  ephemeralNotebookToolbar?: EphemeralNotebookToolbarComponent;
372
+ /**
373
+ * Optional real-time collaboration provider for the ephemeral notebook.
374
+ * When supplied, the notebook joins a shared collaborative room so its state
375
+ * transits over RTC (e.g. between an Agent Node and the SaaS UI) instead of
376
+ * the tunnel. The caller owns the provider lifecycle.
377
+ */
378
+ ephemeralNotebookCollaborationProvider?: ICollaborationProvider;
379
+ /**
380
+ * Explicit collaboration room / document id for the ephemeral notebook. When
381
+ * set it becomes the notebook id directly so multiple peers (e.g. an Agent
382
+ * Node and the SaaS UI) share the same collaborative room and tool scope.
383
+ */
384
+ ephemeralNotebookCollaborationDocumentId?: string;
385
+ /**
386
+ * Optional real-time collaboration configuration for the ephemeral document
387
+ * (Lexical). When supplied the document joins a shared Loro room over
388
+ * WebSocket so its rich-text state transits over RTC (e.g. between an Agent
389
+ * Node and the SaaS UI). Its `roomId` also becomes the document id so all
390
+ * peers share the same collaborative room and lexical tool scope.
391
+ */
392
+ ephemeralDocumentCollaboration?: EphemeralDocumentCollaboration;
393
+ /**
394
+ * Explicit runtime endpoint for the ephemeral notebook kernel. When set, the
395
+ * notebook binds its kernel to this endpoint directly instead of resolving a
396
+ * pod from the user's runtimes list — used to reach an Agent Node's Jupyter
397
+ * server through the runtimes tunnel HTTP/WebSocket proxy.
398
+ */
399
+ ephemeralRuntimeOverride?: EphemeralRuntimeOverride;
369
400
  /** Pre-hook: fires when a tool call starts executing */
370
401
  onToolCallStart?: (context: ToolCallStartContext) => void;
371
402
  /** Post-hook: fires when a tool result is received */
@@ -419,6 +450,18 @@ export interface ChatCommonProps {
419
450
  * so the indicator reflects the surface's runtime.
420
451
  */
421
452
  kernel?: import('@jupyterlab/services/lib/kernel/kernel').IKernelConnection | null;
453
+ /**
454
+ * Optional environment name displayed in the chat header's kernel
455
+ * indicator details (e.g. the agent's local sandbox name). Defaults to
456
+ * the indicator's own "browser-runtime" placeholder when omitted.
457
+ */
458
+ kernelEnvironmentName?: string;
459
+ /** Optional CPU info displayed in the chat header's kernel indicator. */
460
+ kernelCpu?: string;
461
+ /** Optional memory info displayed in the chat header's kernel indicator. */
462
+ kernelMemory?: string;
463
+ /** Optional GPU info displayed in the chat header's kernel indicator. */
464
+ kernelGpu?: string;
422
465
  /**
423
466
  * Disable ChatBase's internal JupyterReactTheme wrapper.
424
467
  * Use this when the host page already provides a JupyterReactTheme boundary.
@@ -746,6 +789,34 @@ export interface ChatBaseProps {
746
789
  * Defaults to the toolbar from `@datalayer/jupyter-react` when omitted.
747
790
  */
748
791
  ephemeralNotebookToolbar?: EphemeralNotebookToolbarComponent;
792
+ /**
793
+ * Optional real-time collaboration provider for the ephemeral notebook.
794
+ * When supplied, the notebook joins a shared collaborative room so its state
795
+ * transits over RTC (e.g. between an Agent Node and the SaaS UI) instead of
796
+ * the tunnel. The caller owns the provider lifecycle.
797
+ */
798
+ ephemeralNotebookCollaborationProvider?: ICollaborationProvider;
799
+ /**
800
+ * Explicit collaboration room / document id for the ephemeral notebook. When
801
+ * set it becomes the notebook id directly so multiple peers (e.g. an Agent
802
+ * Node and the SaaS UI) share the same collaborative room and tool scope.
803
+ */
804
+ ephemeralNotebookCollaborationDocumentId?: string;
805
+ /**
806
+ * Optional real-time collaboration configuration for the ephemeral document
807
+ * (Lexical). When supplied the document joins a shared Loro room over
808
+ * WebSocket so its rich-text state transits over RTC (e.g. between an Agent
809
+ * Node and the SaaS UI). Its `roomId` also becomes the document id so all
810
+ * peers share the same collaborative room and lexical tool scope.
811
+ */
812
+ ephemeralDocumentCollaboration?: EphemeralDocumentCollaboration;
813
+ /**
814
+ * Explicit runtime endpoint for the ephemeral notebook kernel. When set, the
815
+ * notebook binds its kernel to this endpoint directly instead of resolving a
816
+ * pod from the user's runtimes list — used to reach an Agent Node's Jupyter
817
+ * server through the runtimes tunnel HTTP/WebSocket proxy.
818
+ */
819
+ ephemeralRuntimeOverride?: EphemeralRuntimeOverride;
749
820
  /**
750
821
  * Callback when the agent requests authorization for an external service.
751
822
  * This is called when a tool needs OAuth access to a service like GitHub.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",
@@ -102,8 +102,8 @@
102
102
  "start:acp:vite": "VITE_ACP_WS_URL=ws://localhost:8765/api/v1/acp/ws vite",
103
103
  "start:agent": "run-p server:start start:agent:vite",
104
104
  "start:agent:vite": "vite --open /html/agent.html",
105
- "start:agent-node": "run-p server:start:agent-node start:agent-node:vite",
106
- "start:agent-node:vite": "vite --open /html/agent-node.html",
105
+ "start:agent-node": "run-p server:start:agent-node jupyter:start start:agent-node:vite",
106
+ "start:agent-node:vite": "VITE_JUPYTER_SANDBOX_URL=${VITE_JUPYTER_SANDBOX_URL:-http://localhost:8888/api/jupyter-server?token=60c1661cc408f978c309d04157af55c9588ff9557c9380e4fb50785750703da6} vite --open /html/agent-node.html",
107
107
  "start:agent-notebook": "run-p server:start jupyter:start start:agent-notebook:vite",
108
108
  "start:agent-notebook:vite": "vite --open /html/agent-notebook.html",
109
109
  "start:agent-document": "run-p server:start jupyter:start start:agent-document:vite",
@@ -141,6 +141,7 @@
141
141
  "@datalayer/core": "^1.1.48",
142
142
  "@datalayer/icons-react": "^1.0.7",
143
143
  "@datalayer/jupyter-lexical": "^1.0.16",
144
+ "@datalayer/lexical-loro": "^1.0.0",
144
145
  "@datalayer/jupyter-react": "^2.0.7",
145
146
  "@datalayer/primer-addons": "^1.0.15",
146
147
  "@datalayer/primer-rjsf": "^1.0.3",