@alia.onl/sdk 22.1.1 → 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 +26 -0
- package/package.json +1 -1
- package/src/components/AmbientField.tsx +1 -1
- package/src/components/ReplyMarkdown.tsx +8 -8
- package/src/components/canvas/ChartRenderer.tsx +1 -1
- package/src/components/thought/ThoughtPanel.tsx +1 -1
- package/src/components/thread/AliaThread.tsx +2 -2
- package/src/hooks/useTimelineWindow.ts +1 -1
- package/src/lib/audio-level.ts +2 -2
- package/src/lib/catalogue-tool.ts +1 -1
- package/src/lib/citations.ts +1 -1
- package/src/runtime/config.tsx +5 -0
- package/src/runtime/conversations.ts +9 -5
- package/src/runtime/host-tools.ts +97 -18
- package/src/runtime/index.ts +2 -1
- package/src/runtime/message-history.ts +1 -1
- package/src/runtime/routes.ts +2 -0
- package/src/runtime/stream-events.ts +16 -2
- package/src/runtime/thread/thought-utils.ts +3 -2
- package/src/runtime/thread/thread-history.ts +1 -1
- package/src/runtime/thread/timeline.ts +2 -2
- package/src/runtime/thread/tool-cards.ts +11 -7
- package/src/runtime/use-conversation-stream.ts +34 -5
- package/src/runtime/use-conversation-thread.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
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
|
+
|
|
20
|
+
## 22.1.2
|
|
21
|
+
|
|
22
|
+
- **Every entry type-checks under `noUncheckedIndexedAccess`.** The SDK ships source, so a
|
|
23
|
+
consumer compiling with `"noUncheckedIndexedAccess": true` (Astro) type-checked the SDK under
|
|
24
|
+
it too and got errors from indexed reads in the runtime, thread, citation and catalogue-tool
|
|
25
|
+
modules (27 from `@alia.onl/sdk/runtime` alone). Each read is now narrowed where the index is
|
|
26
|
+
known to be in range, with a loop over the values where that is simpler. Behaviour is unchanged.
|
|
27
|
+
- `check:entry-types` compiles every entry with `noUncheckedIndexedAccess` on, so CI keeps it so.
|
|
28
|
+
|
|
3
29
|
## 22.1.1
|
|
4
30
|
|
|
5
31
|
- A delayed Stop acknowledgement cannot idle a newer turn, replace its sidebar
|
package/package.json
CHANGED
|
@@ -121,7 +121,7 @@ function parseBlockData(type: string, match: RegExpExecArray): any {
|
|
|
121
121
|
switch (type) {
|
|
122
122
|
case 'COMPACTLIST': {
|
|
123
123
|
const title = match[1];
|
|
124
|
-
const itemsText = match[2];
|
|
124
|
+
const itemsText = match[2] ?? '';
|
|
125
125
|
const items = itemsText
|
|
126
126
|
.split('\n')
|
|
127
127
|
.filter((line) => line.trim().startsWith('-'))
|
|
@@ -140,26 +140,26 @@ function parseBlockData(type: string, match: RegExpExecArray): any {
|
|
|
140
140
|
return {
|
|
141
141
|
type: match[1],
|
|
142
142
|
title: match[2],
|
|
143
|
-
content: match[3].trim(),
|
|
143
|
+
content: (match[3] ?? '').trim(),
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
case 'COMPARISON': {
|
|
147
147
|
const title = match[1];
|
|
148
|
-
const content = match[2];
|
|
148
|
+
const content = match[2] ?? '';
|
|
149
149
|
const leftMatch = content.match(/LEFT:\s*({.*?})/s);
|
|
150
150
|
const rightMatch = content.match(/RIGHT:\s*({.*?})/s);
|
|
151
151
|
const conclusionMatch = content.match(/CONCLUSION:\s*(.*?)$/s);
|
|
152
152
|
|
|
153
153
|
return {
|
|
154
154
|
title,
|
|
155
|
-
left: leftMatch ? JSON.parse(leftMatch[1]) : {},
|
|
156
|
-
right: rightMatch ? JSON.parse(rightMatch[1]) : {},
|
|
157
|
-
conclusion: conclusionMatch ? conclusionMatch[1].trim() : undefined,
|
|
155
|
+
left: leftMatch ? JSON.parse(leftMatch[1] ?? '') : {},
|
|
156
|
+
right: rightMatch ? JSON.parse(rightMatch[1] ?? '') : {},
|
|
157
|
+
conclusion: conclusionMatch ? (conclusionMatch[1] ?? '').trim() : undefined,
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
160
|
case 'TIMELINE': {
|
|
161
161
|
const title = match[1];
|
|
162
|
-
const itemsText = match[2];
|
|
162
|
+
const itemsText = match[2] ?? '';
|
|
163
163
|
const items = itemsText
|
|
164
164
|
.split('\n')
|
|
165
165
|
.filter((line) => line.trim().startsWith('-'))
|
|
@@ -183,7 +183,7 @@ function parseBlockData(type: string, match: RegExpExecArray): any {
|
|
|
183
183
|
}
|
|
184
184
|
case 'CREDIBILITY': {
|
|
185
185
|
return {
|
|
186
|
-
level: parseInt(match[1], 10),
|
|
186
|
+
level: parseInt(match[1] ?? '', 10),
|
|
187
187
|
source: match[2],
|
|
188
188
|
};
|
|
189
189
|
}
|
|
@@ -30,7 +30,7 @@ export function ChartRenderer({ data, title }: ChartRendererProps) {
|
|
|
30
30
|
labels.map((label, i) => ({ label, value: values[i] || 0 }));
|
|
31
31
|
|
|
32
32
|
if (chartType === 'line' && datasets.length === 1) {
|
|
33
|
-
const series = datasets[0];
|
|
33
|
+
const series = datasets[0] as (typeof datasets)[number];
|
|
34
34
|
return (
|
|
35
35
|
<LineChartCard
|
|
36
36
|
title={series.label || title}
|
|
@@ -129,7 +129,7 @@ function StepsTab({ steps, lifecycle }: { steps: ThoughtStep[]; lifecycle: TurnL
|
|
|
129
129
|
return <EmptyState variant="compact" description={t('thought.noSteps')} />;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
const last = steps[steps.length - 1];
|
|
132
|
+
const last = steps[steps.length - 1] as (typeof steps)[number];
|
|
133
133
|
const tail = live && (last.type === 'thinking' || last.type === 'writing') ? last : null;
|
|
134
134
|
const rows = tail === null ? steps : steps.slice(0, -1);
|
|
135
135
|
|
|
@@ -213,7 +213,7 @@ export const AliaThread = memo(function AliaThread({
|
|
|
213
213
|
if (index !== lastReplyIndex || index !== rows.length - 1) return false;
|
|
214
214
|
if (index < history.length || failedTurn?.anchorMessageId === message.id) return false;
|
|
215
215
|
for (let i = index - 1; i >= history.length; i--) {
|
|
216
|
-
if (rows[i]
|
|
216
|
+
if (rows[i]?.role === 'user') return true;
|
|
217
217
|
}
|
|
218
218
|
return false;
|
|
219
219
|
};
|
|
@@ -223,7 +223,7 @@ export const AliaThread = memo(function AliaThread({
|
|
|
223
223
|
* an old message belongs to the stretch that ended, not to the one on screen.
|
|
224
224
|
*/
|
|
225
225
|
const conversationOf = (index: number): string | undefined =>
|
|
226
|
-
index < history.length ? history[index]
|
|
226
|
+
index < history.length ? history[index]?.conversationId : conversationId;
|
|
227
227
|
|
|
228
228
|
// ── Which rows are mounted, and the scroll that goes with them ──
|
|
229
229
|
/** The row a cursor jump was aimed at, as an index into what is shown, or -1. */
|
package/src/lib/audio-level.ts
CHANGED
|
@@ -71,8 +71,8 @@ export function levelFromFrames(frames: readonly number[]): number {
|
|
|
71
71
|
if (frames.length === 0) return 0;
|
|
72
72
|
|
|
73
73
|
let sumSquares = 0;
|
|
74
|
-
for (
|
|
75
|
-
sumSquares +=
|
|
74
|
+
for (const frame of frames) {
|
|
75
|
+
sumSquares += frame * frame;
|
|
76
76
|
}
|
|
77
77
|
const rms = Math.sqrt(sumSquares / frames.length);
|
|
78
78
|
|
|
@@ -12,7 +12,7 @@ const CANONICAL_TOOL = /^(?:oxy|self)_([a-z][a-z0-9-]{0,63})__([A-Za-z0-9_-]+)$/
|
|
|
12
12
|
/** The app and tool of a canonical catalogue tool name, or `null` for any other name. */
|
|
13
13
|
export function parseCatalogueTool(toolName: string): { app: string; tool: string } | null {
|
|
14
14
|
const match = CANONICAL_TOOL.exec(toolName);
|
|
15
|
-
return match ? { app: match[1], tool: match[2] } : null;
|
|
15
|
+
return match ? { app: match[1] ?? '', tool: match[2] ?? '' } : null;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** The renderer key of a tool name (`<app>:<tool>`), or `null` when it is not a catalogue tool. */
|
package/src/lib/citations.ts
CHANGED
|
@@ -180,7 +180,7 @@ export function splitReferences(
|
|
|
180
180
|
const m = REFERENCE_LINE_RE.exec(line);
|
|
181
181
|
if (m) {
|
|
182
182
|
flush();
|
|
183
|
-
current = { id: Number(m[1]), rest: m[2] };
|
|
183
|
+
current = { id: Number(m[1]), rest: m[2] ?? '' };
|
|
184
184
|
} else if (current && line.trim().length > 0) {
|
|
185
185
|
// A legacy entry's URL sits on the indented line after the title.
|
|
186
186
|
current.rest += ` ${line.trim()}`;
|
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. */
|
|
@@ -275,13 +275,14 @@ function upsertOnTop(
|
|
|
275
275
|
if (moveExisting) {
|
|
276
276
|
// Remove the conversation from its current position (if it is in any page)
|
|
277
277
|
for (let i = 0; i < pages.length; i++) {
|
|
278
|
-
const
|
|
278
|
+
const page = pages[i] as ConversationsPage;
|
|
279
|
+
const existingIndex = page.conversations.findIndex((c) => c.id === row.id);
|
|
279
280
|
if (existingIndex >= 0) {
|
|
280
281
|
pages[i] = {
|
|
281
|
-
...
|
|
282
|
+
...page,
|
|
282
283
|
conversations: [
|
|
283
|
-
...
|
|
284
|
-
...
|
|
284
|
+
...page.conversations.slice(0, existingIndex),
|
|
285
|
+
...page.conversations.slice(existingIndex + 1),
|
|
285
286
|
],
|
|
286
287
|
};
|
|
287
288
|
break;
|
|
@@ -347,7 +348,10 @@ export function useSaveConversation() {
|
|
|
347
348
|
id,
|
|
348
349
|
title: offlineTitle,
|
|
349
350
|
lastMessage,
|
|
350
|
-
createdAt:
|
|
351
|
+
createdAt:
|
|
352
|
+
existingIndex >= 0
|
|
353
|
+
? (conversations[existingIndex] as Conversation).createdAt
|
|
354
|
+
: new Date(),
|
|
351
355
|
updatedAt: new Date(),
|
|
352
356
|
messages,
|
|
353
357
|
};
|
|
@@ -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';
|
|
@@ -102,7 +102,7 @@ export function normalizeConversationMessages(messages: readonly Message[]): Mes
|
|
|
102
102
|
messages = messages.filter((message) => message.role !== 'system');
|
|
103
103
|
|
|
104
104
|
for (let index = 0; index < messages.length; index += 1) {
|
|
105
|
-
const current = messages[index];
|
|
105
|
+
const current = messages[index] as Message;
|
|
106
106
|
const placeholder = messages[index + 1];
|
|
107
107
|
const duplicate = messages[index + 2];
|
|
108
108
|
|
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). */
|
|
@@ -718,8 +718,9 @@ export function turnTiming(
|
|
|
718
718
|
const index = messages.findIndex((m) => m.id === message.id);
|
|
719
719
|
let sendStamp: number | null = null;
|
|
720
720
|
for (let i = index - 1; i >= 0; i -= 1) {
|
|
721
|
-
|
|
722
|
-
|
|
721
|
+
const earlier = messages[i];
|
|
722
|
+
if (earlier?.role === 'user') {
|
|
723
|
+
sendStamp = stampToMs(earlier.createdAt);
|
|
723
724
|
break;
|
|
724
725
|
}
|
|
725
726
|
}
|
|
@@ -95,7 +95,7 @@ export function threadHistory(
|
|
|
95
95
|
): ThreadMessage[] {
|
|
96
96
|
const history: ThreadMessage[] = [];
|
|
97
97
|
for (let page = pages.length - 1; page >= 0; page -= 1) {
|
|
98
|
-
for (const message of pages[page]
|
|
98
|
+
for (const message of pages[page]?.messages ?? []) {
|
|
99
99
|
if (message.conversationId === activeConversationId) continue;
|
|
100
100
|
history.push({ ...message, id: message.id ?? message.cursor });
|
|
101
101
|
}
|
|
@@ -76,7 +76,7 @@ function tailStart(count: number): number {
|
|
|
76
76
|
/** The anchor that keeps exactly the rows mounted now, from the row at `start`. */
|
|
77
77
|
export function pinAnchor(ids: readonly string[], start: number): TimelineAnchor {
|
|
78
78
|
if (start <= 0 || ids.length === 0) return TOP;
|
|
79
|
-
return { kind: 'row', id: ids[start] };
|
|
79
|
+
return { kind: 'row', id: ids[start] as string };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
@@ -124,7 +124,7 @@ export function sameTimelineShape(previous: readonly Shape[], next: readonly Sha
|
|
|
124
124
|
if (previous === next) return true;
|
|
125
125
|
if (previous.length !== next.length) return false;
|
|
126
126
|
for (let i = 0; i < next.length; i += 1) {
|
|
127
|
-
if (!sameShape(previous[i], next[i])) return false;
|
|
127
|
+
if (!sameShape(previous[i] as Shape, next[i] as Shape)) return false;
|
|
128
128
|
}
|
|
129
129
|
return true;
|
|
130
130
|
}
|
|
@@ -125,8 +125,8 @@ function toPoints(series: readonly PricePoint[], intraday: boolean, locale: stri
|
|
|
125
125
|
|
|
126
126
|
/** First-to-last change as a ratio, or none for a run that started at nothing. */
|
|
127
127
|
function runDelta(series: readonly PricePoint[]): number | undefined {
|
|
128
|
-
const first = series[0][1];
|
|
129
|
-
const last = series[series.length - 1][1];
|
|
128
|
+
const first = (series[0] as PricePoint)[1];
|
|
129
|
+
const last = (series[series.length - 1] as PricePoint)[1];
|
|
130
130
|
return first === 0 ? undefined : (last - first) / first;
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -189,7 +189,7 @@ export function marketChart(data: MarketCardData, t: Translate, locale: string):
|
|
|
189
189
|
id,
|
|
190
190
|
label: t(`market.range.${id}`),
|
|
191
191
|
data: toPoints(run, id === '1D', locale),
|
|
192
|
-
headline: quoted(data.price) ? data.price : run[run.length - 1][1],
|
|
192
|
+
headline: quoted(data.price) ? data.price : (run[run.length - 1] as PricePoint)[1],
|
|
193
193
|
delta: id === '1D' && quoted(data.changePct) ? data.changePct / 100 : runDelta(run),
|
|
194
194
|
});
|
|
195
195
|
}
|
|
@@ -205,7 +205,7 @@ export function marketChart(data: MarketCardData, t: Translate, locale: string):
|
|
|
205
205
|
if (intraday.length >= 2) {
|
|
206
206
|
notes.push(
|
|
207
207
|
[
|
|
208
|
-
`${t('market.open')} ${format(intraday[0])}`,
|
|
208
|
+
`${t('market.open')} ${format(intraday[0] as number)}`,
|
|
209
209
|
`${t('market.dayHigh')} ${format(Math.max(...intraday))}`,
|
|
210
210
|
`${t('market.dayLow')} ${format(Math.min(...intraday))}`,
|
|
211
211
|
].join(' · '),
|
|
@@ -220,7 +220,9 @@ export function marketChart(data: MarketCardData, t: Translate, locale: string):
|
|
|
220
220
|
return {
|
|
221
221
|
title: `${data.name} · ${data.symbol}`,
|
|
222
222
|
ranges,
|
|
223
|
-
defaultRange: ranges.some((r) => r.id === '1D')
|
|
223
|
+
defaultRange: ranges.some((r) => r.id === '1D')
|
|
224
|
+
? '1D'
|
|
225
|
+
: (ranges[0] as (typeof ranges)[number]).id,
|
|
224
226
|
format,
|
|
225
227
|
formatAxisValue: (value) => formatAxis(value, locale),
|
|
226
228
|
notes,
|
|
@@ -275,7 +277,7 @@ export function faircoinChart(
|
|
|
275
277
|
id,
|
|
276
278
|
label: t(`faircoin.range.${id}`),
|
|
277
279
|
data: toPoints(run, id === '24h', locale),
|
|
278
|
-
headline: quoted(data.price) ? data.price : run[run.length - 1][1],
|
|
280
|
+
headline: quoted(data.price) ? data.price : (run[run.length - 1] as PricePoint)[1],
|
|
279
281
|
delta: id === '24h' && quoted(data.changePct) ? data.changePct / 100 : runDelta(run),
|
|
280
282
|
});
|
|
281
283
|
}
|
|
@@ -331,7 +333,9 @@ export function faircoinChart(
|
|
|
331
333
|
chart: {
|
|
332
334
|
title: `${t('faircoin.name')} · ${t('faircoin.symbol')}`,
|
|
333
335
|
ranges,
|
|
334
|
-
defaultRange: ranges.some((r) => r.id === '24h')
|
|
336
|
+
defaultRange: ranges.some((r) => r.id === '24h')
|
|
337
|
+
? '24h'
|
|
338
|
+
: (ranges[0] as (typeof ranges)[number]).id,
|
|
335
339
|
format,
|
|
336
340
|
formatAxisValue: (value) => formatAxis(value, locale),
|
|
337
341
|
notes: [],
|
|
@@ -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 }),
|
|
@@ -897,7 +906,7 @@ export function useConversationStream({
|
|
|
897
906
|
const idx = invocations.findIndex((t) => t.toolCallId === tool_call_id);
|
|
898
907
|
if (idx >= 0) {
|
|
899
908
|
invocations[idx] = {
|
|
900
|
-
...invocations[idx],
|
|
909
|
+
...(invocations[idx] as ToolInvocation),
|
|
901
910
|
state: 'result',
|
|
902
911
|
result: output,
|
|
903
912
|
};
|
|
@@ -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': {
|
|
@@ -1254,7 +1278,11 @@ export function useConversationStream({
|
|
|
1254
1278
|
const idx = invocations.findIndex((t) => t.toolCallId === tool_call_id);
|
|
1255
1279
|
|
|
1256
1280
|
if (idx >= 0) {
|
|
1257
|
-
invocations[idx] = {
|
|
1281
|
+
invocations[idx] = {
|
|
1282
|
+
...(invocations[idx] as ToolInvocation),
|
|
1283
|
+
state: 'result',
|
|
1284
|
+
result: output,
|
|
1285
|
+
};
|
|
1258
1286
|
} else {
|
|
1259
1287
|
invocations.push({
|
|
1260
1288
|
toolCallId: tool_call_id,
|
|
@@ -1413,6 +1441,7 @@ export function useConversationStream({
|
|
|
1413
1441
|
}
|
|
1414
1442
|
},
|
|
1415
1443
|
[
|
|
1444
|
+
api,
|
|
1416
1445
|
apiUrl,
|
|
1417
1446
|
streamFetch,
|
|
1418
1447
|
host,
|
|
@@ -435,7 +435,8 @@ export function useConversationThread({
|
|
|
435
435
|
(assistantMessageId: string): Message | undefined => {
|
|
436
436
|
const idx = messages.findIndex((msg) => msg.id === assistantMessageId);
|
|
437
437
|
for (let i = idx - 1; i >= 0; i--) {
|
|
438
|
-
|
|
438
|
+
const earlier = messages[i];
|
|
439
|
+
if (earlier?.role === 'user') return earlier;
|
|
439
440
|
}
|
|
440
441
|
return undefined;
|
|
441
442
|
},
|