@datalayer/agent-runtimes 1.5.26 → 1.5.28
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/dist-embed/chunks/{ChatView-CUWE6I5M.js → ChatView-C0PWJ_wj.js} +4 -4
- package/dist-embed/datalayer-app-main.js +111 -101
- package/lib/apps/apps/AppKept.js +5 -1
- package/lib/apps/embed/element.js +10 -4
- package/lib/apps/embed/embedConfig.d.ts +10 -1
- package/lib/apps/embed/embedConfig.js +25 -1
- package/lib/apps/plugins/chat/ChatView.js +2 -2
- package/lib/chat/assistant/AssistantStage.js +17 -0
- package/lib/chat/assistant/clearance.d.ts +10 -0
- package/lib/chat/assistant/clearance.js +14 -0
- package/lib/chat/base/ChatBase.js +375 -437
- package/lib/chat/base/conversationItems.d.ts +55 -0
- package/lib/chat/base/conversationItems.js +126 -0
- package/lib/portable/adapterEvents.d.ts +107 -0
- package/lib/portable/adapterEvents.js +239 -0
- package/lib/portable/approvalCard.d.ts +56 -0
- package/lib/portable/approvalCard.js +56 -0
- package/lib/portable/conversation.d.ts +71 -18
- package/lib/portable/conversation.js +330 -74
- package/lib/portable/fixtures.js +12 -0
- package/lib/portable/index.d.ts +4 -1
- package/lib/portable/index.js +7 -1
- package/lib/portable/presence.js +4 -1
- package/lib/portable/runMeasures.d.ts +46 -0
- package/lib/portable/runMeasures.js +156 -0
- package/lib/protocols/ACPAdapter.js +13 -8
- package/lib/specs/voices.js +47 -0
- package/lib/utils/utils.d.ts +2 -8
- package/lib/utils/utils.js +3 -26
- package/lib/voice/ComposerVoice.js +25 -3
- package/lib/voice/VoiceInput.d.ts +2 -2
- package/lib/voice/VoiceInput.js +6 -4
- package/lib/voice/VoiceMenu.js +2 -1
- package/lib/voice/chatVoice.d.ts +4 -3
- package/lib/voice/chatVoice.js +6 -9
- package/lib/voice/consent.d.ts +8 -0
- package/lib/voice/consent.js +10 -0
- package/lib/voice/hearing.d.ts +23 -1
- package/lib/voice/hearing.js +18 -3
- package/lib/voice/index.d.ts +11 -6
- package/lib/voice/index.js +1 -0
- package/lib/voice/serverHearing.d.ts +58 -0
- package/lib/voice/serverHearing.js +136 -0
- package/lib/voice/settings.d.ts +5 -0
- package/lib/voice/settings.js +8 -0
- package/lib/voice/speaker.d.ts +4 -3
- package/lib/voice/speaker.js +1 -2
- package/lib/voice/useVoiceSpeaker.js +0 -3
- package/package.json +2 -2
|
@@ -3,24 +3,35 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* A conversation as a run stream builds it, for
|
|
7
|
-
*
|
|
8
|
-
* application's session or an agent's run,
|
|
6
|
+
* A conversation as a run stream builds it, for every client that draws one:
|
|
7
|
+
* the web chat (`ChatBase`) and Datalayer Mobile. One pure reducer over the
|
|
8
|
+
* AG-UI events of an application's session or an agent's run, the events
|
|
9
|
+
* every other protocol adapter is normalised into (`portable/adapterEvents`),
|
|
10
|
+
* and the person's own actions.
|
|
9
11
|
*
|
|
10
|
-
* It reads the stream as the
|
|
12
|
+
* It reads the stream as the chat reads it — `TEXT_MESSAGE_*` into one
|
|
11
13
|
* message streamed in place, `TOOL_CALL_*` into a collapsed row whose result
|
|
12
14
|
* is read by `toolResultOf`, `STEP_*` into a step row, a `MESSAGES_SNAPSHOT`
|
|
13
|
-
*
|
|
14
|
-
* what a session says besides:
|
|
15
|
-
* asks (`RUN_FINISHED` with
|
|
16
|
-
*
|
|
17
|
-
* `gone`).
|
|
15
|
+
* (its tool calls and their results too) through `messagesOfThread`, a
|
|
16
|
+
* `reactor.ui` op on the run refused — and adds what a session says besides:
|
|
17
|
+
* `loop.session`, the question its application asks (`RUN_FINISHED` with
|
|
18
|
+
* `result.asking`, LOOP `loop.ask`), and a turn that ended without words said
|
|
19
|
+
* by how it ended (`answered`, `stopped`, `gone`).
|
|
20
|
+
*
|
|
21
|
+
* An adapter that tells a message or a tool call whole rather than by its
|
|
22
|
+
* deltas — A2A, Vercel AI, ACP, the in-page agent, and the web's AG-UI
|
|
23
|
+
* adapter, which accumulates — says it with three events of this reducer's
|
|
24
|
+
* own, never AG-UI's: `MESSAGE_SAID` (a message's words so far),
|
|
25
|
+
* `TOOL_CALL_SAID` (a call's arguments as an object) and `TOOL_CALL_DONE`
|
|
26
|
+
* (its result as a value). They are refused when malformed.
|
|
18
27
|
*
|
|
19
28
|
* Bounded: a long tool run cannot grow it without end. Each tool's arguments
|
|
20
|
-
* and result keep their first `
|
|
21
|
-
*
|
|
22
|
-
* (`dropped` counts the rest), and
|
|
23
|
-
* its type and name, the last
|
|
29
|
+
* and result keep their first `limits.toolText` characters (their values only
|
|
30
|
+
* when kept whole), an answer its first `limits.messageText`, the
|
|
31
|
+
* conversation its last `limits.items` rows (`dropped` counts the rest), and
|
|
32
|
+
* an event it does not draw is kept only as its type and name, the last
|
|
33
|
+
* `limits.diagnostics` of them — never its payload. A phone keeps
|
|
34
|
+
* `DEVICE_LIMITS`, a browser `BROWSER_LIMITS`.
|
|
24
35
|
*
|
|
25
36
|
* @module portable/conversation
|
|
26
37
|
*/
|
|
@@ -28,26 +39,48 @@ import { REACTOR_UI_NAME, toolResultOf, UI_ON_RUN, } from './agui';
|
|
|
28
39
|
import { displayOfToolResult } from './displays';
|
|
29
40
|
import { RUN_END_WORDS } from './records';
|
|
30
41
|
import { messagesOfThread } from './thread';
|
|
31
|
-
/** How many rows a conversation keeps: its newest. */
|
|
42
|
+
/** How many rows a conversation on a phone keeps: its newest. */
|
|
32
43
|
export const ITEM_LIMIT = 400;
|
|
33
|
-
/** How many characters of an answer
|
|
44
|
+
/** How many characters of an answer a phone keeps. */
|
|
34
45
|
export const MESSAGE_TEXT_LIMIT = 100_000;
|
|
35
|
-
/** How many characters of a tool's arguments, and of its result,
|
|
46
|
+
/** How many characters of a tool's arguments, and of its result, a phone keeps. */
|
|
36
47
|
export const TOOL_TEXT_LIMIT = 4_000;
|
|
37
48
|
/** How many events it does not draw are kept, by type and name. */
|
|
38
49
|
export const DIAGNOSTIC_LIMIT = 50;
|
|
50
|
+
/** A phone's bounds (Datalayer Mobile). */
|
|
51
|
+
export const DEVICE_LIMITS = {
|
|
52
|
+
items: ITEM_LIMIT,
|
|
53
|
+
messageText: MESSAGE_TEXT_LIMIT,
|
|
54
|
+
toolText: TOOL_TEXT_LIMIT,
|
|
55
|
+
diagnostics: DIAGNOSTIC_LIMIT,
|
|
56
|
+
};
|
|
57
|
+
/** A browser's bounds (the web chat): a long working session, still bounded. */
|
|
58
|
+
export const BROWSER_LIMITS = {
|
|
59
|
+
items: 5_000,
|
|
60
|
+
messageText: 1_000_000,
|
|
61
|
+
toolText: 1_000_000,
|
|
62
|
+
diagnostics: DIAGNOSTIC_LIMIT,
|
|
63
|
+
};
|
|
39
64
|
/** What is said after a text cut at its limit. */
|
|
40
65
|
export const CUT_MARK = '… (cut: too long to keep on this device)';
|
|
41
|
-
/** A conversation with nothing in it yet. */
|
|
42
|
-
export const emptyConversation = () =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
/** A conversation with nothing in it yet, kept within `limits` (a phone's by default). */
|
|
67
|
+
export const emptyConversation = (limits = DEVICE_LIMITS) => {
|
|
68
|
+
for (const [name, bound] of Object.entries(limits)) {
|
|
69
|
+
if (!Number.isInteger(bound) || bound < 1) {
|
|
70
|
+
throw new Error(`A conversation's ${name} bound is ${String(bound)}: give a whole number of at least 1.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
items: [],
|
|
75
|
+
running: false,
|
|
76
|
+
session: null,
|
|
77
|
+
asking: null,
|
|
78
|
+
dropped: 0,
|
|
79
|
+
diagnostics: [],
|
|
80
|
+
turnRows: 0,
|
|
81
|
+
limits,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
51
84
|
const text = (value) => typeof value === 'string' ? value : '';
|
|
52
85
|
const cut = (value, limit) => value.length > limit ? `${value.slice(0, limit)}${CUT_MARK}` : value;
|
|
53
86
|
/** Text appended to what is kept, never past its limit. */
|
|
@@ -57,12 +90,10 @@ const appended = (kept, delta, limit) => {
|
|
|
57
90
|
}
|
|
58
91
|
return cut(kept + delta, limit);
|
|
59
92
|
};
|
|
93
|
+
const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
60
94
|
/** A result as the model read it: the page's `_meta` (its display) is the card's, not words. */
|
|
61
95
|
function withoutMeta(result) {
|
|
62
|
-
if (result &&
|
|
63
|
-
typeof result === 'object' &&
|
|
64
|
-
!Array.isArray(result) &&
|
|
65
|
-
'_meta' in result) {
|
|
96
|
+
if (isRecord(result) && '_meta' in result) {
|
|
66
97
|
const { _meta: _page, ...view } = result;
|
|
67
98
|
return view;
|
|
68
99
|
}
|
|
@@ -96,14 +127,12 @@ export function questionOf(raw) {
|
|
|
96
127
|
kind,
|
|
97
128
|
prompt: text(asked.prompt),
|
|
98
129
|
options: strings(asked.options),
|
|
99
|
-
schema: schema
|
|
100
|
-
? schema
|
|
101
|
-
: null,
|
|
130
|
+
schema: isRecord(schema) ? schema : null,
|
|
102
131
|
accept: strings(asked.accept),
|
|
103
132
|
};
|
|
104
133
|
}
|
|
105
134
|
function withItems(state, items, added = 0) {
|
|
106
|
-
const over = items.length -
|
|
135
|
+
const over = items.length - state.limits.items;
|
|
107
136
|
return {
|
|
108
137
|
...state,
|
|
109
138
|
items: over > 0 ? items.slice(over) : items,
|
|
@@ -127,11 +156,151 @@ function noted(state, event) {
|
|
|
127
156
|
};
|
|
128
157
|
return {
|
|
129
158
|
...state,
|
|
130
|
-
diagnostics: [...state.diagnostics, entry].slice(-
|
|
159
|
+
diagnostics: [...state.diagnostics, entry].slice(-state.limits.diagnostics),
|
|
131
160
|
};
|
|
132
161
|
}
|
|
133
162
|
let rowCount = 0;
|
|
134
163
|
const rowId = (prefix) => `${prefix}-${Date.now().toString(36)}-${(rowCount += 1)}`;
|
|
164
|
+
/** A tool call just begun: running, nothing said of it yet. */
|
|
165
|
+
function toolRow(id, name) {
|
|
166
|
+
return {
|
|
167
|
+
kind: 'tool',
|
|
168
|
+
id,
|
|
169
|
+
name,
|
|
170
|
+
args: '',
|
|
171
|
+
input: null,
|
|
172
|
+
status: 'running',
|
|
173
|
+
awaiting: null,
|
|
174
|
+
result: '',
|
|
175
|
+
value: undefined,
|
|
176
|
+
error: '',
|
|
177
|
+
executionError: '',
|
|
178
|
+
codeError: null,
|
|
179
|
+
exitCode: null,
|
|
180
|
+
display: null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** Its arguments kept: their words cut at the limit, the object only when kept whole. */
|
|
184
|
+
function withInput(item, input, limit) {
|
|
185
|
+
const words = JSON.stringify(input);
|
|
186
|
+
const whole = words.length <= limit;
|
|
187
|
+
return { ...item, args: cut(words, limit), input: whole ? input : null };
|
|
188
|
+
}
|
|
189
|
+
/** A human-in-the-loop call: its arguments are steps the person confirms. */
|
|
190
|
+
const asksThePerson = (input) => !!input && Array.isArray(input.steps);
|
|
191
|
+
/**
|
|
192
|
+
* A tool call with its result: failed when the protocol, its sandbox, its
|
|
193
|
+
* code or its exit code says so; still running when it waits on an approval
|
|
194
|
+
* (`pending_approval`) or on the person (its steps). A result read from a
|
|
195
|
+
* conversation's past is its outcome only: nothing waits there any more.
|
|
196
|
+
*/
|
|
197
|
+
function withResult(item, result, limit, past = false) {
|
|
198
|
+
const given = result.result;
|
|
199
|
+
const words = said(withoutMeta(given));
|
|
200
|
+
const record = isRecord(given) ? given : null;
|
|
201
|
+
const executionError = typeof record?.execution_error === 'string' ? record.execution_error : '';
|
|
202
|
+
const raised = isRecord(record?.code_error) ? record.code_error : null;
|
|
203
|
+
const codeError = raised
|
|
204
|
+
? {
|
|
205
|
+
name: text(raised.name) || 'Error',
|
|
206
|
+
value: text(raised.value) || 'Unknown error',
|
|
207
|
+
traceback: cut(text(raised.traceback), limit),
|
|
208
|
+
}
|
|
209
|
+
: null;
|
|
210
|
+
const exitCode = record && typeof record.exit_code === 'number' ? record.exit_code : null;
|
|
211
|
+
const failed = !result.success ||
|
|
212
|
+
executionError !== '' ||
|
|
213
|
+
codeError !== null ||
|
|
214
|
+
(exitCode !== null && exitCode !== 0) ||
|
|
215
|
+
record?.execution_ok === false;
|
|
216
|
+
const awaiting = failed || past
|
|
217
|
+
? null
|
|
218
|
+
: record?.pending_approval === true
|
|
219
|
+
? 'approval'
|
|
220
|
+
: asksThePerson(item.input)
|
|
221
|
+
? 'person'
|
|
222
|
+
: null;
|
|
223
|
+
return {
|
|
224
|
+
...item,
|
|
225
|
+
status: failed ? 'error' : awaiting ? 'running' : 'complete',
|
|
226
|
+
awaiting,
|
|
227
|
+
result: cut(words, limit),
|
|
228
|
+
value: words.length <= limit ? given : undefined,
|
|
229
|
+
error: result.error ?? '',
|
|
230
|
+
executionError: cut(executionError, limit),
|
|
231
|
+
codeError,
|
|
232
|
+
exitCode,
|
|
233
|
+
display: displayOfToolResult(item.name, given, item.id),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/** The rows a conversation said again holds: its messages, and its tool calls with their results. */
|
|
237
|
+
function rowsOfSnapshot(messages, limits, unread) {
|
|
238
|
+
if (!Array.isArray(messages)) {
|
|
239
|
+
throw new Error('A messages snapshot carries no list of messages.');
|
|
240
|
+
}
|
|
241
|
+
const rows = [];
|
|
242
|
+
const tools = new Map();
|
|
243
|
+
for (const message of messages) {
|
|
244
|
+
for (const drawn of messagesOfThread([message])) {
|
|
245
|
+
rows.push({
|
|
246
|
+
kind: 'message',
|
|
247
|
+
id: drawn.id,
|
|
248
|
+
role: drawn.role === 'user' ? 'user' : 'assistant',
|
|
249
|
+
text: cut(typeof drawn.content === 'string' ? drawn.content : '', limits.messageText),
|
|
250
|
+
streaming: false,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (!isRecord(message)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (message.role === 'assistant' && message.toolCalls !== undefined) {
|
|
257
|
+
if (!Array.isArray(message.toolCalls)) {
|
|
258
|
+
throw new Error(`Message ${String(message.id)}'s tool calls are not a list.`);
|
|
259
|
+
}
|
|
260
|
+
for (const call of message.toolCalls) {
|
|
261
|
+
const fn = isRecord(call) ? call.function : undefined;
|
|
262
|
+
if (!isRecord(call) || typeof call.id !== 'string' || !isRecord(fn)) {
|
|
263
|
+
throw new Error(`Message ${String(message.id)} holds a tool call without its id and function.`);
|
|
264
|
+
}
|
|
265
|
+
const argsWords = text(fn.arguments);
|
|
266
|
+
let input = null;
|
|
267
|
+
if (argsWords && argsWords.length <= limits.toolText) {
|
|
268
|
+
let parsed;
|
|
269
|
+
try {
|
|
270
|
+
parsed = JSON.parse(argsWords);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
parsed = undefined;
|
|
274
|
+
}
|
|
275
|
+
if (!isRecord(parsed)) {
|
|
276
|
+
throw new Error(`Tool call ${call.id}'s arguments are not a JSON object.`);
|
|
277
|
+
}
|
|
278
|
+
input = parsed;
|
|
279
|
+
}
|
|
280
|
+
tools.set(call.id, rows.length);
|
|
281
|
+
rows.push({
|
|
282
|
+
...toolRow(call.id, text(fn.name)),
|
|
283
|
+
args: cut(argsWords, limits.toolText),
|
|
284
|
+
input: argsWords ? input : {},
|
|
285
|
+
// Said again from the past: it ended, as its result says.
|
|
286
|
+
status: 'complete',
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (message.role === 'tool') {
|
|
291
|
+
const id = text(message.toolCallId);
|
|
292
|
+
const at = tools.get(id);
|
|
293
|
+
if (at === undefined) {
|
|
294
|
+
// A result whose call the snapshot does not carry (a client that
|
|
295
|
+
// reads words only): kept as the unread events are, by name.
|
|
296
|
+
unread.push(id || 'no call');
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
rows[at] = withResult(rows[at], toolResultOf(message.content), limits.toolText, true);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return rows;
|
|
303
|
+
}
|
|
135
304
|
/** The person's prompt, added before it is sent: its own row. */
|
|
136
305
|
export function withPrompt(state, id, words) {
|
|
137
306
|
return withItems({ ...state, asking: null }, [
|
|
@@ -139,6 +308,14 @@ export function withPrompt(state, id, words) {
|
|
|
139
308
|
{ kind: 'message', id, role: 'user', text: words, streaming: false },
|
|
140
309
|
]);
|
|
141
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* The conversation without some of its rows: those the person removed (a
|
|
313
|
+
* turn taken away), or a message its application took back.
|
|
314
|
+
*/
|
|
315
|
+
export function withoutItems(state, ids) {
|
|
316
|
+
const drop = new Set(ids);
|
|
317
|
+
return { ...state, items: state.items.filter(item => !drop.has(item.id)) };
|
|
318
|
+
}
|
|
142
319
|
/**
|
|
143
320
|
* The turn ended without the stream saying so: the person stopped it
|
|
144
321
|
* (`stopped`), or its client went before the answer (`gone`). What streamed
|
|
@@ -148,7 +325,12 @@ export function withTurnEnded(state, end) {
|
|
|
148
325
|
const items = state.items.map(item => item.kind === 'message' && item.streaming
|
|
149
326
|
? { ...item, streaming: false }
|
|
150
327
|
: item.kind === 'tool' && item.status === 'running'
|
|
151
|
-
? {
|
|
328
|
+
? {
|
|
329
|
+
...item,
|
|
330
|
+
status: 'error',
|
|
331
|
+
awaiting: null,
|
|
332
|
+
error: RUN_END_WORDS[end],
|
|
333
|
+
}
|
|
152
334
|
: item.kind === 'step' && item.status === 'running'
|
|
153
335
|
? { ...item, status: 'complete' }
|
|
154
336
|
: item);
|
|
@@ -163,7 +345,7 @@ export function withError(state, words) {
|
|
|
163
345
|
const items = state.items.map(item => item.kind === 'message' && item.streaming
|
|
164
346
|
? { ...item, streaming: false }
|
|
165
347
|
: item.kind === 'tool' && item.status === 'running'
|
|
166
|
-
? { ...item, status: 'error' }
|
|
348
|
+
? { ...item, status: 'error', awaiting: null }
|
|
167
349
|
: item.kind === 'step' && item.status === 'running'
|
|
168
350
|
? { ...item, status: 'complete' }
|
|
169
351
|
: item);
|
|
@@ -172,8 +354,17 @@ export function withError(state, words) {
|
|
|
172
354
|
{ kind: 'error', id: rowId('error'), text: words },
|
|
173
355
|
]);
|
|
174
356
|
}
|
|
357
|
+
/** The string field an event must carry, or a refusal that names it. */
|
|
358
|
+
function required(event, field) {
|
|
359
|
+
const value = event[field];
|
|
360
|
+
if (typeof value !== 'string' || !value) {
|
|
361
|
+
throw new Error(`${event.type} carries no ${field}.`);
|
|
362
|
+
}
|
|
363
|
+
return value;
|
|
364
|
+
}
|
|
175
365
|
/** A conversation with one more event of its stream. */
|
|
176
366
|
export function reduceConversation(state, event) {
|
|
367
|
+
const { limits } = state;
|
|
177
368
|
switch (event.type) {
|
|
178
369
|
case 'RUN_STARTED':
|
|
179
370
|
return { ...state, running: true, asking: null, turnRows: 0 };
|
|
@@ -193,7 +384,7 @@ export function reduceConversation(state, event) {
|
|
|
193
384
|
const delta = text(event.delta);
|
|
194
385
|
const id = text(event.messageId);
|
|
195
386
|
const grown = changed(state, id, item => item.kind === 'message'
|
|
196
|
-
? { ...item, text: appended(item.text, delta,
|
|
387
|
+
? { ...item, text: appended(item.text, delta, limits.messageText) }
|
|
197
388
|
: item);
|
|
198
389
|
if (grown) {
|
|
199
390
|
return grown;
|
|
@@ -204,13 +395,37 @@ export function reduceConversation(state, event) {
|
|
|
204
395
|
kind: 'message',
|
|
205
396
|
id: id || rowId('message'),
|
|
206
397
|
role: event.role === 'user' ? 'user' : 'assistant',
|
|
207
|
-
text: cut(delta,
|
|
398
|
+
text: cut(delta, limits.messageText),
|
|
208
399
|
streaming: event.type === 'TEXT_MESSAGE_CONTENT',
|
|
209
400
|
},
|
|
210
401
|
], 1);
|
|
211
402
|
}
|
|
212
403
|
case 'TEXT_MESSAGE_END':
|
|
213
404
|
return (changed(state, text(event.messageId), item => item.kind === 'message' ? { ...item, streaming: false } : item) ?? state);
|
|
405
|
+
case 'MESSAGE_SAID': {
|
|
406
|
+
// A message told whole so far, by an adapter that accumulates it.
|
|
407
|
+
const id = required(event, 'messageId');
|
|
408
|
+
if (typeof event.text !== 'string') {
|
|
409
|
+
throw new Error(`MESSAGE_SAID of ${id} carries no text.`);
|
|
410
|
+
}
|
|
411
|
+
if (event.role !== 'user' && event.role !== 'assistant') {
|
|
412
|
+
throw new Error(`MESSAGE_SAID of ${id} is said by ${String(event.role)}: only the person or the assistant speaks here.`);
|
|
413
|
+
}
|
|
414
|
+
const role = event.role;
|
|
415
|
+
const words = cut(event.text, limits.messageText);
|
|
416
|
+
const streaming = role === 'assistant' && event.streaming !== false;
|
|
417
|
+
const at = state.items.findIndex(item => item.id === id);
|
|
418
|
+
if (at >= 0) {
|
|
419
|
+
const kept = state.items[at];
|
|
420
|
+
if (kept.kind !== 'message') {
|
|
421
|
+
throw new Error(`MESSAGE_SAID of ${id} names a ${kept.kind} row.`);
|
|
422
|
+
}
|
|
423
|
+
const items = state.items.slice();
|
|
424
|
+
items[at] = { ...kept, text: words, streaming };
|
|
425
|
+
return { ...state, items };
|
|
426
|
+
}
|
|
427
|
+
return withItems(state, [...state.items, { kind: 'message', id, role, text: words, streaming }], 1);
|
|
428
|
+
}
|
|
214
429
|
case 'TOOL_CALL_START': {
|
|
215
430
|
const id = text(event.toolCallId) || rowId('tool');
|
|
216
431
|
if (state.items.some(item => item.id === id)) {
|
|
@@ -218,37 +433,88 @@ export function reduceConversation(state, event) {
|
|
|
218
433
|
}
|
|
219
434
|
return withItems(state, [
|
|
220
435
|
...state.items,
|
|
221
|
-
|
|
222
|
-
kind: 'tool',
|
|
223
|
-
id,
|
|
224
|
-
name: text(event.toolCallName) || text(event.name),
|
|
225
|
-
args: '',
|
|
226
|
-
status: 'running',
|
|
227
|
-
result: '',
|
|
228
|
-
error: '',
|
|
229
|
-
display: null,
|
|
230
|
-
},
|
|
436
|
+
toolRow(id, text(event.toolCallName) || text(event.name)),
|
|
231
437
|
], 1);
|
|
232
438
|
}
|
|
233
439
|
case 'TOOL_CALL_ARGS':
|
|
234
440
|
return (changed(state, text(event.toolCallId), item => item.kind === 'tool'
|
|
235
441
|
? {
|
|
236
442
|
...item,
|
|
237
|
-
args: appended(item.args, text(event.delta),
|
|
443
|
+
args: appended(item.args, text(event.delta), limits.toolText),
|
|
238
444
|
}
|
|
239
445
|
: item) ?? state);
|
|
240
446
|
case 'TOOL_CALL_END':
|
|
241
|
-
|
|
447
|
+
// Its arguments are whole: read them, unless they were cut.
|
|
448
|
+
return (changed(state, text(event.toolCallId), item => {
|
|
449
|
+
if (item.kind !== 'tool' || item.args.endsWith(CUT_MARK)) {
|
|
450
|
+
return item;
|
|
451
|
+
}
|
|
452
|
+
if (!item.args) {
|
|
453
|
+
return { ...item, input: {} };
|
|
454
|
+
}
|
|
455
|
+
let parsed;
|
|
456
|
+
try {
|
|
457
|
+
parsed = JSON.parse(item.args);
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
parsed = undefined;
|
|
461
|
+
}
|
|
462
|
+
if (!isRecord(parsed)) {
|
|
463
|
+
return {
|
|
464
|
+
...item,
|
|
465
|
+
status: 'error',
|
|
466
|
+
awaiting: null,
|
|
467
|
+
error: 'Its arguments are not a JSON object.',
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return { ...item, input: parsed };
|
|
471
|
+
}) ?? state);
|
|
472
|
+
case 'TOOL_CALL_SAID': {
|
|
473
|
+
// A call's arguments told whole, by an adapter that parsed them.
|
|
474
|
+
const id = required(event, 'toolCallId');
|
|
475
|
+
const name = required(event, 'toolCallName');
|
|
476
|
+
if (!isRecord(event.args)) {
|
|
477
|
+
throw new Error(`TOOL_CALL_SAID of ${id} carries arguments that are not an object.`);
|
|
478
|
+
}
|
|
479
|
+
const args = event.args;
|
|
480
|
+
const at = state.items.findIndex(item => item.id === id);
|
|
481
|
+
if (at < 0) {
|
|
482
|
+
return withItems(state, [...state.items, withInput(toolRow(id, name), args, limits.toolText)], 1);
|
|
483
|
+
}
|
|
484
|
+
const kept = state.items[at];
|
|
485
|
+
if (kept.kind !== 'tool') {
|
|
486
|
+
throw new Error(`TOOL_CALL_SAID of ${id} names a ${kept.kind} row.`);
|
|
487
|
+
}
|
|
488
|
+
const items = state.items.slice();
|
|
489
|
+
items[at] = withInput(kept, { ...(kept.input ?? {}), ...args }, limits.toolText);
|
|
490
|
+
return { ...state, items };
|
|
491
|
+
}
|
|
242
492
|
case 'TOOL_CALL_RESULT': {
|
|
243
493
|
const result = toolResultOf(event.content);
|
|
244
494
|
return (changed(state, text(event.toolCallId), item => item.kind === 'tool'
|
|
245
|
-
?
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
495
|
+
? withResult(item, result, limits.toolText)
|
|
496
|
+
: item) ?? noted(state, event));
|
|
497
|
+
}
|
|
498
|
+
case 'TOOL_CALL_DONE': {
|
|
499
|
+
// A call's result as a value, by an adapter (or the page) that has it;
|
|
500
|
+
// `answered` when it is the person's answer.
|
|
501
|
+
const id = required(event, 'toolCallId');
|
|
502
|
+
if (typeof event.success !== 'boolean') {
|
|
503
|
+
throw new Error(`TOOL_CALL_DONE of ${id} does not say whether it succeeded.`);
|
|
504
|
+
}
|
|
505
|
+
if (event.error !== undefined && typeof event.error !== 'string') {
|
|
506
|
+
throw new Error(`TOOL_CALL_DONE of ${id} says its error in no words.`);
|
|
507
|
+
}
|
|
508
|
+
const result = {
|
|
509
|
+
result: event.result,
|
|
510
|
+
success: event.success,
|
|
511
|
+
...(event.error !== undefined ? { error: event.error } : {}),
|
|
512
|
+
};
|
|
513
|
+
return (changed(state, id, item => item.kind === 'tool'
|
|
514
|
+
? withResult(item, result, limits.toolText,
|
|
515
|
+
// The person's own answer (an approval decided, steps
|
|
516
|
+
// confirmed): nothing waits after it.
|
|
517
|
+
event.answered === true)
|
|
252
518
|
: item) ?? noted(state, event));
|
|
253
519
|
}
|
|
254
520
|
case 'STEP_STARTED': {
|
|
@@ -277,27 +543,19 @@ export function reduceConversation(state, event) {
|
|
|
277
543
|
return { ...state, items };
|
|
278
544
|
}
|
|
279
545
|
case 'MESSAGES_SNAPSHOT': {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
id: message.id,
|
|
284
|
-
role: message.role === 'user'
|
|
285
|
-
? 'user'
|
|
286
|
-
: 'assistant',
|
|
287
|
-
text: cut(typeof message.content === 'string' ? message.content : '', MESSAGE_TEXT_LIMIT),
|
|
288
|
-
streaming: false,
|
|
289
|
-
})),
|
|
546
|
+
const unread = [];
|
|
547
|
+
const rows = rowsOfSnapshot(event.messages, limits, unread);
|
|
548
|
+
const restored = withItems({ ...state, dropped: 0 }, rows,
|
|
290
549
|
// The conversation said again is what this turn says (a resume):
|
|
291
550
|
// never also said as a wordless answer.
|
|
292
551
|
1);
|
|
552
|
+
return unread.reduce((kept, id) => noted(kept, { type: 'TOOL_CALL_RESULT', name: `${id} (no call)` }), restored);
|
|
293
553
|
}
|
|
294
554
|
case 'CUSTOM': {
|
|
295
555
|
if (event.name === REACTOR_UI_NAME) {
|
|
296
556
|
return withError(state, UI_ON_RUN);
|
|
297
557
|
}
|
|
298
|
-
if (event.name === 'loop.session' &&
|
|
299
|
-
event.value &&
|
|
300
|
-
typeof event.value === 'object') {
|
|
558
|
+
if (event.name === 'loop.session' && isRecord(event.value)) {
|
|
301
559
|
const session = event.value;
|
|
302
560
|
return {
|
|
303
561
|
...state,
|
|
@@ -314,9 +572,7 @@ export function reduceConversation(state, event) {
|
|
|
314
572
|
}
|
|
315
573
|
return noted(state, event);
|
|
316
574
|
case 'RUN_FINISHED': {
|
|
317
|
-
const result = event.result
|
|
318
|
-
? event.result
|
|
319
|
-
: {};
|
|
575
|
+
const result = isRecord(event.result) ? event.result : {};
|
|
320
576
|
const asking = questionOf(result.asking);
|
|
321
577
|
const items = state.items.map(item => item.kind === 'message' && item.streaming
|
|
322
578
|
? { ...item, streaming: false }
|
package/lib/portable/fixtures.js
CHANGED
|
@@ -29,9 +29,15 @@ export function conversationFixture() {
|
|
|
29
29
|
id: 't-1',
|
|
30
30
|
name: 'lookup_order',
|
|
31
31
|
args: '{"order":7}',
|
|
32
|
+
input: { order: 7 },
|
|
32
33
|
status: 'complete',
|
|
34
|
+
awaiting: null,
|
|
33
35
|
result: 'Shipped on Monday.',
|
|
36
|
+
value: 'Shipped on Monday.',
|
|
34
37
|
error: '',
|
|
38
|
+
executionError: '',
|
|
39
|
+
codeError: null,
|
|
40
|
+
exitCode: null,
|
|
35
41
|
display: null,
|
|
36
42
|
},
|
|
37
43
|
{
|
|
@@ -39,9 +45,15 @@ export function conversationFixture() {
|
|
|
39
45
|
id: 't-2',
|
|
40
46
|
name: 'send_mail',
|
|
41
47
|
args: '{}',
|
|
48
|
+
input: {},
|
|
42
49
|
status: 'error',
|
|
50
|
+
awaiting: null,
|
|
43
51
|
result: '',
|
|
52
|
+
value: undefined,
|
|
44
53
|
error: 'Refused by its rule.',
|
|
54
|
+
executionError: '',
|
|
55
|
+
codeError: null,
|
|
56
|
+
exitCode: null,
|
|
45
57
|
display: null,
|
|
46
58
|
},
|
|
47
59
|
{
|
package/lib/portable/index.d.ts
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* @module portable
|
|
8
8
|
*/
|
|
9
9
|
export { messageOfApproval, normalizeApproval, whyAsked, type ApprovalArgs, type ApprovalRecord, type MessageAsked, } from './approvals';
|
|
10
|
+
export { actionOf, APPROVAL_CARD_WORDS, approvalCardOf, argumentsShown, type ApprovalCard, type ApprovalCardProps, type ApprovalCardSource, } from './approvalCard';
|
|
10
11
|
export { AgUiFrames, endsRun, eventsOfStream, NOT_STREAMING, REACTOR_UI_NAME, readRunStream, RUN_CUT_SHORT, toolResultOf, UI_ON_RUN, type AgUiEvent, type ToolResultSaid, } from './agui';
|
|
11
12
|
export { AG_UI_NOT_ALLOWED, AG_UI_NOT_ON_RUNTIME, agUiRefusal, refusalBody, refusalDetail, } from './agUiRefusal';
|
|
12
13
|
export { ASKED_FOR_TAG, askedOf, RUN_END_WORDS } from './records';
|
|
13
14
|
export { messagesOfThread, openThread, snapshotOfStream, THREAD_WORDS, } from './thread';
|
|
14
|
-
export { CUT_MARK, DIAGNOSTIC_LIMIT, emptyConversation, formFieldsOf, ITEM_LIMIT, MESSAGE_TEXT_LIMIT, questionOf, questionWords, reduceConversation, TOOL_TEXT_LIMIT, withError, withPrompt, withTurnEnded, type ConversationItem, type ConversationQuestion, type ConversationState, type TurnEnd, } from './conversation';
|
|
15
|
+
export { BROWSER_LIMITS, CUT_MARK, DEVICE_LIMITS, DIAGNOSTIC_LIMIT, emptyConversation, formFieldsOf, ITEM_LIMIT, MESSAGE_TEXT_LIMIT, questionOf, questionWords, reduceConversation, TOOL_TEXT_LIMIT, withError, withoutItems, withPrompt, withTurnEnded, type ConversationItem, type ConversationLimits, type ConversationQuestion, type ConversationState, type ToolAwaiting, type ToolCodeError, type TurnEnd, } from './conversation';
|
|
16
|
+
export { conversationEventsOf, sanitizeAssistantContent, snapshotOfMessages, type AdapterEvent, type SaidMessage, } from './adapterEvents';
|
|
15
17
|
export { PRESENCE_LINES, presenceOfConversation, presenceState, presenceToolOf, type PresenceState, type PresenceTool, } from './presence';
|
|
16
18
|
export { ANSWER_BELOW, ANSWER_COMPLETE, composerOf, droppedWords, ENDED_WORDS, messageLabel, OPENING_WORDS, presenceLine, showsQuestion, stepLine, TOOL_STATUS_WORDS, toolLabel, type ComposerProps, type ComposerState, type ConversationViewProps, type ErrorItem, type ErrorRowProps, type MessageItem, type MessageRowProps, type PresenceLineProps, type ProtocolDisplayProps, type QuestionCardProps, type StepItem, type StepRowProps, type ToolItem, type ToolRowProps, type TurnEndItem, type TurnEndRowProps, } from './views';
|
|
17
19
|
export { conversationFixture } from './fixtures';
|
|
@@ -20,5 +22,6 @@ export { isPapSignInTool, PAP_SIGN_IN_WORDS, readPapSignIn, safeSignInUrl, type
|
|
|
20
22
|
export { DIRECT_MAX, DIRECT_WORDS, directAnswerOf, directAuthor, directClient, openDirect, withDirectAnswer, type DirectAnswer, type DirectClient, type DirectClientOptions, type DirectLine, type DirectOpen, type DirectReply, type DirectTarget, } from './direct';
|
|
21
23
|
export { SIGN_IN_POLL_MS, SIGN_IN_STATUS_PATH, signInStandsOf, signInStatusClient, type SignInStands, } from './signInStatus';
|
|
22
24
|
export { emptyFields, fieldProblems, MAX_VALUE_LENGTH, PROTECTED_INPUT_WORDS, protectedInputClient, protectedRequestOf, ProtectedRequestGone, sentWords, submitBody, submitWords, ticketComplete, ticketOfPage, type ProtectedField, type ProtectedRequest, } from './protectedInput';
|
|
25
|
+
export { failureOf, otlpMetricsOf, RunMeter, type RunMeasure, type StreamFailure, } from './runMeasures';
|
|
23
26
|
export { agentRunClient, agentRuntimesUrlOf, appSessionsClient, RuntimeRefusal, type AgentRunClient, type AgentTurn, type AppSessionsClient, type RuntimeClientOptions, type SessionStart, type StreamOptions, } from './sessions';
|
|
24
27
|
export type { ChatMessage } from '../types/messages';
|
package/lib/portable/index.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* @module portable
|
|
12
12
|
*/
|
|
13
13
|
export { messageOfApproval, normalizeApproval, whyAsked, } from './approvals';
|
|
14
|
+
// An approval as every card says it (MOBILE M-016).
|
|
15
|
+
export { actionOf, APPROVAL_CARD_WORDS, approvalCardOf, argumentsShown, } from './approvalCard';
|
|
14
16
|
// A run's stream, read as every client reads it.
|
|
15
17
|
export { AgUiFrames, endsRun, eventsOfStream, NOT_STREAMING, REACTOR_UI_NAME, readRunStream, RUN_CUT_SHORT, toolResultOf, UI_ON_RUN, } from './agui';
|
|
16
18
|
export { AG_UI_NOT_ALLOWED, AG_UI_NOT_ON_RUNTIME, agUiRefusal, refusalBody, refusalDetail, } from './agUiRefusal';
|
|
@@ -18,7 +20,9 @@ export { AG_UI_NOT_ALLOWED, AG_UI_NOT_ON_RUNTIME, agUiRefusal, refusalBody, refu
|
|
|
18
20
|
export { ASKED_FOR_TAG, askedOf, RUN_END_WORDS } from './records';
|
|
19
21
|
export { messagesOfThread, openThread, snapshotOfStream, THREAD_WORDS, } from './thread';
|
|
20
22
|
// A conversation as its stream builds it, for a native client.
|
|
21
|
-
export { CUT_MARK, DIAGNOSTIC_LIMIT, emptyConversation, formFieldsOf, ITEM_LIMIT, MESSAGE_TEXT_LIMIT, questionOf, questionWords, reduceConversation, TOOL_TEXT_LIMIT, withError, withPrompt, withTurnEnded, } from './conversation';
|
|
23
|
+
export { BROWSER_LIMITS, CUT_MARK, DEVICE_LIMITS, DIAGNOSTIC_LIMIT, emptyConversation, formFieldsOf, ITEM_LIMIT, MESSAGE_TEXT_LIMIT, questionOf, questionWords, reduceConversation, TOOL_TEXT_LIMIT, withError, withoutItems, withPrompt, withTurnEnded, } from './conversation';
|
|
24
|
+
// What every protocol adapter says, as the conversation reads it (M-030).
|
|
25
|
+
export { conversationEventsOf, sanitizeAssistantContent, snapshotOfMessages, } from './adapterEvents';
|
|
22
26
|
// What an application is doing, in the words beside its face (LOOP T-08).
|
|
23
27
|
export { PRESENCE_LINES, presenceOfConversation, presenceState, presenceToolOf, } from './presence';
|
|
24
28
|
// One contract for every renderer of a conversation's components.
|
|
@@ -33,5 +37,7 @@ export { DIRECT_MAX, DIRECT_WORDS, directAnswerOf, directAuthor, directClient, o
|
|
|
33
37
|
export { SIGN_IN_POLL_MS, SIGN_IN_STATUS_PATH, signInStandsOf, signInStatusClient, } from './signInStatus';
|
|
34
38
|
// What a company asks the person to type in a mediated sign-in, sent with its one-use ticket (MOBILE M-039).
|
|
35
39
|
export { emptyFields, fieldProblems, MAX_VALUE_LENGTH, PROTECTED_INPUT_WORDS, protectedInputClient, protectedRequestOf, ProtectedRequestGone, sentWords, submitBody, submitWords, ticketComplete, ticketOfPage, } from './protectedInput';
|
|
40
|
+
// How a streamed turn went, measured without its words (MOBILE M-036).
|
|
41
|
+
export { failureOf, otlpMetricsOf, RunMeter, } from './runMeasures';
|
|
36
42
|
// The runtime's routes a native client drives.
|
|
37
43
|
export { agentRunClient, agentRuntimesUrlOf, appSessionsClient, RuntimeRefusal, } from './sessions';
|
package/lib/portable/presence.js
CHANGED
|
@@ -62,7 +62,10 @@ export function presenceOfConversation(state, paused = false) {
|
|
|
62
62
|
for (let index = state.items.length - 1; index >= 0; index -= 1) {
|
|
63
63
|
const item = state.items[index];
|
|
64
64
|
if (item.kind === 'tool') {
|
|
65
|
-
tool = {
|
|
65
|
+
tool = {
|
|
66
|
+
open: item.status === 'running',
|
|
67
|
+
pendingApproval: item.awaiting === 'approval',
|
|
68
|
+
};
|
|
66
69
|
break;
|
|
67
70
|
}
|
|
68
71
|
}
|