@datalayer/agent-runtimes 1.3.61 → 1.3.62
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/AgentA2ATeamExample.d.ts +23 -0
- package/lib/examples/AgentA2ATeamExample.js +279 -0
- package/lib/examples/ChatAssistantExample.js +1 -1
- package/lib/examples/ChatAssistantGalleryExample.js +1 -1
- package/lib/examples/LoopStrategyExample.d.ts +12 -0
- package/lib/examples/{AgentStrategyExample.js → LoopStrategyExample.js} +9 -9
- package/lib/examples/example-selector.js +2 -1
- package/lib/examples/index.d.ts +2 -1
- package/lib/examples/index.js +2 -1
- package/lib/examples/main.js +3 -3
- package/lib/hooks/useA2A.d.ts +37 -1
- package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +60 -0
- package/lib/runtimes/browser/a2aPeer.d.ts +93 -0
- package/lib/runtimes/browser/a2aPeer.js +241 -0
- package/lib/specs/actions.js +54 -0
- package/lib/specs/apps.d.ts +2 -0
- package/lib/specs/apps.js +387 -0
- package/lib/specs/teams/teams.d.ts +1 -0
- package/lib/specs/teams/teams.js +80 -0
- package/lib/types/teams.d.ts +20 -0
- package/package.json +2 -2
- package/scripts/codegen/generate_teams.py +42 -3
- package/lib/examples/AgentStrategyExample.d.ts +0 -12
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A team of two applications over A2A (agentspecs `sales-and-accounting`).
|
|
3
|
+
*
|
|
4
|
+
* Sales runs here, in the browser: its instructions and model turn in the
|
|
5
|
+
* page (the Vercel AI SDK's loop, the in-browser harness), and its one tool
|
|
6
|
+
* asks Accounting over A2A with `@a2a-js/sdk` 1.x. Accounting runs on a
|
|
7
|
+
* runtime, served with fasta2a: its agent reads the Odoo books through the
|
|
8
|
+
* `odoo-accounting` toolset, and only reads them.
|
|
9
|
+
*
|
|
10
|
+
* Both are on screen as Office Assistant characters, Sales the paper clip and
|
|
11
|
+
* Accounting the wizard, as their Appspecs say. The exchange is shown as it
|
|
12
|
+
* happens: Sales asks, Accounting works and then answers, each in its own
|
|
13
|
+
* state and balloon. Then the report appears.
|
|
14
|
+
*
|
|
15
|
+
* Where Accounting is: `VITE_A2A_ACCOUNTING_URL`, else the examples' local
|
|
16
|
+
* server (`…:8765/api/v1/a2a/agents/accounting`), which answers this machine
|
|
17
|
+
* without a key. A cloud runtime needs a key granted to its route:
|
|
18
|
+
* `VITE_A2A_ACCOUNTING_KEY`, which `examples/sales-accounting-a2a/make_temp_key.py`
|
|
19
|
+
* writes to `.env.local`, never committed.
|
|
20
|
+
*/
|
|
21
|
+
import type { JSX } from 'react';
|
|
22
|
+
export declare function AgentA2ATeamExample(): JSX.Element;
|
|
23
|
+
export default AgentA2ATeamExample;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Button, Flash, Heading, Label, Text, Textarea } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { stepCountIs, ToolLoopAgent } from 'ai';
|
|
6
|
+
import { Streamdown } from 'streamdown';
|
|
7
|
+
import { ThemedProvider } from './utils/themedProvider';
|
|
8
|
+
import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
|
|
9
|
+
import { AssistantStage } from '../chat/assistant/AssistantStage';
|
|
10
|
+
import { useBrowserInference } from '../hooks/useBrowserInference';
|
|
11
|
+
import { createBrowserModel } from '../runtimes/browser/model';
|
|
12
|
+
import { a2aPeerTool, connectA2APeer, } from '../runtimes/browser/a2aPeer';
|
|
13
|
+
import { ACCOUNTING_APP_0_0_1, SALES_APP_0_0_1 } from '../specs/apps';
|
|
14
|
+
import { SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1 } from '../specs/teams/teams';
|
|
15
|
+
const TEAM = SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1;
|
|
16
|
+
const SALES = SALES_APP_0_0_1;
|
|
17
|
+
const ACCOUNTING = ACCOUNTING_APP_0_0_1;
|
|
18
|
+
/** The name Sales's tool goes by, as its instructions call it. */
|
|
19
|
+
const ASK_TOOL = 'ask_accounting';
|
|
20
|
+
const STAGE_SIZE = 104;
|
|
21
|
+
const STAGE_HEIGHT = 300;
|
|
22
|
+
const AT_REST = { state: 'idle', insist: false, away: false };
|
|
23
|
+
/** A line short enough for a balloon. */
|
|
24
|
+
function line(text, length = 120) {
|
|
25
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
26
|
+
return flat.length > length ? `${flat.slice(0, length - 1)}…` : flat;
|
|
27
|
+
}
|
|
28
|
+
/** The members of the team, as the team spec says them. */
|
|
29
|
+
function members() {
|
|
30
|
+
const sales = TEAM.agents.find(member => member.id === TEAM.entry);
|
|
31
|
+
const asked = sales?.talksTo?.find(link => link.over === 'a2a');
|
|
32
|
+
const accounting = TEAM.agents.find(member => member.id === asked?.member);
|
|
33
|
+
if (!sales || !accounting) {
|
|
34
|
+
throw new Error(`The team ${TEAM.id} does not say who a person talks to and whom it asks over A2A.`);
|
|
35
|
+
}
|
|
36
|
+
return { sales, accounting };
|
|
37
|
+
}
|
|
38
|
+
/** One persona at its desk, with its name and where it runs under it. */
|
|
39
|
+
function Desk({ app, persona, where, left, onToggle, onAway, }) {
|
|
40
|
+
const stageRef = useRef(null);
|
|
41
|
+
const character = app.interface.assistant ?? 'paperclip';
|
|
42
|
+
return (_jsxs(Box, { sx: {
|
|
43
|
+
position: 'absolute',
|
|
44
|
+
left,
|
|
45
|
+
top: 0,
|
|
46
|
+
width: 220,
|
|
47
|
+
height: STAGE_HEIGHT,
|
|
48
|
+
}, "data-team-member": app.id, "data-member-state": persona.state, children: [persona.away ? (_jsxs(Button, { size: "small", sx: { position: 'absolute', top: STAGE_HEIGHT - 150 }, onClick: () => onAway(false), children: ["Call ", app.name, " back"] })) : (_jsx(AssistantStage, { character: character, state: persona.state, size: STAGE_SIZE, place: { position: 'absolute', left: 56, top: STAGE_HEIGHT - 170 }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: onToggle, balloon: persona.saying
|
|
49
|
+
? { text: persona.saying, more: persona.saying.endsWith('…') }
|
|
50
|
+
: undefined, insist: persona.insist, onDismiss: away => onAway(away !== 'none') })), _jsxs(Box, { sx: {
|
|
51
|
+
position: 'absolute',
|
|
52
|
+
bottom: 0,
|
|
53
|
+
width: '100%',
|
|
54
|
+
textAlign: 'center',
|
|
55
|
+
}, children: [_jsxs(Text, { sx: { fontWeight: 600, display: 'block' }, children: [app.emoji, " ", app.name] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: where })] })] }));
|
|
56
|
+
}
|
|
57
|
+
function AgentA2ATeam() {
|
|
58
|
+
const { sales: salesMember } = useMemo(members, []);
|
|
59
|
+
const runtimeUrl = useExampleAgentRuntimesUrl();
|
|
60
|
+
const accountingUrl = import.meta.env.VITE_A2A_ACCOUNTING_URL ||
|
|
61
|
+
`${runtimeUrl}/api/v1/a2a/agents/${ACCOUNTING.id}`;
|
|
62
|
+
const accountingKey = import.meta.env.VITE_A2A_ACCOUNTING_KEY || undefined;
|
|
63
|
+
const { inference, needsSignIn } = useBrowserInference(true);
|
|
64
|
+
const [peer, setPeer] = useState(null);
|
|
65
|
+
const [peerError, setPeerError] = useState(null);
|
|
66
|
+
const [sales, setSales] = useState({
|
|
67
|
+
...AT_REST,
|
|
68
|
+
state: 'greeting',
|
|
69
|
+
saying: line(SALES.interface.welcome ?? ''),
|
|
70
|
+
insist: true,
|
|
71
|
+
});
|
|
72
|
+
const [accounting, setAccounting] = useState(AT_REST);
|
|
73
|
+
const [turns, setTurns] = useState([]);
|
|
74
|
+
const [report, setReport] = useState(null);
|
|
75
|
+
const [exchange, setExchange] = useState([]);
|
|
76
|
+
const [draft, setDraft] = useState('');
|
|
77
|
+
const [busy, setBusy] = useState(false);
|
|
78
|
+
const composer = useRef(null);
|
|
79
|
+
const history = useRef([]);
|
|
80
|
+
const abort = useRef(null);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
let current = true;
|
|
83
|
+
setPeer(null);
|
|
84
|
+
setPeerError(null);
|
|
85
|
+
connectA2APeer({ url: accountingUrl, key: accountingKey })
|
|
86
|
+
.then(found => current && setPeer(found))
|
|
87
|
+
.catch(error => current &&
|
|
88
|
+
setPeerError(error instanceof Error ? error.message : String(error)));
|
|
89
|
+
return () => {
|
|
90
|
+
current = false;
|
|
91
|
+
};
|
|
92
|
+
}, [accountingUrl, accountingKey]);
|
|
93
|
+
// What Accounting does, as it does it: its own state and balloon.
|
|
94
|
+
const onPeerEvent = useCallback((event) => {
|
|
95
|
+
if (event.phase === 'asked') {
|
|
96
|
+
setExchange(prev => [...prev, `Sales → Accounting: ${event.request}`]);
|
|
97
|
+
setAccounting(prev => ({
|
|
98
|
+
...prev,
|
|
99
|
+
state: 'greeting',
|
|
100
|
+
saying: 'On it. Let me read the books.',
|
|
101
|
+
insist: true,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
else if (event.phase === 'working') {
|
|
105
|
+
if (event.note) {
|
|
106
|
+
setExchange(prev => [...prev, `Accounting: ${event.note}`]);
|
|
107
|
+
}
|
|
108
|
+
setAccounting(prev => ({
|
|
109
|
+
...prev,
|
|
110
|
+
state: 'working',
|
|
111
|
+
saying: event.note ? line(event.note) : prev.saying,
|
|
112
|
+
insist: Boolean(event.note) || prev.insist,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
else if (event.phase === 'answered') {
|
|
116
|
+
setExchange(prev => [...prev, 'Accounting → Sales: the report']);
|
|
117
|
+
setReport(event.answer);
|
|
118
|
+
setAccounting(prev => ({
|
|
119
|
+
...prev,
|
|
120
|
+
state: 'speaking',
|
|
121
|
+
saying: line(event.answer),
|
|
122
|
+
insist: true,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
setExchange(prev => [
|
|
127
|
+
...prev,
|
|
128
|
+
`Accounting could not answer: ${event.error}`,
|
|
129
|
+
]);
|
|
130
|
+
setAccounting(prev => ({
|
|
131
|
+
...prev,
|
|
132
|
+
state: 'idle',
|
|
133
|
+
saying: line(event.error),
|
|
134
|
+
insist: true,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
}, []);
|
|
138
|
+
const agent = useMemo(() => {
|
|
139
|
+
if (!peer) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return new ToolLoopAgent({
|
|
143
|
+
id: SALES.id,
|
|
144
|
+
model: createBrowserModel({
|
|
145
|
+
...inference,
|
|
146
|
+
model: SALES.model || undefined,
|
|
147
|
+
}),
|
|
148
|
+
instructions: SALES.instructions,
|
|
149
|
+
tools: { [ASK_TOOL]: a2aPeerTool({ peer, onEvent: onPeerEvent }) },
|
|
150
|
+
stopWhen: stepCountIs(6),
|
|
151
|
+
});
|
|
152
|
+
}, [peer, inference, onPeerEvent]);
|
|
153
|
+
const send = useCallback(async (text) => {
|
|
154
|
+
const asked = text.trim();
|
|
155
|
+
if (!asked || !agent || busy) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
setBusy(true);
|
|
159
|
+
setDraft('');
|
|
160
|
+
setReport(null);
|
|
161
|
+
setExchange([]);
|
|
162
|
+
setTurns(prev => [...prev, { role: 'user', text: asked }]);
|
|
163
|
+
setSales({
|
|
164
|
+
...AT_REST,
|
|
165
|
+
state: 'thinking',
|
|
166
|
+
saying: 'Let me see…',
|
|
167
|
+
insist: true,
|
|
168
|
+
});
|
|
169
|
+
setAccounting(AT_REST);
|
|
170
|
+
history.current = [...history.current, { role: 'user', content: asked }];
|
|
171
|
+
abort.current = new AbortController();
|
|
172
|
+
let said = '';
|
|
173
|
+
try {
|
|
174
|
+
const result = await agent.stream({
|
|
175
|
+
messages: history.current,
|
|
176
|
+
abortSignal: abort.current.signal,
|
|
177
|
+
});
|
|
178
|
+
for await (const part of result.fullStream) {
|
|
179
|
+
if (part.type === 'tool-call' && part.toolName === ASK_TOOL) {
|
|
180
|
+
const request = String(part.input?.request ?? '');
|
|
181
|
+
setSales(prev => ({
|
|
182
|
+
...prev,
|
|
183
|
+
state: 'waiting',
|
|
184
|
+
saying: line(`Accounting, could you: ${request}`),
|
|
185
|
+
insist: true,
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
else if (part.type === 'tool-result' &&
|
|
189
|
+
part.toolName === ASK_TOOL) {
|
|
190
|
+
setSales(prev => ({
|
|
191
|
+
...prev,
|
|
192
|
+
state: 'thinking',
|
|
193
|
+
saying: 'Thanks!',
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
else if (part.type === 'text-delta') {
|
|
197
|
+
said += part.text;
|
|
198
|
+
setSales(prev => ({
|
|
199
|
+
...prev,
|
|
200
|
+
state: 'speaking',
|
|
201
|
+
saying: line(said),
|
|
202
|
+
insist: true,
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
else if (part.type === 'error') {
|
|
206
|
+
throw part.error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
history.current = [
|
|
210
|
+
...history.current,
|
|
211
|
+
{ role: 'assistant', content: said },
|
|
212
|
+
];
|
|
213
|
+
setTurns(prev => [...prev, { role: 'assistant', text: said }]);
|
|
214
|
+
setSales(prev => ({ ...prev, state: 'idle', saying: line(said) }));
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
setTurns(prev => [
|
|
219
|
+
...prev,
|
|
220
|
+
{ role: 'assistant', text: `Something went wrong: ${message}` },
|
|
221
|
+
]);
|
|
222
|
+
setSales(prev => ({
|
|
223
|
+
...prev,
|
|
224
|
+
state: 'idle',
|
|
225
|
+
saying: line(`Something went wrong: ${message}`),
|
|
226
|
+
insist: true,
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
abort.current = null;
|
|
231
|
+
setBusy(false);
|
|
232
|
+
}
|
|
233
|
+
}, [agent, busy]);
|
|
234
|
+
useEffect(() => () => abort.current?.abort(), []);
|
|
235
|
+
const accountingWhere = accountingKey
|
|
236
|
+
? `on a runtime, ${new URL(accountingUrl).host}, with a temporary key`
|
|
237
|
+
: `on a runtime, ${new URL(accountingUrl).host}`;
|
|
238
|
+
return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [_jsxs(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: [TEAM.emoji, " ", TEAM.name] }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, children: TEAM.description }), needsSignIn && (_jsx(Flash, { variant: "warning", sx: { mb: 3 }, children: "Sales asks its model through the Datalayer inference service: sign in, or wait for a visitor's trial key." })), peerError && (_jsxs(Flash, { variant: "danger", sx: { mb: 3 }, children: ["Accounting is not reachable at ", accountingUrl, ": ", peerError, " Serve it with", ' ', _jsx("code", { children: "python examples/sales-accounting-a2a/serve_accounting.py" }), ' ', "on the local server, or", ' ', _jsx("code", { children: "loop apps run accounting.yaml --cloud --a2a" }), " and set", ' ', _jsx("code", { children: "VITE_A2A_ACCOUNTING_URL" }), "."] })), _jsxs(Box, { sx: {
|
|
239
|
+
position: 'relative',
|
|
240
|
+
height: STAGE_HEIGHT,
|
|
241
|
+
border: '1px solid',
|
|
242
|
+
borderColor: 'border.default',
|
|
243
|
+
borderRadius: 2,
|
|
244
|
+
bg: 'canvas.subtle',
|
|
245
|
+
overflow: 'visible',
|
|
246
|
+
mb: 3,
|
|
247
|
+
}, children: [_jsx(Desk, { app: SALES, persona: sales, where: salesMember.runsIn === 'browser'
|
|
248
|
+
? 'in your browser'
|
|
249
|
+
: 'on a runtime', left: "8%", onToggle: () => composer.current?.focus(), onAway: away => setSales(prev => ({ ...prev, away })) }), _jsxs(Box, { sx: {
|
|
250
|
+
position: 'absolute',
|
|
251
|
+
left: '38%',
|
|
252
|
+
right: '38%',
|
|
253
|
+
top: STAGE_HEIGHT / 2,
|
|
254
|
+
textAlign: 'center',
|
|
255
|
+
}, children: [_jsx(Box, { sx: {
|
|
256
|
+
borderTop: '2px dashed',
|
|
257
|
+
borderColor: peer ? 'accent.emphasis' : 'border.muted',
|
|
258
|
+
} }), _jsxs(Label, { variant: peer ? 'accent' : 'secondary', sx: { mt: 2 }, children: ["A2A ", peer ? `· ${peer.skill.name}` : '· not connected'] })] }), _jsx(Desk, { app: ACCOUNTING, persona: accounting, where: accountingWhere, left: "66%", onToggle: () => undefined, onAway: away => setAccounting(prev => ({ ...prev, away })) })] }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }, children: (SALES.interface.starters ?? []).map(starter => (_jsx(Button, { size: "small", disabled: !agent || busy, onClick: () => void send(starter.message), children: starter.label }, starter.label))) }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
|
|
259
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
260
|
+
event.preventDefault();
|
|
261
|
+
void send(draft);
|
|
262
|
+
}
|
|
263
|
+
} }), _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { variant: "primary", disabled: !agent || busy || !draft.trim(), onClick: () => void send(draft), children: "Ask" }), busy && (_jsx(Button, { onClick: () => abort.current?.abort(), children: "Stop" }))] }), _jsx(Box, { sx: { mt: 3 }, "data-team-conversation": "", children: turns.map((turn, index) => (_jsxs(Box, { sx: {
|
|
264
|
+
p: 2,
|
|
265
|
+
mb: 2,
|
|
266
|
+
borderRadius: 2,
|
|
267
|
+
bg: turn.role === 'user' ? 'accent.subtle' : 'canvas.subtle',
|
|
268
|
+
}, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? 'You' : SALES.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }), _jsxs(Box, { sx: { flex: '1 1 360px', minWidth: 0 }, children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: "The exchange" }), _jsx(Box, { as: "ol", sx: { pl: 3, color: 'fg.muted', fontSize: 1 }, "data-team-exchange": "", children: exchange.length === 0 ? (_jsx(Text, { as: "li", children: "Nothing asked yet." })) : (exchange.map((step, index) => (_jsx(Text, { as: "li", children: step }, index)))) }), _jsx(Heading, { as: "h3", sx: { fontSize: 2, mt: 3, mb: 2 }, children: "The report" }), _jsx(Box, { sx: {
|
|
269
|
+
p: 3,
|
|
270
|
+
border: '1px solid',
|
|
271
|
+
borderColor: 'border.default',
|
|
272
|
+
borderRadius: 2,
|
|
273
|
+
minHeight: 120,
|
|
274
|
+
}, "data-team-report": "", children: report ? (_jsx(Streamdown, { children: report })) : (_jsx(Text, { sx: { color: 'fg.muted' }, children: "The report Accounting returns appears here, as it returned it." })) })] })] })] }));
|
|
275
|
+
}
|
|
276
|
+
export function AgentA2ATeamExample() {
|
|
277
|
+
return (_jsx(ThemedProvider, { children: _jsx(AgentA2ATeam, {}) }));
|
|
278
|
+
}
|
|
279
|
+
export default AgentA2ATeamExample;
|
|
@@ -69,7 +69,7 @@ const ChatAssistantExample = () => {
|
|
|
69
69
|
? assistantCharacterNamed(reactor, character)
|
|
70
70
|
: character;
|
|
71
71
|
const [loadError, setLoadError] = useState();
|
|
72
|
-
return (_jsx(ThemedProvider, { children: _jsxs(Box, { sx: { minHeight: '100vh', bg: 'canvas.default', p: 4 }, children: [_jsxs(Box, { sx: { maxWidth: 720, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "
|
|
72
|
+
return (_jsx(ThemedProvider, { children: _jsxs(Box, { sx: { minHeight: '100vh', bg: 'canvas.default', p: 4 }, children: [_jsxs(Box, { sx: { maxWidth: 720, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Chat Assistant" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mb: 3 }, children: "The chat as a character on the page. It greets you, acts out what the agent is doing, and says the agent\u2019s words in a balloon while the conversation is closed. Click it to talk, drag it to move it, hover it to send it away." }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: CATALOGUE.map(option => (_jsx(Button, { variant: option.id === character ? 'primary' : 'default', onClick: () => {
|
|
73
73
|
setCharacter(option.id);
|
|
74
74
|
setLoadError(undefined);
|
|
75
75
|
}, "aria-pressed": option.id === character, "data-assistant-character": option.id, children: option.name }, option.id))) }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept: ".acs,.js,.png,.gif,.webp,.jpg,.jpeg", "aria-label": "Character files", onChange: async (event) => {
|
|
@@ -142,7 +142,7 @@ const ChatAssistantGalleryExample = () => {
|
|
|
142
142
|
bg: 'canvas.default',
|
|
143
143
|
color: 'fg.default',
|
|
144
144
|
p: 4,
|
|
145
|
-
}, children: _jsxs(Box, { sx: { maxWidth: 1200, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "
|
|
145
|
+
}, children: _jsxs(Box, { sx: { maxWidth: 1200, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Chat Assistant gallery" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 3, maxWidth: 760 }, children: ["Every character in every state the floating assistant acts, with its balloon, in light and dark. Datalayer\u2019s four and the owl are what the enabled plugins contribute to", ' ', _jsx("code", { children: "loop.assistant.character" }), "; Pixel is a test sprite of our own, read through the clippy.js reader. No agent is behind it: the words are a sample conversation."] }), _jsxs(Box, { as: "section", "aria-label": "Gallery controls", sx: {
|
|
146
146
|
display: 'grid',
|
|
147
147
|
gap: 3,
|
|
148
148
|
mb: 4,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ServiceManager } from '@jupyterlab/services';
|
|
2
|
+
interface LoopStrategyExampleInnerProps {
|
|
3
|
+
serviceManager?: ServiceManager.IManager;
|
|
4
|
+
}
|
|
5
|
+
export declare function LoopStrategyExampleInner({ serviceManager, }: LoopStrategyExampleInnerProps): import("react").JSX.Element;
|
|
6
|
+
/**
|
|
7
|
+
* Main example component with Jupyter provider wrapper.
|
|
8
|
+
*/
|
|
9
|
+
export declare function LoopStrategyExample({ serviceManager, }: {
|
|
10
|
+
serviceManager?: ServiceManager.IManager;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
export default LoopStrategyExample;
|
|
@@ -4,7 +4,7 @@ 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
|
+
* LoopStrategyExample - Define and launch an agent reasoning *strategy*.
|
|
8
8
|
*
|
|
9
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
|
|
@@ -24,7 +24,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
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/LoopStrategyExample
|
|
28
28
|
*/
|
|
29
29
|
import { useMemo, useState } from 'react';
|
|
30
30
|
import { Box } from '@datalayer/primer-addons';
|
|
@@ -131,7 +131,7 @@ function StrategySummary({ strategy }) {
|
|
|
131
131
|
fontWeight: 'bold',
|
|
132
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
|
|
134
|
+
export function LoopStrategyExampleInner({ serviceManager, }) {
|
|
135
135
|
// All available strategies come from the generated strategy catalogue.
|
|
136
136
|
const strategies = useMemo(() => listStrategies(), []);
|
|
137
137
|
const [selectedStrategyId, setSelectedStrategyId] = useState(DEFAULT_STRATEGY);
|
|
@@ -141,9 +141,9 @@ export function AgentStrategyExampleInner({ serviceManager, }) {
|
|
|
141
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: 'LoopStrategyExample',
|
|
145
145
|
agentName: DEFAULT_AGENT_ID,
|
|
146
|
-
description: `Strategy agent (${selectedStrategy.name}) for
|
|
146
|
+
description: `Strategy agent (${selectedStrategy.name}) for LoopStrategyExample`,
|
|
147
147
|
systemPrompt,
|
|
148
148
|
serviceManager,
|
|
149
149
|
frontendTools: tools,
|
|
@@ -204,7 +204,7 @@ export function AgentStrategyExampleInner({ 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: [selectedStrategy.emoji, "
|
|
207
|
+
}, children: [_jsxs(Box, { children: [_jsxs("h1", { style: { margin: 0, fontSize: '1.5rem' }, children: [selectedStrategy.emoji, " Loop 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
208
|
? 'Strategy selector is temporarily disabled while launching the agent'
|
|
209
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,
|
|
@@ -250,7 +250,7 @@ export function AgentStrategyExampleInner({ 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 LoopStrategyExample({ serviceManager, }) {
|
|
254
|
+
return (_jsx(ThemedProvider, { children: _jsx(LoopStrategyExampleInner, { serviceManager: serviceManager }) }));
|
|
255
255
|
}
|
|
256
|
-
export default
|
|
256
|
+
export default LoopStrategyExample;
|
|
@@ -113,7 +113,7 @@ export const EXAMPLE_ENTRIES = [
|
|
|
113
113
|
// `owns-sandbox-control`: the shell is pinned to the browser sandbox, so
|
|
114
114
|
// the page's selector would offer three targets it cannot move to.
|
|
115
115
|
['example', 'loop', 'shell', 'prompt', 'editors', 'owns-sandbox-control']),
|
|
116
|
-
makeEntry('
|
|
116
|
+
makeEntry('LoopStrategyExample', () => import('./LoopStrategyExample'), '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']),
|
|
117
117
|
makeEntry('AgentToolApprovalsExample', () => import('./AgentToolApprovalsExample'), 'Tool approval workflows and manual decisions.'),
|
|
118
118
|
makeEntry('AgentMemoryExample', () => import('./AgentMemoryExample'), 'Memory-aware conversation and retrieval example.'),
|
|
119
119
|
makeEntry('AgentSkillsExample', () => import('./AgentSkillsExample'), 'Skills discovery, execution, and monitoring example.'),
|
|
@@ -123,6 +123,7 @@ export const EXAMPLE_ENTRIES = [
|
|
|
123
123
|
makeEntry('AgentMonitoringExample', () => import('./AgentMonitoringExample'), 'Runtime monitoring and live metrics example.'),
|
|
124
124
|
makeEntry('AgentSubagentsExample', () => import('./AgentSubagentsExample'), 'Multi-agent delegation with the in-repo subagents capability.'),
|
|
125
125
|
makeEntry('AgentA2AExample', () => import('./AgentA2AExample'), 'Delegation to separate agents over the A2A protocol, launched locally or on Datalayer runtimes.'),
|
|
126
|
+
makeEntry('AgentA2ATeamExample', () => import('./AgentA2ATeamExample'), 'Two applications as a team over A2A: Sales in the browser (@a2a-js/sdk) asks Accounting on a runtime (fasta2a, Odoo read only), each an Office Assistant character.'),
|
|
126
127
|
makeEntry('AgentNotificationsExample', () => import('./AgentNotificationsExample'), 'Notifications and event routing example.'),
|
|
127
128
|
makeEntry('AgentOutputsExample', () => import('./AgentOutputsExample'), 'Structured outputs and rendering patterns.'),
|
|
128
129
|
makeEntry('AgentParametersExample', () => import('./AgentParametersExample'), 'Launch-time parameterized agent creation with JSON schema.'),
|
package/lib/examples/index.d.ts
CHANGED
|
@@ -26,11 +26,12 @@ export { default as AgentCompactionExample } from './AgentCompactionExample';
|
|
|
26
26
|
export { default as AgentCodemodeExample } from './AgentCodemodeExample';
|
|
27
27
|
export { default as AgentCodeSandboxesExample } from './AgentCodeSandboxesExample';
|
|
28
28
|
export { default as AgentDecideExample } from './AgentDecideExample';
|
|
29
|
+
export { default as AgentA2ATeamExample } from './AgentA2ATeamExample';
|
|
29
30
|
export { default as AgentEvalsExample } from './AgentEvalsExample';
|
|
30
31
|
export { default as AgentGuardrailsExample } from './AgentGuardrailsExample';
|
|
31
32
|
export { default as AgentHooksExample } from './AgentHooksExample';
|
|
32
33
|
export { default as AgentInferenceProviderExample } from './AgentInferenceProviderExample';
|
|
33
|
-
export { default as
|
|
34
|
+
export { default as LoopStrategyExample } from './LoopStrategyExample';
|
|
34
35
|
export { default as AgentToolApprovalsExample } from './AgentToolApprovalsExample';
|
|
35
36
|
export { default as AgentMemoryExample } from './AgentMemoryExample';
|
|
36
37
|
export { default as AgentOtelExample } from './AgentOtelExample';
|
package/lib/examples/index.js
CHANGED
|
@@ -32,11 +32,12 @@ export { default as AgentCompactionExample } from './AgentCompactionExample';
|
|
|
32
32
|
export { default as AgentCodemodeExample } from './AgentCodemodeExample';
|
|
33
33
|
export { default as AgentCodeSandboxesExample } from './AgentCodeSandboxesExample';
|
|
34
34
|
export { default as AgentDecideExample } from './AgentDecideExample';
|
|
35
|
+
export { default as AgentA2ATeamExample } from './AgentA2ATeamExample';
|
|
35
36
|
export { default as AgentEvalsExample } from './AgentEvalsExample';
|
|
36
37
|
export { default as AgentGuardrailsExample } from './AgentGuardrailsExample';
|
|
37
38
|
export { default as AgentHooksExample } from './AgentHooksExample';
|
|
38
39
|
export { default as AgentInferenceProviderExample } from './AgentInferenceProviderExample';
|
|
39
|
-
export { default as
|
|
40
|
+
export { default as LoopStrategyExample } from './LoopStrategyExample';
|
|
40
41
|
export { default as AgentToolApprovalsExample } from './AgentToolApprovalsExample';
|
|
41
42
|
export { default as AgentMemoryExample } from './AgentMemoryExample';
|
|
42
43
|
export { default as AgentOtelExample } from './AgentOtelExample';
|
package/lib/examples/main.js
CHANGED
|
@@ -51,10 +51,10 @@ const EXAMPLE_GROUP_ORDER = [
|
|
|
51
51
|
'A2UI',
|
|
52
52
|
'A2A',
|
|
53
53
|
'AG-UI',
|
|
54
|
-
'Capabilities',
|
|
55
54
|
'Chat',
|
|
56
55
|
'Document',
|
|
57
56
|
'Notebook',
|
|
57
|
+
'Capabilities',
|
|
58
58
|
'Cell',
|
|
59
59
|
'CopilotKit',
|
|
60
60
|
];
|
|
@@ -91,7 +91,7 @@ const ANONYMOUS_EXAMPLES = new Set([
|
|
|
91
91
|
]);
|
|
92
92
|
const getExampleGroup = (id) => {
|
|
93
93
|
if (id === 'AgentspecsExample' ||
|
|
94
|
-
id === '
|
|
94
|
+
id === 'LoopStrategyExample' ||
|
|
95
95
|
id === 'LoopWorkspaceExample' ||
|
|
96
96
|
id === 'LoopShellExample' ||
|
|
97
97
|
id === 'DecksAgent') {
|
|
@@ -1078,7 +1078,7 @@ const ExampleAppThemed = ({ selectedExample, isChangingExample, error, ExampleCo
|
|
|
1078
1078
|
const LOOP_ORDER = [
|
|
1079
1079
|
'LoopShellExample',
|
|
1080
1080
|
'LoopWorkspaceExample',
|
|
1081
|
-
'
|
|
1081
|
+
'LoopStrategyExample',
|
|
1082
1082
|
'AgentspecsExample',
|
|
1083
1083
|
];
|
|
1084
1084
|
const loopOrder = (id) => {
|
package/lib/hooks/useA2A.d.ts
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The A2A objects this hook reads, as the JSON it receives (the 0.3 wire it
|
|
3
|
+
* was written against). Not the SDK's types: `@a2a-js/sdk` 1.x types are the
|
|
4
|
+
* 1.0 protobuf shapes, which `runtimes/browser/a2aPeer` uses with the SDK's
|
|
5
|
+
* own client.
|
|
6
|
+
*/
|
|
7
|
+
export interface A2AWireTextPart {
|
|
8
|
+
kind: 'text';
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
export type A2AWirePart = A2AWireTextPart | {
|
|
12
|
+
kind: string;
|
|
13
|
+
text?: string;
|
|
14
|
+
};
|
|
15
|
+
export interface A2AWireTask {
|
|
16
|
+
id: string;
|
|
17
|
+
contextId: string;
|
|
18
|
+
status: {
|
|
19
|
+
state: string;
|
|
20
|
+
message?: {
|
|
21
|
+
parts?: A2AWirePart[];
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
artifacts?: {
|
|
25
|
+
parts?: A2AWirePart[];
|
|
26
|
+
}[];
|
|
27
|
+
}
|
|
28
|
+
export interface A2AWireAgentCard {
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
capabilities?: {
|
|
32
|
+
streaming?: boolean;
|
|
33
|
+
};
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
type AgentCard = A2AWireAgentCard;
|
|
37
|
+
type Task = A2AWireTask;
|
|
2
38
|
export interface Message {
|
|
3
39
|
role: 'user' | 'assistant' | 'system';
|
|
4
40
|
content: string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Accounting",
|
|
3
|
+
"description": "Answers requests for financial reports, such as open invoices, aged balances, a trial balance or a customer's ledger, from the Odoo books, which it only reads.",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"supportedInterfaces": [
|
|
6
|
+
{
|
|
7
|
+
"protocolBinding": "JSONRPC",
|
|
8
|
+
"url": "http://runtime.test/api/v1/a2a/agents/accounting/",
|
|
9
|
+
"protocolVersion": "1.0"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"skills": [
|
|
13
|
+
{
|
|
14
|
+
"id": "accounting",
|
|
15
|
+
"name": "Accounting",
|
|
16
|
+
"description": "Answers requests for financial reports, such as open invoices, aged balances, a trial balance or a customer's ledger, from the Odoo books, which it only reads.",
|
|
17
|
+
"tags": ["example", "accounting", "finance", "odoo", "a2a", "team"],
|
|
18
|
+
"inputModes": ["text/plain"],
|
|
19
|
+
"outputModes": ["text/plain"],
|
|
20
|
+
"examples": [
|
|
21
|
+
"List the customer invoices that are still open, with the total due.",
|
|
22
|
+
"Give the aged receivables as of today, by customer.",
|
|
23
|
+
"Give the trial balance for last month."
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"defaultInputModes": ["application/json"],
|
|
28
|
+
"defaultOutputModes": ["application/json"],
|
|
29
|
+
"capabilities": {
|
|
30
|
+
"streaming": true,
|
|
31
|
+
"pushNotifications": false,
|
|
32
|
+
"extensions": [
|
|
33
|
+
{
|
|
34
|
+
"uri": "https://datalayer.ai/extensions/orchestration/v1",
|
|
35
|
+
"description": "Pause at a checkpoint and resume from it; steer a task while it works.",
|
|
36
|
+
"required": false,
|
|
37
|
+
"params": {
|
|
38
|
+
"pause": "/api/v1/a2a/pause",
|
|
39
|
+
"steer": "/api/v1/a2a/steer"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"securitySchemes": {
|
|
45
|
+
"datalayer": {
|
|
46
|
+
"httpAuthSecurityScheme": {
|
|
47
|
+
"scheme": "Bearer",
|
|
48
|
+
"bearerFormat": "JWT",
|
|
49
|
+
"description": "A Datalayer key granted to this route: a token exchanged from a task grant whose task is this application's route on this runtime. The machine itself needs none."
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"securityRequirements": [
|
|
54
|
+
{
|
|
55
|
+
"schemes": {
|
|
56
|
+
"datalayer": []
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|