lemma-sdk 0.2.8 → 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,10 +284,22 @@ 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) {
|
|
299
|
+
// Avoid a network roundtrip on every send when we already have this conversation in state.
|
|
300
|
+
if (conversation?.id === existingId) {
|
|
301
|
+
return conversation;
|
|
302
|
+
}
|
|
206
303
|
const existing = await refreshConversation(existingId);
|
|
207
304
|
if (existing)
|
|
208
305
|
return existing;
|
|
@@ -215,7 +312,7 @@ export function useAssistantSession(options) {
|
|
|
215
312
|
...(options.createConversation ?? {}),
|
|
216
313
|
setActive: true,
|
|
217
314
|
});
|
|
218
|
-
}, [conversationId, createConversation, refreshConversation]);
|
|
315
|
+
}, [conversation, conversationId, createConversation, refreshConversation]);
|
|
219
316
|
const sendMessage = useCallback(async (content, input = {}) => {
|
|
220
317
|
const resolvedConversation = await ensureConversation(input.conversationId, input);
|
|
221
318
|
const resolvedConversationId = requireConversationId(resolvedConversation.id);
|
|
@@ -228,11 +325,21 @@ export function useAssistantSession(options) {
|
|
|
228
325
|
pod_id: scope.podId ?? undefined,
|
|
229
326
|
signal: controller.signal,
|
|
230
327
|
});
|
|
231
|
-
|
|
328
|
+
setConversationStatus("RUNNING");
|
|
329
|
+
await consume({
|
|
330
|
+
stream,
|
|
331
|
+
controller,
|
|
332
|
+
streamConversationId: resolvedConversationId,
|
|
333
|
+
syncAfterStream: input.syncOnTurnEnd,
|
|
334
|
+
});
|
|
232
335
|
return resolvedConversation;
|
|
233
|
-
}, [cancel, client, consume, defaultScope, ensureConversation]);
|
|
234
|
-
const resume = useCallback(async (
|
|
235
|
-
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
|
+
}
|
|
236
343
|
cancel();
|
|
237
344
|
const controller = new AbortController();
|
|
238
345
|
abortRef.current = controller;
|
|
@@ -242,8 +349,39 @@ export function useAssistantSession(options) {
|
|
|
242
349
|
pod_id: scope.podId ?? undefined,
|
|
243
350
|
signal: controller.signal,
|
|
244
351
|
});
|
|
245
|
-
|
|
246
|
-
|
|
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]);
|
|
247
385
|
const stop = useCallback(async (explicitConversationId) => {
|
|
248
386
|
const id = requireConversationId(explicitConversationId ?? conversationId);
|
|
249
387
|
const scope = normalizeScope(client, defaultScope);
|
|
@@ -251,22 +389,50 @@ export function useAssistantSession(options) {
|
|
|
251
389
|
await client.conversations.stopRun(id, {
|
|
252
390
|
pod_id: scope.podId ?? undefined,
|
|
253
391
|
});
|
|
392
|
+
setConversationStatus("WAITING");
|
|
393
|
+
clearStreamingText();
|
|
254
394
|
}, [client, conversationId, defaultScope]);
|
|
255
395
|
const clearMessages = useCallback(() => {
|
|
256
396
|
setMessages([]);
|
|
257
397
|
}, []);
|
|
398
|
+
useEffect(() => {
|
|
399
|
+
autoResumedKeyRef.current = null;
|
|
400
|
+
}, [conversationId]);
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
if (!isConversationRunningStatus(status)) {
|
|
403
|
+
autoResumedKeyRef.current = null;
|
|
404
|
+
}
|
|
405
|
+
}, [status]);
|
|
258
406
|
useEffect(() => {
|
|
259
407
|
if (!autoLoad || !conversationId) {
|
|
260
408
|
return;
|
|
261
409
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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]);
|
|
265
430
|
return {
|
|
266
431
|
conversationId,
|
|
267
432
|
conversation,
|
|
268
433
|
status,
|
|
269
434
|
messages,
|
|
435
|
+
streamingText,
|
|
270
436
|
isStreaming,
|
|
271
437
|
error,
|
|
272
438
|
setConversationId,
|
|
@@ -276,6 +442,7 @@ export function useAssistantSession(options) {
|
|
|
276
442
|
loadMessages,
|
|
277
443
|
sendMessage,
|
|
278
444
|
resume,
|
|
445
|
+
resumeIfRunning,
|
|
279
446
|
stop,
|
|
280
447
|
cancel,
|
|
281
448
|
clearMessages,
|