@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.
- package/lib/examples/AgentStrategyExample.d.ts +12 -0
- package/lib/examples/{AgentLoopExample.js → AgentStrategyExample.js} +62 -62
- package/lib/examples/example-selector.js +1 -1
- package/lib/examples/index.d.ts +1 -1
- package/lib/examples/index.js +1 -1
- package/lib/examples/main.js +3 -3
- package/lib/loop/apps/visitorToken.d.ts +35 -0
- package/lib/loop/apps/visitorToken.js +114 -0
- package/lib/loop/embed/LoopEmbed.js +1 -0
- package/lib/loop/plugins/agents/DatalayerAgentBridge.js +15 -3
- package/lib/loop/plugins/agents/plugin.d.ts +24 -0
- package/lib/loop/plugins/agents/plugin.js +0 -17
- package/lib/loop/plugins/chat/ChatView.js +10 -1
- package/lib/loop/presets.d.ts +2 -0
- package/lib/loop/presets.js +2 -1
- package/lib/specs/index.d.ts +1 -1
- package/lib/specs/index.js +1 -1
- package/lib/specs/strategies.d.ts +34 -0
- package/lib/specs/{loops.js → strategies.js} +30 -30
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.js +1 -1
- package/lib/types/{loops.d.ts → strategies.d.ts} +17 -17
- package/package.json +2 -2
- package/scripts/codegen/generate_agents.py +1 -1
- package/scripts/codegen/{generate_loops.py → generate_strategies.py} +81 -81
- package/lib/examples/AgentLoopExample.d.ts +0 -12
- package/lib/specs/loops.d.ts +0 -34
- /package/lib/types/{loops.js → strategies.js} +0 -0
|
@@ -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
|
-
*
|
|
7
|
+
* AgentStrategyExample - Define and launch an agent reasoning *strategy*.
|
|
8
8
|
*
|
|
9
|
-
* This example demonstrates the
|
|
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
|
-
*
|
|
11
|
+
* strategy specifications defined in agentspecs (`src/specs/strategies.ts`):
|
|
12
12
|
*
|
|
13
|
-
* - Pick any
|
|
14
|
-
* Human-in-the-Loop). The list is read from the
|
|
15
|
-
* new YAML
|
|
16
|
-
* - The agent's system prompt is composed generically from the selected
|
|
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
|
-
*
|
|
19
|
-
* - The agent then runs the
|
|
20
|
-
*
|
|
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/
|
|
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 {
|
|
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-
|
|
41
|
-
// Use the imported Matplotlib notebook as the working surface for the
|
|
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 || '
|
|
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
|
|
46
|
+
* Compose an agent system prompt generically from a strategy specification.
|
|
47
47
|
*
|
|
48
|
-
* This is intentionally spec-driven: every field comes from the
|
|
49
|
-
* the same code drives any
|
|
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
|
|
51
|
+
function buildStrategySystemPrompt(strategy) {
|
|
52
52
|
const lines = [];
|
|
53
|
-
lines.push(`You are an agent that operates as a control
|
|
54
|
-
lines.push(`
|
|
55
|
-
if (
|
|
56
|
-
lines.push(`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 (
|
|
59
|
-
lines.push(`Each iteration, execute these phases in order: ${
|
|
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 (
|
|
63
|
-
lines.push(`Constraints you must respect:\n${
|
|
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 (
|
|
68
|
-
const t =
|
|
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 (
|
|
78
|
-
lines.push(`Human-in-the-loop: mode "${
|
|
79
|
-
(
|
|
80
|
-
? `; request explicit approval before: ${
|
|
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
|
|
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
|
|
111
|
+
* A compact, read-only summary of the selected strategy specification.
|
|
112
112
|
*/
|
|
113
|
-
function
|
|
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:
|
|
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))) }),
|
|
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
|
|
135
|
-
// All available
|
|
136
|
-
const
|
|
137
|
-
const [
|
|
138
|
-
const
|
|
139
|
-
const systemPrompt = useMemo(() =>
|
|
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
|
|
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: '
|
|
144
|
+
exampleId: 'AgentStrategyExample',
|
|
145
145
|
agentName: DEFAULT_AGENT_ID,
|
|
146
|
-
description: `
|
|
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
|
|
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
|
|
159
|
+
// Chat suggestions derived from the selected strategy.
|
|
160
160
|
const suggestions = useMemo(() => [
|
|
161
161
|
{
|
|
162
|
-
title: `${
|
|
163
|
-
message:
|
|
164
|
-
? `Run the ${
|
|
165
|
-
: `Run the ${
|
|
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
|
-
], [
|
|
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: [
|
|
208
|
-
? '
|
|
209
|
-
: 'Choose a
|
|
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:
|
|
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: `${
|
|
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
|
|
254
|
-
return (_jsx(ThemedProvider, { children: _jsx(
|
|
253
|
+
export function AgentStrategyExample({ serviceManager, }) {
|
|
254
|
+
return (_jsx(ThemedProvider, { children: _jsx(AgentStrategyExampleInner, { serviceManager: serviceManager }) }));
|
|
255
255
|
}
|
|
256
|
-
export default
|
|
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('
|
|
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.'),
|
package/lib/examples/index.d.ts
CHANGED
|
@@ -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
|
|
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';
|
package/lib/examples/index.js
CHANGED
|
@@ -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
|
|
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';
|
package/lib/examples/main.js
CHANGED
|
@@ -86,7 +86,7 @@ const ANONYMOUS_EXAMPLES = new Set([
|
|
|
86
86
|
]);
|
|
87
87
|
const getExampleGroup = (id) => {
|
|
88
88
|
if (id === 'AgentspecsExample' ||
|
|
89
|
-
id === '
|
|
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
|
|
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
|
-
'
|
|
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
|
|
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,
|
package/lib/loop/presets.d.ts
CHANGED
|
@@ -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. */
|
package/lib/loop/presets.js
CHANGED
|
@@ -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,
|