@springbrand/chat-client 0.1.1 → 0.1.3-alpha.0
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 +6 -5
- package/src/chat-sessions.ts +36 -5
- package/src/index.ts +1 -0
- package/src/use-universal-agent-chat.ts +127 -10
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/chat-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3-alpha.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
|
-
"src"
|
|
6
|
+
"src",
|
|
7
|
+
"!src/**/*.test.ts"
|
|
7
8
|
],
|
|
8
9
|
"publishConfig": {
|
|
9
10
|
"access": "public"
|
|
@@ -17,14 +18,14 @@
|
|
|
17
18
|
"react": "^19.0.0"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"nanoid": "^5.1.16"
|
|
21
|
-
"@springbrand/agent-runtime": "0.1.1"
|
|
21
|
+
"nanoid": "^5.1.16"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/react": "^19.2.17",
|
|
25
25
|
"@types/react-dom": "^19.2.3",
|
|
26
26
|
"react-dom": "^19.2.7",
|
|
27
|
-
"typescript": "^7.0.2"
|
|
27
|
+
"typescript": "^7.0.2",
|
|
28
|
+
"@springbrand/agent-runtime": "0.1.3-alpha.1"
|
|
28
29
|
},
|
|
29
30
|
"scripts": {
|
|
30
31
|
"typecheck": "tsc --noEmit"
|
package/src/chat-sessions.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { useCallback, useRef } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useAgent } from "agents/react";
|
|
3
|
-
import type { RuntimeActivity } from "@springbrand/agent-runtime/contracts";
|
|
4
3
|
|
|
5
4
|
export interface ChatSessionSummary {
|
|
6
5
|
id: string;
|
|
@@ -11,7 +10,7 @@ export interface ChatSessionSummary {
|
|
|
11
10
|
updatedAt: number;
|
|
12
11
|
pinned: boolean;
|
|
13
12
|
archived: boolean;
|
|
14
|
-
activity:
|
|
13
|
+
activity: "idle" | "working" | "needs-input";
|
|
15
14
|
usage?: {
|
|
16
15
|
totalTokens: number;
|
|
17
16
|
totalCost: number;
|
|
@@ -25,18 +24,49 @@ interface InboxState {
|
|
|
25
24
|
|
|
26
25
|
export const CURRENT_INBOX_AGENT_PATH = "api/agent-connections/inbox";
|
|
27
26
|
|
|
27
|
+
type Request = typeof fetch;
|
|
28
|
+
|
|
29
|
+
export async function loadChatSessions(
|
|
30
|
+
signal?: AbortSignal,
|
|
31
|
+
request: Request = fetch,
|
|
32
|
+
): Promise<ChatSessionSummary[]> {
|
|
33
|
+
const response = await request("/api/chats", {
|
|
34
|
+
cache: "no-store",
|
|
35
|
+
headers: { Accept: "application/json" },
|
|
36
|
+
signal,
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) throw new Error("Unable to load chat sessions");
|
|
39
|
+
const body = await response.json().catch(() => null) as
|
|
40
|
+
| { chats?: unknown }
|
|
41
|
+
| null;
|
|
42
|
+
if (!Array.isArray(body?.chats)) {
|
|
43
|
+
throw new Error("Invalid chat sessions response");
|
|
44
|
+
}
|
|
45
|
+
return body.chats as ChatSessionSummary[];
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
/** Browser client for the user Inbox. Routing and presentation stay with the host UI. */
|
|
29
49
|
export function useChatSessions() {
|
|
30
50
|
const inbox = useAgent<InboxState>({
|
|
31
51
|
agent: "Inbox",
|
|
32
52
|
basePath: CURRENT_INBOX_AGENT_PATH,
|
|
33
53
|
});
|
|
54
|
+
const [snapshot, setSnapshot] = useState<ChatSessionSummary[]>();
|
|
34
55
|
const creatingRef = useRef(false);
|
|
35
56
|
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const controller = new AbortController();
|
|
59
|
+
void loadChatSessions(controller.signal)
|
|
60
|
+
.then(setSnapshot)
|
|
61
|
+
.catch(() => undefined); // WebSocket state remains the fallback.
|
|
62
|
+
return () => controller.abort();
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
36
65
|
const createChat = useCallback(async (userAgentId?: string) => {
|
|
37
66
|
if (creatingRef.current) return undefined;
|
|
38
67
|
creatingRef.current = true;
|
|
39
68
|
try {
|
|
69
|
+
await inbox.ready;
|
|
40
70
|
return await inbox.call<ChatSessionSummary>(
|
|
41
71
|
"createChat",
|
|
42
72
|
userAgentId ? [{ userAgentId }] : undefined,
|
|
@@ -83,8 +113,9 @@ export function useChatSessions() {
|
|
|
83
113
|
);
|
|
84
114
|
|
|
85
115
|
return {
|
|
86
|
-
chats: inbox.state?.chats ?? [],
|
|
87
|
-
synced: inbox.state !== undefined,
|
|
116
|
+
chats: inbox.state?.chats ?? snapshot ?? [],
|
|
117
|
+
synced: inbox.state !== undefined || snapshot !== undefined,
|
|
118
|
+
connected: inbox.identified,
|
|
88
119
|
workspaceRev: inbox.state?.workspaceRev ?? 0,
|
|
89
120
|
createChat,
|
|
90
121
|
forkChat,
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { nanoid } from "nanoid";
|
|
3
|
-
import type { FileUIPart, UIMessage } from "ai";
|
|
4
|
-
import {
|
|
3
|
+
import type { ChatStatus, FileUIPart, UIMessage } from "ai";
|
|
4
|
+
import type { AgentToolRunState } from "agents";
|
|
5
|
+
import { getAgentMessages, useAgentChat } from "agents/chat/react";
|
|
5
6
|
import { useAgent, useAgentToolEvents } from "agents/react";
|
|
6
7
|
import {
|
|
7
|
-
USER_STOP_REASON,
|
|
8
8
|
type MessageDelivery,
|
|
9
9
|
type MessageDispatchReceipt,
|
|
10
10
|
type RuntimeQueuedSubmission,
|
|
@@ -23,6 +23,92 @@ type FailedDispatch = {
|
|
|
23
23
|
projection: "message" | "queue";
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
type ChatRuntimeLoadState =
|
|
27
|
+
| { status: "idle"; available: false }
|
|
28
|
+
| {
|
|
29
|
+
status: "loading";
|
|
30
|
+
phase: "config" | "plugins" | "mcp" | "pi";
|
|
31
|
+
available: boolean;
|
|
32
|
+
startedAt: number;
|
|
33
|
+
updatedAt: number;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
status: "ready";
|
|
37
|
+
available: true;
|
|
38
|
+
startedAt: number;
|
|
39
|
+
completedAt: number;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
status: "error";
|
|
43
|
+
phase: "config" | "plugins" | "mcp" | "pi";
|
|
44
|
+
available: boolean;
|
|
45
|
+
startedAt: number;
|
|
46
|
+
failedAt: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type ChatApproval = {
|
|
50
|
+
executionId: string;
|
|
51
|
+
source: "action" | "codemode" | "temporary-agent";
|
|
52
|
+
action: string;
|
|
53
|
+
summary: string;
|
|
54
|
+
executionLevel: "safe" | "low" | "medium" | "high";
|
|
55
|
+
requiredExecutionLevel: "safe" | "low" | "medium" | "high";
|
|
56
|
+
inputJson: string;
|
|
57
|
+
requestId: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type ChatQueuedSubmission = {
|
|
61
|
+
submissionId: string;
|
|
62
|
+
messageId: string;
|
|
63
|
+
preview: string;
|
|
64
|
+
position: number;
|
|
65
|
+
createdAt: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type ChatTurn = {
|
|
69
|
+
activeSubmissionId?: string;
|
|
70
|
+
steerable: boolean;
|
|
71
|
+
hasPendingSteer: boolean;
|
|
72
|
+
queued: ChatQueuedSubmission[];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export interface ChatRuntime {
|
|
76
|
+
messages: ChatMessage[];
|
|
77
|
+
status: ChatStatus;
|
|
78
|
+
runtimeLoad: ChatRuntimeLoadState | undefined;
|
|
79
|
+
isStreaming: boolean;
|
|
80
|
+
error: string | Error | undefined;
|
|
81
|
+
sendText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
|
|
82
|
+
steerText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
|
|
83
|
+
enqueueText: (text: string, files?: readonly FileUIPart[]) => Promise<boolean>;
|
|
84
|
+
steerQueued: (submissionId: string) => Promise<boolean>;
|
|
85
|
+
cancelQueued: (submissionId: string) => Promise<boolean>;
|
|
86
|
+
stop: () => Promise<void>;
|
|
87
|
+
regenerate: () => void;
|
|
88
|
+
canRetry: boolean;
|
|
89
|
+
agentToolRuns: AgentToolRunState<ChatMessage["parts"][number]>[];
|
|
90
|
+
isServerStreaming: boolean;
|
|
91
|
+
isRecovering: boolean;
|
|
92
|
+
isToolContinuation: boolean;
|
|
93
|
+
approvals: ChatApproval[] | undefined;
|
|
94
|
+
approvalsLoaded: boolean;
|
|
95
|
+
turn: ChatTurn | undefined;
|
|
96
|
+
turnActive: boolean;
|
|
97
|
+
canSteer: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type SharedChatConnectionOptions = {
|
|
101
|
+
host?: string;
|
|
102
|
+
credentials?: RequestCredentials;
|
|
103
|
+
inboxAgent?: string;
|
|
104
|
+
sessionAgent?: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type ChatConnectionOptions = SharedChatConnectionOptions & (
|
|
108
|
+
| { inboxName: string; basePath?: never }
|
|
109
|
+
| { inboxName?: never; basePath?: string }
|
|
110
|
+
);
|
|
111
|
+
|
|
26
112
|
type ChatTurnTiming = {
|
|
27
113
|
chatId: string;
|
|
28
114
|
messageId: string;
|
|
@@ -47,6 +133,8 @@ const CHAT_TURN_SEGMENTS = {
|
|
|
47
133
|
ready: ["firstText", "universal-agent.chat.first_text_to_ready"],
|
|
48
134
|
} as const;
|
|
49
135
|
|
|
136
|
+
const USER_STOP_REASON = "Stopped by user";
|
|
137
|
+
|
|
50
138
|
function now(): number {
|
|
51
139
|
return typeof performance === "undefined" ? Date.now() : performance.now();
|
|
52
140
|
}
|
|
@@ -115,6 +203,19 @@ function queuedPreview(message: ChatMessage): string {
|
|
|
115
203
|
}).join(" ").trim().slice(0, 160) || "Queued message";
|
|
116
204
|
}
|
|
117
205
|
|
|
206
|
+
function loadInitialMessages(
|
|
207
|
+
url: string | undefined,
|
|
208
|
+
credentials: RequestCredentials | undefined,
|
|
209
|
+
): Promise<ChatMessage[]> {
|
|
210
|
+
if (!url) return Promise.resolve([]);
|
|
211
|
+
const messagesUrl = new URL(url);
|
|
212
|
+
messagesUrl.pathname = `${messagesUrl.pathname.replace(/\/$/, "")}/get-messages`;
|
|
213
|
+
return getAgentMessages({
|
|
214
|
+
url: messagesUrl.toString(),
|
|
215
|
+
...(credentials === undefined ? {} : { credentials }),
|
|
216
|
+
}) as Promise<ChatMessage[]>;
|
|
217
|
+
}
|
|
218
|
+
|
|
118
219
|
/**
|
|
119
220
|
* 薄适配层:把 useAgent + UIMessage chat + useAgentToolEvents
|
|
120
221
|
* 收敛成一个归一化 runtime,对外只暴露 messages / 发送 / 状态 / 子 agent 运行,
|
|
@@ -126,7 +227,10 @@ function queuedPreview(message: ChatMessage): string {
|
|
|
126
227
|
* sub 数组由客户端 kebab 化;服务端按 ctx.exports 反解回 CamelCase className,
|
|
127
228
|
* 与 Inbox.onBeforeSubAgent 的严格门卫(hasSubAgent)对齐。
|
|
128
229
|
*/
|
|
129
|
-
export function useUniversalAgentChat(
|
|
230
|
+
export function useUniversalAgentChat(
|
|
231
|
+
chatId: string,
|
|
232
|
+
connection: ChatConnectionOptions = {},
|
|
233
|
+
): ChatRuntime {
|
|
130
234
|
const [optimisticMessages, setOptimisticMessages] =
|
|
131
235
|
useState<ChatMessage[]>([]);
|
|
132
236
|
const [optimisticQueued, setOptimisticQueued] =
|
|
@@ -135,10 +239,17 @@ export function useUniversalAgentChat(chatId: string) {
|
|
|
135
239
|
const [canRetry, setCanRetry] = useState(false);
|
|
136
240
|
const failedDispatchRef = useRef<FailedDispatch | undefined>(undefined);
|
|
137
241
|
const activeSubmissionIdRef = useRef<string | undefined>(undefined);
|
|
242
|
+
const inboxAgent = connection.inboxAgent ?? "Inbox";
|
|
138
243
|
const agent = useAgent<RuntimeState>({
|
|
139
|
-
agent:
|
|
140
|
-
|
|
141
|
-
|
|
244
|
+
agent: inboxAgent,
|
|
245
|
+
...(connection.inboxName === undefined
|
|
246
|
+
? { basePath: connection.basePath ?? CURRENT_INBOX_AGENT_PATH }
|
|
247
|
+
: { name: connection.inboxName }),
|
|
248
|
+
...(connection.host === undefined ? {} : { host: connection.host }),
|
|
249
|
+
sub: [{
|
|
250
|
+
agent: connection.sessionAgent ?? "UniversalAgent",
|
|
251
|
+
name: chatId,
|
|
252
|
+
}],
|
|
142
253
|
});
|
|
143
254
|
const chatAgent = useMemo(
|
|
144
255
|
() => createSafeUIMessageAgentConnection(agent),
|
|
@@ -167,6 +278,11 @@ export function useUniversalAgentChat(chatId: string) {
|
|
|
167
278
|
connectionError,
|
|
168
279
|
} = useAgentChat<RuntimeState, ChatMessage>({
|
|
169
280
|
agent: chatAgent,
|
|
281
|
+
getInitialMessages: ({ url }) =>
|
|
282
|
+
loadInitialMessages(url, connection.credentials),
|
|
283
|
+
...(connection.credentials === undefined
|
|
284
|
+
? {}
|
|
285
|
+
: { credentials: connection.credentials }),
|
|
170
286
|
syncMessagesToServer: false,
|
|
171
287
|
throttle: 100,
|
|
172
288
|
});
|
|
@@ -287,8 +403,10 @@ export function useUniversalAgentChat(chatId: string) {
|
|
|
287
403
|
|
|
288
404
|
// 子 agent 运行(agentTool 前台 + runAgentTool detached 后台)的实时事件投影:
|
|
289
405
|
// 状态机 + progress snapshot + durable milestones(knowledge/upstream-adoption.csv F6)
|
|
290
|
-
const { runsById } = useAgentToolEvents({
|
|
291
|
-
|
|
406
|
+
const { runsById } = useAgentToolEvents<ChatMessage["parts"][number]>({
|
|
407
|
+
agent,
|
|
408
|
+
});
|
|
409
|
+
const agentToolRuns: AgentToolRunState<ChatMessage["parts"][number]>[] = useMemo(
|
|
292
410
|
() => Object.values(runsById).sort((a, b) => a.order - b.order),
|
|
293
411
|
[runsById],
|
|
294
412
|
);
|
|
@@ -538,5 +656,4 @@ export function useUniversalAgentChat(chatId: string) {
|
|
|
538
656
|
};
|
|
539
657
|
}
|
|
540
658
|
|
|
541
|
-
export type ChatRuntime = ReturnType<typeof useUniversalAgentChat>;
|
|
542
659
|
export type AgentToolRun = ChatRuntime["agentToolRuns"][number];
|