lemma-sdk 0.2.4 → 0.2.5

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.
Files changed (71) hide show
  1. package/README.md +59 -0
  2. package/dist/assistant-events.d.ts +7 -0
  3. package/dist/assistant-events.js +78 -0
  4. package/dist/auth.d.ts +6 -4
  5. package/dist/auth.js +42 -41
  6. package/dist/browser/lemma-client.js +71 -61
  7. package/dist/browser.d.ts +1 -1
  8. package/dist/browser.js +1 -1
  9. package/dist/index.d.ts +7 -1
  10. package/dist/index.js +4 -1
  11. package/dist/namespaces/desks.d.ts +2 -2
  12. package/dist/namespaces/desks.js +7 -7
  13. package/dist/namespaces/icons.js +4 -1
  14. package/dist/openapi_client/index.d.ts +10 -7
  15. package/dist/openapi_client/models/{AgentNode.d.ts → AgentNode_Input.d.ts} +1 -1
  16. package/dist/openapi_client/models/AgentNode_Output.d.ts +11 -0
  17. package/dist/openapi_client/models/AppDescriptorResponse.d.ts +2 -2
  18. package/dist/openapi_client/models/Body_upload_file_files__resource_type___resource_id__upload_post.d.ts +1 -1
  19. package/dist/openapi_client/models/{Body_file_upload.d.ts → DatastoreFileUploadRequest.d.ts} +2 -2
  20. package/dist/openapi_client/models/{DecisionNode.d.ts → DecisionNode_Input.d.ts} +1 -1
  21. package/dist/openapi_client/models/DecisionNode_Output.d.ts +11 -0
  22. package/dist/openapi_client/models/DeskBundleUploadRequest.d.ts +4 -0
  23. package/dist/openapi_client/models/FlowEntity.d.ts +4 -4
  24. package/dist/openapi_client/models/{FunctionNode.d.ts → FunctionNode_Input.d.ts} +1 -1
  25. package/dist/openapi_client/models/FunctionNode_Output.d.ts +11 -0
  26. package/dist/openapi_client/models/FunctionNode_Output.js +1 -0
  27. package/dist/openapi_client/models/IconUploadRequest.d.ts +3 -0
  28. package/dist/openapi_client/models/IconUploadRequest.js +1 -0
  29. package/dist/openapi_client/models/ValidationError.d.ts +2 -0
  30. package/dist/openapi_client/models/WorkflowGraphUpdateRequest.d.ts +4 -4
  31. package/dist/openapi_client/models/{Body_file_update.d.ts → update.d.ts} +2 -2
  32. package/dist/openapi_client/models/update.js +1 -0
  33. package/dist/openapi_client/services/ApplicationsService.d.ts +2 -2
  34. package/dist/openapi_client/services/ApplicationsService.js +3 -3
  35. package/dist/openapi_client/services/DesksService.d.ts +9 -9
  36. package/dist/openapi_client/services/DesksService.js +8 -8
  37. package/dist/openapi_client/services/FilesService.d.ts +4 -4
  38. package/dist/openapi_client/services/IconsService.d.ts +2 -2
  39. package/dist/openapi_client/services/PublicSdkService.d.ts +1 -1
  40. package/dist/openapi_client/services/PublicSdkService.js +1 -1
  41. package/dist/react/index.d.ts +12 -0
  42. package/dist/react/index.js +6 -0
  43. package/dist/react/useAssistantRun.d.ts +1 -1
  44. package/dist/react/useAssistantRun.js +23 -69
  45. package/dist/react/useAssistantRuntime.d.ts +13 -0
  46. package/dist/react/useAssistantRuntime.js +108 -0
  47. package/dist/react/useAssistantSession.d.ts +61 -0
  48. package/dist/react/useAssistantSession.js +278 -0
  49. package/dist/react/useFlowRunHistory.d.ts +19 -0
  50. package/dist/react/useFlowRunHistory.js +77 -0
  51. package/dist/react/useFlowSession.d.ts +39 -0
  52. package/dist/react/useFlowSession.js +195 -0
  53. package/dist/react/useFunctionSession.d.ts +32 -0
  54. package/dist/react/useFunctionSession.js +147 -0
  55. package/dist/react/useTaskSession.d.ts +35 -0
  56. package/dist/react/useTaskSession.js +207 -0
  57. package/dist/run-utils.d.ts +18 -0
  58. package/dist/run-utils.js +62 -0
  59. package/dist/task-events.d.ts +7 -0
  60. package/dist/task-events.js +78 -0
  61. package/dist/types.d.ts +3 -1
  62. package/package.json +1 -1
  63. package/dist/openapi_client/models/Body_icon_upload.d.ts +0 -3
  64. package/dist/openapi_client/models/Body_pod_desk_bundle_upload.d.ts +0 -4
  65. /package/dist/openapi_client/models/{AgentNode.js → AgentNode_Input.js} +0 -0
  66. /package/dist/openapi_client/models/{Body_file_update.js → AgentNode_Output.js} +0 -0
  67. /package/dist/openapi_client/models/{Body_file_upload.js → DatastoreFileUploadRequest.js} +0 -0
  68. /package/dist/openapi_client/models/{Body_icon_upload.js → DecisionNode_Input.js} +0 -0
  69. /package/dist/openapi_client/models/{Body_pod_desk_bundle_upload.js → DecisionNode_Output.js} +0 -0
  70. /package/dist/openapi_client/models/{DecisionNode.js → DeskBundleUploadRequest.js} +0 -0
  71. /package/dist/openapi_client/models/{FunctionNode.js → FunctionNode_Input.js} +0 -0
