@datalayer/agent-runtimes 1.3.53 → 1.3.55

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.
@@ -0,0 +1,12 @@
1
+ import { ServiceManager } from '@jupyterlab/services';
2
+ interface AgentStrategyExampleInnerProps {
3
+ serviceManager?: ServiceManager.IManager;
4
+ }
5
+ export declare function AgentStrategyExampleInner({ serviceManager, }: AgentStrategyExampleInnerProps): import("react").JSX.Element;
6
+ /**
7
+ * Main example component with Jupyter provider wrapper.
8
+ */
9
+ export declare function AgentStrategyExample({ serviceManager, }: {
10
+ serviceManager?: ServiceManager.IManager;
11
+ }): import("react").JSX.Element;
12
+ export default AgentStrategyExample;
@@ -4,27 +4,27 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  * Distributed under the terms of the Modified BSD License.
5
5
  */
6
6
  /**
7
- * AgentLoopExample - Define and launch an agent execution *loop*.
7
+ * AgentStrategyExample - Define and launch an agent reasoning *strategy*.
8
8
  *
9
- * This example demonstrates the "loop" paradigm (observe → think → act →
9
+ * This example demonstrates the control-loop paradigm (observe → think → act →
10
10
  * evaluate) instead of one-shot prompting. It is fully driven by the generic
11
- * loop specifications defined in agentspecs (`src/specs/loops.ts`):
11
+ * strategy specifications defined in agentspecs (`src/specs/strategies.ts`):
12
12
  *
13
- * - Pick any loop spec (Data Analysis, Plan/Execute/Critic, OODA,
14
- * Human-in-the-Loop). The list is read from the loop catalogue, so adding a
15
- * new YAML loop spec automatically makes it available here.
16
- * - The agent's system prompt is composed generically from the selected loop's
13
+ * - Pick any strategy spec (Data Analysis, Plan/Execute/Critic, OODA,
14
+ * Human-in-the-Loop). The list is read from the strategy catalogue, so adding a
15
+ * new YAML strategy spec automatically makes it available here.
16
+ * - The agent's system prompt is composed generically from the selected strategy's
17
17
  * objective, phases, constraints, termination policy and human settings — no
18
- * loop-specific code.
19
- * - The agent then runs the loop against a live Jupyter notebook, where the
20
- * loop state (dataframes, charts, intermediate results) lives outside the
18
+ * strategy-specific code.
19
+ * - The agent then runs the strategy against a live Jupyter notebook, where the
20
+ * strategy state (dataframes, charts, intermediate results) lives outside the
21
21
  * model so each iteration stays small and focused.
22
22
  *
23
23
  * To run this example:
24
24
  * 1. Start the agent-runtimes server.
25
25
  * 2. Select this example from the header dropdown (Agent group).
26
26
  *
27
- * @module examples/AgentLoopExample
27
+ * @module examples/AgentStrategyExample
28
28
  */
29
29
  import { useMemo, useState } from 'react';
30
30
  import { Box } from '@datalayer/primer-addons';
@@ -32,40 +32,40 @@ import { Notebook } from '@datalayer/jupyter-react';
32
32
  import { useNotebookTools } from '../tools/adapters/agent-runtimes/notebookHooks';
33
33
  import { ThemedJupyterProvider, ThemedProvider } from './utils/themedProvider';
34
34
  import { ChatSidebar } from '../chat';
35
- import { listLoops, getLoop, DEFAULT_LOOP } from '../specs';
35
+ import { listStrategies, getStrategy, DEFAULT_STRATEGY } from '../specs';
36
36
  import { useExampleJupyterAgent } from './hooks/useExampleJupyterAgent';
37
37
  import MatplotlibNotebook from './utils/notebooks/Matplotlib.ipynb.json';
38
38
  import { ExampleNotebookToolbar } from './utils/notebookToolbarItems';
39
39
  // Fixed notebook ID
40
- const NOTEBOOK_ID = 'agent-loop-example';
41
- // Use the imported Matplotlib notebook as the working surface for the loop.
40
+ const NOTEBOOK_ID = 'agent-strategy-example';
41
+ // Use the imported Matplotlib notebook as the working surface for the strategy.
42
42
  const NOTEBOOK_CONTENT = MatplotlibNotebook;
43
43
  // Default configuration
44
- const DEFAULT_AGENT_ID = import.meta.env.VITE_AGENT_ID || 'loop-agent-runtime-example';
44
+ const DEFAULT_AGENT_ID = import.meta.env.VITE_AGENT_ID || 'strategy-agent-runtime-example';
45
45
  /**
46
- * Compose an agent system prompt generically from a loop specification.
46
+ * Compose an agent system prompt generically from a strategy specification.
47
47
  *
48
- * This is intentionally spec-driven: every field comes from the loop YAML, so
49
- * the same code drives any loop (data analysis, plan/execute/critic, OODA, ...).
48
+ * This is intentionally spec-driven: every field comes from the strategy YAML, so
49
+ * the same code drives any strategy (data analysis, plan/execute/critic, OODA, ...).
50
50
  */
