@springbrand/chat-client 0.1.3-alpha.40 → 0.1.3-alpha.42

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.40",
3
+ "version": "0.1.3-alpha.42",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -13,6 +13,7 @@
13
13
  ".": "./src/index.ts"
14
14
  },
15
15
  "peerDependencies": {
16
+ "@cloudflare/ai-chat": "^0.11.0",
16
17
  "agents": "^0.22.0",
17
18
  "ai": "^7.0.0",
18
19
  "react": "^19.0.0"
@@ -21,11 +22,12 @@
21
22
  "nanoid": "^5.1.16"
22
23
  },
23
24
  "devDependencies": {
24
- "@types/react": "^19.2.17",
25
- "@types/react-dom": "^19.2.3",
26
- "react-dom": "^19.2.7",
25
+ "@cloudflare/ai-chat": "0.11.0",
26
+ "@types/react": "^19.2.18",
27
+ "@types/react-dom": "^19.2.5",
28
+ "react-dom": "^19.2.8",
27
29
  "typescript": "^7.0.2",
28
- "@springbrand/agent-runtime": "0.2.0-alpha.51"
30
+ "@springbrand/agent-runtime": "0.2.0-alpha.54"
29
31
  },
30
32
  "scripts": {
31
33
  "typecheck": "tsc --noEmit"
@@ -15,7 +15,7 @@ import type { ChatStatus, FileUIPart, UIMessage } from "ai";
15
15
  import type { AgentToolRunState } from "agents";
16
16
  import type { AgentConnectionError } from "agents/client";
17
17
  import { MessageType } from "agents/chat";
18
- import { getAgentMessages, useAgentChat } from "agents/chat/react";
18
+ import { getAgentMessages, useAgentChat } from "@cloudflare/ai-chat/react";
19
19
  import { useAgent, useAgentToolEvents } from "agents/react";
20
20
  import {
21
21
  type ApprovalDecision,
@@ -34,6 +34,12 @@ type ChatMessageMetadata = Record<string, unknown> & {
34
34
  completedAt?: number;
35
35
  error?: string;
36
36
  interruptedByUser?: boolean;
37
+ // AI SDK keeps one assistant UIMessage for the live stream. This local-only
38
+ // anchor lets presentation place later steps below the steer that caused them.
39
+ optimisticSteer?: {
40
+ assistantMessageId: string;
41
+ afterStep: number;
42
+ };
37
43
  requestedCapabilities?: readonly RequestedCapability[];
38
44
  turnId?: string;
39
45
  turnStatus?: string;
@@ -95,6 +101,7 @@ type ChatQueuedSubmission = {
95
101
  type ChatTurn = {
96
102
  activeSubmissionId?: string;
97
103
  activeRequestId?: string;
104
+ activeMessageId?: string;
98
105
  recoveryAttempt?: number;
99
106
  recoveryMax?: number;
100
107
  recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
@@ -368,7 +375,35 @@ export function mergeOptimisticMessages<T extends UIMessage>(
368
375
  if (optimistic.length === 0) return messages;
369
376
  const ids = new Set(messages.map(({ id }) => id));
370
377
  const pending = optimistic.filter(({ id }) => !ids.has(id));
371
- return pending.length === 0 ? messages : [...messages, ...pending];
378
+ if (pending.length === 0) return messages;
379
+ const steer = pending.find((message) =>
380
+ (message.metadata as ChatMessageMetadata | undefined)?.optimisticSteer
381
+ );
382
+ const anchor = (steer?.metadata as ChatMessageMetadata | undefined)
383
+ ?.optimisticSteer;
384
+ const assistantIndex = anchor
385
+ ? messages.findIndex(({ id, role }) =>
386
+ id === anchor.assistantMessageId && role === "assistant"
387
+ )
388
+ : -1;
389
+ if (!steer || !anchor || assistantIndex < 0) return [...messages, ...pending];
390
+
391
+ const assistant = messages[assistantIndex]!;
392
+ let step = 0;
393
+ const splitAt = assistant.parts.findIndex((part) => {
394
+ if (part.type !== "step-start") return false;
395
+ step += 1;
396
+ return step > anchor.afterStep;
397
+ });
398
+ if (splitAt < 0) return [...messages, ...pending];
399
+
400
+ return [
401
+ ...messages.slice(0, assistantIndex),
402
+ { ...assistant, id: `${assistant.id}:before:${steer.id}`, parts: assistant.parts.slice(0, splitAt) },
403
+ ...pending,
404
+ { ...assistant, id: `${assistant.id}:after:${steer.id}`, parts: assistant.parts.slice(splitAt) },
405
+ ...messages.slice(assistantIndex + 1),
406
+ ];
372
407
  }
373
408
 
374
409
  function queuedPreview(message: ChatMessage): string {
@@ -382,10 +417,12 @@ function queuedPreview(message: ChatMessage): string {
382
417
  function loadInitialMessages(
383
418
  url: string | undefined,
384
419
  credentials: RequestCredentials | undefined,
420
+ view: "hydration" | "full" = "hydration",
385
421
  ): Promise<ChatMessage[]> {
386
422
  if (!url) return Promise.resolve([]);
387
423
  const messagesUrl = new URL(url);
388
424
  messagesUrl.pathname = `${messagesUrl.pathname.replace(/\/$/, "")}/get-messages`;
425
+ if (view === "hydration") messagesUrl.searchParams.set("view", "hydration");
389
426
  return getAgentMessages({
390
427
  url: messagesUrl.toString(),
391
428
  ...(credentials === undefined ? {} : { credentials }),
@@ -449,7 +486,6 @@ export function useUniversalAgentChat(): ChatRuntime {
449
486
  ? {}
450
487
  : { credentials: connection.credentials }),
451
488
  syncMessagesToServer: false,
452
- throttle: 16,
453
489
  });
454
490
  // The SDK keeps its advisory recovery flag until terminal settlement; a
455
491
  // live server stream proves the recovered Turn is executing again.
@@ -481,14 +517,15 @@ export function useUniversalAgentChat(): ChatRuntime {
481
517
  const runtimeErrorMessage = runtimeError instanceof Error
482
518
  ? runtimeError.message
483
519
  : runtimeError;
484
- const presentationError = latestTurnStatus === "error" &&
485
- latestMessage?.metadata?.error === runtimeErrorMessage
520
+ const presentationError = isServerStreaming || isRecovering ||
521
+ (latestTurnStatus === "error" && latestMessage?.metadata?.error === runtimeErrorMessage)
486
522
  ? undefined
487
523
  : runtimeError;
488
524
  // RPC admissions bypass useChat's request lifecycle. Project the durable
489
525
  // Turn here so presentation stays correct without a second send path.
490
- const authoritativeStatus =
491
- normalizedSdkStatus === "ready" &&
526
+ const authoritativeStatus = isServerStreaming
527
+ ? "streaming"
528
+ : normalizedSdkStatus === "ready" &&
492
529
  !isRecovering &&
493
530
  approvals?.length === 0 &&
494
531
  authoritativeTurn?.activeSubmissionId
@@ -506,6 +543,35 @@ export function useUniversalAgentChat(): ChatRuntime {
506
543
  : authoritativeStatus;
507
544
  const messagesRef = useRef(messages);
508
545
  messagesRef.current = messages;
546
+ const replayFallbackMessageIdRef = useRef<string | undefined>(undefined);
547
+ useEffect(() => {
548
+ const activeMessageId = facetState?.turn?.activeMessageId;
549
+ if (
550
+ !activeMessageId ||
551
+ sdkStatus !== "error" ||
552
+ sdkIsRecovering ||
553
+ isServerStreaming ||
554
+ replayFallbackMessageIdRef.current === activeMessageId
555
+ ) return;
556
+ const url = chatAgent.getHttpUrl();
557
+ if (!url) return;
558
+ replayFallbackMessageIdRef.current = activeMessageId;
559
+ void loadInitialMessages(url, connection.credentials, "full")
560
+ .then((snapshot) => {
561
+ setMessages(snapshot);
562
+ clearError();
563
+ })
564
+ .catch(() => undefined);
565
+ }, [
566
+ chatAgent,
567
+ clearError,
568
+ connection.credentials,
569
+ facetState?.turn?.activeMessageId,
570
+ isServerStreaming,
571
+ sdkIsRecovering,
572
+ sdkStatus,
573
+ setMessages,
574
+ ]);
509
575
  const turnTimingRef = useRef<ChatTurnTiming | null>(null);
510
576
 
511
577
  useEffect(() => {
@@ -721,6 +787,32 @@ export function useUniversalAgentChat(): ChatRuntime {
721
787
  rollback();
722
788
  return true;
723
789
  }
790
+ if (delivery === "steer" && receipt.kind === "accepted") {
791
+ const activeAssistant = messagesRef.current.findLast((candidate) =>
792
+ candidate.role === "assistant" &&
793
+ candidate.metadata?.turnId === submissionId
794
+ );
795
+ const assistantMessageId = activeAssistant?.id ??
796
+ (authoritativeTurn?.activeSubmissionId === submissionId
797
+ ? authoritativeTurn.activeMessageId
798
+ : undefined);
799
+ if (assistantMessageId) {
800
+ const afterStep = activeAssistant?.parts.filter(
801
+ ({ type }) => type === "step-start",
802
+ ).length ?? 0;
803
+ setOptimisticMessages((current) => current.map((optimistic) =>
804
+ optimistic.id === message.id
805
+ ? {
806
+ ...optimistic,
807
+ metadata: {
808
+ ...optimistic.metadata,
809
+ optimisticSteer: { assistantMessageId, afterStep },
810
+ },
811
+ }
812
+ : optimistic
813
+ ));
814
+ }
815
+ }
724
816
  if (turnTimingRef.current?.messageId === message.id) {
725
817
  turnTimingRef.current.submissionId = submissionId;
726
818
  }
@@ -753,7 +845,13 @@ export function useUniversalAgentChat(): ChatRuntime {
753
845
  );
754
846
  return false;
755
847
  }
756
- }, [chatId, clearError, dispatchMessage]);
848
+ }, [
849
+ authoritativeTurn?.activeMessageId,
850
+ authoritativeTurn?.activeSubmissionId,
851
+ chatId,
852
+ clearError,
853
+ dispatchMessage,
854
+ ]);
757
855
 
758
856
  const sendText = useCallback(async (
759
857
  text: string,