@springbrand/chat-client 0.1.3-alpha.0 → 0.1.3-alpha.10
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/package.json +3 -3
- package/src/chat-sessions.ts +18 -41
- package/src/index.ts +1 -1
- package/src/use-universal-agent-chat.ts +164 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/chat-client",
|
|
3
|
-
"version": "0.1.3-alpha.
|
|
3
|
+
"version": "0.1.3-alpha.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
".": "./src/index.ts"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"agents": "^0.
|
|
16
|
+
"agents": "^0.20.1",
|
|
17
17
|
"ai": "^7.0.0",
|
|
18
18
|
"react": "^19.0.0"
|
|
19
19
|
},
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@types/react-dom": "^19.2.3",
|
|
26
26
|
"react-dom": "^19.2.7",
|
|
27
27
|
"typescript": "^7.0.2",
|
|
28
|
-
"@springbrand/agent-runtime": "0.
|
|
28
|
+
"@springbrand/agent-runtime": "0.2.0-alpha.15"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
package/src/chat-sessions.ts
CHANGED
|
@@ -1,35 +1,22 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useAgent } from "agents/react";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
userAgentId: string;
|
|
7
|
-
title: string;
|
|
8
|
-
titleSource: "default" | "generated" | "manual";
|
|
9
|
-
createdAt: number;
|
|
10
|
-
updatedAt: number;
|
|
11
|
-
pinned: boolean;
|
|
12
|
-
archived: boolean;
|
|
13
|
-
activity: "idle" | "working" | "needs-input";
|
|
14
|
-
usage?: {
|
|
15
|
-
totalTokens: number;
|
|
16
|
-
totalCost: number;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface InboxState {
|
|
21
|
-
chats: ChatSessionSummary[];
|
|
4
|
+
interface InboxState<Session> {
|
|
5
|
+
chats: Session[];
|
|
22
6
|
workspaceRev?: number;
|
|
23
7
|
}
|
|
24
8
|
|
|
25
9
|
export const CURRENT_INBOX_AGENT_PATH = "api/agent-connections/inbox";
|
|
26
10
|
|
|
27
11
|
type Request = typeof fetch;
|
|
12
|
+
export type ChatSessionsOptions = {
|
|
13
|
+
protocols?: string | string[];
|
|
14
|
+
};
|
|
28
15
|
|
|
29
|
-
export async function loadChatSessions(
|
|
16
|
+
export async function loadChatSessions<Session>(
|
|
30
17
|
signal?: AbortSignal,
|
|
31
18
|
request: Request = fetch,
|
|
32
|
-
): Promise<
|
|
19
|
+
): Promise<Session[]> {
|
|
33
20
|
const response = await request("/api/chats", {
|
|
34
21
|
cache: "no-store",
|
|
35
22
|
headers: { Accept: "application/json" },
|
|
@@ -42,21 +29,22 @@ export async function loadChatSessions(
|
|
|
42
29
|
if (!Array.isArray(body?.chats)) {
|
|
43
30
|
throw new Error("Invalid chat sessions response");
|
|
44
31
|
}
|
|
45
|
-
return body.chats as
|
|
32
|
+
return body.chats as Session[];
|
|
46
33
|
}
|
|
47
34
|
|
|
48
35
|
/** Browser client for the user Inbox. Routing and presentation stay with the host UI. */
|
|
49
|
-
export function useChatSessions() {
|
|
50
|
-
const inbox = useAgent<InboxState
|
|
36
|
+
export function useChatSessions<Session>(options: ChatSessionsOptions = {}) {
|
|
37
|
+
const inbox = useAgent<InboxState<Session>>({
|
|
51
38
|
agent: "Inbox",
|
|
52
39
|
basePath: CURRENT_INBOX_AGENT_PATH,
|
|
40
|
+
...(options.protocols === undefined ? {} : { protocols: options.protocols }),
|
|
53
41
|
});
|
|
54
|
-
const [snapshot, setSnapshot] = useState<
|
|
42
|
+
const [snapshot, setSnapshot] = useState<Session[]>();
|
|
55
43
|
const creatingRef = useRef(false);
|
|
56
44
|
|
|
57
45
|
useEffect(() => {
|
|
58
46
|
const controller = new AbortController();
|
|
59
|
-
void loadChatSessions(controller.signal)
|
|
47
|
+
void loadChatSessions<Session>(controller.signal)
|
|
60
48
|
.then(setSnapshot)
|
|
61
49
|
.catch(() => undefined); // WebSocket state remains the fallback.
|
|
62
50
|
return () => controller.abort();
|
|
@@ -67,7 +55,7 @@ export function useChatSessions() {
|
|
|
67
55
|
creatingRef.current = true;
|
|
68
56
|
try {
|
|
69
57
|
await inbox.ready;
|
|
70
|
-
return await inbox.call<
|
|
58
|
+
return await inbox.call<Session>(
|
|
71
59
|
"createChat",
|
|
72
60
|
userAgentId ? [{ userAgentId }] : undefined,
|
|
73
61
|
);
|
|
@@ -78,7 +66,7 @@ export function useChatSessions() {
|
|
|
78
66
|
|
|
79
67
|
const forkChat = useCallback(
|
|
80
68
|
(sourceChatId: string, messageId: string) =>
|
|
81
|
-
inbox.call<
|
|
69
|
+
inbox.call<Session>("forkChat", [sourceChatId, messageId]),
|
|
82
70
|
[inbox],
|
|
83
71
|
);
|
|
84
72
|
|
|
@@ -100,18 +88,6 @@ export function useChatSessions() {
|
|
|
100
88
|
await inbox.call("archiveChat", [id, archived]);
|
|
101
89
|
}, [inbox]);
|
|
102
90
|
|
|
103
|
-
const reloadChatRuntime = useCallback(
|
|
104
|
-
async (chatId: string, userAgentId: string) => {
|
|
105
|
-
const result = await inbox.call<{ userAgentId: string } | null>(
|
|
106
|
-
"chatReloadRuntime",
|
|
107
|
-
[chatId, userAgentId],
|
|
108
|
-
);
|
|
109
|
-
if (!result) throw new Error("Session is unavailable");
|
|
110
|
-
return result;
|
|
111
|
-
},
|
|
112
|
-
[inbox],
|
|
113
|
-
);
|
|
114
|
-
|
|
115
91
|
return {
|
|
116
92
|
chats: inbox.state?.chats ?? snapshot ?? [],
|
|
117
93
|
synced: inbox.state !== undefined || snapshot !== undefined,
|
|
@@ -123,8 +99,9 @@ export function useChatSessions() {
|
|
|
123
99
|
deleteChat,
|
|
124
100
|
pinChat,
|
|
125
101
|
archiveChat,
|
|
126
|
-
reloadChatRuntime,
|
|
127
102
|
};
|
|
128
103
|
}
|
|
129
104
|
|
|
130
|
-
export type ChatSessions = ReturnType<
|
|
105
|
+
export type ChatSessions<Session> = ReturnType<
|
|
106
|
+
typeof useChatSessions<Session>
|
|
107
|
+
>;
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,15 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import { nanoid } from "nanoid";
|
|
3
3
|
import type { ChatStatus, FileUIPart, UIMessage } from "ai";
|
|
4
4
|
import type { AgentToolRunState } from "agents";
|
|
5
|
+
import { MessageType } from "agents/chat";
|
|
5
6
|
import { getAgentMessages, useAgentChat } from "agents/chat/react";
|
|
6
7
|
import { useAgent, useAgentToolEvents } from "agents/react";
|
|
7
8
|
import {
|
|
8
9
|
type MessageDelivery,
|
|
9
10
|
type MessageDispatchReceipt,
|
|
11
|
+
type RequestedCapability,
|
|
12
|
+
type RuntimeAssemblyView,
|
|
13
|
+
type RuntimeConfigUpdateResult,
|
|
10
14
|
type RuntimeQueuedSubmission,
|
|
11
15
|
type RuntimeState,
|
|
12
16
|
} from "@springbrand/agent-runtime/contracts";
|
|
@@ -14,6 +18,7 @@ import { createSafeUIMessageAgentConnection } from "./ui-message-stream-guard";
|
|
|
14
18
|
import { CURRENT_INBOX_AGENT_PATH } from "./chat-sessions";
|
|
15
19
|
|
|
16
20
|
type ChatMessageMetadata = Record<string, unknown> & {
|
|
21
|
+
requestedCapabilities?: readonly RequestedCapability[];
|
|
17
22
|
turnStatus?: string;
|
|
18
23
|
};
|
|
19
24
|
type ChatMessage = UIMessage<ChatMessageMetadata>;
|
|
@@ -27,7 +32,7 @@ type ChatRuntimeLoadState =
|
|
|
27
32
|
| { status: "idle"; available: false }
|
|
28
33
|
| {
|
|
29
34
|
status: "loading";
|
|
30
|
-
phase: "config" | "
|
|
35
|
+
phase: "config" | "assembly" | "mcp" | "pi";
|
|
31
36
|
available: boolean;
|
|
32
37
|
startedAt: number;
|
|
33
38
|
updatedAt: number;
|
|
@@ -40,7 +45,7 @@ type ChatRuntimeLoadState =
|
|
|
40
45
|
}
|
|
41
46
|
| {
|
|
42
47
|
status: "error";
|
|
43
|
-
phase: "config" | "
|
|
48
|
+
phase: "config" | "assembly" | "mcp" | "pi";
|
|
44
49
|
available: boolean;
|
|
45
50
|
startedAt: number;
|
|
46
51
|
failedAt: number;
|
|
@@ -67,6 +72,10 @@ type ChatQueuedSubmission = {
|
|
|
67
72
|
|
|
68
73
|
type ChatTurn = {
|
|
69
74
|
activeSubmissionId?: string;
|
|
75
|
+
activeRequestId?: string;
|
|
76
|
+
recoveryAttempt?: number;
|
|
77
|
+
recoveryMax?: number;
|
|
78
|
+
recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
|
|
70
79
|
steerable: boolean;
|
|
71
80
|
hasPendingSteer: boolean;
|
|
72
81
|
queued: ChatQueuedSubmission[];
|
|
@@ -78,12 +87,47 @@ export interface ChatRuntime {
|
|
|
78
87
|
runtimeLoad: ChatRuntimeLoadState | undefined;
|
|
79
88
|
isStreaming: boolean;
|
|
80
89
|
error: string | Error | undefined;
|
|
81
|
-
sendText: (
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
sendText: (
|
|
91
|
+
text: string,
|
|
92
|
+
files?: readonly FileUIPart[],
|
|
93
|
+
capabilities?: readonly RequestedCapability[],
|
|
94
|
+
) => Promise<boolean>;
|
|
95
|
+
steerText: (
|
|
96
|
+
text: string,
|
|
97
|
+
files?: readonly FileUIPart[],
|
|
98
|
+
capabilities?: readonly RequestedCapability[],
|
|
99
|
+
) => Promise<boolean>;
|
|
100
|
+
enqueueText: (
|
|
101
|
+
text: string,
|
|
102
|
+
files?: readonly FileUIPart[],
|
|
103
|
+
capabilities?: readonly RequestedCapability[],
|
|
104
|
+
) => Promise<boolean>;
|
|
84
105
|
steerQueued: (submissionId: string) => Promise<boolean>;
|
|
85
106
|
cancelQueued: (submissionId: string) => Promise<boolean>;
|
|
86
107
|
stop: () => Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* 把用户对某张交互卡片的操作回写成那次工具调用的结果。
|
|
110
|
+
*
|
|
111
|
+
* 走的是这条已开的 WS 连接(`agent.call`),不是 HTTP —— ack 与随后流下来的
|
|
112
|
+
* `tool-output-available` chunk 同序到达,UI 状态不会倒挂。
|
|
113
|
+
*/
|
|
114
|
+
respondToolInteraction: (
|
|
115
|
+
toolCallId: string,
|
|
116
|
+
response: unknown,
|
|
117
|
+
) => Promise<boolean>;
|
|
118
|
+
/**
|
|
119
|
+
* 读取该 Session facet 当前真正装配出来的 Runtime。
|
|
120
|
+
*
|
|
121
|
+
* 复用这条已开的 WS 连接,调用方不必为一个只读控制面方法另建 `useAgent`。
|
|
122
|
+
*
|
|
123
|
+
* 与其它动作不同,失败会原样抛出:这是读取而不是提交,调用方需要知道是
|
|
124
|
+
* 装配未就绪还是连接断了,收敛成 `undefined` 会让调试面板无从显示原因。
|
|
125
|
+
*/
|
|
126
|
+
getRuntimeAssembly: () => Promise<RuntimeAssemblyView>;
|
|
127
|
+
updateConfig: <Change = unknown>(
|
|
128
|
+
command: unknown,
|
|
129
|
+
) => Promise<RuntimeConfigUpdateResult<Change>>;
|
|
130
|
+
reloadRuntime: () => Promise<void>;
|
|
87
131
|
regenerate: () => void;
|
|
88
132
|
canRetry: boolean;
|
|
89
133
|
agentToolRuns: AgentToolRunState<ChatMessage["parts"][number]>[];
|
|
@@ -100,6 +144,7 @@ export interface ChatRuntime {
|
|
|
100
144
|
type SharedChatConnectionOptions = {
|
|
101
145
|
host?: string;
|
|
102
146
|
credentials?: RequestCredentials;
|
|
147
|
+
protocols?: string | string[];
|
|
103
148
|
inboxAgent?: string;
|
|
104
149
|
sessionAgent?: string;
|
|
105
150
|
};
|
|
@@ -164,10 +209,18 @@ function markChatTurnPhase(
|
|
|
164
209
|
function createChatMessage(
|
|
165
210
|
text: string,
|
|
166
211
|
files?: readonly FileUIPart[],
|
|
212
|
+
capabilities: readonly RequestedCapability[] = [],
|
|
167
213
|
): ChatMessage | null {
|
|
168
214
|
const normalizedText = text.trim();
|
|
169
215
|
if (!normalizedText && !files?.length) return null;
|
|
170
216
|
const createdAt = Date.now();
|
|
217
|
+
const seen = new Set<string>();
|
|
218
|
+
const requestedCapabilities = capabilities.filter((capability) => {
|
|
219
|
+
const key = `${capability.kind}:${capability.name}`;
|
|
220
|
+
if (seen.has(key)) return false;
|
|
221
|
+
seen.add(key);
|
|
222
|
+
return true;
|
|
223
|
+
});
|
|
171
224
|
return {
|
|
172
225
|
id: nanoid(),
|
|
173
226
|
role: "user",
|
|
@@ -181,6 +234,7 @@ function createChatMessage(
|
|
|
181
234
|
createdAt,
|
|
182
235
|
authorDisplayName: "You",
|
|
183
236
|
messageSource: "Web",
|
|
237
|
+
...(requestedCapabilities.length > 0 ? { requestedCapabilities } : {}),
|
|
184
238
|
},
|
|
185
239
|
};
|
|
186
240
|
}
|
|
@@ -246,6 +300,7 @@ export function useUniversalAgentChat(
|
|
|
246
300
|
? { basePath: connection.basePath ?? CURRENT_INBOX_AGENT_PATH }
|
|
247
301
|
: { name: connection.inboxName }),
|
|
248
302
|
...(connection.host === undefined ? {} : { host: connection.host }),
|
|
303
|
+
...(connection.protocols === undefined ? {} : { protocols: connection.protocols }),
|
|
249
304
|
sub: [{
|
|
250
305
|
agent: connection.sessionAgent ?? "UniversalAgent",
|
|
251
306
|
name: chatId,
|
|
@@ -270,10 +325,11 @@ export function useUniversalAgentChat(
|
|
|
270
325
|
status: sdkStatus,
|
|
271
326
|
isStreaming,
|
|
272
327
|
error,
|
|
328
|
+
clearError,
|
|
273
329
|
// autonomous responses 纪律(CF 文档采纳 2026-07-05):框架已提供三个流态标志,
|
|
274
330
|
// 透出供 UI 区分「服务端主动流 vs 用户发起流」+「恢复态」(与 stall 看门狗配套)。
|
|
275
331
|
isServerStreaming,
|
|
276
|
-
isRecovering,
|
|
332
|
+
isRecovering: sdkIsRecovering,
|
|
277
333
|
isToolContinuation,
|
|
278
334
|
connectionError,
|
|
279
335
|
} = useAgentChat<RuntimeState, ChatMessage>({
|
|
@@ -286,15 +342,28 @@ export function useUniversalAgentChat(
|
|
|
286
342
|
syncMessagesToServer: false,
|
|
287
343
|
throttle: 100,
|
|
288
344
|
});
|
|
345
|
+
// The SDK keeps its advisory recovery flag until terminal settlement; a
|
|
346
|
+
// live server stream proves the recovered Turn is executing again.
|
|
347
|
+
const isRecovering = sdkIsRecovering && !isServerStreaming;
|
|
289
348
|
const visibleMessages = useMemo(
|
|
290
349
|
() => mergeOptimisticMessages(messages, optimisticMessages),
|
|
291
350
|
[messages, optimisticMessages],
|
|
292
351
|
);
|
|
293
352
|
const authoritativeTurn = facetState?.turn;
|
|
294
353
|
const stoppedByUser = error?.message === USER_STOP_REASON;
|
|
295
|
-
const
|
|
354
|
+
const latestMessage = visibleMessages.at(-1);
|
|
355
|
+
const latestTurnStatus = latestMessage?.role === "assistant"
|
|
356
|
+
? latestMessage.metadata?.turnStatus
|
|
357
|
+
: undefined;
|
|
358
|
+
const hasTerminalMessage = latestTurnStatus === "completed" ||
|
|
359
|
+
latestTurnStatus === "error" ||
|
|
360
|
+
latestTurnStatus === "aborted" ||
|
|
361
|
+
latestTurnStatus === "skipped";
|
|
362
|
+
const normalizedSdkStatus = sdkStatus === "error" &&
|
|
363
|
+
(stoppedByUser || hasTerminalMessage)
|
|
296
364
|
? "ready"
|
|
297
365
|
: sdkStatus;
|
|
366
|
+
const sdkError = stoppedByUser || hasTerminalMessage ? undefined : error;
|
|
298
367
|
// RPC admissions bypass useChat's request lifecycle. Project the durable
|
|
299
368
|
// Turn here so presentation stays correct without a second send path.
|
|
300
369
|
const authoritativeStatus =
|
|
@@ -392,12 +461,17 @@ export function useUniversalAgentChat(
|
|
|
392
461
|
queued,
|
|
393
462
|
};
|
|
394
463
|
}, [authoritativeTurn, visibleMessages, optimisticQueued]);
|
|
395
|
-
const
|
|
464
|
+
const sdkTurnActive =
|
|
396
465
|
status === "submitted" ||
|
|
397
466
|
status === "streaming" ||
|
|
398
467
|
isServerStreaming ||
|
|
399
468
|
isRecovering ||
|
|
400
469
|
isToolContinuation;
|
|
470
|
+
const pendingLocalTurn = optimisticMessages.length > 0 &&
|
|
471
|
+
(status === "submitted" || status === "streaming");
|
|
472
|
+
const turnActive = Boolean(turn?.activeSubmissionId) ||
|
|
473
|
+
pendingLocalTurn ||
|
|
474
|
+
(authoritativeTurn === undefined && sdkTurnActive);
|
|
401
475
|
const canSteer = turnActive &&
|
|
402
476
|
(!turn?.activeSubmissionId || turn.steerable);
|
|
403
477
|
|
|
@@ -426,6 +500,7 @@ export function useUniversalAgentChat(
|
|
|
426
500
|
projection: "message" | "queue",
|
|
427
501
|
): Promise<boolean> => {
|
|
428
502
|
setDispatchError(undefined);
|
|
503
|
+
clearError();
|
|
429
504
|
failedDispatchRef.current = undefined;
|
|
430
505
|
setCanRetry(false);
|
|
431
506
|
if (projection === "message" && delivery === "enqueue") {
|
|
@@ -441,6 +516,7 @@ export function useUniversalAgentChat(
|
|
|
441
516
|
};
|
|
442
517
|
}
|
|
443
518
|
if (projection === "message") {
|
|
519
|
+
activeSubmissionIdRef.current = undefined;
|
|
444
520
|
setOptimisticMessages((current) =>
|
|
445
521
|
current.some(({ id }) => id === message.id)
|
|
446
522
|
? current
|
|
@@ -517,13 +593,14 @@ export function useUniversalAgentChat(
|
|
|
517
593
|
);
|
|
518
594
|
return false;
|
|
519
595
|
}
|
|
520
|
-
}, [chatId, dispatchMessage]);
|
|
596
|
+
}, [chatId, clearError, dispatchMessage]);
|
|
521
597
|
|
|
522
598
|
const sendText = useCallback(async (
|
|
523
599
|
text: string,
|
|
524
600
|
files?: readonly FileUIPart[],
|
|
601
|
+
capabilities?: readonly RequestedCapability[],
|
|
525
602
|
): Promise<boolean> => {
|
|
526
|
-
const message = createChatMessage(text, files);
|
|
603
|
+
const message = createChatMessage(text, files, capabilities);
|
|
527
604
|
return message
|
|
528
605
|
? submitMessage(message, "enqueue", "message")
|
|
529
606
|
: false;
|
|
@@ -532,21 +609,28 @@ export function useUniversalAgentChat(
|
|
|
532
609
|
const dispatchText = useCallback(async (
|
|
533
610
|
text: string,
|
|
534
611
|
files: readonly FileUIPart[] | undefined,
|
|
612
|
+
capabilities: readonly RequestedCapability[] | undefined,
|
|
535
613
|
delivery: MessageDelivery,
|
|
536
614
|
projection: "message" | "queue",
|
|
537
615
|
): Promise<boolean> => {
|
|
538
|
-
const message = createChatMessage(text, files);
|
|
616
|
+
const message = createChatMessage(text, files, capabilities);
|
|
539
617
|
return message ? submitMessage(message, delivery, projection) : false;
|
|
540
618
|
}, [submitMessage]);
|
|
541
619
|
|
|
542
620
|
const steerText = useCallback(
|
|
543
|
-
(
|
|
544
|
-
|
|
621
|
+
(
|
|
622
|
+
text: string,
|
|
623
|
+
files?: readonly FileUIPart[],
|
|
624
|
+
capabilities?: readonly RequestedCapability[],
|
|
625
|
+
) => dispatchText(text, files, capabilities, "steer", "message"),
|
|
545
626
|
[dispatchText],
|
|
546
627
|
);
|
|
547
628
|
const enqueueText = useCallback(
|
|
548
|
-
(
|
|
549
|
-
|
|
629
|
+
(
|
|
630
|
+
text: string,
|
|
631
|
+
files?: readonly FileUIPart[],
|
|
632
|
+
capabilities?: readonly RequestedCapability[],
|
|
633
|
+
) => dispatchText(text, files, capabilities, "enqueue", "queue"),
|
|
550
634
|
[dispatchText],
|
|
551
635
|
);
|
|
552
636
|
const steerQueued = useCallback(async (submissionId: string) => {
|
|
@@ -597,22 +681,37 @@ export function useUniversalAgentChat(
|
|
|
597
681
|
}, []);
|
|
598
682
|
|
|
599
683
|
const stop = useCallback(async () => {
|
|
684
|
+
const requestId = authoritativeTurn?.activeRequestId;
|
|
600
685
|
const submissionId = authoritativeTurn?.activeSubmissionId ??
|
|
601
686
|
activeSubmissionIdRef.current;
|
|
602
|
-
if (!submissionId) return;
|
|
603
687
|
setDispatchError(undefined);
|
|
604
688
|
try {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
689
|
+
if (requestId) {
|
|
690
|
+
agentRef.current.send(JSON.stringify({
|
|
691
|
+
type: MessageType.CF_AGENT_CHAT_REQUEST_CANCEL,
|
|
692
|
+
id: requestId,
|
|
693
|
+
}));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const result = submissionId
|
|
697
|
+
? await agentRef.current.call<{ ok: boolean }>(
|
|
698
|
+
"cancelSubmissionById",
|
|
699
|
+
[submissionId, USER_STOP_REASON],
|
|
700
|
+
)
|
|
701
|
+
: await agentRef.current.call<{ ok: boolean }>(
|
|
702
|
+
"stopTurn",
|
|
703
|
+
[undefined, USER_STOP_REASON],
|
|
704
|
+
);
|
|
609
705
|
if (!result.ok) setDispatchError("The active Turn could not be stopped");
|
|
610
706
|
} catch (cause) {
|
|
611
707
|
setDispatchError(
|
|
612
708
|
cause instanceof Error ? cause.message : String(cause),
|
|
613
709
|
);
|
|
614
710
|
}
|
|
615
|
-
}, [
|
|
711
|
+
}, [
|
|
712
|
+
authoritativeTurn?.activeRequestId,
|
|
713
|
+
authoritativeTurn?.activeSubmissionId,
|
|
714
|
+
]);
|
|
616
715
|
|
|
617
716
|
const retry = useCallback(() => {
|
|
618
717
|
const failed = failedDispatchRef.current;
|
|
@@ -624,12 +723,55 @@ export function useUniversalAgentChat(
|
|
|
624
723
|
);
|
|
625
724
|
}, [submitMessage]);
|
|
626
725
|
|
|
726
|
+
// 交互卡片的写回口。失败只返回 false —— 调用方(块组件)自己决定怎么提示;
|
|
727
|
+
// 权威状态永远来自 part 的 state,不来自这里的返回值。
|
|
728
|
+
const respondToolInteraction = useCallback(async (
|
|
729
|
+
toolCallId: string,
|
|
730
|
+
response: unknown,
|
|
731
|
+
): Promise<boolean> => {
|
|
732
|
+
try {
|
|
733
|
+
const receipt = await agentRef.current.call<{ ok: boolean }>(
|
|
734
|
+
"respondToolInteraction",
|
|
735
|
+
[toolCallId, response],
|
|
736
|
+
);
|
|
737
|
+
return receipt.ok;
|
|
738
|
+
} catch {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
}, []);
|
|
742
|
+
|
|
743
|
+
// 只读控制面:Runtime 尚未装配时由 Agent 侧 `ensureRuntimeReady` 负责等待,
|
|
744
|
+
// 这里不缓存结果 —— 换模型、加技能或 reload 之后调用方要拿到的是新装配。
|
|
745
|
+
const getRuntimeAssembly = useCallback(
|
|
746
|
+
() => agentRef.current.call<RuntimeAssemblyView>("getRuntimeAssembly", []),
|
|
747
|
+
[],
|
|
748
|
+
);
|
|
749
|
+
const updateConfig = useCallback(
|
|
750
|
+
<Change,>(command: unknown) =>
|
|
751
|
+
agentRef.current.call<RuntimeConfigUpdateResult<Change>>(
|
|
752
|
+
"updateConfig",
|
|
753
|
+
[command],
|
|
754
|
+
),
|
|
755
|
+
[],
|
|
756
|
+
);
|
|
757
|
+
const reloadRuntime = useCallback(
|
|
758
|
+
() => agentRef.current.call<void>(
|
|
759
|
+
"reloadRuntime",
|
|
760
|
+
[undefined, { force: true }],
|
|
761
|
+
),
|
|
762
|
+
[],
|
|
763
|
+
);
|
|
764
|
+
|
|
627
765
|
return {
|
|
766
|
+
respondToolInteraction,
|
|
767
|
+
getRuntimeAssembly,
|
|
768
|
+
updateConfig,
|
|
769
|
+
reloadRuntime,
|
|
628
770
|
messages: visibleMessages,
|
|
629
771
|
status,
|
|
630
772
|
runtimeLoad: facetState?.runtimeLoad,
|
|
631
773
|
isStreaming,
|
|
632
|
-
error: dispatchError ??
|
|
774
|
+
error: dispatchError ?? sdkError ??
|
|
633
775
|
connectionError ?? undefined,
|
|
634
776
|
sendText,
|
|
635
777
|
steerText,
|