@springbrand/chat-client 0.1.1 → 0.1.2

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.
@@ -0,0 +1,36 @@
1
+ import type { FileUIPart } from "ai";
2
+ export interface UploadReceipt {
3
+ path: string;
4
+ size: number;
5
+ mime: string;
6
+ markdownPath?: string | null;
7
+ }
8
+ export declare function isVisionFile(meta: {
9
+ mediaType?: string;
10
+ size?: number;
11
+ }): boolean;
12
+ export declare function uploadFileToWorkspace(file: File, chatId: string): Promise<UploadReceipt>;
13
+ export type AttachmentStatus = "uploading" | "ready" | "error";
14
+ export type Attachment = {
15
+ id: string;
16
+ type: "file";
17
+ filename?: string;
18
+ mediaType?: string;
19
+ url: string;
20
+ size?: number;
21
+ isVision: boolean;
22
+ status: AttachmentStatus;
23
+ receipt?: UploadReceipt;
24
+ };
25
+ export declare function prepareAttachmentParts(files: readonly Attachment[]): Promise<FileUIPart[]>;
26
+ /** 浏览器附件 module:公共 Composer 负责选择与展示,这里维护 Workspace 上传和提交状态。 */
27
+ export declare function useWorkspaceAttachments(chatId: string | null): {
28
+ files: Attachment[];
29
+ add: (incoming: readonly File[]) => void;
30
+ remove: (id: string) => void;
31
+ retry: (id: string) => void;
32
+ reorder: (fromIndex: number, toIndex: number) => void;
33
+ clear: () => void;
34
+ allReady: boolean;
35
+ toSubmit: () => Promise<FileUIPart[]>;
36
+ };
@@ -0,0 +1,173 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { nanoid } from "nanoid";
3
+ const VISION_MAX_BYTES = 500 * 1024;
4
+ const VISION_TYPES = /^image\/(png|jpe?g|webp|gif)$/i;
5
+ export function isVisionFile(meta) {
6
+ return (VISION_TYPES.test(meta.mediaType ?? "") && (meta.size ?? 0) <= VISION_MAX_BYTES);
7
+ }
8
+ export async function uploadFileToWorkspace(file, chatId) {
9
+ const form = new FormData();
10
+ form.append("file", file);
11
+ const query = new URLSearchParams({ chat: chatId });
12
+ const response = await fetch(`/api/files?${query.toString()}`, {
13
+ method: "POST",
14
+ body: form,
15
+ });
16
+ if (!response.ok)
17
+ throw new Error(`upload failed: ${response.status}`);
18
+ return (await response.json());
19
+ }
20
+ async function toDataUrl(part) {
21
+ if (!part.url.startsWith("blob:"))
22
+ return part;
23
+ try {
24
+ const blob = await (await fetch(part.url)).blob();
25
+ const dataUrl = await new Promise((resolve) => {
26
+ const reader = new FileReader();
27
+ reader.onloadend = () => resolve(reader.result);
28
+ reader.onerror = () => resolve(null);
29
+ reader.readAsDataURL(blob);
30
+ });
31
+ return { ...part, url: dataUrl ?? part.url };
32
+ }
33
+ catch {
34
+ return part;
35
+ }
36
+ }
37
+ export async function prepareAttachmentParts(files) {
38
+ return Promise.all(files.map(async (file) => {
39
+ if (file.isVision) {
40
+ const prepared = await toDataUrl({
41
+ filename: file.filename,
42
+ mediaType: file.mediaType,
43
+ url: file.url,
44
+ });
45
+ if (prepared.url.startsWith("blob:")) {
46
+ throw new Error(`${file.filename ?? "Image"} could not be prepared`);
47
+ }
48
+ return {
49
+ type: "file",
50
+ ...(prepared.filename ? { filename: prepared.filename } : {}),
51
+ mediaType: prepared.mediaType || "application/octet-stream",
52
+ url: prepared.url,
53
+ };
54
+ }
55
+ if (!file.receipt) {
56
+ throw new Error(`${file.filename ?? "Attachment"} is not uploaded`);
57
+ }
58
+ return {
59
+ type: "file",
60
+ ...(file.filename ? { filename: file.filename } : {}),
61
+ mediaType: file.receipt.mime,
62
+ url: file.receipt.path,
63
+ };
64
+ }));
65
+ }
66
+ /** 浏览器附件 module:公共 Composer 负责选择与展示,这里维护 Workspace 上传和提交状态。 */
67
+ export function useWorkspaceAttachments(chatId) {
68
+ const [files, setFiles] = useState([]);
69
+ const rawRef = useRef(new Map());
70
+ const patch = useCallback((id, value) => {
71
+ setFiles((current) => current.map((file) => (file.id === id ? { ...file, ...value } : file)));
72
+ }, []);
73
+ const startUpload = useCallback((id, file) => {
74
+ patch(id, { status: "uploading" });
75
+ if (!chatId) {
76
+ patch(id, { status: "error" });
77
+ return;
78
+ }
79
+ uploadFileToWorkspace(file, chatId)
80
+ .then((receipt) => patch(id, { status: "ready", receipt }))
81
+ .catch(() => patch(id, { status: "error" }));
82
+ }, [chatId, patch]);
83
+ const add = useCallback((incoming) => {
84
+ if (incoming.length === 0)
85
+ return;
86
+ const entries = incoming.map((file) => {
87
+ const id = nanoid();
88
+ rawRef.current.set(id, file);
89
+ const isVision = isVisionFile({
90
+ mediaType: file.type,
91
+ size: file.size,
92
+ });
93
+ return {
94
+ id,
95
+ type: "file",
96
+ filename: file.name,
97
+ mediaType: file.type,
98
+ size: file.size,
99
+ url: URL.createObjectURL(file),
100
+ isVision,
101
+ status: (isVision ? "ready" : "uploading"),
102
+ };
103
+ });
104
+ setFiles((current) => [...current, ...entries]);
105
+ for (const entry of entries) {
106
+ if (!entry.isVision) {
107
+ const raw = rawRef.current.get(entry.id);
108
+ if (raw)
109
+ startUpload(entry.id, raw);
110
+ }
111
+ }
112
+ }, [startUpload]);
113
+ const retry = useCallback((id) => {
114
+ const raw = rawRef.current.get(id);
115
+ if (raw)
116
+ startUpload(id, raw);
117
+ }, [startUpload]);
118
+ const remove = useCallback((id) => {
119
+ rawRef.current.delete(id);
120
+ setFiles((current) => {
121
+ const found = current.find((file) => file.id === id);
122
+ if (found?.url.startsWith("blob:"))
123
+ URL.revokeObjectURL(found.url);
124
+ return current.filter((file) => file.id !== id);
125
+ });
126
+ }, []);
127
+ const reorder = useCallback((fromIndex, toIndex) => {
128
+ setFiles((current) => {
129
+ if (fromIndex < 0 ||
130
+ toIndex < 0 ||
131
+ fromIndex >= current.length ||
132
+ toIndex >= current.length) {
133
+ return current;
134
+ }
135
+ const next = [...current];
136
+ const [moved] = next.splice(fromIndex, 1);
137
+ next.splice(toIndex, 0, moved);
138
+ return next;
139
+ });
140
+ }, []);
141
+ const clear = useCallback(() => {
142
+ rawRef.current.clear();
143
+ setFiles((current) => {
144
+ for (const file of current) {
145
+ if (file.url.startsWith("blob:"))
146
+ URL.revokeObjectURL(file.url);
147
+ }
148
+ return [];
149
+ });
150
+ }, []);
151
+ const allReady = useMemo(() => files.every((file) => file.status === "ready"), [files]);
152
+ const toSubmit = useCallback(() => prepareAttachmentParts(files), [files]);
153
+ const filesRef = useRef(files);
154
+ useEffect(() => {
155
+ filesRef.current = files;
156
+ }, [files]);
157
+ useEffect(() => () => {
158
+ for (const file of filesRef.current) {
159
+ if (file.url.startsWith("blob:"))
160
+ URL.revokeObjectURL(file.url);
161
+ }
162
+ }, []);
163
+ return {
164
+ files,
165
+ add,
166
+ remove,
167
+ retry,
168
+ reorder,
169
+ clear,
170
+ allReady,
171
+ toSubmit,
172
+ };
173
+ }
@@ -0,0 +1,35 @@
1
+ export interface ChatSessionSummary {
2
+ id: string;
3
+ userAgentId: string;
4
+ title: string;
5
+ titleSource: "default" | "generated" | "manual";
6
+ createdAt: number;
7
+ updatedAt: number;
8
+ pinned: boolean;
9
+ archived: boolean;
10
+ activity: "idle" | "working" | "needs-input";
11
+ usage?: {
12
+ totalTokens: number;
13
+ totalCost: number;
14
+ };
15
+ }
16
+ export declare const CURRENT_INBOX_AGENT_PATH = "api/agent-connections/inbox";
17
+ type Request = typeof fetch;
18
+ export declare function loadChatSessions(signal?: AbortSignal, request?: Request): Promise<ChatSessionSummary[]>;
19
+ /** Browser client for the user Inbox. Routing and presentation stay with the host UI. */
20
+ export declare function useChatSessions(): {
21
+ chats: ChatSessionSummary[];
22
+ synced: boolean;
23
+ workspaceRev: number;
24
+ createChat: (userAgentId?: string) => Promise<ChatSessionSummary | undefined>;
25
+ forkChat: (sourceChatId: string, messageId: string) => Promise<ChatSessionSummary>;
26
+ renameChat: (id: string, title: string) => Promise<void>;
27
+ deleteChat: (id: string) => Promise<void>;
28
+ pinChat: (id: string, pinned: boolean) => Promise<void>;
29
+ archiveChat: (id: string, archived: boolean) => Promise<void>;
30
+ reloadChatRuntime: (chatId: string, userAgentId: string) => Promise<{
31
+ userAgentId: string;
32
+ }>;
33
+ };
34
+ export type ChatSessions = ReturnType<typeof useChatSessions>;
35
+ export {};
@@ -0,0 +1,78 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useAgent } from "agents/react";
3
+ export const CURRENT_INBOX_AGENT_PATH = "api/agent-connections/inbox";
4
+ export async function loadChatSessions(signal, request = fetch) {
5
+ const response = await request("/api/chats", {
6
+ cache: "no-store",
7
+ headers: { Accept: "application/json" },
8
+ signal,
9
+ });
10
+ if (!response.ok)
11
+ throw new Error("Unable to load chat sessions");
12
+ const body = await response.json().catch(() => null);
13
+ if (!Array.isArray(body?.chats)) {
14
+ throw new Error("Invalid chat sessions response");
15
+ }
16
+ return body.chats;
17
+ }
18
+ /** Browser client for the user Inbox. Routing and presentation stay with the host UI. */
19
+ export function useChatSessions() {
20
+ const inbox = useAgent({
21
+ agent: "Inbox",
22
+ basePath: CURRENT_INBOX_AGENT_PATH,
23
+ });
24
+ const [snapshot, setSnapshot] = useState();
25
+ const creatingRef = useRef(false);
26
+ useEffect(() => {
27
+ const controller = new AbortController();
28
+ void loadChatSessions(controller.signal)
29
+ .then(setSnapshot)
30
+ .catch(() => undefined); // WebSocket state remains the fallback.
31
+ return () => controller.abort();
32
+ }, []);
33
+ const createChat = useCallback(async (userAgentId) => {
34
+ if (creatingRef.current)
35
+ return undefined;
36
+ creatingRef.current = true;
37
+ try {
38
+ return await inbox.call("createChat", userAgentId ? [{ userAgentId }] : undefined);
39
+ }
40
+ finally {
41
+ creatingRef.current = false;
42
+ }
43
+ }, [inbox]);
44
+ const forkChat = useCallback((sourceChatId, messageId) => inbox.call("forkChat", [sourceChatId, messageId]), [inbox]);
45
+ const renameChat = useCallback(async (id, title) => {
46
+ const normalizedTitle = title.trim();
47
+ if (!normalizedTitle)
48
+ return;
49
+ await inbox.call("renameChat", [id, normalizedTitle]);
50
+ }, [inbox]);
51
+ const deleteChat = useCallback(async (id) => {
52
+ await inbox.call("deleteChat", [id]);
53
+ }, [inbox]);
54
+ const pinChat = useCallback(async (id, pinned) => {
55
+ await inbox.call("pinChat", [id, pinned]);
56
+ }, [inbox]);
57
+ const archiveChat = useCallback(async (id, archived) => {
58
+ await inbox.call("archiveChat", [id, archived]);
59
+ }, [inbox]);
60
+ const reloadChatRuntime = useCallback(async (chatId, userAgentId) => {
61
+ const result = await inbox.call("chatReloadRuntime", [chatId, userAgentId]);
62
+ if (!result)
63
+ throw new Error("Session is unavailable");
64
+ return result;
65
+ }, [inbox]);
66
+ return {
67
+ chats: inbox.state?.chats ?? snapshot ?? [],
68
+ synced: inbox.state !== undefined || snapshot !== undefined,
69
+ workspaceRev: inbox.state?.workspaceRev ?? 0,
70
+ createChat,
71
+ forkChat,
72
+ renameChat,
73
+ deleteChat,
74
+ pinChat,
75
+ archiveChat,
76
+ reloadChatRuntime,
77
+ };
78
+ }
@@ -0,0 +1,13 @@
1
+ import { type UIMessage } from "ai";
2
+ export interface ChatSummarySource {
3
+ kind: "file" | "link";
4
+ label: string;
5
+ href?: string;
6
+ }
7
+ export interface ChatMessageSummary {
8
+ activityCount: number;
9
+ currentGoal: string;
10
+ sources: ChatSummarySource[];
11
+ }
12
+ export declare function extractUrls(output: unknown): string[];
13
+ export declare function summarizeChatMessages(messages: readonly UIMessage[]): ChatMessageSummary;
@@ -0,0 +1,96 @@
1
+ import { isToolUIPart } from "ai";
2
+ export function extractUrls(output) {
3
+ if (output == null)
4
+ return [];
5
+ const text = typeof output === "string" ? output : JSON.stringify(output);
6
+ const matches = text.match(/https?:\/\/[^\s"'<>()[\]\\]+/g) ?? [];
7
+ return [...new Set(matches.map((url) => url.replace(/[.,;:!?]+$/, "")))];
8
+ }
9
+ function urlLabel(url) {
10
+ try {
11
+ const parsed = new URL(url);
12
+ return `${parsed.host}${parsed.pathname === "/" ? "" : parsed.pathname}`;
13
+ }
14
+ catch {
15
+ return url;
16
+ }
17
+ }
18
+ function stringField(record, field) {
19
+ return typeof record[field] === "string" ? record[field] : undefined;
20
+ }
21
+ export function summarizeChatMessages(messages) {
22
+ const sources = [];
23
+ const seen = new Set();
24
+ let activityCount = 0;
25
+ let currentGoal = "";
26
+ const add = (key, source) => {
27
+ if (seen.has(key))
28
+ return;
29
+ seen.add(key);
30
+ sources.push(source);
31
+ };
32
+ const addUrls = (value) => {
33
+ for (const url of extractUrls(value)) {
34
+ add(`url:${url}`, {
35
+ kind: "link",
36
+ label: urlLabel(url),
37
+ href: url,
38
+ });
39
+ }
40
+ };
41
+ for (const message of messages) {
42
+ if (message.role === "user") {
43
+ const text = message.parts
44
+ .flatMap((part) => part.type === "text" && typeof part.text === "string"
45
+ ? [part.text]
46
+ : [])
47
+ .join(" ")
48
+ .trim();
49
+ if (text)
50
+ currentGoal = text;
51
+ }
52
+ for (const part of message.parts) {
53
+ const record = part;
54
+ if (part.type === "file") {
55
+ const url = part.url;
56
+ const label = part.filename ?? part.mediaType ?? "附件";
57
+ add(`file:${url || label}`, {
58
+ kind: "file",
59
+ label,
60
+ ...(url.startsWith("http") ? { href: url } : {}),
61
+ });
62
+ }
63
+ else if (part.type === "source-document") {
64
+ const sourceId = stringField(record, "sourceId") ?? "";
65
+ add(`document:${sourceId}`, {
66
+ kind: "file",
67
+ label: stringField(record, "filename") ??
68
+ stringField(record, "title") ??
69
+ "文档",
70
+ });
71
+ }
72
+ else if (part.type === "source-url") {
73
+ const url = stringField(record, "url");
74
+ if (!url)
75
+ continue;
76
+ add(`url:${url}`, {
77
+ kind: "link",
78
+ label: stringField(record, "title") || urlLabel(url),
79
+ href: url,
80
+ });
81
+ }
82
+ }
83
+ for (const part of message.parts) {
84
+ if (isToolUIPart(part)) {
85
+ activityCount += 1;
86
+ addUrls(part.output);
87
+ }
88
+ }
89
+ for (const part of message.parts) {
90
+ if (part.type === "text" || part.type === "reasoning")
91
+ addUrls(part.text);
92
+ }
93
+ addUrls(message.metadata);
94
+ }
95
+ return { activityCount, currentGoal, sources: sources.slice(0, 5) };
96
+ }
@@ -0,0 +1,4 @@
1
+ export { useUniversalAgentChat, type AgentToolRun, type ChatConnectionOptions, type ChatRuntime, } from "./use-universal-agent-chat";
2
+ export { prepareAttachmentParts, useWorkspaceAttachments, type Attachment, type AttachmentStatus, type UploadReceipt, } from "./chat-attachments";
3
+ export { extractUrls, summarizeChatMessages, type ChatMessageSummary, type ChatSummarySource, } from "./chat-summary";
4
+ export { useChatSessions, type ChatSessionSummary, type ChatSessions, } from "./chat-sessions";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { useUniversalAgentChat, } from "./use-universal-agent-chat";
2
+ export { prepareAttachmentParts, useWorkspaceAttachments, } from "./chat-attachments";
3
+ export { extractUrls, summarizeChatMessages, } from "./chat-summary";
4
+ export { useChatSessions, } from "./chat-sessions";
@@ -0,0 +1,8 @@
1
+ type AgentListener = (event: MessageEvent) => void;
2
+ interface AgentConnection {
3
+ addEventListener(type: string, listener: AgentListener, options?: AddEventListenerOptions): void;
4
+ removeEventListener(type: string, listener: AgentListener): void;
5
+ }
6
+ /** Drops orphan AI SDK streaming chunks so one malformed part cannot stop chat. */
7
+ export declare function createSafeUIMessageAgentConnection<T extends AgentConnection>(agent: T): T;
8
+ export {};
@@ -0,0 +1,104 @@
1
+ function createStreamState() {
2
+ return {
3
+ text: new Set(),
4
+ reasoning: new Set(),
5
+ toolInput: new Set(),
6
+ };
7
+ }
8
+ function acceptsChunk(state, chunk) {
9
+ switch (chunk.type) {
10
+ case "text-start":
11
+ state.text.add(chunk.id);
12
+ return true;
13
+ case "text-delta":
14
+ return state.text.has(chunk.id);
15
+ case "text-end":
16
+ return state.text.delete(chunk.id);
17
+ case "reasoning-start":
18
+ state.reasoning.add(chunk.id);
19
+ return true;
20
+ case "reasoning-delta":
21
+ return state.reasoning.has(chunk.id);
22
+ case "reasoning-end":
23
+ return state.reasoning.delete(chunk.id);
24
+ case "tool-input-start":
25
+ state.toolInput.add(chunk.toolCallId);
26
+ return true;
27
+ case "tool-input-delta":
28
+ return state.toolInput.has(chunk.toolCallId);
29
+ case "finish-step":
30
+ state.text.clear();
31
+ state.reasoning.clear();
32
+ return true;
33
+ default:
34
+ return true;
35
+ }
36
+ }
37
+ /** Drops orphan AI SDK streaming chunks so one malformed part cannot stop chat. */
38
+ export function createSafeUIMessageAgentConnection(agent) {
39
+ const states = new Map();
40
+ const decisions = new WeakMap();
41
+ const listenerMaps = new Map();
42
+ const bound = new Map();
43
+ const shouldForward = (event) => {
44
+ const decided = decisions.get(event);
45
+ if (decided !== undefined)
46
+ return decided;
47
+ let forward = true;
48
+ try {
49
+ const frame = JSON.parse(event.data);
50
+ if (frame.type === "cf_agent_use_chat_response" &&
51
+ typeof frame.id === "string") {
52
+ if (typeof frame.body === "string" && frame.body.trim()) {
53
+ const chunk = JSON.parse(frame.body);
54
+ const state = chunk.type === "start"
55
+ ? createStreamState()
56
+ : states.get(frame.id) ?? createStreamState();
57
+ states.set(frame.id, state);
58
+ forward = acceptsChunk(state, chunk);
59
+ }
60
+ if (frame.done === true)
61
+ states.delete(frame.id);
62
+ }
63
+ }
64
+ catch {
65
+ // Leave unrelated or malformed frames to the owning SDK listener.
66
+ }
67
+ decisions.set(event, forward);
68
+ return forward;
69
+ };
70
+ const addEventListener = (type, listener, options) => {
71
+ if (type !== "message") {
72
+ agent.addEventListener(type, listener, options);
73
+ return;
74
+ }
75
+ const wrapped = (event) => {
76
+ if (typeof event.data !== "string" || shouldForward(event))
77
+ listener(event);
78
+ };
79
+ const listeners = listenerMaps.get(type) ?? new Map();
80
+ listeners.set(listener, wrapped);
81
+ listenerMaps.set(type, listeners);
82
+ options?.signal?.addEventListener("abort", () => listeners.delete(listener), { once: true });
83
+ agent.addEventListener(type, wrapped, options);
84
+ };
85
+ const removeEventListener = (type, listener) => {
86
+ const wrapped = listenerMaps.get(type)?.get(listener) ?? listener;
87
+ listenerMaps.get(type)?.delete(listener);
88
+ agent.removeEventListener(type, wrapped);
89
+ };
90
+ return new Proxy(agent, {
91
+ get(target, property) {
92
+ if (property === "addEventListener")
93
+ return addEventListener;
94
+ if (property === "removeEventListener")
95
+ return removeEventListener;
96
+ const value = Reflect.get(target, property, target);
97
+ if (typeof value !== "function")
98
+ return value;
99
+ if (!bound.has(property))
100
+ bound.set(property, value.bind(target));
101
+ return bound.get(property);
102
+ },
103
+ });
104
+ }
@@ -0,0 +1,102 @@
1
+ import type { ChatStatus, FileUIPart, UIMessage } from "ai";
2
+ import type { AgentToolRunState } from "agents";
3
+ type ChatMessageMetadata = Record<string, unknown> & {
4
+ turnStatus?: string;
5
+ };
6
+ type ChatMessage = UIMessage<ChatMessageMetadata>;
7
+ type ChatRuntimeLoadState = {
8
+ status: "idle";
9
+ available: false;
10
+ } | {
11
+ status: "loading";
12
+ phase: "config" | "plugins" | "mcp" | "pi";
13
+ available: boolean;
14
+ startedAt: number;
15
+ updatedAt: number;
16
+ } | {
17
+ status: "ready";
18
+ available: true;
19
+ startedAt: number;
20
+ completedAt: number;
21
+ } | {
22
+ status: "error";
23
+ phase: "config" | "plugins" | "mcp" | "pi";
24
+ available: boolean;
25
+ startedAt: number;
26
+ failedAt: number;
27
+ };
28
+ type ChatApproval = {
29
+ executionId: string;
30
+ source: "action" | "codemode" | "temporary-agent";
31
+ action: string;
32
+ summary: string;
33
+ executionLevel: "safe" | "low" | "medium" | "high";
34
+ requiredExecutionLevel: "safe" | "low" | "medium" | "high";
35
+ inputJson: string;
36
+ requestId: string;
37
+ };
38
+ type ChatQueuedSubmission = {
39
+ submissionId: string;
40
+ messageId: string;
41
+ preview: string;
42
+ position: number;
43
+ createdAt: number;
44
+ };
45
+ type ChatTurn = {
46
+ activeSubmissionId?: string;
47
+ steerable: boolean;
48
+ hasPendingSteer: boolean;
49
+ queued: ChatQueuedSubmission[];
50
+ };
51
+ export interface ChatRuntime {
52
+ messages: ChatMessage[];
53
+ status: ChatStatus;
54
+ runtimeLoad: ChatRuntimeLoadState | undefined;
55
+ isStreaming: boolean;
56
+ error: string | Error | undefined;
57
+ sendText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
58
+ steerText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
59
+ enqueueText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
60
+ steerQueued: (submissionId: string) => Promise<boolean>;
61
+ cancelQueued: (submissionId: string) => Promise<boolean>;
62
+ stop: () => Promise<void>;
63
+ regenerate: () => void;
64
+ canRetry: boolean;
65
+ agentToolRuns: AgentToolRunState<ChatMessage["parts"][number]>[];
66
+ isServerStreaming: boolean;
67
+ isRecovering: boolean;
68
+ isToolContinuation: boolean;
69
+ approvals: ChatApproval[] | undefined;
70
+ approvalsLoaded: boolean;
71
+ turn: ChatTurn | undefined;
72
+ turnActive: boolean;
73
+ canSteer: boolean;
74
+ }
75
+ type SharedChatConnectionOptions = {
76
+ host?: string;
77
+ credentials?: RequestCredentials;
78
+ inboxAgent?: string;
79
+ sessionAgent?: string;
80
+ };
81
+ export type ChatConnectionOptions = SharedChatConnectionOptions & ({
82
+ inboxName: string;
83
+ basePath?: never;
84
+ } | {
85
+ inboxName?: never;
86
+ basePath?: string;
87
+ });
88
+ export declare function mergeOptimisticMessages<T extends UIMessage>(messages: T[], optimistic: T[]): T[];
89
+ /**
90
+ * 薄适配层:把 useAgent + UIMessage chat + useAgentToolEvents
91
+ * 收敛成一个归一化 runtime,对外只暴露 messages / 发送 / 状态 / 子 agent 运行,
92
+ * 隐藏 SDK 细节。
93
+ *
94
+ * 多会话接线(官方 facet 模式):经父级 Inbox 连 UniversalAgent facet,
95
+ * URL = /api/agent-connections/inbox/sub/universal-agent/{chatId};可信用户
96
+ * 由 Worker 从 Session 解析后再路由到真正的 Inbox。
97
+ * sub 数组由客户端 kebab 化;服务端按 ctx.exports 反解回 CamelCase className,
98
+ * 与 Inbox.onBeforeSubAgent 的严格门卫(hasSubAgent)对齐。
99
+ */
100
+ export declare function useUniversalAgentChat(chatId: string, connection?: ChatConnectionOptions): ChatRuntime;
101
+ export type AgentToolRun = ChatRuntime["agentToolRuns"][number];
102
+ export {};