@alia.onl/sdk 22.1.2 → 22.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @alia.onl/sdk
2
2
 
3
+ ## 22.2.0
4
+
5
+ - **Awaited host tools.** `AliaHostToolDefinition.awaitsResult: true` declares a tool whose
6
+ result the model reads in the same turn ("this tab's text"). When the model calls it, the
7
+ runtime runs the host's `execute` once and POSTs `{ toolCallId, outcome, detail?, result? }` at
8
+ once to `/v1/chat/turns/:turnId/host-results` through the authenticated API (with the app
9
+ assertion), instead of queueing the outcome for the next request. `AliaHostOutcome.result`
10
+ carries a string or JSON value of at most `HOST_RESULT_MAX_CHARS` (100,000) characters as JSON;
11
+ a larger one is answered as `failed`. A failed POST is reported to `AliaHostTools.onUndelivered`
12
+ and never retried. `parseHostAction` keeps `awaitsResult` and `turnId`.
13
+ - **Personalization off for one turn.** `AliaTurnRequest.personalization: false` (read at send
14
+ time from `host.turnRequest()`) sends `personalization: false`: Alia reads no memory and offers
15
+ no memory tools for that turn. Nothing is sent otherwise.
16
+ - `awaitsResult` is sent only for a tool that declares it, so existing hosts send the same bytes.
17
+ A host that declares one needs the Alia API receiver for awaited host tools deployed first
18
+ (`docs/agent-embedding.md` §2); an older API refuses the field with `invalid_host`.
19
+
3
20
  ## 22.1.2
4
21
 
5
22
  - **Every entry type-checks under `noUncheckedIndexedAccess`.** The SDK ships source, so a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "22.1.2",
3
+ "version": "22.2.0",
4
4
  "description": "Alia AI chat SDK — the conversation runtime, chat UI, voice calls and dictation for Oxy apps",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -39,6 +39,11 @@ export interface AliaTurnRequest {
39
39
  /** Only `false` is sent: `true` is the server's default. */
40
40
  webSearch?: boolean;
41
41
  deepResearch?: boolean;
42
+ /**
43
+ * `false` turns personalization off for this turn: Alia reads no memory of the person, recalls
44
+ * nothing and offers no memory tools. The conversation is stored as usual. Only `false` is sent.
45
+ */
46
+ personalization?: false;
42
47
  }
43
48
 
44
49
  /** Why the thread asks its host to show the thought panel. */
@@ -7,10 +7,15 @@
7
7
  * the outcome with its NEXT request, where it becomes that call's result. A host tool is never
8
8
  * authority over data: it moves the host's own screen.
9
9
  *
10
- * Two rules hold here, in a queue that knows nothing of React:
10
+ * A tool declared with `awaitsResult: true` is read in the SAME turn instead ("this tab's text",
11
+ * "the open tabs"): the server waits, and this client answers it at once on the turn's
12
+ * `host-results` route with the outcome and the `result` its `execute` returned.
13
+ *
14
+ * Three rules hold here, in a queue that knows nothing of React:
11
15
  * - an action is performed at most once, whatever the stream repeats;
12
16
  * - an outcome stays queued until a request carrying it has been accepted, so a turn that ends
13
- * before its outcomes are sent hands them to the next one.
17
+ * before its outcomes are sent hands them to the next one;
18
+ * - an awaited action's answer is sent once, never queued and never retried.
14
19
  */
15
20
 
16
21
  import type { HostAction } from './stream-events';
@@ -23,6 +28,11 @@ export interface AliaHostToolDefinition {
23
28
  description: string;
24
29
  /** A JSON Schema with an object root. */
25
30
  inputSchema: Record<string, unknown>;
31
+ /**
32
+ * The model reads this tool's result in the same turn: `execute`'s `result` goes back to it at
33
+ * once. Needs an Alia API with awaited host tools (`docs/agent-embedding.md` §2).
34
+ */
35
+ awaitsResult?: boolean;
26
36
  }
27
37
 
28
38
  export const HOST_OUTCOMES = ['applied', 'inline', 'rejected', 'stale', 'failed'] as const;
@@ -33,6 +43,11 @@ export interface AliaHostOutcome {
33
43
  outcome: AliaHostOutcomeKind;
34
44
  /** At most 500 characters; longer is cut. */
35
45
  detail?: string;
46
+ /**
47
+ * What an awaited tool read, for the model: a string or any JSON value, at most
48
+ * {@link HOST_RESULT_MAX_CHARS} characters as JSON. Ignored for a tool that does not await.
49
+ */
50
+ result?: unknown;
36
51
  }
