@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/dist/chat/chat-core.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface InterruptedRun {
|
|
|
27
27
|
}
|
|
28
28
|
export interface ChatCoreOptions {
|
|
29
29
|
intelligentUi?: ChatUiHost;
|
|
30
|
+
uiFenceLanguage?: string;
|
|
30
31
|
providers: ProviderRegistry;
|
|
31
32
|
store?: ChatStore;
|
|
32
33
|
checkpoints?: TurnCheckpoints;
|
|
@@ -190,6 +191,7 @@ export declare class ChatCore {
|
|
|
190
191
|
readonly claudeProjectsDir: string;
|
|
191
192
|
readonly composerPreferences: ComposerPreferences;
|
|
192
193
|
private readonly uiQueries;
|
|
194
|
+
private readonly uiFenceLanguage;
|
|
193
195
|
private readonly checkpoints;
|
|
194
196
|
private readonly instructions;
|
|
195
197
|
private readonly skillIndex;
|
|
@@ -290,6 +292,7 @@ export declare class ChatCore {
|
|
|
290
292
|
conflicted?: boolean | undefined;
|
|
291
293
|
relativePath?: string | undefined;
|
|
292
294
|
label?: string | undefined;
|
|
295
|
+
code?: string | undefined;
|
|
293
296
|
reason?: string | undefined;
|
|
294
297
|
}>;
|
|
295
298
|
queryUi(payload: ChatUiQueryPayload, clientId: string): Promise<{
|
|
@@ -297,9 +300,10 @@ export declare class ChatCore {
|
|
|
297
300
|
value?: unknown;
|
|
298
301
|
readId?: string | undefined;
|
|
299
302
|
readAt: number;
|
|
303
|
+
code?: string | undefined;
|
|
300
304
|
reason?: string | undefined;
|
|
301
305
|
}>;
|
|
302
|
-
choose(payload: ChatUiChoicePayload): Promise<{
|
|
306
|
+
choose(payload: ChatUiChoicePayload, clientId: string): Promise<{
|
|
303
307
|
queued: boolean;
|
|
304
308
|
turnId: string;
|
|
305
309
|
}>;
|
package/dist/chat/chat-core.js
CHANGED
|
@@ -11,6 +11,7 @@ import { ChatLog, COMPACT_ABOVE_BYTES } from './chat-log.js';
|
|
|
11
11
|
import { ChatSession } from './chat-session.js';
|
|
12
12
|
import { ComposerPreferences } from './composer-preferences.js';
|
|
13
13
|
import { DeltaCoalescer } from './delta-coalescer.js';
|
|
14
|
+
import { readableAssistantText } from './readable-text.js';
|
|
14
15
|
import { ChatError } from './errors.js';
|
|
15
16
|
import { commandLabel, isBackgroundWork, runsInBackground } from './background-work.js';
|
|
16
17
|
import { SubagentReader } from './subagent-reader.js';
|
|
@@ -25,7 +26,8 @@ function isBookmarkable(item) {
|
|
|
25
26
|
/* The start of a message on one line; a message of only attachments is its file names. */
|
|
26
27
|
function excerptOf(item) {
|
|
27
28
|
const files = item.kind === 'user' ? (item.attachments ?? []).map((attachment) => attachment.name).join(', ') : '';
|
|
28
|
-
|
|
29
|
+
const text = item.kind === 'assistant' ? readableAssistantText(item) : item.text;
|
|
30
|
+
return (text.trim() === '' ? files : text).replace(/\s+/g, ' ').trim();
|
|
29
31
|
}
|
|
30
32
|
// One process worked on the turn and one more may take it up after a restart; a loop of resumes could redo a command forever.
|
|
31
33
|
const MAX_ATTEMPTS = 2;
|
|
@@ -61,6 +63,7 @@ export class ChatCore {
|
|
|
61
63
|
claudeProjectsDir;
|
|
62
64
|
composerPreferences = new ComposerPreferences();
|
|
63
65
|
uiQueries;
|
|
66
|
+
uiFenceLanguage;
|
|
64
67
|
checkpoints;
|
|
65
68
|
instructions;
|
|
66
69
|
skillIndex;
|
|
@@ -74,7 +77,7 @@ export class ChatCore {
|
|
|
74
77
|
// How big each record was the last time it went to disk, and the writes waiting for a big one.
|
|
75
78
|
sizes = new Map();
|
|
76
79
|
waiting = new Map();
|
|
77
|
-
// What the last record held that no event carries (preambles, the host's extras), as its JSON.
|
|
80
|
+
// What the last record held that no event carries (preambles, the host's extras, captured UI access), as its JSON.
|
|
78
81
|
unlogged = new Map();
|
|
79
82
|
// The write in flight per chat, so the next one queues behind it instead of racing it.
|
|
80
83
|
writes = new Map();
|
|
@@ -87,6 +90,7 @@ export class ChatCore {
|
|
|
87
90
|
limitResume;
|
|
88
91
|
constructor(options) {
|
|
89
92
|
this.uiQueries = options.intelligentUi ? new ChatUiQueries(options.intelligentUi) : null;
|
|
93
|
+
this.uiFenceLanguage = options.uiFenceLanguage;
|
|
90
94
|
this.providers = options.providers;
|
|
91
95
|
this.store = options.store ?? null;
|
|
92
96
|
this.bookmarks = options.bookmarks ?? null;
|
|
@@ -272,9 +276,11 @@ export class ChatCore {
|
|
|
272
276
|
const references = this.referencesFor(payload.chatId);
|
|
273
277
|
const session = new ChatSession({
|
|
274
278
|
info,
|
|
279
|
+
...(this.uiFenceLanguage ? { uiFenceLanguage: this.uiFenceLanguage } : {}),
|
|
275
280
|
...(this.uiQueries ? { ui: { schemas: this.uiQueries.schemas, observe: (item, final) => this.uiQueries.observe(session, item, final) } } : {}),
|
|
276
281
|
items: stored?.items ?? [],
|
|
277
282
|
preambles: stored?.preambles ?? [],
|
|
283
|
+
uiAccess: stored?.uiAccess ?? {},
|
|
278
284
|
provider,
|
|
279
285
|
command: this.commands[kind] ?? provider.command,
|
|
280
286
|
...(this.spawn ? { spawn: this.spawn } : {}),
|
|
@@ -573,7 +579,10 @@ export class ChatCore {
|
|
|
573
579
|
}
|
|
574
580
|
return this.uiQueries.query(this.require(payload.chatId), payload);
|
|
575
581
|
}
|
|
576
|
-
async choose(payload) {
|
|
582
|
+
async choose(payload, clientId) {
|
|
583
|
+
if (!this.attached.get(payload.chatId)?.has(clientId)) {
|
|
584
|
+
throw new ChatError('refused-query', 'Attach to the chat before answering its UI.');
|
|
585
|
+
}
|
|
577
586
|
const session = this.require(payload.chatId);
|
|
578
587
|
const queries = await this.uiQueries?.choiceValues(session, payload);
|
|
579
588
|
const result = session.choose(payload, queries);
|
|
@@ -837,7 +846,7 @@ export class ChatCore {
|
|
|
837
846
|
const log = this.logs.get(chatId);
|
|
838
847
|
// Not folded here: an older write still in flight may land after this one, and the log is what covers for it.
|
|
839
848
|
try {
|
|
840
|
-
this.store.writeSync(chatId, info, items, { seq: log?.seq ?? 0, resetSeq: log?.resetSeq ?? 0 }, session.preambles, this.recordExtras(chatId));
|
|
849
|
+
this.store.writeSync(chatId, info, items, { seq: log?.seq ?? 0, resetSeq: log?.resetSeq ?? 0 }, session.preambles, this.recordExtras(chatId), session.uiAccessRecord);
|
|
841
850
|
}
|
|
842
851
|
catch (e) {
|
|
843
852
|
console.error(`Chat record for ${chatId} failed:`, errorText(e));
|
|
@@ -970,7 +979,8 @@ export class ChatCore {
|
|
|
970
979
|
// A delta held back is already in the thread, so it gets its seq before the snapshot says where it ends.
|
|
971
980
|
this.coalescers.get(chatId)?.flush();
|
|
972
981
|
const extras = this.recordExtras(chatId);
|
|
973
|
-
const
|
|
982
|
+
const uiAccess = session.uiAccessRecord;
|
|
983
|
+
const unlogged = JSON.stringify([session.preambles, extras, uiAccess]);
|
|
974
984
|
const folds = fold || log.size > COMPACT_ABOVE_BYTES || !log.onDisk;
|
|
975
985
|
// The log already holds every change to the thread, so the whole record is rewritten only to fold
|
|
976
986
|
// the log, on a chat's first write, or for what no event carries.
|
|
@@ -979,7 +989,7 @@ export class ChatCore {
|
|
|
979
989
|
}
|
|
980
990
|
const { info, items } = session.thread.snapshot();
|
|
981
991
|
const at = { seq: log.seq, resetSeq: log.resetSeq };
|
|
982
|
-
this.sizes.set(chatId, await this.store.write(chatId, info, items, at, session.preambles, extras));
|
|
992
|
+
this.sizes.set(chatId, await this.store.write(chatId, info, items, at, session.preambles, extras, uiAccess));
|
|
983
993
|
this.unlogged.set(chatId, unlogged);
|
|
984
994
|
// A chat killed while the write was out has no log left to fold.
|
|
985
995
|
if (this.logs.get(chatId) === log && folds) {
|
|
@@ -32,6 +32,7 @@ export interface ChatSessionOptions {
|
|
|
32
32
|
clock?: OutboxClock;
|
|
33
33
|
items?: ChatItem[];
|
|
34
34
|
preambles?: string[];
|
|
35
|
+
uiAccess?: Readonly<Record<string, unknown>>;
|
|
35
36
|
provider: ChatProvider;
|
|
36
37
|
command: string[];
|
|
37
38
|
env(account: string | undefined): Record<string, string>;
|
|
@@ -43,6 +44,7 @@ export interface ChatSessionOptions {
|
|
|
43
44
|
promptNotes?: PromptNotes;
|
|
44
45
|
references?: ChatReferences;
|
|
45
46
|
checkpoints?: TurnCheckpoints;
|
|
47
|
+
uiFenceLanguage?: string;
|
|
46
48
|
ui?: {
|
|
47
49
|
schemas: UiCompileOptions['querySchemas'];
|
|
48
50
|
observe(item: ChatAssistantItem, final: boolean): void;
|
|
@@ -105,6 +107,11 @@ export declare class ChatSession {
|
|
|
105
107
|
private readonly steering;
|
|
106
108
|
private readonly uiStreams;
|
|
107
109
|
private readonly uiCompiled;
|
|
110
|
+
private readonly capturedUiAccess;
|
|
111
|
+
private readonly uiScanned;
|
|
112
|
+
private readonly uiReferencing;
|
|
113
|
+
private readonly uiAttachments;
|
|
114
|
+
private readonly uiFenceLanguage;
|
|
108
115
|
constructor(options: ChatSessionOptions);
|
|
109
116
|
get uiPreviews(): ChatEvent[];
|
|
110
117
|
previewItems(items: ChatItem[]): ChatItem[];
|
|
@@ -124,6 +131,9 @@ export declare class ChatSession {
|
|
|
124
131
|
queued: boolean;
|
|
125
132
|
turnId: string;
|
|
126
133
|
};
|
|
134
|
+
uiAccess(itemId: string): unknown;
|
|
135
|
+
setUiAccess(itemId: string, access: unknown): void;
|
|
136
|
+
get uiAccessRecord(): Record<string, unknown>;
|
|
127
137
|
updateUiQueries(itemId: string, uiQueries: ChatUiQueryState): void;
|
|
128
138
|
choose(payload: ChatUiChoicePayload, queries?: Readonly<Record<string, unknown>>): {
|
|
129
139
|
queued: boolean;
|
|
@@ -226,7 +236,10 @@ export declare class ChatSession {
|
|
|
226
236
|
private settleFromTranscripts;
|
|
227
237
|
private settleRows;
|
|
228
238
|
private emit;
|
|
239
|
+
private unscannedUiText;
|
|
240
|
+
private forgetUiScan;
|
|
229
241
|
private latestUiAttachment;
|
|
242
|
+
private findUiAttachment;
|
|
230
243
|
private clearUi;
|
|
231
244
|
private delegatingChange;
|
|
232
245
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { GeneratedImageInputSchema, notResumedNote } from '@adecore/agent-contracts';
|
|
3
|
-
import { resolveUiChoice, UiFailure } from '@adecore/intelligent-ui';
|
|
3
|
+
import { resolveUiChoice, UI_FENCE_LANGUAGE, UiFailure, uiHasFence, uiMayReferenceHost } from '@adecore/intelligent-ui';
|
|
4
4
|
import { UiStream } from '@adecore/intelligent-ui/stream';
|
|
5
5
|
import { commandLabel, isBackgroundWork, reportsOnBackgroundWork, runningInBackground } from './background-work.js';
|
|
6
6
|
import { ChatError } from './errors.js';
|
|
7
7
|
import { limitedTurn, limitResumeAt, limitResumeWake } from './limit-resume.js';
|
|
8
8
|
import { isMainAgentOutput, ThreadProjector } from './projector.js';
|
|
9
|
+
import { readableAssistantText } from './readable-text.js';
|
|
9
10
|
import { ChatThread } from './thread.js';
|
|
10
11
|
import { errorText } from '../error-text.js';
|
|
11
12
|
import { systemClock } from '../outbox/outbox-worker.js';
|
|
12
13
|
// Claude Code names a session about six seconds after its first prompt, and a first turn can run for minutes.
|
|
13
14
|
const TITLE_RECHECK_MS = 10_000;
|
|
15
|
+
// Longer than any fence line or the name of a tag a scan looks for, so a match never falls between two deltas.
|
|
16
|
+
const UI_SCAN_OVERLAP = 64;
|
|
14
17
|
/* One per limited turn, so a second fork of the same turn writes no second note. */
|
|
15
18
|
function continuedNoteId(turnId) {
|
|
16
19
|
return `continued-${turnId}`;
|
|
@@ -74,9 +77,17 @@ export class ChatSession {
|
|
|
74
77
|
steering = new Set();
|
|
75
78
|
uiStreams = new Map();
|
|
76
79
|
uiCompiled = new Map();
|
|
80
|
+
capturedUiAccess;
|
|
81
|
+
// How much of a streaming reply's text the fence and host scans already covered, so a delta scans only its tail.
|
|
82
|
+
uiScanned = new Map();
|
|
83
|
+
uiReferencing = new Set();
|
|
84
|
+
uiAttachments = new Map();
|
|
85
|
+
uiFenceLanguage;
|
|
77
86
|
constructor(options) {
|
|
78
87
|
this.options = options;
|
|
88
|
+
this.uiFenceLanguage = options.uiFenceLanguage ?? UI_FENCE_LANGUAGE;
|
|
79
89
|
this.pendingPreambles = options.preambles ?? [];
|
|
90
|
+
this.capturedUiAccess = new Map(Object.entries(options.uiAccess ?? {}));
|
|
80
91
|
this.thread = new ChatThread(options.info, options.items);
|
|
81
92
|
this.projector = new ThreadProjector(this.thread, { providerName: options.provider.name });
|
|
82
93
|
}
|
|
@@ -182,6 +193,21 @@ export class ChatSession {
|
|
|
182
193
|
this.dispatch(text, extras, turnId);
|
|
183
194
|
return { queued: false, turnId };
|
|
184
195
|
}
|
|
196
|
+
/* What a UI host captured for a reply; null where the capture was denied or is still out, undefined where none started. */
|
|
197
|
+
uiAccess(itemId) {
|
|
198
|
+
return this.capturedUiAccess.get(itemId);
|
|
199
|
+
}
|
|
200
|
+
setUiAccess(itemId, access) {
|
|
201
|
+
if (this.frozen) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.capturedUiAccess.set(itemId, access);
|
|
205
|
+
this.options.persistSoon();
|
|
206
|
+
}
|
|
207
|
+
/* The captured access of the replies still in the thread, for the record. */
|
|
208
|
+
get uiAccessRecord() {
|
|
209
|
+
return Object.fromEntries([...this.capturedUiAccess].filter(([itemId]) => this.thread.get(itemId) !== undefined));
|
|
210
|
+
}
|
|
185
211
|
updateUiQueries(itemId, uiQueries) {
|
|
186
212
|
const item = this.thread.get(itemId);
|
|
187
213
|
if (this.frozen || item?.kind !== 'assistant' || item.parentToolUseId) {
|
|
@@ -1598,7 +1624,7 @@ export class ChatSession {
|
|
|
1598
1624
|
this.naming = true;
|
|
1599
1625
|
const turnId = done[0].id;
|
|
1600
1626
|
const answer = items
|
|
1601
|
-
.map((item) => (item.kind === 'assistant' && item.turnId === turnId ? item
|
|
1627
|
+
.map((item) => (item.kind === 'assistant' && item.turnId === turnId ? readableAssistantText(item) : ''))
|
|
1602
1628
|
.filter((text) => text !== '')
|
|
1603
1629
|
.join('\n\n');
|
|
1604
1630
|
const agentSessionId = this.thread.info.agentSessionId;
|
|
@@ -1652,19 +1678,28 @@ export class ChatSession {
|
|
|
1652
1678
|
if (event.type === 'reset') {
|
|
1653
1679
|
this.clearUi();
|
|
1654
1680
|
}
|
|
1681
|
+
if (event.type === 'item' && event.item.kind === 'tool') {
|
|
1682
|
+
this.uiAttachments.delete(event.item.turnId);
|
|
1683
|
+
}
|
|
1655
1684
|
const item = event.type === 'item' ? event.item : event.type === 'delta' ? this.thread.get(event.itemId) : null;
|
|
1656
1685
|
if (item?.kind !== 'assistant' || item.parentToolUseId) {
|
|
1657
1686
|
this.options.emit(event);
|
|
1658
1687
|
continue;
|
|
1659
1688
|
}
|
|
1689
|
+
const tail = this.unscannedUiText(item, event.type === 'item');
|
|
1690
|
+
if (uiMayReferenceHost(tail)) {
|
|
1691
|
+
this.uiReferencing.add(item.id);
|
|
1692
|
+
}
|
|
1660
1693
|
let stream = this.uiStreams.get(item.id);
|
|
1661
|
-
if (!stream && !
|
|
1694
|
+
if (!stream && !uiHasFence(tail, this.uiFenceLanguage)) {
|
|
1695
|
+
this.forgetUiScan(item);
|
|
1662
1696
|
this.options.emit(event);
|
|
1663
1697
|
continue;
|
|
1664
1698
|
}
|
|
1665
1699
|
if (!stream) {
|
|
1666
1700
|
stream = new UiStream({
|
|
1667
1701
|
querySchemas: this.options.ui?.schemas,
|
|
1702
|
+
fenceLanguage: this.uiFenceLanguage,
|
|
1668
1703
|
id: item.id,
|
|
1669
1704
|
clock: this.options.clock ?? systemClock,
|
|
1670
1705
|
emit: (preview) => {
|
|
@@ -1674,7 +1709,10 @@ export class ChatSession {
|
|
|
1674
1709
|
});
|
|
1675
1710
|
this.uiStreams.set(item.id, stream);
|
|
1676
1711
|
}
|
|
1677
|
-
this.
|
|
1712
|
+
if (this.uiReferencing.has(item.id)) {
|
|
1713
|
+
this.options.ui?.observe(item, false);
|
|
1714
|
+
}
|
|
1715
|
+
this.forgetUiScan(item);
|
|
1678
1716
|
const latestAttachment = this.latestUiAttachment(item.turnId);
|
|
1679
1717
|
if (event.type === 'item' && !item.streaming) {
|
|
1680
1718
|
const ui = stream
|
|
@@ -1684,7 +1722,7 @@ export class ChatSession {
|
|
|
1684
1722
|
this.uiCompiled.delete(item.id);
|
|
1685
1723
|
const diagnostics = [...new Set(ui.flatMap((block) => block.diagnostics.map((diagnostic) => diagnostic.message)))].slice(0, 6);
|
|
1686
1724
|
if (diagnostics.length > 0) {
|
|
1687
|
-
const repair = `Your last
|
|
1725
|
+
const repair = `Your last ${this.uiFenceLanguage} block had these diagnostics: ${diagnostics.join(' ')} Use the catalog's supported props and components.`.slice(0, 1200);
|
|
1688
1726
|
if (!this.pendingPreambles.includes(repair)) {
|
|
1689
1727
|
this.pendingPreambles = [...this.pendingPreambles, repair];
|
|
1690
1728
|
}
|
|
@@ -1701,7 +1739,36 @@ export class ChatSession {
|
|
|
1701
1739
|
}
|
|
1702
1740
|
}
|
|
1703
1741
|
}
|
|
1742
|
+
/*
|
|
1743
|
+
* The text a scan has not covered yet, from the start of the line the last scan ended in, since a fence
|
|
1744
|
+
* is one line; a long line is cut short, behind a character no fence or tag can follow.
|
|
1745
|
+
*/
|
|
1746
|
+
unscannedUiText(item, whole) {
|
|
1747
|
+
const scanned = whole ? 0 : (this.uiScanned.get(item.id) ?? 0);
|
|
1748
|
+
this.uiScanned.set(item.id, item.text.length);
|
|
1749
|
+
if (scanned === 0 || scanned > item.text.length) {
|
|
1750
|
+
return item.text;
|
|
1751
|
+
}
|
|
1752
|
+
const line = item.text.lastIndexOf('\n', scanned - 1) + 1;
|
|
1753
|
+
const from = Math.max(line, scanned - UI_SCAN_OVERLAP);
|
|
1754
|
+
return from === line ? item.text.slice(from) : `\0${item.text.slice(from)}`;
|
|
1755
|
+
}
|
|
1756
|
+
forgetUiScan(item) {
|
|
1757
|
+
if (!item.streaming) {
|
|
1758
|
+
this.uiScanned.delete(item.id);
|
|
1759
|
+
this.uiReferencing.delete(item.id);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
/* Looked up once per turn, and again only after one of its tool calls changed. */
|
|
1704
1763
|
latestUiAttachment(turnId) {
|
|
1764
|
+
if (this.uiAttachments.has(turnId)) {
|
|
1765
|
+
return this.uiAttachments.get(turnId);
|
|
1766
|
+
}
|
|
1767
|
+
const attachment = this.findUiAttachment(turnId);
|
|
1768
|
+
this.uiAttachments.set(turnId, attachment);
|
|
1769
|
+
return attachment;
|
|
1770
|
+
}
|
|
1771
|
+
findUiAttachment(turnId) {
|
|
1705
1772
|
for (const item of this.thread.list().toReversed()) {
|
|
1706
1773
|
if (item.kind !== 'tool' || item.turnId !== turnId || item.name !== 'ImageGeneration' || item.state !== 'done') {
|
|
1707
1774
|
continue;
|
|
@@ -1719,6 +1786,9 @@ export class ChatSession {
|
|
|
1719
1786
|
}
|
|
1720
1787
|
this.uiStreams.clear();
|
|
1721
1788
|
this.uiCompiled.clear();
|
|
1789
|
+
this.uiScanned.clear();
|
|
1790
|
+
this.uiReferencing.clear();
|
|
1791
|
+
this.uiAttachments.clear();
|
|
1722
1792
|
}
|
|
1723
1793
|
/* Said on the info as well as by the rows, since a client with the thread closed sees only the info. */
|
|
1724
1794
|
delegatingChange(events) {
|
|
@@ -8,6 +8,7 @@ export interface ChatRecord {
|
|
|
8
8
|
seq: number;
|
|
9
9
|
resetSeq: number;
|
|
10
10
|
preambles: string[];
|
|
11
|
+
uiAccess: Record<string, unknown>;
|
|
11
12
|
extras: ChatRecordExtras;
|
|
12
13
|
lines: ChatLogLine[];
|
|
13
14
|
}
|
|
@@ -28,8 +29,8 @@ export declare class ChatStore {
|
|
|
28
29
|
list(): Promise<string[]>;
|
|
29
30
|
has(chatId: string): Promise<boolean>;
|
|
30
31
|
discardLog(chatId: string): Promise<number>;
|
|
31
|
-
write(chatId: string, info: ChatInfo, items: ChatItem[], at?: ChatSeq, preambles?: readonly string[], extras?: ChatRecordExtras): Promise<number>;
|
|
32
|
-
writeSync(chatId: string, info: ChatInfo, items: ChatItem[], at?: ChatSeq, preambles?: readonly string[], extras?: ChatRecordExtras): void;
|
|
32
|
+
write(chatId: string, info: ChatInfo, items: ChatItem[], at?: ChatSeq, preambles?: readonly string[], extras?: ChatRecordExtras, uiAccess?: Readonly<Record<string, unknown>>): Promise<number>;
|
|
33
|
+
writeSync(chatId: string, info: ChatInfo, items: ChatItem[], at?: ChatSeq, preambles?: readonly string[], extras?: ChatRecordExtras, uiAccess?: Readonly<Record<string, unknown>>): void;
|
|
33
34
|
read(chatId: string): Promise<ChatRecord | null>;
|
|
34
35
|
unreadable(chatId: string): Promise<boolean>;
|
|
35
36
|
private readSnapshot;
|
package/dist/chat/chat-store.js
CHANGED
|
@@ -19,17 +19,40 @@ const RecordSchema = z.looseObject({
|
|
|
19
19
|
seq: z.number().int().nonnegative().optional(),
|
|
20
20
|
resetSeq: z.number().int().nonnegative().optional(),
|
|
21
21
|
// Never on the wire: what goes in front of the next real prompt, once (a fork's note for its agent).
|
|
22
|
-
preambles: z.array(z.string()).optional()
|
|
22
|
+
preambles: z.array(z.string()).optional(),
|
|
23
|
+
// Never on the wire: the access a UI host captured for each reply that reads it, by item id.
|
|
24
|
+
uiAccess: z.record(z.string(), z.unknown()).optional()
|
|
23
25
|
});
|
|
24
|
-
const OWN_KEYS = new Set(['info', 'items', 'seq', 'resetSeq', 'preambles']);
|
|
26
|
+
const OWN_KEYS = new Set(['info', 'items', 'seq', 'resetSeq', 'preambles', 'uiAccess']);
|
|
25
27
|
function extrasOf(record) {
|
|
26
28
|
return Object.fromEntries(Object.entries(record).filter(([key]) => !OWN_KEYS.has(key)));
|
|
27
29
|
}
|
|
28
30
|
function logName(chatId) {
|
|
29
31
|
return `${encodeURIComponent(chatId)}.log`;
|
|
30
32
|
}
|
|
31
|
-
function recordBody(info, items, at, preambles, extras) {
|
|
32
|
-
return JSON.stringify({
|
|
33
|
+
function recordBody(info, items, at, preambles, extras, uiAccess) {
|
|
34
|
+
return JSON.stringify({
|
|
35
|
+
info,
|
|
36
|
+
items,
|
|
37
|
+
...at,
|
|
38
|
+
...(preambles.length === 0 ? {} : { preambles }),
|
|
39
|
+
...(Object.keys(uiAccess).length === 0 ? {} : { uiAccess }),
|
|
40
|
+
...withoutOwnKeys(extras)
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/* A record from before `uiAccess` kept the access inside each reply's `uiQueries`, which the item schema now drops. */
|
|
44
|
+
function liftUiAccess(record) {
|
|
45
|
+
if (typeof record !== 'object' || record === null || 'uiAccess' in record || !Array.isArray(record.items)) {
|
|
46
|
+
return record;
|
|
47
|
+
}
|
|
48
|
+
const uiAccess = {};
|
|
49
|
+
for (const item of record.items) {
|
|
50
|
+
const queries = item?.uiQueries;
|
|
51
|
+
if (typeof queries === 'object' && queries !== null && 'access' in queries) {
|
|
52
|
+
uiAccess[String(item.id)] = queries.access;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Object.keys(uiAccess).length === 0 ? record : { ...record, uiAccess };
|
|
33
56
|
}
|
|
34
57
|
function withoutOwnKeys(extras) {
|
|
35
58
|
return Object.fromEntries(Object.entries(extras).filter(([key]) => !OWN_KEYS.has(key)));
|
|
@@ -80,8 +103,8 @@ export class ChatStore {
|
|
|
80
103
|
return lines.at(-1)?.seq ?? 0;
|
|
81
104
|
}
|
|
82
105
|
/* Answers how many bytes the record took, which is what tells a caller whether writing it is cheap. */
|
|
83
|
-
async write(chatId, info, items, at = { seq: 0, resetSeq: 0 }, preambles = [], extras = {}) {
|
|
84
|
-
const body = recordBody(info, items, at, preambles, extras);
|
|
106
|
+
async write(chatId, info, items, at = { seq: 0, resetSeq: 0 }, preambles = [], extras = {}, uiAccess = {}) {
|
|
107
|
+
const body = recordBody(info, items, at, preambles, extras, uiAccess);
|
|
85
108
|
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
86
109
|
await writeAtomic(join(this.dir, recordFileName(chatId)), body);
|
|
87
110
|
return body.length;
|
|
@@ -90,9 +113,9 @@ export class ChatStore {
|
|
|
90
113
|
* The same write without a turn of the event loop. A `bun --watch` reload restarts the module
|
|
91
114
|
* before an awaited write comes back, so a shutdown has to put the threads down synchronously.
|
|
92
115
|
*/
|
|
93
|
-
writeSync(chatId, info, items, at = { seq: 0, resetSeq: 0 }, preambles = [], extras = {}) {
|
|
116
|
+
writeSync(chatId, info, items, at = { seq: 0, resetSeq: 0 }, preambles = [], extras = {}, uiAccess = {}) {
|
|
94
117
|
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
95
|
-
writeAtomicSync(join(this.dir, recordFileName(chatId)), recordBody(info, items, at, preambles, extras));
|
|
118
|
+
writeAtomicSync(join(this.dir, recordFileName(chatId)), recordBody(info, items, at, preambles, extras, uiAccess));
|
|
96
119
|
}
|
|
97
120
|
/* Null for a chat with nothing on disk to make it of, and for one whose record does not parse (see `unreadable`). */
|
|
98
121
|
async read(chatId) {
|
|
@@ -117,7 +140,15 @@ export class ChatStore {
|
|
|
117
140
|
resetSeq = line.seq;
|
|
118
141
|
}
|
|
119
142
|
}
|
|
120
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
...thread.snapshot(),
|
|
145
|
+
seq,
|
|
146
|
+
resetSeq,
|
|
147
|
+
preambles: snapshot.preambles ?? [],
|
|
148
|
+
uiAccess: snapshot.uiAccess ?? {},
|
|
149
|
+
extras: extrasOf(snapshot),
|
|
150
|
+
lines
|
|
151
|
+
};
|
|
121
152
|
}
|
|
122
153
|
/*
|
|
123
154
|
* Whether a record is on disk that this host cannot read: one a newer version wrote, one cut short,
|
|
@@ -146,7 +177,7 @@ export class ChatStore {
|
|
|
146
177
|
await writeAtomic(join(this.dir, recordFileName(chatId)), JSON.stringify(migrated));
|
|
147
178
|
}
|
|
148
179
|
}
|
|
149
|
-
const parsed = RecordSchema.safeParse(record);
|
|
180
|
+
const parsed = RecordSchema.safeParse(liftUiAccess(record));
|
|
150
181
|
return parsed.success ? parsed.data : UNREADABLE;
|
|
151
182
|
}
|
|
152
183
|
async delete(chatId) {
|
|
@@ -199,5 +230,5 @@ function fromLogAlone(lines) {
|
|
|
199
230
|
resetSeq = line.seq;
|
|
200
231
|
}
|
|
201
232
|
}
|
|
202
|
-
return { ...thread.snapshot(), seq: 0, resetSeq, preambles: [], extras: {}, lines };
|
|
233
|
+
return { ...thread.snapshot(), seq: 0, resetSeq, preambles: [], uiAccess: {}, extras: {}, lines };
|
|
203
234
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { uiFallbackText } from '@adecore/intelligent-ui/text';
|
|
2
|
+
/*
|
|
3
|
+
* An assistant's words as another agent or a person outside the thread reads them: each UI block becomes
|
|
4
|
+
* its text fallback, with the first query values when they were frozen for the block as stored. It works
|
|
5
|
+
* from the stored block ranges, so it holds whatever fence language the host compiled.
|
|
6
|
+
*/
|
|
7
|
+
export function readableAssistantText(item) {
|
|
8
|
+
if (!item.ui?.length) {
|
|
9
|
+
return item.text;
|
|
10
|
+
}
|
|
11
|
+
const blocks = item.ui.map((block) => {
|
|
12
|
+
const frozen = item.uiQueries?.blocks[block.id];
|
|
13
|
+
return frozen && frozen.revision === block.revision ? { ...block, fallback: frozen.fallback } : block;
|
|
14
|
+
});
|
|
15
|
+
return uiFallbackText(item.text, blocks);
|
|
16
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { ChatUiLinkPayload, ChatUiLinkReading, ChatAssistantItem, ChatInfo,
|
|
|
3
3
|
import { type UiValue } from '@adecore/intelligent-ui';
|
|
4
4
|
import { type UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
5
5
|
import type { ChatSession } from './chat-session.ts';
|
|
6
|
+
import { ChatError } from './errors.ts';
|
|
6
7
|
export interface ChatUiSource {
|
|
7
8
|
args: z.ZodType;
|
|
8
9
|
result: z.ZodType;
|
|
@@ -14,11 +15,19 @@ export interface ChatUiHost {
|
|
|
14
15
|
sources: Readonly<Record<string, ChatUiSource>>;
|
|
15
16
|
link?(info: ChatInfo, access: unknown, target: UiLinkTarget): Promise<ChatUiLinkReading>;
|
|
16
17
|
}
|
|
18
|
+
declare class UiQueryError extends ChatError<string> {
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
constructor(code: string, reason: string, message: string);
|
|
21
|
+
}
|
|
22
|
+
export declare class ChatUiRefusal extends UiQueryError {
|
|
23
|
+
constructor(code: string, reason: string);
|
|
24
|
+
}
|
|
17
25
|
export declare class ChatUiQueries {
|
|
18
26
|
readonly schemas: Record<string, z.ZodType>;
|
|
19
27
|
private readonly captures;
|
|
20
28
|
private readonly freezing;
|
|
21
29
|
private readonly cache;
|
|
30
|
+
private readonly latest;
|
|
22
31
|
private readonly reading;
|
|
23
32
|
private readonly attempts;
|
|
24
33
|
private readonly active;
|
|
@@ -33,6 +42,8 @@ export declare class ChatUiQueries {
|
|
|
33
42
|
choiceValues(session: ChatSession, payload: ChatUiChoicePayload | ChatUiLinkPayload): Promise<Record<string, unknown>>;
|
|
34
43
|
link(session: ChatSession, payload: ChatUiLinkPayload): Promise<ChatUiLinkReading>;
|
|
35
44
|
forget(chatId: string): void;
|
|
45
|
+
private evict;
|
|
36
46
|
private linkReading;
|
|
37
|
-
private
|
|
47
|
+
private failure;
|
|
38
48
|
}
|
|
49
|
+
export {};
|