@calliopelabs/ui 0.14.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 +142 -1
- package/dist/agent/AgentChat.d.ts +8 -1
- package/dist/agent/AgentChat.js +43 -2
- package/dist/agent/WorkflowRenderers.d.ts +61 -0
- package/dist/agent/WorkflowRenderers.js +102 -0
- package/dist/agent/controller.d.ts +19 -0
- package/dist/agent/controller.js +33 -0
- package/dist/agent/copilotkit/CopilotKitAgentProvider.js +2 -2
- package/dist/agent/index.d.ts +4 -2
- package/dist/agent/index.js +3 -1
- package/dist/agent/mock.d.ts +26 -4
- package/dist/agent/mock.js +68 -37
- package/dist/agent/renderers.d.ts +49 -0
- package/dist/agent/renderers.js +27 -0
- package/dist/appearance/brands.d.ts +17 -0
- package/dist/appearance/brands.js +14 -0
- package/dist/chat/MessageBubble.js +1 -1
- package/dist/chat/MessageList.d.ts +9 -2
- package/dist/chat/MessageList.js +11 -8
- 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 +6 -1
- package/dist/index.js +6 -1
- package/dist/primitives/Badge.d.ts +14 -0
- package/dist/primitives/Badge.js +25 -0
- 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 +69 -0
- package/dist/provider/messages.js +60 -0
- package/dist/theme/css-layer.js +2 -1
- package/dist/theme/tokens.generated.d.ts +3 -0
- package/dist/theme/tokens.generated.js +3 -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 +5 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ It is built Storybook first: every component and every screen pattern is built a
|
|
|
16
16
|
npm i @calliopelabs/ui
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Peer dependencies: `react` and `react-dom` 19, and `tailwindcss` 4 for styling.
|
|
19
|
+
Peer dependencies: `react` and `react-dom` 19, and `tailwindcss` 4 for styling. The opt-in agent entries add `@ag-ui/client` and `@copilotkit/react-core` (see [Agent](#agent)).
|
|
20
20
|
|
|
21
21
|
## Styles
|
|
22
22
|
|
|
@@ -428,6 +428,147 @@ const chat = useAgUiRun();
|
|
|
428
428
|
|
|
429
429
|
`agent` above is the host's own client. The catalog's `Chat/ChatPanel` composes the pieces the same way, driven by a recorded run. Their copy is in the `chat` message group.
|
|
430
430
|
|
|
431
|
+
## Agent
|
|
432
|
+
|
|
433
|
+
`CalliopeAgent` is the one embedded agent for every Calliope AI product: a floating bubble, the shell's right rail, inline in a page, or a page of its own. It speaks [AG-UI](https://docs.ag-ui.com) to any agent, directly or through a [CopilotKit](https://docs.copilotkit.ai) runtime, and draws every pixel with the chat kit above, on the product's tokens, mark and runtime brand. CopilotKit wires the runtime and runs the agent; its UI and stylesheet are never loaded.
|
|
434
|
+
|
|
435
|
+
It is an opt-in entry, so apps without an agent never load its dependencies:
|
|
436
|
+
|
|
437
|
+
```sh
|
|
438
|
+
npm i @ag-ui/client # @calliopelabs/ui/agent
|
|
439
|
+
npm i @ag-ui/client @copilotkit/react-core # @calliopelabs/ui/agent/copilotkit (1.79.1 or later, 1.x)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Both are optional peers. `npm run pack:check` fails if the main entry ever reaches them. Most of the entry's weight is `@ag-ui/client` itself (with its protobuf codec, about 150 KB gzipped; the agent's own code is about 24 KB minified), which a CopilotKit app already ships. To keep it off a page's first load, load the provider when the person first opens the assistant (`next/dynamic` with `ssr: false`) and keep it mounted after.
|
|
443
|
+
|
|
444
|
+
### A Next.js app with a CopilotKit runtime
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
// app/api/copilotkit/route.ts: the runtime, on the server (agents, keys and auth stay here)
|
|
448
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
449
|
+
import { CopilotRuntime, createCopilotRuntimeHandler } from "@copilotkit/runtime/v2";
|
|
450
|
+
|
|
451
|
+
const runtime = new CopilotRuntime({
|
|
452
|
+
agents: { default: new HttpAgent({ url: process.env.AGENT_URL! }) },
|
|
453
|
+
});
|
|
454
|
+
const handler = createCopilotRuntimeHandler({
|
|
455
|
+
runtime,
|
|
456
|
+
basePath: "/api/copilotkit",
|
|
457
|
+
mode: "single-route",
|
|
458
|
+
});
|
|
459
|
+
export const POST = (request: Request) => handler(request);
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
```tsx
|
|
463
|
+
// app/assistant.tsx
|
|
464
|
+
"use client";
|
|
465
|
+
import { CalliopeAgent, useAgentReadable, useAgentTool } from "@calliopelabs/ui/agent";
|
|
466
|
+
import { CopilotKitAgentProvider } from "@calliopelabs/ui/agent/copilotkit";
|
|
467
|
+
import { z } from "zod";
|
|
468
|
+
|
|
469
|
+
export function Assistant({ children, project }: { children: React.ReactNode; project: string }) {
|
|
470
|
+
return (
|
|
471
|
+
<CopilotKitAgentProvider
|
|
472
|
+
runtimeUrl="/api/copilotkit"
|
|
473
|
+
useSingleEndpoint
|
|
474
|
+
headers={{ "x-project": project }}
|
|
475
|
+
>
|
|
476
|
+
{children}
|
|
477
|
+
<CalliopeAgent mode="bubble" persistKey="app.assistant.open" />
|
|
478
|
+
</CopilotKitAgentProvider>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Anywhere under it, while mounted:
|
|
483
|
+
useAgentReadable("The open project", { project });
|
|
484
|
+
useAgentTool({
|
|
485
|
+
name: "open_scene",
|
|
486
|
+
parameters: z.object({ id: z.string() }),
|
|
487
|
+
handler: ({ id }) => router.push(`/scenes/${id}`),
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Mount it from the root layout (a Server Component) as `<Assistant project={slug}>{children}</Assistant>`. Tools carry functions, so they are registered in client components, never passed from a server one.
|
|
492
|
+
|
|
493
|
+
### A plain AG-UI endpoint
|
|
494
|
+
|
|
495
|
+
```tsx
|
|
496
|
+
"use client";
|
|
497
|
+
import { CalliopeAgent, CalliopeAgentProvider } from "@calliopelabs/ui/agent";
|
|
498
|
+
|
|
499
|
+
<CalliopeAgentProvider url="/api/agui" headers={{ "x-project": slug }} suggestions={startingPoints}>
|
|
500
|
+
<AppShell
|
|
501
|
+
{...shell}
|
|
502
|
+
rightRail={<CalliopeAgent mode="rail" open={open} onOpenChange={setOpen} />}
|
|
503
|
+
rightRailLabel="Assistant"
|
|
504
|
+
onToggleRightRail={() => setOpen((o) => !o)}
|
|
505
|
+
>
|
|
506
|
+
{children}
|
|
507
|
+
</AppShell>
|
|
508
|
+
</CalliopeAgentProvider>;
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
`url` makes an `HttpAgent` (same origin, so the session cookie goes along); `agent` takes any `AbstractAgent` instead. `createFunctionAgent(run)` turns a function that yields AG-UI events (or an existing non-AG-UI endpoint, adapted) into one.
|
|
512
|
+
|
|
513
|
+
### What it does
|
|
514
|
+
|
|
515
|
+
- **One conversation, one store.** `useCalliopeAgent()` returns the chat kit's `ChatView` (messages, tool calls, steps, interrupts, shared state, custom events) and the actions: `send(text, { parts })`, `stop()` (aborts the run and any tool waiting), `regenerate()`, `approve(call)` and `deny(call, reason)`, `resolveInterrupt(id, response)`, `respond(toolCallId, result)`, `getState()` and `setState(state)`, `switchThread(id)` (with the provider's `loadThread`) and `newThread()`, plus `running`, `waiting`, `unread` and `suggestions`.
|
|
516
|
+
- **Tools.** `useAgentTool(defineTool({ name, description, parameters, handler, render, followUp }))` offers a browser tool while the component is mounted. `parameters` is a Standard Schema (zod 4, valibot, arktype), which types and validates the arguments, or a JSON Schema. A tool with `render` and no `handler` is human-in-the-loop: the run waits until its card calls `respond(result)`, and the composer holds meanwhile.
|
|
517
|
+
- **Readables and suggestions.** `useAgentReadable(description, value)` sends page context with every run; `useAgentSuggestions([{ title, message }])` offers starting points. Both also come as provider props for the whole app.
|
|
518
|
+
- **Approvals.** A run that ends on an AG-UI interrupt pauses. A tool call it names gets approve and deny on its card (a denial always with a reason); any other interrupt gets `InterruptCard`, or the host's `renderers.interrupts[reason]`. The run resumes with AG-UI `resume` once every open interrupt has an answer; a new message cancels what is still open.
|
|
519
|
+
- **Generative UI.** `renderers` on `CalliopeAgent` maps tool names (backend tools too), `CUSTOM` event names and interrupt reasons to host components, and draws the shared state above the conversation. `workflowRenderers` draws the run and review components: `calliope.run` on `RunTimeline` (events with one `id` update one card), `calliope.artifact` on `ArtifactCard` with `DocumentReview`, `MediaReview` or `FormReview` (signing or sending the review resumes the run), and `calliope.question` interrupts on `WorkflowQuestionCard`.
|
|
520
|
+
- **Surfaces.** `mode="bubble"` (a launcher and a non-modal panel; a drawer on phones), `"rail"` (a `RailPanel` for `AppShell`'s `rightRail`; the shell's drawer on phones), `"inline"` and `"page"`. Open state is controlled, held, or remembered per person with `persistKey`; `shortcut` (`mod+/` by default) toggles it; answers that land while it is closed show as unread on the launcher. Slots: `renderContent` (the host's markdown), `attachments` and `sendOptions` (attachments as AG-UI input parts), `composerActions`, `modelPicker` (pass the choice in the provider's `forwardedProps`), `headerActions`, `empty`.
|
|
521
|
+
- **Telemetry and errors.** `onEvent(event)` sees every AG-UI event; `onError(error)` every failed run (also shown in the conversation with retry).
|
|
522
|
+
- **Testing.** `createMockAgent({ turns })` plays recorded turns; `createFunctionAgent(run)` makes an agent of any function that yields AG-UI events. Both are real `HttpAgent`s answered in the page, so stories and tests need no network.
|
|
523
|
+
|
|
524
|
+
Headers on the provider run in the browser: never put a secret there. Keys, the model and authorization stay on the server, in the runtime route or the agent. Copy is in the `agent` message group.
|
|
525
|
+
|
|
526
|
+
### Moving to it
|
|
527
|
+
|
|
528
|
+
| Product | Today | With `CalliopeAgent` |
|
|
529
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
530
|
+
| Astrolift Studio | `CopilotKitProvider` (1.79.1) and its own `AssistantPanel`, `AssistantTranscript`, `AssistantInput`, `SuggestionChips`, `AssistantLauncher`, `dock.tsx`, `useStored` open state | `CopilotKitAgentProvider runtimeUrl="/api/assistant" useSingleEndpoint agentId="studio" headers={{ [PROJECT_HEADER]: slug }}` (or keep its `CopilotKitProvider`: ours reuses it) and `<CalliopeAgent mode="rail" persistKey={openKey}>` in the shell's rail. The 15 `useHumanInTheLoop` actions become `useAgentTool({ name, parameters, render })` with `ActionCard` as `render` (calling `respond`); the whiteboard tools become `useAgentTool` with their zod schemas; `useAgentContext` readables become `useAgentReadable`; `startingPoints` becomes `useAgentSuggestions`; thread loading becomes `loadThread`. The server route and runner stay. |
|
|
531
|
+
| Zentinelle | `ChatBubble` (sparkle button, `Drawer`), `use-chat` parsing a custom `data:` stream, `MessageList`, `ChatInput`, `ModelSelector`, full-page `AssistantScreen` | `<CalliopeAgent mode="bubble">` and `mode="page"`, with `modelPicker` fed from `/assistant/providers` and the model in `forwardedProps`. Transport: port `/assistant/chat` to AG-UI (`pending_action` as an interrupt, `navigation` as a `CUSTOM` event drawn as a link chip), or until then adapt it with `createFunctionAgent` over `authenticatedFetch`. An approval resolves the interrupt; the host's `execute-tool` call with the approval token stays a host action in the interrupt renderer, gated on `canMutate`. |
|
|
532
|
+
| Astrolift app (Wayfinding) | `WayfindingBubble` and `use-wayfinding` posting to `/wayfinding/ask/` | `<CalliopeAgent mode="bubble">` over `createFunctionAgent` that posts the question and returns the answer as text and the routes as a `CUSTOM` event, drawn by a `renderers.custom.routes` component of `Link` chips that close the panel. No tools: read-only by construction. A 503 thrown from the function shows as the run's error. |
|
|
533
|
+
| Boilerworks django-nextjs-copilotkit | CopilotKit 1.63.1 v1 (`CopilotKit`, `CopilotSidebar`, `useCopilotAction`, `useCopilotReadable`, `react-ui` CSS re-themed) | Upgrade to `@copilotkit/react-core` 1.79+ and `@copilotkit/runtime/v2` (the runtime route keeps its `HttpAgent` to Django), then `CopilotKitAgentProvider runtimeUrl="/api/copilotkit" agentId="boilerworks_agent"` and `<CalliopeAgent mode="rail">` in place of `CopilotSidebar`. `useCopilotReadable` becomes `useAgentReadable`; `renderAndWaitForResponse` becomes `useAgentTool` with `render` and `respond`; the render-only `draft_form_definition` card becomes `renderers.tools.draft_form_definition`. Drop `@copilotkit/react-ui` and `copilot.css`. |
|
|
534
|
+
|
|
535
|
+
## Workbench
|
|
536
|
+
|
|
537
|
+
`@calliopelabs/ui/workbench` is an opt-in entry for IDE-style tools (File Manager, Loadr, Chat Studio): an app without a file browser never loads it. It needs nothing beyond the package's own dependencies, and its styles come from the preset. Like the rest, it is host-driven: data comes in as props, every change goes out through a callback, and nothing fetches.
|
|
538
|
+
|
|
539
|
+
- **`FileBrowser`**: breadcrumb (its crumbs are drop targets), list and grid views, sorting (folders first, natural order), selection, a context menu, drag-and-drop moves (Alt copies) and an optional folder tree. View, sort and selection work uncontrolled or controlled. Copy for an empty folder, a toolbar slot, and thumbnails for grid tiles.
|
|
540
|
+
- **`FileTree`**: a lazy tree on the WAI-ARIA tree pattern (arrows open and move, Home, End, typeahead). The host answers `onLoad(path)` with a `FileBranch` (`loading`, `error`, `ready`) per directory in `branches`; a failed branch retries through `onRetry`.
|
|
541
|
+
- **`FileList`** and **`FileGrid`**, the two views on their own: a grid (WAI-ARIA grid pattern: arrows between cells, selection follows focus, Shift extends, Cmd/Ctrl moves without selecting, Cmd/Ctrl+A, Escape, Enter opens, Delete, F2 renames) and a multi-select listbox of tiles with two-dimensional arrows.
|
|
542
|
+
- **`UploadDropzone`**: wraps an area (an overlay while desktop files hover, progress floating in a corner) or draws its own box with a picker. `accept` and `maxSize` filter into `onFiles` and `onReject`; the host's `uploads` (`queued`, `uploading` with a percentage or none, `done`, `error`, `canceled`) draw as progress bars with cancel and dismiss.
|
|
543
|
+
- **`FilePreview`**: CSV and TSV (parsed here, quoted fields included) and server-parsed tables in the library's `DataTable` with search, sort and paging; JSON pretty-printed; images; text; markdown through the host's `renderMarkdown` (the package ships no markdown parser, so without one the source shows); a binary fallback, loading, error and a truncation note.
|
|
544
|
+
- Helpers: `FileItem`, `sortFiles`, `previewKindFor`, `parseDelimited`, `formatBytes`, `canDrop`, `pathSegments`, `parentPath`, `joinPath`, `FileIcon`.
|
|
545
|
+
|
|
546
|
+
```tsx
|
|
547
|
+
import {
|
|
548
|
+
FileBrowser,
|
|
549
|
+
FilePreview,
|
|
550
|
+
UploadDropzone,
|
|
551
|
+
previewKindFor,
|
|
552
|
+
} from "@calliopelabs/ui/workbench";
|
|
553
|
+
|
|
554
|
+
<UploadDropzone onFiles={(files) => upload(files, path)} uploads={uploads} onCancel={cancel}>
|
|
555
|
+
<FileBrowser
|
|
556
|
+
path={path}
|
|
557
|
+
items={listing.data?.entries.map(toFileItem)}
|
|
558
|
+
error={listing.error}
|
|
559
|
+
onRetry={listing.refetch}
|
|
560
|
+
onNavigate={setPath}
|
|
561
|
+
onOpen={(item) => setPreview(item.path)}
|
|
562
|
+
onMove={(sources, target, { copy }) => (copy ? copyTo : moveTo)(sources, target)}
|
|
563
|
+
onDelete={confirmDelete}
|
|
564
|
+
contextMenu={(targets) => <FileActions targets={targets} />}
|
|
565
|
+
tree={{ root, branches, onLoad: loadFolder, onRetry: loadFolder }}
|
|
566
|
+
/>
|
|
567
|
+
</UploadDropzone>;
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Their copy is in the `workbench` message group. The catalog's **Workbench** section shows every state, light and dark, narrow, and large trees.
|
|
571
|
+
|
|
431
572
|
## Appearance
|
|
432
573
|
|
|
433
574
|
The person sets four axes, per product: the **ground** (black, charcoal, graphite, emerald, navy, paper, mist; it decides light or dark), the **accent** (the product's own, a family accent, or any colour that stands 3:1 off the ground), the **density** (`compact` hairline rows or boxed `cards`) and the **corners** (0, 2, 4 or 10 px). `AppearancePicker` is the settings UI for all four.
|
|
@@ -3,6 +3,7 @@ import type { ChatContentRenderer } from "../chat/MessageBubble.js";
|
|
|
3
3
|
import { type ChatToolCall } from "../chat/model.js";
|
|
4
4
|
import { type ModelPickerProps } from "../chat/ModelPicker.js";
|
|
5
5
|
import type { SendOptions } from "./controller.js";
|
|
6
|
+
import { type AgentRenderers } from "./renderers.js";
|
|
6
7
|
export interface AgentChatProps {
|
|
7
8
|
/** The panel's title. Defaults to the product's assistant ("Astrolift AI assistant"). */
|
|
8
9
|
title?: string;
|
|
@@ -23,6 +24,12 @@ export interface AgentChatProps {
|
|
|
23
24
|
renderToolCall?: (call: ChatToolCall) => React.ReactNode;
|
|
24
25
|
/** The tool's name for people, per call. */
|
|
25
26
|
toolTitle?: (call: ChatToolCall) => string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Generative UI: host components for tool calls, custom events, interrupts
|
|
29
|
+
* and the shared state (see `AgentRenderers`; `workflowRenderers` draws the
|
|
30
|
+
* run and review components). `renderToolCall` still wins for a call.
|
|
31
|
+
*/
|
|
32
|
+
renderers?: AgentRenderers;
|
|
26
33
|
/** "line": a one-line status while running (bubble, rail). "steps": the run's steps on the run timeline (page). */
|
|
27
34
|
progress?: "line" | "steps" | "none";
|
|
28
35
|
/** Replaces the empty state (the suggestions still show under it). */
|
|
@@ -53,4 +60,4 @@ export declare function NewConversationButton({ className }: {
|
|
|
53
60
|
* their own calls; one that waits on the person gets `respond`, and the
|
|
54
61
|
* composer holds until it is answered.
|
|
55
62
|
*/
|
|
56
|
-
export declare function AgentChat({ title, hideHeader, headerActions, onClose, expanded, onExpandedChange, hideNewConversation, renderContent, renderToolCall, toolTitle, progress, empty, attachments, composerActions, modelPicker, sendOptions, placeholder, autoFocus, suggestionsAfterRun, className, }: AgentChatProps): React.JSX.Element;
|
|
63
|
+
export declare function AgentChat({ title, hideHeader, headerActions, onClose, expanded, onExpandedChange, hideNewConversation, renderContent, renderToolCall, toolTitle, renderers, progress, empty, attachments, composerActions, modelPicker, sendOptions, placeholder, autoFocus, suggestionsAfterRun, className, }: AgentChatProps): React.JSX.Element;
|
package/dist/agent/AgentChat.js
CHANGED
|
@@ -14,6 +14,8 @@ import { useUI } from "../provider/UIProvider.js";
|
|
|
14
14
|
import { EmptyState } from "../states/EmptyState.js";
|
|
15
15
|
import { StatusDot } from "../status/StatusDot.js";
|
|
16
16
|
import { useCalliopeAgent } from "./AgentProvider.js";
|
|
17
|
+
import { parseResult } from "./renderers.js";
|
|
18
|
+
import { InterruptCard } from "./WorkflowRenderers.js";
|
|
17
19
|
function ToolRender({ tool, call, waiting, respond, }) {
|
|
18
20
|
return (_jsx(_Fragment, { children: tool.render?.({
|
|
19
21
|
call,
|
|
@@ -43,7 +45,7 @@ function Suggestions({ suggestions, disabled, onPick, }) {
|
|
|
43
45
|
* their own calls; one that waits on the person gets `respond`, and the
|
|
44
46
|
* composer holds until it is answered.
|
|
45
47
|
*/
|
|
46
|
-
export function AgentChat({ title, hideHeader = false, headerActions, onClose, expanded, onExpandedChange, hideNewConversation = false, renderContent, renderToolCall, toolTitle, progress = "line", empty, attachments, composerActions, modelPicker, sendOptions, placeholder, autoFocus, suggestionsAfterRun = false, className, }) {
|
|
48
|
+
export function AgentChat({ title, hideHeader = false, headerActions, onClose, expanded, onExpandedChange, hideNewConversation = false, renderContent, renderToolCall, toolTitle, renderers, progress = "line", empty, attachments, composerActions, modelPicker, sendOptions, placeholder, autoFocus, suggestionsAfterRun = false, className, }) {
|
|
47
49
|
const agent = useCalliopeAgent();
|
|
48
50
|
const { messages } = useUI();
|
|
49
51
|
const product = useProduct();
|
|
@@ -64,13 +66,52 @@ export function AgentChat({ title, hideHeader = false, headerActions, onClose, e
|
|
|
64
66
|
const custom = renderToolCall?.(call);
|
|
65
67
|
if (custom !== undefined)
|
|
66
68
|
return custom;
|
|
69
|
+
const Renderer = renderers?.tools?.[call.name];
|
|
70
|
+
if (Renderer) {
|
|
71
|
+
return (_jsx(Renderer, { call: call, args: parseToolCallArgs(call.args), result: parseResult(call), agent: agent, ...(waiting.includes(call.id)
|
|
72
|
+
? { respond: (result) => agent.respond(call.id, result) }
|
|
73
|
+
: {}) }));
|
|
74
|
+
}
|
|
67
75
|
const tool = tools.find((t) => t.name === call.name && t.render);
|
|
68
76
|
if (!tool)
|
|
69
77
|
return undefined;
|
|
70
78
|
return (_jsx(ToolRender, { tool: tool, call: call, waiting: waiting.includes(call.id), respond: agent.respond }));
|
|
71
79
|
};
|
|
80
|
+
// Custom events, each after the timeline item it arrived after (the first, when before any).
|
|
81
|
+
const first = view.timeline[0];
|
|
82
|
+
const firstKey = first ? `${first.kind}:${first.id}` : null;
|
|
83
|
+
const drawAfter = (item) => {
|
|
84
|
+
const key = `${item.kind}:${item.id}`;
|
|
85
|
+
const here = agent.custom.filter((c) => renderers?.custom?.[c.name] && (c.after ?? firstKey) === key);
|
|
86
|
+
if (here.length === 0)
|
|
87
|
+
return null;
|
|
88
|
+
return here.map((c) => {
|
|
89
|
+
const Renderer = renderers.custom[c.name];
|
|
90
|
+
return (_jsx("div", { "data-slot": "agent-custom", className: "min-w-0", children: _jsx(Renderer, { name: c.name, value: c.value, history: c.history, agent: agent }) }, c.key));
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
// Interrupts that no tool card or artifact answers: drawn after the conversation.
|
|
94
|
+
const claimed = new Set(agent.custom.flatMap((c) => {
|
|
95
|
+
const id = c.value?.interruptId;
|
|
96
|
+
return renderers?.custom?.[c.name] && typeof id === "string" ? [id] : [];
|
|
97
|
+
}));
|
|
98
|
+
const loose = view.status === "interrupted"
|
|
99
|
+
? view.interrupts.filter((i) => !i.toolCallId && !claimed.has(i.id))
|
|
100
|
+
: [];
|
|
101
|
+
const footer = loose.length > 0
|
|
102
|
+
? loose.map((interrupt) => {
|
|
103
|
+
const Renderer = renderers?.interrupts?.[interrupt.reason] ?? InterruptCard;
|
|
104
|
+
return (_jsx(Renderer, { interrupt: interrupt, agent: agent, resolve: (payload) => agent.resolveInterrupt(interrupt.id, {
|
|
105
|
+
status: "resolved",
|
|
106
|
+
...(payload === undefined ? {} : { payload }),
|
|
107
|
+
}), cancel: () => agent.resolveInterrupt(interrupt.id, { status: "cancelled" }) }, interrupt.id));
|
|
108
|
+
})
|
|
109
|
+
: undefined;
|
|
110
|
+
const StateView = renderers?.state;
|
|
72
111
|
const step = [...view.steps].reverse().find((s) => s.status === "running");
|
|
73
|
-
return (_jsxs("section", { ref: root, "aria-labelledby": hideHeader ? undefined : headingId, "aria-label": hideHeader ? name : undefined, "data-slot": "agent-chat", className: cn("flex min-h-0 min-w-0 flex-col bg-card text-card-foreground", className), children: [!hideHeader && (_jsxs("div", { className: "flex min-w-0 items-center gap-1 border-b px-3 py-2", children: [_jsx("h2", { id: headingId, className: "min-w-0 flex-1 truncate text-sm font-semibold", children: name }), headerActions, !hideNewConversation && _jsx(NewConversationButton, {}), onExpandedChange && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": expanded ? t.restore : t.expand, title: expanded ? t.restore : t.expand, onClick: () => onExpandedChange(!expanded), children: expanded ? _jsx(Minimize2Icon, { "aria-hidden": true }) : _jsx(Maximize2Icon, { "aria-hidden": true }) })), onClose && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": t.close, title: t.close, onClick: onClose, children: _jsx(XIcon, { "aria-hidden": true }) }))] })), progress === "steps" && view.steps.length > 0 && (_jsx(AgentProgress, { view: view, className: "border-b px-3 py-2" })),
|
|
112
|
+
return (_jsxs("section", { ref: root, "aria-labelledby": hideHeader ? undefined : headingId, "aria-label": hideHeader ? name : undefined, "data-slot": "agent-chat", className: cn("flex min-h-0 min-w-0 flex-col bg-card text-card-foreground", className), children: [!hideHeader && (_jsxs("div", { className: "flex min-w-0 items-center gap-1 border-b px-3 py-2", children: [_jsx("h2", { id: headingId, className: "min-w-0 flex-1 truncate text-sm font-semibold", children: name }), headerActions, !hideNewConversation && _jsx(NewConversationButton, {}), onExpandedChange && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": expanded ? t.restore : t.expand, title: expanded ? t.restore : t.expand, onClick: () => onExpandedChange(!expanded), children: expanded ? _jsx(Minimize2Icon, { "aria-hidden": true }) : _jsx(Maximize2Icon, { "aria-hidden": true }) })), onClose && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": t.close, title: t.close, onClick: onClose, children: _jsx(XIcon, { "aria-hidden": true }) }))] })), progress === "steps" && view.steps.length > 0 && (_jsx(AgentProgress, { view: view, className: "border-b px-3 py-2" })), StateView && view.state !== undefined && (_jsx("div", { role: "region", "aria-label": t.sharedState, "data-slot": "agent-state",
|
|
113
|
+
// A scrolling region is reached by keyboard (WCAG 2.1.1).
|
|
114
|
+
tabIndex: 0, className: "max-h-48 min-w-0 shrink-0 overflow-y-auto border-b px-3 py-2", children: _jsx(StateView, { state: view.state, setState: agent.setState, agent: agent }) })), _jsx(MessageList, { view: view, renderContent: renderContent, renderToolCall: drawCall, renderAfter: drawAfter, footer: footer, toolTitle: toolTitle, onApprove: (call) => agent.approve(call), onDeny: (call, reason) => agent.deny(call, reason), onRetry: () => void agent.regenerate(), empty: _jsxs("div", { className: "flex flex-col items-center gap-4 py-6", children: [empty ?? _jsx(EmptyState, { title: t.emptyTitle, description: t.emptyDescription }), _jsx(Suggestions, { suggestions: suggestions, disabled: running, onPick: pick })] }), className: "min-h-0 flex-1" }), _jsxs("div", { className: "flex min-w-0 flex-col gap-2 border-t p-3", children: [suggestionsAfterRun && !fresh && !running && (_jsx(Suggestions, { suggestions: suggestions, disabled: running, onPick: pick })), _jsx("p", { role: "status", "data-slot": "agent-status", className: cn("flex min-w-0 items-center gap-2 text-xs text-muted-foreground", !(progress === "line" && running) && !blocked && "sr-only"), children: blocked ? (t.waitingOnYou) : running ? (_jsxs(_Fragment, { children: [_jsx(StatusDot, { tone: "info", pulse: true }), _jsx("span", { className: "truncate", children: step ? step.name : messages.chat.runStatus("running") })] })) : null }), _jsx(Composer, { onSubmit: send, running: running, onStop: agent.stop, disabled: blocked, placeholder: placeholder,
|
|
74
115
|
// eslint-disable-next-line jsx-a11y/no-autofocus -- the person just opened the assistant to type
|
|
75
116
|
autoFocus: autoFocus, attachments: attachments, actions: modelPicker || composerActions ? (_jsxs(_Fragment, { children: [modelPicker && _jsx(ModelPicker, { ...modelPicker }), composerActions] })) : undefined })] })] }));
|
|
76
117
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type DocumentReviewProps } from "../workflow/DocumentReview.js";
|
|
3
|
+
import { type FormReviewProps } from "../workflow/FormReview.js";
|
|
4
|
+
import { type MediaReviewProps } from "../workflow/MediaReview.js";
|
|
5
|
+
import type { WorkflowQuestion } from "../workflow/question.js";
|
|
6
|
+
import type { Artifact, RunStage } from "../workflow/run.js";
|
|
7
|
+
import type { AgentRenderers, CustomRendererProps, InterruptRendererProps } from "./renderers.js";
|
|
8
|
+
/** `calliope.run`: a run's stages as the agent reports them. Send it again with the same `id` to update. */
|
|
9
|
+
export interface RunProgressValue {
|
|
10
|
+
id?: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
stages: readonly RunStage[];
|
|
13
|
+
}
|
|
14
|
+
/** The review viewer an artifact opens in. */
|
|
15
|
+
export type ArtifactReviewValue = ({
|
|
16
|
+
viewer: "document";
|
|
17
|
+
} & Pick<DocumentReviewProps, "title" | "pages" | "annotations" | "signOffs" | "signOffRole">) | ({
|
|
18
|
+
viewer: "media";
|
|
19
|
+
} & Pick<MediaReviewProps, "kind" | "src" | "title" | "poster" | "captions" | "duration" | "comments">) | ({
|
|
20
|
+
viewer: "form";
|
|
21
|
+
} & Pick<FormReviewProps, "title" | "pages" | "fields" | "threshold">);
|
|
22
|
+
/**
|
|
23
|
+
* `calliope.artifact`: an output for the person to look at, and to review
|
|
24
|
+
* when the run waits on it. `interruptId` names the run's interrupt the
|
|
25
|
+
* review answers: signing a document resolves it with `{ action: "sign-off" }`,
|
|
26
|
+
* sending a form's review with `{ action: "form-review", fields }`.
|
|
27
|
+
*/
|
|
28
|
+
export interface ArtifactValue {
|
|
29
|
+
id: string;
|
|
30
|
+
artifact: Artifact;
|
|
31
|
+
review?: ArtifactReviewValue;
|
|
32
|
+
interruptId?: string;
|
|
33
|
+
}
|
|
34
|
+
/** `calliope.question` interrupts carry a question in `metadata.question`. */
|
|
35
|
+
export interface QuestionInterruptMetadata {
|
|
36
|
+
question: WorkflowQuestion;
|
|
37
|
+
}
|
|
38
|
+
/** The run timeline for `calliope.run` events: progress drawn in the conversation. */
|
|
39
|
+
export declare function RunProgressCard({ value }: CustomRendererProps<RunProgressValue>): React.JSX.Element;
|
|
40
|
+
/**
|
|
41
|
+
* An artifact the agent produced, with its review viewer (document, media or
|
|
42
|
+
* form) as the preview. While the run waits on its review, signing off or
|
|
43
|
+
* sending the form's review answers the interrupt and resumes the run.
|
|
44
|
+
*/
|
|
45
|
+
export declare function ArtifactReviewCard({ value, agent }: CustomRendererProps<ArtifactValue>): React.JSX.Element;
|
|
46
|
+
/** A `calliope.question` interrupt: the runtime question card, its answers resuming the run. */
|
|
47
|
+
export declare function QuestionInterruptCard({ interrupt, resolve }: InterruptRendererProps): React.JSX.Element;
|
|
48
|
+
/**
|
|
49
|
+
* Any other open interrupt not tied to a tool call: the agent's question in
|
|
50
|
+
* its words, with approve and decline (a decline always with a reason).
|
|
51
|
+
*/
|
|
52
|
+
export declare function InterruptCard({ interrupt, resolve, className, }: InterruptRendererProps & {
|
|
53
|
+
className?: string;
|
|
54
|
+
}): React.JSX.Element;
|
|
55
|
+
/**
|
|
56
|
+
* The workflow run and review components as generative UI: `calliope.run`
|
|
57
|
+
* custom events draw the run timeline, `calliope.artifact` events draw the
|
|
58
|
+
* artifact with its document, media or form review, and `calliope.question`
|
|
59
|
+
* interrupts draw the runtime question card. Merge with the host's own.
|
|
60
|
+
*/
|
|
61
|
+
export declare const workflowRenderers: AgentRenderers;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { Button } from "../primitives/Button.js";
|
|
6
|
+
import { useUI } from "../provider/UIProvider.js";
|
|
7
|
+
import { ArtifactCard } from "../workflow/ArtifactCard.js";
|
|
8
|
+
import { DocumentReview } from "../workflow/DocumentReview.js";
|
|
9
|
+
import { FormReview } from "../workflow/FormReview.js";
|
|
10
|
+
import { MediaReview } from "../workflow/MediaReview.js";
|
|
11
|
+
import { RunTimeline } from "../workflow/RunTimeline.js";
|
|
12
|
+
import { WorkflowQuestionCard } from "../workflow/WorkflowQuestionCard.js";
|
|
13
|
+
/** The run timeline for `calliope.run` events: progress drawn in the conversation. */
|
|
14
|
+
export function RunProgressCard({ value }) {
|
|
15
|
+
return (_jsx("div", { className: "min-w-0 rounded-lg border bg-card p-3", children: _jsx(RunTimeline, { stages: value.stages, label: value.label }) }));
|
|
16
|
+
}
|
|
17
|
+
function openInterrupt(agent, id) {
|
|
18
|
+
if (!id || agent.view.status !== "interrupted")
|
|
19
|
+
return undefined;
|
|
20
|
+
return agent.view.interrupts.find((i) => i.id === id);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* An artifact the agent produced, with its review viewer (document, media or
|
|
24
|
+
* form) as the preview. While the run waits on its review, signing off or
|
|
25
|
+
* sending the form's review answers the interrupt and resumes the run.
|
|
26
|
+
*/
|
|
27
|
+
export function ArtifactReviewCard({ value, agent }) {
|
|
28
|
+
const { messages } = useUI();
|
|
29
|
+
const waiting = openInterrupt(agent, value.interruptId);
|
|
30
|
+
const [fields, setFields] = React.useState({});
|
|
31
|
+
const resolve = (payload) => waiting && agent.resolveInterrupt(waiting.id, { status: "resolved", payload });
|
|
32
|
+
const review = value.review;
|
|
33
|
+
let preview = null;
|
|
34
|
+
if (review?.viewer === "document") {
|
|
35
|
+
const { viewer: _, ...props } = review;
|
|
36
|
+
preview = (_jsx(DocumentReview, { ...props, ...(waiting
|
|
37
|
+
? { onSignOff: () => resolve({ action: "sign-off", artifactId: value.id }) }
|
|
38
|
+
: {}) }));
|
|
39
|
+
}
|
|
40
|
+
else if (review?.viewer === "media") {
|
|
41
|
+
const { viewer: _, ...props } = review;
|
|
42
|
+
preview = _jsx(MediaReview, { ...props });
|
|
43
|
+
}
|
|
44
|
+
else if (review?.viewer === "form") {
|
|
45
|
+
const { viewer: _, ...props } = review;
|
|
46
|
+
const current = props.fields.map((f) => fields[f.id] ?? f);
|
|
47
|
+
preview = (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsx(FormReview, { ...props, fields: current, ...(waiting
|
|
48
|
+
? {
|
|
49
|
+
onAccept: (field) => setFields((s) => ({ ...s, [field.id]: { ...field, status: "accepted" } })),
|
|
50
|
+
onCorrect: (field, corrected) => setFields((s) => ({
|
|
51
|
+
...s,
|
|
52
|
+
[field.id]: { ...field, status: "corrected", corrected },
|
|
53
|
+
})),
|
|
54
|
+
}
|
|
55
|
+
: {}) }), waiting && (_jsx(Button, { size: "sm", className: "self-end", onClick: () => resolve({
|
|
56
|
+
action: "form-review",
|
|
57
|
+
artifactId: value.id,
|
|
58
|
+
fields: current.map(({ id, status, corrected }) => ({ id, status, corrected })),
|
|
59
|
+
}), children: messages.agent.sendReview }))] }));
|
|
60
|
+
}
|
|
61
|
+
return _jsx(ArtifactCard, { artifact: value.artifact, preview: preview, headingLevel: "h3" });
|
|
62
|
+
}
|
|
63
|
+
/** A `calliope.question` interrupt: the runtime question card, its answers resuming the run. */
|
|
64
|
+
export function QuestionInterruptCard({ interrupt, resolve }) {
|
|
65
|
+
const question = interrupt.metadata?.question;
|
|
66
|
+
const [state, setState] = React.useState("waiting");
|
|
67
|
+
return (_jsx(WorkflowQuestionCard, { question: question ?? null, state: state, onSubmit: (submission) => {
|
|
68
|
+
setState("submitting");
|
|
69
|
+
resolve({ answers: submission.answers, revision: submission.revision });
|
|
70
|
+
} }));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Any other open interrupt not tied to a tool call: the agent's question in
|
|
74
|
+
* its words, with approve and decline (a decline always with a reason).
|
|
75
|
+
*/
|
|
76
|
+
export function InterruptCard({ interrupt, resolve, className, }) {
|
|
77
|
+
const { messages } = useUI();
|
|
78
|
+
const t = messages.agent;
|
|
79
|
+
const [declining, setDeclining] = React.useState(false);
|
|
80
|
+
const [reason, setReason] = React.useState("");
|
|
81
|
+
const reasonId = React.useId();
|
|
82
|
+
return (_jsxs("section", { "aria-label": t.interrupt, "data-slot": "agent-interrupt", className: cn("flex min-w-0 flex-col gap-2 rounded-lg border border-warning-border bg-warning-bg p-3 text-sm text-warning-fg", className), children: [_jsx("p", { className: "[overflow-wrap:anywhere]", children: interrupt.message ?? messages.chat.approvalNeeded }), declining ? (_jsxs("form", { className: "flex min-w-0 flex-col gap-2", onSubmit: (event) => {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
if (reason.trim())
|
|
85
|
+
resolve({ approved: false, reason: reason.trim() });
|
|
86
|
+
}, children: [_jsx("label", { htmlFor: reasonId, className: "text-xs font-medium", children: messages.chat.denyReason }), _jsx("textarea", { id: reasonId, value: reason, onChange: (event) => setReason(event.target.value), rows: 2, className: "min-w-0 rounded-md border border-input bg-background p-2 text-sm text-foreground" }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setDeclining(false), children: t.back }), _jsx(Button, { type: "submit", variant: "destructive", size: "sm", disabled: !reason.trim(), children: t.decline })] })] })) : (_jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setDeclining(true), children: t.decline }), _jsx(Button, { size: "sm", onClick: () => resolve({ approved: true }), children: messages.chat.approve })] }))] }));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The workflow run and review components as generative UI: `calliope.run`
|
|
90
|
+
* custom events draw the run timeline, `calliope.artifact` events draw the
|
|
91
|
+
* artifact with its document, media or form review, and `calliope.question`
|
|
92
|
+
* interrupts draw the runtime question card. Merge with the host's own.
|
|
93
|
+
*/
|
|
94
|
+
export const workflowRenderers = {
|
|
95
|
+
custom: {
|
|
96
|
+
"calliope.run": RunProgressCard,
|
|
97
|
+
"calliope.artifact": ArtifactReviewCard,
|
|
98
|
+
},
|
|
99
|
+
interrupts: {
|
|
100
|
+
"calliope.question": QuestionInterruptCard,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -38,6 +38,20 @@ export interface AgentControllerOptions {
|
|
|
38
38
|
/** Every AG-UI event, for the host's telemetry. */
|
|
39
39
|
onEvent?: (event: AgUiEvent) => void;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A custom event as the conversation draws it: events whose value carries
|
|
43
|
+
* the same `id` are one item (progress updating in place), placed where the
|
|
44
|
+
* first of them arrived.
|
|
45
|
+
*/
|
|
46
|
+
export interface AgentCustomItem {
|
|
47
|
+
key: string;
|
|
48
|
+
name: string;
|
|
49
|
+
/** The latest value. */
|
|
50
|
+
value: unknown;
|
|
51
|
+
history: readonly unknown[];
|
|
52
|
+
/** The timeline item it arrived after ("message:<id>", "tool-call:<id>"), or null before any. */
|
|
53
|
+
after: string | null;
|
|
54
|
+
}
|
|
41
55
|
export interface AgentSnapshot {
|
|
42
56
|
view: ChatView;
|
|
43
57
|
/** True from send until the agent is done, follow-ups and browser tools included. */
|
|
@@ -49,6 +63,8 @@ export interface AgentSnapshot {
|
|
|
49
63
|
unread: number;
|
|
50
64
|
suggestions: readonly AgentSuggestion[];
|
|
51
65
|
tools: readonly AgentTool[];
|
|
66
|
+
/** The conversation's custom events, grouped and placed (see `AgentCustomItem`). */
|
|
67
|
+
custom: readonly AgentCustomItem[];
|
|
52
68
|
}
|
|
53
69
|
export interface SendOptions {
|
|
54
70
|
/** Attachments and other parts after the text (AG-UI input content). */
|
|
@@ -76,6 +92,8 @@ export declare class AgentController {
|
|
|
76
92
|
private visible;
|
|
77
93
|
private read;
|
|
78
94
|
private snapshot;
|
|
95
|
+
/** Where each custom event arrived, by its index in the view. */
|
|
96
|
+
private anchors;
|
|
79
97
|
private disposed;
|
|
80
98
|
private connected;
|
|
81
99
|
private threadSeq;
|
|
@@ -97,6 +115,7 @@ export declare class AgentController {
|
|
|
97
115
|
private emit;
|
|
98
116
|
private finishedAnswers;
|
|
99
117
|
private build;
|
|
118
|
+
private customItems;
|
|
100
119
|
private bind;
|
|
101
120
|
/** The view the agent's own messages and state make, for a fresh start or a thread switch. */
|
|
102
121
|
private fromAgent;
|
package/dist/agent/controller.js
CHANGED
|
@@ -43,6 +43,8 @@ export class AgentController {
|
|
|
43
43
|
visible = true;
|
|
44
44
|
read = 0;
|
|
45
45
|
snapshot;
|
|
46
|
+
/** Where each custom event arrived, by its index in the view. */
|
|
47
|
+
anchors = [];
|
|
46
48
|
disposed = false;
|
|
47
49
|
connected = false;
|
|
48
50
|
threadSeq = 0;
|
|
@@ -140,8 +142,32 @@ export class AgentController {
|
|
|
140
142
|
unread: Math.max(0, this.finishedAnswers() - this.read),
|
|
141
143
|
suggestions,
|
|
142
144
|
tools: [...this.tools.values()].sort((a, b) => a.order - b.order).map((t) => t.tool),
|
|
145
|
+
custom: this.customItems(),
|
|
143
146
|
};
|
|
144
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
|
+
}
|
|
145
171
|
bind() {
|
|
146
172
|
const agent = this.engine.agent;
|
|
147
173
|
this.unsubscribe();
|
|
@@ -160,6 +186,7 @@ export class AgentController {
|
|
|
160
186
|
/** The view the agent's own messages and state make, for a fresh start or a thread switch. */
|
|
161
187
|
fromAgent(threadId) {
|
|
162
188
|
const agent = this.engine.agent;
|
|
189
|
+
this.anchors = [];
|
|
163
190
|
let view = { ...createChatView(), threadId };
|
|
164
191
|
if (agent.messages.length > 0) {
|
|
165
192
|
view = reduceChat(view, {
|
|
@@ -180,7 +207,12 @@ export class AgentController {
|
|
|
180
207
|
if (this.halted && event.type !== EventType.RUN_STARTED)
|
|
181
208
|
return;
|
|
182
209
|
this.halted = false;
|
|
210
|
+
const before = this.view.custom.length;
|
|
183
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
|
+
}
|
|
184
216
|
this.emit();
|
|
185
217
|
}
|
|
186
218
|
/** Tool results the agent's messages gained outside the event stream: the browser's own tools. */
|
|
@@ -435,6 +467,7 @@ export class AgentController {
|
|
|
435
467
|
this.agent.setMessages([]);
|
|
436
468
|
this.agent.setState({});
|
|
437
469
|
this.failed.clear();
|
|
470
|
+
this.anchors = [];
|
|
438
471
|
this.view = { ...createChatView(), threadId: id };
|
|
439
472
|
this.emit();
|
|
440
473
|
return id;
|
|
@@ -38,13 +38,13 @@ export function createCopilotKitEngine(copilotkit, agent, invoke, agentId) {
|
|
|
38
38
|
},
|
|
39
39
|
setTools(next) {
|
|
40
40
|
clearTools();
|
|
41
|
-
|
|
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)) {
|
|
42
43
|
copilotkit.addTool({
|
|
43
44
|
name: tool.name,
|
|
44
45
|
...(tool.description !== undefined ? { description: tool.description } : {}),
|
|
45
46
|
parameters: toolStandardSchema(tool),
|
|
46
47
|
...(tool.followUp !== undefined ? { followUp: tool.followUp } : {}),
|
|
47
|
-
...(tool.available !== undefined ? { available: tool.available } : {}),
|
|
48
48
|
...(agentId ? { agentId } : {}),
|
|
49
49
|
handler: (args, ctx) => invoke(tool, args, {
|
|
50
50
|
toolCallId: ctx.toolCall.id,
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* `@ag-ui/client`, which the package's main entry never loads. For a
|
|
5
5
|
* CopilotKit runtime, see `@calliopelabs/ui/agent/copilotkit`.
|
|
6
6
|
*/
|
|
7
|
-
export { AgentController, agentId, type AgentContextEntry, type AgentControllerOptions, type AgentSnapshot, type AgentSuggestion, type AgentThread, type SendOptions, } from "./controller.js";
|
|
7
|
+
export { AgentController, agentId, type AgentContextEntry, type AgentCustomItem, type AgentControllerOptions, type AgentSnapshot, type AgentSuggestion, type AgentThread, type SendOptions, } from "./controller.js";
|
|
8
8
|
export { createDirectEngine, type AgentEngine, type EngineRunInput, type ToolInvoker, } from "./engine.js";
|
|
9
9
|
export { defineTool, isStandardSchema, toolJsonSchema, toolResultText, toolStandardSchema, validateToolArgs, type AgentTool, type AgentToolContext, type AgentToolRenderProps, type JsonSchema, type StandardSchemaV1, } from "./tool.js";
|
|
10
10
|
export { CalliopeAgentProvider, useAgentController, useAgentReadable, useAgentSuggestions, useAgentTool, useCalliopeAgent, type CalliopeAgentApi, type CalliopeAgentOptions, type CalliopeAgentProviderProps, } from "./AgentProvider.js";
|
|
11
|
-
export { createMockAgent, textEvents, toolCallEvents, type MockAgentOptions, type MockTurn, } from "./mock.js";
|
|
11
|
+
export { createFunctionAgent, createMockAgent, type AgentRunFunction, type FunctionAgentOptions, textEvents, toolCallEvents, type MockAgentOptions, type MockTurn, } from "./mock.js";
|
|
12
12
|
export { AgentChat, NewConversationButton, type AgentChatProps } from "./AgentChat.js";
|
|
13
13
|
export { AgentLauncher, type AgentLauncherProps } from "./AgentLauncher.js";
|
|
14
14
|
export { CalliopeAgent, type CalliopeAgentMode, type CalliopeAgentProps } from "./CalliopeAgent.js";
|
|
15
15
|
export { shortcutLabel, useMediaQuery, useOpenState, useShortcut } from "./surface.js";
|
|
16
|
+
export { mergeRenderers, parseResult, type AgentRenderers, type CustomRendererProps, type InterruptRendererProps, type StateRendererProps, type ToolRendererProps, } from "./renderers.js";
|
|
17
|
+
export { ArtifactReviewCard, InterruptCard, QuestionInterruptCard, RunProgressCard, workflowRenderers, type ArtifactReviewValue, type ArtifactValue, type QuestionInterruptMetadata, type RunProgressValue, } from "./WorkflowRenderers.js";
|
package/dist/agent/index.js
CHANGED
|
@@ -8,8 +8,10 @@ export { AgentController, agentId, } from "./controller.js";
|
|
|
8
8
|
export { createDirectEngine, } from "./engine.js";
|
|
9
9
|
export { defineTool, isStandardSchema, toolJsonSchema, toolResultText, toolStandardSchema, validateToolArgs, } from "./tool.js";
|
|
10
10
|
export { CalliopeAgentProvider, useAgentController, useAgentReadable, useAgentSuggestions, useAgentTool, useCalliopeAgent, } from "./AgentProvider.js";
|
|
11
|
-
export { createMockAgent, textEvents, toolCallEvents, } from "./mock.js";
|
|
11
|
+
export { createFunctionAgent, createMockAgent, textEvents, toolCallEvents, } from "./mock.js";
|
|
12
12
|
export { AgentChat, NewConversationButton } from "./AgentChat.js";
|
|
13
13
|
export { AgentLauncher } from "./AgentLauncher.js";
|
|
14
14
|
export { CalliopeAgent } from "./CalliopeAgent.js";
|
|
15
15
|
export { shortcutLabel, useMediaQuery, useOpenState, useShortcut } from "./surface.js";
|
|
16
|
+
export { mergeRenderers, parseResult, } from "./renderers.js";
|
|
17
|
+
export { ArtifactReviewCard, InterruptCard, QuestionInterruptCard, RunProgressCard, workflowRenderers, } from "./WorkflowRenderers.js";
|