@springbrand/chat-client 0.1.3-alpha.0 → 0.1.3-alpha.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.
- package/package.json +2 -2
- package/src/chat-sessions.ts +18 -28
- package/src/index.ts +1 -1
- package/src/use-universal-agent-chat.ts +50 -10
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.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -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.1.3-alpha.
|
|
28
|
+
"@springbrand/agent-runtime": "0.1.3-alpha.3"
|
|
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
|
|
|
@@ -127,4 +115,6 @@ export function useChatSessions() {
|
|
|
127
115
|
};
|
|
128
116
|
}
|
|
129
117
|
|
|
130
|
-
export type ChatSessions = ReturnType<
|
|
118
|
+
export type ChatSessions<Session> = ReturnType<
|
|
119
|
+
typeof useChatSessions<Session>
|
|
120
|
+
>;
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ 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 {
|
|
@@ -67,6 +68,10 @@ type ChatQueuedSubmission = {
|
|
|
67
68
|
|
|
68
69
|
type ChatTurn = {
|
|
69
70
|
activeSubmissionId?: string;
|
|
71
|
+
activeRequestId?: string;
|
|
72
|
+
recoveryAttempt?: number;
|
|
73
|
+
recoveryMax?: number;
|
|
74
|
+
recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
|
|
70
75
|
steerable: boolean;
|
|
71
76
|
hasPendingSteer: boolean;
|
|
72
77
|
queued: ChatQueuedSubmission[];
|
|
@@ -100,6 +105,7 @@ export interface ChatRuntime {
|
|
|
100
105
|
type SharedChatConnectionOptions = {
|
|
101
106
|
host?: string;
|
|
102
107
|
credentials?: RequestCredentials;
|
|
108
|
+
protocols?: string | string[];
|
|
103
109
|
inboxAgent?: string;
|
|
104
110
|
sessionAgent?: string;
|
|
105
111
|
};
|
|
@@ -246,6 +252,7 @@ export function useUniversalAgentChat(
|
|
|
246
252
|
? { basePath: connection.basePath ?? CURRENT_INBOX_AGENT_PATH }
|
|
247
253
|
: { name: connection.inboxName }),
|
|
248
254
|
...(connection.host === undefined ? {} : { host: connection.host }),
|
|
255
|
+
...(connection.protocols === undefined ? {} : { protocols: connection.protocols }),
|
|
249
256
|
sub: [{
|
|
250
257
|
agent: connection.sessionAgent ?? "UniversalAgent",
|
|
251
258
|
name: chatId,
|
|
@@ -270,6 +277,7 @@ export function useUniversalAgentChat(
|
|
|
270
277
|
status: sdkStatus,
|
|
271
278
|
isStreaming,
|
|
272
279
|
error,
|
|
280
|
+
clearError,
|
|
273
281
|
// autonomous responses 纪律(CF 文档采纳 2026-07-05):框架已提供三个流态标志,
|
|
274
282
|
// 透出供 UI 区分「服务端主动流 vs 用户发起流」+「恢复态」(与 stall 看门狗配套)。
|
|
275
283
|
isServerStreaming,
|
|
@@ -292,9 +300,19 @@ export function useUniversalAgentChat(
|
|
|
292
300
|
);
|
|
293
301
|
const authoritativeTurn = facetState?.turn;
|
|
294
302
|
const stoppedByUser = error?.message === USER_STOP_REASON;
|
|
295
|
-
const
|
|
303
|
+
const latestMessage = visibleMessages.at(-1);
|
|
304
|
+
const latestTurnStatus = latestMessage?.role === "assistant"
|
|
305
|
+
? latestMessage.metadata?.turnStatus
|
|
306
|
+
: undefined;
|
|
307
|
+
const hasTerminalMessage = latestTurnStatus === "completed" ||
|
|
308
|
+
latestTurnStatus === "error" ||
|
|
309
|
+
latestTurnStatus === "aborted" ||
|
|
310
|
+
latestTurnStatus === "skipped";
|
|
311
|
+
const normalizedSdkStatus = sdkStatus === "error" &&
|
|
312
|
+
(stoppedByUser || hasTerminalMessage)
|
|
296
313
|
? "ready"
|
|
297
314
|
: sdkStatus;
|
|
315
|
+
const sdkError = stoppedByUser || hasTerminalMessage ? undefined : error;
|
|
298
316
|
// RPC admissions bypass useChat's request lifecycle. Project the durable
|
|
299
317
|
// Turn here so presentation stays correct without a second send path.
|
|
300
318
|
const authoritativeStatus =
|
|
@@ -392,12 +410,17 @@ export function useUniversalAgentChat(
|
|
|
392
410
|
queued,
|
|
393
411
|
};
|
|
394
412
|
}, [authoritativeTurn, visibleMessages, optimisticQueued]);
|
|
395
|
-
const
|
|
413
|
+
const sdkTurnActive =
|
|
396
414
|
status === "submitted" ||
|
|
397
415
|
status === "streaming" ||
|
|
398
416
|
isServerStreaming ||
|
|
399
417
|
isRecovering ||
|
|
400
418
|
isToolContinuation;
|
|
419
|
+
const pendingLocalTurn = optimisticMessages.length > 0 &&
|
|
420
|
+
(status === "submitted" || status === "streaming");
|
|
421
|
+
const turnActive = Boolean(turn?.activeSubmissionId) ||
|
|
422
|
+
pendingLocalTurn ||
|
|
423
|
+
(authoritativeTurn === undefined && sdkTurnActive);
|
|
401
424
|
const canSteer = turnActive &&
|
|
402
425
|
(!turn?.activeSubmissionId || turn.steerable);
|
|
403
426
|
|
|
@@ -426,6 +449,7 @@ export function useUniversalAgentChat(
|
|
|
426
449
|
projection: "message" | "queue",
|
|
427
450
|
): Promise<boolean> => {
|
|
428
451
|
setDispatchError(undefined);
|
|
452
|
+
clearError();
|
|
429
453
|
failedDispatchRef.current = undefined;
|
|
430
454
|
setCanRetry(false);
|
|
431
455
|
if (projection === "message" && delivery === "enqueue") {
|
|
@@ -441,6 +465,7 @@ export function useUniversalAgentChat(
|
|
|
441
465
|
};
|
|
442
466
|
}
|
|
443
467
|
if (projection === "message") {
|
|
468
|
+
activeSubmissionIdRef.current = undefined;
|
|
444
469
|
setOptimisticMessages((current) =>
|
|
445
470
|
current.some(({ id }) => id === message.id)
|
|
446
471
|
? current
|
|
@@ -517,7 +542,7 @@ export function useUniversalAgentChat(
|
|
|
517
542
|
);
|
|
518
543
|
return false;
|
|
519
544
|
}
|
|
520
|
-
}, [chatId, dispatchMessage]);
|
|
545
|
+
}, [chatId, clearError, dispatchMessage]);
|
|
521
546
|
|
|
522
547
|
const sendText = useCallback(async (
|
|
523
548
|
text: string,
|
|
@@ -597,22 +622,37 @@ export function useUniversalAgentChat(
|
|
|
597
622
|
}, []);
|
|
598
623
|
|
|
599
624
|
const stop = useCallback(async () => {
|
|
625
|
+
const requestId = authoritativeTurn?.activeRequestId;
|
|
600
626
|
const submissionId = authoritativeTurn?.activeSubmissionId ??
|
|
601
627
|
activeSubmissionIdRef.current;
|
|
602
|
-
if (!submissionId) return;
|
|
603
628
|
setDispatchError(undefined);
|
|
604
629
|
try {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
630
|
+
if (requestId) {
|
|
631
|
+
agentRef.current.send(JSON.stringify({
|
|
632
|
+
type: MessageType.CF_AGENT_CHAT_REQUEST_CANCEL,
|
|
633
|
+
id: requestId,
|
|
634
|
+
}));
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const result = submissionId
|
|
638
|
+
? await agentRef.current.call<{ ok: boolean }>(
|
|
639
|
+
"cancelSubmissionById",
|
|
640
|
+
[submissionId, USER_STOP_REASON],
|
|
641
|
+
)
|
|
642
|
+
: await agentRef.current.call<{ ok: boolean }>(
|
|
643
|
+
"stopTurn",
|
|
644
|
+
[undefined, USER_STOP_REASON],
|
|
645
|
+
);
|
|
609
646
|
if (!result.ok) setDispatchError("The active Turn could not be stopped");
|
|
610
647
|
} catch (cause) {
|
|
611
648
|
setDispatchError(
|
|
612
649
|
cause instanceof Error ? cause.message : String(cause),
|
|
613
650
|
);
|
|
614
651
|
}
|
|
615
|
-
}, [
|
|
652
|
+
}, [
|
|
653
|
+
authoritativeTurn?.activeRequestId,
|
|
654
|
+
authoritativeTurn?.activeSubmissionId,
|
|
655
|
+
]);
|
|
616
656
|
|
|
617
657
|
const retry = useCallback(() => {
|
|
618
658
|
const failed = failedDispatchRef.current;
|
|
@@ -629,7 +669,7 @@ export function useUniversalAgentChat(
|
|
|
629
669
|
status,
|
|
630
670
|
runtimeLoad: facetState?.runtimeLoad,
|
|
631
671
|
isStreaming,
|
|
632
|
-
error: dispatchError ??
|
|
672
|
+
error: dispatchError ?? sdkError ??
|
|
633
673
|
connectionError ?? undefined,
|
|
634
674
|
sendText,
|
|
635
675
|
steerText,
|