@springbrand/chat-client 0.1.3-alpha.4 → 0.1.3-alpha.40
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 +7 -3
- package/src/index.ts +3 -0
- package/src/use-universal-agent-chat.ts +269 -53
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.40",
|
|
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.22.0",
|
|
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.51"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
package/src/chat-sessions.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useAgent } from "agents/react";
|
|
3
3
|
|
|
4
|
-
interface InboxState<Session> {
|
|
4
|
+
export interface InboxState<Session> {
|
|
5
5
|
chats: Session[];
|
|
6
6
|
workspaceRev?: number;
|
|
7
7
|
}
|
|
@@ -33,8 +33,11 @@ export async function loadChatSessions<Session>(
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Browser client for the user Inbox. Routing and presentation stay with the host UI. */
|
|
36
|
-
export function useChatSessions<
|
|
37
|
-
|
|
36
|
+
export function useChatSessions<
|
|
37
|
+
Session,
|
|
38
|
+
State extends InboxState<Session> = InboxState<Session>,
|
|
39
|
+
>(options: ChatSessionsOptions = {}) {
|
|
40
|
+
const inbox = useAgent<State>({
|
|
38
41
|
agent: "Inbox",
|
|
39
42
|
basePath: CURRENT_INBOX_AGENT_PATH,
|
|
40
43
|
...(options.protocols === undefined ? {} : { protocols: options.protocols }),
|
|
@@ -93,6 +96,7 @@ export function useChatSessions<Session>(options: ChatSessionsOptions = {}) {
|
|
|
93
96
|
synced: inbox.state !== undefined || snapshot !== undefined,
|
|
94
97
|
connected: inbox.identified,
|
|
95
98
|
workspaceRev: inbox.state?.workspaceRev ?? 0,
|
|
99
|
+
inboxState: inbox.state,
|
|
96
100
|
createChat,
|
|
97
101
|
forkChat,
|
|
98
102
|
renameChat,
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export {
|
|
2
|
+
UniversalAgentChatProvider,
|
|
2
3
|
useUniversalAgentChat,
|
|
3
4
|
type AgentToolRun,
|
|
5
|
+
type ChatConnectionStatus,
|
|
4
6
|
type ChatConnectionOptions,
|
|
5
7
|
type ChatRuntime,
|
|
6
8
|
} from "./use-universal-agent-chat";
|
|
@@ -21,4 +23,5 @@ export {
|
|
|
21
23
|
useChatSessions,
|
|
22
24
|
type ChatSessionsOptions,
|
|
23
25
|
type ChatSessions,
|
|
26
|
+
type InboxState,
|
|
24
27
|
} from "./chat-sessions";
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createElement,
|
|
4
|
+
Suspense,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react";
|
|
2
13
|
import { nanoid } from "nanoid";
|
|
3
14
|
import type { ChatStatus, FileUIPart, UIMessage } from "ai";
|
|
4
15
|
import type { AgentToolRunState } from "agents";
|
|
16
|
+
import type { AgentConnectionError } from "agents/client";
|
|
5
17
|
import { MessageType } from "agents/chat";
|
|
6
18
|
import { getAgentMessages, useAgentChat } from "agents/chat/react";
|
|
7
19
|
import { useAgent, useAgentToolEvents } from "agents/react";
|
|
8
20
|
import {
|
|
21
|
+
type ApprovalDecision,
|
|
9
22
|
type MessageDelivery,
|
|
10
23
|
type MessageDispatchReceipt,
|
|
11
24
|
type RequestedCapability,
|
|
@@ -18,10 +31,19 @@ import { createSafeUIMessageAgentConnection } from "./ui-message-stream-guard";
|
|
|
18
31
|
import { CURRENT_INBOX_AGENT_PATH } from "./chat-sessions";
|
|
19
32
|
|
|
20
33
|
type ChatMessageMetadata = Record<string, unknown> & {
|
|
34
|
+
completedAt?: number;
|
|
35
|
+
error?: string;
|
|
36
|
+
interruptedByUser?: boolean;
|
|
21
37
|
requestedCapabilities?: readonly RequestedCapability[];
|
|
38
|
+
turnId?: string;
|
|
22
39
|
turnStatus?: string;
|
|
23
40
|
};
|
|
24
41
|
type ChatMessage = UIMessage<ChatMessageMetadata>;
|
|
42
|
+
export type ChatConnectionStatus =
|
|
43
|
+
| "connecting"
|
|
44
|
+
| "connected"
|
|
45
|
+
| "rejected"
|
|
46
|
+
| "error";
|
|
25
47
|
type FailedDispatch = {
|
|
26
48
|
message: ChatMessage;
|
|
27
49
|
delivery: MessageDelivery;
|
|
@@ -84,6 +106,8 @@ type ChatTurn = {
|
|
|
84
106
|
export interface ChatRuntime {
|
|
85
107
|
messages: ChatMessage[];
|
|
86
108
|
status: ChatStatus;
|
|
109
|
+
connectionStatus: ChatConnectionStatus;
|
|
110
|
+
connectionError: AgentConnectionError | undefined;
|
|
87
111
|
runtimeLoad: ChatRuntimeLoadState | undefined;
|
|
88
112
|
isStreaming: boolean;
|
|
89
113
|
error: string | Error | undefined;
|
|
@@ -115,6 +139,10 @@ export interface ChatRuntime {
|
|
|
115
139
|
toolCallId: string,
|
|
116
140
|
response: unknown,
|
|
117
141
|
) => Promise<boolean>;
|
|
142
|
+
decideApproval: (
|
|
143
|
+
executionId: string,
|
|
144
|
+
decision: ApprovalDecision,
|
|
145
|
+
) => Promise<{ ok: boolean }>;
|
|
118
146
|
/**
|
|
119
147
|
* 读取该 Session facet 当前真正装配出来的 Runtime。
|
|
120
148
|
*
|
|
@@ -124,6 +152,10 @@ export interface ChatRuntime {
|
|
|
124
152
|
* 装配未就绪还是连接断了,收敛成 `undefined` 会让调试面板无从显示原因。
|
|
125
153
|
*/
|
|
126
154
|
getRuntimeAssembly: () => Promise<RuntimeAssemblyView>;
|
|
155
|
+
getRuntimeBinding: <Binding = unknown>() => Promise<Binding>;
|
|
156
|
+
readChatMetadata: <Metadata = unknown>() => Promise<Metadata>;
|
|
157
|
+
renameChat: <Metadata = unknown>(title: string) => Promise<Metadata>;
|
|
158
|
+
rebindRuntime: <Input = unknown, Result = unknown>(input: Input) => Promise<Result>;
|
|
127
159
|
updateConfig: <Change = unknown>(
|
|
128
160
|
command: unknown,
|
|
129
161
|
) => Promise<RuntimeConfigUpdateResult<Change>>;
|
|
@@ -145,8 +177,15 @@ type SharedChatConnectionOptions = {
|
|
|
145
177
|
host?: string;
|
|
146
178
|
credentials?: RequestCredentials;
|
|
147
179
|
protocols?: string | string[];
|
|
180
|
+
onMessage?: (message: MessageEvent) => void;
|
|
148
181
|
inboxAgent?: string;
|
|
149
182
|
sessionAgent?: string;
|
|
183
|
+
/** Skip history loading when the caller has just created this Chat. */
|
|
184
|
+
skipInitialMessages?: boolean;
|
|
185
|
+
/** Host projection used only until the selected Session sends its first state. */
|
|
186
|
+
initialTurnActive?: boolean;
|
|
187
|
+
/** Optional Agent RPC method injected by the Host to receive Turn timings. */
|
|
188
|
+
performanceRpc?: string;
|
|
150
189
|
};
|
|
151
190
|
|
|
152
191
|
export type ChatConnectionOptions = SharedChatConnectionOptions & (
|
|
@@ -154,9 +193,61 @@ export type ChatConnectionOptions = SharedChatConnectionOptions & (
|
|
|
154
193
|
| { inboxName?: never; basePath?: string }
|
|
155
194
|
);
|
|
156
195
|
|
|
196
|
+
type SessionAgentConnection = ReturnType<typeof useAgent<RuntimeState>>;
|
|
197
|
+
type UniversalAgentChatContextValue = {
|
|
198
|
+
chatId: string;
|
|
199
|
+
connection: ChatConnectionOptions;
|
|
200
|
+
agent: SessionAgentConnection;
|
|
201
|
+
chatAgent: SessionAgentConnection;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const UniversalAgentChatContext = createContext<UniversalAgentChatContextValue | null>(null);
|
|
205
|
+
|
|
206
|
+
export function UniversalAgentChatProvider({
|
|
207
|
+
chatId,
|
|
208
|
+
connection = {},
|
|
209
|
+
fallback = null,
|
|
210
|
+
children,
|
|
211
|
+
}: {
|
|
212
|
+
chatId: string;
|
|
213
|
+
connection?: ChatConnectionOptions;
|
|
214
|
+
fallback?: ReactNode;
|
|
215
|
+
children: ReactNode;
|
|
216
|
+
}) {
|
|
217
|
+
const inboxAgent = connection.inboxAgent ?? "Inbox";
|
|
218
|
+
const agent = useAgent<RuntimeState>({
|
|
219
|
+
agent: inboxAgent,
|
|
220
|
+
...(connection.inboxName === undefined
|
|
221
|
+
? { basePath: connection.basePath ?? CURRENT_INBOX_AGENT_PATH }
|
|
222
|
+
: { name: connection.inboxName }),
|
|
223
|
+
...(connection.host === undefined ? {} : { host: connection.host }),
|
|
224
|
+
...(connection.protocols === undefined ? {} : { protocols: connection.protocols }),
|
|
225
|
+
...(connection.onMessage === undefined ? {} : { onMessage: connection.onMessage }),
|
|
226
|
+
connectionTimeout: 6_000,
|
|
227
|
+
sub: [{
|
|
228
|
+
agent: connection.sessionAgent ?? "UniversalAgent",
|
|
229
|
+
name: chatId,
|
|
230
|
+
}],
|
|
231
|
+
});
|
|
232
|
+
const chatAgent = useMemo(
|
|
233
|
+
() => createSafeUIMessageAgentConnection(agent),
|
|
234
|
+
[agent],
|
|
235
|
+
);
|
|
236
|
+
const value = useMemo(
|
|
237
|
+
() => ({ chatId, connection, agent, chatAgent }),
|
|
238
|
+
[agent, chatAgent, chatId, connection],
|
|
239
|
+
);
|
|
240
|
+
return createElement(
|
|
241
|
+
UniversalAgentChatContext.Provider,
|
|
242
|
+
{ value },
|
|
243
|
+
createElement(Suspense, { fallback }, children),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
157
247
|
type ChatTurnTiming = {
|
|
158
248
|
chatId: string;
|
|
159
249
|
messageId: string;
|
|
250
|
+
submissionId?: string;
|
|
160
251
|
baselineAssistantIds: Set<string>;
|
|
161
252
|
click: number;
|
|
162
253
|
rpcReceipt?: number;
|
|
@@ -165,6 +256,37 @@ type ChatTurnTiming = {
|
|
|
165
256
|
ready?: number;
|
|
166
257
|
};
|
|
167
258
|
|
|
259
|
+
type ClientPerformanceMetrics = {
|
|
260
|
+
clickToRpcMs: number;
|
|
261
|
+
rpcToStreamMs: number;
|
|
262
|
+
streamToFirstTextMs?: number;
|
|
263
|
+
firstTextToReadyMs?: number;
|
|
264
|
+
ttftMs?: number;
|
|
265
|
+
totalMs: number;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export function clientPerformanceMetrics(
|
|
269
|
+
timing: ChatTurnTiming,
|
|
270
|
+
): ClientPerformanceMetrics | null {
|
|
271
|
+
if (
|
|
272
|
+
timing.rpcReceipt === undefined ||
|
|
273
|
+
timing.streamStart === undefined ||
|
|
274
|
+
timing.ready === undefined
|
|
275
|
+
) return null;
|
|
276
|
+
return {
|
|
277
|
+
clickToRpcMs: timing.rpcReceipt - timing.click,
|
|
278
|
+
rpcToStreamMs: timing.streamStart - timing.rpcReceipt,
|
|
279
|
+
...(timing.firstText === undefined
|
|
280
|
+
? {}
|
|
281
|
+
: {
|
|
282
|
+
streamToFirstTextMs: timing.firstText - timing.streamStart,
|
|
283
|
+
firstTextToReadyMs: timing.ready - timing.firstText,
|
|
284
|
+
ttftMs: timing.firstText - timing.click,
|
|
285
|
+
}),
|
|
286
|
+
totalMs: timing.ready - timing.click,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
168
290
|
const CHAT_TURN_SEGMENTS = {
|
|
169
291
|
rpcReceipt: ["click", "universal-agent.chat.click_to_rpc_receipt"],
|
|
170
292
|
streamStart: [
|
|
@@ -281,10 +403,12 @@ function loadInitialMessages(
|
|
|
281
403
|
* sub 数组由客户端 kebab 化;服务端按 ctx.exports 反解回 CamelCase className,
|
|
282
404
|
* 与 Inbox.onBeforeSubAgent 的严格门卫(hasSubAgent)对齐。
|
|
283
405
|
*/
|
|
284
|
-
export function useUniversalAgentChat(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
406
|
+
export function useUniversalAgentChat(): ChatRuntime {
|
|
407
|
+
const context = useContext(UniversalAgentChatContext);
|
|
408
|
+
if (!context) {
|
|
409
|
+
throw new Error("useUniversalAgentChat must be used within UniversalAgentChatProvider");
|
|
410
|
+
}
|
|
411
|
+
const { chatId, connection, agent, chatAgent } = context;
|
|
288
412
|
const [optimisticMessages, setOptimisticMessages] =
|
|
289
413
|
useState<ChatMessage[]>([]);
|
|
290
414
|
const [optimisticQueued, setOptimisticQueued] =
|
|
@@ -292,25 +416,7 @@ export function useUniversalAgentChat(
|
|
|
292
416
|
const [dispatchError, setDispatchError] = useState<string>();
|
|
293
417
|
const [canRetry, setCanRetry] = useState(false);
|
|
294
418
|
const failedDispatchRef = useRef<FailedDispatch | undefined>(undefined);
|
|
295
|
-
const
|
|
296
|
-
const inboxAgent = connection.inboxAgent ?? "Inbox";
|
|
297
|
-
const agent = useAgent<RuntimeState>({
|
|
298
|
-
agent: inboxAgent,
|
|
299
|
-
...(connection.inboxName === undefined
|
|
300
|
-
? { basePath: connection.basePath ?? CURRENT_INBOX_AGENT_PATH }
|
|
301
|
-
: { name: connection.inboxName }),
|
|
302
|
-
...(connection.host === undefined ? {} : { host: connection.host }),
|
|
303
|
-
...(connection.protocols === undefined ? {} : { protocols: connection.protocols }),
|
|
304
|
-
sub: [{
|
|
305
|
-
agent: connection.sessionAgent ?? "UniversalAgent",
|
|
306
|
-
name: chatId,
|
|
307
|
-
}],
|
|
308
|
-
});
|
|
309
|
-
const chatAgent = useMemo(
|
|
310
|
-
() => createSafeUIMessageAgentConnection(agent),
|
|
311
|
-
[agent],
|
|
312
|
-
);
|
|
313
|
-
|
|
419
|
+
const stopRevisionRef = useRef(0);
|
|
314
420
|
// 轮询→推送:这条 chat 连接连的是 UniversalAgent facet,`agent.state` 即 facet 的
|
|
315
421
|
// AgentState 广播(useAgent 内部 useState,收到 cf_agent_state 即 re-render)。待批项
|
|
316
422
|
// 随它推来 —— 审批读侧复用这条已开的连接,不另开第二条 WebSocket。三态(原则 VII):
|
|
@@ -326,22 +432,28 @@ export function useUniversalAgentChat(
|
|
|
326
432
|
isStreaming,
|
|
327
433
|
error,
|
|
328
434
|
clearError,
|
|
435
|
+
setMessages,
|
|
436
|
+
stop: stopChat,
|
|
329
437
|
// autonomous responses 纪律(CF 文档采纳 2026-07-05):框架已提供三个流态标志,
|
|
330
438
|
// 透出供 UI 区分「服务端主动流 vs 用户发起流」+「恢复态」(与 stall 看门狗配套)。
|
|
331
439
|
isServerStreaming,
|
|
332
|
-
isRecovering,
|
|
440
|
+
isRecovering: sdkIsRecovering,
|
|
333
441
|
isToolContinuation,
|
|
334
|
-
connectionError,
|
|
442
|
+
connectionError: sdkConnectionError,
|
|
335
443
|
} = useAgentChat<RuntimeState, ChatMessage>({
|
|
336
444
|
agent: chatAgent,
|
|
337
|
-
getInitialMessages:
|
|
338
|
-
|
|
445
|
+
getInitialMessages: connection.skipInitialMessages
|
|
446
|
+
? null
|
|
447
|
+
: ({ url }) => loadInitialMessages(url, connection.credentials),
|
|
339
448
|
...(connection.credentials === undefined
|
|
340
449
|
? {}
|
|
341
450
|
: { credentials: connection.credentials }),
|
|
342
451
|
syncMessagesToServer: false,
|
|
343
|
-
throttle:
|
|
452
|
+
throttle: 16,
|
|
344
453
|
});
|
|
454
|
+
// The SDK keeps its advisory recovery flag until terminal settlement; a
|
|
455
|
+
// live server stream proves the recovered Turn is executing again.
|
|
456
|
+
const isRecovering = sdkIsRecovering && !isServerStreaming;
|
|
345
457
|
const visibleMessages = useMemo(
|
|
346
458
|
() => mergeOptimisticMessages(messages, optimisticMessages),
|
|
347
459
|
[messages, optimisticMessages],
|
|
@@ -361,6 +473,18 @@ export function useUniversalAgentChat(
|
|
|
361
473
|
? "ready"
|
|
362
474
|
: sdkStatus;
|
|
363
475
|
const sdkError = stoppedByUser || hasTerminalMessage ? undefined : error;
|
|
476
|
+
const connectionError = sdkConnectionError ?? undefined;
|
|
477
|
+
const connectionStatus: ChatConnectionStatus = connectionError
|
|
478
|
+
? connectionError.code === 1008 ? "rejected" : "error"
|
|
479
|
+
: agent.identified ? "connected" : "connecting";
|
|
480
|
+
const runtimeError = dispatchError ?? sdkError ?? connectionError;
|
|
481
|
+
const runtimeErrorMessage = runtimeError instanceof Error
|
|
482
|
+
? runtimeError.message
|
|
483
|
+
: runtimeError;
|
|
484
|
+
const presentationError = latestTurnStatus === "error" &&
|
|
485
|
+
latestMessage?.metadata?.error === runtimeErrorMessage
|
|
486
|
+
? undefined
|
|
487
|
+
: runtimeError;
|
|
364
488
|
// RPC admissions bypass useChat's request lifecycle. Project the durable
|
|
365
489
|
// Turn here so presentation stays correct without a second send path.
|
|
366
490
|
const authoritativeStatus =
|
|
@@ -372,9 +496,12 @@ export function useUniversalAgentChat(
|
|
|
372
496
|
? "streaming"
|
|
373
497
|
: "submitted"
|
|
374
498
|
: normalizedSdkStatus;
|
|
499
|
+
const hydratingTurnActive = facetState === undefined &&
|
|
500
|
+
connection.initialTurnActive === true;
|
|
375
501
|
const status = dispatchError
|
|
376
502
|
? "error"
|
|
377
|
-
: optimisticMessages.length > 0
|
|
503
|
+
: (optimisticMessages.length > 0 || hydratingTurnActive) &&
|
|
504
|
+
authoritativeStatus === "ready"
|
|
378
505
|
? "submitted"
|
|
379
506
|
: authoritativeStatus;
|
|
380
507
|
const messagesRef = useRef(messages);
|
|
@@ -386,7 +513,8 @@ export function useUniversalAgentChat(
|
|
|
386
513
|
if (!timing) return;
|
|
387
514
|
const assistant = messages.find((message) =>
|
|
388
515
|
message.role === "assistant" &&
|
|
389
|
-
!timing.baselineAssistantIds.has(message.id)
|
|
516
|
+
!timing.baselineAssistantIds.has(message.id) &&
|
|
517
|
+
(!timing.submissionId || message.metadata?.turnId === timing.submissionId)
|
|
390
518
|
);
|
|
391
519
|
if (assistant) markChatTurnPhase(timing, "streamStart");
|
|
392
520
|
if (
|
|
@@ -398,9 +526,18 @@ export function useUniversalAgentChat(
|
|
|
398
526
|
}
|
|
399
527
|
if (status === "ready" && timing.streamStart !== undefined) {
|
|
400
528
|
if (timing.firstText !== undefined) markChatTurnPhase(timing, "ready");
|
|
529
|
+
else timing.ready ??= now();
|
|
401
530
|
turnTimingRef.current = null;
|
|
531
|
+
const metrics = clientPerformanceMetrics(timing);
|
|
532
|
+
if (connection.performanceRpc && timing.submissionId && metrics) {
|
|
533
|
+
void agentRef.current.call(connection.performanceRpc, [{
|
|
534
|
+
submissionId: timing.submissionId,
|
|
535
|
+
messageId: timing.messageId,
|
|
536
|
+
...metrics,
|
|
537
|
+
}]).catch(() => {});
|
|
538
|
+
}
|
|
402
539
|
}
|
|
403
|
-
}, [messages, status]);
|
|
540
|
+
}, [connection.performanceRpc, messages, status]);
|
|
404
541
|
|
|
405
542
|
useEffect(() => {
|
|
406
543
|
if (optimisticMessages.length === 0 || messages.length === 0) return;
|
|
@@ -466,7 +603,8 @@ export function useUniversalAgentChat(
|
|
|
466
603
|
isToolContinuation;
|
|
467
604
|
const pendingLocalTurn = optimisticMessages.length > 0 &&
|
|
468
605
|
(status === "submitted" || status === "streaming");
|
|
469
|
-
const turnActive =
|
|
606
|
+
const turnActive = hydratingTurnActive ||
|
|
607
|
+
Boolean(turn?.activeSubmissionId) ||
|
|
470
608
|
pendingLocalTurn ||
|
|
471
609
|
(authoritativeTurn === undefined && sdkTurnActive);
|
|
472
610
|
const canSteer = turnActive &&
|
|
@@ -485,17 +623,32 @@ export function useUniversalAgentChat(
|
|
|
485
623
|
const dispatchMessage = useCallback(async (
|
|
486
624
|
message: ChatMessage,
|
|
487
625
|
delivery: MessageDelivery,
|
|
488
|
-
): Promise<MessageDispatchReceipt> =>
|
|
489
|
-
agentRef.current.call<MessageDispatchReceipt>(
|
|
626
|
+
): Promise<MessageDispatchReceipt> => {
|
|
627
|
+
const dispatch = () => agentRef.current.call<MessageDispatchReceipt>(
|
|
490
628
|
"dispatchMessage",
|
|
491
629
|
[message, delivery],
|
|
492
|
-
)
|
|
630
|
+
);
|
|
631
|
+
try {
|
|
632
|
+
return await dispatch();
|
|
633
|
+
} catch (error) {
|
|
634
|
+
// The server deduplicates dispatches by message.id, so replay only the
|
|
635
|
+
// transport failures that mean admission may already have succeeded.
|
|
636
|
+
const retryable = error instanceof Error && (
|
|
637
|
+
/^RPC call to dispatchMessage timed out after \d+ms$/u.test(error.message) ||
|
|
638
|
+
(error.message === "Connection closed" && agentRef.current.shouldReconnect)
|
|
639
|
+
);
|
|
640
|
+
if (!retryable) throw error;
|
|
641
|
+
await agentRef.current.ready;
|
|
642
|
+
return dispatch();
|
|
643
|
+
}
|
|
644
|
+
}, []);
|
|
493
645
|
|
|
494
646
|
const submitMessage = useCallback(async (
|
|
495
647
|
message: ChatMessage,
|
|
496
648
|
delivery: MessageDelivery,
|
|
497
649
|
projection: "message" | "queue",
|
|
498
650
|
): Promise<boolean> => {
|
|
651
|
+
const stopRevision = stopRevisionRef.current;
|
|
499
652
|
setDispatchError(undefined);
|
|
500
653
|
clearError();
|
|
501
654
|
failedDispatchRef.current = undefined;
|
|
@@ -513,7 +666,6 @@ export function useUniversalAgentChat(
|
|
|
513
666
|
};
|
|
514
667
|
}
|
|
515
668
|
if (projection === "message") {
|
|
516
|
-
activeSubmissionIdRef.current = undefined;
|
|
517
669
|
setOptimisticMessages((current) =>
|
|
518
670
|
current.some(({ id }) => id === message.id)
|
|
519
671
|
? current
|
|
@@ -558,9 +710,20 @@ export function useUniversalAgentChat(
|
|
|
558
710
|
return false;
|
|
559
711
|
}
|
|
560
712
|
if (projection === "message") {
|
|
561
|
-
|
|
713
|
+
const submissionId = receipt.kind === "queued"
|
|
562
714
|
? receipt.submission.submissionId
|
|
563
715
|
: receipt.submissionId;
|
|
716
|
+
if (stopRevision !== stopRevisionRef.current) {
|
|
717
|
+
await agentRef.current.call<{ ok: boolean }>(
|
|
718
|
+
"cancelSubmissionById",
|
|
719
|
+
[submissionId, USER_STOP_REASON],
|
|
720
|
+
);
|
|
721
|
+
rollback();
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
if (turnTimingRef.current?.messageId === message.id) {
|
|
725
|
+
turnTimingRef.current.submissionId = submissionId;
|
|
726
|
+
}
|
|
564
727
|
}
|
|
565
728
|
if (projection === "queue") {
|
|
566
729
|
if (receipt.kind !== "queued" || receipt.position < 1) {
|
|
@@ -678,29 +841,49 @@ export function useUniversalAgentChat(
|
|
|
678
841
|
}, []);
|
|
679
842
|
|
|
680
843
|
const stop = useCallback(async () => {
|
|
844
|
+
stopRevisionRef.current += 1;
|
|
681
845
|
const requestId = authoritativeTurn?.activeRequestId;
|
|
682
|
-
const submissionId = authoritativeTurn?.activeSubmissionId
|
|
683
|
-
|
|
846
|
+
const submissionId = authoritativeTurn?.activeSubmissionId;
|
|
847
|
+
const beforeStop = messagesRef.current;
|
|
848
|
+
const userIndex = beforeStop.findLastIndex(({ role }) => role === "user");
|
|
849
|
+
const assistantIndex = beforeStop.findLastIndex(
|
|
850
|
+
({ role }, index) => index > userIndex && role === "assistant",
|
|
851
|
+
);
|
|
852
|
+
const marker = {
|
|
853
|
+
completedAt: Date.now(),
|
|
854
|
+
interruptedByUser: true,
|
|
855
|
+
turnStatus: "aborted",
|
|
856
|
+
} as const;
|
|
857
|
+
setMessages(assistantIndex < 0
|
|
858
|
+
? [...beforeStop, {
|
|
859
|
+
id: `optimistic-stop:${submissionId ?? requestId ?? crypto.randomUUID()}`,
|
|
860
|
+
role: "assistant",
|
|
861
|
+
metadata: marker,
|
|
862
|
+
parts: [],
|
|
863
|
+
}]
|
|
864
|
+
: beforeStop.map((message, index) =>
|
|
865
|
+
index === assistantIndex
|
|
866
|
+
? { ...message, metadata: { ...message.metadata, ...marker } }
|
|
867
|
+
: message
|
|
868
|
+
));
|
|
869
|
+
const rollback = () => setMessages(beforeStop);
|
|
684
870
|
setDispatchError(undefined);
|
|
685
871
|
try {
|
|
872
|
+
await agentRef.current.call<{ ok: boolean }>(
|
|
873
|
+
"stopAllSubmissions",
|
|
874
|
+
[USER_STOP_REASON],
|
|
875
|
+
);
|
|
876
|
+
setOptimisticMessages([]);
|
|
877
|
+
setOptimisticQueued([]);
|
|
686
878
|
if (requestId) {
|
|
687
879
|
agentRef.current.send(JSON.stringify({
|
|
688
880
|
type: MessageType.CF_AGENT_CHAT_REQUEST_CANCEL,
|
|
689
881
|
id: requestId,
|
|
690
882
|
}));
|
|
691
|
-
|
|
883
|
+
await stopChat();
|
|
692
884
|
}
|
|
693
|
-
const result = submissionId
|
|
694
|
-
? await agentRef.current.call<{ ok: boolean }>(
|
|
695
|
-
"cancelSubmissionById",
|
|
696
|
-
[submissionId, USER_STOP_REASON],
|
|
697
|
-
)
|
|
698
|
-
: await agentRef.current.call<{ ok: boolean }>(
|
|
699
|
-
"stopTurn",
|
|
700
|
-
[undefined, USER_STOP_REASON],
|
|
701
|
-
);
|
|
702
|
-
if (!result.ok) setDispatchError("The active Turn could not be stopped");
|
|
703
885
|
} catch (cause) {
|
|
886
|
+
rollback();
|
|
704
887
|
setDispatchError(
|
|
705
888
|
cause instanceof Error ? cause.message : String(cause),
|
|
706
889
|
);
|
|
@@ -708,6 +891,7 @@ export function useUniversalAgentChat(
|
|
|
708
891
|
}, [
|
|
709
892
|
authoritativeTurn?.activeRequestId,
|
|
710
893
|
authoritativeTurn?.activeSubmissionId,
|
|
894
|
+
stopChat,
|
|
711
895
|
]);
|
|
712
896
|
|
|
713
897
|
const retry = useCallback(() => {
|
|
@@ -736,6 +920,14 @@ export function useUniversalAgentChat(
|
|
|
736
920
|
return false;
|
|
737
921
|
}
|
|
738
922
|
}, []);
|
|
923
|
+
const decideApproval = useCallback(
|
|
924
|
+
(executionId: string, decision: ApprovalDecision) =>
|
|
925
|
+
agentRef.current.call<{ ok: boolean }>(
|
|
926
|
+
"decideApproval",
|
|
927
|
+
[executionId, decision],
|
|
928
|
+
),
|
|
929
|
+
[],
|
|
930
|
+
);
|
|
739
931
|
|
|
740
932
|
// 只读控制面:Runtime 尚未装配时由 Agent 侧 `ensureRuntimeReady` 负责等待,
|
|
741
933
|
// 这里不缓存结果 —— 换模型、加技能或 reload 之后调用方要拿到的是新装配。
|
|
@@ -743,6 +935,24 @@ export function useUniversalAgentChat(
|
|
|
743
935
|
() => agentRef.current.call<RuntimeAssemblyView>("getRuntimeAssembly", []),
|
|
744
936
|
[],
|
|
745
937
|
);
|
|
938
|
+
const getRuntimeBinding = useCallback(
|
|
939
|
+
<Binding,>() => agentRef.current.call<Binding>("getRuntimeBinding", []),
|
|
940
|
+
[],
|
|
941
|
+
);
|
|
942
|
+
const readChatMetadata = useCallback(
|
|
943
|
+
<Metadata,>() => agentRef.current.call<Metadata>("readChatMetadata", []),
|
|
944
|
+
[],
|
|
945
|
+
);
|
|
946
|
+
const renameChat = useCallback(
|
|
947
|
+
<Metadata,>(title: string) =>
|
|
948
|
+
agentRef.current.call<Metadata>("renameChat", [title]),
|
|
949
|
+
[],
|
|
950
|
+
);
|
|
951
|
+
const rebindRuntime = useCallback(
|
|
952
|
+
<Input, Result,>(input: Input) =>
|
|
953
|
+
agentRef.current.call<Result>("rebindRuntime", [input]),
|
|
954
|
+
[],
|
|
955
|
+
);
|
|
746
956
|
const updateConfig = useCallback(
|
|
747
957
|
<Change,>(command: unknown) =>
|
|
748
958
|
agentRef.current.call<RuntimeConfigUpdateResult<Change>>(
|
|
@@ -761,15 +971,21 @@ export function useUniversalAgentChat(
|
|
|
761
971
|
|
|
762
972
|
return {
|
|
763
973
|
respondToolInteraction,
|
|
974
|
+
decideApproval,
|
|
764
975
|
getRuntimeAssembly,
|
|
976
|
+
getRuntimeBinding,
|
|
977
|
+
readChatMetadata,
|
|
978
|
+
renameChat,
|
|
979
|
+
rebindRuntime,
|
|
765
980
|
updateConfig,
|
|
766
981
|
reloadRuntime,
|
|
767
982
|
messages: visibleMessages,
|
|
768
983
|
status,
|
|
984
|
+
connectionStatus,
|
|
985
|
+
connectionError,
|
|
769
986
|
runtimeLoad: facetState?.runtimeLoad,
|
|
770
987
|
isStreaming,
|
|
771
|
-
error:
|
|
772
|
-
connectionError ?? undefined,
|
|
988
|
+
error: presentationError,
|
|
773
989
|
sendText,
|
|
774
990
|
steerText,
|
|
775
991
|
enqueueText,
|