@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
@@ -0,0 +1,383 @@
1
+ import type { AutomationTrigger, ToolInvocation } from '../types';
2
+
3
+ /**
4
+ * The cards a tool result draws inside the assistant turn, as data.
5
+ *
6
+ * Everything here is pure: the snapshot the tool stored with the message goes
7
+ * in, and what Bloom's `LineChartCard`, `Card` and `Item` are handed comes
8
+ * out. A thread reopened next month shows the numbers it was answered with,
9
+ * because nothing is fetched — every range the card offers is already in the
10
+ * snapshot. The producers are `packages/api/src/lib/tools/{weather,market,
11
+ * faircoin}.ts` and the scheduling tool.
12
+ */
13
+
14
+ type Translate = (key: string, params?: Record<string, string>) => string;
15
+
16
+ /** The tool results that are a card rather than a step of the turn's work. */
17
+ const CARD_TYPES = new Set(['weather', 'market', 'faircoin', 'scheduled-task']);
18
+
19
+ export type ToolCard =
20
+ | { type: 'weather'; data: WeatherCardData }
21
+ | { type: 'market'; data: MarketCardData }
22
+ | { type: 'faircoin'; data: FairCoinCardData }
23
+ | { type: 'scheduled-task'; data: ScheduledTaskCardData };
24
+
25
+ /**
26
+ * The card a finished call returned, if it is one this conversation draws.
27
+ *
28
+ * It checks the card's NAME, not the shape of its `data`, which is then cast
29
+ * unchecked — so each card renders inside its own error boundary, and the
30
+ * builders below guard every field they read.
31
+ */
32
+ export function cardOf(t: ToolInvocation): ToolCard | null {
33
+ if (t.state !== 'result') return null;
34
+ const card = (t.result as { card?: { type?: string; data?: unknown } } | undefined)?.card;
35
+ if (card?.type === undefined || !CARD_TYPES.has(card.type) || !card.data) return null;
36
+ return { type: card.type, data: card.data } as ToolCard;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Shared formatting
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /** `[msSinceEpoch, price]`, exactly as the tool stored it. */
44
+ export type PricePoint = [number, number];
45
+
46
+ /** A coin worth 0.000023 is not "0.00", and one worth 60000 is not "60000.000000". */
47
+ const priceDigits = (value: number) => (Math.abs(value) < 1 ? 6 : 2);
48
+
49
+ /**
50
+ * ISO 4217 is three letters and `Intl` throws a `RangeError` on anything that
51
+ * is not. The currency arrives from whatever the reader typed, so a typo must
52
+ * not take the whole card down.
53
+ */
54
+ const isCurrencyCode = (code: string) => /^[A-Za-z]{3}$/.test(code);
55
+
56
+ export function formatPrice(value: number, currency: string, locale: string): string {
57
+ const options: Intl.NumberFormatOptions = {
58
+ minimumFractionDigits: 2,
59
+ maximumFractionDigits: priceDigits(value),
60
+ };
61
+ if (!isCurrencyCode(currency)) {
62
+ return `${value.toLocaleString(locale, options)} ${currency.toUpperCase()}`;
63
+ }
64
+ return value.toLocaleString(locale, {
65
+ ...options,
66
+ style: 'currency',
67
+ currency: currency.toUpperCase(),
68
+ });
69
+ }
70
+
71
+ /**
72
+ * The axis labels a price scale, not a currency: no symbol, compact, and three
73
+ * significant digits — the chart's axis is 44px wide, and "60,000" does not fit
74
+ * in it where "60K" does.
75
+ */
76
+ const formatAxis = (value: number, locale: string) =>
77
+ value.toLocaleString(locale, { notation: 'compact', maximumSignificantDigits: 3 });
78
+
79
+ /**
80
+ * The scale names go through a translation whole — Spanish "billón" is 10¹²,
81
+ * not 10⁹ — so a suffix is never appended in one language and reused in the
82
+ * next.
83
+ */
84
+ const SCALES = [
85
+ { at: 1e12, key: 'trillion' },
86
+ { at: 1e9, key: 'billion' },
87
+ { at: 1e6, key: 'million' },
88
+ { at: 1e3, key: 'thousand' },
89
+ ] as const;
90
+
91
+ function formatCompact(
92
+ value: number,
93
+ namespace: string,
94
+ locale: string,
95
+ t: Translate,
96
+ fallback: string,
97
+ ): string {
98
+ const scale = SCALES.find((s) => Math.abs(value) >= s.at);
99
+ if (!scale) return fallback;
100
+ return t(`${namespace}.scale.${scale.key}`, {
101
+ value: (value / scale.at).toLocaleString(locale, { maximumFractionDigits: 2 }),
102
+ });
103
+ }
104
+
105
+ /**
106
+ * A field the row did not carry reads the same as one that could not be
107
+ * priced: `null` from the tool, `undefined` from an older row and a `NaN` that
108
+ * survived a parse are all "no number".
109
+ */
110
+ const quoted = (value: number | null | undefined): value is number =>
111
+ typeof value === 'number' && Number.isFinite(value);
112
+
113
+ /** A series point as the chart reads it, labelled with its moment. */
114
+ function toPoints(series: readonly PricePoint[], intraday: boolean, locale: string) {
115
+ return series.map(([at, value]) => ({
116
+ label: new Date(at).toLocaleString(
117
+ locale,
118
+ intraday
119
+ ? { hour: 'numeric', minute: '2-digit' }
120
+ : { day: 'numeric', month: 'short', year: 'numeric' },
121
+ ),
122
+ value,
123
+ }));
124
+ }
125
+
126
+ /** First-to-last change as a ratio, or none for a run that started at nothing. */
127
+ function runDelta(series: readonly PricePoint[]): number | undefined {
128
+ const first = series[0][1];
129
+ const last = series[series.length - 1][1];
130
+ return first === 0 ? undefined : (last - first) / first;
131
+ }
132
+
133
+ export interface PriceChartRange {
134
+ id: string;
135
+ label: string;
136
+ data: { label: string; value: number }[];
137
+ headline: number;
138
+ delta?: number;
139
+ }
140
+
141
+ /** What `LineChartCard` is handed, plus the lines the card cannot draw itself. */
142
+ export interface PriceChart {
143
+ title: string;
144
+ ranges: PriceChartRange[];
145
+ defaultRange: string;
146
+ format: (value: number) => string;
147
+ formatAxisValue: (value: number) => string;
148
+ /** Secondary lines under the chart, in reading order. */
149
+ notes: string[];
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Market
154
+ // ---------------------------------------------------------------------------
155
+
156
+ export interface MarketCardData {
157
+ id: string;
158
+ name: string;
159
+ symbol: string;
160
+ currency: string;
161
+ price?: number;
162
+ changePct?: number;
163
+ changeAbs?: number;
164
+ marketCap?: number;
165
+ volume24h?: number;
166
+ /** Optional per key: a row stored by an older version of the tool is not a crash. */
167
+ series: Record<string, PricePoint[] | undefined>;
168
+ }
169
+
170
+ /** The order the switcher offers, not the order the tool happens to store. */
171
+ const MARKET_RANGES = ['1D', '5D', '1M', '6M', 'YTD', '1Y', '5Y', 'MAX'] as const;
172
+
173
+ /**
174
+ * A quote as a line chart, or `null` when no range has two points to draw —
175
+ * the card then says the price in words.
176
+ *
177
+ * A range with one point cannot be drawn and a range with none was never
178
+ * stored, so neither is offered (YTD in the first days of January is the real
179
+ * case). 1D reports the tool's own change rather than the ends of the series,
180
+ * which starts whenever the window opened.
181
+ */
182
+ export function marketChart(data: MarketCardData, t: Translate, locale: string): PriceChart | null {
183
+ const series = data.series ?? {};
184
+ const ranges: PriceChartRange[] = [];
185
+ for (const id of MARKET_RANGES) {
186
+ const run = series[id];
187
+ if (!run || run.length < 2) continue;
188
+ ranges.push({
189
+ id,
190
+ label: t(`market.range.${id}`),
191
+ data: toPoints(run, id === '1D', locale),
192
+ headline: quoted(data.price) ? data.price : run[run.length - 1][1],
193
+ delta: id === '1D' && quoted(data.changePct) ? data.changePct / 100 : runDelta(run),
194
+ });
195
+ }
196
+ if (ranges.length === 0) return null;
197
+
198
+ const format = (value: number) => formatPrice(value, data.currency, locale);
199
+ const compact = (value: number) => {
200
+ const scaled = formatCompact(value, 'market', locale, t, '');
201
+ return scaled === '' ? format(value) : `${scaled} ${data.currency.toUpperCase()}`;
202
+ };
203
+ const notes: string[] = [];
204
+ const intraday = (series['1D'] ?? []).map(([, price]) => price);
205
+ if (intraday.length >= 2) {
206
+ notes.push(
207
+ [
208
+ `${t('market.open')} ${format(intraday[0])}`,
209
+ `${t('market.dayHigh')} ${format(Math.max(...intraday))}`,
210
+ `${t('market.dayLow')} ${format(Math.min(...intraday))}`,
211
+ ].join(' · '),
212
+ );
213
+ }
214
+ const totals = [
215
+ quoted(data.volume24h) ? `${t('market.volume')} ${compact(data.volume24h)}` : null,
216
+ quoted(data.marketCap) ? `${t('market.marketCap')} ${compact(data.marketCap)}` : null,
217
+ ].filter((part): part is string => part !== null);
218
+ if (totals.length > 0) notes.push(totals.join(' · '));
219
+
220
+ return {
221
+ title: `${data.name} · ${data.symbol}`,
222
+ ranges,
223
+ defaultRange: ranges.some((r) => r.id === '1D') ? '1D' : ranges[0].id,
224
+ format,
225
+ formatAxisValue: (value) => formatAxis(value, locale),
226
+ notes,
227
+ };
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // FairCoin
232
+ // ---------------------------------------------------------------------------
233
+
234
+ export interface FairCoinCardData {
235
+ /** `null` is a real answer: the pool had no indexed price when asked. */
236
+ price: number | null;
237
+ changePct: number | null;
238
+ volume24h: number | null;
239
+ liquidityUsd: number | null;
240
+ marketCapUsd: number | null;
241
+ /** Where the number came from, which is the one thing this card cannot drop. */
242
+ source: string;
243
+ updatedAt: string;
244
+ /**
245
+ * Absent key and empty array mean different things: the tool omits a period
246
+ * whose fetch failed, and stores `[]` for one with no samples yet.
247
+ */
248
+ series: Record<string, PricePoint[] | undefined>;
249
+ }
250
+
251
+ /** Exactly what the explorer retains, in the order the switcher offers them. */
252
+ const FAIRCOIN_RANGES = ['24h', '7d', '30d', '1y', 'all'] as const;
253
+
254
+ /**
255
+ * FairCoin's price as a line chart, with its provenance ALWAYS in the notes.
256
+ *
257
+ * FairCoin has no exchange listing: the price is GeckoTerminal's indexed price
258
+ * for a thin pool, so where it came from and when is printed in the reading
259
+ * order — a number off a thin pool that looks exactly like an exchange quote
260
+ * is the one thing this card must not ship. `chart` is `null` when no period
261
+ * has two samples; the notes still say why.
262
+ */
263
+ export function faircoinChart(
264
+ data: FairCoinCardData,
265
+ t: Translate,
266
+ locale: string,
267
+ ): { chart: PriceChart | null; notes: string[] } {
268
+ const series = data.series ?? {};
269
+ const format = (value: number) => formatPrice(value, 'USD', locale);
270
+ const ranges: PriceChartRange[] = [];
271
+ for (const id of FAIRCOIN_RANGES) {
272
+ const run = series[id];
273
+ if (!run || run.length < 2) continue;
274
+ ranges.push({
275
+ id,
276
+ label: t(`faircoin.range.${id}`),
277
+ data: toPoints(run, id === '24h', locale),
278
+ headline: quoted(data.price) ? data.price : run[run.length - 1][1],
279
+ delta: id === '24h' && quoted(data.changePct) ? data.changePct / 100 : runDelta(run),
280
+ });
281
+ }
282
+
283
+ const notes: string[] = [t('faircoin.indexed')];
284
+ const at = Date.parse(data.updatedAt);
285
+ // An unparseable stamp is left out rather than printed raw: the source alone
286
+ // is still true.
287
+ notes.push(
288
+ [
289
+ t('faircoin.source', { source: data.source }),
290
+ Number.isNaN(at)
291
+ ? null
292
+ : t('faircoin.updated', {
293
+ time: new Date(at).toLocaleString(locale, { dateStyle: 'medium', timeStyle: 'short' }),
294
+ }),
295
+ ]
296
+ .filter((part): part is string => part !== null)
297
+ .join(' · '),
298
+ );
299
+ if (!quoted(data.price)) notes.push(t('faircoin.noPriceHint'));
300
+ // A period the explorer never answered for is named; one it answered with too
301
+ // few samples is said out loud, so neither reads as a period nobody asked about.
302
+ const missing = FAIRCOIN_RANGES.filter((id) => series[id] === undefined);
303
+ if (missing.length > 0) {
304
+ notes.push(
305
+ t('faircoin.unavailable', {
306
+ ranges: missing.map((id) => t(`faircoin.range.${id}`)).join(', '),
307
+ }),
308
+ );
309
+ }
310
+ if (ranges.length === 0 && missing.length < FAIRCOIN_RANGES.length) {
311
+ const anySample = FAIRCOIN_RANGES.some((id) => (series[id]?.length ?? 0) === 1);
312
+ notes.push(t(anySample ? 'faircoin.noChart' : 'faircoin.noSamples'));
313
+ }
314
+ // FairCoin's scale strings are SUFFIXES ("K", " mil"), unlike the market's
315
+ // templates, so the number is written here and the word appended.
316
+ const compact = (value: number) => {
317
+ const scale = SCALES.find((s) => Math.abs(value) >= s.at);
318
+ if (!scale) return format(value);
319
+ const short = (value / scale.at).toLocaleString(locale, { maximumFractionDigits: 2 });
320
+ return `$${short}${t(`faircoin.scale.${scale.key}`)}`;
321
+ };
322
+ const totals = [
323
+ quoted(data.volume24h) ? `${t('faircoin.volume')} ${compact(data.volume24h)}` : null,
324
+ quoted(data.liquidityUsd) ? `${t('faircoin.liquidity')} ${compact(data.liquidityUsd)}` : null,
325
+ quoted(data.marketCapUsd) ? `${t('faircoin.marketCap')} ${compact(data.marketCapUsd)}` : null,
326
+ ].filter((part): part is string => part !== null);
327
+ if (totals.length > 0) notes.push(totals.join(' · '));
328
+
329
+ if (ranges.length === 0) return { chart: null, notes };
330
+ return {
331
+ chart: {
332
+ title: `${t('faircoin.name')} · ${t('faircoin.symbol')}`,
333
+ ranges,
334
+ defaultRange: ranges.some((r) => r.id === '24h') ? '24h' : ranges[0].id,
335
+ format,
336
+ formatAxisValue: (value) => formatAxis(value, locale),
337
+ notes: [],
338
+ },
339
+ notes,
340
+ };
341
+ }
342
+
343
+ // ---------------------------------------------------------------------------
344
+ // Weather
345
+ // ---------------------------------------------------------------------------
346
+
347
+ export interface WeatherCardData {
348
+ place: string;
349
+ timezone: string;
350
+ current: { temperature: number; humidity: number; windSpeed: number; condition: string };
351
+ hourly: { time: string; temperature: number; precipitationChance: number }[];
352
+ daily: { date: string; condition: string; high: number; low: number }[];
353
+ }
354
+
355
+ /** Celsius on the wire; what the reader sees is their choice, not the answer's. */
356
+ export type TemperatureUnit = 'C' | 'F';
357
+
358
+ export const degrees = (celsius: number, unit: TemperatureUnit) =>
359
+ `${Math.round(unit === 'C' ? celsius : (celsius * 9) / 5 + 32)}°`;
360
+
361
+ /**
362
+ * The weekday a forecast date names, in the reader's language.
363
+ *
364
+ * `YYYY-MM-DD` parses as UTC midnight, which is the previous evening west of
365
+ * Greenwich — so the day is read at noon UTC, which is the same calendar day
366
+ * everywhere a person lives.
367
+ */
368
+ export function forecastWeekday(date: string, locale: string): string {
369
+ const at = Date.parse(`${date}T12:00:00Z`);
370
+ if (Number.isNaN(at)) return date;
371
+ return new Date(at).toLocaleDateString(locale, { weekday: 'short', timeZone: 'UTC' });
372
+ }
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // Scheduled task
376
+ // ---------------------------------------------------------------------------
377
+
378
+ export interface ScheduledTaskCardData {
379
+ id: string;
380
+ objective: string;
381
+ enabled: boolean;
382
+ trigger: AutomationTrigger;
383
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * A turn that did not get its answer, and what the thread shows for it.
3
+ *
4
+ * ## Why this is a thing at all
5
+ *
6
+ * A hosted-inference failure reaches the client as a typed error, never as
7
+ * an answer: on the open stream, an `{"error": {code, retryable, reference}}`
8
+ * frame and no stop chunk; before the stream opened, an HTTP 503/500 with the
9
+ * same envelope (`packages/api/src/routes/v1/chat-completions.ts`,
10
+ * `failTurn`). Older servers answered a failure BEFORE any output with HTTP
11
+ * 200 and an ordinary content delta — "all models are busy, try again in a few
12
+ * seconds" — flagged only by `alia_meta: { synthetic: true, retryable: true }`
13
+ * on the chunk; that flag is still read, so a stand-in is never rendered.
14
+ * Rendered as prose a stand-in reads as Alia declining; rolled back either
15
+ * vanishes with a toast, and once the toast is gone there is nothing on screen
16
+ * that says what happened or offers to try again.
17
+ *
18
+ * So a failed send keeps the person's turn in the thread and hangs this on it:
19
+ * a persistent error the row is drawn with, and the material a retry needs.
20
+ *
21
+ * ## What the server persists when the reply is synthetic — measured, not assumed
22
+ *
23
+ * Nothing. `saveConversationResult` is called from `lib/chat/provider-loop.ts`
24
+ * only after a stream COMPLETES (`:331`); every other exit returns
25
+ * `{ status: 'exhausted' }` (`:458`) or throws, and both the route's synthetic
26
+ * branch and its outer `catch` write the stand-in chunk without ever reaching
27
+ * the saver. So the user message of a failed turn is not in the database, and
28
+ * a retry can simply re-send it through the same path — no second user row is
29
+ * created because no first one was. (Were a turn ever saved before its tail
30
+ * failed, `saveConversation` converges storage on the client's history anyway,
31
+ * by full rewrite on divergence.)
32
+ */
33
+ export interface FailedTurn {
34
+ /** The person's message the missing answer belonged to. */
35
+ userMessageId: string;
36
+ /**
37
+ * The row the error is drawn UNDER: the user message when nothing came
38
+ * back, or the assistant message when real output arrived before the tail
39
+ * failed — in which case the output is kept and the error sits after it.
40
+ */
41
+ anchorMessageId: string;
42
+ /** Whether trying again is offered. `false` when the server said so. */
43
+ retryable: boolean;
44
+ /** Real output arrived before the failure; the error is a tail, not the answer. */
45
+ partial: boolean;
46
+ /** The server's own words, when it sent any worth repeating. */
47
+ detail?: string;
48
+ }
49
+
50
+ /** What a chunk's `alia_meta` says about itself. */
51
+ export interface AliaMeta {
52
+ /** The content is a stand-in the server wrote, not something the model said. */
53
+ synthetic: boolean;
54
+ /** Sending the same turn again is worth a try. Defaults to `true`. */
55
+ retryable: boolean;
56
+ /** Stable product error code; never an upstream/provider message. */
57
+ code?: string;
58
+ /** Safe Alia run identifier support can use to trace the failed turn. */
59
+ reference?: string;
60
+ retryAfter?: number;
61
+ }
62
+
63
+ /**
64
+ * Read `alia_meta` off a parsed stream chunk.
65
+ *
66
+ * Tolerant of every shape a chunk can have — no meta, meta with only one of
67
+ * the flags, a non-object where an object was expected — because this runs
68
+ * on every content delta and a throw here would take the whole stream down.
69
+ */
70
+ export function readAliaMeta(chunk: unknown): AliaMeta {
71
+ const meta = (chunk as { alia_meta?: unknown } | null | undefined)?.alia_meta;
72
+ if (meta === null || typeof meta !== 'object') {
73
+ return { synthetic: false, retryable: true };
74
+ }
75
+ const { synthetic, retryable, error } = meta as {
76
+ synthetic?: unknown;
77
+ retryable?: unknown;
78
+ error?: unknown;
79
+ };
80
+ const failure =
81
+ error !== null && typeof error === 'object'
82
+ ? (error as { code?: unknown; reference?: unknown; retryAfter?: unknown })
83
+ : null;
84
+ return {
85
+ synthetic: synthetic === true,
86
+ retryable: retryable !== false,
87
+ ...(typeof failure?.code === 'string' ? { code: failure.code } : {}),
88
+ ...(typeof failure?.reference === 'string' ? { reference: failure.reference } : {}),
89
+ ...(typeof failure?.retryAfter === 'number' ? { retryAfter: failure.retryAfter } : {}),
90
+ };
91
+ }
92
+
93
+ /**
94
+ * The line under a failed turn's card: the stable code and the run reference.
95
+ *
96
+ * Never the server's prose. The card's own line is the app's, in the reader's
97
+ * language; this is what support needs, and both halves are language-free.
98
+ * Empty when the server sent neither.
99
+ */
100
+ export function failureDetail(failure: { code?: string; reference?: string }): string | undefined {
101
+ const detail = [
102
+ failure.code,
103
+ failure.reference === undefined ? undefined : `Ref ${failure.reference}`,
104
+ ]
105
+ .filter((value): value is string => typeof value === 'string' && value !== '')
106
+ .join(' · ');
107
+ return detail === '' ? undefined : detail;
108
+ }
@@ -0,0 +1,148 @@
1
+ import type { MessagePlan, ResearchProgress } from '../types';
2
+
3
+ /**
4
+ * The canonical tool invocation: streaming, the conversation hooks and the
5
+ * thread UI all read this one shape.
6
+ */
7
+ export interface ToolInvocation {
8
+ toolCallId: string;
9
+ toolName: string;
10
+ state: 'partial-call' | 'call' | 'result';
11
+ args?: Record<string, unknown>;
12
+ // Tool output is dynamically shaped per tool and consumed structurally
13
+ // (e.g. `result.results`, `result.url`) by the thread UI; typing it more
14
+ // narrowly would require validating every tool's output shape.
15
+ // biome-ignore lint/suspicious/noExplicitAny: structural per-tool output, see above
16
+ result?: any;
17
+ }
18
+
19
+ /** What a message carries to the model: plain text, or multi-part once images are attached. */
20
+ export type MessageContent = string | Array<{ type: string; [key: string]: unknown }>;
21
+
22
+ export interface Message {
23
+ id: string;
24
+ role: 'user' | 'assistant' | 'system';
25
+ content: MessageContent;
26
+ /** Extended thinking content (when thinking mode is enabled). */
27
+ thinking?: string;
28
+ toolInvocations?: ToolInvocation[];
29
+ /** Voice fields (optional, only present for voice-originated messages). */
30
+ source?: 'text' | 'voice';
31
+ /** Speaker identity in voice mode. */
32
+ speaker?: 'primary';
33
+ /**
34
+ * The turn is being streamed into this message right now.
35
+ *
36
+ * Set by `useConversationStream` on the assistant placeholder it appends and
37
+ * cleared by it when the stream ends, whichever way it ends; voice sets it
38
+ * on its own rows. Absent on a message read back from the server.
39
+ */
40
+ isStreaming?: boolean;
41
+ /**
42
+ * How the turn that wrote this message ended, once it has: on its own
43
+ * (`completed`), on an error (`failed`), or because the person stopped it
44
+ * (`cancelled`). Stamped by `useConversationStream` alongside clearing
45
+ * `isStreaming`, and absent on a message the server sent.
46
+ */
47
+ turnOutcome?: 'completed' | 'failed' | 'cancelled';
48
+ /**
49
+ * The server does not hold this message yet, so nothing can be addressed to
50
+ * it by id — a vote, a read-aloud clip stored on it.
51
+ *
52
+ * Set on every message of a turn the stream sends (the question, the reply,
53
+ * a delegated agent's answer) and cleared when that turn completes, which is
54
+ * when the server has stored it under these same ids. A turn that failed or
55
+ * was stopped keeps it.
56
+ */
57
+ unsaved?: true;
58
+ /**
59
+ * The consulted agent that wrote this message (`consultAgent`, ADR 0020 §5): one message per
60
+ * consultation, drawn under the agent's own identity.
61
+ *
62
+ * `character` is the agent's look as the server stores it (JSON), parsed
63
+ * where it is drawn; absent, the default look.
64
+ */
65
+ agentInfo?: {
66
+ id: string;
67
+ name: string;
68
+ character?: unknown;
69
+ handle?: string;
70
+ color?: string;
71
+ };
72
+ /**
73
+ * When the message was written, ISO — what the date separators are derived
74
+ * from, in the reader's own timezone. Absent means the turn has not been
75
+ * persisted yet and carries no stamp of its own.
76
+ */
77
+ createdAt?: string;
78
+ // Streaming UI metadata (deep research, plan preview)
79
+ researchProgress?: ResearchProgress;
80
+ plan?: MessagePlan;
81
+ }
82
+
83
+ export interface Conversation {
84
+ id: string;
85
+ title: string;
86
+ lastMessage?: string;
87
+ source?: string;
88
+ /**
89
+ * The agent this conversation is a stretch of, or none.
90
+ *
91
+ * NULLABLE, not optional-absent: the column is `agent_id text` and the list
92
+ * route sends `c.agentId` straight through, so an ordinary conversation
93
+ * arrives carrying an explicit `null`.
94
+ */
95
+ agentId?: string | null;
96
+ createdAt: Date;
97
+ updatedAt: Date;
98
+ messages: Message[];
99
+ }
100
+
101
+ /** The run a delegated agent started for a turn: its session is `turnId`, in the agent's own room. */
102
+ export interface AgentRun {
103
+ turnId: string;
104
+ agentId: string;
105
+ }
106
+
107
+ /** One page of the paginated conversation list. */
108
+ export interface ConversationsPage {
109
+ conversations: Conversation[];
110
+ nextCursor: string | null;
111
+ hasMore: boolean;
112
+ }
113
+
114
+ /** React Query infinite-query cache shape for the conversation list. */
115
+ export interface ConversationsInfiniteData {
116
+ pages: ConversationsPage[];
117
+ pageParams: unknown[];
118
+ }
119
+
120
+ /** What a chat turn is sent with. */
121
+ export interface SendOptions {
122
+ /** `null` explicitly withholds MCP tools; omission preserves legacy callers. */
123
+ mcpServerId?: string | null;
124
+ /**
125
+ * The skills chosen for THIS message, by name. Per turn rather than per
126
+ * session; omitted means the person chose none.
127
+ */
128
+ skillNames?: string[];
129
+ }
130
+
131
+ /** A resource an automation acts on or listens to. */
132
+ export interface AutomationResource {
133
+ appId: string;
134
+ effectiveAccountId: string;
135
+ resourceType: string;
136
+ resourceId: string;
137
+ }
138
+
139
+ /** What starts a scheduled task. */
140
+ export type AutomationTrigger =
141
+ | { type: 'manual' }
142
+ | {
143
+ type: 'event';
144
+ appId: string | null;
145
+ eventType: string | null;
146
+ resource?: AutomationResource | null;
147
+ }
148
+ | { type: 'schedule'; cron: string | null; timezone: string | null };
@@ -0,0 +1,44 @@
1
+ export type UsageLimitType = 'credits' | 'rate_limit' | 'model_access';
2
+
3
+ export interface UsageLimitDetails {
4
+ type: UsageLimitType;
5
+ code: string;
6
+ message: string;
7
+ retryable: boolean;
8
+ retryAfterSeconds?: number;
9
+ suggestedAction?: 'upgrade' | 'wait';
10
+ limitType?: string;
11
+ current?: number;
12
+ limit?: number;
13
+ tier?: string;
14
+ }
15
+
16
+ export class UsageLimitError extends Error {
17
+ public details: UsageLimitDetails;
18
+
19
+ constructor(details: UsageLimitDetails) {
20
+ super(details.message);
21
+ this.name = 'UsageLimitError';
22
+ this.details = details;
23
+ }
24
+
25
+ get isCreditsError(): boolean {
26
+ return this.details.type === 'credits';
27
+ }
28
+
29
+ get isRateLimitError(): boolean {
30
+ return this.details.type === 'rate_limit';
31
+ }
32
+
33
+ get isModelAccessError(): boolean {
34
+ return this.details.type === 'model_access';
35
+ }
36
+
37
+ get shouldShowUpgrade(): boolean {
38
+ return (
39
+ this.details.suggestedAction === 'upgrade' ||
40
+ this.details.type === 'credits' ||
41
+ this.details.type === 'model_access'
42
+ );
43
+ }
44
+ }