@adecore/agents 0.38.0 → 0.40.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/async.d.ts +5 -0
- package/dist/async.js +13 -5
- package/dist/chat/acp-protocol.js +3 -5
- package/dist/chat/chat-core.d.ts +1 -0
- package/dist/chat/chat-core.js +9 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.d.ts +2 -0
- package/dist/chat/chat-session.js +9 -10
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex.js +0 -1
- package/dist/providers/detect.js +1 -1
- package/dist/providers/shell-commands.js +1 -2
- package/dist/run-process.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -8
- package/dist/tasks/background-limit.js +3 -2
- package/dist/tasks/task-coordinator.js +11 -17
- package/dist/testing/watch.js +0 -1
- package/dist/usage/limits/monitor.js +1 -2
- package/dist/usage/readers/claude.js +3 -10
- package/dist/usage/readers/codex.js +2 -9
- package/dist/usage/readers/grok.js +3 -10
- package/dist/usage/readers/host-log.js +3 -10
- package/dist/usage/record.d.ts +2 -0
- package/dist/usage/record.js +13 -0
- package/package.json +6 -6
- package/src/async.ts +23 -5
- package/src/chat/acp-backend.ts +121 -0
- package/src/chat/acp-protocol.ts +172 -6
- package/src/chat/attachment-store.ts +21 -0
- package/src/chat/backend.ts +0 -2
- package/src/chat/bookmark-store.ts +23 -0
- package/src/chat/chat-core.ts +218 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +414 -13
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-rules.ts +3 -0
- package/src/providers/codex.ts +2 -1
- package/src/providers/detect.ts +6 -1
- package/src/providers/provider.ts +1 -1
- package/src/providers/registry.ts +18 -2
- package/src/providers/shell-commands.ts +37 -2
- package/src/record-directory.ts +19 -0
- package/src/run-process.ts +24 -3
- package/src/serializer.ts +3 -0
- package/src/skills.ts +73 -10
- package/src/tasks/background-limit.ts +9 -2
- package/src/tasks/end-children.ts +9 -0
- package/src/tasks/give-task.ts +8 -0
- package/src/tasks/task-coordinator.ts +81 -17
- package/src/tasks/task-store.ts +28 -0
- package/src/tasks/waiting-child.ts +21 -0
- package/src/tasks/wake-parent.ts +26 -0
- package/src/tasks/wiring.ts +22 -0
- package/src/testing/accounts.ts +2 -0
- package/src/testing/fake-acp-capture.ts +16 -0
- package/src/testing/fake-acp.ts +66 -0
- package/src/testing/fake-claude.ts +177 -0
- package/src/testing/fake-cli.ts +29 -0
- package/src/testing/fake-codex.ts +122 -0
- package/src/testing/manual-clock.ts +2 -0
- package/src/testing/watch.ts +7 -1
- package/src/title-file.ts +12 -0
- package/src/usage/accounts.ts +6 -0
- package/src/usage/aggregate.ts +19 -0
- package/src/usage/exchange.ts +12 -0
- package/src/usage/index-file.ts +27 -0
- package/src/usage/limits/monitor.ts +31 -3
- package/src/usage/limits/normalize.ts +30 -0
- package/src/usage/limits/probe.ts +17 -0
- package/src/usage/pricing.ts +35 -0
- package/src/usage/projects.ts +8 -0
- package/src/usage/readers/claude.ts +12 -9
- package/src/usage/readers/codex.ts +40 -8
- package/src/usage/readers/grok.ts +17 -9
- package/src/usage/readers/host-log.ts +9 -9
- package/src/usage/record.ts +22 -0
- package/src/usage/roots.ts +6 -0
- package/src/usage/scanner.ts +61 -0
- package/src/usage/usage-service.ts +14 -0
- package/src/watch-seam.ts +8 -0
- package/src/windows-command.ts +12 -0
package/src/chat/ui-queries.ts
CHANGED
|
@@ -108,28 +108,37 @@ export class ChatUiQueries {
|
|
|
108
108
|
const key = `${session.id}:${item.id}`;
|
|
109
109
|
// Once a capture started, every delta of a streaming reply would scan its whole text again for nothing.
|
|
110
110
|
const captured = this.captures.has(key) || item.uiQueries?.authorChatId === session.id;
|
|
111
|
+
|
|
111
112
|
if (!captured && !uiMayReferenceHost(item.text) && !item.ui?.some((block) => Object.keys(block.queries).length)) {
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
115
|
+
|
|
114
116
|
const capture = this.access(session, item);
|
|
117
|
+
|
|
115
118
|
if (!final || !item.ui?.length) {
|
|
116
119
|
return;
|
|
117
120
|
}
|
|
121
|
+
|
|
118
122
|
if (this.freezing.has(key)) {
|
|
119
123
|
return;
|
|
120
124
|
}
|
|
125
|
+
|
|
121
126
|
const frozen = capture
|
|
122
127
|
.then(async () => {
|
|
123
128
|
for (const block of (item.ui ?? []).slice(0, 16)) {
|
|
124
129
|
const current = session.thread.get(item.id);
|
|
130
|
+
|
|
125
131
|
if (current?.kind !== 'assistant' || current.ui?.find((entry) => entry.id === block.id)?.revision !== block.revision) {
|
|
126
132
|
return;
|
|
127
133
|
}
|
|
134
|
+
|
|
128
135
|
if (current.uiQueries?.blocks[block.id]?.revision === block.revision) {
|
|
129
136
|
continue;
|
|
130
137
|
}
|
|
138
|
+
|
|
131
139
|
const readings: Record<string, ChatUiQueryReading> = Object.create(null);
|
|
132
140
|
const values: Record<string, unknown> = Object.create(null);
|
|
141
|
+
|
|
133
142
|
for (const name of Object.keys(block.queries).slice(0, UI_HOST_LIMITS.queries)) {
|
|
134
143
|
const reading = await this.query(session, {
|
|
135
144
|
chatId: session.id,
|
|
@@ -138,15 +147,20 @@ export class ChatUiQueries {
|
|
|
138
147
|
revision: block.revision!,
|
|
139
148
|
query: name
|
|
140
149
|
});
|
|
150
|
+
|
|
141
151
|
readings[name] = reading;
|
|
152
|
+
|
|
142
153
|
if (reading.state === 'fresh') {
|
|
143
154
|
values[name] = reading.value;
|
|
144
155
|
}
|
|
145
156
|
}
|
|
157
|
+
|
|
146
158
|
const links: Record<string, ChatUiLinkReading> = Object.create(null);
|
|
159
|
+
|
|
147
160
|
if (this.host.link) {
|
|
148
161
|
const targets = uiLinkTargets(block, {}, values);
|
|
149
162
|
const access = await this.access(session, current);
|
|
163
|
+
|
|
150
164
|
for (const [id, target] of Object.entries(targets).slice(0, UI_HOST_LIMITS.links)) {
|
|
151
165
|
try {
|
|
152
166
|
links[id] = this.linkReading(await this.host.link(session.info, access, target));
|
|
@@ -155,21 +169,28 @@ export class ChatUiQueries {
|
|
|
155
169
|
}
|
|
156
170
|
}
|
|
157
171
|
}
|
|
172
|
+
|
|
158
173
|
const { places, found, ended } = await this.lookUpPlaces(session, item.id, block, values);
|
|
174
|
+
|
|
159
175
|
if (ended) {
|
|
160
176
|
return;
|
|
161
177
|
}
|
|
178
|
+
|
|
162
179
|
const latest = session.thread.get(item.id);
|
|
180
|
+
|
|
163
181
|
if (latest?.kind !== 'assistant' || !latest.uiQueries || latest.ui?.find((entry) => entry.id === block.id)?.revision !== block.revision) {
|
|
164
182
|
return;
|
|
165
183
|
}
|
|
184
|
+
|
|
166
185
|
const names = Object.entries(block.queries)
|
|
167
186
|
.map(([name, query]) => `${name}: ${query.source}`)
|
|
168
187
|
.join(', ');
|
|
188
|
+
|
|
169
189
|
const failures = Object.entries(readings)
|
|
170
190
|
.filter(([, reading]) => reading.state !== 'fresh')
|
|
171
191
|
.map(([name, reading]) => `${name}: ${reading.reason}`)
|
|
172
192
|
.join('\n');
|
|
193
|
+
|
|
173
194
|
const frozen: ChatUiQueryState['blocks'][string] = {
|
|
174
195
|
revision: block.revision!,
|
|
175
196
|
readings,
|
|
@@ -177,7 +198,9 @@ export class ChatUiQueries {
|
|
|
177
198
|
...(Object.keys(places).length > 0 ? { places } : {}),
|
|
178
199
|
fallback: `${uiQueryFallback(block, values).slice(0, 8192)}${names ? `\nSources: ${names}` : ''}${failures ? `\n${failures}` : ''}${found.length > 0 ? `\nPlaces: ${found.join('; ')}` : ''}`
|
|
179
200
|
};
|
|
201
|
+
|
|
180
202
|
const blocks = { ...latest.uiQueries.blocks, [block.id]: frozen };
|
|
203
|
+
|
|
181
204
|
if (Buffer.byteLength(JSON.stringify(blocks)) > MAX_SNAPSHOT_BYTES) {
|
|
182
205
|
frozen.readings = Object.fromEntries(
|
|
183
206
|
Object.keys(readings).map((name) => [
|
|
@@ -185,9 +208,11 @@ export class ChatUiQueries {
|
|
|
185
208
|
{ state: 'failed', readAt: Date.now(), code: 'snapshot-too-large', reason: 'The frozen query values are too large.' }
|
|
186
209
|
])
|
|
187
210
|
);
|
|
211
|
+
|
|
188
212
|
frozen.links = Object.fromEntries(
|
|
189
213
|
Object.keys(links).map((id) => [id, { state: 'plain', code: 'link-unchecked', reason: 'This link must be checked again.' }])
|
|
190
214
|
);
|
|
215
|
+
|
|
191
216
|
if (frozen.places) {
|
|
192
217
|
frozen.places = Object.fromEntries(
|
|
193
218
|
Object.entries(places).map(([id, reading]) => [
|
|
@@ -196,17 +221,21 @@ export class ChatUiQueries {
|
|
|
196
221
|
])
|
|
197
222
|
);
|
|
198
223
|
}
|
|
224
|
+
|
|
199
225
|
frozen.fallback = `${block.fallback.slice(0, 2048)}\nSources: ${names}\nThe frozen query values are too large.`;
|
|
200
226
|
}
|
|
227
|
+
|
|
201
228
|
if (Buffer.byteLength(JSON.stringify(blocks)) > MAX_SNAPSHOT_BYTES) {
|
|
202
229
|
// The block stays marked as frozen with nothing in it, so a reader stops waiting for readings that will not come.
|
|
203
230
|
blocks[block.id] = { revision: block.revision!, readings: {}, fallback: '' };
|
|
204
231
|
}
|
|
232
|
+
|
|
205
233
|
session.updateUiQueries(item.id, { ...latest.uiQueries, blocks });
|
|
206
234
|
}
|
|
207
235
|
})
|
|
208
236
|
.catch(() => {})
|
|
209
237
|
.finally(() => this.freezing.delete(key));
|
|
238
|
+
|
|
210
239
|
this.freezing.set(key, frozen);
|
|
211
240
|
}
|
|
212
241
|
|
|
@@ -229,15 +258,19 @@ export class ChatUiQueries {
|
|
|
229
258
|
): Promise<{ places: Record<string, ChatUiPlaceReading>; found: string[]; ended: boolean }> {
|
|
230
259
|
const places: Record<string, ChatUiPlaceReading> = Object.create(null);
|
|
231
260
|
const found: string[] = [];
|
|
261
|
+
|
|
232
262
|
if (!this.host.place) {
|
|
233
263
|
return { places, found, ended: false };
|
|
234
264
|
}
|
|
265
|
+
|
|
235
266
|
let queries: Array<[string, UiPlaceQuery]>;
|
|
267
|
+
|
|
236
268
|
try {
|
|
237
269
|
queries = Object.entries(uiPlaceQueries(block, {}, values)).slice(0, UI_HOST_LIMITS.places);
|
|
238
270
|
} catch {
|
|
239
271
|
return { places, found, ended: false };
|
|
240
272
|
}
|
|
273
|
+
|
|
241
274
|
const life = this.lifeOf(session.id).signal;
|
|
242
275
|
const budget = new AbortController();
|
|
243
276
|
const timer = setTimeout(() => budget.abort(), PLACE_BLOCK_BUDGET_MS);
|
|
@@ -246,49 +279,64 @@ export class ChatUiQueries {
|
|
|
246
279
|
const readings = new Map<string, ChatUiPlaceReading>();
|
|
247
280
|
let timeouts = 0;
|
|
248
281
|
let next = 0;
|
|
282
|
+
|
|
249
283
|
const stillThere = (): boolean => {
|
|
250
284
|
const current = session.thread.get(itemId);
|
|
285
|
+
|
|
251
286
|
return !life.aborted && current?.kind === 'assistant' && current.ui?.find((entry) => entry.id === block.id)?.revision === block.revision;
|
|
252
287
|
};
|
|
288
|
+
|
|
253
289
|
const worker = async (): Promise<void> => {
|
|
254
290
|
for (let index = next++; index < queries.length; index = next++) {
|
|
255
291
|
const [id, query] = queries[index]!;
|
|
292
|
+
|
|
256
293
|
if (!stillThere()) {
|
|
257
294
|
return;
|
|
258
295
|
}
|
|
296
|
+
|
|
259
297
|
if (budget.signal.aborted || timeouts >= PLACE_TIMEOUTS_IN_A_ROW) {
|
|
260
298
|
readings.set(id, { state: 'missing', address: query.address, code: 'timed-out', reason: 'The place lookup timed out.' });
|
|
299
|
+
|
|
261
300
|
continue;
|
|
262
301
|
}
|
|
302
|
+
|
|
263
303
|
const reading = await this.placeReading(session.info, query, signal);
|
|
264
304
|
timeouts = reading.code === 'timed-out' ? timeouts + 1 : 0;
|
|
265
305
|
readings.set(id, reading);
|
|
266
306
|
}
|
|
267
307
|
};
|
|
308
|
+
|
|
268
309
|
try {
|
|
269
310
|
await Promise.all(Array.from({ length: Math.min(PLACE_PARALLEL, queries.length) }, worker));
|
|
270
311
|
} finally {
|
|
271
312
|
clearTimeout(timer);
|
|
272
313
|
}
|
|
314
|
+
|
|
273
315
|
for (const [id, query] of queries) {
|
|
274
316
|
const reading = readings.get(id);
|
|
317
|
+
|
|
275
318
|
if (reading === undefined) {
|
|
276
319
|
continue;
|
|
277
320
|
}
|
|
321
|
+
|
|
278
322
|
places[id] = reading;
|
|
323
|
+
|
|
279
324
|
if (reading.state === 'found') {
|
|
280
325
|
found.push(`${query.name || query.address}: ${reading.lat}, ${reading.lon}`);
|
|
281
326
|
}
|
|
282
327
|
}
|
|
328
|
+
|
|
283
329
|
return { places, found, ended: life.aborted };
|
|
284
330
|
}
|
|
285
331
|
|
|
286
332
|
private lifeOf(chatId: string): AbortController {
|
|
287
333
|
let life = this.lives.get(chatId);
|
|
334
|
+
|
|
288
335
|
if (life === undefined) {
|
|
289
336
|
life = new AbortController();
|
|
290
337
|
this.lives.set(chatId, life);
|
|
291
338
|
}
|
|
339
|
+
|
|
292
340
|
return life;
|
|
293
341
|
}
|
|
294
342
|
|
|
@@ -302,6 +350,7 @@ export class ChatUiQueries {
|
|
|
302
350
|
const timer = setTimeout(() => timeout.abort(), PLACE_TIMEOUT_MS);
|
|
303
351
|
timer.unref?.();
|
|
304
352
|
const signal = AbortSignal.any([timeout.signal, outer]);
|
|
353
|
+
|
|
305
354
|
try {
|
|
306
355
|
const reading = await Promise.race([
|
|
307
356
|
this.host.place!(info, query, signal),
|
|
@@ -311,7 +360,9 @@ export class ChatUiQueries {
|
|
|
311
360
|
})
|
|
312
361
|
)
|
|
313
362
|
]);
|
|
363
|
+
|
|
314
364
|
const parsed = ChatUiPlaceReadingSchema.parse({ ...(copyUiValue(reading, new UiBudget()) as object), address: query.address });
|
|
365
|
+
|
|
315
366
|
return parsed.state === 'found' && (parsed.lat === undefined || parsed.lon === undefined)
|
|
316
367
|
? { state: 'missing', address: query.address, code: 'place-unreadable', reason: 'The host found no point for this address.' }
|
|
317
368
|
: parsed;
|
|
@@ -328,28 +379,37 @@ export class ChatUiQueries {
|
|
|
328
379
|
private access(session: ChatSession, item: ChatAssistantItem): Promise<unknown> {
|
|
329
380
|
const key = `${session.id}:${item.id}`;
|
|
330
381
|
const existing = this.captures.get(key);
|
|
382
|
+
|
|
331
383
|
if (existing) {
|
|
332
384
|
return existing;
|
|
333
385
|
}
|
|
386
|
+
|
|
334
387
|
if (item.uiQueries?.authorChatId === session.id) {
|
|
335
388
|
return Promise.resolve(session.uiAccess(item.id) ?? null);
|
|
336
389
|
}
|
|
390
|
+
|
|
337
391
|
// A pending capture is persisted as denied, so a restart cannot widen the writer's rights.
|
|
338
392
|
session.setUiAccess(item.id, null);
|
|
339
393
|
session.updateUiQueries(item.id, { authorChatId: session.id, blocks: {} });
|
|
394
|
+
|
|
340
395
|
const captured = this.host
|
|
341
396
|
.capture(session.info)
|
|
342
397
|
.then((access) => {
|
|
343
398
|
const captured = copyUiValue(access, new UiBudget());
|
|
399
|
+
|
|
344
400
|
if (Buffer.byteLength(JSON.stringify(captured)) > 32 * 1024) {
|
|
345
401
|
throw new Error('The UI access snapshot is too large.');
|
|
346
402
|
}
|
|
403
|
+
|
|
347
404
|
session.setUiAccess(item.id, captured);
|
|
405
|
+
|
|
348
406
|
return captured as unknown;
|
|
349
407
|
})
|
|
350
408
|
.catch(() => null)
|
|
351
409
|
.finally(() => this.captures.delete(key));
|
|
410
|
+
|
|
352
411
|
this.captures.set(key, captured);
|
|
412
|
+
|
|
353
413
|
return captured;
|
|
354
414
|
}
|
|
355
415
|
|
|
@@ -358,23 +418,30 @@ export class ChatUiQueries {
|
|
|
358
418
|
payload: Pick<ChatUiQueryPayload, 'itemId' | 'blockId' | 'revision'>
|
|
359
419
|
): { item: ChatAssistantItem; block: UiBlock } {
|
|
360
420
|
const item = session.thread.get(payload.itemId);
|
|
421
|
+
|
|
361
422
|
const block =
|
|
362
423
|
item?.kind === 'assistant' && !item.streaming && !item.parentToolUseId ? item.ui?.find((entry) => entry.id === payload.blockId) : undefined;
|
|
424
|
+
|
|
363
425
|
if (item?.kind !== 'assistant' || !block?.complete || block.revision !== payload.revision) {
|
|
364
426
|
throw new UiQueryError('stale-ui-block', 'block-stale', 'This completed UI block is no longer available.');
|
|
365
427
|
}
|
|
428
|
+
|
|
366
429
|
return { item, block };
|
|
367
430
|
}
|
|
368
431
|
|
|
369
432
|
private stored(session: ChatSession, payload: ChatUiQueryPayload): { item: ChatAssistantItem; block: UiBlock; source: ChatUiSource } {
|
|
370
433
|
const { item, block } = this.storedBlock(session, payload);
|
|
434
|
+
|
|
371
435
|
if (!Object.hasOwn(block.queries, payload.query) || Object.keys(block.queries).length > UI_HOST_LIMITS.queries) {
|
|
372
436
|
throw new UiQueryError('refused-query', 'query-undeclared', 'This block does not declare an allowed query.');
|
|
373
437
|
}
|
|
438
|
+
|
|
374
439
|
const name = block.queries[payload.query].source;
|
|
440
|
+
|
|
375
441
|
if (!Object.hasOwn(this.host.sources, name)) {
|
|
376
442
|
throw new UiQueryError('refused-query', 'source-unregistered', 'This query source is not registered.');
|
|
377
443
|
}
|
|
444
|
+
|
|
378
445
|
return { item, block, source: this.host.sources[name] };
|
|
379
446
|
}
|
|
380
447
|
|
|
@@ -384,49 +451,63 @@ export class ChatUiQueries {
|
|
|
384
451
|
let block: UiBlock;
|
|
385
452
|
let item: ChatAssistantItem;
|
|
386
453
|
let authorAccess: unknown;
|
|
454
|
+
|
|
387
455
|
try {
|
|
388
456
|
({ source, block, item } = this.stored(session, payload));
|
|
389
457
|
args = uiQueryArguments(block, payload.query, payload.values, {}, this.heldValues(session, item, block));
|
|
390
458
|
args = source.args.parse(args) as Record<string, UiValue>;
|
|
391
459
|
authorAccess = await this.access(session, item);
|
|
460
|
+
|
|
392
461
|
if (authorAccess === null) {
|
|
393
462
|
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
394
463
|
}
|
|
464
|
+
|
|
395
465
|
await source.authorize(session.info, authorAccess, args);
|
|
396
466
|
} catch (error) {
|
|
397
467
|
return { state: 'refused', readAt: Date.now(), ...this.failure(error) };
|
|
398
468
|
}
|
|
469
|
+
|
|
399
470
|
const key = JSON.stringify([session.id, item.id, block.id, block.revision, payload.query, args]);
|
|
400
471
|
const cached = this.cache.get(this.latest.get(key) ?? '');
|
|
472
|
+
|
|
401
473
|
if (cached && Date.now() - cached.reading.readAt < UI_HOST_LIMITS.refreshMilliseconds) {
|
|
402
474
|
return cached.reading;
|
|
403
475
|
}
|
|
476
|
+
|
|
404
477
|
const pending = this.reading.get(key);
|
|
478
|
+
|
|
405
479
|
if (pending) {
|
|
406
480
|
return pending;
|
|
407
481
|
}
|
|
482
|
+
|
|
408
483
|
const identity = JSON.stringify([session.id, item.id, block.id, block.revision, payload.query]);
|
|
409
484
|
const now = Date.now();
|
|
485
|
+
|
|
410
486
|
if (
|
|
411
487
|
now - (this.attempts.get(key) ?? -Infinity) < UI_HOST_LIMITS.refreshMilliseconds ||
|
|
412
488
|
now - (this.attempts.get(identity) ?? -Infinity) < INPUT_READ_MS
|
|
413
489
|
) {
|
|
414
490
|
return { state: 'failed', readAt: now, code: 'refresh-limit', reason: 'This query may refresh once every ten seconds.' };
|
|
415
491
|
}
|
|
492
|
+
|
|
416
493
|
if ((this.active.get(session.id) ?? 0) >= 2 || this.totalActive >= 16) {
|
|
417
494
|
return { state: 'failed', readAt: now, code: 'busy', reason: 'Too many UI queries are being read.' };
|
|
418
495
|
}
|
|
496
|
+
|
|
419
497
|
this.attempts.set(key, now);
|
|
420
498
|
this.attempts.set(identity, now);
|
|
499
|
+
|
|
421
500
|
while (this.attempts.size > 256) {
|
|
422
501
|
this.attempts.delete(this.attempts.keys().next().value!);
|
|
423
502
|
}
|
|
503
|
+
|
|
424
504
|
this.active.set(session.id, (this.active.get(session.id) ?? 0) + 1);
|
|
425
505
|
this.totalActive++;
|
|
426
506
|
const controller = new AbortController();
|
|
427
507
|
const work = Promise.resolve().then(() => source.read(session.info, args, controller.signal, authorAccess));
|
|
428
508
|
const timeout = setTimeout(() => controller.abort(), READ_TIMEOUT_MS);
|
|
429
509
|
timeout.unref?.();
|
|
510
|
+
|
|
430
511
|
const read = Promise.race([
|
|
431
512
|
work,
|
|
432
513
|
new Promise<never>((_, reject) =>
|
|
@@ -437,16 +518,20 @@ export class ChatUiQueries {
|
|
|
437
518
|
])
|
|
438
519
|
.then((result): ChatUiQueryReading => {
|
|
439
520
|
const value = copyUiValue(source.result.parse(result), new UiBudget());
|
|
521
|
+
|
|
440
522
|
if (Buffer.byteLength(JSON.stringify(value)) > MAX_QUERY_BYTES) {
|
|
441
523
|
throw new UiQueryError('refused-query', 'result-too-large', 'The UI query result is too large.');
|
|
442
524
|
}
|
|
525
|
+
|
|
443
526
|
const reading: ChatUiQueryReading = { state: 'fresh', value, readId: randomUUID(), readAt: Date.now() };
|
|
444
527
|
this.cache.set(reading.readId!, { key, identity, chatId: session.id, reading });
|
|
445
528
|
this.latest.set(key, reading.readId!);
|
|
446
529
|
this.newest.set(identity, reading.readId!);
|
|
530
|
+
|
|
447
531
|
while (this.cache.size > 128) {
|
|
448
532
|
this.evict(this.cache.keys().next().value!);
|
|
449
533
|
}
|
|
534
|
+
|
|
450
535
|
return reading;
|
|
451
536
|
})
|
|
452
537
|
.catch((error): ChatUiQueryReading => ({ state: 'failed', readAt: Date.now(), ...this.failure(error) }))
|
|
@@ -454,19 +539,24 @@ export class ChatUiQueries {
|
|
|
454
539
|
clearTimeout(timeout);
|
|
455
540
|
this.reading.delete(key);
|
|
456
541
|
});
|
|
542
|
+
|
|
457
543
|
// Timed-out sources still occupy their slot until their actual work ends.
|
|
458
544
|
void work
|
|
459
545
|
.catch(() => {})
|
|
460
546
|
.finally(() => {
|
|
461
547
|
const count = (this.active.get(session.id) ?? 1) - 1;
|
|
548
|
+
|
|
462
549
|
if (count) {
|
|
463
550
|
this.active.set(session.id, count);
|
|
464
551
|
} else {
|
|
465
552
|
this.active.delete(session.id);
|
|
466
553
|
}
|
|
554
|
+
|
|
467
555
|
this.totalActive--;
|
|
468
556
|
});
|
|
557
|
+
|
|
469
558
|
this.reading.set(key, read);
|
|
559
|
+
|
|
470
560
|
return read;
|
|
471
561
|
}
|
|
472
562
|
|
|
@@ -474,36 +564,48 @@ export class ChatUiQueries {
|
|
|
474
564
|
async choiceValues(session: ChatSession, payload: ChatUiChoicePayload | ChatUiLinkPayload): Promise<Record<string, unknown>> {
|
|
475
565
|
const item = session.thread.get(payload.itemId);
|
|
476
566
|
const block = item?.kind === 'assistant' ? item.ui?.find((entry) => entry.id === payload.blockId) : undefined;
|
|
567
|
+
|
|
477
568
|
if (!block || item?.kind !== 'assistant' || ('choiceId' in payload && item.uiAnswers?.[block.id]?.revision === payload.revision)) {
|
|
478
569
|
return {};
|
|
479
570
|
}
|
|
571
|
+
|
|
480
572
|
const names = Object.keys(block.queries);
|
|
481
573
|
const values: Record<string, unknown> = Object.create(null);
|
|
574
|
+
|
|
482
575
|
if (names.length === 0) {
|
|
483
576
|
return values;
|
|
484
577
|
}
|
|
578
|
+
|
|
485
579
|
const access = await this.access(session, item);
|
|
580
|
+
|
|
486
581
|
if (access === null) {
|
|
487
582
|
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
488
583
|
}
|
|
584
|
+
|
|
489
585
|
const frozen = item.uiQueries?.blocks[block.id];
|
|
586
|
+
|
|
490
587
|
const readIdOf = (name: string): string | undefined =>
|
|
491
588
|
payload.reads !== undefined && Object.hasOwn(payload.reads, name) ? payload.reads[name] : undefined;
|
|
589
|
+
|
|
492
590
|
// What the person saw, by the read ids it names; each one is checked against its arguments below.
|
|
493
591
|
const seen: Record<string, unknown> = Object.create(null);
|
|
592
|
+
|
|
494
593
|
for (const name of names) {
|
|
495
594
|
const readId = readIdOf(name);
|
|
496
595
|
const cached = readId === undefined ? undefined : this.cache.get(readId);
|
|
596
|
+
|
|
497
597
|
const reading =
|
|
498
598
|
cached?.identity === JSON.stringify([session.id, item.id, block.id, block.revision, name])
|
|
499
599
|
? cached.reading
|
|
500
600
|
: frozen?.revision === block.revision
|
|
501
601
|
? frozen?.readings[name]
|
|
502
602
|
: undefined;
|
|
603
|
+
|
|
503
604
|
if (readId !== undefined && reading?.readId === readId && reading.state === 'fresh') {
|
|
504
605
|
seen[name] = reading.value;
|
|
505
606
|
}
|
|
506
607
|
}
|
|
608
|
+
|
|
507
609
|
for (const name of names) {
|
|
508
610
|
const { source } = this.stored(session, { ...payload, query: name });
|
|
509
611
|
const args = source.args.parse(uiQueryArguments(block, name, payload.values, {}, seen)) as Record<string, UiValue>;
|
|
@@ -512,6 +614,7 @@ export class ChatUiQueries {
|
|
|
512
614
|
const key = JSON.stringify([session.id, item.id, block.id, block.revision, name, args]);
|
|
513
615
|
const cached = readId === undefined ? undefined : this.cache.get(readId);
|
|
514
616
|
const snapshot = frozen?.revision === block.revision ? frozen?.readings[name] : undefined;
|
|
617
|
+
|
|
515
618
|
const reading =
|
|
516
619
|
readId === undefined
|
|
517
620
|
? undefined
|
|
@@ -520,11 +623,14 @@ export class ChatUiQueries {
|
|
|
520
623
|
: snapshot?.readId === readId && JSON.stringify(args) === JSON.stringify(source.args.parse(uiQueryArguments(block, name)))
|
|
521
624
|
? snapshot
|
|
522
625
|
: undefined;
|
|
626
|
+
|
|
523
627
|
if (!reading || reading.state !== 'fresh') {
|
|
524
628
|
throw new UiQueryError('stale-ui-query', 'stale-read', 'Refresh this query before sending its choice.');
|
|
525
629
|
}
|
|
630
|
+
|
|
526
631
|
values[name] = reading.value;
|
|
527
632
|
}
|
|
633
|
+
|
|
528
634
|
return values;
|
|
529
635
|
}
|
|
530
636
|
|
|
@@ -533,19 +639,26 @@ export class ChatUiQueries {
|
|
|
533
639
|
if (!this.host.link) {
|
|
534
640
|
throw new UiQueryError('refused-query', 'links-unsupported', 'This host does not provide UI links.');
|
|
535
641
|
}
|
|
642
|
+
|
|
536
643
|
const { item, block } = this.storedBlock(session, payload);
|
|
644
|
+
|
|
537
645
|
if (!item.uiQueries) {
|
|
538
646
|
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
539
647
|
}
|
|
648
|
+
|
|
540
649
|
const access = await this.access(session, item);
|
|
650
|
+
|
|
541
651
|
if (access === null) {
|
|
542
652
|
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
543
653
|
}
|
|
654
|
+
|
|
544
655
|
const queries = await this.choiceValues(session, payload);
|
|
545
656
|
const target = uiLinkTargets(block, payload.values, queries)[payload.nodeId];
|
|
657
|
+
|
|
546
658
|
if (!target) {
|
|
547
659
|
throw new UiQueryError('refused-query', 'link-unsupported', 'This visible node is not a supported link.');
|
|
548
660
|
}
|
|
661
|
+
|
|
549
662
|
return this.linkReading(await this.host.link(session.info, access, target));
|
|
550
663
|
} catch (error) {
|
|
551
664
|
return { state: 'plain', ...this.failure(error) };
|
|
@@ -555,6 +668,7 @@ export class ChatUiQueries {
|
|
|
555
668
|
forget(chatId: string): void {
|
|
556
669
|
this.lives.get(chatId)?.abort();
|
|
557
670
|
this.lives.delete(chatId);
|
|
671
|
+
|
|
558
672
|
for (const [id, cached] of this.cache) {
|
|
559
673
|
if (cached.chatId === chatId) {
|
|
560
674
|
this.evict(id);
|
|
@@ -565,9 +679,11 @@ export class ChatUiQueries {
|
|
|
565
679
|
private evict(readId: string): void {
|
|
566
680
|
const cached = this.cache.get(readId);
|
|
567
681
|
this.cache.delete(readId);
|
|
682
|
+
|
|
568
683
|
if (cached && this.latest.get(cached.key) === readId) {
|
|
569
684
|
this.latest.delete(cached.key);
|
|
570
685
|
}
|
|
686
|
+
|
|
571
687
|
if (cached && this.newest.get(cached.identity) === readId) {
|
|
572
688
|
this.newest.delete(cached.identity);
|
|
573
689
|
}
|
|
@@ -577,23 +693,29 @@ export class ChatUiQueries {
|
|
|
577
693
|
private heldValues(session: ChatSession, item: ChatAssistantItem, block: UiBlock): Record<string, unknown> {
|
|
578
694
|
const frozen = item.uiQueries?.blocks[block.id];
|
|
579
695
|
const values: Record<string, unknown> = Object.create(null);
|
|
696
|
+
|
|
580
697
|
for (const name of Object.keys(block.queries)) {
|
|
581
698
|
const readId = this.newest.get(JSON.stringify([session.id, item.id, block.id, block.revision, name]));
|
|
699
|
+
|
|
582
700
|
const reading =
|
|
583
701
|
(readId === undefined ? undefined : this.cache.get(readId)?.reading) ??
|
|
584
702
|
(frozen?.revision === block.revision ? frozen?.readings[name] : undefined);
|
|
703
|
+
|
|
585
704
|
if (reading?.state === 'fresh') {
|
|
586
705
|
values[name] = reading.value;
|
|
587
706
|
}
|
|
588
707
|
}
|
|
708
|
+
|
|
589
709
|
return values;
|
|
590
710
|
}
|
|
591
711
|
|
|
592
712
|
private linkReading(value: unknown): ChatUiLinkReading {
|
|
593
713
|
const reading = UiLinkResolutionSchema.parse(copyUiValue(value, new UiBudget()));
|
|
714
|
+
|
|
594
715
|
if (reading.state === 'chip' && !reading.target) {
|
|
595
716
|
throw new Error('The host did not resolve a link target.');
|
|
596
717
|
}
|
|
718
|
+
|
|
597
719
|
return reading;
|
|
598
720
|
}
|
|
599
721
|
|