@alia.onl/sdk 15.8.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 (141) hide show
  1. package/CHANGELOG.md +242 -13
  2. package/README.md +223 -151
  3. package/package.json +15 -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/hooks/useAliaActivity.ts +2 -2
  43. package/src/hooks/useAtBottom.ts +51 -0
  44. package/src/hooks/useAttachMenu.ts +94 -0
  45. package/src/hooks/useChatComposer.ts +299 -0
  46. package/src/hooks/useChatVoice.ts +129 -0
  47. package/src/hooks/useDocumentPicker.ts +3 -29
  48. package/src/hooks/useFailedTurnToast.ts +43 -0
  49. package/src/hooks/useMessageVote.ts +50 -0
  50. package/src/hooks/useOpenThought.ts +79 -0
  51. package/src/hooks/usePendingQuestions.ts +2 -2
  52. package/src/hooks/useReadAloud.ts +92 -0
  53. package/src/hooks/useShareHandler.ts +24 -0
  54. package/src/hooks/useSoundEffects.ts +1 -1
  55. package/src/hooks/useThoughtScope.ts +59 -0
  56. package/src/hooks/useThreadRows.ts +145 -0
  57. package/src/hooks/useThreadView.ts +68 -0
  58. package/src/hooks/useTimelineWindow.ts +256 -0
  59. package/src/hooks/useVoiceLauncher.ts +180 -0
  60. package/src/hooks/useVoiceRoom.ts +0 -3
  61. package/src/index.ts +43 -29
  62. package/src/lib/activity-store.ts +1 -1
  63. package/src/lib/add-menu.ts +7 -0
  64. package/src/lib/call-thread.ts +33 -0
  65. package/src/lib/citations.ts +79 -2
  66. package/src/lib/clipboard.ts +23 -0
  67. package/src/lib/content-utils.ts +10 -0
  68. package/src/lib/copy-en.ts +284 -0
  69. package/src/lib/copy.ts +32 -0
  70. package/src/lib/document-picker.ts +31 -0
  71. package/src/lib/flights.ts +22 -13
  72. package/src/lib/focus-return.ts +66 -0
  73. package/src/lib/keyboard.native.tsx +19 -0
  74. package/src/lib/keyboard.tsx +9 -0
  75. package/src/lib/labels.tsx +69 -17
  76. package/src/lib/participants.ts +86 -0
  77. package/src/lib/power-level-options.ts +66 -0
  78. package/src/lib/questions.ts +2 -2
  79. package/src/lib/task-labels.ts +4 -4
  80. package/src/lib/tool-call-status.ts +1 -1
  81. package/src/lib/tool-icons.ts +36 -0
  82. package/src/lib/tool-renderers.tsx +48 -0
  83. package/src/lib/turn-state.ts +2 -2
  84. package/src/lib/voice-call-request.ts +49 -0
  85. package/src/lib/voice-call.ts +76 -34
  86. package/src/lib/voice-controls-labels.ts +31 -0
  87. package/src/lib/voice-protocol.ts +32 -14
  88. package/src/lib/work-log.ts +6 -6
  89. package/src/runtime/agents.ts +151 -0
  90. package/src/runtime/api.ts +123 -0
  91. package/src/runtime/attachment-utils.ts +96 -0
  92. package/src/runtime/composer-draft-store.ts +270 -0
  93. package/src/runtime/config.tsx +186 -0
  94. package/src/runtime/conversation-store.ts +49 -0
  95. package/src/runtime/conversations.ts +557 -0
  96. package/src/runtime/error-utils.ts +104 -0
  97. package/src/runtime/host-tools.ts +140 -0
  98. package/src/runtime/index.ts +113 -0
  99. package/src/runtime/message-history.ts +124 -0
  100. package/src/runtime/model.ts +39 -0
  101. package/src/runtime/oxy-files.ts +37 -0
  102. package/src/runtime/power-levels.ts +66 -0
  103. package/src/runtime/query-keys.ts +34 -0
  104. package/src/runtime/random-uuid.ts +25 -0
  105. package/src/runtime/routes.ts +51 -0
  106. package/src/runtime/sharing.ts +165 -0
  107. package/src/runtime/sse-frame-reader.ts +88 -0
  108. package/src/runtime/stream-events.ts +146 -0
  109. package/src/runtime/stream-outcome.ts +21 -0
  110. package/src/runtime/thought-store.ts +103 -0
  111. package/src/runtime/thread/agent-activity.ts +261 -0
  112. package/src/runtime/thread/automation-labels.ts +79 -0
  113. package/src/runtime/thread/conversation-export.ts +135 -0
  114. package/src/runtime/thread/message-days.ts +120 -0
  115. package/src/runtime/thread/message-processor.ts +93 -0
  116. package/src/runtime/thread/task-utils.ts +11 -0
  117. package/src/runtime/thread/thought-utils.ts +752 -0
  118. package/src/runtime/thread/thread-history.ts +139 -0
  119. package/src/runtime/thread/timeline.ts +202 -0
  120. package/src/runtime/thread/title-tags.ts +8 -0
  121. package/src/runtime/thread/tool-cards.ts +383 -0
  122. package/src/runtime/turn-failure.ts +108 -0
  123. package/src/runtime/types.ts +148 -0
  124. package/src/runtime/usage-limit-error.ts +44 -0
  125. package/src/runtime/use-agent-activity.ts +80 -0
  126. package/src/runtime/use-conversation-stream.ts +1427 -0
  127. package/src/runtime/use-conversation-thread.ts +556 -0
  128. package/src/runtime/use-start-conversation.ts +107 -0
  129. package/src/runtime/use-thread-history.ts +64 -0
  130. package/src/runtime/use-thread-search.ts +95 -0
  131. package/src/runtime/use-turn-edit.ts +99 -0
  132. package/src/tools.ts +2 -2
  133. package/src/types.ts +18 -47
  134. package/src/components/AliaChatMessageList.tsx +0 -532
  135. package/src/components/ThinkingBlock.tsx +0 -43
  136. package/src/hooks/useAliaChat.ts +0 -573
  137. package/src/lib/catalogue.ts +0 -236
  138. package/src/lib/chat-stream.ts +0 -655
  139. package/src/lib/chat-transport.ts +0 -98
  140. package/src/lib/delegation.ts +0 -34
  141. package/src/lib/work-log-copy.ts +0 -61
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @alia.onl/sdk
2
2
 