@@ -1,82 +1,36 @@
1
- import { useCallback, useRef, useState } from "react";
2
- import { parseSSEJson, readSSE } from "../streams.js";
1
+ import { useAssistantSession } from "./useAssistantSession.js";
3
2
  function requireConversationId(conversationId) {
4
3
  if (!conversationId) {
5
4
  throw new Error("conversationId is required.");
6
5
  }
7
6
  return conversationId;
8
7
  }
9
- function resolvePodId(client, podId) {
10
- const resolved = podId ?? client.podId;
11
- if (!resolved) {
12
- throw new Error("podId is required. Pass podId or set it on LemmaClient.");
13
- }
14
- return resolved;
15
- }
16
8
  export function useAssistantRun({ client, podId, conversationId, onEvent, onError, }) {
17
- const [isStreaming, setIsStreaming] = useState(false);
18
- const [error, setError] = useState(null);
19
- const abortRef = useRef(null);
20
- const consume = useCallback(async (stream, controller) => {
21
- setIsStreaming(true);
22
- setError(null);
23
- try {
24
- for await (const event of readSSE(stream)) {
25
- if (controller.signal.aborted) {
26
- break;
27
- }
28
- onEvent?.(event, parseSSEJson(event));
29
- }
30
- }
31
- catch (streamError) {
32
- if (!(streamError instanceof Error && streamError.name === "AbortError")) {
33
- const normalized = streamError instanceof Error
34
- ? streamError
35
- : new Error("Failed to stream assistant run.");
36
- setError(normalized);
37
- onError?.(streamError);
38
- }
39
- }
40
- finally {
41
- if (abortRef.current === controller) {
42
- abortRef.current = null;
43
- }
44
- setIsStreaming(false);
45
- }
46
- }, [onError, onEvent]);
47
- const cancel = useCallback(() => {
48
- abortRef.current?.abort();
49
- abortRef.current = null;
50
- }, []);
51
- const sendMessage = useCallback(async (content) => {
52
- const id = requireConversationId(conversationId);
53
- cancel();
54
- const controller = new AbortController();
55
- abortRef.current = controller;
56
- client.setPodId(resolvePodId(client, podId));
57
- const stream = await client.conversations.sendMessageStream(id, { content }, { signal: controller.signal });
58
- await consume(stream, controller);
59
- }, [cancel, client, consume, conversationId, podId]);
60
- const resume = useCallback(async () => {
61
- const id = requireConversationId(conversationId);
62
- cancel();
63
- const controller = new AbortController();
64
- abortRef.current = controller;
65
- client.setPodId(resolvePodId(client, podId));
66
- const stream = await client.conversations.resumeStream(id, { signal: controller.signal });
67
- await consume(stream, controller);
68
- }, [cancel, client, consume, conversationId, podId]);
69
- const stop = useCallback(async () => {
70
- const id = requireConversationId(conversationId);
71
- client.setPodId(resolvePodId(client, podId));
72
- await client.conversations.stopRun(id);
73
- }, [client, conversationId, podId]);
9
+ const session = useAssistantSession({
10
+ client,
11
+ podId,
12
+ conversationId,
13
+ onEvent,
14
+ onError,
15
+ });
16
+ const sendMessage = async (content) => {
17
+ await session.sendMessage(content, {
18
+ conversationId: requireConversationId(conversationId ?? session.conversationId),
19
+ createIfMissing: false,
20
+ });
21
+ };
22
+ const resume = async () => {
23
+ await session.resume(requireConversationId(conversationId ?? session.conversationId));
24
+ };
25
+ const stop = async () => {
26
+ await session.stop(requireConversationId(conversationId ?? session.conversationId));
27
+ };
74
28
  return {
75
- isStreaming,
76
- error,
29
+ isStreaming: session.isStreaming,
30
+ error: session.error,
77
31
  sendMessage,
78
32
  resume,
79
33
  stop,
80
- cancel,
34
+ cancel: session.cancel,
81
35
  };
82
36
  }
