@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
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
 
@@ -28,12 +31,25 @@ The chat is built from Bloom (`@oxy.so/bloom`, a peer dependency): mount Bloom's
28
31
  Alia's avatar is Bloom's 3D character (`AgentAvatar`). It draws through a runtime of
29
32
  static files (about 214 MB), which the chat loads from Alia's own origin
30
33
  (`ALIA_CHARACTER_RUNTIME_URL`, served with CORS open), so the app hosts nothing and mounts
31
- no avatar provider. An app with a strict CSP allows `https://alia.onl` in `script-src` and
32
- `connect-src` (and `'wasm-unsafe-eval'`). To serve your own copy instead, serve the Bloom
34
+ no avatar provider. To serve your own copy instead, serve the Bloom
33
35
  package's `assets/character-runtime/` directory unchanged and pass the URL of `runtime.mjs`
34
36
  as `characterRuntimeUrl` to `AliaChatSheet` or `AliaChatScreen`; native avatars run in a
35
37
  WebView, so there the URL must be absolute and reachable from the device.
36
38
 
39
+ A web app with a Content-Security-Policy composes the SDK's into its own instead of listing
40
+ Alia's origins by hand: `@alia.onl/sdk/csp` exports `ALIA_SDK_CSP` and `withAliaSdkCsp(extensions)`
41
+ in the shape of `@oxy.so/core`'s `OxyCspExtensions` (a leaf entry, safe on a server):
42
+
43
+ ```ts
44
+ import { withAliaSdkCsp } from '@alia.onl/sdk/csp';
45
+ app.use(createOxySecurityHeaders({ csp: withAliaSdkCsp(APP_CSP_EXTENSIONS) }));
46
+ ```
47
+
48
+ It declares the API and its WebSocket (`connect-src`), the avatar runtime and the voice call's
49
+ worklets on `https://alia.onl` (`script-src`, with `'wasm-unsafe-eval'` for the runtime's
50
+ WebAssembly), attachments and read-aloud clips (`img-src`, `media-src`). No `blob:` script:
51
+ the worklets are a content-addressed file on `alia.onl`, not a blob URL.
52
+
37
53
  The empty chat greets the signed-in person by name ("Hi Nate, what's on your mind?"); pass
38
54
  `welcomeGreeting` to say something else, or `labels.greeting` / `labels.greetingNamed` to translate it.
39
55
 
@@ -44,12 +60,28 @@ The chat's own copy is English. Pass `labels` to `AliaChatSheet` or
44
60
  `AliaChatScreen` to translate it (`AliaChatLabels`; anything omitted stays
45
61
  English). Bloom's own strings follow `BloomProvider`'s `locale`.
46
62
 
47
- Two entry points:
48
-
49
- - `@alia.onl/sdk` — text chat, dictation, read-aloud, catalogue, primitives.
50
- - `@alia.onl/sdk/voice` — `VoiceSession` and `useVoiceRoom`, the voice call.
51
- Pass `VoiceSession` to the shell as `voiceSession` to offer calls; leave it
52
- out for text-only chat and the call is never compiled.
63
+ Entry points:
64
+
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.
74
+ - `@alia.onl/sdk/csp` — the CSP the SDK needs from a host page (above).
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.
53
85
 
54
86
  ## Speech
55
87
 
@@ -72,7 +104,7 @@ transcript content.
72
104
 
73
105
  On iOS and Android a call needs