51
- function buildLoopSystemPrompt(loop) {
51
+ function buildStrategySystemPrompt(strategy) {
52
52
  const lines = [];
53
- lines.push(`You are an agent that operates as a control LOOP rather than answering in a single prompt.`);
54
- lines.push(`Loop: ${loop.name} (${loop.strategy}).`);
55
- if (loop.objective) {
56
- lines.push(`Objective: ${loop.objective}`);
53
+ lines.push(`You are an agent that operates as a control loop rather than answering in a single prompt.`);
54
+ lines.push(`Strategy: ${strategy.name} (${strategy.strategy}).`);
55
+ if (strategy.objective) {
56
+ lines.push(`Objective: ${strategy.objective}`);
57
57
  }
58
- if (loop.phases?.length) {
59
- lines.push(`Each iteration, execute these phases in order: ${loop.phases.join(' → ')}. ` +
58
+ if (strategy.phases?.length) {
59
+ lines.push(`Each iteration, execute these phases in order: ${strategy.phases.join(' → ')}. ` +
60
60
  `Decide the single best next action each iteration; do not try to solve everything at once.`);
61
61
  }
62
- if (loop.constraints?.length) {
63
- lines.push(`Constraints you must respect:\n${loop.constraints
62
+ if (strategy.constraints?.length) {
63
+ lines.push(`Constraints you must respect:\n${strategy.constraints
64
64
  .map(c => `- ${c}`)
65
65
  .join('\n')}`);
66
66
  }
67
- if (loop.termination) {
68
- const t = loop.termination;
67
+ if (strategy.termination) {
68
+ const t = strategy.termination;
69
69
  lines.push(`Termination: stop after at most ${t.maxIterations} iterations, or when the goal is reached.`);
70
70
  if (t.successCriteria?.length) {
71
71
  lines.push(`Success criteria:\n${t.successCriteria.map(s => `- ${s}`).join('\n')}`);
@@ -74,13 +74,13 @@ function buildLoopSystemPrompt(loop) {
74
74
  lines.push(`If blocked: ${t.onBlocked}.`);
75
75
  }
76
76
  }
77
- if (loop.human && loop.human.mode !== 'none') {
78
- lines.push(`Human-in-the-loop: mode "${loop.human.mode}"` +
79
- (loop.human.approvalRequired
80
- ? `; request explicit approval before: ${loop.human.approvalFor.join(', ') || 'sensitive actions'}.`
77
+ if (strategy.human && strategy.human.mode !== 'none') {
78
+ lines.push(`Human-in-the-loop: mode "${strategy.human.mode}"` +
79
+ (strategy.human.approvalRequired
80
+ ? `; request explicit approval before: ${strategy.human.approvalFor.join(', ') || 'sensitive actions'}.`
81
81
  : '.'));
82
82
  }
83
- lines.push(`Keep loop state (dataframes, charts, intermediate results) in the notebook/runtime, not in the prompt. ` +
83
+ lines.push(`Keep strategy state (dataframes, charts, intermediate results) in the notebook/runtime, not in the prompt. ` +
84
84
  `For notebook operations, always use the notebook frontend tools (runCell, readAllCells, readCell, insertCell, updateCell, deleteCells) so actions happen in the live notebook UI. ` +
85
85
  `Use executeCodeInNotebook only for temporary inspection code that should not modify notebook cells.`);
86
86
  return lines.join('\n\n');
@@ -108,9 +108,9 @@ function NotebookUI({ serviceManager }) {
108
108
  return (_jsx(ThemedJupyterProvider, { children: _jsx(Notebook, { nbformat: NOTEBOOK_CONTENT, id: NOTEBOOK_ID, Toolbar: ExampleNotebookToolbar, serviceManager: serviceManager, height: "100%", cellSidebarMargin: 120, startDefaultKernel: true }) }));
109
109
  }
110
110
  /**
111
- * A compact, read-only summary of the selected loop specification.
111
+ * A compact, read-only summary of the selected strategy specification.
112
112
  */
113
- function LoopSummary({ loop }) {
113
+ function StrategySummary({ strategy }) {
114
114
  return (_jsxs(Box, { sx: {
115
115
  mt: 2,
116
116
  p: 2,
@@ -119,7 +119,7 @@ function LoopSummary({ loop }) {
119
119
  borderRadius: 2,
120
120
  bg: 'canvas.default',
121
121
  fontSize: 0,
122
- }, children: [_jsx(Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 2, mb: 1 }, children: loop.phases.map((phase, index) => (_jsxs(Box, { sx: {
122
+ }, children: [_jsx(Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 2, mb: 1 }, children: strategy.phases.map((phase, index) => (_jsxs(Box, { sx: {
123
123
  display: 'flex',
124
124
  alignItems: 'center',
125
125
  gap: 1,
@@ -129,21 +129,21 @@ function LoopSummary({ loop }) {
129
129
  bg: 'neutral.muted',
130
130
  color: 'fg.default',
131
131
  fontWeight: 'bold',
132
- }, children: [index + 1, ". ", phase] }, phase))) }), loop.objective && (_jsxs(Box, { sx: { color: 'fg.muted' }, children: [_jsx("strong", { children: "Objective:" }), " ", loop.objective] })), loop.termination && (_jsxs(Box, { sx: { color: 'fg.muted', mt: 1 }, children: [_jsx("strong", { children: "Max iterations:" }), " ", loop.termination.maxIterations, " \u00B7", ' ', _jsx("strong", { children: "On blocked:" }), " ", loop.termination.onBlocked] }))] }));
132
+ }, children: [index + 1, ". ", phase] }, phase))) }), strategy.objective && (_jsxs(Box, { sx: { color: 'fg.muted' }, children: [_jsx("strong", { children: "Objective:" }), " ", strategy.objective] })), strategy.termination && (_jsxs(Box, { sx: { color: 'fg.muted', mt: 1 }, children: [_jsx("strong", { children: "Max iterations:" }), " ", strategy.termination.maxIterations, ' ', "\u00B7 ", _jsx("strong", { children: "On blocked:" }), " ", strategy.termination.onBlocked] }))] }));
133
133
  }
134
- export function AgentLoopExampleInner({ serviceManager, }) {
135
- // All available loops come from the generated loop catalogue.
136
- const loops = useMemo(() => listLoops(), []);
137
- const [selectedLoopId, setSelectedLoopId] = useState(DEFAULT_LOOP);
138
- const selectedLoop = useMemo(() => getLoop(selectedLoopId) ?? loops[0], [selectedLoopId, loops]);
139
- const systemPrompt = useMemo(() => buildLoopSystemPrompt(selectedLoop), [selectedLoop]);
134
+ export function AgentStrategyExampleInner({ serviceManager, }) {
135
+ // All available strategies come from the generated strategy catalogue.
136
+ const strategies = useMemo(() => listStrategies(), []);
137
+ const [selectedStrategyId, setSelectedStrategyId] = useState(DEFAULT_STRATEGY);
138
+ const selectedStrategy = useMemo(() => getStrategy(selectedStrategyId) ?? strategies[0], [selectedStrategyId, strategies]);
139
+ const systemPrompt = useMemo(() => buildStrategySystemPrompt(selectedStrategy), [selectedStrategy]);
140
140
  // The tools. Which of the chat and the harness runs them depends on
141
- // where the loop turns, and the hook decides that.
141
+ // where the strategy turns, and the hook decides that.
142
142
  const tools = useNotebookTools(NOTEBOOK_ID);
143
143
  const { agentReady, protocol: protocolConfig, chatFrontendTools, error, unavailableReason, createAttempted, } = useExampleJupyterAgent({
144
- exampleId: 'AgentLoopExample',
144
+ exampleId: 'AgentStrategyExample',
145
145
  agentName: DEFAULT_AGENT_ID,
146
- description: `Loop agent (${selectedLoop.name}) for AgentLoopExample`,
146
+ description: `Strategy agent (${selectedStrategy.name}) for AgentStrategyExample`,
147
147
  systemPrompt,
148
148
  serviceManager,
149
149
  frontendTools: tools,
@@ -153,16 +153,16 @@ export function AgentLoopExampleInner({ serviceManager, }) {
153
153
  // The example's own agent, not merely the runtime: on the cloud target the
154
154
  // runtime is ready before this agent has been registered on it.
155
155
  const effectiveReady = agentReady;
156
- const isLoopSelectorDisabled = createAttempted;
156
+ const isStrategySelectorDisabled = createAttempted;
157
157
  // Get notebook tools for ChatSidebar
158
158
  // Build Vercel AI protocol config
159
- // Chat suggestions derived from the selected loop.
159
+ // Chat suggestions derived from the selected strategy.
160
160
  const suggestions = useMemo(() => [
161
161
  {
162
- title: `${selectedLoop.emoji} Run the loop`,
163
- message: selectedLoop.objective
164
- ? `Run the ${selectedLoop.name}. ${selectedLoop.objective}`
165
- : `Run the ${selectedLoop.name} against the data in this notebook.`,
162
+ title: `${selectedStrategy.emoji} Run the strategy`,
163
+ message: selectedStrategy.objective
164
+ ? `Run the ${selectedStrategy.name}. ${selectedStrategy.objective}`
165
+ : `Run the ${selectedStrategy.name} against the data in this notebook.`,
166
166
  },
167
167
  {
168
168
  title: '🔁 Next iteration',
@@ -176,7 +176,7 @@ export function AgentLoopExampleInner({ serviceManager, }) {
176
176
  title: '📊 Summarize findings',
177
177
  message: 'Summarize the key findings and produce a final, decision-oriented report.',
178
178
  },
179
- ], [selectedLoop]);
179
+ ], [selectedStrategy]);
180
180
  return (_jsx(_Fragment, { children: _jsxs(Box, { sx: {
181
181
  // The wrapper is the viewport, not the window: it sits below the
182
182
  // header and beside the sidebar, so viewport units size this box to
@@ -204,9 +204,9 @@ export function AgentLoopExampleInner({ serviceManager, }) {
204
204
  justifyContent: 'space-between',
205
205
  gap: 3,
206
206
  flexWrap: 'wrap',
207
- }, children: [_jsxs(Box, { children: [_jsxs("h1", { style: { margin: 0, fontSize: '1.5rem' }, children: [selectedLoop.emoji, " Agent Loop Example"] }), _jsx("p", { style: { margin: '8px 0 0', color: 'var(--fgColor-muted)' }, children: "Define and launch an agent execution loop \u2014 observe, think, act, evaluate \u2014 over a live notebook." })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, title: isLoopSelectorDisabled
208
- ? 'Loop selector is temporarily disabled while launching the agent'
209
- : 'Choose a loop', children: [_jsx(Box, { as: "label", sx: { fontSize: 1, fontWeight: 'bold' }, children: "Loop" }), _jsx(Box, { as: "select", value: selectedLoopId, onChange: (e) => setSelectedLoopId(e.target.value), disabled: isLoopSelectorDisabled, "aria-label": "Loop specification", sx: {
207
+ }, children: [_jsxs(Box, { children: [_jsxs("h1", { style: { margin: 0, fontSize: '1.5rem' }, children: [selectedStrategy.emoji, " Agent Strategy Example"] }), _jsx("p", { style: { margin: '8px 0 0', color: 'var(--fgColor-muted)' }, children: "Define and launch an agent reasoning strategy \u2014 observe, think, act, evaluate \u2014 over a live notebook." })] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, title: isStrategySelectorDisabled
208
+ ? 'Strategy selector is temporarily disabled while launching the agent'
209
+ : 'Choose a strategy', children: [_jsx(Box, { as: "label", sx: { fontSize: 1, fontWeight: 'bold' }, children: "Strategy" }), _jsx(Box, { as: "select", value: selectedStrategyId, onChange: (e) => setSelectedStrategyId(e.target.value), disabled: isStrategySelectorDisabled, "aria-label": "Strategy specification", sx: {
210
210
  px: 2,
211
211
  py: '6px',
212
212
  fontSize: 1,
@@ -215,7 +215,7 @@ export function AgentLoopExampleInner({ serviceManager, }) {
215
215
  borderColor: 'border.default',
216
216
  bg: 'canvas.default',
217
217
  color: 'fg.default',
218
- }, children: loops.map(loop => (_jsxs("option", { value: loop.id, children: [loop.emoji, " ", loop.name] }, loop.id))) })] })] }), _jsx(LoopSummary, { loop: selectedLoop })] }), _jsx(Box, { sx: {
218
+ }, children: strategies.map(strategy => (_jsxs("option", { value: strategy.id, children: [strategy.emoji, " ", strategy.name] }, strategy.id))) })] })] }), _jsx(StrategySummary, { strategy: selectedStrategy })] }), _jsx(Box, { sx: {
219
219
  flex: 1,
220
220
  display: 'flex',
221
221
  overflow: 'hidden',
@@ -227,7 +227,7 @@ export function AgentLoopExampleInner({ serviceManager, }) {
227
227
  borderColor: 'border.default',
228
228
  borderRadius: 2,
229
229
  overflow: 'hidden',
230
- }, children: _jsx(NotebookUI, { serviceManager: serviceManager }) }) })] }), effectiveReady && (_jsx(ChatSidebar, { title: `${selectedLoop.emoji} ${selectedLoop.name}`, protocol: protocolConfig, position: "right", width: 400, clickOutsideToClose: false, showNewChatButton: true, showClearButton: true, showSettingsButton: true, defaultOpen: true, panelProps: {
230
+ }, children: _jsx(NotebookUI, { serviceManager: serviceManager }) }) })] }), effectiveReady && (_jsx(ChatSidebar, { title: `${selectedStrategy.emoji} ${selectedStrategy.name}`, protocol: protocolConfig, position: "right", width: 400, clickOutsideToClose: false, showNewChatButton: true, showClearButton: true, showSettingsButton: true, defaultOpen: true, panelProps: {
231
231
  protocol: protocolConfig,
232
232
  frontendTools: chatFrontendTools,
233
233
  useStore: false,
@@ -250,7 +250,7 @@ export function AgentLoopExampleInner({ serviceManager, }) {
250
250
  /**
251
251
  * Main example component with Jupyter provider wrapper.
252
252
  */
253
- export function AgentLoopExample({ serviceManager, }) {
254
- return (_jsx(ThemedProvider, { children: _jsx(AgentLoopExampleInner, { serviceManager: serviceManager }) }));
253
+ export function AgentStrategyExample({ serviceManager, }) {
254
+ return (_jsx(ThemedProvider, { children: _jsx(AgentStrategyExampleInner, { serviceManager: serviceManager }) }));
255
255
  }
256
- export default AgentLoopExample;
256
+ export default AgentStrategyExample;
@@ -111,7 +111,7 @@ export const EXAMPLE_ENTRIES = [
111
111
  // `owns-sandbox-control`: the shell is pinned to the browser sandbox, so
112
112
  // the page's selector would offer three targets it cannot move to.
113
113
  ['example', 'loop', 'shell', 'prompt', 'editors', 'owns-sandbox-control']),
114
- makeEntry('AgentLoopExample', () => import('./AgentLoopExample'), 'Define and launch an agent execution loop (observe/think/act/evaluate) over a live notebook, driven by generic loop specs.', ['example', 'agent', 'loop', 'notebook', 'agentspecs']),
114
+ makeEntry('AgentStrategyExample', () => import('./AgentStrategyExample'), 'Define and launch an agent reasoning strategy (a control loop: observe/think/act/evaluate) over a live notebook, driven by generic strategy specs.', ['example', 'agent', 'strategy', 'notebook', 'agentspecs']),
115
115
  makeEntry('AgentToolApprovalsExample', () => import('./AgentToolApprovalsExample'), 'Tool approval workflows and manual decisions.'),
116
116
  makeEntry('AgentMemoryExample', () => import('./AgentMemoryExample'), 'Memory-aware conversation and retrieval example.'),
117
117
  makeEntry('AgentSkillsExample', () => import('./AgentSkillsExample'), 'Skills discovery, execution, and monitoring example.'),
@@ -28,7 +28,7 @@ export { default as AgentEvalsExample } from './AgentEvalsExample';
28
28
  export { default as AgentGuardrailsExample } from './AgentGuardrailsExample';
29
29
  export { default as AgentHooksExample } from './AgentHooksExample';
30
30
  export { default as AgentInferenceProviderExample } from './AgentInferenceProviderExample';
31
- export { default as AgentLoopExample } from './AgentLoopExample';
31
+ export { default as AgentStrategyExample } from './AgentStrategyExample';
32
32
  export { default as AgentToolApprovalsExample } from './AgentToolApprovalsExample';
33
33
  export { default as AgentMemoryExample } from './AgentMemoryExample';
34
34
  export { default as AgentOtelExample } from './AgentOtelExample';
@@ -34,7 +34,7 @@ export { default as AgentEvalsExample } from './AgentEvalsExample';
34
34
  export { default as AgentGuardrailsExample } from './AgentGuardrailsExample';
35
35
  export { default as AgentHooksExample } from './AgentHooksExample';
36
36
  export { default as AgentInferenceProviderExample } from './AgentInferenceProviderExample';
37
- export { default as AgentLoopExample } from './AgentLoopExample';
37
+ export { default as AgentStrategyExample } from './AgentStrategyExample';
38
38
  export { default as AgentToolApprovalsExample } from './AgentToolApprovalsExample';
39
39
  export { default as AgentMemoryExample } from './AgentMemoryExample';
40
40
  export { default as AgentOtelExample } from './AgentOtelExample';
@@ -86,7 +86,7 @@ const ANONYMOUS_EXAMPLES = new Set([
86
86
  ]);
87
87
  const getExampleGroup = (id) => {
88
88
  if (id === 'AgentspecsExample' ||
89
- id === 'AgentLoopExample' ||
89
+ id === 'AgentStrategyExample' ||
90
90
  id === 'LoopWorkspaceExample' ||
91
91
  id === 'LoopShellExample' ||
92
92
  id === 'DecksAgent') {
@@ -1068,12 +1068,12 @@ const ExampleAppThemed = ({ selectedExample, isChangingExample, error, ExampleCo
1068
1068
  for (const [groupName, examples] of groups) {
1069
1069
  examples.sort((left, right) => {
1070
1070
  if (groupName === 'Loop') {
1071
- // The shells first, most naked first; then the loop that drives a
1071
+ // The shells first, most naked first; then the strategy that drives a
1072
1072
  // notebook; then the library of specs behind them all.
1073
1073
  const LOOP_ORDER = [
1074
1074
  'LoopShellExample',
1075
1075
  'LoopWorkspaceExample',
1076
- 'AgentLoopExample',
1076
+ 'AgentStrategyExample',
1077
1077
  'AgentspecsExample',
1078
1078
  ];
1079
1079
  const loopOrder = (id) => {
@@ -0,0 +1,35 @@
1
+ /** Where a conversation without an account runs: the visitors' runtime, and what it is for. */
2
+ export type DatalayerVisitors = {
3
+ /** The visitors' runtime, as the chat addresses it. */
4
+ url: string;
5
+ /** The application the visitor's token is for: an example's id, or `at:<slug>`. */
6
+ app: string;
7
+ /** ai-inference, which mints the visitor's token. */
8
+ inferenceUrl: string;
9
+ };
10
+ /** A visitor's id: what ai-inference accepts (8 to 64 letters, digits or dashes). */
11
+ export declare function isVisitorId(value: string | null | undefined): value is string;
12
+ /** The id this tab keeps for its visitor, or `''` until ai-inference made one. */
13
+ export declare function keptVisitorId(): string;
14
+ /** A visitor's token, and when it is to be minted again. */
15
+ export type VisitorToken = {
16
+ token: string;
17
+ visitor: string;
18
+ /** Epoch milliseconds: the token's own lifetime from now. */
19
+ renewAt: number;
20
+ };
21
+ /** The body ai-inference is asked with. */
22
+ export declare function visitorTokenRequest(app: string, visitor: string): {
23
+ app: string;
24
+ visitor?: string;
25
+ };
26
+ /** Mint a visitor's token for an application; throws with ai-inference's sentence. */
27
+ export declare function fetchVisitorToken(inferenceUrl: string, app: string, visitor?: string, fetcher?: typeof fetch): Promise<VisitorToken>;
28
+ /**
29
+ * The visitor's token for an application while `visitors` is set: minted at
30
+ * once and again before it runs out. `error` says why none could be had.
31
+ */
32
+ export declare function useVisitorToken(visitors: DatalayerVisitors | undefined): {
33
+ token?: string;
34
+ error?: string;
35
+ };
@@ -0,0 +1,114 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * A visitor's token, for a conversation without an account on the visitors'
7
+ * runtime (LOOP R-30).
8
+ *
9
+ * ai-inference mints it (`POST /anonymous/token`) naming the visitor — the id
10
+ * this tab keeps for them, so that a new token is the same visitor, whose
11
+ * session, turns and rate it does not reset — and the one application it is
12
+ * for: an example's id, or `at:<slug>` for an application at its address.
13
+ * It lives minutes, so it is minted again before it runs out: what limits a
14
+ * visitor is on the servers (turns a day, a session's minutes, a rate and a
15
+ * ceiling for the day), not the life of a key.
16
+ *
17
+ * @module loop/apps/visitorToken
18
+ */
19
+ import { useEffect, useState } from 'react';
20
+ /** The id this tab keeps for its visitor. */
21
+ const VISITOR_KEY = 'datalayer-loop-visitor';
22
+ /** Minted again this long before the token runs out. */
23
+ const MARGIN_MS = 30_000;
24
+ /** A visitor's id: what ai-inference accepts (8 to 64 letters, digits or dashes). */
25
+ export function isVisitorId(value) {
26
+ return typeof value === 'string' && /^[A-Za-z0-9-]{8,64}$/.test(value);
27
+ }
28
+ /** The id this tab keeps for its visitor, or `''` until ai-inference made one. */
29
+ export function keptVisitorId() {
30
+ try {
31
+ const kept = window.sessionStorage.getItem(VISITOR_KEY);
32
+ return isVisitorId(kept) ? kept : '';
33
+ }
34
+ catch {
35
+ return '';
36
+ }
37
+ }
38
+ function keepVisitorId(visitor) {
39
+ try {
40
+ window.sessionStorage.setItem(VISITOR_KEY, visitor);
41
+ }
42
+ catch {
43
+ // A tab that keeps nothing is a new visitor at each token.
44
+ }
45
+ }
46
+ /** The body ai-inference is asked with. */
47
+ export function visitorTokenRequest(app, visitor) {
48
+ return isVisitorId(visitor) ? { app, visitor } : { app };
49
+ }
50
+ /** Mint a visitor's token for an application; throws with ai-inference's sentence. */
51
+ export async function fetchVisitorToken(inferenceUrl, app, visitor = keptVisitorId(), fetcher = fetch) {
52
+ const base = inferenceUrl.replace(/\/+$/, '');
53
+ const response = await fetcher(`${base}/api/ai-inference/v1/anonymous/token`, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify(visitorTokenRequest(app, visitor)),
57
+ });
58
+ const body = (await response.json().catch(() => ({})));
59
+ if (!response.ok || !body.token) {
60
+ throw new Error(body.detail ||
61
+ `A visitor's token was refused by ${base} (HTTP ${response.status}).`);
62
+ }
63
+ if (isVisitorId(body.visitor)) {
64
+ keepVisitorId(body.visitor);
65
+ }
66
+ const lifetimeMs = Math.max(60_000, (body.expires_in ?? 300) * 1000);
67
+ return {
68
+ token: body.token,
69
+ visitor: body.visitor ?? visitor,
70
+ renewAt: Date.now() + lifetimeMs - MARGIN_MS,
71
+ };
72
+ }
73
+ /**
74
+ * The visitor's token for an application while `visitors` is set: minted at
75
+ * once and again before it runs out. `error` says why none could be had.
76
+ */
77
+ export function useVisitorToken(visitors) {
78
+ const [state, setState] = useState({});
79
+ const app = visitors?.app;
80
+ const inferenceUrl = visitors?.inferenceUrl;
81
+ useEffect(() => {
82
+ if (!app || !inferenceUrl) {
83
+ setState({});
84
+ return;
85
+ }
86
+ let cancelled = false;
87
+ let timer;
88
+ const mint = () => {
89
+ fetchVisitorToken(inferenceUrl, app)
90
+ .then(minted => {
91
+ if (cancelled) {
92
+ return;
93
+ }
94
+ setState({ token: minted.token });
95
+ timer = setTimeout(mint, Math.max(5_000, minted.renewAt - Date.now()));
96
+ })
97
+ .catch((error) => {
98
+ if (!cancelled) {
99
+ setState({
100
+ error: error instanceof Error ? error.message : String(error),
101
+ });
102
+ }
103
+ });
104
+ };
105
+ mint();
106
+ return () => {
107
+ cancelled = true;
108
+ if (timer) {
109
+ clearTimeout(timer);
110
+ }
111
+ };
112
+ }, [app, inferenceUrl]);
113
+ return state;
114
+ }
@@ -76,6 +76,7 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
76
76
  preset.showTokenUsage,
77
77
  // By its content: an application's spec written inline is a new object each render.
78
78
  JSON.stringify(preset.datalayerCreatePayload ?? null),
79
+ JSON.stringify(preset.datalayerVisitors ?? null),
79
80
  preset.defaultEditor,
80
81
  preset.showViewSelector,
81
82
  preset.hideChatHeader,
@@ -22,6 +22,9 @@ export function DatalayerAgentBridge() {
22
22
  const agentsConfig = reactor.getConfig(AGENTS_PLUGIN_NAME);
23
23
  const agentSpecId = agentsConfig?.datalayerAgentSpecId ?? DEFAULT_DATALAYER_AGENTSPEC;
24
24
  const createPayload = agentsConfig?.datalayerCreatePayload;
25
+ // A conversation without an account (LOOP R-30): the agent is already on
26
+ // the visitors' runtime, kept warm there; nothing is allocated or made.
27
+ const visitors = agentsConfig?.datalayerVisitors;
25
28
  const service = useOptionalSandboxService();
26
29
  const target = useSignalValue(service?.target ?? IDLE_SANDBOX_TARGET_SIGNAL);
27
30
  const onDatalayer = target === 'datalayer';
@@ -65,13 +68,22 @@ export function DatalayerAgentBridge() {
65
68
  agentSpecId,
66
69
  agentConfig,
67
70
  variant: 'cloud-pydanticai',
68
- autoStart: onDatalayer,
69
- autoCreateAgent: onDatalayer,
71
+ autoStart: onDatalayer && !visitors,
72
+ autoCreateAgent: onDatalayer && !visitors,
70
73
  });
71
74
  useEffect(() => {
72
75
  if (!service || !onDatalayer) {
73
76
  return;
74
77
  }
78
+ if (visitors) {
79
+ service.report({
80
+ variant: 'datalayer',
81
+ // No sandbox: a visitor's application runs no code.
82
+ sandbox_running: false,
83
+ agent_base_url: visitors.url,
84
+ });
85
+ return;
86
+ }
75
87
  if (error) {
76
88
  // The reason travels with the state. "connected-dead" and a column of
77
89
  // "unknown" is what a reader saw before, which names the symptom and
@@ -106,7 +118,7 @@ export function DatalayerAgentBridge() {
106
118
  // host's server for an agent that is not on it.
107
119
  agent_base_url: runtime.agentBaseUrl,
108
120
  });
109
- }, [service, onDatalayer, runtime, status, error]);
121
+ }, [service, onDatalayer, runtime, status, error, visitors]);
110
122
  return null;
111
123
  }
112
124
  export default DatalayerAgentBridge;
@@ -1,3 +1,21 @@
1
+ /**
2
+ * The Agents plugin: where the agent runs, and the sandbox that comes with it.
3
+ *
4
+ * It was the "code sandbox" plugin, and the rename follows what the choice
5
+ * came to mean. Picking a target used to say only where *code* ran; now it
6
+ * decides where the *agent* runs too — in this page with the Vercel AI SDK, or
7
+ * on a server with pydantic-ai — because the location is what a harness can
8
+ * be, whatever a spec asks for. One control, one answer.
9
+ *
10
+ * It owns the sandbox and publishes it as a service; the notebook and document
11
+ * plugins depend on this plugin rather than on a connection of their own.
12
+ *
13
+ * Which agentspec the session runs is a different question, and belongs to
14
+ * `@datalayer/loop-plugin-agentspecs`.
15
+ *
16
+ * @module loop/plugins/agents/plugin
17
+ */
18
+ import type { DatalayerVisitors } from '../../apps/visitorToken';
1
19
  import type { ReactorSlotComponent } from '@datalayer/reactor/react';
2
20
  import type { ServiceManagerSource } from './browserService';
3
21
  import { type TeamSelection } from './team';
@@ -79,6 +97,12 @@ export type AgentsConfig = {
79
97
  * decides its tool calls by its rules (LOOP R-05, R-03).
80
98
  */
81
99
  datalayerCreatePayload?: Record<string, unknown>;
100
+ /**
101
+ * A conversation without an account (LOOP R-30): the application's agent
102
+ * is already on the visitors' runtime, so nothing is allocated or created,
103
+ * and the chat speaks to it with a visitor's token for the application.
104
+ */
105
+ datalayerVisitors?: DatalayerVisitors;
82
106
  /**
83
107
  * The target is the host's decision, not the reader's: started there even
84
108
  * with the control hidden. For an application that runs where it is
@@ -2,23 +2,6 @@
2
2
  * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- /**
6
- * The Agents plugin: where the agent runs, and the sandbox that comes with it.
7
- *
8
- * It was the "code sandbox" plugin, and the rename follows what the choice
9
- * came to mean. Picking a target used to say only where *code* ran; now it
10
- * decides where the *agent* runs too — in this page with the Vercel AI SDK, or
11
- * on a server with pydantic-ai — because the location is what a harness can
12
- * be, whatever a spec asks for. One control, one answer.
13
- *
14
- * It owns the sandbox and publishes it as a service; the notebook and document
15
- * plugins depend on this plugin rather than on a connection of their own.
16
- *
17
- * Which agentspec the session runs is a different question, and belongs to
18
- * `@datalayer/loop-plugin-agentspecs`.
19
- *
20
- * @module loop/plugins/agents/plugin
21
- */
22
5
  import { ServerIcon } from '@primer/octicons-react';
23
6
  import { contribution, definePlugin } from '@datalayer/reactor';
24
7
  import { LoopAgentBlueprint, LoopAgentGate, LoopCommand, LoopSlots, LoopViewType, } from '../../core';
@@ -36,6 +36,8 @@ import { CHAT_PLUGIN_NAME } from './index';
36
36
  import { INPUT_PROMPT_PLUGIN_NAME, } from '../input-prompt';
37
37
  import { agentRuntimeStore, useAgentRuntimeContextSnapshot, } from '../../../stores';
38
38
  import { useIAMStore } from '../../../state';
39
+ import { useVisitorToken } from '../../apps/visitorToken';
40
+ import { AGENTS_PLUGIN_NAME } from '../agents/plugin';
39
41
  import { useConfig } from '../../../hooks/useConfig';
40
42
  import { useSkills, useSkillActions } from '../../../hooks/useSkills';
41
43
  import { AI_MODEL_CATALOGUE, isChatModel } from '../../../specs/models';
@@ -1137,7 +1139,14 @@ export default function ChatView({ workspace }) {
1137
1139
  * chat says so with the person's token. The embed hands its own.
1138
1140
  */
1139
1141
  const runsApp = Boolean(blueprintTurn?.createPayload?.app_spec);
1140
- const iamToken = useIAMStore(state => state.token);
1142
+ const memberToken = useIAMStore(state => state.token);
1143
+ /*
1144
+ * Without an account (LOOP R-30) the application runs on the visitors'
1145
+ * runtime, which answers a visitor's token for it and nobody signed in.
1146
+ */
1147
+ const visitors = reactor.getConfig(AGENTS_PLUGIN_NAME)?.datalayerVisitors;
1148
+ const visitor = useVisitorToken(visitors);
1149
+ const iamToken = visitors ? visitor.token : memberToken;
1141
1150
  const protocol = useMemo(() => inPage
1142
1151
  ? browserProtocolConfig({
1143
1152
  agentId: member?.specId ?? agentId,
@@ -137,6 +137,8 @@ export type LoopPresetOptions = {
137
137
  datalayerAgentSpecId?: string;
138
138
  /** What the agent on that runtime is created with besides its spec — an `app_spec`. */
139
139
  datalayerCreatePayload?: Record<string, unknown>;
140
+ /** A conversation without an account, on the visitors' runtime (LOOP R-30). */
141
+ datalayerVisitors?: AgentsConfig['datalayerVisitors'];
140
142
  /** The target is the host's to fix, even with the agent control hidden. */
141
143
  targetFixed?: boolean;
142
144
  /** The theme the conversation wears, by name; the person's unless said. */
@@ -52,7 +52,7 @@ import { LoopPageLayoutPlugin } from './plugins/page-layout';
52
52
  * be installed or signed into, and the switches a demonstration wants are off.
53
53
  */
54
54
  export function loopPlugins(options = {}) {
55
- const { serverUrl, target = 'browser', defaultEditor: askedEditor, showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, suggestionLabels = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutArrangement = 'page', pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, datalayerAgentSpecId, datalayerCreatePayload, targetFixed, themeVariant, themeOverrides, colorMode, presence, showTokenUsage, floatingPrompt = false, editorSelector = false, editors = true, conversation = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
55
+ const { serverUrl, target = 'browser', defaultEditor: askedEditor, showViewSelector = true, hideChatHeader = false, chatHeaderButtons = false, promptPlacement, autoFocusPrompt = true, suggestionLabels = true, fullScreenTopOffset = 0, showAgentVariants = false, agentSummary = true, teamId, teamPicker = true, pageLayout = false, pageLayoutArrangement = 'page', pageLayoutTurnPanel = 'below', pageLayoutPrompt = 'docked', pageLayoutPromptAnchor = 'top', pageLayoutSize, pageLayoutTurnPanelFooter = 'full', localAgent, localAgentSpec, datalayerAgentSpecId, datalayerCreatePayload, datalayerVisitors, targetFixed, themeVariant, themeOverrides, colorMode, presence, showTokenUsage, floatingPrompt = false, editorSelector = false, editors = true, conversation = true, graph = false, commandPalette = false, pluginsPanel = false, windowFrame = false, firstPromptHook, } = options;
56
56
  const defaultEditor = askedEditor ?? (editors ? 'notebook' : 'none');
57
57
  // A workspace without a conversation: the theme, the shell's points, and
58
58
  // the host's chrome; its view is a host's plugin's.
@@ -111,6 +111,7 @@ export function loopPlugins(options = {}) {
111
111
  target,
112
112
  datalayerAgentSpecId,
113
113
  datalayerCreatePayload,
114
+ datalayerVisitors,
114
115
  targetFixed,
115
116
  showAgentVariants,
116
117
  showAgentSummary: agentSummary,