@@ -0,0 +1,13 @@
1
+ import type { ConversationMessage } from "../types.js";
2
+ export interface UseAssistantRuntimeOptions {
3
+ conversationId?: string | null;
4
+ sessionMessages?: ConversationMessage[];
5
+ }
6
+ export interface UseAssistantRuntimeResult {
7
+ runtimeMessages: ConversationMessage[];
8
+ appendOptimisticUserMessage: (content: string) => ConversationMessage;
9
+ replaceLoadedMessages: (messages: ConversationMessage[]) => void;
10
+ mergeMessages: (messages: ConversationMessage[]) => void;
11
+ clear: () => void;
12
+ }
13
+ export declare function useAssistantRuntime({ conversationId, sessionMessages, }: UseAssistantRuntimeOptions): UseAssistantRuntimeResult;
@@ -0,0 +1,108 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ function isRecord(value) {
3
+ return !!value && typeof value === "object" && !Array.isArray(value);
4
+ }
5
+ function messageText(content) {
6
+ if (typeof content === "string") {
7
+ return content.trim();
8
+ }
9
+ if (isRecord(content)) {
10
+ if (typeof content.content === "string")
11
+ return content.content.trim();
12
+ if (typeof content.text === "string")
13
+ return content.text.trim();
14
+ }
15
+ return "";
16
+ }
17
+ function messageTime(message) {
18
+ const timestamp = new Date(message.created_at).getTime();
19
+ return Number.isFinite(timestamp) ? timestamp : 0;
20
+ }
21
+ function isOptimisticId(messageId) {
22
+ return messageId.startsWith("optimistic-user-");
23
+ }
24
+ function upsertRuntimeMessage(previous, incoming) {
25
+ const next = [...previous];
26
+ const directIndex = next.findIndex((message) => message.id === incoming.id);
27
+ if (directIndex >= 0) {
28
+ next[directIndex] = incoming;
29
+ return next;
30
+ }
31
+ if (incoming.role === "user") {
32
+ const incomingText = messageText(incoming.content);
33
+ if (incomingText) {
34
+ const optimisticIndex = next.findIndex((message) => (message.role === "user"
35
+ && isOptimisticId(message.id)
36
+ && messageText(message.content) === incomingText));
37
+ if (optimisticIndex >= 0) {
38
+ next[optimisticIndex] = incoming;
39
+ return next;
40
+ }
41
+ }
42
+ }
43
+ next.push(incoming);
44
+ return next;
45
+ }
46
+ function toRuntimeMessage(message) {
47
+ return message;
48
+ }
49
+ function buildOptimisticId() {
50
+ if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
51
+ return `optimistic-user-${globalThis.crypto.randomUUID()}`;
52
+ }
53
+ return `optimistic-user-${Date.now()}-${Math.random().toString(36).slice(2)}`;
54
+ }
55
+ export function useAssistantRuntime({ conversationId = null, sessionMessages = [], }) {
56
+ const [runtimeMessages, setRuntimeMessages] = useState([]);
57
+ const mergeMessages = useCallback((messages) => {
58
+ setRuntimeMessages((previous) => {
59
+ const merged = messages.reduce((accumulator, message) => upsertRuntimeMessage(accumulator, toRuntimeMessage(message)), previous);
60
+ return [...merged].sort((a, b) => messageTime(a) - messageTime(b));
61
+ });
62
+ }, []);
63
+ const replaceLoadedMessages = useCallback((messages) => {
64
+ setRuntimeMessages((previous) => {
65
+ const next = messages.reduce((accumulator, message) => upsertRuntimeMessage(accumulator, toRuntimeMessage(message)), previous);
66
+ return [...next].sort((a, b) => messageTime(a) - messageTime(b));
67
+ });
68
+ }, []);
69
+ const appendOptimisticUserMessage = useCallback((content) => {
70
+ const trimmed = content.trim();
71
+ const optimistic = {
72
+ id: buildOptimisticId(),
73
+ role: "user",
74
+ content: { content: trimmed },
75
+ created_at: new Date().toISOString(),
76
+ metadata: null,
77
+ ...(conversationId ? { conversation_id: conversationId } : {}),
78
+ };
79
+ setRuntimeMessages((previous) => {
80
+ const next = upsertRuntimeMessage(previous, optimistic);
81
+ return [...next].sort((a, b) => messageTime(a) - messageTime(b));
82
+ });
83
+ return optimistic;
84
+ }, [conversationId]);
85
+ const clear = useCallback(() => {
86
+ setRuntimeMessages([]);
87
+ }, []);
88
+ useEffect(() => {
89
+ setRuntimeMessages([]);
90
+ }, [conversationId]);
91
+ useEffect(() => {
92
+ if (sessionMessages.length === 0)
93
+ return;
94
+ const normalized = sessionMessages
95
+ .map((message) => toRuntimeMessage(message))
96
+ .filter((message) => !conversationId || !message.conversation_id || message.conversation_id === conversationId);
97
+ if (normalized.length === 0)
98
+ return;
99
+ mergeMessages(normalized);
100
+ }, [conversationId, mergeMessages, sessionMessages]);
101
+ return {
102
+ runtimeMessages,
103
+ appendOptimisticUserMessage,
104
+ replaceLoadedMessages,
105
+ mergeMessages,
106
+ clear,
107
+ };
108
+ }
@@ -0,0 +1,61 @@
1
+ import type { LemmaClient } from "../client.js";
2
+ import { type SseRawEvent } from "../streams.js";
3
+ import type { Conversation, ConversationMessage, ConversationModel, CursorPage } from "../types.js";
4
+ interface ConversationScope {
5
+ podId?: string | null;
6
+ assistantId?: string | null;
7
+ organizationId?: string | null;
8
+ }
9
+ export interface UseAssistantSessionOptions {
10
+ client: LemmaClient;
11
+ podId?: string;
12
+ assistantId?: string;
13
+ organizationId?: string;
14
+ conversationId?: string | null;
15
+ autoLoad?: boolean;
16
+ onEvent?: (event: SseRawEvent, payload: unknown | null) => void;
17
+ onStatus?: (status: string) => void;
18
+ onMessage?: (message: ConversationMessage) => void;
19
+ onError?: (error: unknown) => void;
20
+ }
21
+ export interface CreateConversationInput {
22
+ title?: string | null;
23
+ model?: ConversationModel | null;
24
+ podId?: string | null;
25
+ assistantId?: string | null;
26
+ organizationId?: string | null;
27
+ setActive?: boolean;
28
+ }
29
+ export interface SendAssistantMessageOptions {
30
+ conversationId?: string | null;
31
+ createIfMissing?: boolean;
32
+ createConversation?: CreateConversationInput;
33
+ }
34
+ export interface UseAssistantSessionResult {
35
+ conversationId: string | null;
36
+ conversation: Conversation | null;
37
+ status?: string;
38
+ messages: ConversationMessage[];
39
+ isStreaming: boolean;
40
+ error: Error | null;
41
+ setConversationId: (conversationId: string | null) => void;
42
+ listConversations: (options?: {
43
+ limit?: number;
44
+ pageToken?: string;
45
+ scope?: ConversationScope;
46
+ }) => Promise<CursorPage<Conversation>>;
47
+ createConversation: (input?: CreateConversationInput) => Promise<Conversation>;
48
+ refreshConversation: (conversationId?: string | null) => Promise<Conversation | null>;
49
+ loadMessages: (options?: {
50
+ conversationId?: string | null;
51
+ limit?: number;
52
+ pageToken?: string;
53
+ }) => Promise<CursorPage<ConversationMessage>>;
54
+ sendMessage: (content: string, options?: SendAssistantMessageOptions) => Promise<Conversation>;
55
+ resume: (conversationId?: string | null) => Promise<void>;
56
+ stop: (conversationId?: string | null) => Promise<void>;
57
+ cancel: () => void;
58
+ clearMessages: () => void;
59
+ }
60
+ export declare function useAssistantSession(options: UseAssistantSessionOptions): UseAssistantSessionResult;
61
+ export {};
@@ -0,0 +1,278 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { parseSSEJson, readSSE } from "../streams.js";
3
+ import { parseAssistantStreamEvent, upsertConversationMessage } from "../assistant-events.js";
4
+ function resolveOptionalPodId(client, podId) {
5
+ return podId ?? client.podId;
6
+ }
7
+ function applyPodScope(client, podId) {
8
+ const resolvedPodId = resolveOptionalPodId(client, podId);
9
+ if (resolvedPodId) {
10
+ client.setPodId(resolvedPodId);
11
+ }
12
+ return resolvedPodId;
13
+ }
14
+ function requireConversationId(conversationId) {
15
+ if (!conversationId) {
16
+ throw new Error("conversationId is required.");
17
+ }
18
+ return conversationId;
19
+ }
20
+ function normalizeError(error, fallback) {
21
+ if (error instanceof Error)
22
+ return error;
23
+ return new Error(fallback);
24
+ }
25
+ function normalizeScope(client, options, override) {
26
+ return {
27
+ podId: override?.podId ?? options.podId ?? client.podId ?? null,
28
+ assistantId: override?.assistantId ?? options.assistantId ?? null,
29
+ organizationId: override?.organizationId ?? options.organizationId ?? null,
30
+ };
31
+ }
32
+ export function useAssistantSession(options) {
33
+ const { client, conversationId: externalConversationId = null, autoLoad = true, onEvent, onStatus, onMessage, onError, } = options;
34
+ const [conversationId, setConversationIdState] = useState(externalConversationId);
35
+ const [conversation, setConversation] = useState(null);
36
+ const [status, setStatus] = useState(undefined);
37
+ const [messages, setMessages] = useState([]);
38
+ const [isStreaming, setIsStreaming] = useState(false);
39
+ const [error, setError] = useState(null);
40
+ const abortRef = useRef(null);
41
+ const setConversationId = useCallback((nextConversationId) => {
42
+ setConversationIdState(nextConversationId);
43
+ if (!nextConversationId) {
44
+ setConversation(null);
45
+ setStatus(undefined);
46
+ setMessages([]);
47
+ }
48
+ }, []);
49
+ useEffect(() => {
50
+ setConversationIdState(externalConversationId);
51
+ }, [externalConversationId]);
52
+ const cancel = useCallback(() => {
53
+ abortRef.current?.abort();
54
+ abortRef.current = null;
55
+ }, []);
56
+ const listConversations = useCallback(async (input = {}) => {
57
+ try {
58
+ const scope = normalizeScope(client, options, input.scope);
59
+ applyPodScope(client, scope.podId);
60
+ const response = await client.conversations.list({
61
+ pod_id: scope.podId ?? undefined,
62
+ assistant_id: scope.assistantId ?? undefined,
63
+ organization_id: scope.organizationId ?? undefined,
64
+ limit: input.limit,
65
+ page_token: input.pageToken,
66
+ });
67
+ return {
68
+ items: response.items ?? [],
69
+ limit: response.limit ?? input.limit ?? 20,
70
+ next_page_token: response.next_page_token,
71
+ };
72
+ }
73
+ catch (listError) {
74
+ const normalized = normalizeError(listError, "Failed to list conversations.");
75
+ setError(normalized);
76
+ onError?.(listError);
77
+ return {
78
+ items: [],
79
+ limit: input.limit ?? 20,
80
+ next_page_token: null,
81
+ };
82
+ }
83
+ }, [client, onError, options]);
84
+ const createConversation = useCallback(async (input = {}) => {
85
+ applyPodScope(client, input.podId ?? options.podId ?? null);
86
+ const payload = {
87
+ title: input.title ?? undefined,
88
+ pod_id: input.podId ?? options.podId ?? client.podId ?? undefined,
89
+ assistant_id: input.assistantId ?? options.assistantId ?? undefined,
90
+ organization_id: input.organizationId ?? options.organizationId ?? undefined,
91
+ model: typeof input.model === "undefined"
92
+ ? undefined
93
+ : input.model,
94
+ };
95
+ const created = await client.conversations.create(payload);
96
+ if (input.setActive !== false) {
97
+ setConversationIdState(created.id);
98
+ setConversation(created);
99
+ setStatus(typeof created.status === "string" ? created.status.toUpperCase() : undefined);
100
+ setMessages([]);
101
+ }
102
+ return created;
103
+ }, [client, options.assistantId, options.organizationId, options.podId]);
104
+ const refreshConversation = useCallback(async (explicitConversationId) => {
105
+ const id = explicitConversationId ?? conversationId;
106
+ if (!id)
107
+ return null;
108
+ try {
109
+ const scope = normalizeScope(client, options);
110
+ applyPodScope(client, scope.podId);
111
+ const nextConversation = await client.conversations.get(id, {
112
+ pod_id: scope.podId ?? undefined,
113
+ });
114
+ setConversation(nextConversation);
115
+ const nextStatus = typeof nextConversation.status === "string"
116
+ ? nextConversation.status.toUpperCase()
117
+ : undefined;
118
+ setStatus(nextStatus);
119
+ if (nextStatus) {
120
+ onStatus?.(nextStatus);
121
+ }
122
+ return nextConversation;
123
+ }
124
+ catch (refreshError) {
125
+ const normalized = normalizeError(refreshError, "Failed to fetch conversation.");
126
+ setError(normalized);
127
+ onError?.(refreshError);
128
+ return null;
129
+ }
130
+ }, [client, conversationId, onError, onStatus, options]);
131
+ const loadMessages = useCallback(async (input = {}) => {
132
+ const id = input.conversationId ?? conversationId;
133
+ if (!id) {
134
+ return { items: [], limit: input.limit ?? 20, next_page_token: null };
135
+ }
136
+ try {
137
+ const scope = normalizeScope(client, options);
138
+ applyPodScope(client, scope.podId);
139
+ const response = await client.conversations.messages.list(id, {
140
+ pod_id: scope.podId ?? undefined,
141
+ limit: input.limit,
142
+ page_token: input.pageToken,
143
+ });
144
+ const nextMessages = response.items ?? [];
145
+ setMessages(nextMessages);
146
+ return {
147
+ items: nextMessages,
148
+ limit: response.limit ?? input.limit ?? 20,
149
+ next_page_token: response.next_page_token,
150
+ };
151
+ }
152
+ catch (messageError) {
153
+ const normalized = normalizeError(messageError, "Failed to fetch conversation messages.");
154
+ setError(normalized);
155
+ onError?.(messageError);
156
+ return {
157
+ items: [],
158
+ limit: input.limit ?? 20,
159
+ next_page_token: null,
160
+ };
161
+ }
162
+ }, [client, conversationId, onError, options]);
163
+ const consume = useCallback(async (stream, controller) => {
164
+ setIsStreaming(true);
165
+ setError(null);
166
+ try {
167
+ for await (const event of readSSE(stream)) {
168
+ if (controller.signal.aborted) {
169
+ break;
170
+ }
171
+ const payload = parseSSEJson(event);
172
+ onEvent?.(event, payload);
173
+ const parsed = parseAssistantStreamEvent(payload);
174
+ if (parsed.message) {
175
+ setMessages((previous) => upsertConversationMessage(previous, parsed.message));
176
+ onMessage?.(parsed.message);
177
+ }
178
+ if (parsed.status) {
179
+ setStatus(parsed.status);
180
+ onStatus?.(parsed.status);
181
+ }
182
+ }
183
+ }
184
+ catch (streamError) {
185
+ if (!(streamError instanceof Error && streamError.name === "AbortError")) {
186
+ const normalized = normalizeError(streamError, "Failed to stream assistant run.");
187
+ setError(normalized);
188
+ onError?.(streamError);
189
+ }
190
+ }
191
+ finally {
192
+ if (abortRef.current === controller) {
193
+ abortRef.current = null;
194
+ }
195
+ setIsStreaming(false);
196
+ }
197
+ }, [onError, onEvent, onMessage, onStatus]);
198
+ const ensureConversation = useCallback(async (overrideConversationId, options) => {
199
+ const existingId = overrideConversationId ?? conversationId;
200
+ if (existingId) {
201
+ const existing = await refreshConversation(existingId);
202
+ if (existing)
203
+ return existing;
204
+ throw new Error("Failed to resolve existing conversation.");
205
+ }
206
+ if (options?.createIfMissing !== true) {
207
+ throw new Error("conversationId is required.");
208
+ }
209
+ return createConversation({
210
+ ...(options.createConversation ?? {}),
211
+ setActive: true,
212
+ });
213
+ }, [conversationId, createConversation, refreshConversation]);
214
+ const sendMessage = useCallback(async (content, input = {}) => {
215
+ const resolvedConversation = await ensureConversation(input.conversationId, input);
216
+ const resolvedConversationId = requireConversationId(resolvedConversation.id);
217
+ cancel();
218
+ const controller = new AbortController();
219
+ abortRef.current = controller;
220
+ const scope = normalizeScope(client, options, input.createConversation);
221
+ applyPodScope(client, scope.podId);
222
+ const stream = await client.conversations.sendMessageStream(resolvedConversationId, { content }, {
223
+ pod_id: scope.podId ?? undefined,
224
+ signal: controller.signal,
225
+ });
226
+ await consume(stream, controller);
227
+ return resolvedConversation;
228
+ }, [cancel, client, consume, ensureConversation, options]);
229
+ const resume = useCallback(async (explicitConversationId) => {
230
+ const id = requireConversationId(explicitConversationId ?? conversationId);
231
+ cancel();
232
+ const controller = new AbortController();
233
+ abortRef.current = controller;
234
+ const scope = normalizeScope(client, options);
235
+ applyPodScope(client, scope.podId);
236
+ const stream = await client.conversations.resumeStream(id, {
237
+ pod_id: scope.podId ?? undefined,
238
+ signal: controller.signal,
239
+ });
240
+ await consume(stream, controller);
241
+ }, [cancel, client, consume, conversationId, options]);
242
+ const stop = useCallback(async (explicitConversationId) => {
243
+ const id = requireConversationId(explicitConversationId ?? conversationId);
244
+ const scope = normalizeScope(client, options);
245
+ applyPodScope(client, scope.podId);
246
+ await client.conversations.stopRun(id, {
247
+ pod_id: scope.podId ?? undefined,
248
+ });
249
+ }, [client, conversationId, options]);
250
+ const clearMessages = useCallback(() => {
251
+ setMessages([]);
252
+ }, []);
253
+ useEffect(() => {
254
+ if (!autoLoad || !conversationId) {
255
+ return;
256
+ }
257
+ void refreshConversation(conversationId);
258
+ void loadMessages({ conversationId });
259
+ }, [autoLoad, conversationId, loadMessages, refreshConversation]);
260
+ return {
261
+ conversationId,
262
+ conversation,
263
+ status,
264
+ messages,
265
+ isStreaming,
266
+ error,
267
+ setConversationId,
268
+ listConversations,
269
+ createConversation,
270
+ refreshConversation,
271
+ loadMessages,
272
+ sendMessage,
273
+ resume,
274
+ stop,
275
+ cancel,
276
+ clearMessages,
277
+ };
278
+ }
@@ -0,0 +1,19 @@
1
+ import type { FlowRun } from "../types.js";
2
+ import type { UseFlowSessionResult } from "./useFlowSession.js";
3
+ export interface UseFlowRunHistoryOptions {
4
+ session: Pick<UseFlowSessionResult, "run" | "runId" | "setRunId" | "listHistory">;
5
+ flowName: string;
6
+ limit?: number;
7
+ autoRefresh?: boolean;
8
+ }
9
+ export interface UseFlowRunHistoryResult {
10
+ runs: FlowRun[];
11
+ selectedRunId: string | null;
12
+ effectiveSelectedRunId: string | null;
13
+ selectedRun: FlowRun | null;
14
+ isLoading: boolean;
15
+ error: Error | null;
16
+ setSelectedRunId: (runId: string | null) => void;
17
+ refresh: () => Promise<FlowRun[]>;
18
+ }
19
+ export declare function useFlowRunHistory({ session, flowName, limit, autoRefresh, }: UseFlowRunHistoryOptions): UseFlowRunHistoryResult;
@@ -0,0 +1,77 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ function normalizeError(error, fallback) {
3
+ if (error instanceof Error)
4
+ return error;
5
+ return new Error(fallback);
6
+ }
7
+ export function useFlowRunHistory({ session, flowName, limit = 100, autoRefresh = true, }) {
8
+ const { listHistory, run: liveRun, runId: liveRunId, setRunId } = session;
9
+ const [runs, setRuns] = useState([]);
10
+ const [selectedRunId, setSelectedRunId] = useState(null);
11
+ const [isLoading, setIsLoading] = useState(false);
12
+ const [error, setError] = useState(null);
13
+ const effectiveSelectedRunId = selectedRunId ?? runs[0]?.id ?? null;
14
+ const refresh = useCallback(async () => {
15
+ setIsLoading(true);
16
+ setError(null);
17
+ try {
18
+ const nextRuns = await listHistory({ flowName, limit });
19
+ setRuns(nextRuns);
20
+ setSelectedRunId((previous) => (previous && nextRuns.some((run) => run.id === previous) ? previous : null));
21
+ return nextRuns;
22
+ }
23
+ catch (refreshError) {
24
+ const normalized = normalizeError(refreshError, "Failed to list flow runs.");
25
+ setError(normalized);
26
+ return [];
27
+ }
28
+ finally {
29
+ setIsLoading(false);
30
+ }
31
+ }, [flowName, limit, listHistory]);
32
+ useEffect(() => {
33
+ if (!autoRefresh)
34
+ return;
35
+ void refresh();
36
+ }, [autoRefresh, flowName, refresh]);
37
+ useEffect(() => {
38
+ if (!autoRefresh || !liveRunId)
39
+ return;
40
+ void refresh();
41
+ }, [autoRefresh, liveRunId, refresh]);
42
+ useEffect(() => {
43
+ if (!effectiveSelectedRunId)
44
+ return;
45
+ setRunId(effectiveSelectedRunId);
46
+ }, [effectiveSelectedRunId, setRunId]);
47
+ useEffect(() => {
48
+ if (!liveRun?.id)
49
+ return;
50
+ setRuns((previous) => {
51
+ const index = previous.findIndex((run) => run.id === liveRun.id);
52
+ if (index === -1) {
53
+ return [liveRun, ...previous];
54
+ }
55
+ const next = [...previous];
56
+ next[index] = { ...next[index], ...liveRun };
57
+ return next;
58
+ });
59
+ }, [liveRun]);
60
+ const selectedRun = useMemo(() => {
61
+ if (!effectiveSelectedRunId)
62
+ return null;
63
+ if (liveRun?.id === effectiveSelectedRunId)
64
+ return liveRun;
65
+ return runs.find((run) => run.id === effectiveSelectedRunId) ?? null;
66
+ }, [effectiveSelectedRunId, liveRun, runs]);
67
+ return {
68
+ runs,
69
+ selectedRunId,
70
+ effectiveSelectedRunId,
71
+ selectedRun,
72
+ isLoading,
73
+ error,
74
+ setSelectedRunId,
75
+ refresh,
76
+ };
77
+ }