3
- Alia's chat SDK for React Native and Expo apps built on Oxy: the chat sheet and
4
- screen, the streaming hook, speech in and out, and the primitives they are made
5
- of. It renders inside the one `OxyProvider` your app already mounts and sends
3
+ Alia's chat SDK for React Native and Expo apps built on Oxy: the conversation (the chat sheet,
4
+ the screen, the thread, the thought panel and the composer), the streaming hook, speech in and
5
+ out, and the primitives they are made of. It renders inside the one `OxyProvider` your app already mounts and sends
6
6
  the signed-in user's Oxy session with every request; it never holds a token of
7
7
  its own.
8
8
 
@@ -17,8 +17,11 @@ a prebuilt library.
17
17
  bun add @alia.onl/sdk
18
18
  ```
19
19
 
20
- `react`, `react-native`, `@oxy.so/core`, `@oxy.so/services` and the Expo modules
21
- listed under `peerDependencies` in `package.json` are yours to install. Metro
20
+ `react`, `react-native`, `@oxy.so/core`, `@oxy.so/services`, `@tanstack/react-query`
21
+ (the conversation runtime's cache; `OxyProvider` mounts the client), `react-native-css`
22
+ (the thread's images take `className`) and the Expo modules listed under `peerDependencies` in
23
+ `package.json` — among them `expo-clipboard` and `expo-web-browser`, which the thread's copy
24
+ button and its sources use — are yours to install. Metro
22
25
  must resolve package `exports` (`resolver.unstable_enablePackageExports = true`,
23
26
  the default in Expo SDK 53+), because every icon import is a subpath.
24
27
 
@@ -59,11 +62,26 @@ English). Bloom's own strings follow `BloomProvider`'s `locale`.
59
62
 
60
63
  Entry points:
61
64
 
62
- - `@alia.onl/sdk` — text chat, dictation, read-aloud, catalogue, primitives.
65
+ - `@alia.onl/sdk` — the conversation screen (`AliaChatScreen`, `AliaChatSheet`,
66
+ `AliaChatContent`, the thread `AliaThread`, the work panel `ThoughtPanel`) with its voice call included: the button that opens it, the
67
+ bar with its honest state, the notices (failure, moved to another device, time
68
+ limit) and the transcript in the same thread. Also dictation, read-aloud,
69
+ the conversation runtime, primitives. The call loads on the first call.
70
+ - `@alia.onl/sdk/runtime` — the conversation data layer alone (hooks, cache, stores,
71
+ `AliaRuntimeProvider`), for a sidebar or a test that must not load the chat UI.
72
+ - `@alia.onl/sdk/model` — the conversation as pure data (the records, the timeline, the thought
73
+ panel's steps and sources, tool cards, the markdown export): no React, no Oxy client.
63
74
  - `@alia.onl/sdk/csp` — the CSP the SDK needs from a host page (above).
64
- - `@alia.onl/sdk/voice` — `VoiceSession` and `useVoiceRoom`, the voice call.
65
- Pass `VoiceSession` to the shell as `voiceSession` to offer calls; leave it
66
- out for text-only chat and the call is never compiled.
75
+ - `@alia.onl/sdk/voice` — the call's parts (`VoiceSession`, `useVoiceRoom`,
76
+ the audio engine) for a host that builds its own screen.
77
+
78
+ Voice is on by default; `voice={false}` makes a chat text-only, and
79
+ `voice={{ canStartVoice, onVoiceBlocked, onCallEnded, onNotice, sounds }}` brings
80
+ what only the host knows (the plan and credits gate, what to refresh when a call
81
+ ends, where notices go, the cues it plays). Whether the screen holding the call is the one on
82
+ show is `isFocused` of the chat. The conversation a call lives in is the chat's own. Its words are `labels.voiceControls`,
83
+ `voiceError`, `voiceEnded`, `voiceEndingSoon` and `voiceBlocked`. A host with its
84
+ own screen uses `useVoiceLauncher` and `AliaVoiceCall`, the same pieces.
67
85
 
68
86
  ## Speech
69
87
 
@@ -86,7 +104,7 @@ transcript content.
86
104
 
87
105
  On iOS and Android a call needs
88
106
  [`react-native-audio-api`](https://github.com/software-mansion/react-native-audio-api)
89
- (an optional peer of `@alia.onl/sdk/voice`; the root entry never reaches it)
107
+ (a peer of the SDK: the conversation screen includes the call)
90
108
  and its config plugin, then a new native build:
91
109
 
92
110
  ```json
