@springbrand/chat-client 0.1.3-alpha.31 → 0.1.3-alpha.34

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.31",
3
+ "version": "0.1.3-alpha.34",
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.2.0-alpha.42"
28
+ "@springbrand/agent-runtime": "0.2.0-alpha.45"
29
29
  },
30
30
  "scripts": {
31
31
  "typecheck": "tsc --noEmit"
@@ -31,7 +31,9 @@ import { createSafeUIMessageAgentConnection } from "./ui-message-stream-guard";
31
31
  import { CURRENT_INBOX_AGENT_PATH } from "./chat-sessions";
32
32
 
33
33
  type ChatMessageMetadata = Record<string, unknown> & {
34
+ completedAt?: number;
34
35
  error?: string;
36
+ interruptedByUser?: boolean;
35
37
  requestedCapabilities?: readonly RequestedCapability[];
36
38
  turnId?: string;
37
39
  turnStatus?: string;
@@ -182,6 +184,8 @@ type SharedChatConnectionOptions = {
182
184
  skipInitialMessages?: boolean;
183
185
  /** Host projection used only until the selected Session sends its first state. */
184
186
  initialTurnActive?: boolean;
187
+ /** Optional Agent RPC method injected by the Host to receive Turn timings. */
188
+ performanceRpc?: string;
185
189
  };
186
190
 
187
191
  export type ChatConnectionOptions = SharedChatConnectionOptions & (
@@ -243,6 +247,7 @@ export function UniversalAgentChatProvider({
243
247
  type ChatTurnTiming = {
244
248
  chatId: string;
245
249
  messageId: string;
250
+ submissionId?: string;
246
251
  baselineAssistantIds: Set<string>;
247
252
  click: number;
248
253
  rpcReceipt?: number;
@@ -251,6 +256,37 @@ type ChatTurnTiming = {
251
256
  ready?: number;
252
257
  };
253
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
+
254
290
  const CHAT_TURN_SEGMENTS = {
255
291
  rpcReceipt: ["click", "universal-agent.chat.click_to_rpc_receipt"],
256
292
  streamStart: [
@@ -380,7 +416,7 @@ export function useUniversalAgentChat(): ChatRuntime {
380
416
  const [dispatchError, setDispatchError] = useState<string>();
381
417
  const [canRetry, setCanRetry] = useState(false);
382
418
  const failedDispatchRef = useRef<FailedDispatch | undefined>(undefined);
383
- const activeSubmissionIdRef = useRef<string | undefined>(undefined);
419
+ const stopRevisionRef = useRef(0);
384
420
  // 轮询→推送:这条 chat 连接连的是 UniversalAgent facet,`agent.state` 即 facet 的
385
421
  // AgentState 广播(useAgent 内部 useState,收到 cf_agent_state 即 re-render)。待批项
386
422
  // 随它推来 —— 审批读侧复用这条已开的连接,不另开第二条 WebSocket。三态(原则 VII):
@@ -396,6 +432,7 @@ export function useUniversalAgentChat(): ChatRuntime {
396
432
  isStreaming,
397
433
  error,
398
434
  clearError,
435
+ setMessages,
399
436
  stop: stopChat,
400
437
  // autonomous responses 纪律(CF 文档采纳 2026-07-05):框架已提供三个流态标志,
401
438
  // 透出供 UI 区分「服务端主动流 vs 用户发起流」+「恢复态」(与 stall 看门狗配套)。
@@ -476,7 +513,8 @@ export function useUniversalAgentChat(): ChatRuntime {
476
513
  if (!timing) return;
477
514
  const assistant = messages.find((message) =>
478
515
  message.role === "assistant" &&
479
- !timing.baselineAssistantIds.has(message.id)
516
+ !timing.baselineAssistantIds.has(message.id) &&
517
+ (!timing.submissionId || message.metadata?.turnId === timing.submissionId)
480
518
  );
481
519
  if (assistant) markChatTurnPhase(timing, "streamStart");
482
520
  if (
@@ -488,9 +526,18 @@ export function useUniversalAgentChat(): ChatRuntime {
488
526
  }
489
527
  if (status === "ready" && timing.streamStart !== undefined) {
490
528
  if (timing.firstText !== undefined) markChatTurnPhase(timing, "ready");
529
+ else timing.ready ??= now();
491
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
+ }
492
539
  }
493
- }, [messages, status]);
540
+ }, [connection.performanceRpc, messages, status]);
494
541
 
495
542
  useEffect(() => {
496
543
  if (optimisticMessages.length === 0 || messages.length === 0) return;
@@ -601,6 +648,7 @@ export function useUniversalAgentChat(): ChatRuntime {
601
648
  delivery: MessageDelivery,
602
649
  projection: "message" | "queue",
603
650
  ): Promise<boolean> => {
651
+ const stopRevision = stopRevisionRef.current;
604
652
  setDispatchError(undefined);
605
653
  clearError();
606
654
  failedDispatchRef.current = undefined;
@@ -618,7 +666,6 @@ export function useUniversalAgentChat(): ChatRuntime {
618
666
  };
619
667
  }
620
668
  if (projection === "message") {
621
- activeSubmissionIdRef.current = undefined;
622
669
  setOptimisticMessages((current) =>
623
670
  current.some(({ id }) => id === message.id)
624
671
  ? current
@@ -663,9 +710,20 @@ export function useUniversalAgentChat(): ChatRuntime {
663
710
  return false;
664
711
  }
665
712
  if (projection === "message") {
666
- activeSubmissionIdRef.current = receipt.kind === "queued"
713
+ const submissionId = receipt.kind === "queued"
667
714
  ? receipt.submission.submissionId
668
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
+ }
669
727
  }
670
728
  if (projection === "queue") {
671
729
  if (receipt.kind !== "queued" || receipt.position < 1) {
@@ -783,29 +841,49 @@ export function useUniversalAgentChat(): ChatRuntime {
783
841
  }, []);
784
842
 
785
843
  const stop = useCallback(async () => {
844
+ stopRevisionRef.current += 1;
786
845
  const requestId = authoritativeTurn?.activeRequestId;
787
- const submissionId = authoritativeTurn?.activeSubmissionId ??
788
- activeSubmissionIdRef.current;
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);
789
870
  setDispatchError(undefined);
790
871
  try {
872
+ await agentRef.current.call<{ ok: boolean }>(
873
+ "stopAllSubmissions",
874
+ [USER_STOP_REASON],
875
+ );
876
+ setOptimisticMessages([]);
877
+ setOptimisticQueued([]);
791
878
  if (requestId) {
792
879
  agentRef.current.send(JSON.stringify({
793
880
  type: MessageType.CF_AGENT_CHAT_REQUEST_CANCEL,
794
881
  id: requestId,
795
882
  }));
796
883
  await stopChat();
797
- return;
798
884
  }
799
- await (submissionId
800
- ? agentRef.current.call<{ ok: boolean }>(
801
- "cancelSubmissionById",
802
- [submissionId, USER_STOP_REASON],
803
- )
804
- : agentRef.current.call<{ ok: boolean }>(
805
- "stopTurn",
806
- [undefined, USER_STOP_REASON],
807
- ));
808
885
  } catch (cause) {
886
+ rollback();
809
887
  setDispatchError(
810
888
  cause instanceof Error ? cause.message : String(cause),
811
889
  );