@alia.onl/sdk 10.0.4 → 10.1.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @alia.onl/sdk
2
2
 
3
+ ## 10.1.1
4
+
5
+ `ChatTextInput` no longer passes `placeholderClassName`. No styling runtime ever
6
+ read it (react-native-css maps only `className` on `TextInput`, and the
7
+ placeholder colour comes from the `placeholder:text-muted-foreground` class), and
8
+ react-native-css 3.1.0-rc.0, the compiler Bloom 7.28+ requires, dropped the type
9
+ declaration that let it compile, so apps that type-check this package's source
10
+ failed on it. Nothing renders differently.
11
+
12
+ ## 10.1.0
13
+
14
+ A failed turn is a typed error. Alia now answers an inference failure before any
15
+ output with its error envelope (an in-stream error frame, or an HTTP 503/500)
16
+ instead of an "all models are busy" answer. `AliaChatStreamError` (now exported,
17
+ with `AliaChatFailure`) carries `status`, `code`, `retryable`, `retryAfter` and
18
+ `reference` from either. The stream reader also accepts `alia.context` and
19
+ `alia.agent_turn`, which it used to reject as unknown events, ending a streaming
20
+ turn before its answer or its error could be read.
21
+
3
22
  ## 10.0.4
4
23
 
5
24
  Admit `@oxy.so/services` 11 and `@oxy.so/core` 4 alongside the earlier majors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "10.0.4",
3
+ "version": "10.1.1",
4
4
  "description": "Alia AI chat SDK — reusable chat UI, voice, and streaming components for Oxy apps",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -168,7 +168,6 @@ const ChatTextInput = React.forwardRef<TextInput, ChatTextInputProps>(
168
168
  props.editable === false && "opacity-50 web:cursor-not-allowed",
169
169
  className
170
170
  )}
171
- placeholderClassName={cn("text-muted-foreground", props.placeholderClassName)}
172
171
  onKeyPress={handleKeyPress}
173
172
  onContentSizeChange={handleContentSizeChange}
174
173
  scrollEnabled={fillContainer || props.multiline}
package/src/index.ts CHANGED
@@ -22,6 +22,8 @@ export { PlanPreviewCard } from './components/PlanPreviewCard';
22
22
  // ── Chat hook ──
23
23
  export { useAliaChat } from './hooks/useAliaChat';
24
24
  export type { UseAliaChatOptions, UseAliaChatReturn } from './hooks/useAliaChat';
25
+ export { AliaChatStreamError } from './lib/chat-stream';
26
+ export type { AliaChatFailure } from './lib/chat-stream';
25
27
 
26
28
  // ── Text-to-speech / speech-to-text (no livekit) ──
27
29
  export { useTTS } from './hooks/useTTS';
@@ -38,13 +38,61 @@ export interface AliaChatStreamResult {
38
38
  readonly finishReason: string;
39
39
  }
40
40
 
