@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.
@@ -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
  }>;
@@ -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
- return (item.text.trim() === '' ? files : item.text).replace(/\s+/g, ' ').trim();
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 unlogged = JSON.stringify([session.preambles, extras]);
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.text : ''))
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 && !item.text.includes('ruimte-ui')) {
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.options.ui?.observe(item, false);
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 ruimte-ui block had these diagnostics: ${diagnostics.join(' ')} Use the catalog's supported props and components.`.slice(0, 1200);
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;
@@ -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({ info, items, ...at, ...(preambles.length === 0 ? {} : { preambles }), ...withoutOwnKeys(extras) });
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 { ...thread.snapshot(), seq, resetSeq, preambles: snapshot.preambles ?? [], extras: extrasOf(snapshot), lines };
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,2 @@
1
+ import type { ChatAssistantItem } from '@adecore/agent-contracts';
2
+ export declare function readableAssistantText(item: Pick<ChatAssistantItem, 'text' | 'ui' | 'uiQueries'>): string;
@@ -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 reason;
47
+ private failure;
38
48
  }
49
+ export {};