@calliopelabs/ui 0.13.0 → 0.16.0
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/README.md +192 -4
- package/dist/agent/AgentChat.d.ts +63 -0
- package/dist/agent/AgentChat.js +117 -0
- package/dist/agent/AgentLauncher.d.ts +25 -0
- package/dist/agent/AgentLauncher.js +25 -0
- package/dist/agent/AgentProvider.d.ts +82 -0
- package/dist/agent/AgentProvider.js +176 -0
- package/dist/agent/CalliopeAgent.d.ts +43 -0
- package/dist/agent/CalliopeAgent.js +111 -0
- package/dist/agent/WorkflowRenderers.d.ts +61 -0
- package/dist/agent/WorkflowRenderers.js +102 -0
- package/dist/agent/controller.d.ts +167 -0
- package/dist/agent/controller.js +496 -0
- package/dist/agent/copilotkit/CopilotKitAgentProvider.d.ts +41 -0
- package/dist/agent/copilotkit/CopilotKitAgentProvider.js +145 -0
- package/dist/agent/copilotkit/index.d.ts +6 -0
- package/dist/agent/copilotkit/index.js +6 -0
- package/dist/agent/engine.d.ts +41 -0
- package/dist/agent/engine.js +93 -0
- package/dist/agent/index.d.ts +17 -0
- package/dist/agent/index.js +17 -0
- package/dist/agent/mock.d.ts +56 -0
- package/dist/agent/mock.js +143 -0
- package/dist/agent/renderers.d.ts +49 -0
- package/dist/agent/renderers.js +27 -0
- package/dist/agent/surface.d.ts +30 -0
- package/dist/agent/surface.js +90 -0
- package/dist/agent/tool.d.ts +98 -0
- package/dist/agent/tool.js +81 -0
- package/dist/appearance/brands.d.ts +31 -12
- package/dist/appearance/brands.js +32 -51
- package/dist/chat/AgentProgress.d.ts +19 -0
- package/dist/chat/AgentProgress.js +34 -0
- package/dist/chat/Composer.d.ts +31 -0
- package/dist/chat/Composer.js +44 -0
- package/dist/chat/MessageBubble.d.ts +23 -0
- package/dist/chat/MessageBubble.js +26 -0
- package/dist/chat/MessageList.d.ts +43 -0
- package/dist/chat/MessageList.js +71 -0
- package/dist/chat/ModelPicker.d.ts +17 -0
- package/dist/chat/ModelPicker.js +13 -0
- package/dist/chat/ToolCallCard.d.ts +27 -0
- package/dist/chat/ToolCallCard.js +52 -0
- package/dist/chat/model.d.ts +123 -0
- package/dist/chat/model.js +337 -0
- package/dist/chat/patch.d.ts +12 -0
- package/dist/chat/patch.js +149 -0
- package/dist/chat/useAgUiRun.d.ts +52 -0
- package/dist/chat/useAgUiRun.js +137 -0
- package/dist/css/appearance.js +1 -1
- package/dist/css/calliope.css +5 -0
- package/dist/css/tokens.css +5 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.js +16 -1
- package/dist/primitives/Badge.d.ts +14 -0
- package/dist/primitives/Badge.js +25 -0
- package/dist/primitives/Button.d.ts +1 -1
- package/dist/primitives/ContextMenu.d.ts +18 -0
- package/dist/primitives/ContextMenu.js +34 -0
- package/dist/primitives/ScrollArea.d.ts +16 -0
- package/dist/primitives/ScrollArea.js +11 -0
- package/dist/primitives/Toast.d.ts +37 -0
- package/dist/primitives/Toast.js +43 -0
- package/dist/primitives/Tooltip.d.ts +16 -0
- package/dist/primitives/Tooltip.js +26 -0
- package/dist/provider/messages.d.ts +135 -0
- package/dist/provider/messages.js +131 -0
- package/dist/theme/css-layer.js +2 -1
- package/dist/theme/mappings/palette.d.ts +52 -0
- package/dist/theme/mappings/palette.js +208 -0
- package/dist/theme/mappings/render.d.ts +9 -0
- package/dist/theme/mappings/render.js +520 -0
- package/dist/theme/mappings/targets.d.ts +43 -0
- package/dist/theme/mappings/targets.js +222 -0
- package/dist/theme/tokens.d.ts +10 -9
- package/dist/theme/tokens.generated.d.ts +356 -0
- package/dist/theme/tokens.generated.js +360 -0
- package/dist/theme/tokens.js +20 -264
- package/dist/tokens/README.jupyterlab.md +125 -0
- package/dist/tokens/README.md +15 -0
- package/dist/tokens/README.native.md +29 -0
- package/dist/tokens/README.plain.md +84 -0
- package/dist/tokens/README.python.md +33 -0
- package/dist/tokens/README.vscode.md +114 -0
- package/dist/tokens/jupyterlab.css +1710 -0
- package/dist/tokens/plain.css +930 -0
- package/dist/tokens/tokens.native.d.ts +85 -0
- package/dist/tokens/tokens.native.js +773 -0
- package/dist/tokens/tokens.py +1043 -0
- package/dist/tokens/vscode.css +1138 -0
- package/dist/workbench/FileBrowser.d.ts +62 -0
- package/dist/workbench/FileBrowser.js +117 -0
- package/dist/workbench/FileGrid.d.ts +13 -0
- package/dist/workbench/FileGrid.js +114 -0
- package/dist/workbench/FileIcon.d.ts +10 -0
- package/dist/workbench/FileIcon.js +24 -0
- package/dist/workbench/FileList.d.ts +35 -0
- package/dist/workbench/FileList.js +160 -0
- package/dist/workbench/FilePreview.d.ts +66 -0
- package/dist/workbench/FilePreview.js +108 -0
- package/dist/workbench/FileTree.d.ts +37 -0
- package/dist/workbench/FileTree.js +218 -0
- package/dist/workbench/UploadDropzone.d.ts +52 -0
- package/dist/workbench/UploadDropzone.js +115 -0
- package/dist/workbench/dnd.d.ts +17 -0
- package/dist/workbench/dnd.js +77 -0
- package/dist/workbench/index.d.ts +16 -0
- package/dist/workbench/index.js +16 -0
- package/dist/workbench/model.d.ts +72 -0
- package/dist/workbench/model.js +214 -0
- package/dist/workbench/selection.d.ts +16 -0
- package/dist/workbench/selection.js +46 -0
- package/dist/workbench/typeahead.d.ts +15 -0
- package/dist/workbench/typeahead.js +22 -0
- package/package.json +39 -2
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { EventType, } from "@ag-ui/core";
|
|
2
|
+
import { createChatView, reduceChat, } from "../chat/model.js";
|
|
3
|
+
import { userMessageEvents } from "../chat/useAgUiRun.js";
|
|
4
|
+
import { validateToolArgs } from "./tool.js";
|
|
5
|
+
const contextText = (value) => {
|
|
6
|
+
if (typeof value === "string")
|
|
7
|
+
return value;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.stringify(value) ?? "";
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return String(value);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
let counter = 0;
|
|
16
|
+
/** An id unique in this page: crypto when there is one, a counter otherwise. */
|
|
17
|
+
export function agentId(prefix = "msg") {
|
|
18
|
+
const c = globalThis.crypto;
|
|
19
|
+
return c?.randomUUID ? c.randomUUID() : `${prefix}-${Date.now().toString(36)}-${++counter}`;
|
|
20
|
+
}
|
|
21
|
+
const errorText = (error) => error instanceof Error && error.message
|
|
22
|
+
? error.message
|
|
23
|
+
: typeof error === "string" && error
|
|
24
|
+
? error
|
|
25
|
+
: "The agent run failed.";
|
|
26
|
+
const isAbort = (error) => error instanceof Error && (error.name === "AbortError" || /abort/i.test(error.message));
|
|
27
|
+
export class AgentController {
|
|
28
|
+
options;
|
|
29
|
+
engine;
|
|
30
|
+
unsubscribe = () => { };
|
|
31
|
+
listeners = new Set();
|
|
32
|
+
view = createChatView();
|
|
33
|
+
running = false;
|
|
34
|
+
runSeq = 0;
|
|
35
|
+
tools = new Map();
|
|
36
|
+
toolOrder = 0;
|
|
37
|
+
context = new Map();
|
|
38
|
+
suggestionSets = new Map();
|
|
39
|
+
waiting = new Map();
|
|
40
|
+
executing = new Set();
|
|
41
|
+
failed = new Map();
|
|
42
|
+
decisions = new Map();
|
|
43
|
+
visible = true;
|
|
44
|
+
read = 0;
|
|
45
|
+
snapshot;
|
|
46
|
+
/** Where each custom event arrived, by its index in the view. */
|
|
47
|
+
anchors = [];
|
|
48
|
+
disposed = false;
|
|
49
|
+
connected = false;
|
|
50
|
+
threadSeq = 0;
|
|
51
|
+
/** Set by stop until the next run: a stopped run's late events (its abort error) are dropped. */
|
|
52
|
+
halted = false;
|
|
53
|
+
constructor(createEngine, options = {}) {
|
|
54
|
+
this.options = options;
|
|
55
|
+
this.engine = createEngine(this.invoke);
|
|
56
|
+
this.view = this.fromAgent(this.engine.agent.threadId);
|
|
57
|
+
this.snapshot = this.build();
|
|
58
|
+
this.read = this.finishedAnswers();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Starts reading the agent's events; returns the disconnect, which stops a
|
|
62
|
+
* run in flight. Called from an effect, so nothing subscribes on the server
|
|
63
|
+
* and a StrictMode remount reconnects cleanly.
|
|
64
|
+
*/
|
|
65
|
+
connect() {
|
|
66
|
+
this.disposed = false;
|
|
67
|
+
this.bind();
|
|
68
|
+
this.syncTools();
|
|
69
|
+
this.syncContext();
|
|
70
|
+
return () => {
|
|
71
|
+
this.stop();
|
|
72
|
+
this.unsubscribe();
|
|
73
|
+
this.unsubscribe = () => { };
|
|
74
|
+
this.connected = false;
|
|
75
|
+
// Takes back what the engine registered (CopilotKit tools and context in a shared core).
|
|
76
|
+
this.engine.dispose();
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
get agent() {
|
|
80
|
+
return this.engine.agent;
|
|
81
|
+
}
|
|
82
|
+
setOptions(options) {
|
|
83
|
+
this.options = options;
|
|
84
|
+
}
|
|
85
|
+
/** Swaps the engine (CopilotKit hands over a new agent once its runtime connects). */
|
|
86
|
+
attach(createEngine) {
|
|
87
|
+
this.stop();
|
|
88
|
+
this.unsubscribe();
|
|
89
|
+
this.engine.dispose();
|
|
90
|
+
this.engine = createEngine(this.invoke);
|
|
91
|
+
this.view = this.fromAgent(this.engine.agent.threadId);
|
|
92
|
+
if (this.connected)
|
|
93
|
+
this.bind();
|
|
94
|
+
this.syncTools();
|
|
95
|
+
this.syncContext();
|
|
96
|
+
this.emit();
|
|
97
|
+
}
|
|
98
|
+
// The store.
|
|
99
|
+
subscribe = (listener) => {
|
|
100
|
+
this.listeners.add(listener);
|
|
101
|
+
return () => this.listeners.delete(listener);
|
|
102
|
+
};
|
|
103
|
+
getSnapshot = () => this.snapshot;
|
|
104
|
+
emit() {
|
|
105
|
+
if (this.visible)
|
|
106
|
+
this.read = this.finishedAnswers();
|
|
107
|
+
this.snapshot = this.build();
|
|
108
|
+
for (const listener of this.listeners)
|
|
109
|
+
listener();
|
|
110
|
+
}
|
|
111
|
+
finishedAnswers() {
|
|
112
|
+
return this.view.messages.filter((m) => m.role === "assistant" && !m.streaming && m.content)
|
|
113
|
+
.length;
|
|
114
|
+
}
|
|
115
|
+
build() {
|
|
116
|
+
const touched = this.executing.size > 0 || this.failed.size > 0;
|
|
117
|
+
const view = touched
|
|
118
|
+
? {
|
|
119
|
+
...this.view,
|
|
120
|
+
toolCalls: this.view.toolCalls.map((c) => {
|
|
121
|
+
const error = this.failed.get(c.id);
|
|
122
|
+
if (error !== undefined)
|
|
123
|
+
return { ...c, status: "failed", error };
|
|
124
|
+
if (this.executing.has(c.id) && c.result === undefined)
|
|
125
|
+
return { ...c, status: "running" };
|
|
126
|
+
return c;
|
|
127
|
+
}),
|
|
128
|
+
}
|
|
129
|
+
: this.view;
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const suggestions = [...this.suggestionSets.values()].flat().filter((s) => {
|
|
132
|
+
if (seen.has(s.message))
|
|
133
|
+
return false;
|
|
134
|
+
seen.add(s.message);
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
view,
|
|
139
|
+
running: this.running,
|
|
140
|
+
waiting: [...this.waiting.keys()],
|
|
141
|
+
threadId: this.agent.threadId,
|
|
142
|
+
unread: Math.max(0, this.finishedAnswers() - this.read),
|
|
143
|
+
suggestions,
|
|
144
|
+
tools: [...this.tools.values()].sort((a, b) => a.order - b.order).map((t) => t.tool),
|
|
145
|
+
custom: this.customItems(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
customItems() {
|
|
149
|
+
const items = new Map();
|
|
150
|
+
this.view.custom.forEach((event, i) => {
|
|
151
|
+
const value = event.value;
|
|
152
|
+
const id = typeof value === "object" && value !== null && typeof value.id === "string";
|
|
153
|
+
const key = id ? `${event.name}:${value.id}` : `${event.name}#${i}`;
|
|
154
|
+
const item = items.get(key);
|
|
155
|
+
if (item) {
|
|
156
|
+
item.value = event.value;
|
|
157
|
+
item.history.push(event.value);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
items.set(key, {
|
|
161
|
+
key,
|
|
162
|
+
name: event.name,
|
|
163
|
+
value: event.value,
|
|
164
|
+
history: [event.value],
|
|
165
|
+
after: this.anchors[i] ?? null,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return [...items.values()];
|
|
170
|
+
}
|
|
171
|
+
bind() {
|
|
172
|
+
const agent = this.engine.agent;
|
|
173
|
+
this.unsubscribe();
|
|
174
|
+
this.connected = true;
|
|
175
|
+
const subscriber = {
|
|
176
|
+
onEvent: ({ event }) => {
|
|
177
|
+
const e = event;
|
|
178
|
+
this.fold(e);
|
|
179
|
+
this.options.onEvent?.(e);
|
|
180
|
+
},
|
|
181
|
+
onMessagesChanged: () => this.syncResults(),
|
|
182
|
+
};
|
|
183
|
+
const { unsubscribe } = agent.subscribe(subscriber);
|
|
184
|
+
this.unsubscribe = unsubscribe;
|
|
185
|
+
}
|
|
186
|
+
/** The view the agent's own messages and state make, for a fresh start or a thread switch. */
|
|
187
|
+
fromAgent(threadId) {
|
|
188
|
+
const agent = this.engine.agent;
|
|
189
|
+
this.anchors = [];
|
|
190
|
+
let view = { ...createChatView(), threadId };
|
|
191
|
+
if (agent.messages.length > 0) {
|
|
192
|
+
view = reduceChat(view, {
|
|
193
|
+
type: EventType.MESSAGES_SNAPSHOT,
|
|
194
|
+
messages: agent.messages,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const state = agent.state;
|
|
198
|
+
if (state !== undefined && !(isEmptyObject(state) && view.state === undefined)) {
|
|
199
|
+
view = reduceChat(view, { type: EventType.STATE_SNAPSHOT, snapshot: state });
|
|
200
|
+
}
|
|
201
|
+
return view;
|
|
202
|
+
}
|
|
203
|
+
fold(event) {
|
|
204
|
+
if (this.disposed)
|
|
205
|
+
return;
|
|
206
|
+
// A run started elsewhere (CopilotKit's own follow-up) ends the halt; anything else is the stopped run's.
|
|
207
|
+
if (this.halted && event.type !== EventType.RUN_STARTED)
|
|
208
|
+
return;
|
|
209
|
+
this.halted = false;
|
|
210
|
+
const before = this.view.custom.length;
|
|
211
|
+
this.view = reduceChat(this.view, event);
|
|
212
|
+
if (this.view.custom.length > before) {
|
|
213
|
+
const last = this.view.timeline.at(-1);
|
|
214
|
+
this.anchors.push(last ? `${last.kind}:${last.id}` : null);
|
|
215
|
+
}
|
|
216
|
+
this.emit();
|
|
217
|
+
}
|
|
218
|
+
/** Tool results the agent's messages gained outside the event stream: the browser's own tools. */
|
|
219
|
+
syncResults() {
|
|
220
|
+
let changed = false;
|
|
221
|
+
for (const m of this.agent.messages) {
|
|
222
|
+
if (m.role !== "tool")
|
|
223
|
+
continue;
|
|
224
|
+
const call = this.view.toolCalls.find((c) => c.id === m.toolCallId);
|
|
225
|
+
if (!call || call.result !== undefined)
|
|
226
|
+
continue;
|
|
227
|
+
this.view = reduceChat(this.view, {
|
|
228
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
229
|
+
messageId: m.id,
|
|
230
|
+
toolCallId: m.toolCallId,
|
|
231
|
+
content: typeof m.content === "string" ? m.content : "",
|
|
232
|
+
});
|
|
233
|
+
const error = m.error;
|
|
234
|
+
if (error)
|
|
235
|
+
this.failed.set(m.toolCallId, error);
|
|
236
|
+
this.executing.delete(m.toolCallId);
|
|
237
|
+
changed = true;
|
|
238
|
+
}
|
|
239
|
+
if (changed)
|
|
240
|
+
this.emit();
|
|
241
|
+
}
|
|
242
|
+
// Running.
|
|
243
|
+
invoke = async (tool, raw, { toolCallId, signal }) => {
|
|
244
|
+
this.executing.add(toolCallId);
|
|
245
|
+
this.emit();
|
|
246
|
+
try {
|
|
247
|
+
const args = await validateToolArgs(tool, raw);
|
|
248
|
+
if (tool.handler)
|
|
249
|
+
return await tool.handler(args, { signal, toolCallId });
|
|
250
|
+
if (!tool.render)
|
|
251
|
+
return undefined;
|
|
252
|
+
return await new Promise((resolve, reject) => {
|
|
253
|
+
if (signal.aborted)
|
|
254
|
+
return reject(new Error("aborted"));
|
|
255
|
+
this.waiting.set(toolCallId, { resolve });
|
|
256
|
+
this.emit();
|
|
257
|
+
signal.addEventListener("abort", () => {
|
|
258
|
+
this.waiting.delete(toolCallId);
|
|
259
|
+
reject(new Error("aborted"));
|
|
260
|
+
}, { once: true });
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
this.waiting.delete(toolCallId);
|
|
265
|
+
this.executing.delete(toolCallId);
|
|
266
|
+
this.emit();
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
async run(resume) {
|
|
270
|
+
const seq = ++this.runSeq;
|
|
271
|
+
this.halted = false;
|
|
272
|
+
this.decisions.clear();
|
|
273
|
+
this.running = true;
|
|
274
|
+
this.emit();
|
|
275
|
+
try {
|
|
276
|
+
const forwardedProps = this.options.forwardedProps?.();
|
|
277
|
+
await this.engine.run({
|
|
278
|
+
...(forwardedProps ? { forwardedProps } : {}),
|
|
279
|
+
...(resume ? { resume } : {}),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (seq === this.runSeq && !isAbort(error)) {
|
|
284
|
+
if (this.view.status !== "error") {
|
|
285
|
+
this.view = reduceChat(this.view, {
|
|
286
|
+
type: EventType.RUN_ERROR,
|
|
287
|
+
message: errorText(error),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
this.options.onError?.(error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
if (seq === this.runSeq) {
|
|
295
|
+
this.running = false;
|
|
296
|
+
this.emit();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Answers every interrupt still open as cancelled, so a new message can start a run. */
|
|
301
|
+
cancelOpen() {
|
|
302
|
+
const open = this.view.status === "interrupted" ? this.view.interrupts : [];
|
|
303
|
+
if (open.length === 0)
|
|
304
|
+
return undefined;
|
|
305
|
+
return open.map((i) => this.decisions.get(i.id) ?? { interruptId: i.id, status: "cancelled" });
|
|
306
|
+
}
|
|
307
|
+
/** Sends the person's message and runs the agent. Ignored while a run is in flight. */
|
|
308
|
+
send = (text, options = {}) => {
|
|
309
|
+
if (this.running || (!text.trim() && !options.parts?.length))
|
|
310
|
+
return Promise.resolve();
|
|
311
|
+
const id = agentId();
|
|
312
|
+
const content = options.parts?.length
|
|
313
|
+
? [{ type: "text", text }, ...options.parts]
|
|
314
|
+
: text;
|
|
315
|
+
this.agent.addMessage({ id, role: "user", content });
|
|
316
|
+
for (const event of userMessageEvents(id, text))
|
|
317
|
+
this.view = reduceChat(this.view, event);
|
|
318
|
+
return this.run(this.cancelOpen());
|
|
319
|
+
};
|
|
320
|
+
/** Stops the run in flight and any tool waiting on the person. */
|
|
321
|
+
stop = () => {
|
|
322
|
+
if (!this.running && this.view.status !== "running")
|
|
323
|
+
return;
|
|
324
|
+
this.runSeq++;
|
|
325
|
+
this.halted = true;
|
|
326
|
+
this.engine.stop();
|
|
327
|
+
this.running = false;
|
|
328
|
+
if (this.view.status === "running" || this.executing.size > 0) {
|
|
329
|
+
this.view = reduceChat({ ...this.view, status: "running" }, {
|
|
330
|
+
type: EventType.RUN_FINISHED,
|
|
331
|
+
threadId: this.view.threadId ?? "",
|
|
332
|
+
runId: this.view.runId ?? "",
|
|
333
|
+
outcome: { type: "cancelled" },
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
this.executing.clear();
|
|
337
|
+
this.waiting.clear();
|
|
338
|
+
this.emit();
|
|
339
|
+
};
|
|
340
|
+
/** Drops the answer to the last message and asks again. */
|
|
341
|
+
regenerate = () => {
|
|
342
|
+
if (this.running)
|
|
343
|
+
return Promise.resolve();
|
|
344
|
+
const messages = this.agent.messages;
|
|
345
|
+
let last = -1;
|
|
346
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
347
|
+
if (messages[i]?.role === "user") {
|
|
348
|
+
last = i;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (last < 0)
|
|
353
|
+
return Promise.resolve();
|
|
354
|
+
this.agent.setMessages(messages.slice(0, last + 1));
|
|
355
|
+
this.failed.clear();
|
|
356
|
+
this.view = this.fromAgent(this.agent.threadId);
|
|
357
|
+
this.emit();
|
|
358
|
+
return this.run();
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* Answers one interrupt. When every open interrupt has an answer, the run
|
|
362
|
+
* resumes with them (AG-UI `resume`).
|
|
363
|
+
*/
|
|
364
|
+
resolveInterrupt = (interruptId, response) => {
|
|
365
|
+
if (this.view.status !== "interrupted")
|
|
366
|
+
return;
|
|
367
|
+
this.decisions.set(interruptId, { interruptId, ...response });
|
|
368
|
+
const open = this.view.interrupts;
|
|
369
|
+
if (open.every((i) => this.decisions.has(i.id))) {
|
|
370
|
+
void this.run(open.map((i) => this.decisions.get(i.id)));
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
/** Approves a tool call the run paused on. */
|
|
374
|
+
approve = (call, payload) => {
|
|
375
|
+
if (call.approval)
|
|
376
|
+
this.resolveInterrupt(call.approval.interruptId, {
|
|
377
|
+
status: "resolved",
|
|
378
|
+
payload: { approved: true, ...payload },
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
/** Denies it, with the person's reason. */
|
|
382
|
+
deny = (call, reason) => {
|
|
383
|
+
if (call.approval)
|
|
384
|
+
this.resolveInterrupt(call.approval.interruptId, {
|
|
385
|
+
status: "resolved",
|
|
386
|
+
payload: { approved: false, reason },
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
/** Answers a human-in-the-loop tool call: the result goes back to the agent. */
|
|
390
|
+
respond = (toolCallId, result) => {
|
|
391
|
+
const pending = this.waiting.get(toolCallId);
|
|
392
|
+
if (!pending)
|
|
393
|
+
return;
|
|
394
|
+
this.waiting.delete(toolCallId);
|
|
395
|
+
pending.resolve(result);
|
|
396
|
+
this.emit();
|
|
397
|
+
};
|
|
398
|
+
// Shared state.
|
|
399
|
+
getState = () => this.view.state;
|
|
400
|
+
/** Replaces the shared state: the next run sends it, and the view shows it now. */
|
|
401
|
+
setState = (state) => {
|
|
402
|
+
this.agent.setState(state);
|
|
403
|
+
this.view = reduceChat(this.view, { type: EventType.STATE_SNAPSHOT, snapshot: state });
|
|
404
|
+
this.emit();
|
|
405
|
+
};
|
|
406
|
+
// Registries.
|
|
407
|
+
registerTool = (given) => {
|
|
408
|
+
const tool = given;
|
|
409
|
+
const entry = { tool, order: this.toolOrder++ };
|
|
410
|
+
this.tools.set(tool.name, entry);
|
|
411
|
+
this.syncTools();
|
|
412
|
+
this.emit();
|
|
413
|
+
return () => {
|
|
414
|
+
if (this.tools.get(tool.name) !== entry)
|
|
415
|
+
return;
|
|
416
|
+
this.tools.delete(tool.name);
|
|
417
|
+
this.syncTools();
|
|
418
|
+
this.emit();
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
syncTools() {
|
|
422
|
+
this.engine.setTools([...this.tools.values()].map((t) => t.tool));
|
|
423
|
+
}
|
|
424
|
+
addContext = (entry) => {
|
|
425
|
+
const id = agentId("ctx");
|
|
426
|
+
this.context.set(id, { description: entry.description, value: contextText(entry.value) });
|
|
427
|
+
this.syncContext();
|
|
428
|
+
return () => {
|
|
429
|
+
this.context.delete(id);
|
|
430
|
+
this.syncContext();
|
|
431
|
+
};
|
|
432
|
+
};
|
|
433
|
+
syncContext() {
|
|
434
|
+
this.engine.setContext([...this.context.values()]);
|
|
435
|
+
}
|
|
436
|
+
addSuggestions = (suggestions) => {
|
|
437
|
+
const id = agentId("sug");
|
|
438
|
+
this.suggestionSets.set(id, suggestions);
|
|
439
|
+
this.emit();
|
|
440
|
+
return () => {
|
|
441
|
+
this.suggestionSets.delete(id);
|
|
442
|
+
this.emit();
|
|
443
|
+
};
|
|
444
|
+
};
|
|
445
|
+
// Threads.
|
|
446
|
+
/** Opens another conversation: stops this one, loads the other's history. */
|
|
447
|
+
switchThread = async (threadId) => {
|
|
448
|
+
this.stop();
|
|
449
|
+
const seq = ++this.threadSeq;
|
|
450
|
+
const thread = (await this.options.loadThread?.(threadId)) ?? { messages: [] };
|
|
451
|
+
// A later switch (or a dispose) while this one loaded wins.
|
|
452
|
+
if (this.disposed || seq !== this.threadSeq)
|
|
453
|
+
return;
|
|
454
|
+
this.agent.threadId = threadId;
|
|
455
|
+
this.agent.setMessages(thread.messages);
|
|
456
|
+
this.agent.setState((thread.state ?? {}));
|
|
457
|
+
this.failed.clear();
|
|
458
|
+
this.view = this.fromAgent(threadId);
|
|
459
|
+
this.emit();
|
|
460
|
+
};
|
|
461
|
+
/** Starts a new, empty conversation and returns its id. */
|
|
462
|
+
newThread = async () => {
|
|
463
|
+
const id = agentId("thread");
|
|
464
|
+
this.threadSeq++;
|
|
465
|
+
this.stop();
|
|
466
|
+
this.agent.threadId = id;
|
|
467
|
+
this.agent.setMessages([]);
|
|
468
|
+
this.agent.setState({});
|
|
469
|
+
this.failed.clear();
|
|
470
|
+
this.anchors = [];
|
|
471
|
+
this.view = { ...createChatView(), threadId: id };
|
|
472
|
+
this.emit();
|
|
473
|
+
return id;
|
|
474
|
+
};
|
|
475
|
+
// The surface.
|
|
476
|
+
/** Whether the person can see the conversation: answers that land while hidden count as unread. */
|
|
477
|
+
setVisible = (visible) => {
|
|
478
|
+
if (this.visible === visible)
|
|
479
|
+
return;
|
|
480
|
+
this.visible = visible;
|
|
481
|
+
this.emit();
|
|
482
|
+
};
|
|
483
|
+
dispose() {
|
|
484
|
+
this.stop();
|
|
485
|
+
this.disposed = true;
|
|
486
|
+
this.unsubscribe();
|
|
487
|
+
this.engine.dispose();
|
|
488
|
+
this.listeners.clear();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function isEmptyObject(value) {
|
|
492
|
+
return (typeof value === "object" &&
|
|
493
|
+
value !== null &&
|
|
494
|
+
!Array.isArray(value) &&
|
|
495
|
+
Object.keys(value).length === 0);
|
|
496
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
2
|
+
import { CopilotKitCoreReact } from "@copilotkit/react-core/v2/context";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { type CalliopeAgentOptions } from "../AgentProvider.js";
|
|
5
|
+
import type { AgentEngine, ToolInvoker } from "../engine.js";
|
|
6
|
+
type CopilotKitCore = CopilotKitCoreReact;
|
|
7
|
+
/**
|
|
8
|
+
* The engine over CopilotKit's core: CopilotKit runs the agent, executes the
|
|
9
|
+
* browser's tools, makes follow-ups and carries context, so a host's own
|
|
10
|
+
* `useFrontendTool`, `useAgentContext` or `useHumanInTheLoop` calls keep
|
|
11
|
+
* working beside the ones registered here.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createCopilotKitEngine(copilotkit: CopilotKitCore, agent: AbstractAgent, invoke: ToolInvoker, agentId?: string): AgentEngine;
|
|
14
|
+
export interface CopilotKitAgentProviderProps extends Omit<CalliopeAgentOptions, "forwardedProps"> {
|
|
15
|
+
/** The CopilotKit runtime's route (the Next.js handler around `CopilotRuntime`). */
|
|
16
|
+
runtimeUrl?: string;
|
|
17
|
+
/** True for a single-route runtime (`createCopilotRuntimeHandler({ mode: "single-route" })`). */
|
|
18
|
+
useSingleEndpoint?: boolean;
|
|
19
|
+
/** Request headers for the runtime (a project id). Never a secret: this runs in the browser. */
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
credentials?: RequestCredentials;
|
|
22
|
+
/** Sent to the runtime with every request (CopilotKit `properties`). */
|
|
23
|
+
properties?: Record<string, unknown>;
|
|
24
|
+
/** Agents that run in the page instead of behind the runtime: a story's mock, a test's. */
|
|
25
|
+
agents?: Record<string, AbstractAgent>;
|
|
26
|
+
/** The runtime's agent. Defaults to CopilotKit's default agent. */
|
|
27
|
+
agentId?: string;
|
|
28
|
+
/** Sent with every run (the model picked, the page). */
|
|
29
|
+
forwardedProps?: Record<string, unknown>;
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The agent for a page tree, over a CopilotKit runtime (`runtimeUrl`, the
|
|
34
|
+
* route around `CopilotRuntime`). CopilotKit wires the runtime and runs the
|
|
35
|
+
* agent through its headless core; every pixel is the library's
|
|
36
|
+
* (`CalliopeAgent`, the chat kit), never CopilotKit's UI or stylesheet.
|
|
37
|
+
* Inside an existing `CopilotKitProvider` it uses that one instead, so a
|
|
38
|
+
* host's own CopilotKit hooks and these share one core.
|
|
39
|
+
*/
|
|
40
|
+
export declare function CopilotKitAgentProvider({ runtimeUrl, useSingleEndpoint, headers, credentials, properties, agents, agentId, forwardedProps, children, ...options }: CopilotKitAgentProviderProps): React.JSX.Element;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { CopilotKitContext, CopilotKitCoreReact, useCopilotKit, } from "@copilotkit/react-core/v2/context";
|
|
4
|
+
import { useAgent } from "@copilotkit/react-core/v2/headless";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { AgentHost } from "../AgentProvider.js";
|
|
7
|
+
import { toolStandardSchema } from "../tool.js";
|
|
8
|
+
/**
|
|
9
|
+
* The engine over CopilotKit's core: CopilotKit runs the agent, executes the
|
|
10
|
+
* browser's tools, makes follow-ups and carries context, so a host's own
|
|
11
|
+
* `useFrontendTool`, `useAgentContext` or `useHumanInTheLoop` calls keep
|
|
12
|
+
* working beside the ones registered here.
|
|
13
|
+
*/
|
|
14
|
+
export function createCopilotKitEngine(copilotkit, agent, invoke, agentId) {
|
|
15
|
+
let tools = [];
|
|
16
|
+
let context = [];
|
|
17
|
+
const clearTools = () => {
|
|
18
|
+
for (const name of tools)
|
|
19
|
+
copilotkit.removeTool(name, agentId);
|
|
20
|
+
tools = [];
|
|
21
|
+
};
|
|
22
|
+
const clearContext = () => {
|
|
23
|
+
for (const id of context)
|
|
24
|
+
copilotkit.removeContext(id);
|
|
25
|
+
context = [];
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
agent,
|
|
29
|
+
async run({ forwardedProps, resume }) {
|
|
30
|
+
await copilotkit.runAgent({
|
|
31
|
+
agent,
|
|
32
|
+
...(forwardedProps ? { forwardedProps } : {}),
|
|
33
|
+
...(resume ? { resume } : {}),
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
stop() {
|
|
37
|
+
copilotkit.stopAgent({ agent });
|
|
38
|
+
},
|
|
39
|
+
setTools(next) {
|
|
40
|
+
clearTools();
|
|
41
|
+
// A render-only tool (available: false) draws the agent's own calls; CopilotKit must not run it.
|
|
42
|
+
for (const tool of next.filter((t) => t.available !== false)) {
|
|
43
|
+
copilotkit.addTool({
|
|
44
|
+
name: tool.name,
|
|
45
|
+
...(tool.description !== undefined ? { description: tool.description } : {}),
|
|
46
|
+
parameters: toolStandardSchema(tool),
|
|
47
|
+
...(tool.followUp !== undefined ? { followUp: tool.followUp } : {}),
|
|
48
|
+
...(agentId ? { agentId } : {}),
|
|
49
|
+
handler: (args, ctx) => invoke(tool, args, {
|
|
50
|
+
toolCallId: ctx.toolCall.id,
|
|
51
|
+
signal: ctx.signal ?? new AbortController().signal,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
tools.push(tool.name);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
setContext(next) {
|
|
58
|
+
clearContext();
|
|
59
|
+
context = next.map((entry) => copilotkit.addContext({ ...entry, ...(agentId ? { agentIds: [agentId] } : {}) }));
|
|
60
|
+
},
|
|
61
|
+
dispose() {
|
|
62
|
+
clearTools();
|
|
63
|
+
clearContext();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function Bridge({ agentId, forwardedProps, options, children, }) {
|
|
68
|
+
const { copilotkit } = useCopilotKit();
|
|
69
|
+
// The view comes from the agent's events, so the hook need not re-render on each one.
|
|
70
|
+
const { agent } = useAgent({ ...(agentId ? { agentId } : {}), updates: [] });
|
|
71
|
+
const props = React.useRef(forwardedProps);
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
props.current = forwardedProps;
|
|
74
|
+
});
|
|
75
|
+
return (_jsx(AgentHost, { engineKey: agent, createEngine: (invoke) => createCopilotKitEngine(copilotkit, agent, invoke, agentId), options: { ...options, forwardedProps: () => props.current }, children: children }));
|
|
76
|
+
}
|
|
77
|
+
const EMPTY = {};
|
|
78
|
+
function transport(single) {
|
|
79
|
+
return single === true ? "single" : single === false ? "rest" : "auto";
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* CopilotKit's core for the tree, without CopilotKit's UI: the headless
|
|
83
|
+
* equivalent of its provider (no styled chat, no stylesheet, no inspector).
|
|
84
|
+
*/
|
|
85
|
+
function CopilotKitCoreHost({ runtimeUrl, useSingleEndpoint, headers: givenHeaders = EMPTY, credentials, properties: givenProperties = EMPTY, agents = EMPTY, onError, children, }) {
|
|
86
|
+
// Inline objects from the host keep their identity while their content does not change.
|
|
87
|
+
const headersKey = JSON.stringify(givenHeaders);
|
|
88
|
+
const propertiesKey = JSON.stringify(givenProperties);
|
|
89
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
90
|
+
const headers = React.useMemo(() => givenHeaders, [headersKey]);
|
|
91
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
|
+
const properties = React.useMemo(() => givenProperties, [propertiesKey]);
|
|
93
|
+
const [copilotkit] = React.useState(() => new CopilotKitCoreReact({
|
|
94
|
+
runtimeUrl,
|
|
95
|
+
runtimeTransport: transport(useSingleEndpoint),
|
|
96
|
+
deferInitialConnection: true,
|
|
97
|
+
headers,
|
|
98
|
+
credentials,
|
|
99
|
+
properties,
|
|
100
|
+
agents__unsafe_dev_only: agents,
|
|
101
|
+
}));
|
|
102
|
+
const [executing, setExecuting] = React.useState(() => new Set());
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
copilotkit.setRuntimeUrl(runtimeUrl);
|
|
105
|
+
copilotkit.setRuntimeTransport(transport(useSingleEndpoint));
|
|
106
|
+
copilotkit.setHeaders(headers);
|
|
107
|
+
copilotkit.setCredentials(credentials);
|
|
108
|
+
copilotkit.setProperties(properties);
|
|
109
|
+
copilotkit.setAgents__unsafe_dev_only(agents);
|
|
110
|
+
copilotkit.connect();
|
|
111
|
+
}, [copilotkit, runtimeUrl, useSingleEndpoint, headers, credentials, properties, agents]);
|
|
112
|
+
const onErrorRef = React.useRef(onError);
|
|
113
|
+
React.useEffect(() => {
|
|
114
|
+
onErrorRef.current = onError;
|
|
115
|
+
});
|
|
116
|
+
React.useEffect(() => {
|
|
117
|
+
const { unsubscribe } = copilotkit.subscribe({
|
|
118
|
+
onError: (event) => onErrorRef.current?.(event.error),
|
|
119
|
+
onToolExecutionStart: ({ toolCallId }) => setExecuting((prev) => new Set(prev).add(toolCallId)),
|
|
120
|
+
onToolExecutionEnd: ({ toolCallId }) => setExecuting((prev) => {
|
|
121
|
+
const next = new Set(prev);
|
|
122
|
+
next.delete(toolCallId);
|
|
123
|
+
return next;
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
return unsubscribe;
|
|
127
|
+
}, [copilotkit]);
|
|
128
|
+
const value = React.useMemo(() => ({ copilotkit, executingToolCallIds: executing, showIntelligenceIndicator: false }), [copilotkit, executing]);
|
|
129
|
+
return _jsx(CopilotKitContext.Provider, { value: value, children: children });
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The agent for a page tree, over a CopilotKit runtime (`runtimeUrl`, the
|
|
133
|
+
* route around `CopilotRuntime`). CopilotKit wires the runtime and runs the
|
|
134
|
+
* agent through its headless core; every pixel is the library's
|
|
135
|
+
* (`CalliopeAgent`, the chat kit), never CopilotKit's UI or stylesheet.
|
|
136
|
+
* Inside an existing `CopilotKitProvider` it uses that one instead, so a
|
|
137
|
+
* host's own CopilotKit hooks and these share one core.
|
|
138
|
+
*/
|
|
139
|
+
export function CopilotKitAgentProvider({ runtimeUrl, useSingleEndpoint, headers, credentials, properties, agents, agentId, forwardedProps, children, ...options }) {
|
|
140
|
+
const existing = React.useContext(CopilotKitContext);
|
|
141
|
+
const bridge = (_jsx(Bridge, { agentId: agentId, forwardedProps: forwardedProps, options: options, children: children }));
|
|
142
|
+
if (existing)
|
|
143
|
+
return bridge;
|
|
144
|
+
return (_jsx(CopilotKitCoreHost, { runtimeUrl, useSingleEndpoint, headers, credentials, properties, agents, ...(options.onError ? { onError: options.onError } : {}), children: bridge }));
|
|
145
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@calliopelabs/ui/agent/copilotkit`: the agent over a CopilotKit runtime.
|
|
3
|
+
* Needs `@copilotkit/react-core` (1.79.1 or later, 1.x) and `@ag-ui/client`;
|
|
4
|
+
* uses only CopilotKit's headless core, never its UI or stylesheet.
|
|
5
|
+
*/
|
|
6
|
+
export { CopilotKitAgentProvider, createCopilotKitEngine, type CopilotKitAgentProviderProps, } from "./CopilotKitAgentProvider.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@calliopelabs/ui/agent/copilotkit`: the agent over a CopilotKit runtime.
|
|
3
|
+
* Needs `@copilotkit/react-core` (1.79.1 or later, 1.x) and `@ag-ui/client`;
|
|
4
|
+
* uses only CopilotKit's headless core, never its UI or stylesheet.
|
|
5
|
+
*/
|
|
6
|
+
export { CopilotKitAgentProvider, createCopilotKitEngine, } from "./CopilotKitAgentProvider.js";
|