@adecore/agents 0.21.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/chat-core.d.ts +5 -1
- package/dist/chat/chat-core.js +16 -6
- package/dist/chat/chat-session.d.ts +13 -0
- package/dist/chat/chat-session.js +75 -5
- package/dist/chat/chat-store.d.ts +3 -2
- package/dist/chat/chat-store.js +42 -11
- package/dist/chat/readable-text.d.ts +2 -0
- package/dist/chat/readable-text.js +16 -0
- package/dist/chat/ui-queries.d.ts +12 -1
- package/dist/chat/ui-queries.js +107 -60
- package/dist/host/handlers.js +1 -1
- package/dist/tasks/task-coordinator.js +6 -2
- package/package.json +3 -3
- package/src/chat/chat-core.ts +26 -6
- package/src/chat/chat-session.ts +85 -5
- package/src/chat/chat-store.ts +53 -11
- package/src/chat/readable-text.ts +18 -0
- package/src/chat/ui-queries.ts +120 -64
- package/src/host/handlers.ts +1 -1
- package/src/tasks/task-coordinator.ts +6 -2
package/src/chat/ui-queries.ts
CHANGED
|
@@ -7,10 +7,9 @@ import type {
|
|
|
7
7
|
ChatInfo,
|
|
8
8
|
ChatUiChoicePayload,
|
|
9
9
|
ChatUiQueryPayload,
|
|
10
|
-
ChatUiQueryReading
|
|
11
|
-
ChatUiQueryState
|
|
10
|
+
ChatUiQueryReading
|
|
12
11
|
} from '@adecore/agent-contracts';
|
|
13
|
-
import { copyUiValue, UiBudget, type UiValue } from '@adecore/intelligent-ui';
|
|
12
|
+
import { copyUiValue, UI_HOST_LIMITS, UiBudget, uiMayReferenceHost, type UiValue } from '@adecore/intelligent-ui';
|
|
14
13
|
import { UiLinkResolutionSchema, uiLinkTargets, type UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
15
14
|
import { uiQueryArguments, uiQueryFallback } from '@adecore/intelligent-ui/query';
|
|
16
15
|
import type { UiBlock } from '@adecore/intelligent-ui/compiler';
|
|
@@ -36,17 +35,42 @@ interface CachedReading {
|
|
|
36
35
|
reading: ChatUiQueryReading;
|
|
37
36
|
}
|
|
38
37
|
|
|
38
|
+
/*
|
|
39
|
+
* A failure a client can say in its own words: `reason` is a code that stays the same across versions,
|
|
40
|
+
* the message is the fallback for a client that does not know the code.
|
|
41
|
+
*/
|
|
42
|
+
class UiQueryError extends ChatError<string> {
|
|
43
|
+
readonly reason: string;
|
|
44
|
+
|
|
45
|
+
constructor(code: string, reason: string, message: string) {
|
|
46
|
+
super(code, message);
|
|
47
|
+
this.reason = reason;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* What a host's `authorize`, `read` or `link` throws to say why with a code: `code` reaches the reading's
|
|
53
|
+
* `code` and should stay the same across versions, `reason` is the plain sentence a client shows without it.
|
|
54
|
+
*/
|
|
55
|
+
export class ChatUiRefusal extends UiQueryError {
|
|
56
|
+
constructor(code: string, reason: string) {
|
|
57
|
+
super('refused-query', code, reason);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
const MAX_QUERY_BYTES = 64 * 1024;
|
|
40
62
|
const MAX_SNAPSHOT_BYTES = 128 * 1024;
|
|
41
|
-
const MAX_QUERIES = 8;
|
|
42
|
-
const REFRESH_MS = 10_000;
|
|
43
63
|
const READ_TIMEOUT_MS = 8_000;
|
|
64
|
+
// A changed input reads at once, but a query never reads more often than this across its inputs.
|
|
65
|
+
const INPUT_READ_MS = 250;
|
|
44
66
|
|
|
45
67
|
export class ChatUiQueries {
|
|
46
68
|
readonly schemas: Record<string, z.ZodType>;
|
|
47
|
-
private readonly captures = new Map<string, Promise<
|
|
69
|
+
private readonly captures = new Map<string, Promise<unknown>>();
|
|
48
70
|
private readonly freezing = new Set<string>();
|
|
49
71
|
private readonly cache = new Map<string, CachedReading>();
|
|
72
|
+
// The read id of the newest reading per cache key, which a remount or a second client is answered with.
|
|
73
|
+
private readonly latest = new Map<string, string>();
|
|
50
74
|
private readonly reading = new Map<string, Promise<ChatUiQueryReading>>();
|
|
51
75
|
private readonly attempts = new Map<string, number>();
|
|
52
76
|
private readonly active = new Map<string, number>();
|
|
@@ -60,14 +84,16 @@ export class ChatUiQueries {
|
|
|
60
84
|
}
|
|
61
85
|
|
|
62
86
|
observe(session: ChatSession, item: ChatAssistantItem, final: boolean): void {
|
|
63
|
-
|
|
87
|
+
const key = `${session.id}:${item.id}`;
|
|
88
|
+
// Once a capture started, every delta of a streaming reply would scan its whole text again for nothing.
|
|
89
|
+
const captured = this.captures.has(key) || item.uiQueries?.authorChatId === session.id;
|
|
90
|
+
if (!captured && !uiMayReferenceHost(item.text) && !item.ui?.some((block) => Object.keys(block.queries).length)) {
|
|
64
91
|
return;
|
|
65
92
|
}
|
|
66
93
|
const capture = this.access(session, item);
|
|
67
94
|
if (!final || !item.ui?.length) {
|
|
68
95
|
return;
|
|
69
96
|
}
|
|
70
|
-
const key = `${session.id}:${item.id}`;
|
|
71
97
|
if (this.freezing.has(key)) {
|
|
72
98
|
return;
|
|
73
99
|
}
|
|
@@ -84,7 +110,7 @@ export class ChatUiQueries {
|
|
|
84
110
|
}
|
|
85
111
|
const readings: Record<string, ChatUiQueryReading> = Object.create(null);
|
|
86
112
|
const values: Record<string, unknown> = Object.create(null);
|
|
87
|
-
for (const name of Object.keys(block.queries).slice(0,
|
|
113
|
+
for (const name of Object.keys(block.queries).slice(0, UI_HOST_LIMITS.queries)) {
|
|
88
114
|
const reading = await this.query(session, {
|
|
89
115
|
chatId: session.id,
|
|
90
116
|
itemId: item.id,
|
|
@@ -101,11 +127,11 @@ export class ChatUiQueries {
|
|
|
101
127
|
if (this.host.link) {
|
|
102
128
|
const targets = uiLinkTargets(block, {}, values);
|
|
103
129
|
const access = await this.access(session, current);
|
|
104
|
-
for (const [id, target] of Object.entries(targets).slice(0,
|
|
130
|
+
for (const [id, target] of Object.entries(targets).slice(0, UI_HOST_LIMITS.links)) {
|
|
105
131
|
try {
|
|
106
|
-
links[id] = this.linkReading(await this.host.link(session.info, access
|
|
132
|
+
links[id] = this.linkReading(await this.host.link(session.info, access, target));
|
|
107
133
|
} catch (error) {
|
|
108
|
-
links[id] = { state: 'plain',
|
|
134
|
+
links[id] = { state: 'plain', ...this.failure(error) };
|
|
109
135
|
}
|
|
110
136
|
}
|
|
111
137
|
}
|
|
@@ -131,10 +157,12 @@ export class ChatUiQueries {
|
|
|
131
157
|
frozen.readings = Object.fromEntries(
|
|
132
158
|
Object.keys(readings).map((name) => [
|
|
133
159
|
name,
|
|
134
|
-
{ state: 'failed', readAt: Date.now(), reason: 'The frozen query values are too large.' }
|
|
160
|
+
{ state: 'failed', readAt: Date.now(), code: 'snapshot-too-large', reason: 'The frozen query values are too large.' }
|
|
135
161
|
])
|
|
136
162
|
);
|
|
137
|
-
frozen.links = Object.fromEntries(
|
|
163
|
+
frozen.links = Object.fromEntries(
|
|
164
|
+
Object.keys(links).map((id) => [id, { state: 'plain', code: 'link-unchecked', reason: 'This link must be checked again.' }])
|
|
165
|
+
);
|
|
138
166
|
frozen.fallback = `${block.fallback.slice(0, 2048)}\nSources: ${names}\nThe frozen query values are too large.`;
|
|
139
167
|
}
|
|
140
168
|
if (Buffer.byteLength(JSON.stringify(blocks)) > MAX_SNAPSHOT_BYTES) {
|
|
@@ -147,17 +175,19 @@ export class ChatUiQueries {
|
|
|
147
175
|
.finally(() => this.freezing.delete(key));
|
|
148
176
|
}
|
|
149
177
|
|
|
150
|
-
|
|
178
|
+
/* The writer's access for a reply, captured once; null where it was denied or is unavailable. */
|
|
179
|
+
private access(session: ChatSession, item: ChatAssistantItem): Promise<unknown> {
|
|
151
180
|
const key = `${session.id}:${item.id}`;
|
|
152
181
|
const existing = this.captures.get(key);
|
|
153
182
|
if (existing) {
|
|
154
183
|
return existing;
|
|
155
184
|
}
|
|
156
185
|
if (item.uiQueries?.authorChatId === session.id) {
|
|
157
|
-
return Promise.resolve(item.
|
|
186
|
+
return Promise.resolve(session.uiAccess(item.id) ?? null);
|
|
158
187
|
}
|
|
159
188
|
// A pending capture is persisted as denied, so a restart cannot widen the writer's rights.
|
|
160
|
-
session.
|
|
189
|
+
session.setUiAccess(item.id, null);
|
|
190
|
+
session.updateUiQueries(item.id, { authorChatId: session.id, blocks: {} });
|
|
161
191
|
const captured = this.host
|
|
162
192
|
.capture(session.info)
|
|
163
193
|
.then((access) => {
|
|
@@ -165,11 +195,10 @@ export class ChatUiQueries {
|
|
|
165
195
|
if (Buffer.byteLength(JSON.stringify(captured)) > 32 * 1024) {
|
|
166
196
|
throw new Error('The UI access snapshot is too large.');
|
|
167
197
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return state;
|
|
198
|
+
session.setUiAccess(item.id, captured);
|
|
199
|
+
return captured as unknown;
|
|
171
200
|
})
|
|
172
|
-
.catch(() =>
|
|
201
|
+
.catch(() => null)
|
|
173
202
|
.finally(() => this.captures.delete(key));
|
|
174
203
|
this.captures.set(key, captured);
|
|
175
204
|
return captured;
|
|
@@ -183,19 +212,19 @@ export class ChatUiQueries {
|
|
|
183
212
|
const block =
|
|
184
213
|
item?.kind === 'assistant' && !item.streaming && !item.parentToolUseId ? item.ui?.find((entry) => entry.id === payload.blockId) : undefined;
|
|
185
214
|
if (item?.kind !== 'assistant' || !block?.complete || block.revision !== payload.revision) {
|
|
186
|
-
throw new
|
|
215
|
+
throw new UiQueryError('stale-ui-block', 'block-stale', 'This completed UI block is no longer available.');
|
|
187
216
|
}
|
|
188
217
|
return { item, block };
|
|
189
218
|
}
|
|
190
219
|
|
|
191
220
|
private stored(session: ChatSession, payload: ChatUiQueryPayload): { item: ChatAssistantItem; block: UiBlock; source: ChatUiSource } {
|
|
192
221
|
const { item, block } = this.storedBlock(session, payload);
|
|
193
|
-
if (!Object.hasOwn(block.queries, payload.query) || Object.keys(block.queries).length >
|
|
194
|
-
throw new
|
|
222
|
+
if (!Object.hasOwn(block.queries, payload.query) || Object.keys(block.queries).length > UI_HOST_LIMITS.queries) {
|
|
223
|
+
throw new UiQueryError('refused-query', 'query-undeclared', 'This block does not declare an allowed query.');
|
|
195
224
|
}
|
|
196
225
|
const name = block.queries[payload.query].source;
|
|
197
226
|
if (!Object.hasOwn(this.host.sources, name)) {
|
|
198
|
-
throw new
|
|
227
|
+
throw new UiQueryError('refused-query', 'source-unregistered', 'This query source is not registered.');
|
|
199
228
|
}
|
|
200
229
|
return { item, block, source: this.host.sources[name] };
|
|
201
230
|
}
|
|
@@ -210,18 +239,17 @@ export class ChatUiQueries {
|
|
|
210
239
|
({ source, block, item } = this.stored(session, payload));
|
|
211
240
|
args = uiQueryArguments(block, payload.query, payload.values);
|
|
212
241
|
args = source.args.parse(args) as Record<string, UiValue>;
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
throw new
|
|
242
|
+
authorAccess = await this.access(session, item);
|
|
243
|
+
if (authorAccess === null) {
|
|
244
|
+
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
216
245
|
}
|
|
217
|
-
authorAccess = access.access;
|
|
218
246
|
await source.authorize(session.info, authorAccess, args);
|
|
219
247
|
} catch (error) {
|
|
220
|
-
return { state: 'refused', readAt: Date.now(),
|
|
248
|
+
return { state: 'refused', readAt: Date.now(), ...this.failure(error) };
|
|
221
249
|
}
|
|
222
250
|
const key = JSON.stringify([session.id, item.id, block.id, block.revision, payload.query, args]);
|
|
223
|
-
const cached =
|
|
224
|
-
if (cached && Date.now() - cached.reading.readAt <
|
|
251
|
+
const cached = this.cache.get(this.latest.get(key) ?? '');
|
|
252
|
+
if (cached && Date.now() - cached.reading.readAt < UI_HOST_LIMITS.refreshMilliseconds) {
|
|
225
253
|
return cached.reading;
|
|
226
254
|
}
|
|
227
255
|
const pending = this.reading.get(key);
|
|
@@ -229,14 +257,19 @@ export class ChatUiQueries {
|
|
|
229
257
|
return pending;
|
|
230
258
|
}
|
|
231
259
|
const identity = JSON.stringify([session.id, item.id, block.id, block.revision, payload.query]);
|
|
232
|
-
|
|
233
|
-
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
if (
|
|
262
|
+
now - (this.attempts.get(key) ?? -Infinity) < UI_HOST_LIMITS.refreshMilliseconds ||
|
|
263
|
+
now - (this.attempts.get(identity) ?? -Infinity) < INPUT_READ_MS
|
|
264
|
+
) {
|
|
265
|
+
return { state: 'failed', readAt: now, code: 'refresh-limit', reason: 'This query may refresh once every ten seconds.' };
|
|
234
266
|
}
|
|
235
267
|
if ((this.active.get(session.id) ?? 0) >= 2 || this.totalActive >= 16) {
|
|
236
|
-
return { state: 'failed', readAt:
|
|
268
|
+
return { state: 'failed', readAt: now, code: 'busy', reason: 'Too many UI queries are being read.' };
|
|
237
269
|
}
|
|
238
|
-
this.attempts.set(
|
|
239
|
-
|
|
270
|
+
this.attempts.set(key, now);
|
|
271
|
+
this.attempts.set(identity, now);
|
|
272
|
+
while (this.attempts.size > 256) {
|
|
240
273
|
this.attempts.delete(this.attempts.keys().next().value!);
|
|
241
274
|
}
|
|
242
275
|
this.active.set(session.id, (this.active.get(session.id) ?? 0) + 1);
|
|
@@ -247,21 +280,26 @@ export class ChatUiQueries {
|
|
|
247
280
|
timeout.unref?.();
|
|
248
281
|
const read = Promise.race([
|
|
249
282
|
work,
|
|
250
|
-
new Promise<never>((_, reject) =>
|
|
283
|
+
new Promise<never>((_, reject) =>
|
|
284
|
+
controller.signal.addEventListener('abort', () => reject(new UiQueryError('refused-query', 'timed-out', 'The UI query timed out.')), {
|
|
285
|
+
once: true
|
|
286
|
+
})
|
|
287
|
+
)
|
|
251
288
|
])
|
|
252
289
|
.then((result): ChatUiQueryReading => {
|
|
253
290
|
const value = copyUiValue(source.result.parse(result), new UiBudget());
|
|
254
291
|
if (Buffer.byteLength(JSON.stringify(value)) > MAX_QUERY_BYTES) {
|
|
255
|
-
throw new
|
|
292
|
+
throw new UiQueryError('refused-query', 'result-too-large', 'The UI query result is too large.');
|
|
256
293
|
}
|
|
257
294
|
const reading: ChatUiQueryReading = { state: 'fresh', value, readId: randomUUID(), readAt: Date.now() };
|
|
258
295
|
this.cache.set(reading.readId!, { key, chatId: session.id, reading });
|
|
296
|
+
this.latest.set(key, reading.readId!);
|
|
259
297
|
while (this.cache.size > 128) {
|
|
260
|
-
this.
|
|
298
|
+
this.evict(this.cache.keys().next().value!);
|
|
261
299
|
}
|
|
262
300
|
return reading;
|
|
263
301
|
})
|
|
264
|
-
.catch((error): ChatUiQueryReading => ({ state: 'failed', readAt: Date.now(),
|
|
302
|
+
.catch((error): ChatUiQueryReading => ({ state: 'failed', readAt: Date.now(), ...this.failure(error) }))
|
|
265
303
|
.finally(() => {
|
|
266
304
|
clearTimeout(timeout);
|
|
267
305
|
this.reading.delete(key);
|
|
@@ -282,32 +320,41 @@ export class ChatUiQueries {
|
|
|
282
320
|
return read;
|
|
283
321
|
}
|
|
284
322
|
|
|
323
|
+
/* The issued value of every query the block declares, each checked against the read id the person saw. */
|
|
285
324
|
async choiceValues(session: ChatSession, payload: ChatUiChoicePayload | ChatUiLinkPayload): Promise<Record<string, unknown>> {
|
|
286
325
|
const item = session.thread.get(payload.itemId);
|
|
287
326
|
const block = item?.kind === 'assistant' ? item.ui?.find((entry) => entry.id === payload.blockId) : undefined;
|
|
288
327
|
if (!block || item?.kind !== 'assistant' || ('choiceId' in payload && item.uiAnswers?.[block.id]?.revision === payload.revision)) {
|
|
289
328
|
return {};
|
|
290
329
|
}
|
|
330
|
+
const names = Object.keys(block.queries);
|
|
291
331
|
const values: Record<string, unknown> = Object.create(null);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
332
|
+
if (names.length === 0) {
|
|
333
|
+
return values;
|
|
334
|
+
}
|
|
335
|
+
const access = await this.access(session, item);
|
|
336
|
+
if (access === null) {
|
|
337
|
+
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
338
|
+
}
|
|
339
|
+
for (const name of names) {
|
|
340
|
+
const { source } = this.stored(session, { ...payload, query: name });
|
|
295
341
|
const args = source.args.parse(uiQueryArguments(block, name, payload.values)) as Record<string, UiValue>;
|
|
296
|
-
|
|
297
|
-
|
|
342
|
+
await source.authorize(session.info, access, args);
|
|
343
|
+
const readId = payload.reads !== undefined && Object.hasOwn(payload.reads, name) ? payload.reads[name] : undefined;
|
|
298
344
|
const key = JSON.stringify([session.id, item.id, block.id, block.revision, name, args]);
|
|
299
|
-
const
|
|
300
|
-
const cached = this.cache.get(readId);
|
|
345
|
+
const cached = readId === undefined ? undefined : this.cache.get(readId);
|
|
301
346
|
const frozen = item.uiQueries?.blocks[block.id];
|
|
302
347
|
const snapshot = frozen?.revision === block.revision ? frozen?.readings[name] : undefined;
|
|
303
348
|
const reading =
|
|
304
|
-
|
|
305
|
-
?
|
|
306
|
-
:
|
|
307
|
-
?
|
|
308
|
-
:
|
|
349
|
+
readId === undefined
|
|
350
|
+
? undefined
|
|
351
|
+
: cached?.key === key
|
|
352
|
+
? cached.reading
|
|
353
|
+
: snapshot?.readId === readId && JSON.stringify(args) === JSON.stringify(source.args.parse(uiQueryArguments(block, name)))
|
|
354
|
+
? snapshot
|
|
355
|
+
: undefined;
|
|
309
356
|
if (!reading || reading.state !== 'fresh') {
|
|
310
|
-
throw new
|
|
357
|
+
throw new UiQueryError('stale-ui-query', 'stale-read', 'Refresh this query before sending its choice.');
|
|
311
358
|
}
|
|
312
359
|
values[name] = reading.value;
|
|
313
360
|
}
|
|
@@ -317,35 +364,43 @@ export class ChatUiQueries {
|
|
|
317
364
|
async link(session: ChatSession, payload: ChatUiLinkPayload): Promise<ChatUiLinkReading> {
|
|
318
365
|
try {
|
|
319
366
|
if (!this.host.link) {
|
|
320
|
-
throw new
|
|
367
|
+
throw new UiQueryError('refused-query', 'links-unsupported', 'This host does not provide UI links.');
|
|
321
368
|
}
|
|
322
369
|
const { item, block } = this.storedBlock(session, payload);
|
|
323
370
|
if (!item.uiQueries) {
|
|
324
|
-
throw new
|
|
371
|
+
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
325
372
|
}
|
|
326
373
|
const access = await this.access(session, item);
|
|
327
|
-
if (access
|
|
328
|
-
throw new
|
|
374
|
+
if (access === null) {
|
|
375
|
+
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
329
376
|
}
|
|
330
377
|
const queries = await this.choiceValues(session, payload);
|
|
331
378
|
const target = uiLinkTargets(block, payload.values, queries)[payload.nodeId];
|
|
332
379
|
if (!target) {
|
|
333
|
-
throw new
|
|
380
|
+
throw new UiQueryError('refused-query', 'link-unsupported', 'This visible node is not a supported link.');
|
|
334
381
|
}
|
|
335
|
-
return this.linkReading(await this.host.link(session.info, access
|
|
382
|
+
return this.linkReading(await this.host.link(session.info, access, target));
|
|
336
383
|
} catch (error) {
|
|
337
|
-
return { state: 'plain',
|
|
384
|
+
return { state: 'plain', ...this.failure(error) };
|
|
338
385
|
}
|
|
339
386
|
}
|
|
340
387
|
|
|
341
388
|
forget(chatId: string): void {
|
|
342
389
|
for (const [id, cached] of this.cache) {
|
|
343
390
|
if (cached.chatId === chatId) {
|
|
344
|
-
this.
|
|
391
|
+
this.evict(id);
|
|
345
392
|
}
|
|
346
393
|
}
|
|
347
394
|
}
|
|
348
395
|
|
|
396
|
+
private evict(readId: string): void {
|
|
397
|
+
const cached = this.cache.get(readId);
|
|
398
|
+
this.cache.delete(readId);
|
|
399
|
+
if (cached && this.latest.get(cached.key) === readId) {
|
|
400
|
+
this.latest.delete(cached.key);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
349
404
|
private linkReading(value: unknown): ChatUiLinkReading {
|
|
350
405
|
const reading = UiLinkResolutionSchema.parse(copyUiValue(value, new UiBudget()));
|
|
351
406
|
if (reading.state === 'chip' && !reading.target) {
|
|
@@ -354,7 +409,8 @@ export class ChatUiQueries {
|
|
|
354
409
|
return reading;
|
|
355
410
|
}
|
|
356
411
|
|
|
357
|
-
private
|
|
358
|
-
|
|
412
|
+
private failure(error: unknown): { reason: string; code?: string } {
|
|
413
|
+
const reason = (error instanceof Error ? error.message : 'This query could not be read.').slice(0, 240);
|
|
414
|
+
return error instanceof UiQueryError ? { reason, code: error.reason } : { reason };
|
|
359
415
|
}
|
|
360
416
|
}
|
package/src/host/handlers.ts
CHANGED
|
@@ -66,7 +66,7 @@ export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T
|
|
|
66
66
|
{ delivery: payload.delivery, mentions: payload.mentions, skills: payload.skills, chats: payload.chats },
|
|
67
67
|
payload.attachments
|
|
68
68
|
),
|
|
69
|
-
'chat.uiChoice': (payload) => chats.choose(payload),
|
|
69
|
+
'chat.uiChoice': (payload, clientId) => chats.choose(payload, clientId),
|
|
70
70
|
'ui.link': (payload, clientId) => chats.linkUi(payload, clientId),
|
|
71
71
|
'ui.query': (payload, clientId) => chats.queryUi(payload, clientId),
|
|
72
72
|
'chat.unqueue': (payload) => ({ message: chats.unqueue(payload.chatId, payload.messageId) }),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChatBackgroundTask, ChatItem, ChatTurnItem, Task, TaskResult } from '@adecore/agent-contracts';
|
|
2
2
|
import { abortedByMachine, clipText } from '@adecore/agent-contracts';
|
|
3
3
|
import { BACKGROUND_COMMAND_LIMIT_MS, commandLabel, isBackgroundWork, runningInBackground, runsInBackground } from '../chat/background-work.ts';
|
|
4
|
+
import { readableAssistantText } from '../chat/readable-text.ts';
|
|
4
5
|
import { errorText } from '../error-text.ts';
|
|
5
6
|
import type { AgentEvent } from '../events.ts';
|
|
6
7
|
import type { TaskStore } from './task-store.ts';
|
|
@@ -40,8 +41,11 @@ export interface TaskCoordinatorDeps {
|
|
|
40
41
|
function answerOf(items: readonly ChatItem[], turnId: string): string | null {
|
|
41
42
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
42
43
|
const item = items[i]!;
|
|
43
|
-
if (item.kind === 'assistant' && item.turnId === turnId && !item.parentToolUseId
|
|
44
|
-
|
|
44
|
+
if (item.kind === 'assistant' && item.turnId === turnId && !item.parentToolUseId) {
|
|
45
|
+
const text = readableAssistantText(item).trim();
|
|
46
|
+
if (text !== '') {
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
return null;
|