@springbrand/chat-client 0.1.3-alpha.0 → 0.1.3-alpha.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/chat-client",
3
- "version": "0.1.3-alpha.0",
3
+ "version": "0.1.3-alpha.1",
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.1"
28
+ "@springbrand/agent-runtime": "0.1.3-alpha.2"
29
29
  },
30
30
  "scripts": {
31
31
  "typecheck": "tsc --noEmit"
@@ -1,35 +1,22 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { useAgent } from "agents/react";
3
3
 
4
- export interface ChatSessionSummary {
5
- id: string;
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<ChatSessionSummary[]> {
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 ChatSessionSummary[];
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<ChatSessionSummary[]>();
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<ChatSessionSummary>(
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<ChatSessionSummary>("forkChat", [sourceChatId, messageId]),
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<typeof useChatSessions>;
118
+ export type ChatSessions<Session> = ReturnType<
119
+ typeof useChatSessions<Session>
120
+ >;
package/src/index.ts CHANGED
@@ -19,6 +19,6 @@ export {
19
19
  } from "./chat-summary";
20
20
  export {
21
21
  useChatSessions,
22
- type ChatSessionSummary,
22
+ type ChatSessionsOptions,
23
23
  type ChatSessions,
24
24
  } from "./chat-sessions";
@@ -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 normalizedSdkStatus = stoppedByUser && sdkStatus === "error"
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 turnActive = Boolean(turn?.activeSubmissionId) ||
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
- const result = await agentRef.current.call<{ ok: boolean }>(
606
- "cancelSubmissionById",
607
- [submissionId, USER_STOP_REASON],
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
- }, [authoritativeTurn?.activeSubmissionId]);
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 ?? (stoppedByUser ? undefined : error) ??
672
+ error: dispatchError ?? sdkError ??
633
673
  connectionError ?? undefined,
634
674
  sendText,
635
675
  steerText,