@alia.onl/sdk 15.7.0 → 19.0.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.
Files changed (150) hide show
  1. package/CHANGELOG.md +255 -12
  2. package/README.md +241 -155
  3. package/package.json +17 -5
  4. package/src/animations.ts +0 -1
  5. package/src/components/AliaAskProvider.tsx +127 -0
  6. package/src/components/AliaChatContent.tsx +604 -392
  7. package/src/components/AliaChatHeader.tsx +14 -38
  8. package/src/components/AliaChatScreen.tsx +56 -31
  9. package/src/components/AliaChatSheet.tsx +110 -36
  10. package/src/components/AliaComposer.tsx +1 -1
  11. package/src/components/AliaParticipants.tsx +202 -0
  12. package/src/components/AliaParticipantsHeader.tsx +60 -0
  13. package/src/components/AliaSharedConversation.tsx +86 -0
  14. package/src/components/AliaWelcomeMessage.tsx +18 -5
  15. package/src/components/Image.tsx +12 -0
  16. package/src/components/Markdown.tsx +498 -14
  17. package/src/components/PlanPreviewCard.tsx +7 -49
  18. package/src/components/ReplyMarkdown.tsx +359 -0
  19. package/src/components/RichBlocks.tsx +195 -0
  20. package/src/components/canvas/CanvasComponent.tsx +59 -0
  21. package/src/components/canvas/ChartRenderer.tsx +73 -0
  22. package/src/components/canvas/CodeRenderer.tsx +24 -0
  23. package/src/components/canvas/FormRenderer.tsx +101 -0
  24. package/src/components/canvas/MarkdownRenderer.tsx +14 -0
  25. package/src/components/canvas/TableRenderer.tsx +47 -0
  26. package/src/components/thought/FilesAndSources.tsx +120 -0
  27. package/src/components/thought/ThoughtPanel.tsx +417 -0
  28. package/src/components/thought/ToolStep.tsx +141 -0
  29. package/src/components/thread/AgentResultCard.tsx +125 -0
  30. package/src/components/thread/AgentTaskCard.tsx +127 -0
  31. package/src/components/thread/AliaThread.tsx +401 -0
  32. package/src/components/thread/MessageBlockBoundary.tsx +78 -0
  33. package/src/components/thread/NewConversationOffer.tsx +54 -0
  34. package/src/components/thread/ThreadRow.tsx +458 -0
  35. package/src/components/thread/ThreadSearch.tsx +129 -0
  36. package/src/components/thread/ToolResultCard.tsx +254 -0
  37. package/src/components/thread/TurnMenu.tsx +54 -0
  38. package/src/components/thread/TurnStatusLine.tsx +34 -0
  39. package/src/components/voice/AliaVoiceCall.tsx +21 -0
  40. package/src/components/voice/VoiceControls.tsx +2 -28
  41. package/src/components/voice/VoiceSession.tsx +39 -11
  42. package/src/csp.ts +50 -0
  43. package/src/hooks/useAliaActivity.ts +3 -3
  44. package/src/hooks/useAtBottom.ts +51 -0
  45. package/src/hooks/useAttachMenu.ts +94 -0
  46. package/src/hooks/useChatComposer.ts +299 -0
  47. package/src/hooks/useChatVoice.ts +129 -0
  48. package/src/hooks/useDictation.ts +2 -3
  49. package/src/hooks/useDocumentPicker.ts +3 -29
  50. package/src/hooks/useFailedTurnToast.ts +43 -0
  51. package/src/hooks/useMessageVote.ts +50 -0
  52. package/src/hooks/useOpenThought.ts +79 -0
  53. package/src/hooks/usePendingQuestions.ts +2 -2
  54. package/src/hooks/useReadAloud.ts +92 -0
  55. package/src/hooks/useShareHandler.ts +24 -0
  56. package/src/hooks/useSoundEffects.ts +1 -1
  57. package/src/hooks/useTTS.ts +2 -3
  58. package/src/hooks/useThoughtScope.ts +59 -0
  59. package/src/hooks/useThreadRows.ts +145 -0
  60. package/src/hooks/useThreadView.ts +68 -0
  61. package/src/hooks/useTimelineWindow.ts +256 -0
  62. package/src/hooks/useVoiceLauncher.ts +180 -0
  63. package/src/hooks/useVoiceRoom.ts +2 -6
  64. package/src/index.ts +43 -29
  65. package/src/lib/activity-store.ts +1 -1
  66. package/src/lib/add-menu.ts +7 -0
  67. package/src/lib/call-thread.ts +33 -0
  68. package/src/lib/character-runtime.ts +6 -4
  69. package/src/lib/citations.ts +79 -2
  70. package/src/lib/clipboard.ts +23 -0
  71. package/src/lib/content-utils.ts +10 -0
  72. package/src/lib/copy-en.ts +284 -0
  73. package/src/lib/copy.ts +32 -0
  74. package/src/lib/document-picker.ts +31 -0
  75. package/src/lib/flights.ts +22 -13
  76. package/src/lib/focus-return.ts +66 -0
  77. package/src/lib/keyboard.native.tsx +19 -0
  78. package/src/lib/keyboard.tsx +9 -0
  79. package/src/lib/labels.tsx +69 -17
  80. package/src/lib/origins.ts +15 -0
  81. package/src/lib/participants.ts +86 -0
  82. package/src/lib/power-level-options.ts +66 -0
  83. package/src/lib/questions.ts +2 -2
  84. package/src/lib/task-labels.ts +4 -4
  85. package/src/lib/tool-call-status.ts +1 -1
  86. package/src/lib/tool-icons.ts +36 -0
  87. package/src/lib/tool-renderers.tsx +48 -0
  88. package/src/lib/turn-state.ts +2 -2
  89. package/src/lib/voice-audio-engine.ts +14 -158
  90. package/src/lib/voice-call-request.ts +49 -0
  91. package/src/lib/voice-call.ts +76 -34
  92. package/src/lib/voice-controls-labels.ts +31 -0
  93. package/src/lib/voice-protocol.ts +32 -14
  94. package/src/lib/voice-worklets.ts +19 -0
  95. package/src/lib/work-log.ts +6 -6
  96. package/src/runtime/agents.ts +151 -0
  97. package/src/runtime/api.ts +123 -0
  98. package/src/runtime/attachment-utils.ts +96 -0
  99. package/src/runtime/composer-draft-store.ts +270 -0
  100. package/src/runtime/config.tsx +186 -0
  101. package/src/runtime/conversation-store.ts +49 -0
  102. package/src/runtime/conversations.ts +557 -0
  103. package/src/runtime/error-utils.ts +104 -0
  104. package/src/runtime/host-tools.ts +140 -0
  105. package/src/runtime/index.ts +113 -0
  106. package/src/runtime/message-history.ts +124 -0
  107. package/src/runtime/model.ts +39 -0
  108. package/src/runtime/oxy-files.ts +37 -0
  109. package/src/runtime/power-levels.ts +66 -0
  110. package/src/runtime/query-keys.ts +34 -0
  111. package/src/runtime/random-uuid.ts +25 -0
  112. package/src/runtime/routes.ts +51 -0
  113. package/src/runtime/sharing.ts +165 -0
  114. package/src/runtime/sse-frame-reader.ts +88 -0
  115. package/src/runtime/stream-events.ts +146 -0
  116. package/src/runtime/stream-outcome.ts +21 -0
  117. package/src/runtime/thought-store.ts +103 -0
  118. package/src/runtime/thread/agent-activity.ts +261 -0
  119. package/src/runtime/thread/automation-labels.ts +79 -0
  120. package/src/runtime/thread/conversation-export.ts +135 -0
  121. package/src/runtime/thread/message-days.ts +120 -0
  122. package/src/runtime/thread/message-processor.ts +93 -0
  123. package/src/runtime/thread/task-utils.ts +11 -0
  124. package/src/runtime/thread/thought-utils.ts +752 -0
  125. package/src/runtime/thread/thread-history.ts +139 -0
  126. package/src/runtime/thread/timeline.ts +202 -0
  127. package/src/runtime/thread/title-tags.ts +8 -0
  128. package/src/runtime/thread/tool-cards.ts +383 -0
  129. package/src/runtime/turn-failure.ts +108 -0
  130. package/src/runtime/types.ts +148 -0
  131. package/src/runtime/usage-limit-error.ts +44 -0
  132. package/src/runtime/use-agent-activity.ts +80 -0
  133. package/src/runtime/use-conversation-stream.ts +1427 -0
  134. package/src/runtime/use-conversation-thread.ts +556 -0
  135. package/src/runtime/use-start-conversation.ts +107 -0
  136. package/src/runtime/use-thread-history.ts +64 -0
  137. package/src/runtime/use-thread-search.ts +95 -0
  138. package/src/runtime/use-turn-edit.ts +99 -0
  139. package/src/tools.ts +2 -2
  140. package/src/types.ts +18 -47
  141. package/worklets/voice-processors.js +152 -0
  142. package/src/components/AliaChatMessageList.tsx +0 -532
  143. package/src/components/ThinkingBlock.tsx +0 -43
  144. package/src/hooks/useAliaChat.ts +0 -573
  145. package/src/lib/api-url.ts +0 -2
  146. package/src/lib/catalogue.ts +0 -236
  147. package/src/lib/chat-stream.ts +0 -655
  148. package/src/lib/chat-transport.ts +0 -98
  149. package/src/lib/delegation.ts +0 -34
  150. package/src/lib/work-log-copy.ts +0 -61
