@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 +2 -2
- package/src/use-universal-agent-chat.ts +95 -17
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.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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|