@adecore/agents 0.20.1 → 0.21.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/dist/chat/attachment-store.d.ts +2 -0
- package/dist/chat/attachment-store.js +38 -2
- package/dist/chat/backend.d.ts +12 -1
- package/dist/chat/chat-core.d.ts +53 -1
- package/dist/chat/chat-core.js +52 -9
- package/dist/chat/chat-session.d.ts +22 -2
- package/dist/chat/chat-session.js +190 -10
- package/dist/chat/codex-backend.d.ts +2 -0
- package/dist/chat/codex-backend.js +51 -1
- package/dist/chat/codex-protocol.js +30 -3
- package/dist/chat/delta-coalescer.js +1 -1
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/fake-codex.js +33 -0
- package/dist/chat/generated-image.d.ts +6 -0
- package/dist/chat/generated-image.js +74 -0
- package/dist/chat/projector.js +9 -2
- package/dist/chat/thread.js +6 -0
- package/dist/chat/ui-queries.d.ts +38 -0
- package/dist/chat/ui-queries.js +311 -0
- package/dist/host/handlers.js +3 -0
- package/package.json +5 -4
- package/src/chat/attachment-store.ts +38 -2
- package/src/chat/backend.ts +14 -2
- package/src/chat/chat-core.ts +59 -8
- package/src/chat/chat-session.ts +223 -11
- package/src/chat/codex-backend.ts +50 -1
- package/src/chat/codex-protocol.ts +31 -3
- package/src/chat/delta-coalescer.ts +1 -1
- package/src/chat/errors.ts +2 -0
- package/src/chat/fake-codex.ts +34 -0
- package/src/chat/generated-image.ts +88 -0
- package/src/chat/projector.ts +9 -2
- package/src/chat/thread.ts +6 -0
- package/src/chat/ui-queries.ts +360 -0
- package/src/host/handlers.ts +3 -0
package/src/chat/projector.ts
CHANGED
|
@@ -187,7 +187,7 @@ export class ThreadProjector {
|
|
|
187
187
|
break;
|
|
188
188
|
case 'text.delta':
|
|
189
189
|
this.closeThinking(events);
|
|
190
|
-
this.appendText(this.itemId(generation, event.ref), event.text, events);
|
|
190
|
+
this.appendText(this.itemId(generation, event.ref), event.text, events, event.phase);
|
|
191
191
|
break;
|
|
192
192
|
case 'text.done': {
|
|
193
193
|
if (event.parentRef) {
|
|
@@ -206,6 +206,12 @@ export class ThreadProjector {
|
|
|
206
206
|
createdAt: existing?.createdAt ?? this.now(),
|
|
207
207
|
turnId: existing?.turnId ?? info.activeTurnId,
|
|
208
208
|
text: event.text,
|
|
209
|
+
...(existing?.kind === 'assistant' ? { uiAnswers: existing.uiAnswers, uiQueries: existing.uiQueries } : {}),
|
|
210
|
+
...(event.phase === undefined
|
|
211
|
+
? existing?.kind === 'assistant' && existing.phase !== undefined
|
|
212
|
+
? { phase: existing.phase }
|
|
213
|
+
: {}
|
|
214
|
+
: { phase: event.phase }),
|
|
209
215
|
streaming: false
|
|
210
216
|
})
|
|
211
217
|
);
|
|
@@ -507,7 +513,7 @@ export class ThreadProjector {
|
|
|
507
513
|
}
|
|
508
514
|
}
|
|
509
515
|
|
|
510
|
-
private appendText(id: string, text: string, events: ChatEvent[]): void {
|
|
516
|
+
private appendText(id: string, text: string, events: ChatEvent[], phase?: string): void {
|
|
511
517
|
const existing = this.thread.get(id);
|
|
512
518
|
if (!existing) {
|
|
513
519
|
events.push(
|
|
@@ -517,6 +523,7 @@ export class ThreadProjector {
|
|
|
517
523
|
createdAt: this.now(),
|
|
518
524
|
turnId: this.thread.info.activeTurnId,
|
|
519
525
|
text,
|
|
526
|
+
...(phase === undefined ? {} : { phase }),
|
|
520
527
|
streaming: true
|
|
521
528
|
})
|
|
522
529
|
);
|
package/src/chat/thread.ts
CHANGED
|
@@ -93,6 +93,12 @@ export class ChatThread {
|
|
|
93
93
|
break;
|
|
94
94
|
case 'delta':
|
|
95
95
|
this.appendText(event.itemId, event.text);
|
|
96
|
+
if (event.uiQueries) {
|
|
97
|
+
const item = this.items.get(event.itemId);
|
|
98
|
+
if (item?.kind === 'assistant') {
|
|
99
|
+
this.items.set(item.id, { ...item, uiQueries: event.uiQueries });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
96
102
|
break;
|
|
97
103
|
case 'info':
|
|
98
104
|
this.info = event.info;
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import type {
|
|
4
|
+
ChatUiLinkPayload,
|
|
5
|
+
ChatUiLinkReading,
|
|
6
|
+
ChatAssistantItem,
|
|
7
|
+
ChatInfo,
|
|
8
|
+
ChatUiChoicePayload,
|
|
9
|
+
ChatUiQueryPayload,
|
|
10
|
+
ChatUiQueryReading,
|
|
11
|
+
ChatUiQueryState
|
|
12
|
+
} from '@adecore/agent-contracts';
|
|
13
|
+
import { copyUiValue, UiBudget, type UiValue } from '@adecore/intelligent-ui';
|
|
14
|
+
import { UiLinkResolutionSchema, uiLinkTargets, type UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
15
|
+
import { uiQueryArguments, uiQueryFallback } from '@adecore/intelligent-ui/query';
|
|
16
|
+
import type { UiBlock } from '@adecore/intelligent-ui/compiler';
|
|
17
|
+
import type { ChatSession } from './chat-session.ts';
|
|
18
|
+
import { ChatError } from './errors.ts';
|
|
19
|
+
|
|
20
|
+
export interface ChatUiSource {
|
|
21
|
+
args: z.ZodType;
|
|
22
|
+
result: z.ZodType;
|
|
23
|
+
authorize(info: ChatInfo, access: unknown, args: Record<string, UiValue>): Promise<void>;
|
|
24
|
+
read(info: ChatInfo, args: Record<string, UiValue>, signal: AbortSignal, access: unknown): Promise<unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ChatUiHost {
|
|
28
|
+
capture(info: ChatInfo): Promise<unknown>;
|
|
29
|
+
sources: Readonly<Record<string, ChatUiSource>>;
|
|
30
|
+
link?(info: ChatInfo, access: unknown, target: UiLinkTarget): Promise<ChatUiLinkReading>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface CachedReading {
|
|
34
|
+
key: string;
|
|
35
|
+
chatId: string;
|
|
36
|
+
reading: ChatUiQueryReading;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MAX_QUERY_BYTES = 64 * 1024;
|
|
40
|
+
const MAX_SNAPSHOT_BYTES = 128 * 1024;
|
|
41
|
+
const MAX_QUERIES = 8;
|
|
42
|
+
const REFRESH_MS = 10_000;
|
|
43
|
+
const READ_TIMEOUT_MS = 8_000;
|
|
44
|
+
|
|
45
|
+
export class ChatUiQueries {
|
|
46
|
+
readonly schemas: Record<string, z.ZodType>;
|
|
47
|
+
private readonly captures = new Map<string, Promise<ChatUiQueryState>>();
|
|
48
|
+
private readonly freezing = new Set<string>();
|
|
49
|
+
private readonly cache = new Map<string, CachedReading>();
|
|
50
|
+
private readonly reading = new Map<string, Promise<ChatUiQueryReading>>();
|
|
51
|
+
private readonly attempts = new Map<string, number>();
|
|
52
|
+
private readonly active = new Map<string, number>();
|
|
53
|
+
private totalActive = 0;
|
|
54
|
+
|
|
55
|
+
private readonly host: ChatUiHost;
|
|
56
|
+
|
|
57
|
+
constructor(host: ChatUiHost) {
|
|
58
|
+
this.host = host;
|
|
59
|
+
this.schemas = Object.fromEntries(Object.entries(host.sources).map(([name, source]) => [name, source.args]));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
observe(session: ChatSession, item: ChatAssistantItem, final: boolean): void {
|
|
63
|
+
if (!item.text.includes('@Query') && !/<(?:File|Diff|Commit|Node)\b/.test(item.text) && !item.ui?.some((block) => Object.keys(block.queries).length)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const capture = this.access(session, item);
|
|
67
|
+
if (!final || !item.ui?.length) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const key = `${session.id}:${item.id}`;
|
|
71
|
+
if (this.freezing.has(key)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.freezing.add(key);
|
|
75
|
+
void capture
|
|
76
|
+
.then(async () => {
|
|
77
|
+
for (const block of (item.ui ?? []).slice(0, 16)) {
|
|
78
|
+
const current = session.thread.get(item.id);
|
|
79
|
+
if (current?.kind !== 'assistant' || current.ui?.find((entry) => entry.id === block.id)?.revision !== block.revision) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (current.uiQueries?.blocks[block.id]?.revision === block.revision) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const readings: Record<string, ChatUiQueryReading> = Object.create(null);
|
|
86
|
+
const values: Record<string, unknown> = Object.create(null);
|
|
87
|
+
for (const name of Object.keys(block.queries).slice(0, MAX_QUERIES)) {
|
|
88
|
+
const reading = await this.query(session, {
|
|
89
|
+
chatId: session.id,
|
|
90
|
+
itemId: item.id,
|
|
91
|
+
blockId: block.id,
|
|
92
|
+
revision: block.revision!,
|
|
93
|
+
query: name
|
|
94
|
+
});
|
|
95
|
+
readings[name] = reading;
|
|
96
|
+
if (reading.state === 'fresh') {
|
|
97
|
+
values[name] = reading.value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const links: Record<string, ChatUiLinkReading> = Object.create(null);
|
|
101
|
+
if (this.host.link) {
|
|
102
|
+
const targets = uiLinkTargets(block, {}, values);
|
|
103
|
+
const access = await this.access(session, current);
|
|
104
|
+
for (const [id, target] of Object.entries(targets).slice(0, 64)) {
|
|
105
|
+
try {
|
|
106
|
+
links[id] = this.linkReading(await this.host.link(session.info, access.access, target));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
links[id] = { state: 'plain', reason: this.reason(error) };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const latest = session.thread.get(item.id);
|
|
113
|
+
if (latest?.kind !== 'assistant' || !latest.uiQueries || latest.ui?.find((entry) => entry.id === block.id)?.revision !== block.revision) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const names = Object.entries(block.queries)
|
|
117
|
+
.map(([name, query]) => `${name}: ${query.source}`)
|
|
118
|
+
.join(', ');
|
|
119
|
+
const failures = Object.entries(readings)
|
|
120
|
+
.filter(([, reading]) => reading.state !== 'fresh')
|
|
121
|
+
.map(([name, reading]) => `${name}: ${reading.reason}`)
|
|
122
|
+
.join('\n');
|
|
123
|
+
const frozen = {
|
|
124
|
+
revision: block.revision!,
|
|
125
|
+
readings,
|
|
126
|
+
links,
|
|
127
|
+
fallback: `${uiQueryFallback(block, values).slice(0, 8192)}${names ? `\nSources: ${names}` : ''}${failures ? `\n${failures}` : ''}`
|
|
128
|
+
};
|
|
129
|
+
const blocks = { ...latest.uiQueries.blocks, [block.id]: frozen };
|
|
130
|
+
if (Buffer.byteLength(JSON.stringify(blocks)) > MAX_SNAPSHOT_BYTES) {
|
|
131
|
+
frozen.readings = Object.fromEntries(
|
|
132
|
+
Object.keys(readings).map((name) => [
|
|
133
|
+
name,
|
|
134
|
+
{ state: 'failed', readAt: Date.now(), reason: 'The frozen query values are too large.' }
|
|
135
|
+
])
|
|
136
|
+
);
|
|
137
|
+
frozen.links = Object.fromEntries(Object.keys(links).map((id) => [id, { state: 'plain', reason: 'This link must be checked again.' }]));
|
|
138
|
+
frozen.fallback = `${block.fallback.slice(0, 2048)}\nSources: ${names}\nThe frozen query values are too large.`;
|
|
139
|
+
}
|
|
140
|
+
if (Buffer.byteLength(JSON.stringify(blocks)) > MAX_SNAPSHOT_BYTES) {
|
|
141
|
+
delete blocks[block.id];
|
|
142
|
+
}
|
|
143
|
+
session.updateUiQueries(item.id, { ...latest.uiQueries, blocks });
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
.catch(() => {})
|
|
147
|
+
.finally(() => this.freezing.delete(key));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private access(session: ChatSession, item: ChatAssistantItem): Promise<ChatUiQueryState> {
|
|
151
|
+
const key = `${session.id}:${item.id}`;
|
|
152
|
+
const existing = this.captures.get(key);
|
|
153
|
+
if (existing) {
|
|
154
|
+
return existing;
|
|
155
|
+
}
|
|
156
|
+
if (item.uiQueries?.authorChatId === session.id) {
|
|
157
|
+
return Promise.resolve(item.uiQueries);
|
|
158
|
+
}
|
|
159
|
+
// A pending capture is persisted as denied, so a restart cannot widen the writer's rights.
|
|
160
|
+
session.updateUiQueries(item.id, { authorChatId: session.id, access: null, blocks: {} });
|
|
161
|
+
const captured = this.host
|
|
162
|
+
.capture(session.info)
|
|
163
|
+
.then((access) => {
|
|
164
|
+
const captured = copyUiValue(access, new UiBudget());
|
|
165
|
+
if (Buffer.byteLength(JSON.stringify(captured)) > 32 * 1024) {
|
|
166
|
+
throw new Error('The UI access snapshot is too large.');
|
|
167
|
+
}
|
|
168
|
+
const state: ChatUiQueryState = { authorChatId: session.id, access: captured, blocks: {} };
|
|
169
|
+
session.updateUiQueries(item.id, state);
|
|
170
|
+
return state;
|
|
171
|
+
})
|
|
172
|
+
.catch(() => ({ authorChatId: session.id, access: null, blocks: {} }))
|
|
173
|
+
.finally(() => this.captures.delete(key));
|
|
174
|
+
this.captures.set(key, captured);
|
|
175
|
+
return captured;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private storedBlock(
|
|
179
|
+
session: ChatSession,
|
|
180
|
+
payload: Pick<ChatUiQueryPayload, 'itemId' | 'blockId' | 'revision'>
|
|
181
|
+
): { item: ChatAssistantItem; block: UiBlock } {
|
|
182
|
+
const item = session.thread.get(payload.itemId);
|
|
183
|
+
const block =
|
|
184
|
+
item?.kind === 'assistant' && !item.streaming && !item.parentToolUseId ? item.ui?.find((entry) => entry.id === payload.blockId) : undefined;
|
|
185
|
+
if (item?.kind !== 'assistant' || !block?.complete || block.revision !== payload.revision) {
|
|
186
|
+
throw new ChatError('stale-ui-block', 'This completed UI block is no longer available.');
|
|
187
|
+
}
|
|
188
|
+
return { item, block };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private stored(session: ChatSession, payload: ChatUiQueryPayload): { item: ChatAssistantItem; block: UiBlock; source: ChatUiSource } {
|
|
192
|
+
const { item, block } = this.storedBlock(session, payload);
|
|
193
|
+
if (!Object.hasOwn(block.queries, payload.query) || Object.keys(block.queries).length > MAX_QUERIES) {
|
|
194
|
+
throw new ChatError('refused-query', 'This block does not declare an allowed query.');
|
|
195
|
+
}
|
|
196
|
+
const name = block.queries[payload.query].source;
|
|
197
|
+
if (!Object.hasOwn(this.host.sources, name)) {
|
|
198
|
+
throw new ChatError('refused-query', 'This query source is not registered.');
|
|
199
|
+
}
|
|
200
|
+
return { item, block, source: this.host.sources[name] };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async query(session: ChatSession, payload: ChatUiQueryPayload): Promise<ChatUiQueryReading> {
|
|
204
|
+
let args: Record<string, UiValue>;
|
|
205
|
+
let source: ChatUiSource;
|
|
206
|
+
let block: UiBlock;
|
|
207
|
+
let item: ChatAssistantItem;
|
|
208
|
+
let authorAccess: unknown;
|
|
209
|
+
try {
|
|
210
|
+
({ source, block, item } = this.stored(session, payload));
|
|
211
|
+
args = uiQueryArguments(block, payload.query, payload.values);
|
|
212
|
+
args = source.args.parse(args) as Record<string, UiValue>;
|
|
213
|
+
const access = await this.access(session, item);
|
|
214
|
+
if (access.access === null) {
|
|
215
|
+
throw new ChatError('refused-query', 'The writer’s original access is unavailable.');
|
|
216
|
+
}
|
|
217
|
+
authorAccess = access.access;
|
|
218
|
+
await source.authorize(session.info, authorAccess, args);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
return { state: 'refused', readAt: Date.now(), reason: this.reason(error) };
|
|
221
|
+
}
|
|
222
|
+
const key = JSON.stringify([session.id, item.id, block.id, block.revision, payload.query, args]);
|
|
223
|
+
const cached = [...this.cache.values()].find((entry) => entry.key === key);
|
|
224
|
+
if (cached && Date.now() - cached.reading.readAt < REFRESH_MS) {
|
|
225
|
+
return cached.reading;
|
|
226
|
+
}
|
|
227
|
+
const pending = this.reading.get(key);
|
|
228
|
+
if (pending) {
|
|
229
|
+
return pending;
|
|
230
|
+
}
|
|
231
|
+
const identity = JSON.stringify([session.id, item.id, block.id, block.revision, payload.query]);
|
|
232
|
+
if (Date.now() - (this.attempts.get(identity) ?? 0) < REFRESH_MS) {
|
|
233
|
+
return { state: 'failed', readAt: Date.now(), reason: 'This query may refresh once every ten seconds.' };
|
|
234
|
+
}
|
|
235
|
+
if ((this.active.get(session.id) ?? 0) >= 2 || this.totalActive >= 16) {
|
|
236
|
+
return { state: 'failed', readAt: Date.now(), reason: 'Too many UI queries are being read.' };
|
|
237
|
+
}
|
|
238
|
+
this.attempts.set(identity, Date.now());
|
|
239
|
+
while (this.attempts.size > 128) {
|
|
240
|
+
this.attempts.delete(this.attempts.keys().next().value!);
|
|
241
|
+
}
|
|
242
|
+
this.active.set(session.id, (this.active.get(session.id) ?? 0) + 1);
|
|
243
|
+
this.totalActive++;
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
const work = Promise.resolve().then(() => source.read(session.info, args, controller.signal, authorAccess));
|
|
246
|
+
const timeout = setTimeout(() => controller.abort(), READ_TIMEOUT_MS);
|
|
247
|
+
timeout.unref?.();
|
|
248
|
+
const read = Promise.race([
|
|
249
|
+
work,
|
|
250
|
+
new Promise<never>((_, reject) => controller.signal.addEventListener('abort', () => reject(new Error('The UI query timed out.')), { once: true }))
|
|
251
|
+
])
|
|
252
|
+
.then((result): ChatUiQueryReading => {
|
|
253
|
+
const value = copyUiValue(source.result.parse(result), new UiBudget());
|
|
254
|
+
if (Buffer.byteLength(JSON.stringify(value)) > MAX_QUERY_BYTES) {
|
|
255
|
+
throw new Error('The UI query result is too large.');
|
|
256
|
+
}
|
|
257
|
+
const reading: ChatUiQueryReading = { state: 'fresh', value, readId: randomUUID(), readAt: Date.now() };
|
|
258
|
+
this.cache.set(reading.readId!, { key, chatId: session.id, reading });
|
|
259
|
+
while (this.cache.size > 128) {
|
|
260
|
+
this.cache.delete(this.cache.keys().next().value!);
|
|
261
|
+
}
|
|
262
|
+
return reading;
|
|
263
|
+
})
|
|
264
|
+
.catch((error): ChatUiQueryReading => ({ state: 'failed', readAt: Date.now(), reason: this.reason(error) }))
|
|
265
|
+
.finally(() => {
|
|
266
|
+
clearTimeout(timeout);
|
|
267
|
+
this.reading.delete(key);
|
|
268
|
+
});
|
|
269
|
+
// Timed-out sources still occupy their slot until their actual work ends.
|
|
270
|
+
void work
|
|
271
|
+
.catch(() => {})
|
|
272
|
+
.finally(() => {
|
|
273
|
+
const count = (this.active.get(session.id) ?? 1) - 1;
|
|
274
|
+
if (count) {
|
|
275
|
+
this.active.set(session.id, count);
|
|
276
|
+
} else {
|
|
277
|
+
this.active.delete(session.id);
|
|
278
|
+
}
|
|
279
|
+
this.totalActive--;
|
|
280
|
+
});
|
|
281
|
+
this.reading.set(key, read);
|
|
282
|
+
return read;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async choiceValues(session: ChatSession, payload: ChatUiChoicePayload | ChatUiLinkPayload): Promise<Record<string, unknown>> {
|
|
286
|
+
const item = session.thread.get(payload.itemId);
|
|
287
|
+
const block = item?.kind === 'assistant' ? item.ui?.find((entry) => entry.id === payload.blockId) : undefined;
|
|
288
|
+
if (!block || item?.kind !== 'assistant' || ('choiceId' in payload && item.uiAnswers?.[block.id]?.revision === payload.revision)) {
|
|
289
|
+
return {};
|
|
290
|
+
}
|
|
291
|
+
const values: Record<string, unknown> = Object.create(null);
|
|
292
|
+
for (const name of Object.keys(payload.reads ?? {})) {
|
|
293
|
+
const identity = { ...payload, query: name };
|
|
294
|
+
const { source } = this.stored(session, identity);
|
|
295
|
+
const args = source.args.parse(uiQueryArguments(block, name, payload.values)) as Record<string, UiValue>;
|
|
296
|
+
const access = await this.access(session, item);
|
|
297
|
+
await source.authorize(session.info, access.access, args);
|
|
298
|
+
const key = JSON.stringify([session.id, item.id, block.id, block.revision, name, args]);
|
|
299
|
+
const readId = payload.reads![name];
|
|
300
|
+
const cached = this.cache.get(readId);
|
|
301
|
+
const frozen = item.uiQueries?.blocks[block.id];
|
|
302
|
+
const snapshot = frozen?.revision === block.revision ? frozen?.readings[name] : undefined;
|
|
303
|
+
const reading =
|
|
304
|
+
cached?.key === key
|
|
305
|
+
? cached.reading
|
|
306
|
+
: snapshot?.readId === readId && JSON.stringify(args) === JSON.stringify(source.args.parse(uiQueryArguments(block, name)))
|
|
307
|
+
? snapshot
|
|
308
|
+
: undefined;
|
|
309
|
+
if (!reading || reading.state !== 'fresh') {
|
|
310
|
+
throw new ChatError('stale-ui-query', 'Refresh this query before sending its choice.');
|
|
311
|
+
}
|
|
312
|
+
values[name] = reading.value;
|
|
313
|
+
}
|
|
314
|
+
return values;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async link(session: ChatSession, payload: ChatUiLinkPayload): Promise<ChatUiLinkReading> {
|
|
318
|
+
try {
|
|
319
|
+
if (!this.host.link) {
|
|
320
|
+
throw new ChatError('refused-query', 'This host does not provide UI links.');
|
|
321
|
+
}
|
|
322
|
+
const { item, block } = this.storedBlock(session, payload);
|
|
323
|
+
if (!item.uiQueries) {
|
|
324
|
+
throw new ChatError('refused-query', 'The writer’s original access is unavailable.');
|
|
325
|
+
}
|
|
326
|
+
const access = await this.access(session, item);
|
|
327
|
+
if (access.access === null) {
|
|
328
|
+
throw new ChatError('refused-query', 'The writer’s original access is unavailable.');
|
|
329
|
+
}
|
|
330
|
+
const queries = await this.choiceValues(session, payload);
|
|
331
|
+
const target = uiLinkTargets(block, payload.values, queries)[payload.nodeId];
|
|
332
|
+
if (!target) {
|
|
333
|
+
throw new ChatError('refused-query', 'This visible node is not a supported link.');
|
|
334
|
+
}
|
|
335
|
+
return this.linkReading(await this.host.link(session.info, access.access, target));
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return { state: 'plain', reason: this.reason(error) };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
forget(chatId: string): void {
|
|
342
|
+
for (const [id, cached] of this.cache) {
|
|
343
|
+
if (cached.chatId === chatId) {
|
|
344
|
+
this.cache.delete(id);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private linkReading(value: unknown): ChatUiLinkReading {
|
|
350
|
+
const reading = UiLinkResolutionSchema.parse(copyUiValue(value, new UiBudget()));
|
|
351
|
+
if (reading.state === 'chip' && !reading.target) {
|
|
352
|
+
throw new Error('The host did not resolve a link target.');
|
|
353
|
+
}
|
|
354
|
+
return reading;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private reason(error: unknown): string {
|
|
358
|
+
return (error instanceof Error ? error.message : 'This query could not be read.').slice(0, 240);
|
|
359
|
+
}
|
|
360
|
+
}
|
package/src/host/handlers.ts
CHANGED
|
@@ -66,6 +66,9 @@ 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),
|
|
70
|
+
'ui.link': (payload, clientId) => chats.linkUi(payload, clientId),
|
|
71
|
+
'ui.query': (payload, clientId) => chats.queryUi(payload, clientId),
|
|
69
72
|
'chat.unqueue': (payload) => ({ message: chats.unqueue(payload.chatId, payload.messageId) }),
|
|
70
73
|
'chat.sendNow': (payload) => {
|
|
71
74
|
chats.sendNow(payload.chatId, payload.messageId);
|