@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 +17 -0
- package/package.json +1 -1
- package/src/runtime/config.tsx +5 -0
- package/src/runtime/host-tools.ts +97 -18
- package/src/runtime/index.ts +2 -1
- package/src/runtime/routes.ts +2 -0
- package/src/runtime/stream-events.ts +16 -2
- package/src/runtime/use-conversation-stream.ts +28 -3
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
package/src/runtime/config.tsx
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
};
|
package/src/runtime/index.ts
CHANGED
|
@@ -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';
|
package/src/runtime/routes.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
|
943
|
-
|
|
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,
|