41
+ /**
42
+ * What Alia said about a failed turn, when it said anything.
43
+ *
44
+ * A turn that fails — before any output or part-way — ends with Alia's
45
+ * OpenAI-shaped error envelope: an in-stream `{"error": …}` frame, or an HTTP
46
+ * error status with the same body when nothing was sent yet. `code` is Alia's
47
+ * stable error code, `retryable` whether sending the same turn again can help,
48
+ * `retryAfter` the seconds to wait first, and `reference` the run id support
49
+ * traces it by. None of it names the serving operator.
50
+ */
51
+ export interface AliaChatFailure {
52
+ readonly status?: number;
53
+ readonly code?: string;
54
+ readonly retryable?: boolean;
55
+ readonly retryAfter?: number;
56
+ readonly reference?: string;
57
+ }
58
+
41
59
  export class AliaChatStreamError extends Error {
42
- constructor(message: string) {
60
+ readonly status?: number;
61
+ readonly code?: string;
62
+ readonly retryable?: boolean;
63
+ readonly retryAfter?: number;
64
+ readonly reference?: string;
65
+
66
+ constructor(message: string, failure: AliaChatFailure = {}) {
43
67
  super(message);
44
68
  this.name = 'AliaChatStreamError';
69
+ if (failure.status !== undefined) this.status = failure.status;
70
+ if (failure.code !== undefined) this.code = failure.code;
71
+ if (failure.retryable !== undefined) this.retryable = failure.retryable;
72
+ if (failure.retryAfter !== undefined) this.retryAfter = failure.retryAfter;
73
+ if (failure.reference !== undefined) this.reference = failure.reference;
45
74
  }
46
75
  }
47
76
 
77
+ /**
78
+ * Read the failure fields off Alia's error envelope, leniently: a field of the
79
+ * wrong type is left out rather than failing the read of the rest.
80
+ */
81
+ export function readAliaChatFailure(body: unknown): AliaChatFailure {
82
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) return {};
83
+ const error = (body as JsonObject).error;
84
+ if (typeof error !== 'object' || error === null || Array.isArray(error)) return {};
85
+ const detail = error as JsonObject;
86
+ return {
87
+ ...(typeof detail.code === 'string' && detail.code !== '' ? { code: detail.code } : {}),
88
+ ...(typeof detail.retryable === 'boolean' ? { retryable: detail.retryable } : {}),
89
+ ...(typeof detail.retryAfter === 'number' && Number.isFinite(detail.retryAfter) && detail.retryAfter >= 0
90
+ ? { retryAfter: detail.retryAfter }
91
+ : {}),
92
+ ...(typeof detail.reference === 'string' && detail.reference !== '' ? { reference: detail.reference } : {}),
93
+ };
94
+ }
95
+
48
96
  function abortError(): Error {
49
97
  const error = new Error('The Alia stream was aborted.');
50
98
  error.name = 'AbortError';
@@ -177,6 +225,16 @@ function validateIgnoredNamedEvent(eventName: string, payload: JsonObject): void
177
225
  requireEventVersion(payload, eventName);
178
226
 
179
227
  switch (eventName) {
228
+ // Sent on every streaming turn before inference starts (the context-window
229
+ // breakdown) and on a linked agent's turn. Neither is something this SDK
230
+ // shows, and an unknown one ended the turn before its answer — or its
231
+ // error — could be read.
232
+ case 'alia.context':
233
+ return;
234
+ case 'alia.agent_turn':
235
+ asString(payload.turnId, `${eventName} turnId`);
236
+ asString(payload.agentId, `${eventName} agentId`);
237
+ return;
180
238
  case 'alia.suggest_new_conversation':
181
239
  assertOnlyKeys(payload, ['eventVersion', 'reason'], eventName);
182
240
  asString(payload.reason, `${eventName} reason`);
@@ -318,7 +376,9 @@ function parseOpenAIFrame(payload: unknown): ParsedOpenAIFrame {
318
376
  if (body.error !== undefined) {
319
377
  const error = asObject(body.error, 'OpenAI stream error');
320
378
  asString(error.message, 'OpenAI stream error message');
321
- fail('Alia ended the stream with an error.');
379
+ // The server's prose is not carried: the code and retryability are what a
380
+ // caller acts on, and the caller words it in its own language.
381
+ throw new AliaChatStreamError('Alia ended the stream with an error.', readAliaChatFailure(body));
322
382
  }
323
383
  assertOnlyKeys(
324
384
  body,
@@ -1,5 +1,7 @@
1
1
  import {
2
+ AliaChatStreamError,
2
3
  consumeAliaChatStream,
4
+ readAliaChatFailure,
3
5
  type AliaChatStreamEvent,
4
6
  type AliaChatStreamResult,
5
7
  } from './chat-stream';
@@ -59,9 +61,37 @@ export async function streamAliaChat(
59
61
  });
60
62
 
61
63
  if (!response.ok) {
62
- await response.body?.cancel().catch(() => undefined);
63
- throw new Error(`Alia request failed with status ${response.status}.`);
64
+ throw new AliaChatStreamError(`Alia request failed with status ${response.status}.`, {
65
+ ...(await readErrorEnvelope(response)),
66
+ status: response.status,
67
+ });
64
68
  }
65
69
 
66
70
  return consumeAliaChatStream(response, onEvent, signal);
67
71
  }
72
+
73
+ /** Bodies larger than this are not an Alia error envelope and are not read. */
74
+ const MAX_ERROR_BODY_BYTES = 16 * 1024;
75
+
76
+ /**
77
+ * The code, retryability and reference of an HTTP refusal or failed turn.
78
+ *
79
+ * Without them a caller cannot tell "send it again" (a 503 for a turn that
80
+ * failed before any output) from "this will not work" (a 400), so the status
81
+ * is not enough. A body that is large, not JSON or not the envelope yields
82
+ * nothing, and the status alone still reaches the caller.
83
+ */
84
+ async function readErrorEnvelope(response: Response) {
85
+ const length = Number(response.headers.get('content-length') ?? '0');
86
+ if (Number.isFinite(length) && length > MAX_ERROR_BODY_BYTES) {
87
+ await response.body?.cancel().catch(() => undefined);
88
+ return {};
89
+ }
90
+ try {
91
+ const text = await response.text();
92
+ if (text.length > MAX_ERROR_BODY_BYTES) return {};
93
+ return readAliaChatFailure(JSON.parse(text));
94
+ } catch {
95
+ return {};
96
+ }
97
+ }