@@ -1,573 +0,0 @@
1
- import React, { useCallback, useEffect, useId, useRef, useState } from 'react';
2
- import { useOxy } from '@oxy.so/services/ui/client';
3
- import type { ChatMessage, ToolInvocation } from '../types';
4
- import { getTextFromContent, type MessageContent } from '../lib/content-utils';
5
- import { resolveModelId } from '../lib/catalogue';
6
- import { agentMessageId, delegatedAgent } from '../lib/delegation';
7
- import { streamAliaChat } from '../lib/chat-transport';
8
- import type { AliaChatStreamEvent } from '../lib/chat-stream';
9
-
10
- import { ALIA_API_URL } from '../lib/api-url';
11
- import { useAliaActivityStore } from '../lib/activity-store';
12
- import { turnActivityState } from '../lib/turn-state';
13
-
14
- export interface UseAliaChatOptions {
15
- /**
16
- * The agents the host knows, with their look: a delegation's thinking avatar is drawn from here
17
- * until the agent's answer brings its own. Without it (or for an agent it lacks) the default look.
18
- */
19
- agents?: ReadonlyArray<{ id: string; character?: unknown }>;
20
- /**
21
- * Base URL the hook sends its requests to. Defaults to
22
- * `EXPO_PUBLIC_ALIA_API_URL`, then `https://api.alia.onl`.
23
- *
24
- * The hook appends its own paths — `POST /v1/chat/completions` for a turn and
25
- * `GET /catalogue` for the model check — so the value is a host, not a route.
26
- *
27
- * Point this at YOUR OWN backend to use the SDK from an origin Alia does not
28
- * enumerate: Alia's product route, `POST /alia/chat`, answers CORS preflights
29
- * only for Alia's own origins, while a server-to-server call carries no
30
- * `Origin` header and is answered on its credential alone. Your backend
31
- * receives the SDK's request, forwards the `Authorization` header it carries
32
- * (the signed-in user's Oxy access token) to `/alia/chat`, and streams the
33
- * SSE body back unchanged. The README's *consumer-backend path* section has
34
- * the minimal relay; issue #244 records why this is the supported shape today.
35
- */
36
- apiUrl?: string;
37
- /**
38
- * The model to answer with, as `publisher/model` from `GET /catalogue`.
39
- *
40
- * Optional, and checked against the catalogue before a request carries it: an
41
- * identifier the server no longer offers is not sent, so a retirement does not
42
- * turn into a 400 inside a consumer's app. Omitted, the request carries no
43
- * `model` and the server's default model answers.
44
- */
45
- model?: string;
46
- /** App context injected as system message so Alia knows which app the user is in. */
47
- clientContext?: string;
48
- /**
49
- * Optional assertion for callers that already hold the surrounding
50
- * OxyProvider's active token. A different token is refused; the SDK never
51
- * writes this value to an Authorization header itself.
52
- */
53
- accessToken?: string;
54
- }
55
-
56
- export interface SendOptions {
57
- images?: ReadonlyArray<string>;
58
- }
59
-
60
- export interface UseAliaChatReturn {
61
- messages: ChatMessage[];
62
- setMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>;
63
- /** `images` are `data:` or `https:` URLs sent with the turn as `image_url` parts. */
64
- send: (text: string, options?: SendOptions) => void;
65
- isStreaming: boolean;
66
- stop: () => void;
67
- clear: () => void;
68
- /** The person accepted a proposed plan; the card collapses to "approved". */
69
- approvePlan: (planId: string) => void;
70
- /** The person cancelled a proposed plan: the card collapses to "cancelled" and the turn stops. */
71
- rejectPlan: (planId: string) => void;
72
- error: string | null;
73
- }
74
-
75
- interface ActiveRequest {
76
- readonly key: symbol;
77
- readonly controller: AbortController;
78
- }
79
-
80
- interface PendingUpdates {
81
- readonly assistantId: string;
82
- content: string;
83
- reasoning: string;
84
- tools: ToolInvocation[];
85
- /** How many delegated agents have answered in this turn. */
86
- agentAnswers: number;
87
- timer: ReturnType<typeof setTimeout> | null;
88
- }
89
-
90
- function generateId(): string {
91
- if (typeof crypto !== 'undefined' && crypto.randomUUID) {
92
- return `msg-${crypto.randomUUID()}`;
93
- }
94
- return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
95
- }
96
-
97
- function isAbort(error: unknown, signal: AbortSignal): boolean {
98
- return signal.aborted || (error instanceof Error && error.name === 'AbortError');
99
- }
100
-
101
- function abortError(): Error {
102
- const error = new Error('The Alia request was aborted.');
103
- error.name = 'AbortError';
104
- return error;
105
- }
106
-
107
- function waitWithAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
108
- if (signal.aborted) return Promise.reject(abortError());
109
- return new Promise<T>((resolve, reject) => {
110
- const onAbort = (): void => reject(abortError());
111
- signal.addEventListener('abort', onAbort, { once: true });
112
- promise.then(
113
- (value) => {
114
- signal.removeEventListener('abort', onAbort);
115
- resolve(value);
116
- },
117
- (error: unknown) => {
118
- signal.removeEventListener('abort', onAbort);
119
- reject(error);
120
- },
121
- );
122
- });
123
- }
124
-
125
- /**
126
- * SSE streaming chat hook for Alia.
127
- *
128
- * The SDK deliberately stays on `/v1/chat/completions`: unlike the product
129
- * origin, this raw-source package is embedded by apps whose browser origins are
130
- * not all on Alia's narrow CORS allowlist. The endpoint is still Alia's product
131
- * handler during its recorded compatibility window.
132
- */
133
- export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn {
134
- const { apiUrl = ALIA_API_URL, model, clientContext, accessToken: accessTokenProp } = options;
135
- const directoryRef = useRef(new Map<string, { id: string; character?: unknown }>());
136
- directoryRef.current = new Map((options.agents ?? []).map((agent) => [agent.id, agent]));
137
- const requestUrl = '/v1/chat/completions';
138
-
139
- const { oxyServices } = useOxy();
140
- const [messages, setMessages] = useState<ChatMessage[]>([]);
141
- const [isStreaming, setIsStreaming] = useState(false);
142
- const [error, setError] = useState<string | null>(null);
143
- const messagesRef = useRef<ChatMessage[]>([]);
144
- const activeRequestRef = useRef<ActiveRequest | null>(null);
145
- const pendingRef = useRef<PendingUpdates | null>(null);
146
- const mountedRef = useRef(true);
147
-
148
- useEffect(() => {
149
- messagesRef.current = messages;
150
- }, [messages]);
151
-
152
- // The turn's lifecycle, published for every "Ask Alia" button (`useAliaActivity`).
153
- const activitySource = useId();
154
- const activity = turnActivityState(messages, isStreaming);
155
- useEffect(() => {
156
- const { setLocal } = useAliaActivityStore.getState();
157
- setLocal(activitySource, activity);
158
- return () => setLocal(activitySource, 'idle');
159
- }, [activitySource, activity]);
160
-
161
- const updateAssistant = useCallback(
162
- (assistantId: string, updater: (message: ChatMessage) => Partial<ChatMessage>) => {
163
- if (!mountedRef.current) return;
164
- setMessages((previous) => {
165
- const index = previous.findIndex((message) => message.id === assistantId);
166
- if (index < 0 || previous[index]?.role !== 'assistant') return previous;
167
- const updated = [...previous];
168
- const current = updated[index];
169
- if (!current) return previous;
170
- updated[index] = { ...current, ...updater(current) };
171
- messagesRef.current = updated;
172
- return updated;
173
- });
174
- },
175
- [],
176
- );
177
-
178
- const removeEmptyAssistant = useCallback((assistantId: string) => {
179
- if (!mountedRef.current) return;
180
- setMessages((previous) => {
181
- const target = previous.find((message) => message.id === assistantId);
182
- if (
183
- target?.role !== 'assistant' ||
184
- getTextFromContent(target.content).trim().length > 0 ||
185
- (target.thinking?.trim().length ?? 0) > 0 ||
186
- (target.toolInvocations?.length ?? 0) > 0
187
- ) {
188
- return previous;
189
- }
190
- const updated = previous.filter((message) => message.id !== assistantId);
191
- messagesRef.current = updated;
192
- return updated;
193
- });
194
- }, []);
195
-
196
- /**
197
- * An agent's message of this turn, before Alia's own: opened empty by the call that sends for the
198
- * agent, filled by its answer. Without an open one, an answer makes a message of its own.
199
- */
200
- const insertAgentMessage = useCallback(
201
- (
202
- assistantId: string,
203
- agent: { id: string; name: string; character?: unknown },
204
- content: string,
205
- ) => {
206
- if (!mountedRef.current) return;
207
- const id = agentMessageId(assistantId, agent.id);
208
- setMessages((previous) => {
209
- const open = previous.find((message) => message.id === id);
210
- let updated: ChatMessage[];
211
- if (open && (content === '' || getTextFromContent(open.content) === '')) {
212
- // Already open: an answer fills it; a second call to the same agent changes nothing.
213
- if (content === '') return previous;
214
- // The answer carries the look the server has now: it replaces the one the call guessed.
215
- updated = previous.map((message) =>
216
- message === open
217
- ? {
218
- ...open,
219
- content,
220
- agentInfo: {
221
- id: agent.id,
222
- name: agent.name,
223
- character: agent.character ?? open.agentInfo?.character,
224
- },
225
- }
226
- : message,
227
- );
228
- } else {
229
- const message: ChatMessage = {
230
- id: open ? `${id}-${previous.length}` : id,
231
- role: 'assistant',
232
- content,
233
- createdAt: Date.now(),
234
- agentInfo: { id: agent.id, name: agent.name, character: agent.character },
235
- };
236
- const at = previous.findIndex((candidate) => candidate.id === assistantId);
237
- updated = [...previous];
238
- updated.splice(at < 0 ? updated.length : at, 0, message);
239
- }
240
- messagesRef.current = updated;
241
- return updated;
242
- });
243
- },
244
- [],
245
- );
246
-
247
- /** A turn that ends with an agent never answering leaves no empty message of the agent's. */
248
- const removeUnansweredAgents = useCallback((assistantId: string) => {
249
- if (!mountedRef.current) return;
250
- setMessages((previous) => {
251
- const updated = previous.filter(
252
- (message) =>
253
- !(
254
- message.agentInfo !== undefined &&
255
- message.id.startsWith(`${assistantId}-agent-`) &&
256
- getTextFromContent(message.content).trim() === ''
257
- ),
258
- );
259
- if (updated.length === previous.length) return previous;
260
- messagesRef.current = updated;
261
- return updated;
262
- });
263
- }, []);
264
-
265
- const flushPendingUpdates = useCallback(
266
- (pending: PendingUpdates) => {
267
- const content = pending.content;
268
- const reasoning = pending.reasoning;
269
- const tools = [...pending.tools];
270
- if (!content && !reasoning && tools.length === 0) return;
271
-
272
- pending.content = '';
273
- pending.reasoning = '';
274
- updateAssistant(pending.assistantId, (last) => ({
275
- ...(content ? { content: getTextFromContent(last.content) + content } : {}),
276
- ...(reasoning ? { thinking: (last.thinking ?? '') + reasoning } : {}),
277
- ...(tools.length > 0 ? { toolInvocations: tools } : {}),
278
- }));
279
- },
280
- [updateAssistant],
281
- );
282
-
283
- const cancelPendingTimer = useCallback((pending: PendingUpdates) => {
284
- if (pending.timer !== null) {
285
- clearTimeout(pending.timer);
286
- pending.timer = null;
287
- }
288
- }, []);
289
-
290
- const scheduleFlush = useCallback(
291
- (pending: PendingUpdates) => {
292
- if (pending.timer !== null) return;
293
- pending.timer = setTimeout(() => {
294
- pending.timer = null;
295
- flushPendingUpdates(pending);
296
- }, 50);
297
- },
298
- [flushPendingUpdates],
299
- );
300
-
301
- const stop = useCallback(() => {
302
- const active = activeRequestRef.current;
303
- activeRequestRef.current = null;
304
- active?.controller.abort();
305
-
306
- const pending = pendingRef.current;
307
- if (pending !== null) {
308
- cancelPendingTimer(pending);
309
- flushPendingUpdates(pending);
310
- pendingRef.current = null;
311
- }
312
- if (mountedRef.current) setIsStreaming(false);
313
- }, [cancelPendingTimer, flushPendingUpdates]);
314
-
315
- useEffect(() => {
316
- mountedRef.current = true;
317
- return () => {
318
- mountedRef.current = false;
319
- activeRequestRef.current?.controller.abort();
320
- activeRequestRef.current = null;
321
- const pending = pendingRef.current;
322
- if (pending !== null) cancelPendingTimer(pending);
323
- pendingRef.current = null;
324
- };
325
- }, [cancelPendingTimer]);
326
-
327
- const send = useCallback(
328
- async (text: string, options?: SendOptions) => {
329
- const trimmed = text.trim();
330
- const images = options?.images ?? [];
331
- if (!trimmed && images.length === 0) return;
332
-
333
- const activeToken = oxyServices.session.accessToken;
334
- if (accessTokenProp !== undefined && accessTokenProp !== activeToken) {
335
- setError('The supplied token is not the active Oxy session.');
336
- return;
337
- }
338
-
339
- // A new turn supersedes the previous one. Scope every later update to its
340
- // assistant id and request key so an abort racing with the replacement
341
- // cannot mutate the new placeholder or clear its streaming state.
342
- const previousRequest = activeRequestRef.current;
343
- activeRequestRef.current = null;
344
- previousRequest?.controller.abort();
345
- const previousPending = pendingRef.current;
346
- if (previousPending !== null) {
347
- cancelPendingTimer(previousPending);
348
- flushPendingUpdates(previousPending);
349
- }
350
-
351
- const userContent: MessageContent =
352
- images.length > 0
353
- ? [
354
- ...(trimmed ? [{ type: 'text', text: trimmed }] : []),
355
- ...images.map((url) => ({ type: 'image_url', image_url: { url } })),
356
- ]
357
- : trimmed;
358
- const userMessage: ChatMessage = {
359
- id: generateId(),
360
- role: 'user',
361
- content: userContent,
362
- createdAt: Date.now(),
363
- };
364
- const assistantMessage: ChatMessage = {
365
- id: generateId(),
366
- role: 'assistant',
367
- content: '',
368
- toolInvocations: [],
369
- createdAt: Date.now(),
370
- };
371
- const history = messagesRef.current.filter(
372
- (message) =>
373
- message.role !== 'assistant' ||
374
- getTextFromContent(message.content).trim().length > 0 ||
375
- (message.thinking?.trim().length ?? 0) > 0 ||
376
- (message.toolInvocations?.length ?? 0) > 0,
377
- );
378
- setMessages((previous) => {
379
- const updated = [...previous, userMessage, assistantMessage];
380
- messagesRef.current = updated;
381
- return updated;
382
- });
383
- setError(null);
384
- setIsStreaming(true);
385
-
386
- const apiMessages: Array<{ role: string; content: MessageContent }> = [];
387
- if (clientContext) apiMessages.push({ role: 'system', content: clientContext });
388
- for (const message of history) {
389
- if (message.role === 'system') continue;
390
- apiMessages.push({ role: message.role, content: getTextFromContent(message.content) });
391
- for (const tool of message.toolInvocations ?? []) {
392
- if (tool.state === 'result' && tool.result !== undefined && tool.result !== null) {
393
- apiMessages.push({
394
- role: 'system',
395
- content: `[Tool result from ${tool.toolName}: ${JSON.stringify(tool.result).slice(0, 500)}]`,
396
- });
397
- }
398
- }
399
- }
400
- apiMessages.push({ role: 'user', content: userContent });
401
-
402
- const pending: PendingUpdates = {
403
- assistantId: assistantMessage.id,
404
- content: '',
405
- reasoning: '',
406
- tools: [],
407
- agentAnswers: 0,
408
- timer: null,
409
- };
410
- pendingRef.current = pending;
411
- const controller = new AbortController();
412
- const request: ActiveRequest = { key: Symbol('alia-chat-request'), controller };
413
- activeRequestRef.current = request;
414
- const linked = oxyServices.createLinkedClient({ baseURL: apiUrl });
415
-
416
- const applyEvent = (event: AliaChatStreamEvent): void => {
417
- switch (event.kind) {
418
- case 'content':
419
- pending.content += event.content;
420
- scheduleFlush(pending);
421
- return;
422
- case 'reasoning':
423
- pending.reasoning += event.content;
424
- scheduleFlush(pending);
425
- return;
426
- case 'tool_call': {
427
- const invocation: ToolInvocation = {
428
- toolCallId: event.toolCallId,
429
- toolName: event.toolName,
430
- state: 'call',
431
- args: event.args,
432
- };
433
- const index = pending.tools.findIndex((tool) => tool.toolCallId === event.toolCallId);
434
- if (index < 0) pending.tools = [...pending.tools, invocation];
435
- else pending.tools[index] = invocation;
436
- flushPendingUpdates(pending);
437
- // A call to an agent opens the agent's message at once: empty, thinking, until it answers.
438
- const agent = delegatedAgent(invocation);
439
- // The call names the agent; its look is the host's directory's, until the answer brings the server's.
440
- if (agent)
441
- insertAgentMessage(
442
- pending.assistantId,
443
- { ...agent, character: directoryRef.current.get(agent.id)?.character },
444
- '',
445
- );
446
- return;
447
- }
448
- case 'tool_result': {
449
- const index = pending.tools.findIndex((tool) => tool.toolCallId === event.toolCallId);
450
- const result: ToolInvocation = {
451
- toolCallId: event.toolCallId,
452
- toolName: event.toolName,
453
- state: 'result',
454
- result: event.output,
455
- ...(index < 0 ? {} : { args: pending.tools[index]?.args }),
456
- };
457
- if (index < 0) pending.tools = [...pending.tools, result];
458
- else pending.tools[index] = result;
459
- flushPendingUpdates(pending);
460
- return;
461
- }
462
- case 'research_progress':
463
- updateAssistant(pending.assistantId, (last) => ({
464
- researchProgress: {
465
- ...event.progress,
466
- subQuestions: event.progress.subQuestions ?? last.researchProgress?.subQuestions,
467
- },
468
- }));
469
- return;
470
- case 'plan_preview':
471
- updateAssistant(pending.assistantId, () => ({
472
- pendingPlan: {
473
- planId: event.planId,
474
- steps: event.steps,
475
- approved: false,
476
- rejected: false,
477
- },
478
- }));
479
- return;
480
- case 'agent_answer': {
481
- // A delegated agent speaks as itself: its answer is its own message, placed before
482
- // Alia's reply, as the Alia app places it. The call that sent for it already opened
483
- // the message; an answer with no call opens it. Several agents are several messages.
484
- pending.agentAnswers++;
485
- insertAgentMessage(pending.assistantId, event.agent, event.content);
486
- return;
487
- }
488
- }
489
- };
490
-
491
- try {
492
- const effectiveModel = await waitWithAbort(
493
- resolveModelId(apiUrl, model),
494
- controller.signal,
495
- );
496
- await streamAliaChat(
497
- linked.client,
498
- { url: requestUrl, model: effectiveModel, messages: apiMessages },
499
- controller.signal,
500
- applyEvent,
501
- );
502
- } catch (caught: unknown) {
503
- cancelPendingTimer(pending);
504
- flushPendingUpdates(pending);
505
- if (isAbort(caught, controller.signal)) {
506
- removeEmptyAssistant(pending.assistantId);
507
- } else if (mountedRef.current) {
508
- const message = caught instanceof Error ? caught.message : 'Something went wrong.';
509
- setError(message);
510
- updateAssistant(pending.assistantId, (last) =>
511
- getTextFromContent(last.content).trim().length === 0
512
- ? { content: "I'm having trouble connecting right now. Please try again." }
513
- : {},
514
- );
515
- }
516
- } finally {
517
- cancelPendingTimer(pending);
518
- flushPendingUpdates(pending);
519
- removeUnansweredAgents(pending.assistantId);
520
- // Alia said nothing herself after handing the turn over: no empty reply under the answers.
521
- if (pending.agentAnswers > 0) removeEmptyAssistant(pending.assistantId);
522
- linked.dispose();
523
- if (pendingRef.current === pending) pendingRef.current = null;
524
- if (activeRequestRef.current?.key === request.key) {
525
- activeRequestRef.current = null;
526
- if (mountedRef.current) setIsStreaming(false);
527
- }
528
- }
529
- },
530
- [
531
- accessTokenProp,
532
- apiUrl,
533
- cancelPendingTimer,
534
- clientContext,
535
- flushPendingUpdates,
536
- model,
537
- oxyServices,
538
- requestUrl,
539
- removeEmptyAssistant,
540
- removeUnansweredAgents,
541
- insertAgentMessage,
542
- scheduleFlush,
543
- updateAssistant,
544
- ],
545
- );
546
-
547
- const clear = useCallback(() => {
548
- stop();
549
- messagesRef.current = [];
550
- setMessages([]);
551
- setError(null);
552
- }, [stop]);
553
-
554
- const decidePlan = useCallback((planId: string, decision: 'approved' | 'rejected') => {
555
- setMessages((prev) =>
556
- prev.map((message) =>
557
- message.pendingPlan?.planId === planId
558
- ? { ...message, pendingPlan: { ...message.pendingPlan, [decision]: true } }
559
- : message,
560
- ),
561
- );
562
- }, []);
563
- const approvePlan = useCallback((planId: string) => decidePlan(planId, 'approved'), [decidePlan]);
564
- const rejectPlan = useCallback(
565
- (planId: string) => {
566
- decidePlan(planId, 'rejected');
567
- stop();
568
- },
569
- [decidePlan, stop],
570
- );
571
-
572
- return { messages, setMessages, send, isStreaming, stop, clear, approvePlan, rejectPlan, error };
573
- }
@@ -1,2 +0,0 @@
1
- /** Where Alia's API is: `EXPO_PUBLIC_ALIA_API_URL`, then `https://api.alia.onl`. */
2
- export const ALIA_API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';