74
106
  [`react-native-audio-api`](https://github.com/software-mansion/react-native-audio-api)
75
- (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)
76
108
  and its config plugin, then a new native build:
77
109
 
78
110
  ```json
@@ -103,22 +135,59 @@ On iOS and Android its composer rises with the keyboard through
103
135
  `react-native-keyboard-controller`, which needs the app wrapped in its
104
136
  `KeyboardProvider` (re-exported here).
105
137
 
106
- 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.
107
142
 
108
- ```tsx
109
- import { useAliaChat } from '@alia.onl/sdk';
143
+ ## Any agent, from any app
110
144
 
111
- const { messages, send, isStreaming, stop, error } = useAliaChat({
112
- clientContext: 'The user is looking at their order history.',
113
- });
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).
147
+
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
114
168
  ```
115
169
 
116
- `useAliaChat` streams a turn as Server-Sent Events and switches on Alia's
117
- product events — `alia.reasoning`, `alia.tool_result`, `alia.research_progress`,
118
- `alia.plan_preview`, delegated-agent answers — into `ChatMessage` fields your
119
- components render. Those events are the product runtime's, not part of the Oxy
120
- generic inference contract, and this package is written against them on
121
- 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.
122
191
 
123
192
  ## An "Ask Alia" button with her avatar
124
193
 
@@ -139,7 +208,7 @@ you do); run it too when a Bloom upgrade changes how the runtime draws her.
139
208
 
140
209
  `useAliaActivity()` is the busiest of two sources, so a button reacts even with the sheet closed:
141
210
 
142
- - 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;
143
212
  - her runs for the signed-in person elsewhere (background work, another device): the API's
144
213
  `alia:activity` event on the person's `user:<id>` room, over ONE Socket.IO connection per
145
214
  API origin, opened with the Oxy session's token only while someone is signed in and a
@@ -148,13 +217,52 @@ you do); run it too when a Bloom upgrade changes how the runtime draws her.
148
217
  `socket.io-client` is a regular dependency, not a peer: the SDK owns this connection and an
149
218
  app should not have to install a transport to use a button.
150
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
+
151
255
  ## The composer
152
256
 
153
257
  The sheet, the screen and the Alia app share one composer, `AliaComposer`: Bloom's
154
258
  `ComposerPill` with files by paste and drop (read with real progress, retried, refused with a
155
259
  sentence), dictation, suggestions over the field and Alia's questions docked above it.
156
260
  `AliaChatSheet` and `AliaChatScreen` render it with photos, voice and suggestions, and
157
- `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
158
266
  pass what the surface has:
159
267
 
160
268
  ```tsx
@@ -179,151 +287,129 @@ import { AliaComposer } from '@alia.onl/sdk';
179
287
  />;
180
288
  ```
181
289
 
