@calliopelabs/ui 0.13.0 → 0.14.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 +50 -3
- package/dist/agent/AgentChat.d.ts +56 -0
- package/dist/agent/AgentChat.js +76 -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/controller.d.ts +148 -0
- package/dist/agent/controller.js +463 -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 +15 -0
- package/dist/agent/index.js +15 -0
- package/dist/agent/mock.d.ts +34 -0
- package/dist/agent/mock.js +112 -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 +14 -12
- package/dist/appearance/brands.js +18 -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 +36 -0
- package/dist/chat/MessageList.js +68 -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/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/primitives/Button.d.ts +1 -1
- package/dist/provider/messages.d.ts +66 -0
- package/dist/provider/messages.js +71 -0
- 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 +353 -0
- package/dist/theme/tokens.generated.js +357 -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/package.json +35 -2
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation view an AG-UI event stream builds, and the reducer that
|
|
3
|
+
* folds one event into it. Engine-agnostic: any agent that speaks AG-UI
|
|
4
|
+
* (or an adapter that turns an engine's stream into AG-UI events) drives the
|
|
5
|
+
* chat components through this one shape. Pure and immutable: every event
|
|
6
|
+
* returns a new view that shares what did not change, so a message that did
|
|
7
|
+
* not change keeps its identity and a memoized bubble does not re-render.
|
|
8
|
+
* Tolerant: an event for a message or call it has not seen starts one, and a
|
|
9
|
+
* state delta that fails to apply is counted and dropped, never thrown.
|
|
10
|
+
*/
|
|
11
|
+
import { type ContentPart, type Event, type Interrupt } from "@ag-ui/core";
|
|
12
|
+
/** Any AG-UI event, as `@ag-ui/core` types it. */
|
|
13
|
+
export type AgUiEvent = Event;
|
|
14
|
+
/** A run's pause for a person: AG-UI's `RUN_FINISHED` interrupt outcome. */
|
|
15
|
+
export type AgUiInterrupt = Interrupt;
|
|
16
|
+
export type ChatRole = "user" | "assistant" | "system" | "developer" | "tool";
|
|
17
|
+
export interface ChatMessage {
|
|
18
|
+
id: string;
|
|
19
|
+
role: ChatRole;
|
|
20
|
+
/** The text so far; grows while `streaming`. */
|
|
21
|
+
content: string;
|
|
22
|
+
/** True while its deltas are still arriving. */
|
|
23
|
+
streaming: boolean;
|
|
24
|
+
/** The speaker's display name, when the event carried one. */
|
|
25
|
+
name?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `streaming`: its arguments are arriving. `running`: the arguments are in and
|
|
29
|
+
* the result is not. `complete`: a result came, or the run finished without
|
|
30
|
+
* one. `failed`: the tool reported an error or the run errored first.
|
|
31
|
+
* `cancelled`: a person stopped the run first.
|
|
32
|
+
*/
|
|
33
|
+
export type ChatToolCallStatus = "streaming" | "running" | "complete" | "failed" | "cancelled";
|
|
34
|
+
/** A tool call the run paused on, waiting for a person to approve or deny it. */
|
|
35
|
+
export interface ChatApproval {
|
|
36
|
+
interruptId: string;
|
|
37
|
+
reason: string;
|
|
38
|
+
/** What to ask the person, in the agent's words. */
|
|
39
|
+
message?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ChatToolCall {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
/** The arguments as JSON text, as streamed; see `parseToolCallArgs`. */
|
|
45
|
+
args: string;
|
|
46
|
+
status: ChatToolCallStatus;
|
|
47
|
+
/** The result as text. */
|
|
48
|
+
result?: string;
|
|
49
|
+
error?: string;
|
|
50
|
+
/** The assistant message that made the call, when the agent said. */
|
|
51
|
+
parentMessageId?: string;
|
|
52
|
+
/** Set while the run waits on a person for this call; cleared when the next run starts. */
|
|
53
|
+
approval?: ChatApproval;
|
|
54
|
+
}
|
|
55
|
+
/** The conversation in arrival order: messages and tool calls interleaved. */
|
|
56
|
+
export type ChatTimelineItem = {
|
|
57
|
+
kind: "message";
|
|
58
|
+
id: string;
|
|
59
|
+
} | {
|
|
60
|
+
kind: "tool-call";
|
|
61
|
+
id: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* `finished`: the run ended successfully. `interrupted`: it ended waiting on
|
|
65
|
+
* a person (see `interrupts`). `cancelled`: a person stopped it.
|
|
66
|
+
*/
|
|
67
|
+
export type ChatRunStatus = "idle" | "running" | "finished" | "interrupted" | "cancelled" | "error";
|
|
68
|
+
export type ChatStepStatus = "running" | "finished" | "failed" | "cancelled";
|
|
69
|
+
export interface ChatStep {
|
|
70
|
+
name: string;
|
|
71
|
+
status: ChatStepStatus;
|
|
72
|
+
}
|
|
73
|
+
export interface ChatView {
|
|
74
|
+
threadId: string | null;
|
|
75
|
+
/** The latest run's id. */
|
|
76
|
+
runId: string | null;
|
|
77
|
+
status: ChatRunStatus;
|
|
78
|
+
error: {
|
|
79
|
+
message: string;
|
|
80
|
+
code?: string;
|
|
81
|
+
} | null;
|
|
82
|
+
/** What the latest run's `RUN_FINISHED` carried as `result`. */
|
|
83
|
+
result?: unknown;
|
|
84
|
+
messages: readonly ChatMessage[];
|
|
85
|
+
toolCalls: readonly ChatToolCall[];
|
|
86
|
+
timeline: readonly ChatTimelineItem[];
|
|
87
|
+
/** The latest run's steps, in the order they started. */
|
|
88
|
+
steps: readonly ChatStep[];
|
|
89
|
+
/** What the latest run is waiting on, when it ended `interrupted`. */
|
|
90
|
+
interrupts: readonly AgUiInterrupt[];
|
|
91
|
+
/** The shared state: the last `STATE_SNAPSHOT` with every `STATE_DELTA` since. */
|
|
92
|
+
state: unknown;
|
|
93
|
+
/** State deltas that did not apply; the next snapshot repairs them. */
|
|
94
|
+
rejectedDeltas: number;
|
|
95
|
+
/** Every `CUSTOM` event, in order, for the host to read. */
|
|
96
|
+
custom: readonly {
|
|
97
|
+
name: string;
|
|
98
|
+
value: unknown;
|
|
99
|
+
}[];
|
|
100
|
+
/** Every `RAW` event, in order, for the host to read. */
|
|
101
|
+
raw: readonly {
|
|
102
|
+
event: unknown;
|
|
103
|
+
source?: string;
|
|
104
|
+
}[];
|
|
105
|
+
}
|
|
106
|
+
export declare function createChatView(): ChatView;
|
|
107
|
+
/** A tool call's arguments as JSON, or undefined while they do not parse (still streaming). */
|
|
108
|
+
export declare function parseToolCallArgs(args: string): unknown;
|
|
109
|
+
/** The text of an AG-UI content value: a string, or the text parts of a part list. */
|
|
110
|
+
export declare function contentText(content: string | readonly ContentPart[] | undefined): string;
|
|
111
|
+
/**
|
|
112
|
+
* Folds one AG-UI event into the view. Handles the run lifecycle
|
|
113
|
+
* (`RUN_STARTED`, `RUN_FINISHED` with its outcome, `RUN_ERROR`), steps, text
|
|
114
|
+
* messages and their chunks, tool calls and their chunks and results, shared
|
|
115
|
+
* state (`STATE_SNAPSHOT`, `STATE_DELTA` as JSON Patch), `MESSAGES_SNAPSHOT`,
|
|
116
|
+
* `CUSTOM` and `RAW`. Activity, reasoning and sub-agent events leave the view
|
|
117
|
+
* as it is.
|
|
118
|
+
*/
|
|
119
|
+
export declare function reduceChat(view: ChatView, event: AgUiEvent): ChatView;
|
|
120
|
+
/** Folds a sequence of events, from a fresh view or from `view`. */
|
|
121
|
+
export declare function foldChat(events: Iterable<AgUiEvent>, view?: ChatView): ChatView;
|
|
122
|
+
/** True while the latest run is in flight: the composer offers stop, not send. */
|
|
123
|
+
export declare function isChatRunning(view: ChatView): boolean;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation view an AG-UI event stream builds, and the reducer that
|
|
3
|
+
* folds one event into it. Engine-agnostic: any agent that speaks AG-UI
|
|
4
|
+
* (or an adapter that turns an engine's stream into AG-UI events) drives the
|
|
5
|
+
* chat components through this one shape. Pure and immutable: every event
|
|
6
|
+
* returns a new view that shares what did not change, so a message that did
|
|
7
|
+
* not change keeps its identity and a memoized bubble does not re-render.
|
|
8
|
+
* Tolerant: an event for a message or call it has not seen starts one, and a
|
|
9
|
+
* state delta that fails to apply is counted and dropped, never thrown.
|
|
10
|
+
*/
|
|
11
|
+
import { EventType, } from "@ag-ui/core";
|
|
12
|
+
import { applyJsonPatch } from "./patch.js";
|
|
13
|
+
export function createChatView() {
|
|
14
|
+
return {
|
|
15
|
+
threadId: null,
|
|
16
|
+
runId: null,
|
|
17
|
+
status: "idle",
|
|
18
|
+
error: null,
|
|
19
|
+
messages: [],
|
|
20
|
+
toolCalls: [],
|
|
21
|
+
timeline: [],
|
|
22
|
+
steps: [],
|
|
23
|
+
interrupts: [],
|
|
24
|
+
state: undefined,
|
|
25
|
+
rejectedDeltas: 0,
|
|
26
|
+
custom: [],
|
|
27
|
+
raw: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** A tool call's arguments as JSON, or undefined while they do not parse (still streaming). */
|
|
31
|
+
export function parseToolCallArgs(args) {
|
|
32
|
+
if (args.trim() === "")
|
|
33
|
+
return undefined;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(args);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** The text of an AG-UI content value: a string, or the text parts of a part list. */
|
|
42
|
+
export function contentText(content) {
|
|
43
|
+
if (content === undefined)
|
|
44
|
+
return "";
|
|
45
|
+
if (typeof content === "string")
|
|
46
|
+
return content;
|
|
47
|
+
return content.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
48
|
+
}
|
|
49
|
+
function lastIndex(items, id) {
|
|
50
|
+
for (let i = items.length - 1; i >= 0; i--)
|
|
51
|
+
if (items[i]?.id === id)
|
|
52
|
+
return i;
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
function upsertMessage(view, id, update, start = { role: "assistant", streaming: true }) {
|
|
56
|
+
const i = lastIndex(view.messages, id);
|
|
57
|
+
const messages = view.messages.slice();
|
|
58
|
+
if (i < 0) {
|
|
59
|
+
messages.push(update({ id, content: "", ...start }));
|
|
60
|
+
return { ...view, messages, timeline: [...view.timeline, { kind: "message", id }] };
|
|
61
|
+
}
|
|
62
|
+
messages[i] = update(messages[i]);
|
|
63
|
+
return { ...view, messages };
|
|
64
|
+
}
|
|
65
|
+
function upsertCall(view, id, update, name = "") {
|
|
66
|
+
const i = lastIndex(view.toolCalls, id);
|
|
67
|
+
const toolCalls = view.toolCalls.slice();
|
|
68
|
+
if (i < 0) {
|
|
69
|
+
toolCalls.push(update({ id, name, args: "", status: "streaming" }));
|
|
70
|
+
return { ...view, toolCalls, timeline: [...view.timeline, { kind: "tool-call", id }] };
|
|
71
|
+
}
|
|
72
|
+
toolCalls[i] = update(toolCalls[i]);
|
|
73
|
+
return { ...view, toolCalls };
|
|
74
|
+
}
|
|
75
|
+
/** Maps only the items `pick` selects, keeping every other item's identity. */
|
|
76
|
+
function mapWhere(items, pick, update) {
|
|
77
|
+
return items.some(pick) ? items.map((item) => (pick(item) ? update(item) : item)) : items;
|
|
78
|
+
}
|
|
79
|
+
const open = (call) => call.status === "streaming" || call.status === "running";
|
|
80
|
+
/** Closes what a run's end leaves open: streaming messages, running steps, open calls. */
|
|
81
|
+
function closeRun(view, step, call) {
|
|
82
|
+
return {
|
|
83
|
+
messages: mapWhere(view.messages, (m) => m.streaming, (m) => ({ ...m, streaming: false })),
|
|
84
|
+
steps: mapWhere(view.steps, (s) => s.status === "running", (s) => ({ ...s, status: step })),
|
|
85
|
+
toolCalls: mapWhere(view.toolCalls, open, call),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function finish(view, event) {
|
|
89
|
+
const outcome = event.outcome ?? { type: "success" };
|
|
90
|
+
const base = { ...view, error: null, result: event.result };
|
|
91
|
+
if (outcome.type === "cancelled") {
|
|
92
|
+
return {
|
|
93
|
+
...base,
|
|
94
|
+
status: "cancelled",
|
|
95
|
+
interrupts: [],
|
|
96
|
+
...closeRun(view, "cancelled", (c) => ({ ...c, status: "cancelled" })),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (outcome.type === "interrupt") {
|
|
100
|
+
const byCall = new Map(outcome.interrupts.flatMap((i) => (i.toolCallId ? [[i.toolCallId, i]] : [])));
|
|
101
|
+
const closed = closeRun(view, "finished", (c) => c);
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
status: "interrupted",
|
|
105
|
+
interrupts: outcome.interrupts,
|
|
106
|
+
...closed,
|
|
107
|
+
toolCalls: mapWhere(closed.toolCalls, (c) => byCall.has(c.id), (c) => {
|
|
108
|
+
const i = byCall.get(c.id);
|
|
109
|
+
return {
|
|
110
|
+
...c,
|
|
111
|
+
approval: {
|
|
112
|
+
interruptId: i.id,
|
|
113
|
+
reason: i.reason,
|
|
114
|
+
...(i.message === undefined ? {} : { message: i.message }),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const pending = new Set(outcome.pendingToolCallIds ?? []);
|
|
121
|
+
return {
|
|
122
|
+
...base,
|
|
123
|
+
status: "finished",
|
|
124
|
+
interrupts: [],
|
|
125
|
+
...closeRun(view, "finished", (c) => pending.has(c.id) ? { ...c, status: "running" } : { ...c, status: "complete" }),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const ROLES = new Set(["user", "assistant", "system", "developer"]);
|
|
129
|
+
/**
|
|
130
|
+
* A `MESSAGES_SNAPSHOT` replaces the conversation: messages, tool calls and
|
|
131
|
+
* their results, in the snapshot's order. A call keeps a pending approval
|
|
132
|
+
* across it. Activity and reasoning messages are not part of the view.
|
|
133
|
+
*/
|
|
134
|
+
function fromSnapshot(view, snapshot) {
|
|
135
|
+
const messages = [];
|
|
136
|
+
const toolCalls = [];
|
|
137
|
+
const timeline = [];
|
|
138
|
+
const before = new Map(view.toolCalls.map((c) => [c.id, c]));
|
|
139
|
+
for (const m of snapshot) {
|
|
140
|
+
if (m.role === "tool") {
|
|
141
|
+
const call = toolCalls.find((c) => c.id === m.toolCallId);
|
|
142
|
+
const text = contentText(m.content);
|
|
143
|
+
if (call) {
|
|
144
|
+
call.result = text;
|
|
145
|
+
call.status = m.error ? "failed" : "complete";
|
|
146
|
+
if (m.error)
|
|
147
|
+
call.error = m.error;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
messages.push({ id: m.id, role: "tool", content: text, streaming: false });
|
|
151
|
+
timeline.push({ kind: "message", id: m.id });
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!ROLES.has(m.role))
|
|
156
|
+
continue;
|
|
157
|
+
const message = m;
|
|
158
|
+
const text = contentText(message.content);
|
|
159
|
+
if (text || message.role !== "assistant" || !message.toolCalls?.length) {
|
|
160
|
+
messages.push({
|
|
161
|
+
id: message.id,
|
|
162
|
+
role: message.role,
|
|
163
|
+
content: text,
|
|
164
|
+
streaming: false,
|
|
165
|
+
...(message.name ? { name: message.name } : {}),
|
|
166
|
+
});
|
|
167
|
+
timeline.push({ kind: "message", id: message.id });
|
|
168
|
+
}
|
|
169
|
+
if (message.role !== "assistant")
|
|
170
|
+
continue;
|
|
171
|
+
for (const t of message.toolCalls ?? []) {
|
|
172
|
+
const approval = before.get(t.id)?.approval;
|
|
173
|
+
toolCalls.push({
|
|
174
|
+
id: t.id,
|
|
175
|
+
name: t.function.name,
|
|
176
|
+
args: t.function.arguments,
|
|
177
|
+
status: "running",
|
|
178
|
+
parentMessageId: message.id,
|
|
179
|
+
...(approval ? { approval } : {}),
|
|
180
|
+
});
|
|
181
|
+
timeline.push({ kind: "tool-call", id: t.id });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// A call with no result in a snapshot of a run that is over has finished.
|
|
185
|
+
const settled = view.status === "running"
|
|
186
|
+
? toolCalls
|
|
187
|
+
: toolCalls.map((c) => c.status === "running" && !c.approval ? { ...c, status: "complete" } : c);
|
|
188
|
+
return { ...view, messages, toolCalls: settled, timeline };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Folds one AG-UI event into the view. Handles the run lifecycle
|
|
192
|
+
* (`RUN_STARTED`, `RUN_FINISHED` with its outcome, `RUN_ERROR`), steps, text
|
|
193
|
+
* messages and their chunks, tool calls and their chunks and results, shared
|
|
194
|
+
* state (`STATE_SNAPSHOT`, `STATE_DELTA` as JSON Patch), `MESSAGES_SNAPSHOT`,
|
|
195
|
+
* `CUSTOM` and `RAW`. Activity, reasoning and sub-agent events leave the view
|
|
196
|
+
* as it is.
|
|
197
|
+
*/
|
|
198
|
+
export function reduceChat(view, event) {
|
|
199
|
+
switch (event.type) {
|
|
200
|
+
case EventType.RUN_STARTED:
|
|
201
|
+
return {
|
|
202
|
+
...view,
|
|
203
|
+
threadId: event.threadId,
|
|
204
|
+
runId: event.runId,
|
|
205
|
+
status: "running",
|
|
206
|
+
error: null,
|
|
207
|
+
result: undefined,
|
|
208
|
+
steps: [],
|
|
209
|
+
interrupts: [],
|
|
210
|
+
toolCalls: mapWhere(view.toolCalls, (c) => c.approval !== undefined, ({ approval: _, ...c }) => c),
|
|
211
|
+
};
|
|
212
|
+
case EventType.RUN_FINISHED:
|
|
213
|
+
return finish(view, event);
|
|
214
|
+
case EventType.RUN_ERROR:
|
|
215
|
+
return {
|
|
216
|
+
...view,
|
|
217
|
+
status: "error",
|
|
218
|
+
error: { message: event.message, ...(event.code ? { code: event.code } : {}) },
|
|
219
|
+
...closeRun(view, "failed", (c) => ({ ...c, status: "failed" })),
|
|
220
|
+
};
|
|
221
|
+
case EventType.STEP_STARTED:
|
|
222
|
+
return { ...view, steps: [...view.steps, { name: event.stepName, status: "running" }] };
|
|
223
|
+
case EventType.STEP_FINISHED: {
|
|
224
|
+
const steps = view.steps.slice();
|
|
225
|
+
let i = steps.length - 1;
|
|
226
|
+
while (i >= 0 && !(steps[i].name === event.stepName && steps[i].status === "running"))
|
|
227
|
+
i--;
|
|
228
|
+
if (i < 0)
|
|
229
|
+
steps.push({ name: event.stepName, status: "finished" });
|
|
230
|
+
else
|
|
231
|
+
steps[i] = { ...steps[i], status: "finished" };
|
|
232
|
+
return { ...view, steps };
|
|
233
|
+
}
|
|
234
|
+
case EventType.TEXT_MESSAGE_START:
|
|
235
|
+
return upsertMessage(view, event.messageId, (m) => ({ ...m, streaming: true, ...(event.name ? { name: event.name } : {}) }), { role: event.role ?? "assistant", streaming: true });
|
|
236
|
+
case EventType.TEXT_MESSAGE_CONTENT:
|
|
237
|
+
return upsertMessage(view, event.messageId, (m) => ({
|
|
238
|
+
...m,
|
|
239
|
+
content: m.content + event.delta,
|
|
240
|
+
streaming: true,
|
|
241
|
+
}));
|
|
242
|
+
case EventType.TEXT_MESSAGE_END:
|
|
243
|
+
return upsertMessage(view, event.messageId, (m) => ({ ...m, streaming: false }));
|
|
244
|
+
case EventType.TEXT_MESSAGE_CHUNK: {
|
|
245
|
+
const last = view.messages.at(-1);
|
|
246
|
+
const id = event.messageId ?? (last?.streaming ? last.id : undefined);
|
|
247
|
+
if (!id)
|
|
248
|
+
return view;
|
|
249
|
+
// A chunk for a new message closes the one before it.
|
|
250
|
+
const closed = last?.streaming && last.id !== id
|
|
251
|
+
? upsertMessage(view, last.id, (m) => ({ ...m, streaming: false }))
|
|
252
|
+
: view;
|
|
253
|
+
return upsertMessage(closed, id, (m) => ({ ...m, content: m.content + (event.delta ?? ""), streaming: true }), {
|
|
254
|
+
role: event.role ?? "assistant",
|
|
255
|
+
streaming: true,
|
|
256
|
+
...(event.name ? { name: event.name } : {}),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
case EventType.TOOL_CALL_START:
|
|
260
|
+
return upsertCall(view, event.toolCallId, (c) => ({
|
|
261
|
+
...c,
|
|
262
|
+
name: event.toolCallName,
|
|
263
|
+
...(event.parentMessageId ? { parentMessageId: event.parentMessageId } : {}),
|
|
264
|
+
}), event.toolCallName);
|
|
265
|
+
case EventType.TOOL_CALL_ARGS:
|
|
266
|
+
return upsertCall(view, event.toolCallId, (c) => ({ ...c, args: c.args + event.delta }));
|
|
267
|
+
case EventType.TOOL_CALL_END:
|
|
268
|
+
return upsertCall(view, event.toolCallId, (c) => c.status === "streaming" ? { ...c, status: "running" } : c);
|
|
269
|
+
case EventType.TOOL_CALL_CHUNK: {
|
|
270
|
+
const last = view.toolCalls.at(-1);
|
|
271
|
+
const id = event.toolCallId ?? (last?.status === "streaming" ? last.id : undefined);
|
|
272
|
+
if (!id)
|
|
273
|
+
return view;
|
|
274
|
+
return upsertCall(view, id, (c) => ({
|
|
275
|
+
...c,
|
|
276
|
+
name: event.toolCallName ?? c.name,
|
|
277
|
+
args: c.args + (event.delta ?? ""),
|
|
278
|
+
...(event.parentMessageId ? { parentMessageId: event.parentMessageId } : {}),
|
|
279
|
+
}), event.toolCallName);
|
|
280
|
+
}
|
|
281
|
+
case EventType.TOOL_CALL_RESULT:
|
|
282
|
+
return upsertCall(view, event.toolCallId, (c) => ({
|
|
283
|
+
...c,
|
|
284
|
+
result: contentText(event.content),
|
|
285
|
+
status: "complete",
|
|
286
|
+
}));
|
|
287
|
+
case EventType.STATE_SNAPSHOT:
|
|
288
|
+
return { ...view, state: event.snapshot };
|
|
289
|
+
case EventType.STATE_DELTA:
|
|
290
|
+
try {
|
|
291
|
+
return { ...view, state: applyJsonPatch(view.state, event.delta) };
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return { ...view, rejectedDeltas: view.rejectedDeltas + 1 };
|
|
295
|
+
}
|
|
296
|
+
case EventType.MESSAGES_SNAPSHOT:
|
|
297
|
+
return fromSnapshot(view, event.messages);
|
|
298
|
+
case EventType.CUSTOM:
|
|
299
|
+
return { ...view, custom: [...view.custom, { name: event.name, value: event.value }] };
|
|
300
|
+
case EventType.RAW:
|
|
301
|
+
return {
|
|
302
|
+
...view,
|
|
303
|
+
raw: [
|
|
304
|
+
...view.raw,
|
|
305
|
+
{ event: event.event, ...(event.source ? { source: event.source } : {}) },
|
|
306
|
+
],
|
|
307
|
+
};
|
|
308
|
+
case EventType.ACTIVITY_SNAPSHOT:
|
|
309
|
+
case EventType.ACTIVITY_DELTA:
|
|
310
|
+
case EventType.REASONING_START:
|
|
311
|
+
case EventType.REASONING_MESSAGE_START:
|
|
312
|
+
case EventType.REASONING_MESSAGE_CONTENT:
|
|
313
|
+
case EventType.REASONING_MESSAGE_END:
|
|
314
|
+
case EventType.REASONING_MESSAGE_CHUNK:
|
|
315
|
+
case EventType.REASONING_END:
|
|
316
|
+
case EventType.REASONING_ENCRYPTED_VALUE:
|
|
317
|
+
case EventType.SUBAGENT_STARTED:
|
|
318
|
+
case EventType.SUBAGENT_FINISHED:
|
|
319
|
+
case EventType.SUBAGENT_ERROR:
|
|
320
|
+
return view;
|
|
321
|
+
default:
|
|
322
|
+
// Exhaustive over the protocol this was built on; a newer event leaves the view alone.
|
|
323
|
+
event;
|
|
324
|
+
return view;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/** Folds a sequence of events, from a fresh view or from `view`. */
|
|
328
|
+
export function foldChat(events, view = createChatView()) {
|
|
329
|
+
let current = view;
|
|
330
|
+
for (const event of events)
|
|
331
|
+
current = reduceChat(current, event);
|
|
332
|
+
return current;
|
|
333
|
+
}
|
|
334
|
+
/** True while the latest run is in flight: the composer offers stop, not send. */
|
|
335
|
+
export function isChatRunning(view) {
|
|
336
|
+
return view.status === "running";
|
|
337
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 6902 JSON Patch over plain JSON, for AG-UI's `STATE_DELTA`. Pure:
|
|
3
|
+
* `applyJsonPatch` never mutates its input, it returns a new document that
|
|
4
|
+
* shares the untouched branches. A failing operation throws `JsonPatchError`
|
|
5
|
+
* and nothing of the patch applies.
|
|
6
|
+
*/
|
|
7
|
+
import type { JsonPatch } from "@ag-ui/core";
|
|
8
|
+
export declare class JsonPatchError extends Error {
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
/** Applies every operation in order, or throws `JsonPatchError` and applies none. */
|
|
12
|
+
export declare function applyJsonPatch<T>(doc: T, patch: JsonPatch): T;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export class JsonPatchError extends Error {
|
|
2
|
+
name = "JsonPatchError";
|
|
3
|
+
}
|
|
4
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
const has = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
6
|
+
function parse(path) {
|
|
7
|
+
if (path === "")
|
|
8
|
+
return [];
|
|
9
|
+
if (!path.startsWith("/"))
|
|
10
|
+
throw new JsonPatchError(`bad pointer ${path}`);
|
|
11
|
+
return path
|
|
12
|
+
.slice(1)
|
|
13
|
+
.split("/")
|
|
14
|
+
.map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
15
|
+
}
|
|
16
|
+
function index(array, key, forAdd) {
|
|
17
|
+
if (forAdd && key === "-")
|
|
18
|
+
return array.length;
|
|
19
|
+
if (!/^(0|[1-9]\d*)$/.test(key))
|
|
20
|
+
throw new JsonPatchError(`bad array index ${key}`);
|
|
21
|
+
const i = Number(key);
|
|
22
|
+
if (i > array.length || (!forAdd && i === array.length)) {
|
|
23
|
+
throw new JsonPatchError(`index ${key} out of range`);
|
|
24
|
+
}
|
|
25
|
+
return i;
|
|
26
|
+
}
|
|
27
|
+
function get(doc, segments) {
|
|
28
|
+
let current = doc;
|
|
29
|
+
for (const segment of segments) {
|
|
30
|
+
if (Array.isArray(current))
|
|
31
|
+
current = current[index(current, segment, false)];
|
|
32
|
+
else if (isObject(current) && has(current, segment))
|
|
33
|
+
current = current[segment];
|
|
34
|
+
else
|
|
35
|
+
throw new JsonPatchError(`no value at /${segments.join("/")}`);
|
|
36
|
+
}
|
|
37
|
+
return current;
|
|
38
|
+
}
|
|
39
|
+
/** Rebuilds the path to `segments`, with `leaf` deciding the last step. */
|
|
40
|
+
function edit(doc, segments, leaf) {
|
|
41
|
+
const [head, ...rest] = segments;
|
|
42
|
+
if (head === undefined)
|
|
43
|
+
throw new JsonPatchError("the root is replaced, not edited");
|
|
44
|
+
if (rest.length === 0)
|
|
45
|
+
return leaf(doc, head);
|
|
46
|
+
if (Array.isArray(doc)) {
|
|
47
|
+
const i = index(doc, head, false);
|
|
48
|
+
const copy = doc.slice();
|
|
49
|
+
copy[i] = edit(doc[i], rest, leaf);
|
|
50
|
+
return copy;
|
|
51
|
+
}
|
|
52
|
+
if (isObject(doc) && has(doc, head))
|
|
53
|
+
return { ...doc, [head]: edit(doc[head], rest, leaf) };
|
|
54
|
+
throw new JsonPatchError(`no value at /${segments.join("/")}`);
|
|
55
|
+
}
|
|
56
|
+
function add(doc, path, value) {
|
|
57
|
+
const segments = parse(path);
|
|
58
|
+
if (segments.length === 0)
|
|
59
|
+
return value;
|
|
60
|
+
return edit(doc, segments, (parent, key) => {
|
|
61
|
+
if (Array.isArray(parent)) {
|
|
62
|
+
const copy = parent.slice();
|
|
63
|
+
copy.splice(index(parent, key, true), 0, value);
|
|
64
|
+
return copy;
|
|
65
|
+
}
|
|
66
|
+
if (isObject(parent))
|
|
67
|
+
return { ...parent, [key]: value };
|
|
68
|
+
throw new JsonPatchError(`cannot add under a scalar at ${path}`);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function remove(doc, path) {
|
|
72
|
+
return edit(doc, parse(path), (parent, key) => {
|
|
73
|
+
if (Array.isArray(parent)) {
|
|
74
|
+
const copy = parent.slice();
|
|
75
|
+
copy.splice(index(parent, key, false), 1);
|
|
76
|
+
return copy;
|
|
77
|
+
}
|
|
78
|
+
if (isObject(parent) && has(parent, key)) {
|
|
79
|
+
const copy = { ...parent };
|
|
80
|
+
delete copy[key];
|
|
81
|
+
return copy;
|
|
82
|
+
}
|
|
83
|
+
throw new JsonPatchError(`no value at ${path}`);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function replace(doc, path, value) {
|
|
87
|
+
const segments = parse(path);
|
|
88
|
+
if (segments.length === 0)
|
|
89
|
+
return value;
|
|
90
|
+
get(doc, segments);
|
|
91
|
+
return edit(doc, segments, (parent, key) => {
|
|
92
|
+
if (Array.isArray(parent)) {
|
|
93
|
+
const copy = parent.slice();
|
|
94
|
+
copy[index(parent, key, false)] = value;
|
|
95
|
+
return copy;
|
|
96
|
+
}
|
|
97
|
+
return { ...parent, [key]: value };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** Values from the patch are copied in, so a later edit of the event never reaches the state. */
|
|
101
|
+
const clone = (value) => value === undefined ? value : JSON.parse(JSON.stringify(value));
|
|
102
|
+
function equal(a, b) {
|
|
103
|
+
if (a === b)
|
|
104
|
+
return true;
|
|
105
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
106
|
+
return a.length === b.length && a.every((value, i) => equal(value, b[i]));
|
|
107
|
+
}
|
|
108
|
+
if (isObject(a) && isObject(b)) {
|
|
109
|
+
const keys = Object.keys(a);
|
|
110
|
+
return (keys.length === Object.keys(b).length &&
|
|
111
|
+
keys.every((key) => has(b, key) && equal(a[key], b[key])));
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
function applyOne(doc, operation) {
|
|
116
|
+
switch (operation.op) {
|
|
117
|
+
case "add":
|
|
118
|
+
return add(doc, operation.path, clone(operation.value));
|
|
119
|
+
case "remove":
|
|
120
|
+
return remove(doc, operation.path);
|
|
121
|
+
case "replace":
|
|
122
|
+
return replace(doc, operation.path, clone(operation.value));
|
|
123
|
+
case "move": {
|
|
124
|
+
if (operation.path === operation.from)
|
|
125
|
+
return doc;
|
|
126
|
+
if (operation.path.startsWith(`${operation.from}/`)) {
|
|
127
|
+
throw new JsonPatchError("cannot move a value into its own child");
|
|
128
|
+
}
|
|
129
|
+
const value = get(doc, parse(operation.from));
|
|
130
|
+
return add(remove(doc, operation.from), operation.path, value);
|
|
131
|
+
}
|
|
132
|
+
case "copy":
|
|
133
|
+
return add(doc, operation.path, clone(get(doc, parse(operation.from))));
|
|
134
|
+
case "test":
|
|
135
|
+
if (!equal(get(doc, parse(operation.path)), operation.value)) {
|
|
136
|
+
throw new JsonPatchError(`test failed at ${operation.path}`);
|
|
137
|
+
}
|
|
138
|
+
return doc;
|
|
139
|
+
default:
|
|
140
|
+
throw new JsonPatchError(`unknown op ${operation.op}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Applies every operation in order, or throws `JsonPatchError` and applies none. */
|
|
144
|
+
export function applyJsonPatch(doc, patch) {
|
|
145
|
+
let current = doc;
|
|
146
|
+
for (const operation of patch)
|
|
147
|
+
current = applyOne(current, operation);
|
|
148
|
+
return current;
|
|
149
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type AgUiEvent, type ChatView } from "./model.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pushes events as they arrive and says when the run's stream ends; returns
|
|
4
|
+
* the unsubscribe. The shape of an observable or an event emitter.
|
|
5
|
+
*/
|
|
6
|
+
export type AgUiSubscribe = (emit: (event: AgUiEvent) => void, done: (error?: unknown) => void) => () => void;
|
|
7
|
+
/**
|
|
8
|
+
* Where a run's events come from: an async iterable (an `@ag-ui/client`
|
|
9
|
+
* agent's stream, an SSE reader, a recorded run) or a subscribe function.
|
|
10
|
+
* The host owns the connection; the hook only reads.
|
|
11
|
+
*/
|
|
12
|
+
export type AgUiEventSource = AsyncIterable<AgUiEvent> | AgUiSubscribe;
|
|
13
|
+
export interface UseAgUiRunOptions {
|
|
14
|
+
/** The conversation so far, from history. Read once, on mount. */
|
|
15
|
+
initialView?: ChatView;
|
|
16
|
+
/** A stream that failed: the hook also folds it into the view as a run error. */
|
|
17
|
+
onError?: (error: unknown) => void;
|
|
18
|
+
}
|
|
19
|
+
export interface AgUiRun {
|
|
20
|
+
view: ChatView;
|
|
21
|
+
/** True from `run` until its stream ends or `stop`. */
|
|
22
|
+
running: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Reads one run's events into the view until the stream ends. Starting a
|
|
25
|
+
* run stops reading the one before. Resolves when the stream ends, is
|
|
26
|
+
* stopped or fails; a failure lands in the view, never as a rejection.
|
|
27
|
+
*/
|
|
28
|
+
run: (source: AgUiEventSource) => Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Stops reading the current run and closes the view as cancelled. Telling
|
|
31
|
+
* the agent to stop is the host's (its own request); this only lets go.
|
|
32
|
+
*/
|
|
33
|
+
stop: () => void;
|
|
34
|
+
/** Folds events the host makes itself: the person's own message before the run starts. */
|
|
35
|
+
dispatch: (events: AgUiEvent | readonly AgUiEvent[]) => void;
|
|
36
|
+
/** Starts over, from nothing or from `view`. Stops reading first. */
|
|
37
|
+
reset: (view?: ChatView) => void;
|
|
38
|
+
}
|
|
39
|
+
/** The person's own message as the events that fold it into the view. */
|
|
40
|
+
export declare function userMessageEvents(id: string, text: string): AgUiEvent[];
|
|
41
|
+
/**
|
|
42
|
+
* Holds a `ChatView` and folds a host-provided AG-UI event source into it,
|
|
43
|
+
* one run at a time. No network code: the host connects, authenticates and
|
|
44
|
+
* sends input, and hands the hook what comes back.
|
|
45
|
+
*
|
|
46
|
+
* const chat = useAgUiRun();
|
|
47
|
+
* onSubmit={(text) => {
|
|
48
|
+
* chat.dispatch(userMessageEvents(id(), text));
|
|
49
|
+
* void chat.run(agent.stream({ threadId, text }));
|
|
50
|
+
* }}
|
|
51
|
+
*/
|
|
52
|
+
export declare function useAgUiRun({ initialView, onError }?: UseAgUiRunOptions): AgUiRun;
|