37
52
 
38
53
  /**
@@ -48,6 +63,11 @@ export interface AliaHostTools {
48
63
  definitions: readonly AliaHostToolDefinition[];
49
64
  /** Perform `action` and say what came of it. Called once per action. */
50
65
  execute: (action: HostAction) => Promise<AliaHostOutcome>;
66
+ /**
67
+ * An awaited action's answer could not be delivered (the request failed). It is not retried:
68
+ * the model reads that the app did not answer in time.
69
+ */
70
+ onUndelivered?: (action: HostAction, error: unknown) => void;
51
71
  }
52
72
 
53
73
  /** An outcome as a request carries it. */
@@ -57,6 +77,11 @@ export interface HostOutcomeRecord {
57
77
  detail?: string;
58
78
  }
59
79
 
80
+ /** An awaited action's answer, as `POST /v1/chat/turns/:turnId/host-results` carries it. */
81
+ export interface HostResultRecord extends HostOutcomeRecord {
82
+ result?: unknown;
83
+ }
84
+
60
85
  /** The request's `host` member. */
61
86
  export interface HostRequest {
62
87
  app: string;
@@ -68,6 +93,21 @@ export interface HostRequest {
68
93
 
69
94
  const DETAIL_LIMIT = 500;
70
95
 
96
+ /** The server's bound on an awaited action's `result`, as JSON. */
97
+ export const HOST_RESULT_MAX_CHARS = 100_000;
98
+
99
+ function resultOf(result: unknown): { result?: unknown } | 'too_large' {
100
+ if (result === undefined || result === null) return {};
101
+ let text: string | undefined;
102
+ try {
103
+ text = JSON.stringify(result);
104
+ } catch {
105
+ return {};
106
+ }
107
+ if (text === undefined) return {};
108
+ return text.length > HOST_RESULT_MAX_CHARS ? 'too_large' : { result };
109
+ }
110
+
71
111
  function record(toolCallId: string, outcome: AliaHostOutcome | undefined): HostOutcomeRecord {
72
112
  const kind = HOST_OUTCOMES.includes(outcome?.outcome as AliaHostOutcomeKind)
73
113
  ? (outcome?.outcome as AliaHostOutcomeKind)
@@ -92,25 +132,41 @@ export class HostOutcomeQueue {
92
132
  ): Promise<boolean> {
93
133
  if (this.performed.has(action.toolCallId)) return false;
94
134
  this.performed.add(action.toolCallId);
95
- let outcome: AliaHostOutcome;
96
- if (tools === undefined || app === undefined || action.app !== app) {
97
- outcome = { outcome: 'rejected', detail: 'This app did not declare that tool.' };
98
- } else if (!tools.definitions.some((tool) => tool.name === action.name)) {
99
- outcome = { outcome: 'rejected', detail: `Unknown tool "${action.name}".` };
100
- } else {
101
- try {
102
- outcome = await tools.execute(action);
103
- } catch (error) {
104
- outcome = {
105
- outcome: 'failed',
106
- detail: error instanceof Error ? error.message : 'The app could not do it.',
107
- };
108
- }
109
- }
135
+ const outcome = await perform(action, tools, app);
110
136
  this.ready.push(record(action.toolCallId, outcome));
111
137
  return true;
112
138
  }
113
139
 
140
+ /**
141
+ * Perform an awaited `action` once and `send` its answer at once, with its result. A failed send
142
+ * is reported to the host's `onUndelivered` and not retried. `false` when the action had already
143
+ * been performed.
144
+ */
145
+ async answer(
146
+ action: HostAction,
147
+ tools: AliaHostTools | undefined,
148
+ app: string | undefined,
149
+ send: (answer: HostResultRecord) => Promise<unknown>,
150
+ ): Promise<boolean> {
151
+ if (this.performed.has(action.toolCallId)) return false;
152
+ this.performed.add(action.toolCallId);
153
+ const outcome = await perform(action, tools, app);
154
+ const result = resultOf((outcome as AliaHostOutcome | undefined)?.result);
155
+ const answer: HostResultRecord =
156
+ result === 'too_large'
157
+ ? record(action.toolCallId, {
158
+ outcome: 'failed',
159
+ detail: 'The result was too large to send.',
160
+ })
161
+ : { ...record(action.toolCallId, outcome), ...result };
162
+ try {
163
+ await send(answer);
164
+ } catch (error) {
165
+ tools?.onUndelivered?.(action, error);
166
+ }
167
+ return true;
168
+ }
169
+
114
170
  /** The outcomes ready to send, oldest first. */
115
171
  pending(): HostOutcomeRecord[] {
116
172
  return [...this.ready];
@@ -127,6 +183,27 @@ export class HostOutcomeQueue {
127
183
  }
128
184
  }
129
185
 
186
+ async function perform(
187
+ action: HostAction,
188
+ tools: AliaHostTools | undefined,
189
+ app: string | undefined,
190
+ ): Promise<AliaHostOutcome> {
191
+ if (tools === undefined || app === undefined || action.app !== app) {
192
+ return { outcome: 'rejected', detail: 'This app did not declare that tool.' };
193
+ }
194
+ if (!tools.definitions.some((tool) => tool.name === action.name)) {
195
+ return { outcome: 'rejected', detail: `Unknown tool "${action.name}".` };
196
+ }
197
+ try {
198
+ return await tools.execute(action);
199
+ } catch (error) {
200
+ return {
201
+ outcome: 'failed',
202
+ detail: error instanceof Error ? error.message : 'The app could not do it.',
203
+ };
204
+ }
205
+ }
206
+
130
207
  /**
131
208
  * The `host` member of a request: the chat's `app` and its host tools (none is an empty list),
132
209
  * or nothing for a chat that names no app — Alia's own.
@@ -143,10 +220,12 @@ export function hostRequest(
143
220
  return {
144
221
  app,
145
222
  version: 1,
146
- tools: (tools?.definitions ?? []).map(({ name, description, inputSchema }) => ({
223
+ tools: (tools?.definitions ?? []).map(({ name, description, inputSchema, awaitsResult }) => ({
147
224
  name,
148
225
  description,
149
226
  inputSchema,
227
+ // Only when declared: an API before awaited host tools refuses the unknown field.
228
+ ...(awaitsResult === true ? { awaitsResult: true as const } : {}),
150
229
  })),
151
230
  ...(outcomes.length === 0 ? {} : { outcomes: [...outcomes] }),
152
231
  };
@@ -33,13 +33,14 @@ export {
33
33
  useAgentProfile,
34
34
  } from './agents';
35
35
  export type { AliaAgentFeature, AliaAgentProfile, AliaAgentStarter, FeatureOffer } from './agents';
36
- export { HOST_OUTCOMES, HostOutcomeQueue, hostRequest } from './host-tools';
36
+ export { HOST_OUTCOMES, HOST_RESULT_MAX_CHARS, HostOutcomeQueue, hostRequest } from './host-tools';
37
37
  export type {
38
38
  AliaHostAction,
39
39
  AliaHostOutcome,
40
40
  AliaHostOutcomeKind,
41
41
  AliaHostToolDefinition,
42
42
  AliaHostTools,
43
+ HostResultRecord as AliaHostResultRecord,
43
44
  } from './host-tools';
44
45
  export { FEATURE_ENTITLEMENT_REQUIRED, parseConsultEvent, parseHostAction } from './stream-events';
45
46
  export type { ConsultAgent, ConsultEvent } from './stream-events';
@@ -11,6 +11,8 @@
11
11
  */
12
12
  export const API_ROUTES = {
13
13
  chat: '/v1/chat/completions',
14
+ /** The answer to an awaited host action, read by the turn that is waiting for it. */
15
+ hostResults: (turnId: string) => `/v1/chat/turns/${encodeURIComponent(turnId)}/host-results`,
14
16
  conversations: {
15
17
  list: '/v1/conversations',
16
18
  save: '/v1/conversations',
@@ -125,15 +125,29 @@ export interface HostAction {
125
125
  app: string;
126
126
  name: string;
127
127
  input: Record<string, unknown>;
128
+ /**
129
+ * The model is waiting for this action's result within the same turn: its outcome is answered
130
+ * at once on the turn's `host-results` route, never with the next request.
131
+ */
132
+ awaitsResult?: true;
133
+ /** The turn that waits, present exactly when `awaitsResult` is. */
134
+ turnId?: string;
128
135
  }
129
136
 
130
137
  /** The host action an `alia.host_action` frame carries, or `null` when it is not well-formed. */
131
138
  export function parseHostAction(data: unknown): HostAction | null {
132
139
  if (!isObject(data) || data.eventVersion !== 1) return null;
133
- const { toolCallId, app, name, input } = data;
140
+ const { toolCallId, app, name, input, awaitsResult, turnId } = data;
134
141
  if (!isText(toolCallId) || !isText(app) || !isText(name)) return null;
135
142
  if (input !== undefined && !isObject(input)) return null;
136
- return { toolCallId, app, name, input: (input as Record<string, unknown> | undefined) ?? {} };
143
+ if (awaitsResult !== undefined && (awaitsResult !== true || !isText(turnId))) return null;
144
+ return {
145
+ toolCallId,
146
+ app,
147
+ name,
148
+ input: (input as Record<string, unknown> | undefined) ?? {},
149
+ ...(awaitsResult === true ? { awaitsResult: true as const, turnId: turnId as string } : {}),
150
+ };
137
151
  }
138
152
 
139
153
  /** The error code that ends a turn an agent's feature gate refused (§5). */
@@ -3,6 +3,7 @@ import { useOxy } from '@oxy.so/services/ui/client';
3
3
  import { useQueryClient } from '@tanstack/react-query';
4
4
  import { useCallback, useEffect, useRef, useState } from 'react';
5
5
  import { useLabels } from '../lib/labels';
6
+ import { useAliaApi } from './api';
6
7
  import { useAppAssertionHeaders } from './app-assertion';
7
8
  import { type AliaArtifact, useAliaRuntime } from './config';
8
9
  import {
@@ -162,6 +163,8 @@ export function useConversationStream({
162
163
  hostTools,
163
164
  }: UseConversationStreamOptions = {}) {
164
165
  const { apiUrl, streamFetch, host, app } = useAliaRuntime();
166
+ /** Where an awaited host action's answer goes: the same account and app assertion as the turn. */
167
+ const api = useAliaApi();
165
168
  /** Read at send time and when an action arrives: the host's declaration may change between turns. */
166
169
  const hostToolsRef = useRef(hostTools);
167
170
  hostToolsRef.current = hostTools;
@@ -658,7 +661,11 @@ export function useConversationStream({
658
661
  * when the callback was built would send the state the composer had
659
662
  * before the person touched it.
660
663
  */
661
- const { deepResearch: deepResearchMode, webSearch } = host.turnRequest?.() ?? {};
664
+ const {
665
+ deepResearch: deepResearchMode,
666
+ webSearch,
667
+ personalization,
668
+ } = host.turnRequest?.() ?? {};
662
669
 
663
670
  // The host's tools, and the outcomes of the actions it performed since the last request.
664
671
  const outcomes = hostOutcomesRef.current.pending();
@@ -688,6 +695,8 @@ export function useConversationStream({
688
695
  // Only sent when OFF. `true` is the server's default and every
689
696
  // request has behaved that way, so sending it would be noise.
690
697
  ...(webSearch === false && { webSearch: false }),
698
+ // Only sent when OFF, like web search: absent is the server's default.
699
+ ...(personalization === false && { personalization: false }),
691
700
  // The power level (or a device model). Oxy picks the model of that
692
701
  // level; the client never names a hosted model.
693
702
  ...(model && { model }),
@@ -939,13 +948,28 @@ export function useConversationStream({
939
948
  // Performed here, by the client that declared it, once; its outcome rides
940
949
  // the next request. The stream does not wait for it.
941
950
  const action = parseHostAction(parsed);
942
- if (action !== null) {
943
- void hostOutcomesRef.current.dispatch(
951
+ if (action === null) continue;
952
+ if (action.awaitsResult && action.turnId !== undefined) {
953
+ // The turn waits for this one: its answer goes back at once, through the
954
+ // same authenticated API (and app assertion) the turn's account uses.
955
+ const turnSignal = controller.signal;
956
+ const route = API_ROUTES.hostResults(action.turnId);
957
+ void hostOutcomesRef.current.answer(
944
958
  action,
945
959
  hostToolsRef.current,
946
960
  appRef.current,
961
+ async (answer) => {
962
+ if (turnSignal.aborted) throw new Error('The turn ended');
963
+ await api.post(route, answer, { signal: turnSignal });
964
+ },
947
965
  );
966
+ continue;
948
967
  }
968
+ void hostOutcomesRef.current.dispatch(
969
+ action,
970
+ hostToolsRef.current,
971
+ appRef.current,
972
+ );
949
973
  continue;
950
974
  }
951
975
  case 'alia.title': {
@@ -1417,6 +1441,7 @@ export function useConversationStream({
1417
1441
  }
1418
1442
  },
1419
1443
  [
1444
+ api,
1420
1445
  apiUrl,
1421
1446
  streamFetch,
1422
1447
  host,