@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 +7 -5
- package/src/use-universal-agent-chat.ts +106 -8
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.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
|
-
"@
|
|
25
|
-
"@types/react
|
|
26
|
-
"react-dom": "^19.2.
|
|
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.
|
|
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 "
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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,
|