182
- ## Which endpoint the SDK calls, and why
183
-
184
- The hook sends its turns to `POST {apiUrl}/v1/chat/completions`, with `apiUrl`
185
- defaulting to `EXPO_PUBLIC_ALIA_API_URL` or `https://api.alia.onl`. It also
186
- reads `GET {apiUrl}/catalogue` once per base URL when a `model` is passed, to
187
- check that `publisher/model` against the real models the server offers. With no
188
- `model`, the request carries none and the server's default model answers — the
189
- SDK ships no model identifier of its own.
190
-
191
- That is the compatibility surface, not the product route. `POST /alia/chat` is
192
- the same handler with the same authentication, and it is where Alia's own
193
- clients go — but the two mounts answer browsers differently:
194
-
195
- | Preflight | `access-control-allow-origin` |
196
- | --- | --- |
197
- | `OPTIONS /alia/chat` from `https://alia.onl` or `https://console.alia.onl` | that origin, with credentials |
198
- | `OPTIONS /alia/chat` from an origin Alia does not enumerate | **absent** |
199
- | `OPTIONS /v1/chat/completions` from anywhere | `*` |
200
-
201
- `/alia/chat` takes Alia's exact-origin allowlist (`packages/api/src/lib/cors-origins.ts`);
202
- `/v1` is public CORS. Because this package compiles into *your* app on *your*
203
- origin, defaulting the hook to `/alia/chat` would fail every consumer's
204
- preflight on web. So it stays on `/v1/chat/completions`, which is Alia's
205
- **permanent product API** — it does not sunset
206
- ([ADR 0010](https://github.com/OxyHQ/Alia/blob/main/docs/adr/0010-alia-keeps-a-product-api-credentials-come-from-oxy-console.md)).
207
- Generic model access is a different product, Kaana through Oxy; this package
208
- is written against Alia's, and the credential a consumer presents is an Oxy
209
- one — the signed-in user's session today. A change of default to `/alia/chat`
210
- would be a semver-major of this package, would reach only consumers who upgrade
211
- **and** rebuild, and is not needed to escape a sunset; it is taken only if the
212
- per-application origin policy below makes it worthwhile
213
- ([#244](https://github.com/OxyHQ/Alia/issues/244), closed on that decision).
214
-
215
- Native apps are not subject to CORS at all — React Native sends no `Origin`
216
- header — so the table above is about web builds and the backend path below.
217
-
218
- ## Using the SDK from your own product: the consumer-backend path
219
-
220
- The supported way for an application outside Alia's origins to use this SDK
221
- today is to put your own backend between the SDK and Alia. Your backend
222
- receives the SDK's request, calls Alia's product route server-to-server — a
223
- server sends no `Origin` header, so `/alia/chat` answers it on its credential
224
- alone — and streams the SSE body back unchanged. Then point the hook at that
225
- 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:
226
348
 
227
349
  ```tsx
228
- 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
+ }
229
368
  ```
230
369
 
231
- The hook keeps its paths, so your backend answers
232
- `POST /v1/chat/completions` and `GET /catalogue` under whatever base URL you
233
- give it. Nothing about the stream changes: the SDK's parser reads the bytes Alia
234
- wrote.
370
+ ### Voice lives in the conversation
235
371
 
236
- **Which credential.** The SDK attaches the signed-in user's Oxy access token as
237
- `Authorization: Bearer …` on the request your backend receives. Forward that
238
- header as it is. `POST /alia/chat` authenticates with
239
- `authenticateTokenOrApiKey` (`packages/api/src/routes/chat.ts`,
240
- `packages/api/src/middleware/auth.ts`), which accepts an Oxy session token —
241
- verified with Oxy, the same way it would be if the browser had called Alia
242
- directly — and the turn is metered to that user's Alia entitlement, the same as
243
- a direct call. The other credentials the middleware recognises are
244
- 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`):
245
376
 
246
- 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.
247
381
 
248
- ```ts
249
- import express from 'express';
250
-
251
- const ALIA = 'https://api.alia.onl';
252
- const app = express();
253
- app.use(express.json({ limit: '1mb' }));
254
-
255
- /** Relay one request to Alia and stream the answer back unchanged. */
256
- async function relay(
257
- req: express.Request,
258
- res: express.Response,
259
- method: 'GET' | 'POST',
260
- path: string,
261
- ): Promise<void> {
262
- const controller = new AbortController();
263
- req.on('close', () => controller.abort());
264
-
265
- const authorization = req.get('authorization');
266
- const upstream = await fetch(`${ALIA}${path}`, {
267
- method,
268
- headers: {
269
- ...(authorization ? { Authorization: authorization } : {}),
270
- Accept: req.get('accept') ?? 'application/json',
271
- ...(method === 'POST' ? { 'Content-Type': 'application/json' } : {}),
272
- },
273
- body: method === 'POST' ? JSON.stringify(req.body) : undefined,
274
- signal: controller.signal,
275
- });
276
-
277
- res.status(upstream.status);
278
- res.setHeader('Content-Type', upstream.headers.get('content-type') ?? 'application/json');
279
- res.setHeader('Cache-Control', 'no-cache');
280
- res.setHeader('X-Accel-Buffering', 'no'); // keep proxies from buffering the SSE stream
281
- res.flushHeaders();
282
-
283
- if (upstream.body === null) {
284
- res.end();
285
- return;
286
- }
287
- try {
288
- for await (const chunk of upstream.body) res.write(chunk);
289
- } catch (error) {
290
- if (!controller.signal.aborted) throw error;
291
- } finally {
292
- res.end();
293
- }
294
- }
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.
295
384
 
296
- // The SDK's turn: answered by Alia's PRODUCT route, server-to-server.
297
- app.post('/v1/chat/completions', (req, res) => relay(req, res, 'POST', '/alia/chat'));
385
+ ## Which API the SDK calls
298
386
 
299
- // The SDK's model check. Without this the hook still works — an unreadable
300
- // catalogue leaves the requested identifier alone — but it never learns what
301
- // the server offers.
302
- 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.
303
390
 
304
- app.listen(3000);
305
- ```
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`.
306
397
 
307
- The request body the SDK sends is `{ model?, messages, stream: true }` and the
308
- response Alia writes is `text/event-stream`; the relay forwards both without
309
- reading them. CORS between your web app and your backend is yours to configure,
310
- 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.
311
402
 
312
- **The trade-off.** This is one more hop, and one more process in the path of a
313
- stream. Some consumers will not want it, which is why it is documented as the
314
- path that works today rather than chosen as the only one. The other two shapes
315
- — a CORS policy on `/alia/chat` for registered consumer origins, and the SDK
316
- default moving to `/alia/chat` in a major — are recorded in #244: the first is
317
- blocked on Oxy Applications carrying an origin registry (OxyHQ/oxy#972), and
318
- the second is an adoption window rather than a switch, for the raw-source
319
- 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.
320
406
 
321
407
  ## Scripts
322
408
 
323
409
  ```sh
324
410
  bun run typecheck # tsc --noEmit
325
411
  bun run test # vitest
326
- 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
327
413
  ```
328
414
 
329
415
  ## Licence
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "15.7.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",
@@ -9,17 +10,26 @@
9
10
  ".": "./src/index.ts",
10
11
  "./avatar": "./src/avatar.ts",
11
12
  "./content": "./src/lib/content-utils.ts",
13
+ "./csp": "./src/csp.ts",
14
+ "./model": "./src/runtime/model.ts",
15
+ "./runtime": "./src/runtime/index.ts",
12
16
  "./tools": "./src/tools.ts",
13
17
  "./voice": "./src/voice.ts",
14
18
  "./package.json": "./package.json"
15
19
  },
16
20
  "files": [
17
21
  "src",
22
+ "worklets",
18
23
  "NOTICE",
19
24
  "tsconfig.json",
20
25
  "CHANGELOG.md"
21
26
  ],
22
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
+ },
23
33
  "private": false,
24
34
  "publishConfig": {
25
35
  "access": "public"
@@ -28,10 +38,12 @@
28
38
  "typecheck": "tsc --noEmit",
29
39
  "test": "vitest run",
30
40
  "check:entries": "node scripts/check-entry-isolation.mjs",
31
- "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"
32
43
  },
33
44
  "dependencies": {
34
45
  "clsx": "^2.1.1",
46
+ "react-native-markdown-display": "^7.0.2",
35
47
  "socket.io-client": "^4.8.3",
36
48
  "tailwind-merge": "^3.7.0",
37
49
  "zustand": "^5.0.15"
@@ -43,6 +55,7 @@
43
55
  "@oxy.so/bloom": ">=10.9.0",
44
56
  "@oxy.so/core": ">=3.0.0",
45
57
  "@oxy.so/services": ">=8.0.0",
58
+ "@tanstack/react-query": "^5.101",
46
59
  "@react-native-async-storage/async-storage": "^2.2.0",
47
60
  "expo-blur": "^57.0.0",
48
61
  "react-native-reanimated": "^4.5.0",
@@ -58,6 +71,8 @@
58
71
  "nativewind": "^5.0.0-preview.3",
59
72
  "expo-clipboard": "^57.0.0",
60
73
  "expo-file-system": "^57.0.0",
74
+ "expo-web-browser": "^57.0.0",
75
+ "react-native-css": "^3.0.7",
61
76
  "react-native-audio-api": "^0.13.6"
62
77
  },
63
78
  "peerDependenciesMeta": {
@@ -66,9 +81,6 @@
66
81
  },
67
82
  "expo-clipboard": {
68
83
  "optional": true
69
- },
70
- "react-native-audio-api": {
71
- "optional": true
72
84
  }
73
85
  },
74
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);