@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,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { ArrowDownIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { Button } from "../primitives/Button.js";
|
|
7
|
+
import { useUI } from "../provider/UIProvider.js";
|
|
8
|
+
import { EmptyState } from "../states/EmptyState.js";
|
|
9
|
+
import { LoadingState } from "../states/LoadingState.js";
|
|
10
|
+
import { MessageBubble } from "./MessageBubble.js";
|
|
11
|
+
import { ToolCallCard } from "./ToolCallCard.js";
|
|
12
|
+
const MemoBubble = React.memo(MessageBubble);
|
|
13
|
+
const MemoToolCall = React.memo(ToolCallCard);
|
|
14
|
+
/** How close to the end, in pixels, still counts as reading the latest. */
|
|
15
|
+
const PINNED = 48;
|
|
16
|
+
/**
|
|
17
|
+
* The conversation in arrival order: message bubbles with tool call cards
|
|
18
|
+
* between them, a failed run's error at the end. It follows the stream while
|
|
19
|
+
* the person is at the bottom and stops following when they scroll up, with
|
|
20
|
+
* a button back to the latest. Each bubble re-renders only when its message
|
|
21
|
+
* changes, so a long conversation streams without virtualization; a host
|
|
22
|
+
* with thousands of turns pages its history.
|
|
23
|
+
*/
|
|
24
|
+
export function MessageList({ view, renderContent, renderToolCall, renderAfter, footer, toolTitle, onApprove, onDeny, onRetry, loading = false, empty, label, className, }) {
|
|
25
|
+
const { messages } = useUI();
|
|
26
|
+
const t = messages.chat;
|
|
27
|
+
const scroller = React.useRef(null);
|
|
28
|
+
const [pinned, setPinned] = React.useState(true);
|
|
29
|
+
const byId = React.useMemo(() => ({
|
|
30
|
+
messages: new Map(view.messages.map((m) => [m.id, m])),
|
|
31
|
+
calls: new Map(view.toolCalls.map((c) => [c.id, c])),
|
|
32
|
+
}), [view.messages, view.toolCalls]);
|
|
33
|
+
React.useLayoutEffect(() => {
|
|
34
|
+
const el = scroller.current;
|
|
35
|
+
if (pinned && el)
|
|
36
|
+
el.scrollTop = el.scrollHeight;
|
|
37
|
+
}, [pinned, byId, view.error]);
|
|
38
|
+
const jump = () => {
|
|
39
|
+
const el = scroller.current;
|
|
40
|
+
if (el)
|
|
41
|
+
el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
|
|
42
|
+
setPinned(true);
|
|
43
|
+
};
|
|
44
|
+
let body;
|
|
45
|
+
if (loading)
|
|
46
|
+
body = _jsx(LoadingState, { variant: "rows", rows: 3 });
|
|
47
|
+
else if (view.timeline.length === 0 && !view.error)
|
|
48
|
+
body = empty ?? _jsx(EmptyState, { title: t.empty, description: t.emptyDescription });
|
|
49
|
+
else
|
|
50
|
+
body = (_jsxs(_Fragment, { children: [view.timeline.map((item) => {
|
|
51
|
+
const after = renderAfter?.(item);
|
|
52
|
+
let node = null;
|
|
53
|
+
if (item.kind === "message") {
|
|
54
|
+
const message = byId.messages.get(item.id);
|
|
55
|
+
node = message ? _jsx(MemoBubble, { message: message, renderContent: renderContent }) : null;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const call = byId.calls.get(item.id);
|
|
59
|
+
const custom = call ? renderToolCall?.(call) : undefined;
|
|
60
|
+
node = !call ? null : custom !== undefined ? (custom) : (_jsx(MemoToolCall, { call: call, title: toolTitle?.(call), onApprove: onApprove, onDeny: onDeny }));
|
|
61
|
+
}
|
|
62
|
+
return (_jsxs(React.Fragment, { children: [node, after] }, `${item.kind === "message" ? "m" : "t"}:${item.id}`));
|
|
63
|
+
}), footer, view.status === "error" && view.error && (_jsxs("div", { role: "alert", className: "flex min-w-0 flex-col gap-2 rounded-lg border border-danger-border bg-danger-bg px-3 py-2 text-sm text-danger-fg sm:flex-row sm:items-center", children: [_jsxs("p", { className: "min-w-0 flex-1 [overflow-wrap:anywhere]", children: [_jsx("span", { className: "font-medium", children: t.runFailed }), ": ", view.error.message] }), onRetry && (_jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, children: t.retry }))] }))] }));
|
|
64
|
+
return (_jsxs("div", { "data-slot": "message-list", className: cn("relative flex min-h-0 flex-col", className), children: [_jsx("div", { ref: scroller, role: "log", "aria-label": label ?? t.conversation, "aria-busy": view.status === "running" || loading || undefined,
|
|
65
|
+
// A scrolling region is reached by keyboard (WCAG 2.1.1).
|
|
66
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
67
|
+
tabIndex: 0, onScroll: (event) => {
|
|
68
|
+
const el = event.currentTarget;
|
|
69
|
+
setPinned(el.scrollHeight - el.scrollTop - el.clientHeight < PINNED);
|
|
70
|
+
}, className: "flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", children: body }), !pinned && (_jsxs(Button, { variant: "outline", size: "sm", onClick: jump, className: "absolute bottom-3 left-1/2 -translate-x-1/2 shadow-md", children: [_jsx(ArrowDownIcon, { "aria-hidden": true }), t.jumpToLatest] }))] }));
|
|
71
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type SelectOption } from "../primitives/Select.js";
|
|
2
|
+
export interface ModelPickerProps {
|
|
3
|
+
/** The models this host offers, in its words. The package names none. */
|
|
4
|
+
options: readonly SelectOption[];
|
|
5
|
+
value: string;
|
|
6
|
+
onValueChange: (value: string) => void;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
/** Names the picker. Defaults to "Model". */
|
|
9
|
+
label?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The model for the next turn, from the options the host passes: which
|
|
14
|
+
* models exist, which the person may use and what each costs are the host's.
|
|
15
|
+
* Small enough for a composer's actions.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ModelPicker({ options, value, onValueChange, disabled, label, className, }: ModelPickerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useUI } from "../provider/UIProvider.js";
|
|
4
|
+
import { Select } from "../primitives/Select.js";
|
|
5
|
+
/**
|
|
6
|
+
* The model for the next turn, from the options the host passes: which
|
|
7
|
+
* models exist, which the person may use and what each costs are the host's.
|
|
8
|
+
* Small enough for a composer's actions.
|
|
9
|
+
*/
|
|
10
|
+
export function ModelPicker({ options, value, onValueChange, disabled, label, className, }) {
|
|
11
|
+
const { messages } = useUI();
|
|
12
|
+
return (_jsx(Select, { options: options, value: value, onValueChange: onValueChange, disabled: disabled, "aria-label": label ?? messages.chat.model, size: "sm", className: className }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type ChatToolCall } from "./model.js";
|
|
3
|
+
export interface ToolCallCardProps {
|
|
4
|
+
call: ChatToolCall;
|
|
5
|
+
/** The tool's name for people ("Run SQL"). Defaults to the call's name. */
|
|
6
|
+
title?: string;
|
|
7
|
+
/** Draws the result: a table, a chart. Defaults to the result as text, pretty-printed when it is JSON. */
|
|
8
|
+
renderResult?: (call: ChatToolCall) => React.ReactNode;
|
|
9
|
+
/** Whether the details start open. A call waiting for approval always starts open. */
|
|
10
|
+
defaultOpen?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Approves a call the run paused on, after the confirm step. Awaited; see
|
|
13
|
+
* ConfirmDialog. The host resumes the run (AG-UI `resume`); the card only
|
|
14
|
+
* reports the decision.
|
|
15
|
+
*/
|
|
16
|
+
onApprove?: (call: ChatToolCall) => Promise<unknown> | unknown;
|
|
17
|
+
/** Denies it, after the confirm step, with the reason the person gave. */
|
|
18
|
+
onDeny?: (call: ChatToolCall, reason: string) => Promise<unknown> | unknown;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One tool call: its name and status, its arguments as they stream and its
|
|
23
|
+
* result or error behind a disclosure, and, when the run paused on it,
|
|
24
|
+
* approve and deny through the library's confirm flow (a denial always with
|
|
25
|
+
* a reason). Host-driven: it never runs or resumes anything.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ToolCallCard({ call, title, renderResult, defaultOpen, onApprove, onDeny, className, }: ToolCallCardProps): React.JSX.Element;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronRightIcon, WrenchIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { ConfirmDialog } from "../overlays/ConfirmDialog.js";
|
|
7
|
+
import { Button } from "../primitives/Button.js";
|
|
8
|
+
import { useUI } from "../provider/UIProvider.js";
|
|
9
|
+
import { StatusBadge } from "../status/StatusBadge.js";
|
|
10
|
+
import { parseToolCallArgs } from "./model.js";
|
|
11
|
+
const TONE = {
|
|
12
|
+
streaming: "info",
|
|
13
|
+
running: "info",
|
|
14
|
+
complete: "ok",
|
|
15
|
+
failed: "error",
|
|
16
|
+
cancelled: "muted",
|
|
17
|
+
approval: "warn",
|
|
18
|
+
};
|
|
19
|
+
/** JSON text shown indented when it parses, as it came otherwise. */
|
|
20
|
+
const pretty = (text) => {
|
|
21
|
+
const value = parseToolCallArgs(text);
|
|
22
|
+
return value === undefined ? text : JSON.stringify(value, null, 2);
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* One tool call: its name and status, its arguments as they stream and its
|
|
26
|
+
* result or error behind a disclosure, and, when the run paused on it,
|
|
27
|
+
* approve and deny through the library's confirm flow (a denial always with
|
|
28
|
+
* a reason). Host-driven: it never runs or resumes anything.
|
|
29
|
+
*/
|
|
30
|
+
export function ToolCallCard({ call, title, renderResult, defaultOpen = false, onApprove, onDeny, className, }) {
|
|
31
|
+
const { messages } = useUI();
|
|
32
|
+
const t = messages.chat;
|
|
33
|
+
const id = React.useId();
|
|
34
|
+
const name = title ?? call.name;
|
|
35
|
+
const waiting = call.approval !== undefined;
|
|
36
|
+
const [open, setOpen] = React.useState(defaultOpen || waiting);
|
|
37
|
+
const [confirming, setConfirming] = React.useState(null);
|
|
38
|
+
// A call that comes to wait for approval opens, so the person sees what they approve.
|
|
39
|
+
const [wasWaiting, setWasWaiting] = React.useState(waiting);
|
|
40
|
+
if (waiting !== wasWaiting) {
|
|
41
|
+
setWasWaiting(waiting);
|
|
42
|
+
if (waiting)
|
|
43
|
+
setOpen(true);
|
|
44
|
+
}
|
|
45
|
+
const status = waiting ? "approval" : call.status;
|
|
46
|
+
const detailsId = `${id}-details`;
|
|
47
|
+
return (_jsxs("section", { "aria-label": t.toolCall(name), "data-slot": "tool-call-card", "data-status": status, className: cn("flex min-w-0 flex-col rounded-lg border bg-card text-sm", className), children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2 px-3 py-2", children: [_jsx(WrenchIcon, { "aria-hidden": true, className: "size-4 shrink-0 text-muted-foreground" }), _jsxs("button", { type: "button", "aria-expanded": open, "aria-controls": detailsId, onClick: () => setOpen((o) => !o), className: "inline-flex min-w-0 items-center gap-1 rounded-sm text-left outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx(ChevronRightIcon, { "aria-hidden": true, className: cn("size-3.5 shrink-0 transition-transform", open && "rotate-90") }), _jsx("span", { className: "font-mono text-xs font-medium [overflow-wrap:anywhere]", children: name }), " ", _jsx("span", { className: "sr-only", children: t.details })] }), _jsx(StatusBadge, { tone: TONE[status], pulse: status === "streaming" || status === "running", className: "ml-auto", children: t.toolStatus(status) })] }), open && (_jsxs("div", { id: detailsId, className: "flex min-w-0 flex-col gap-2 border-t px-3 py-2", children: [_jsx(Block, { label: t.arguments, children: call.args ? pretty(call.args) : t.noArguments }), call.error !== undefined && (_jsx(Block, { label: t.toolError, tone: "error", children: call.error })), call.result !== undefined &&
|
|
48
|
+
(renderResult ? (_jsx("div", { className: "min-w-0", children: renderResult(call) })) : (_jsx(Block, { label: t.result, children: pretty(call.result) })))] })), waiting && (onApprove || onDeny) && (_jsxs("div", { className: "flex min-w-0 flex-col gap-2 border-t px-3 py-2 sm:flex-row sm:items-center", children: [_jsx("p", { className: "min-w-0 flex-1 [overflow-wrap:anywhere]", children: call.approval?.message ?? t.approvalNeeded }), _jsxs("div", { className: "flex gap-2", children: [onDeny && (_jsx(Button, { variant: "outline", size: "sm", onClick: () => setConfirming("deny"), children: t.deny })), onApprove && (_jsx(Button, { size: "sm", onClick: () => setConfirming("approve"), children: t.approve }))] })] })), _jsx(ConfirmDialog, { open: confirming === "approve", onOpenChange: (next) => !next && setConfirming(null), title: t.approveTitle(name), description: call.approval?.message ?? t.approveDescription, confirmLabel: t.approve, onConfirm: () => onApprove?.(call) }), _jsx(ConfirmDialog, { open: confirming === "deny", onOpenChange: (next) => !next && setConfirming(null), title: t.denyTitle(name), description: t.denyDescription, confirmLabel: t.deny, destructive: true, reason: { label: t.denyReason }, onConfirm: (reason) => onDeny?.(call, reason) })] }));
|
|
49
|
+
}
|
|
50
|
+
function Block({ label, tone, children, }) {
|
|
51
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsx("p", { className: "font-mono text-2xs font-medium tracking-wider text-muted-foreground uppercase", children: label }), _jsx("pre", { role: "region", "aria-label": label, tabIndex: 0, className: cn("max-h-64 min-w-0 overflow-auto rounded-md bg-muted p-2 font-mono text-xs [overflow-wrap:anywhere] whitespace-pre-wrap outline-none focus-visible:ring-2 focus-visible:ring-ring", tone === "error" && "bg-danger-bg text-danger-fg"), children: children })] }));
|
|
52
|
+
}
|
|
@@ -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;
|