lemma-sdk 0.2.9 → 0.2.10
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/README.md
CHANGED
|
@@ -47,6 +47,64 @@ const supportAssistant = await client.assistants.get("support_assistant");
|
|
|
47
47
|
- Ergonomic type aliases exported at top level: `Agent`, `Assistant`, `Conversation`, `Task`, `TaskMessage`, `CreateAgentInput`, `CreateAssistantInput`, etc.
|
|
48
48
|
- `client.withPod(podId)` returns a pod-scoped client that shares auth state with the parent client.
|
|
49
49
|
|
|
50
|
+
## Table Access Grants (`accessible_tables`)
|
|
51
|
+
|
|
52
|
+
For function, agent, and assistant payloads, `accessible_tables` must be an array of objects:
|
|
53
|
+
|
|
54
|
+
- `table_name`: target table
|
|
55
|
+
- `mode`: `READ` or `WRITE`
|
|
56
|
+
|
|
57
|
+
`accessible_tables: ["table_name"]` is no longer valid.
|
|
58
|
+
|
|
59
|
+
You do not pass a datastore name in SDK calls. Table and file operations are pod-scoped (`client.tables`, `client.records`, `client.files`) and take table/file identifiers directly.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import {
|
|
65
|
+
TableAccessMode,
|
|
66
|
+
type CreateFunctionRequest,
|
|
67
|
+
type CreateAgentInput,
|
|
68
|
+
type CreateAssistantInput,
|
|
69
|
+
} from "lemma-sdk";
|
|
70
|
+
|
|
71
|
+
const functionPayload: CreateFunctionRequest = {
|
|
72
|
+
name: "expense_summary",
|
|
73
|
+
code: "def handler(ctx):\n return {'ok': True}",
|
|
74
|
+
config: {},
|
|
75
|
+
accessible_tables: [
|
|
76
|
+
{ table_name: "expenses", mode: TableAccessMode.READ },
|
|
77
|
+
{ table_name: "expense_summaries", mode: TableAccessMode.WRITE },
|
|
78
|
+
],
|
|
79
|
+
accessible_folders: ["/reports"],
|
|
80
|
+
accessible_applications: [],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const agentPayload: CreateAgentInput = {
|
|
84
|
+
name: "expense-summarizer",
|
|
85
|
+
instruction: "Summarize expenses without mutating data.",
|
|
86
|
+
tool_sets: [],
|
|
87
|
+
accessible_tables: [
|
|
88
|
+
{ table_name: "expenses", mode: TableAccessMode.READ },
|
|
89
|
+
{ table_name: "expense_notes", mode: TableAccessMode.WRITE },
|
|
90
|
+
],
|
|
91
|
+
accessible_folders: [],
|
|
92
|
+
accessible_applications: [],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const assistantPayload: CreateAssistantInput = {
|
|
96
|
+
name: "expense_assistant",
|
|
97
|
+
instruction: "Answer expense questions and save approved notes.",
|
|
98
|
+
tool_sets: [],
|
|
99
|
+
accessible_tables: [
|
|
100
|
+
{ table_name: "expenses", mode: TableAccessMode.READ },
|
|
101
|
+
{ table_name: "expense_notes", mode: TableAccessMode.WRITE },
|
|
102
|
+
],
|
|
103
|
+
accessible_folders: ["/notes"],
|
|
104
|
+
accessible_applications: [],
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
50
108
|
## Auth Helpers
|
|
51
109
|
|
|
52
110
|
```ts
|
|
@@ -2,6 +2,7 @@ import type { ConversationMessage } from "./types.js";
|
|
|
2
2
|
export interface ParsedAssistantStreamEvent {
|
|
3
3
|
message?: ConversationMessage;
|
|
4
4
|
status?: string;
|
|
5
|
+
token?: string;
|
|
5
6
|
}
|
|
6
7
|
export declare function parseAssistantStreamEvent(value: unknown): ParsedAssistantStreamEvent;
|
|
7
8
|
export declare function upsertConversationMessage(messages: ConversationMessage[], incoming: ConversationMessage): ConversationMessage[];
|
package/dist/assistant-events.js
CHANGED
|
@@ -51,6 +51,9 @@ export function parseAssistantStreamEvent(value) {
|
|
|
51
51
|
}
|
|
52
52
|
const eventType = typeof value.type === "string" ? value.type.toLowerCase() : "";
|
|
53
53
|
const payload = extractPayload(value);
|
|
54
|
+
if (eventType === "token" && typeof payload === "string") {
|
|
55
|
+
return { token: payload };
|
|
56
|
+
}
|
|
54
57
|
if (eventType === "message" || eventType === "message_added") {
|
|
55
58
|
const message = toConversationMessage(payload);
|
|
56
59
|
return message ? { message } : {};
|
|
@@ -13,6 +13,8 @@ export interface UseAssistantSessionOptions {
|
|
|
13
13
|
organizationId?: string;
|
|
14
14
|
conversationId?: string | null;
|
|
15
15
|
autoLoad?: boolean;
|
|
16
|
+
autoResume?: boolean;
|
|
17
|
+
syncOnTurnEnd?: boolean;
|
|
16
18
|
onEvent?: (event: SseRawEvent, payload: unknown | null) => void;
|
|
17
19
|
onStatus?: (status: string) => void;
|
|
18
20
|
onMessage?: (message: ConversationMessage) => void;
|
|
@@ -30,12 +32,22 @@ export interface SendAssistantMessageOptions {
|
|
|
30
32
|
conversationId?: string | null;
|
|
31
33
|
createIfMissing?: boolean;
|
|
32
34
|
createConversation?: CreateConversationInput;
|
|
35
|
+
syncOnTurnEnd?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface ResumeAssistantOptions {
|
|
38
|
+
conversationId?: string | null;
|
|
39
|
+
/**
|
|
40
|
+
* When true, skips resume unless conversation status is currently RUNNING.
|
|
41
|
+
*/
|
|
42
|
+
onlyIfRunning?: boolean;
|
|
43
|
+
syncOnTurnEnd?: boolean;
|
|
33
44
|
}
|
|
34
45
|
export interface UseAssistantSessionResult {
|
|
35
46
|
conversationId: string | null;
|
|
36
47
|
conversation: Conversation | null;
|
|
37
48
|
status?: string;
|
|
38
49
|
messages: ConversationMessage[];
|
|
50
|
+
streamingText: string;
|
|
39
51
|
isStreaming: boolean;
|
|
40
52
|
error: Error | null;
|
|
41
53
|
setConversationId: (conversationId: string | null) => void;
|
|
@@ -52,7 +64,8 @@ export interface UseAssistantSessionResult {
|
|
|
52
64
|
pageToken?: string;
|
|
53
65
|
}) => Promise<CursorPage<ConversationMessage>>;
|
|
54
66
|
sendMessage: (content: string, options?: SendAssistantMessageOptions) => Promise<Conversation>;
|
|
55
|
-
resume: (conversationId?: string | null) => Promise<void>;
|
|
67
|
+
resume: (conversationId?: string | null | ResumeAssistantOptions) => Promise<void>;
|
|
68
|
+
resumeIfRunning: (conversationId?: string | null) => Promise<boolean>;
|
|
56
69
|
stop: (conversationId?: string | null) => Promise<void>;
|
|
57
70
|
cancel: () => void;
|
|
58
71
|
clearMessages: () => void;
|
|
@@ -29,26 +29,89 @@ function normalizeScope(client, defaults, override) {
|
|
|
29
29
|
organizationId: override?.organizationId ?? defaults.organizationId ?? null,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
+
function normalizeConversationStatus(status) {
|
|
33
|
+
if (typeof status !== "string")
|
|
34
|
+
return undefined;
|
|
35
|
+
const normalized = status.trim().toUpperCase();
|
|
36
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
37
|
+
}
|
|
38
|
+
function isConversationRunningStatus(status) {
|
|
39
|
+
const normalized = normalizeConversationStatus(status);
|
|
40
|
+
if (!normalized)
|
|
41
|
+
return false;
|
|
42
|
+
return normalized === "RUNNING" || normalized === "IN_PROGRESS" || normalized === "PROCESSING";
|
|
43
|
+
}
|
|
44
|
+
function resolveResumeInput(input) {
|
|
45
|
+
if (typeof input === "string" || input === null) {
|
|
46
|
+
return { conversationId: input };
|
|
47
|
+
}
|
|
48
|
+
return input ?? {};
|
|
49
|
+
}
|
|
32
50
|
export function useAssistantSession(options) {
|
|
33
|
-
const { client, podId: defaultPodId, assistantId: defaultAssistantId, organizationId: defaultOrganizationId, conversationId: externalConversationId = null, autoLoad = true, onEvent, onStatus, onMessage, onError, } = options;
|
|
51
|
+
const { client, podId: defaultPodId, assistantId: defaultAssistantId, organizationId: defaultOrganizationId, conversationId: externalConversationId = null, autoLoad = true, autoResume = false, syncOnTurnEnd = false, onEvent, onStatus, onMessage, onError, } = options;
|
|
34
52
|
const [conversationId, setConversationIdState] = useState(externalConversationId);
|
|
35
53
|
const [conversation, setConversation] = useState(null);
|
|
36
54
|
const [status, setStatus] = useState(undefined);
|
|
37
55
|
const [messages, setMessages] = useState([]);
|
|
56
|
+
const [streamingText, setStreamingText] = useState("");
|
|
38
57
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
39
58
|
const [error, setError] = useState(null);
|
|
40
59
|
const abortRef = useRef(null);
|
|
60
|
+
const statusRef = useRef(undefined);
|
|
61
|
+
const streamingTextRef = useRef("");
|
|
62
|
+
const autoResumedKeyRef = useRef(null);
|
|
63
|
+
const onEventRef = useRef(onEvent);
|
|
64
|
+
const onStatusRef = useRef(onStatus);
|
|
65
|
+
const onMessageRef = useRef(onMessage);
|
|
66
|
+
const onErrorRef = useRef(onError);
|
|
41
67
|
const setConversationId = useCallback((nextConversationId) => {
|
|
42
68
|
setConversationIdState(nextConversationId);
|
|
69
|
+
autoResumedKeyRef.current = null;
|
|
70
|
+
streamingTextRef.current = "";
|
|
71
|
+
setStreamingText("");
|
|
43
72
|
if (!nextConversationId) {
|
|
44
73
|
setConversation(null);
|
|
45
74
|
setStatus(undefined);
|
|
75
|
+
statusRef.current = undefined;
|
|
46
76
|
setMessages([]);
|
|
47
77
|
}
|
|
48
78
|
}, []);
|
|
49
79
|
useEffect(() => {
|
|
50
80
|
setConversationIdState(externalConversationId);
|
|
51
81
|
}, [externalConversationId]);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
onEventRef.current = onEvent;
|
|
84
|
+
}, [onEvent]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
onStatusRef.current = onStatus;
|
|
87
|
+
}, [onStatus]);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
onMessageRef.current = onMessage;
|
|
90
|
+
}, [onMessage]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
onErrorRef.current = onError;
|
|
93
|
+
}, [onError]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
statusRef.current = status;
|
|
96
|
+
}, [status]);
|
|
97
|
+
const setConversationStatus = useCallback((nextStatus) => {
|
|
98
|
+
const normalized = normalizeConversationStatus(nextStatus);
|
|
99
|
+
setStatus(normalized);
|
|
100
|
+
statusRef.current = normalized;
|
|
101
|
+
if (normalized) {
|
|
102
|
+
onStatusRef.current?.(normalized);
|
|
103
|
+
}
|
|
104
|
+
}, []);
|
|
105
|
+
const clearStreamingText = useCallback(() => {
|
|
106
|
+
streamingTextRef.current = "";
|
|
107
|
+
setStreamingText("");
|
|
108
|
+
}, []);
|
|
109
|
+
const appendStreamingToken = useCallback((token) => {
|
|
110
|
+
if (!token)
|
|
111
|
+
return;
|
|
112
|
+
streamingTextRef.current += token;
|
|
113
|
+
setStreamingText(streamingTextRef.current);
|
|
114
|
+
}, []);
|
|
52
115
|
const cancel = useCallback(() => {
|
|
53
116
|
abortRef.current?.abort();
|
|
54
117
|
abortRef.current = null;
|
|
@@ -78,14 +141,14 @@ export function useAssistantSession(options) {
|
|
|
78
141
|
catch (listError) {
|
|
79
142
|
const normalized = normalizeError(listError, "Failed to list conversations.");
|
|
80
143
|
setError(normalized);
|
|
81
|
-
|
|
144
|
+
onErrorRef.current?.(listError);
|
|
82
145
|
return {
|
|
83
146
|
items: [],
|
|
84
147
|
limit: input.limit ?? 20,
|
|
85
148
|
next_page_token: null,
|
|
86
149
|
};
|
|
87
150
|
}
|
|
88
|
-
}, [client, defaultScope
|
|
151
|
+
}, [client, defaultScope]);
|
|
89
152
|
const createConversation = useCallback(async (input = {}) => {
|
|
90
153
|
applyPodScope(client, input.podId ?? defaultPodId ?? null);
|
|
91
154
|
const payload = {
|
|
@@ -101,11 +164,13 @@ export function useAssistantSession(options) {
|
|
|
101
164
|
if (input.setActive !== false) {
|
|
102
165
|
setConversationIdState(created.id);
|
|
103
166
|
setConversation(created);
|
|
104
|
-
|
|
167
|
+
setConversationStatus(created.status);
|
|
105
168
|
setMessages([]);
|
|
169
|
+
clearStreamingText();
|
|
170
|
+
autoResumedKeyRef.current = null;
|
|
106
171
|
}
|
|
107
172
|
return created;
|
|
108
|
-
}, [client, defaultAssistantId, defaultOrganizationId, defaultPodId]);
|
|
173
|
+
}, [clearStreamingText, client, defaultAssistantId, defaultOrganizationId, defaultPodId, setConversationStatus]);
|
|
109
174
|
const refreshConversation = useCallback(async (explicitConversationId) => {
|
|
110
175
|
const id = explicitConversationId ?? conversationId;
|
|
111
176
|
if (!id)
|
|
@@ -118,36 +183,30 @@ export function useAssistantSession(options) {
|
|
|
118
183
|
});
|
|
119
184
|
setConversation(nextConversation);
|
|
120
185
|
const nextStatus = typeof nextConversation.status === "string"
|
|
121
|
-
? nextConversation.status
|
|
186
|
+
? nextConversation.status
|
|
122
187
|
: undefined;
|
|
123
|
-
|
|
124
|
-
if (nextStatus) {
|
|
125
|
-
onStatus?.(nextStatus);
|
|
126
|
-
}
|
|
188
|
+
setConversationStatus(nextStatus);
|
|
127
189
|
return nextConversation;
|
|
128
190
|
}
|
|
129
191
|
catch (refreshError) {
|
|
130
192
|
const normalized = normalizeError(refreshError, "Failed to fetch conversation.");
|
|
131
193
|
setError(normalized);
|
|
132
|
-
|
|
194
|
+
onErrorRef.current?.(refreshError);
|
|
133
195
|
return null;
|
|
134
196
|
}
|
|
135
|
-
}, [client, conversationId, defaultScope,
|
|
197
|
+
}, [client, conversationId, defaultScope, setConversationStatus]);
|
|
136
198
|
const loadMessages = useCallback(async (input = {}) => {
|
|
137
199
|
const id = input.conversationId ?? conversationId;
|
|
138
200
|
if (!id) {
|
|
139
201
|
return { items: [], limit: input.limit ?? 20, next_page_token: null };
|
|
140
202
|
}
|
|
141
203
|
try {
|
|
142
|
-
const scope = normalizeScope(client, defaultScope);
|
|
143
|
-
applyPodScope(client, scope.podId);
|
|
144
204
|
const response = await client.conversations.messages.list(id, {
|
|
145
|
-
pod_id: scope.podId ?? undefined,
|
|
146
205
|
limit: input.limit,
|
|
147
206
|
page_token: input.pageToken,
|
|
148
207
|
});
|
|
149
208
|
const nextMessages = response.items ?? [];
|
|
150
|
-
setMessages(nextMessages);
|
|
209
|
+
setMessages((previous) => nextMessages.reduce((accumulator, message) => upsertConversationMessage(accumulator, message), previous));
|
|
151
210
|
return {
|
|
152
211
|
items: nextMessages,
|
|
153
212
|
limit: response.limit ?? input.limit ?? 20,
|
|
@@ -157,32 +216,58 @@ export function useAssistantSession(options) {
|
|
|
157
216
|
catch (messageError) {
|
|
158
217
|
const normalized = normalizeError(messageError, "Failed to fetch conversation messages.");
|
|
159
218
|
setError(normalized);
|
|
160
|
-
|
|
219
|
+
onErrorRef.current?.(messageError);
|
|
161
220
|
return {
|
|
162
221
|
items: [],
|
|
163
222
|
limit: input.limit ?? 20,
|
|
164
223
|
next_page_token: null,
|
|
165
224
|
};
|
|
166
225
|
}
|
|
167
|
-
}, [client, conversationId, defaultScope,
|
|
168
|
-
const consume = useCallback(async (stream, controller) => {
|
|
226
|
+
}, [clearStreamingText, client, conversationId, defaultScope, setConversationStatus]);
|
|
227
|
+
const consume = useCallback(async ({ stream, controller, streamConversationId, syncAfterStream, }) => {
|
|
169
228
|
setIsStreaming(true);
|
|
170
229
|
setError(null);
|
|
230
|
+
clearStreamingText();
|
|
231
|
+
let sawTerminalStatus = false;
|
|
171
232
|
try {
|
|
172
233
|
for await (const event of readSSE(stream)) {
|
|
173
234
|
if (controller.signal.aborted) {
|
|
174
235
|
break;
|
|
175
236
|
}
|
|
176
237
|
const payload = parseSSEJson(event);
|
|
177
|
-
|
|
238
|
+
onEventRef.current?.(event, payload);
|
|
178
239
|
const parsed = parseAssistantStreamEvent(payload);
|
|
240
|
+
if (parsed.token) {
|
|
241
|
+
appendStreamingToken(parsed.token);
|
|
242
|
+
}
|
|
179
243
|
if (parsed.message) {
|
|
180
244
|
setMessages((previous) => upsertConversationMessage(previous, parsed.message));
|
|
181
|
-
|
|
245
|
+
onMessageRef.current?.(parsed.message);
|
|
246
|
+
const role = typeof parsed.message.role === "string"
|
|
247
|
+
? parsed.message.role.toLowerCase()
|
|
248
|
+
: "";
|
|
249
|
+
if (role === "assistant" || role === "tool") {
|
|
250
|
+
clearStreamingText();
|
|
251
|
+
}
|
|
182
252
|
}
|
|
183
253
|
if (parsed.status) {
|
|
184
|
-
|
|
185
|
-
|
|
254
|
+
setConversationStatus(parsed.status);
|
|
255
|
+
if (!isConversationRunningStatus(parsed.status)) {
|
|
256
|
+
sawTerminalStatus = true;
|
|
257
|
+
clearStreamingText();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!controller.signal.aborted) {
|
|
262
|
+
if (!sawTerminalStatus && isConversationRunningStatus(statusRef.current)) {
|
|
263
|
+
setConversationStatus("WAITING");
|
|
264
|
+
}
|
|
265
|
+
clearStreamingText();
|
|
266
|
+
const shouldSync = syncAfterStream ?? syncOnTurnEnd;
|
|
267
|
+
const syncConversationId = streamConversationId ?? conversationId;
|
|
268
|
+
if (shouldSync && syncConversationId) {
|
|
269
|
+
await refreshConversation(syncConversationId);
|
|
270
|
+
await loadMessages({ conversationId: syncConversationId, limit: 100 });
|
|
186
271
|
}
|
|
187
272
|
}
|
|
188
273
|
}
|
|
@@ -190,7 +275,7 @@ export function useAssistantSession(options) {
|
|
|
190
275
|
if (!(streamError instanceof Error && streamError.name === "AbortError")) {
|
|
191
276
|
const normalized = normalizeError(streamError, "Failed to stream assistant run.");
|
|
192
277
|
setError(normalized);
|
|
193
|
-
|
|
278
|
+
onErrorRef.current?.(streamError);
|
|
194
279
|
}
|
|
195
280
|
}
|
|
196
281
|
finally {
|
|
@@ -199,7 +284,15 @@ export function useAssistantSession(options) {
|
|
|
199
284
|
}
|
|
200
285
|
setIsStreaming(false);
|
|
201
286
|
}
|
|
202
|
-
}, [
|
|
287
|
+
}, [
|
|
288
|
+
appendStreamingToken,
|
|
289
|
+
clearStreamingText,
|
|
290
|
+
conversationId,
|
|
291
|
+
loadMessages,
|
|
292
|
+
refreshConversation,
|
|
293
|
+
setConversationStatus,
|
|
294
|
+
syncOnTurnEnd,
|
|
295
|
+
]);
|
|
203
296
|
const ensureConversation = useCallback(async (overrideConversationId, options) => {
|
|
204
297
|
const existingId = overrideConversationId ?? conversationId;
|
|
205
298
|
if (existingId) {
|
|
@@ -232,11 +325,21 @@ export function useAssistantSession(options) {
|
|
|
232
325
|
pod_id: scope.podId ?? undefined,
|
|
233
326
|
signal: controller.signal,
|
|
234
327
|
});
|
|
235
|
-
|
|
328
|
+
setConversationStatus("RUNNING");
|
|
329
|
+
await consume({
|
|
330
|
+
stream,
|
|
331
|
+
controller,
|
|
332
|
+
streamConversationId: resolvedConversationId,
|
|
333
|
+
syncAfterStream: input.syncOnTurnEnd,
|
|
334
|
+
});
|
|
236
335
|
return resolvedConversation;
|
|
237
|
-
}, [cancel, client, consume, defaultScope, ensureConversation]);
|
|
238
|
-
const resume = useCallback(async (
|
|
239
|
-
const
|
|
336
|
+
}, [cancel, client, consume, defaultScope, ensureConversation, setConversationStatus]);
|
|
337
|
+
const resume = useCallback(async (input) => {
|
|
338
|
+
const resumeInput = resolveResumeInput(input);
|
|
339
|
+
const id = requireConversationId(resumeInput.conversationId ?? conversationId);
|
|
340
|
+
if (resumeInput.onlyIfRunning && !isConversationRunningStatus(statusRef.current)) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
240
343
|
cancel();
|
|
241
344
|
const controller = new AbortController();
|
|
242
345
|
abortRef.current = controller;
|
|
@@ -246,8 +349,39 @@ export function useAssistantSession(options) {
|
|
|
246
349
|
pod_id: scope.podId ?? undefined,
|
|
247
350
|
signal: controller.signal,
|
|
248
351
|
});
|
|
249
|
-
|
|
250
|
-
|
|
352
|
+
setConversationStatus("RUNNING");
|
|
353
|
+
await consume({
|
|
354
|
+
stream,
|
|
355
|
+
controller,
|
|
356
|
+
streamConversationId: id,
|
|
357
|
+
syncAfterStream: resumeInput.syncOnTurnEnd,
|
|
358
|
+
});
|
|
359
|
+
}, [cancel, client, consume, conversationId, defaultScope, setConversationStatus]);
|
|
360
|
+
const resumeIfRunning = useCallback(async (explicitConversationId) => {
|
|
361
|
+
const id = explicitConversationId ?? conversationId;
|
|
362
|
+
if (!id)
|
|
363
|
+
return false;
|
|
364
|
+
if (isStreaming)
|
|
365
|
+
return false;
|
|
366
|
+
const statusKey = normalizeConversationStatus(statusRef.current);
|
|
367
|
+
const resumeKey = `${id}:${statusKey ?? "UNKNOWN"}`;
|
|
368
|
+
if (autoResumedKeyRef.current === resumeKey) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
const knownRunning = isConversationRunningStatus(statusRef.current);
|
|
372
|
+
if (!knownRunning) {
|
|
373
|
+
const latestConversation = await refreshConversation(id);
|
|
374
|
+
if (!latestConversation || !isConversationRunningStatus(latestConversation.status)) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
autoResumedKeyRef.current = resumeKey;
|
|
379
|
+
await resume({
|
|
380
|
+
conversationId: id,
|
|
381
|
+
onlyIfRunning: true,
|
|
382
|
+
});
|
|
383
|
+
return true;
|
|
384
|
+
}, [conversationId, isStreaming, refreshConversation, resume]);
|
|
251
385
|
const stop = useCallback(async (explicitConversationId) => {
|
|
252
386
|
const id = requireConversationId(explicitConversationId ?? conversationId);
|
|
253
387
|
const scope = normalizeScope(client, defaultScope);
|
|
@@ -255,22 +389,50 @@ export function useAssistantSession(options) {
|
|
|
255
389
|
await client.conversations.stopRun(id, {
|
|
256
390
|
pod_id: scope.podId ?? undefined,
|
|
257
391
|
});
|
|
392
|
+
setConversationStatus("WAITING");
|
|
393
|
+
clearStreamingText();
|
|
258
394
|
}, [client, conversationId, defaultScope]);
|
|
259
395
|
const clearMessages = useCallback(() => {
|
|
260
396
|
setMessages([]);
|
|
261
397
|
}, []);
|
|
398
|
+
useEffect(() => {
|
|
399
|
+
autoResumedKeyRef.current = null;
|
|
400
|
+
}, [conversationId]);
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
if (!isConversationRunningStatus(status)) {
|
|
403
|
+
autoResumedKeyRef.current = null;
|
|
404
|
+
}
|
|
405
|
+
}, [status]);
|
|
262
406
|
useEffect(() => {
|
|
263
407
|
if (!autoLoad || !conversationId) {
|
|
264
408
|
return;
|
|
265
409
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
410
|
+
let cancelled = false;
|
|
411
|
+
const bootstrapConversation = async () => {
|
|
412
|
+
const latestConversation = await refreshConversation(conversationId);
|
|
413
|
+
if (cancelled)
|
|
414
|
+
return;
|
|
415
|
+
await loadMessages({ conversationId, limit: 100 });
|
|
416
|
+
if (cancelled)
|
|
417
|
+
return;
|
|
418
|
+
if (!autoResume)
|
|
419
|
+
return;
|
|
420
|
+
const latestStatus = normalizeConversationStatus(latestConversation?.status) ?? normalizeConversationStatus(statusRef.current);
|
|
421
|
+
if (!isConversationRunningStatus(latestStatus))
|
|
422
|
+
return;
|
|
423
|
+
await resumeIfRunning(conversationId);
|
|
424
|
+
};
|
|
425
|
+
void bootstrapConversation();
|
|
426
|
+
return () => {
|
|
427
|
+
cancelled = true;
|
|
428
|
+
};
|
|
429
|
+
}, [autoLoad, autoResume, conversationId, loadMessages, refreshConversation, resumeIfRunning]);
|
|
269
430
|
return {
|
|
270
431
|
conversationId,
|
|
271
432
|
conversation,
|
|
272
433
|
status,
|
|
273
434
|
messages,
|
|
435
|
+
streamingText,
|
|
274
436
|
isStreaming,
|
|
275
437
|
error,
|
|
276
438
|
setConversationId,
|
|
@@ -280,6 +442,7 @@ export function useAssistantSession(options) {
|
|
|
280
442
|
loadMessages,
|
|
281
443
|
sendMessage,
|
|
282
444
|
resume,
|
|
445
|
+
resumeIfRunning,
|
|
283
446
|
stop,
|
|
284
447
|
cancel,
|
|
285
448
|
clearMessages,
|