@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,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An engine runs an AG-UI agent for the controller: it starts runs, executes
|
|
3
|
+
* the browser's tools and decides on follow-up runs. Two ship: the direct
|
|
4
|
+
* engine here, over any `@ag-ui/client` agent (an `HttpAgent` on a plain
|
|
5
|
+
* AG-UI endpoint), and the CopilotKit engine (`@calliopelabs/ui/agent/copilotkit`),
|
|
6
|
+
* which hands the same work to CopilotKit's core. The controller reads the
|
|
7
|
+
* agent's events either way, so the conversation looks the same.
|
|
8
|
+
*/
|
|
9
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
10
|
+
import type { Context, ResumeEntry } from "@ag-ui/core";
|
|
11
|
+
import { type AgentTool } from "./tool.js";
|
|
12
|
+
/** Runs a tool call for an engine, with validation and human-in-the-loop waiting. */
|
|
13
|
+
export type ToolInvoker = (tool: AgentTool, args: unknown, call: {
|
|
14
|
+
toolCallId: string;
|
|
15
|
+
signal: AbortSignal;
|
|
16
|
+
}) => Promise<unknown>;
|
|
17
|
+
export interface EngineRunInput {
|
|
18
|
+
forwardedProps?: Record<string, unknown>;
|
|
19
|
+
/** Answers to the previous run's interrupts. */
|
|
20
|
+
resume?: ResumeEntry[];
|
|
21
|
+
}
|
|
22
|
+
export interface AgentEngine {
|
|
23
|
+
readonly agent: AbstractAgent;
|
|
24
|
+
/** Runs until the agent is done: frontend tool calls answered, follow-ups made. */
|
|
25
|
+
run(input: EngineRunInput): Promise<void>;
|
|
26
|
+
/** Stops the run in flight and any tool it is waiting on. */
|
|
27
|
+
stop(): void;
|
|
28
|
+
/** The tools the agent may call now; the controller calls this whenever they change. */
|
|
29
|
+
setTools(tools: readonly AgentTool[]): void;
|
|
30
|
+
/** The context (readables) the next run carries. */
|
|
31
|
+
setContext(context: readonly Context[]): void;
|
|
32
|
+
/** Lets go of everything the engine registered. */
|
|
33
|
+
dispose(): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The engine for a plain AG-UI agent. Each run sends the tools and context;
|
|
37
|
+
* when the agent calls a browser tool, the engine runs it, adds the result to
|
|
38
|
+
* the agent's messages and runs again (unless the tool says `followUp: false`).
|
|
39
|
+
* A run that ends on an interrupt stops there: the person's answer resumes it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createDirectEngine(agent: AbstractAgent, invoke: ToolInvoker): AgentEngine;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { toolJsonSchema, toolResultText } from "./tool.js";
|
|
2
|
+
/** Runs past this many follow-ups in one turn are a loop, not a conversation. */
|
|
3
|
+
const MAX_FOLLOW_UPS = 8;
|
|
4
|
+
/**
|
|
5
|
+
* The engine for a plain AG-UI agent. Each run sends the tools and context;
|
|
6
|
+
* when the agent calls a browser tool, the engine runs it, adds the result to
|
|
7
|
+
* the agent's messages and runs again (unless the tool says `followUp: false`).
|
|
8
|
+
* A run that ends on an interrupt stops there: the person's answer resumes it.
|
|
9
|
+
*/
|
|
10
|
+
export function createDirectEngine(agent, invoke) {
|
|
11
|
+
let tools = [];
|
|
12
|
+
let context = [];
|
|
13
|
+
let aborter = null;
|
|
14
|
+
const answered = (toolCallId) => agent.messages.some((m) => m.role === "tool" && m.toolCallId === toolCallId);
|
|
15
|
+
return {
|
|
16
|
+
agent,
|
|
17
|
+
async run({ forwardedProps, resume }) {
|
|
18
|
+
aborter?.abort();
|
|
19
|
+
const own = new AbortController();
|
|
20
|
+
aborter = own;
|
|
21
|
+
let params = { resume };
|
|
22
|
+
try {
|
|
23
|
+
for (let turn = 0; turn <= MAX_FOLLOW_UPS; turn++) {
|
|
24
|
+
const offered = tools.filter((t) => t.available !== false);
|
|
25
|
+
const { newMessages } = await agent.runAgent({
|
|
26
|
+
...params,
|
|
27
|
+
tools: offered.map((t) => ({
|
|
28
|
+
name: t.name,
|
|
29
|
+
description: t.description ?? "",
|
|
30
|
+
parameters: toolJsonSchema(t),
|
|
31
|
+
})),
|
|
32
|
+
context: [...context],
|
|
33
|
+
...(forwardedProps ? { forwardedProps } : {}),
|
|
34
|
+
});
|
|
35
|
+
if (own.signal.aborted || agent.pendingInterrupts.length > 0)
|
|
36
|
+
return;
|
|
37
|
+
const calls = newMessages.flatMap((m) => m.role === "assistant" ? (m.toolCalls ?? []) : []);
|
|
38
|
+
let again = false;
|
|
39
|
+
for (const call of calls) {
|
|
40
|
+
const tool = offered.find((t) => t.name === call.function.name);
|
|
41
|
+
if (!tool || answered(call.id))
|
|
42
|
+
continue;
|
|
43
|
+
let content;
|
|
44
|
+
let error;
|
|
45
|
+
try {
|
|
46
|
+
const args = call.function.arguments ? JSON.parse(call.function.arguments) : {};
|
|
47
|
+
content = toolResultText(await invoke(tool, args, { toolCallId: call.id, signal: own.signal }));
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
if (own.signal.aborted)
|
|
51
|
+
return;
|
|
52
|
+
error = e instanceof Error ? e.message : String(e);
|
|
53
|
+
content = `Error: ${error}`;
|
|
54
|
+
}
|
|
55
|
+
if (own.signal.aborted)
|
|
56
|
+
return;
|
|
57
|
+
agent.addMessage({
|
|
58
|
+
id: `${call.id}:result`,
|
|
59
|
+
role: "tool",
|
|
60
|
+
toolCallId: call.id,
|
|
61
|
+
content,
|
|
62
|
+
...(error ? { error } : {}),
|
|
63
|
+
});
|
|
64
|
+
if (tool.followUp !== false)
|
|
65
|
+
again = true;
|
|
66
|
+
}
|
|
67
|
+
if (!again)
|
|
68
|
+
return;
|
|
69
|
+
params = {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
if (aborter === own)
|
|
74
|
+
aborter = null;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
stop() {
|
|
78
|
+
aborter?.abort();
|
|
79
|
+
aborter = null;
|
|
80
|
+
if (agent.isRunning)
|
|
81
|
+
agent.abortRun();
|
|
82
|
+
},
|
|
83
|
+
setTools(next) {
|
|
84
|
+
tools = next;
|
|
85
|
+
},
|
|
86
|
+
setContext(next) {
|
|
87
|
+
context = next;
|
|
88
|
+
},
|
|
89
|
+
dispose() {
|
|
90
|
+
this.stop();
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@calliopelabs/ui/agent`: one embedded agent for every Calliope AI product,
|
|
3
|
+
* over the AG-UI protocol, drawn with the chat kit. Opt-in: it needs
|
|
4
|
+
* `@ag-ui/client`, which the package's main entry never loads. For a
|
|
5
|
+
* CopilotKit runtime, see `@calliopelabs/ui/agent/copilotkit`.
|
|
6
|
+
*/
|
|
7
|
+
export { AgentController, agentId, type AgentContextEntry, type AgentCustomItem, type AgentControllerOptions, type AgentSnapshot, type AgentSuggestion, type AgentThread, type SendOptions, } from "./controller.js";
|
|
8
|
+
export { createDirectEngine, type AgentEngine, type EngineRunInput, type ToolInvoker, } from "./engine.js";
|
|
9
|
+
export { defineTool, isStandardSchema, toolJsonSchema, toolResultText, toolStandardSchema, validateToolArgs, type AgentTool, type AgentToolContext, type AgentToolRenderProps, type JsonSchema, type StandardSchemaV1, } from "./tool.js";
|
|
10
|
+
export { CalliopeAgentProvider, useAgentController, useAgentReadable, useAgentSuggestions, useAgentTool, useCalliopeAgent, type CalliopeAgentApi, type CalliopeAgentOptions, type CalliopeAgentProviderProps, } from "./AgentProvider.js";
|
|
11
|
+
export { createFunctionAgent, createMockAgent, type AgentRunFunction, type FunctionAgentOptions, textEvents, toolCallEvents, type MockAgentOptions, type MockTurn, } from "./mock.js";
|
|
12
|
+
export { AgentChat, NewConversationButton, type AgentChatProps } from "./AgentChat.js";
|
|
13
|
+
export { AgentLauncher, type AgentLauncherProps } from "./AgentLauncher.js";
|
|
14
|
+
export { CalliopeAgent, type CalliopeAgentMode, type CalliopeAgentProps } from "./CalliopeAgent.js";
|
|
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";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@calliopelabs/ui/agent`: one embedded agent for every Calliope AI product,
|
|
3
|
+
* over the AG-UI protocol, drawn with the chat kit. Opt-in: it needs
|
|
4
|
+
* `@ag-ui/client`, which the package's main entry never loads. For a
|
|
5
|
+
* CopilotKit runtime, see `@calliopelabs/ui/agent/copilotkit`.
|
|
6
|
+
*/
|
|
7
|
+
export { AgentController, agentId, } from "./controller.js";
|
|
8
|
+
export { createDirectEngine, } from "./engine.js";
|
|
9
|
+
export { defineTool, isStandardSchema, toolJsonSchema, toolResultText, toolStandardSchema, validateToolArgs, } from "./tool.js";
|
|
10
|
+
export { CalliopeAgentProvider, useAgentController, useAgentReadable, useAgentSuggestions, useAgentTool, useCalliopeAgent, } from "./AgentProvider.js";
|
|
11
|
+
export { createFunctionAgent, createMockAgent, textEvents, toolCallEvents, } from "./mock.js";
|
|
12
|
+
export { AgentChat, NewConversationButton } from "./AgentChat.js";
|
|
13
|
+
export { AgentLauncher } from "./AgentLauncher.js";
|
|
14
|
+
export { CalliopeAgent } from "./CalliopeAgent.js";
|
|
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";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents from functions: `createFunctionAgent` adapts any function that
|
|
3
|
+
* produces AG-UI events (a non-AG-UI backend, wrapped), and `createMockAgent`
|
|
4
|
+
* plays recorded turns for Storybook and tests. Both are a real
|
|
5
|
+
* `@ag-ui/client` `HttpAgent` whose fetch is answered in the page as a
|
|
6
|
+
* server-sent event stream, never over the network.
|
|
7
|
+
*/
|
|
8
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
9
|
+
import { type Message, type RunAgentInput } from "@ag-ui/core";
|
|
10
|
+
import type { AgUiEvent } from "../chat/model.js";
|
|
11
|
+
/** What the mock answers one run with: the events, without the run's start and finish. */
|
|
12
|
+
export type MockTurn = readonly AgUiEvent[] | ((input: RunAgentInput, turn: number) => readonly AgUiEvent[]);
|
|
13
|
+
export interface MockAgentOptions {
|
|
14
|
+
/**
|
|
15
|
+
* One entry per run, in order; the last repeats. A function sees the run's
|
|
16
|
+
* input (messages, tools, context, resume) and the run's index.
|
|
17
|
+
*/
|
|
18
|
+
turns: readonly MockTurn[];
|
|
19
|
+
/** Pause between events, in ms; 0 for tests. Defaults to 30. */
|
|
20
|
+
delay?: number;
|
|
21
|
+
threadId?: string;
|
|
22
|
+
initialMessages?: Message[];
|
|
23
|
+
initialState?: unknown;
|
|
24
|
+
/** Every request's input, for a test to read. */
|
|
25
|
+
onRequest?: (input: RunAgentInput) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The events of one run, as a function produces them: a list, a promise of
|
|
29
|
+
* one, or an async iterable that yields them as they come. The run's start
|
|
30
|
+
* and finish are added unless the function emits them.
|
|
31
|
+
*/
|
|
32
|
+
export type AgentRunFunction = (input: RunAgentInput, signal: AbortSignal) => Iterable<AgUiEvent> | AsyncIterable<AgUiEvent> | Promise<Iterable<AgUiEvent>>;
|
|
33
|
+
export interface FunctionAgentOptions {
|
|
34
|
+
threadId?: string;
|
|
35
|
+
initialMessages?: Message[];
|
|
36
|
+
initialState?: unknown;
|
|
37
|
+
/** Pause before each event, in ms (a recording played back at reading speed). */
|
|
38
|
+
delay?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* An AG-UI agent from a function: the adapter for a backend that does not
|
|
42
|
+
* speak AG-UI yet (a request and response endpoint, a custom stream), and
|
|
43
|
+
* the base of `createMockAgent`. It is a real `HttpAgent` whose transport is
|
|
44
|
+
* the function, so the client's parser, verifier and message bookkeeping run
|
|
45
|
+
* as they do against a live endpoint, and `stop` aborts `signal`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createFunctionAgent(run: AgentRunFunction, { threadId, initialMessages, initialState, delay }?: FunctionAgentOptions): HttpAgent;
|
|
48
|
+
/** An agent that answers from `turns`, streaming each event as SSE. */
|
|
49
|
+
export declare function createMockAgent({ turns, delay, threadId, initialMessages, initialState, onRequest, }: MockAgentOptions): HttpAgent;
|
|
50
|
+
/** An assistant's streamed reply, in `chunks`, as the events that make it. */
|
|
51
|
+
export declare function textEvents(messageId: string, ...chunks: string[]): AgUiEvent[];
|
|
52
|
+
/** A tool call with its arguments, optionally with a result (a backend tool). */
|
|
53
|
+
export declare function toolCallEvents(toolCallId: string, name: string, args: unknown, options?: {
|
|
54
|
+
parentMessageId?: string;
|
|
55
|
+
result?: unknown;
|
|
56
|
+
}): AgUiEvent[];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents from functions: `createFunctionAgent` adapts any function that
|
|
3
|
+
* produces AG-UI events (a non-AG-UI backend, wrapped), and `createMockAgent`
|
|
4
|
+
* plays recorded turns for Storybook and tests. Both are a real
|
|
5
|
+
* `@ag-ui/client` `HttpAgent` whose fetch is answered in the page as a
|
|
6
|
+
* server-sent event stream, never over the network.
|
|
7
|
+
*/
|
|
8
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
9
|
+
import { EventType } from "@ag-ui/core";
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
12
|
+
if (signal?.aborted)
|
|
13
|
+
return reject(new DOMException("Aborted", "AbortError"));
|
|
14
|
+
const timer = setTimeout(resolve, ms);
|
|
15
|
+
signal?.addEventListener("abort", () => {
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
18
|
+
}, { once: true });
|
|
19
|
+
});
|
|
20
|
+
async function* eventsOf(output) {
|
|
21
|
+
if (Symbol.asyncIterator in output)
|
|
22
|
+
yield* output;
|
|
23
|
+
else
|
|
24
|
+
yield* output;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* An AG-UI agent from a function: the adapter for a backend that does not
|
|
28
|
+
* speak AG-UI yet (a request and response endpoint, a custom stream), and
|
|
29
|
+
* the base of `createMockAgent`. It is a real `HttpAgent` whose transport is
|
|
30
|
+
* the function, so the client's parser, verifier and message bookkeeping run
|
|
31
|
+
* as they do against a live endpoint, and `stop` aborts `signal`.
|
|
32
|
+
*/
|
|
33
|
+
export function createFunctionAgent(run, { threadId, initialMessages, initialState, delay = 0 } = {}) {
|
|
34
|
+
const fetch = async (_url, init) => {
|
|
35
|
+
const input = JSON.parse(String(init.body));
|
|
36
|
+
const signal = init.signal ?? new AbortController().signal;
|
|
37
|
+
const body = new ReadableStream({
|
|
38
|
+
async start(controller) {
|
|
39
|
+
try {
|
|
40
|
+
let ended = false;
|
|
41
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: EventType.RUN_STARTED, threadId: input.threadId, runId: input.runId })}\n\n`));
|
|
42
|
+
const output = run(input, signal);
|
|
43
|
+
const source = output instanceof Promise ? await output : output;
|
|
44
|
+
for await (const event of eventsOf(source)) {
|
|
45
|
+
if (delay > 0)
|
|
46
|
+
await sleep(delay, signal);
|
|
47
|
+
if (signal.aborted)
|
|
48
|
+
throw new DOMException("Aborted", "AbortError");
|
|
49
|
+
if (event.type === EventType.RUN_STARTED)
|
|
50
|
+
continue;
|
|
51
|
+
if (event.type === EventType.RUN_FINISHED || event.type === EventType.RUN_ERROR)
|
|
52
|
+
ended = true;
|
|
53
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
|
|
54
|
+
}
|
|
55
|
+
if (!ended) {
|
|
56
|
+
const finished = {
|
|
57
|
+
type: EventType.RUN_FINISHED,
|
|
58
|
+
threadId: input.threadId,
|
|
59
|
+
runId: input.runId,
|
|
60
|
+
};
|
|
61
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(finished)}\n\n`));
|
|
62
|
+
}
|
|
63
|
+
controller.close();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (signal.aborted)
|
|
67
|
+
controller.error(error);
|
|
68
|
+
else {
|
|
69
|
+
const failed = {
|
|
70
|
+
type: EventType.RUN_ERROR,
|
|
71
|
+
message: error instanceof Error ? error.message : String(error),
|
|
72
|
+
};
|
|
73
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(failed)}\n\n`));
|
|
74
|
+
controller.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
return new Response(body, { status: 200, headers: { "content-type": "text/event-stream" } });
|
|
80
|
+
};
|
|
81
|
+
return new HttpAgent({
|
|
82
|
+
url: "https://agent.invalid/agui",
|
|
83
|
+
fetch,
|
|
84
|
+
...(threadId ? { threadId } : {}),
|
|
85
|
+
...(initialMessages ? { initialMessages } : {}),
|
|
86
|
+
...(initialState !== undefined ? { initialState: initialState } : {}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** An agent that answers from `turns`, streaming each event as SSE. */
|
|
90
|
+
export function createMockAgent({ turns, delay = 30, threadId, initialMessages, initialState, onRequest, }) {
|
|
91
|
+
let turn = 0;
|
|
92
|
+
return createFunctionAgent(async function* (input) {
|
|
93
|
+
onRequest?.(input);
|
|
94
|
+
const index = turn++;
|
|
95
|
+
const script = turns[Math.min(index, turns.length - 1)] ?? [];
|
|
96
|
+
const events = typeof script === "function" ? script(input, index) : script;
|
|
97
|
+
// Yield once first, so even a delay of 0 streams instead of arriving in one piece.
|
|
98
|
+
for (const event of events) {
|
|
99
|
+
await Promise.resolve();
|
|
100
|
+
yield event;
|
|
101
|
+
}
|
|
102
|
+
}, {
|
|
103
|
+
delay,
|
|
104
|
+
...(threadId ? { threadId } : {}),
|
|
105
|
+
...(initialMessages ? { initialMessages } : {}),
|
|
106
|
+
...(initialState !== undefined ? { initialState } : {}),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** An assistant's streamed reply, in `chunks`, as the events that make it. */
|
|
110
|
+
export function textEvents(messageId, ...chunks) {
|
|
111
|
+
return [
|
|
112
|
+
{ type: EventType.TEXT_MESSAGE_START, messageId, role: "assistant" },
|
|
113
|
+
...chunks.map((delta) => ({
|
|
114
|
+
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
115
|
+
messageId,
|
|
116
|
+
delta,
|
|
117
|
+
})),
|
|
118
|
+
{ type: EventType.TEXT_MESSAGE_END, messageId },
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
/** A tool call with its arguments, optionally with a result (a backend tool). */
|
|
122
|
+
export function toolCallEvents(toolCallId, name, args, options = {}) {
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
type: EventType.TOOL_CALL_START,
|
|
126
|
+
toolCallId,
|
|
127
|
+
toolCallName: name,
|
|
128
|
+
...(options.parentMessageId ? { parentMessageId: options.parentMessageId } : {}),
|
|
129
|
+
},
|
|
130
|
+
{ type: EventType.TOOL_CALL_ARGS, toolCallId, delta: JSON.stringify(args) },
|
|
131
|
+
{ type: EventType.TOOL_CALL_END, toolCallId },
|
|
132
|
+
...(options.result === undefined
|
|
133
|
+
? []
|
|
134
|
+
: [
|
|
135
|
+
{
|
|
136
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
137
|
+
messageId: `${toolCallId}:result`,
|
|
138
|
+
toolCallId,
|
|
139
|
+
content: typeof options.result === "string" ? options.result : JSON.stringify(options.result),
|
|
140
|
+
},
|
|
141
|
+
]),
|
|
142
|
+
];
|
|
143
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import { type AgUiInterrupt, type ChatToolCall } from "../chat/model.js";
|
|
3
|
+
import type { CalliopeAgentApi } from "./AgentProvider.js";
|
|
4
|
+
import type { AgentToolRenderProps } from "./tool.js";
|
|
5
|
+
/** What a tool renderer draws from: the call, its arguments and result, and the agent. */
|
|
6
|
+
export interface ToolRendererProps<Args = unknown> extends AgentToolRenderProps<Args> {
|
|
7
|
+
/** The result parsed as JSON, or as text when it is not JSON. */
|
|
8
|
+
result: unknown;
|
|
9
|
+
agent: CalliopeAgentApi;
|
|
10
|
+
}
|
|
11
|
+
/** What a custom-event renderer draws from: the latest value of its event, and the agent. */
|
|
12
|
+
export interface CustomRendererProps<Value = unknown> {
|
|
13
|
+
name: string;
|
|
14
|
+
value: Value;
|
|
15
|
+
/** Every value the event has had under this key, oldest first. */
|
|
16
|
+
history: readonly Value[];
|
|
17
|
+
agent: CalliopeAgentApi;
|
|
18
|
+
}
|
|
19
|
+
/** What an interrupt renderer draws from: the open interrupt and how to answer it. */
|
|
20
|
+
export interface InterruptRendererProps {
|
|
21
|
+
interrupt: AgUiInterrupt;
|
|
22
|
+
/** Answers it; the run resumes once every open interrupt has an answer. */
|
|
23
|
+
resolve: (payload?: unknown) => void;
|
|
24
|
+
cancel: () => void;
|
|
25
|
+
agent: CalliopeAgentApi;
|
|
26
|
+
}
|
|
27
|
+
export interface StateRendererProps<State = unknown> {
|
|
28
|
+
state: State;
|
|
29
|
+
setState: (state: State) => void;
|
|
30
|
+
agent: CalliopeAgentApi;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Generative UI: host components for what the agent emits. A tool call by
|
|
34
|
+
* name (the agent's backend tools as well as the browser's), an AG-UI
|
|
35
|
+
* `CUSTOM` event by name (drawn where it arrived; an event carrying an `id`
|
|
36
|
+
* in its value replaces its earlier values in place, so progress updates
|
|
37
|
+
* one card), an interrupt by reason, and the shared state.
|
|
38
|
+
*/
|
|
39
|
+
export interface AgentRenderers {
|
|
40
|
+
tools?: Readonly<Record<string, React.ComponentType<ToolRendererProps<any>>>>;
|
|
41
|
+
custom?: Readonly<Record<string, React.ComponentType<CustomRendererProps<any>>>>;
|
|
42
|
+
interrupts?: Readonly<Record<string, React.ComponentType<InterruptRendererProps>>>;
|
|
43
|
+
/** Draws the shared state above the conversation. */
|
|
44
|
+
state?: React.ComponentType<StateRendererProps<any>>;
|
|
45
|
+
}
|
|
46
|
+
/** Several registries as one; a later one wins a name. */
|
|
47
|
+
export declare function mergeRenderers(...registries: (AgentRenderers | undefined)[]): AgentRenderers;
|
|
48
|
+
/** A tool's result as a renderer reads it. */
|
|
49
|
+
export declare function parseResult(call: ChatToolCall): unknown;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseToolCallArgs } from "../chat/model.js";
|
|
2
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
3
|
+
/** Several registries as one; a later one wins a name. */
|
|
4
|
+
export function mergeRenderers(...registries) {
|
|
5
|
+
const merged = {
|
|
6
|
+
tools: {},
|
|
7
|
+
custom: {},
|
|
8
|
+
interrupts: {},
|
|
9
|
+
};
|
|
10
|
+
for (const r of registries) {
|
|
11
|
+
if (!r)
|
|
12
|
+
continue;
|
|
13
|
+
merged.tools = { ...merged.tools, ...r.tools };
|
|
14
|
+
merged.custom = { ...merged.custom, ...r.custom };
|
|
15
|
+
merged.interrupts = { ...merged.interrupts, ...r.interrupts };
|
|
16
|
+
if (r.state)
|
|
17
|
+
merged.state = r.state;
|
|
18
|
+
}
|
|
19
|
+
return merged;
|
|
20
|
+
}
|
|
21
|
+
/** A tool's result as a renderer reads it. */
|
|
22
|
+
export function parseResult(call) {
|
|
23
|
+
if (call.result === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
const parsed = parseToolCallArgs(call.result);
|
|
26
|
+
return parsed === undefined ? call.result : parsed;
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An open state that is controlled (`open`), remembered per person in this
|
|
3
|
+
* browser (`persistKey`), or held here. The server renders `defaultOpen`; the
|
|
4
|
+
* client applies the saved state after hydration, without a mismatch, and a
|
|
5
|
+
* change in one tab reaches the others.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useOpenState({ open, defaultOpen, onOpenChange, persistKey, }: {
|
|
8
|
+
open?: boolean;
|
|
9
|
+
defaultOpen?: boolean;
|
|
10
|
+
onOpenChange?: (open: boolean) => void;
|
|
11
|
+
persistKey?: string;
|
|
12
|
+
}): readonly [boolean, (open: boolean) => void];
|
|
13
|
+
/** A shortcut as given: "mod+/" (Cmd on a Mac, Ctrl elsewhere) or a single key ("]"). */
|
|
14
|
+
export interface ParsedShortcut {
|
|
15
|
+
mod: boolean;
|
|
16
|
+
key: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function parseShortcut(shortcut: string): ParsedShortcut;
|
|
19
|
+
/** The shortcut as people read it on this machine ("⌘/", "Ctrl+/"). */
|
|
20
|
+
export declare function shortcutLabel(shortcut: string): string;
|
|
21
|
+
/** The shortcut in `aria-keyshortcuts` form. */
|
|
22
|
+
export declare function shortcutAria(shortcut: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Calls `onPress` on the shortcut, anywhere in the page. A modified shortcut
|
|
25
|
+
* works while typing (the assistant opens from the middle of a form); a bare
|
|
26
|
+
* key does not, and neither works while a modal dialog holds the keyboard.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useShortcut(shortcut: string | false | undefined, onPress: () => void): void;
|
|
29
|
+
/** Whether a media query matches; the server and the first render say `serverValue`. */
|
|
30
|
+
export declare function useMediaQuery(query: string, serverValue?: boolean): boolean;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { readLocal, subscribeLocal, writeLocal } from "../lib/local-store.js";
|
|
4
|
+
/**
|
|
5
|
+
* An open state that is controlled (`open`), remembered per person in this
|
|
6
|
+
* browser (`persistKey`), or held here. The server renders `defaultOpen`; the
|
|
7
|
+
* client applies the saved state after hydration, without a mismatch, and a
|
|
8
|
+
* change in one tab reaches the others.
|
|
9
|
+
*/
|
|
10
|
+
export function useOpenState({ open, defaultOpen = false, onOpenChange, persistKey, }) {
|
|
11
|
+
const [own, setOwn] = React.useState(defaultOpen);
|
|
12
|
+
const saved = React.useSyncExternalStore(subscribeLocal, () => (persistKey ? readLocal(persistKey) : null), () => null);
|
|
13
|
+
const current = open ?? (persistKey && saved !== null ? saved === "1" : persistKey ? defaultOpen : own);
|
|
14
|
+
const set = React.useCallback((next) => {
|
|
15
|
+
if (open === undefined) {
|
|
16
|
+
if (persistKey)
|
|
17
|
+
writeLocal(persistKey, next ? "1" : "0");
|
|
18
|
+
else
|
|
19
|
+
setOwn(next);
|
|
20
|
+
}
|
|
21
|
+
onOpenChange?.(next);
|
|
22
|
+
}, [open, persistKey, onOpenChange]);
|
|
23
|
+
return [current, set];
|
|
24
|
+
}
|
|
25
|
+
export function parseShortcut(shortcut) {
|
|
26
|
+
const mod = shortcut.toLowerCase().startsWith("mod+");
|
|
27
|
+
return { mod, key: mod ? shortcut.slice(4) : shortcut };
|
|
28
|
+
}
|
|
29
|
+
const isMac = () => typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.platform || "");
|
|
30
|
+
/** The shortcut as people read it on this machine ("⌘/", "Ctrl+/"). */
|
|
31
|
+
export function shortcutLabel(shortcut) {
|
|
32
|
+
const { mod, key } = parseShortcut(shortcut);
|
|
33
|
+
if (!mod)
|
|
34
|
+
return key;
|
|
35
|
+
return isMac() ? `⌘${key}` : `Ctrl+${key}`;
|
|
36
|
+
}
|
|
37
|
+
/** The shortcut in `aria-keyshortcuts` form. */
|
|
38
|
+
export function shortcutAria(shortcut) {
|
|
39
|
+
const { mod, key } = parseShortcut(shortcut);
|
|
40
|
+
return mod ? `Meta+${key} Control+${key}` : key;
|
|
41
|
+
}
|
|
42
|
+
function typing(target) {
|
|
43
|
+
if (!(target instanceof HTMLElement))
|
|
44
|
+
return false;
|
|
45
|
+
return target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Calls `onPress` on the shortcut, anywhere in the page. A modified shortcut
|
|
49
|
+
* works while typing (the assistant opens from the middle of a form); a bare
|
|
50
|
+
* key does not, and neither works while a modal dialog holds the keyboard.
|
|
51
|
+
*/
|
|
52
|
+
export function useShortcut(shortcut, onPress) {
|
|
53
|
+
const latest = React.useRef(onPress);
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
latest.current = onPress;
|
|
56
|
+
});
|
|
57
|
+
React.useEffect(() => {
|
|
58
|
+
if (!shortcut)
|
|
59
|
+
return;
|
|
60
|
+
const { mod, key } = parseShortcut(shortcut);
|
|
61
|
+
const onKey = (event) => {
|
|
62
|
+
if (event.defaultPrevented || event.isComposing || event.altKey)
|
|
63
|
+
return;
|
|
64
|
+
if (event.key !== key)
|
|
65
|
+
return;
|
|
66
|
+
const modified = event.metaKey || event.ctrlKey;
|
|
67
|
+
if (mod !== modified)
|
|
68
|
+
return;
|
|
69
|
+
if (!mod && typing(event.target))
|
|
70
|
+
return;
|
|
71
|
+
const target = event.target instanceof HTMLElement ? event.target : null;
|
|
72
|
+
if (target?.closest("[aria-modal='true'], [role='alertdialog']"))
|
|
73
|
+
return;
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
latest.current();
|
|
76
|
+
};
|
|
77
|
+
window.addEventListener("keydown", onKey);
|
|
78
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
79
|
+
}, [shortcut]);
|
|
80
|
+
}
|
|
81
|
+
/** Whether a media query matches; the server and the first render say `serverValue`. */
|
|
82
|
+
export function useMediaQuery(query, serverValue = true) {
|
|
83
|
+
return React.useSyncExternalStore((onChange) => {
|
|
84
|
+
if (typeof window.matchMedia !== "function")
|
|
85
|
+
return () => { };
|
|
86
|
+
const list = window.matchMedia(query);
|
|
87
|
+
list.addEventListener("change", onChange);
|
|
88
|
+
return () => list.removeEventListener("change", onChange);
|
|
89
|
+
}, () => typeof window.matchMedia === "function" ? window.matchMedia(query).matches : serverValue, () => serverValue);
|
|
90
|
+
}
|