@@ -117,22 +135,59 @@ On iOS and Android its composer rises with the keyboard through
117
135
  `react-native-keyboard-controller`, which needs the app wrapped in its
118
136
  `KeyboardProvider` (re-exported here).
119
137
 
120
- Or the hook on its own, with your own UI:
138
+ The sheet, the screen and the Alia app are three frames around ONE conversation runtime
139
+ (see [The conversation runtime](#the-conversation-runtime)): they list, open, create, send,
140
+ stop, retry, edit and clear conversations through the same hooks, and a conversation started in
141
+ one is the same conversation in the others.
121
142
 
122
- ```tsx
123
- import { useAliaChat } from '@alia.onl/sdk';
143
+ ## Any agent, from any app
144
+
145
+ Every agent — Alia included — is the same agent (ADR 0020), and any Oxy app opens any agent the
146
+ person can reach. The wire contract is [`docs/agent-embedding.md`](../../docs/agent-embedding.md).
124
147
 
125
- const { messages, send, isStreaming, stop, error } = useAliaChat({
126
- clientContext: 'The user is looking at their order history.',
127
- });
148
+ ```tsx
149
+ import { AliaAskProvider, useAskAgent } from '@alia.onl/sdk';
150
+
151
+ <AliaAskProvider
152
+ agents={{ alia: {}, sindi: { id: SINDI_AGENT_ID } }} // no id = Alia
153
+ clientContext={`Homiio — viewing listing ${listing.id}`} // the screen, built per turn
154
+ hostTools={{
155
+ app: 'homiio',
156
+ definitions: [{ name: 'apply_search', description: '…', inputSchema: { type: 'object' } }],
157
+ execute: async (action) => applySearch(action.input), // → { outcome: 'applied' | … }
158
+ }}
159
+ toolRenderers={{ 'homiio:searchListings': (r) => <ListingCards data={r.output} /> }}
160
+ onFeatureLocked={(key) => showHomiioPlus(key)}
161
+ onShare={({ token }) => shareSheet(`https://homiio.com/shared/${token}`)}
162
+ >
163
+ {children}
164
+ </AliaAskProvider>;
165
+
166
+ const { ask } = useAskAgent();
167
+ ask('sindi', { text: 'Is this flat a good price?' }); // signed out: Oxy's sign-in instead
128
168
  ```
129
169
 
130
- `useAliaChat` streams a turn as Server-Sent Events and switches on Alia's
131
- product events — `alia.reasoning`, `alia.tool_result`, `alia.research_progress`,
132
- `alia.plan_preview`, delegated-agent answers — into `ChatMessage` fields your
133
- components render. Those events are the product runtime's, not part of the Oxy
134
- generic inference contract, and this package is written against them on
135
- purpose: it is a **product** client.
170
+ - **The lead.** `agent={{ id }}` (on `AliaChatSheet`, `AliaChatScreen` and `AliaChatContent`) makes
171
+ that agent the conversation's lead: its greeting, name, character and starters
172
+ (`GET /v1/agents/:id/profile`) and the header's first avatar. Omitted, Alia.
173
+ - **The participants.** `AliaChatState.participants` is `{ lead, joined }`; draw it with
174
+ `AliaParticipants` (or `AliaParticipantsHeader` inside Bloom's AI chat shell) wherever the
175
+ chrome puts the conversation's title. When the lead consults an agent (`alia.agent_start`), the
176
+ agent's reply is drawn under its own identity and its avatar flies from there into the header —
177
+ Bloom's multi-agent join — which gains the agent when it lands.
178
+ - **Host tools** are UI effects the app's own client performs. An `alia.host_action` is executed
179
+ once, by `execute`, and its outcome is sent with the next request; an outcome whose request did
180
+ not get through goes with the one after. They are never authority over data.
181
+ - **Tool renderers** are keyed `<app>:<tool>`, parsed only from `oxy_<app>__<tool>` /
182
+ `self_<app>__<tool>`, and drawn before the generic tool card — for consulted agents too.
183
+ - **Feature gates.** The attach menu offers photos and files as the lead offers them
184
+ (`GET /v1/agents/:id/features`); a locked row, and a turn refused with
185
+ `feature_entitlement_required`, call `onFeatureLocked(key)`. Documents are uploaded to the
186
+ person's Oxy Files (`oxyServices.assets.upload`) and sent as a `file` part.
187
+ - **Sharing.** `onShare` receives `{ conversationId, token, expiresAt }` once the SDK has minted
188
+ the link; `useShareConversation()` mints and revokes, and `<AliaSharedConversation token>` reads
189
+ one, signed in or not. `useConversations({ agentId })` lists the person's conversations with one
190
+ agent.
136
191
 
137
192
  ## An "Ask Alia" button with her avatar
138
193
 
@@ -153,7 +208,7 @@ you do); run it too when a Bloom upgrade changes how the runtime draws her.
153
208
 
154
209
  `useAliaActivity()` is the busiest of two sources, so a button reacts even with the sheet closed:
155
210
 
156
- - a turn running in this app: every `useAliaChat` publishes its lifecycle to a shared store;
211
+ - a turn running in this app: every `useConversationThread` publishes its lifecycle to a shared store;
157
212
  - her runs for the signed-in person elsewhere (background work, another device): the API's
158
213
  `alia:activity` event on the person's `user:<id>` room, over ONE Socket.IO connection per
159
214
  API origin, opened with the Oxy session's token only while someone is signed in and a
@@ -162,13 +217,52 @@ you do); run it too when a Bloom upgrade changes how the runtime draws her.
162
217
  `socket.io-client` is a regular dependency, not a peer: the SDK owns this connection and an
163
218
  app should not have to install a transport to use a button.
164
219
 
220
+ ## The thread
221
+
222
+ The sheet, the screen and the Alia app draw ONE thread, `AliaThread` (inside `AliaChatContent`),
223
+ and nothing else draws turns: Bloom's AI Chat turns with their reveal, the feedback row and the
224
+ long-press menu; the reply's Markdown with its rich blocks, its code card and its references;
225
+ the work log (`WebSearch`, `TaskList`) and the "Worked for…" line; tool cards; the questions Alia
226
+ asked; the plan an agent shows; the failed turn's toast with its Retry; day lines; the window over a long
227
+ history with the scroll that goes with it; and the card of an agent's run. What a frame adds is
228
+ around it, never in it (`tests/components/thread/one-thread.test.tsx` mounts it both ways and
229
+ holds the rows equal, and fails if anything else imports Bloom's thread).
230
+
231
+ An agent's permanent thread is `threadHandle`: the conversations behind the open one are paged in
232
+ above it, the thread can be searched (`state.openSearch`), and a hit opens the stretch around it.
233
+ Those routes (`/agents/thread/:handle/…`) answer Alia's own origins only, as `/conversations` did
234
+ before `/v1`: an embedding Oxy app has no agents of its own and passes no handle.
235
+
236
+ ### The work panel, and where it goes
237
+
238
+ A turn that used tools has a "Worked for…" line; pressing it selects the turn (`useThoughtStore`)
239
+ and asks the host to show it (`runtime.showThought`). `ThoughtPanel` — steps, sources, activity —
240
+ draws the selection wherever the host puts it: the Alia app's right panel, the sheet's own sheet
241
+ over the conversation. A host without `showThought` has no panel, and the line is plain text. A
242
+ running turn's first tool opens the panel by itself (`showThought('automatic')`) unless the host
243
+ declines because something else is open. A file the turn generated opens where the host keeps its
244
+ files (`ThoughtPanel`'s `outputActions`).
245
+
246
+ ### Cards: why weather, market and FairCoin are the SDK's
247
+
248
+ The weather, market quote and FairCoin price cards are the thread's. Their tools
249
+ (`getWeather`, `getMarketQuote`, `getFairCoin`) are in the pipeline every Alia surface calls, and
250
+ the card's numbers travel in the message itself (the whole of every range is in the snapshot, so a
251
+ thread reopened next month shows the figures it was answered with). So any surface that draws
252
+ Alia's reply draws them. The scheduled-task card is the same card with one difference: it points at
253
+ a screen of the host's, so it is a link only where `runtime.openAutomation` says where it goes.
254
+
165
255
  ## The composer
166
256
 
167
257
  The sheet, the screen and the Alia app share one composer, `AliaComposer`: Bloom's
168
258
  `ComposerPill` with files by paste and drop (read with real progress, retried, refused with a
169
259
  sentence), dictation, suggestions over the field and Alia's questions docked above it.
170
260
  `AliaChatSheet` and `AliaChatScreen` render it with photos, voice and suggestions, and
171
- `AliaChatSheet` focuses it once the sheet has presented. To build your own surface, mount it and
261
+ `AliaChatSheet` focuses it once the sheet has presented. The conversation also owns the draft
262
+ (per conversation and account), sending, rewriting a question already sent, the questions Alia
263
+ asked and the power-level selector: give `AliaChatContent` an `onModelChange` and the composer
264
+ offers the seven levels (`auto` … `ultra`) with `deviceModels` of the person's own after them;
265
+ give it an `addMenu` (rows and what pressing one does) and that menu replaces the photos one. To build your own surface, mount it and
172
266
  pass what the surface has:
173
267
 
174
268
  ```tsx
@@ -193,151 +287,129 @@ import { AliaComposer } from '@alia.onl/sdk';
193
287
  />;
194
288
  ```
195
289
 
196
- ## Which endpoint the SDK calls, and why
197
-
198
- The hook sends its turns to `POST {apiUrl}/v1/chat/completions`, with `apiUrl`
199
- defaulting to `EXPO_PUBLIC_ALIA_API_URL` or `https://api.alia.onl`. It also
200
- reads `GET {apiUrl}/catalogue` once per base URL when a `model` is passed, to
201
- check that `publisher/model` against the real models the server offers. With no
202
- `model`, the request carries none and the server's default model answers — the
203
- SDK ships no model identifier of its own.
204
-
205
- That is the compatibility surface, not the product route. `POST /alia/chat` is
206
- the same handler with the same authentication, and it is where Alia's own
207
- clients go — but the two mounts answer browsers differently:
208
-
209
- | Preflight | `access-control-allow-origin` |
210
- | --- | --- |
211
- | `OPTIONS /alia/chat` from `https://alia.onl` or `https://console.alia.onl` | that origin, with credentials |
212
- | `OPTIONS /alia/chat` from an origin Alia does not enumerate | **absent** |
213
- | `OPTIONS /v1/chat/completions` from anywhere | `*` |
214
-
215
- `/alia/chat` takes Alia's exact-origin allowlist (`packages/api/src/lib/cors-origins.ts`);
216
- `/v1` is public CORS. Because this package compiles into *your* app on *your*
217
- origin, defaulting the hook to `/alia/chat` would fail every consumer's
218
- preflight on web. So it stays on `/v1/chat/completions`, which is Alia's
219
- **permanent product API** — it does not sunset
220
- ([ADR 0010](https://github.com/OxyHQ/Alia/blob/main/docs/adr/0010-alia-keeps-a-product-api-credentials-come-from-oxy-console.md)).
221
- Generic model access is a different product, Kaana through Oxy; this package
222
- is written against Alia's, and the credential a consumer presents is an Oxy
223
- one — the signed-in user's session today. A change of default to `/alia/chat`
224
- would be a semver-major of this package, would reach only consumers who upgrade
225
- **and** rebuild, and is not needed to escape a sunset; it is taken only if the
226
- per-application origin policy below makes it worthwhile
227
- ([#244](https://github.com/OxyHQ/Alia/issues/244), closed on that decision).
228
-
229
- Native apps are not subject to CORS at all — React Native sends no `Origin`
230
- header — so the table above is about web builds and the backend path below.
231
-
232
- ## Using the SDK from your own product: the consumer-backend path
233
-
234
- The supported way for an application outside Alia's origins to use this SDK
235
- today is to put your own backend between the SDK and Alia. Your backend
236
- receives the SDK's request, calls Alia's product route server-to-server — a
237
- server sends no `Origin` header, so `/alia/chat` answers it on its credential
238
- alone — and streams the SSE body back unchanged. Then point the hook at that
239
- backend:
290
+ ## The conversation runtime
291
+
292
+ Everything that is a conversation lives in `src/runtime`, in layers that depend only downward:
293
+
294
+ | Layer | What | Public names |
295
+ | --- | --- | --- |
296
+ | Transport | Alia's REST API over the signed-in person's Oxy client (token, refresh and the single 401 replay are Oxy's); the turn's SSE stream | `useAliaApi`, `AliaApiError` |
297
+ | Model and state | the records, React Query keys and the cached lists; zustand stores for the composer drafts and the hand-off of a first message | `Message`, `Conversation`, `aliaQueryKeys`, `useComposerDraftStore`, `useConversationStore` |
298
+ | Hooks | `useConversations` (the list, paginated), `useConversation` (one, with its history), `useCreateConversation`, `useRenameConversation`, `useDeleteConversation`, `useClearConversation`, and `useConversationThread`: the live thread of one conversation — messages, `sendMessage`, `createNewConversation`, `editMessage`, `regenerateMessage`, `retryFailedTurn`, `stopGeneration`, `clearConversation`, `startOver`, `ensureConversation` | `use…` |
299
+ | UI | `AliaChatContent` (the conversation screen), `AliaChatScreen`, `AliaChatSheet`, `AliaThread`, `ThoughtPanel` | components |
300
+
301
+ Between the hooks and the components are the hooks of what a person does with a conversation —
302
+ `useChatComposer` (the draft, send, edit, suggestions), `useChatVoice` (the call and its
303
+ transcript in the thread), `useThreadView` (the present or a moment of an agent's thread),
304
+ `useOpenThought`, `useReadAloud`, `useMessageVote` — each used by exactly one layer above it.
305
+
306
+ An agent's thread and runs (`useThreadHistory`, `useThreadSearch`, `useThreadWindow`,
307
+ `useAgentActivity`) read the agent routes through the same API and cache.
308
+
309
+ A turn goes to `POST {apiUrl}/v1/chat/completions`; conversations to `{apiUrl}/v1/conversations`.
310
+ `apiUrl` defaults to `EXPO_PUBLIC_ALIA_API_URL`, then `https://api.alia.onl`. A failure to
311
+ answer is a typed error that keeps the person's turn in the thread with a retry (`FailedTurn`),
312
+ never a filler message; running out of credits or hitting a rate limit is a `UsageLimitError`.
313
+
314
+ ### Configure it once: `AliaRuntimeProvider`
315
+
316
+ Providers nest and merge, so the sheet works with no provider at all and the Alia app mounts
317
+ one above its routes. Everything is optional.
318
+
319
+ ```tsx
320
+ import { AliaRuntimeProvider, type AliaRuntimeHost } from '@alia.onl/sdk';
321
+
322
+ const host: AliaRuntimeHost = {
323
+ // navigation: the runtime created or chose a conversation / the open one is gone
324
+ openConversation: (id) => router.push(`/c/${id}`),
325
+ closeConversation: () => router.replace('/'),
326
+ // what only the host can show: credits, an artifact panel, a haptic tick
327
+ onUsage: (frame) => updateCredits(frame.credits_remaining),
328
+ onArtifact: (artifact) => rightPanel.show(artifact),
329
+ turnRequest: () => ({ webSearch: composerMenu.webSearch, deepResearch: composerMenu.deepResearch }),
330
+ // where the work of a turn is read; return false to decline an automatic opening
331
+ showThought: (opening) => (opening === 'automatic' && panel.isOpen ? false : panel.show('thought')),
332
+ openAutomation: (id) => router.push(`/automations/${id}`), // a scheduled-task card is a link
333
+ faviconUrl: (url) => favicons.of(url), // the work log's source icons
334
+ };
335
+
336
+ <AliaRuntimeProvider
337
+ apiUrl="https://api.alia.onl"
338
+ offline={accountScopedStore} // optional: where a 401-ed session keeps its conversations
339
+ streamFetch={expoFetch} // optional: a fetch that can stream a body on native
340
+ speech={{ voice: 'female', tone: 'brief', language: 'es-ES' }} // read-aloud, dictation, calls
341
+ host={host} // memoise it
342
+ >
343
+ {children}
344
+ </AliaRuntimeProvider>;
345
+ ```
346
+
347
+ The sheet and the Alia app differ ONLY in what they put there:
240
348
 
241
349
  ```tsx
242
- useAliaChat({ apiUrl: 'https://your-backend.example' });
350
+ // The sheet: nothing to configure; the chat keeps its own conversation.
351
+ <AliaChatSheet ref={sheet} />
352
+
353
+ // The Alia app: its route owns the conversation, its sidebar reads the same list.
354
+ <AliaChatContent
355
+ surface // the container draws the page's card
356
+ conversationId={id}
357
+ threadHandle={handle} // an agent's thread: history above, search
358
+ model={powerLevel}
359
+ onModelChange={setPowerLevel} // the composer offers the levels
360
+ addMenu={{ groups, onSelect }} // the host's rows
361
+ chrome={(chat) => ({ header, title, project, actions: <Actions chat={chat} /> })}
362
+ renderUsageLimit={({ error, dismiss }) => <UsageLimitDialog error={error} onDismiss={dismiss} />}
363
+ />
364
+ function Sidebar() {
365
+ const { data } = useConversations(); // the very query the thread keeps up to date
366
+ …
367
+ }
243
368
  ```
244
369
 
245
- The hook keeps its paths, so your backend answers
246
- `POST /v1/chat/completions` and `GET /catalogue` under whatever base URL you
247
- give it. Nothing about the stream changes: the SDK's parser reads the bytes Alia
248
- wrote.
370
+ ### Voice lives in the conversation
249
371
 
250
- **Which credential.** The SDK attaches the signed-in user's Oxy access token as
251
- `Authorization: Bearer …` on the request your backend receives. Forward that
252
- header as it is. `POST /alia/chat` authenticates with
253
- `authenticateTokenOrApiKey` (`packages/api/src/routes/chat.ts`,
254
- `packages/api/src/middleware/auth.ts`), which accepts an Oxy session token —
255
- verified with Oxy, the same way it would be if the browser had called Alia
256
- directly — and the turn is metered to that user's Alia entitlement, the same as
257
- a direct call. The other credentials the middleware recognises are
258
- Alia-internal. There is no consumer-application credential for this route yet — that is Oxy Applications' to issue (OxyHQ/oxy#972).
372
+ A call always has a home. With a conversation open the call lives in it; on a new-chat screen
373
+ the conversation is created when the call starts (after the plan/credits gate) and the call
374
+ opens on the screen that shows it. That is one code path for the sheet, `AliaChatScreen` and the
375
+ Alia app (`useVoiceLauncher`'s `resolveConversation` and `useVoiceCallRequests`):
259
376
 
260
- A minimal relay, Node 18+ and Express:
377
+ - the sheet has no routes, so the chat itself switches to the new conversation and the call opens
378
+ in place;
379
+ - the Alia app's new-chat route is replaced by the conversation's (`host.openConversation`), and
380
+ the call opens on that screen, which takes the pending request.
261
381
 
262
- ```ts
263
- import express from 'express';
264
-
265
- const ALIA = 'https://api.alia.onl';
266
- const app = express();
267
- app.use(express.json({ limit: '1mb' }));
268
-
269
- /** Relay one request to Alia and stream the answer back unchanged. */
270
- async function relay(
271
- req: express.Request,
272
- res: express.Response,
273
- method: 'GET' | 'POST',
274
- path: string,
275
- ): Promise<void> {
276
- const controller = new AbortController();
277
- req.on('close', () => controller.abort());
278
-
279
- const authorization = req.get('authorization');
280
- const upstream = await fetch(`${ALIA}${path}`, {
281
- method,
282
- headers: {
283
- ...(authorization ? { Authorization: authorization } : {}),
284
- Accept: req.get('accept') ?? 'application/json',
285
- ...(method === 'POST' ? { 'Content-Type': 'application/json' } : {}),
286
- },
287
- body: method === 'POST' ? JSON.stringify(req.body) : undefined,
288
- signal: controller.signal,
289
- });
290
-
291
- res.status(upstream.status);
292
- res.setHeader('Content-Type', upstream.headers.get('content-type') ?? 'application/json');
293
- res.setHeader('Cache-Control', 'no-cache');
294
- res.setHeader('X-Accel-Buffering', 'no'); // keep proxies from buffering the SSE stream
295
- res.flushHeaders();
296
-
297
- if (upstream.body === null) {
298
- res.end();
299
- return;
300
- }
301
- try {
302
- for await (const chunk of upstream.body) res.write(chunk);
303
- } catch (error) {
304
- if (!controller.signal.aborted) throw error;
305
- } finally {
306
- res.end();
307
- }
308
- }
382
+ **Who decides which conversation is open:** the host, when it navigates (it passes
383
+ `runtime.openConversation`, or an outer `AliaRuntimeProvider` has one); otherwise the chat.
309
384
 
310
- // The SDK's turn: answered by Alia's PRODUCT route, server-to-server.
311
- app.post('/v1/chat/completions', (req, res) => relay(req, res, 'POST', '/alia/chat'));
385
+ ## Which API the SDK calls
312
386
 
313
- // The SDK's model check. Without this the hook still works — an unreadable
314
- // catalogue leaves the requested identifier alone — but it never learns what
315
- // the server offers.
316
- app.get('/catalogue', (req, res) => relay(req, res, 'GET', '/catalogue'));
387
+ The SDK is Alia's product client and speaks **only to `/v1`**, Alia's product API: a bearer of
388
+ the signed-in person's Oxy session, no cookies, and public CORS — so it works in the browser of
389
+ every Oxy app, on that app's own origin, with nothing to configure and no backend in between.
317
390
 
318
- app.listen(3000);
319
- ```
391
+ - A turn: `POST {apiUrl}/v1/chat/completions` — the same handler `/alia/chat` mounts, with every
392
+ `alia.*` stream event, `conversationId` and the stored history.
393
+ - Conversations: `{apiUrl}/v1/conversations` (list, open, create, save, rename, delete, clear,
394
+ vote). A person's session only: an application key or a bot secret is refused, because a
395
+ conversation is somebody's private history.
396
+ - Dictation, read-aloud and the voice call: `/v1/audio/*` and `/v1/voice/session`.
320
397
 
321
- The request body the SDK sends is `{ model?, messages, stream: true }` and the
322
- response Alia writes is `text/event-stream`; the relay forwards both without
323
- reading them. CORS between your web app and your backend is yours to configure,
324
- as it would be for any of your own routes.
398
+ `apiUrl` defaults to `EXPO_PUBLIC_ALIA_API_URL`, then `https://api.alia.onl`. `routes.ts` holds
399
+ the paths, and `tests/runtime/v1-only.test.ts` fails if the transport builds a URL outside `/v1`.
400
+ The one exception is the agent routes (`AGENT_ROUTES`: an agent's thread and its runs), which only
401
+ Alia's own clients use.
325
402
 
326
- **The trade-off.** This is one more hop, and one more process in the path of a
327
- stream. Some consumers will not want it, which is why it is documented as the
328
- path that works today rather than chosen as the only one. The other two shapes
329
- — a CORS policy on `/alia/chat` for registered consumer origins, and the SDK
330
- default moving to `/alia/chat` in a major — are recorded in #244: the first is
331
- blocked on Oxy Applications carrying an origin registry (OxyHQ/oxy#972), and
332
- the second is an adoption window rather than a switch, for the raw-source
333
- reason above.
403
+ `model` is a power level (`auto`, `instant`, `medium`, `high`, `xhigh`, `pro`, `ultra`) or an
404
+ exact `publisher/model`; it is sent as given, and omitted the server's default answers. The SDK
405
+ ships no model identifier of its own.
334
406
 
335
407
  ## Scripts
336
408
 
337
409
  ```sh
338
410
  bun run typecheck # tsc --noEmit
339
411
  bun run test # vitest
340
- bun run check:entries # the root entry must not reach the voice call
412
+ bun run check:entries # the root entry reaches the call only through its lazy import; no retired modules
341
413
  ```
342
414
 
343
415
  ## Licence
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "15.8.0",
3
+ "version": "19.0.0",
4
+ "description": "Alia AI chat SDK — the conversation runtime, chat UI, voice calls and dictation for Oxy apps",
4
5
  "main": "src/index.ts",
5
6
  "react-native": "src/index.ts",
6
7
  "source": "src/index.ts",
@@ -10,6 +11,8 @@
10
11
  "./avatar": "./src/avatar.ts",
11
12
  "./content": "./src/lib/content-utils.ts",
12
13
  "./csp": "./src/csp.ts",
14
+ "./model": "./src/runtime/model.ts",
15
+ "./runtime": "./src/runtime/index.ts",
13
16
  "./tools": "./src/tools.ts",
14
17
  "./voice": "./src/voice.ts",
15
18
  "./package.json": "./package.json"
@@ -22,6 +25,11 @@
22
25
  "CHANGELOG.md"
23
26
  ],
24
27
  "license": "Apache-2.0",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/OxyHQ/Alia.git",
31
+ "directory": "packages/alia-chat"
32
+ },
25
33
  "private": false,
26
34
  "publishConfig": {
27
35
  "access": "public"
@@ -30,10 +38,12 @@
30
38
  "typecheck": "tsc --noEmit",
31
39
  "test": "vitest run",
32
40
  "check:entries": "node scripts/check-entry-isolation.mjs",
33
- "render:avatar-still": "bun scripts/render-avatar-still.ts"
41
+ "render:avatar-still": "bun scripts/render-avatar-still.ts",
42
+ "prepublishOnly": "bun run typecheck && bun run test && bun run check:entries"
34
43
  },
35
44
  "dependencies": {
36
45
  "clsx": "^2.1.1",
46
+ "react-native-markdown-display": "^7.0.2",
37
47
  "socket.io-client": "^4.8.3",
38
48
  "tailwind-merge": "^3.7.0",
39
49
  "zustand": "^5.0.15"
@@ -45,6 +55,7 @@
45
55
  "@oxy.so/bloom": ">=10.9.0",
46
56
  "@oxy.so/core": ">=3.0.0",
47
57
  "@oxy.so/services": ">=8.0.0",
58
+ "@tanstack/react-query": "^5.101",
48
59
  "@react-native-async-storage/async-storage": "^2.2.0",
49
60
  "expo-blur": "^57.0.0",
50
61
  "react-native-reanimated": "^4.5.0",
@@ -60,6 +71,8 @@
60
71
  "nativewind": "^5.0.0-preview.3",
61
72
  "expo-clipboard": "^57.0.0",
62
73
  "expo-file-system": "^57.0.0",
74
+ "expo-web-browser": "^57.0.0",
75
+ "react-native-css": "^3.0.7",
63
76
  "react-native-audio-api": "^0.13.6"
64
77
  },
65
78
  "peerDependenciesMeta": {
@@ -68,9 +81,6 @@
68
81
  },
69
82
  "expo-clipboard": {
70
83
  "optional": true
71
- },
72
- "react-native-audio-api": {
73
- "optional": true
74
84
  }
75
85
  },
76
86
  "devDependencies": {
package/src/animations.ts CHANGED
@@ -3,4 +3,3 @@ import { Easing, FadeInUp } from 'react-native-reanimated';
3
3
  const easeOut = Easing.bezier(0.16, 1, 0.3, 1);
4
4
 
5
5
  export const planPreviewEnter = FadeInUp.duration(220).easing(easeOut);
6
- export const chatMessageEnter = FadeInUp.duration(220).easing(easeOut);