@adecore/agents 0.20.0 → 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.
Files changed (42) hide show
  1. package/dist/chat/attachment-store.d.ts +2 -0
  2. package/dist/chat/attachment-store.js +38 -2
  3. package/dist/chat/backend.d.ts +13 -1
  4. package/dist/chat/chat-core.d.ts +53 -1
  5. package/dist/chat/chat-core.js +53 -10
  6. package/dist/chat/chat-session.d.ts +29 -2
  7. package/dist/chat/chat-session.js +258 -15
  8. package/dist/chat/claude-backend.d.ts +4 -0
  9. package/dist/chat/claude-backend.js +39 -0
  10. package/dist/chat/claude-protocol.d.ts +2 -0
  11. package/dist/chat/claude-protocol.js +12 -1
  12. package/dist/chat/codex-backend.d.ts +4 -0
  13. package/dist/chat/codex-backend.js +77 -5
  14. package/dist/chat/codex-protocol.d.ts +1 -0
  15. package/dist/chat/codex-protocol.js +74 -5
  16. package/dist/chat/delta-coalescer.js +1 -1
  17. package/dist/chat/errors.d.ts +1 -1
  18. package/dist/chat/fake-codex.js +33 -0
  19. package/dist/chat/generated-image.d.ts +6 -0
  20. package/dist/chat/generated-image.js +74 -0
  21. package/dist/chat/projector.js +9 -2
  22. package/dist/chat/thread.js +6 -0
  23. package/dist/chat/ui-queries.d.ts +38 -0
  24. package/dist/chat/ui-queries.js +311 -0
  25. package/dist/host/handlers.js +4 -1
  26. package/package.json +5 -4
  27. package/src/chat/attachment-store.ts +38 -2
  28. package/src/chat/backend.ts +16 -2
  29. package/src/chat/chat-core.ts +60 -9
  30. package/src/chat/chat-session.ts +294 -16
  31. package/src/chat/claude-backend.ts +40 -0
  32. package/src/chat/claude-protocol.ts +16 -1
  33. package/src/chat/codex-backend.ts +77 -5
  34. package/src/chat/codex-protocol.ts +78 -6
  35. package/src/chat/delta-coalescer.ts +1 -1
  36. package/src/chat/errors.ts +2 -0
  37. package/src/chat/fake-codex.ts +34 -0
  38. package/src/chat/generated-image.ts +88 -0
  39. package/src/chat/projector.ts +9 -2
  40. package/src/chat/thread.ts +6 -0
  41. package/src/chat/ui-queries.ts +360 -0
  42. package/src/host/handlers.ts +9 -1
@@ -39,6 +39,7 @@ export class CodexBackend {
39
39
  acknowledgesTurns = true;
40
40
  promptId = null;
41
41
  launch;
42
+ imageWrites = null;
42
43
  host;
43
44
  protocol;
44
45
  client;
@@ -114,7 +115,7 @@ export class CodexBackend {
114
115
  this.emit(event);
115
116
  }
116
117
  }
117
- sendTurn(input) {
118
+ turnInput(input) {
118
119
  const images = input.attachments.filter((attachment) => attachmentImageMime(attachment) !== null);
119
120
  if (images.length > 0 && this.imageInputSupported === false) {
120
121
  throw new Error(`${this.launch.modelName} does not support image input. Choose a model that accepts images.`);
@@ -140,18 +141,40 @@ export class CodexBackend {
140
141
  if (note !== '') {
141
142
  parts.push(`\n\n${note}`);
142
143
  }
144
+ return [...textInput(parts.join('')), ...images.map((attachment) => ({ type: 'localImage', path: attachment.path }))];
145
+ }
146
+ sendTurn(input) {
147
+ const nativeInput = this.turnInput(input);
143
148
  const effort = this.launch.selection.options.effort;
144
149
  const tier = codexServiceTier(this.launch.selection);
145
150
  this.turnUnnamed = true;
146
151
  this.promptId = input.promptId ?? null;
147
152
  this.request('turn/start', {
148
153
  threadId: this.threadId,
149
- input: [...textInput(parts.join('')), ...images.map((attachment) => ({ type: 'localImage', path: attachment.path }))],
154
+ input: nativeInput,
150
155
  model: this.launch.selection.model,
151
156
  ...(typeof effort === 'string' ? { effort } : {}),
152
157
  ...(tier === null ? {} : { serviceTier: tier })
153
158
  }, () => this.acceptPrompt(input.promptId));
154
159
  }
160
+ async steerTurn(input) {
161
+ const transport = this.transport;
162
+ const turnId = this.protocol.turnId;
163
+ if (!transport || !turnId || this.turnUnnamed) {
164
+ return false;
165
+ }
166
+ try {
167
+ await transport.request('turn/steer', { threadId: this.threadId, expectedTurnId: turnId, input: this.turnInput(input) });
168
+ return true;
169
+ }
170
+ catch (error) {
171
+ const rejected = error instanceof Error && error.name === 'CodexRpcError';
172
+ if (rejected && /no active turn|not.*active|turn.*mismatch|expected.*turn|method not found/i.test(error.message)) {
173
+ return false;
174
+ }
175
+ throw error;
176
+ }
177
+ }
155
178
  acceptPrompt(promptId) {
156
179
  if (promptId !== undefined && this.promptId === promptId) {
157
180
  this.emit({ type: 'turn.accepted', promptId });
@@ -282,9 +305,9 @@ export class CodexBackend {
282
305
  this.request('turn/steer', { threadId: this.threadId, expectedTurnId: turnId, input: textInput(answer.text) });
283
306
  return true;
284
307
  }
285
- /* Codex asks its async question once and waits; forgetting it locally is all a dismissal is. */
286
308
  dismissRequest(requestId) {
287
- return this.protocol.dismissQuestion(requestId);
309
+ const answer = this.protocol.dismissQuestionAnswer(requestId);
310
+ return answer?.kind === 'respond' ? this.respond(requestId, answer.rpcId, answer.result) : this.protocol.dismissQuestion(requestId);
288
311
  }
289
312
  declineRequest(requestId, _message) {
290
313
  const answer = this.protocol.declineRequest(requestId);
@@ -299,7 +322,7 @@ export class CodexBackend {
299
322
  dispose() {
300
323
  const transport = this.transport;
301
324
  this.transport = null;
302
- return transport?.dispose() ?? Promise.resolve();
325
+ return Promise.all([transport?.dispose(), this.imageWrites]).then(() => undefined);
303
326
  }
304
327
  /* A request whose failure the person has to hear about, since it is the turn that cannot go on. */
305
328
  request(method, params, accepted) {
@@ -359,6 +382,55 @@ export class CodexBackend {
359
382
  this.interruptOwed = false;
360
383
  this.promptId = null;
361
384
  }
385
+ if (event.type === 'image.generated' || this.imageWrites !== null) {
386
+ const pending = (this.imageWrites ?? Promise.resolve()).then(async () => {
387
+ if (event.type === 'image.generated') {
388
+ await this.storeImage(event);
389
+ }
390
+ else {
391
+ this.host.onEvent(event);
392
+ }
393
+ });
394
+ this.imageWrites = pending;
395
+ void pending
396
+ .finally(() => {
397
+ if (this.imageWrites === pending) {
398
+ this.imageWrites = null;
399
+ }
400
+ })
401
+ .catch(() => undefined);
402
+ return;
403
+ }
362
404
  this.host.onEvent(event);
363
405
  }
406
+ async storeImage(event) {
407
+ const input = { revisedPrompt: event.revisedPrompt, transparentBackground: event.transparentBackground };
408
+ let attachment;
409
+ try {
410
+ if (!this.launch.saveGeneratedImage) {
411
+ throw new Error('This machine cannot save generated images');
412
+ }
413
+ attachment = await this.launch.saveGeneratedImage(event.ref, event.upload);
414
+ }
415
+ catch (error) {
416
+ this.host.onEvent({ type: 'tool.started', ref: event.ref, name: 'ImageGeneration', input, parentRef: null });
417
+ this.host.onEvent({ type: 'tool.done', ref: event.ref, output: errorText(error), state: 'error' });
418
+ return;
419
+ }
420
+ this.host.onEvent({
421
+ type: 'tool.started',
422
+ ref: event.ref,
423
+ name: 'ImageGeneration',
424
+ input: {
425
+ ...input,
426
+ attachment: {
427
+ ...attachment,
428
+ ...(event.width === undefined ? {} : { width: event.width }),
429
+ ...(event.height === undefined ? {} : { height: event.height })
430
+ }
431
+ },
432
+ parentRef: null
433
+ });
434
+ this.host.onEvent({ type: 'tool.done', ref: event.ref, output: null, state: 'done' });
435
+ }
364
436
  }
@@ -29,6 +29,7 @@ export declare class CodexProtocol {
29
29
  result: unknown;
30
30
  } | null;
31
31
  questionAnswer(requestId: string, answers: Record<string, string>): CodexAnswer | null;
32
+ dismissQuestionAnswer(requestId: string): CodexAnswer | null;
32
33
  dismissQuestion(requestId: string): boolean;
33
34
  forgetPending(): void;
34
35
  declineRequest(requestId: string): {
@@ -1,3 +1,4 @@
1
+ import { generatedImage } from './generated-image.js';
1
2
  import { readCodexLimits } from '../usage/limits/normalize.js';
2
3
  import { codexRuntimeMode } from '../providers/codex.js';
3
4
  import { cleanTitle } from '../title-file.js';
@@ -315,6 +316,13 @@ export class CodexProtocol {
315
316
  /* The reply that settles an approval, or null when nothing waits under that id. */
316
317
  approvalDecision(requestId, decision) {
317
318
  const pending = this.pending.get(requestId);
319
+ if (pending?.type === 'permissions') {
320
+ this.pending.delete(requestId);
321
+ return {
322
+ rpcId: pending.rpcId,
323
+ result: { permissions: decision === 'deny' ? {} : pending.permissions, scope: decision === 'allow-always' ? 'session' : 'turn' }
324
+ };
325
+ }
318
326
  if (pending?.type === 'elicitation') {
319
327
  this.pending.delete(requestId);
320
328
  return { rpcId: pending.rpcId, result: elicitationAnswer(decision, pending.persist) };
@@ -346,6 +354,10 @@ export class CodexProtocol {
346
354
  }
347
355
  return { kind: 'respond', rpcId: pending.rpcId, result: { answers: byId } };
348
356
  }
357
+ dismissQuestionAnswer(requestId) {
358
+ const pending = this.pending.get(requestId);
359
+ return pending?.type === 'question' && pending.async === true && pending.rpcId !== null ? this.questionAnswer(requestId, {}) : null;
360
+ }
349
361
  /* Drops an asynchronous question the person walked away from; a blocking one has to be answered. */
350
362
  dismissQuestion(requestId) {
351
363
  const pending = this.pending.get(requestId);
@@ -427,8 +439,39 @@ export class CodexProtocol {
427
439
  if (questions.length === 0) {
428
440
  return;
429
441
  }
430
- this.pending.set(requestId, { ...this.owner(params), type: 'question', rpcId, questionIds: questions.map((question) => question.id) });
431
- events.push({ type: 'question.requested', requestId, questions, ...(this.isChildRequest(params) ? { background: true } : {}) });
442
+ this.pending.set(requestId, {
443
+ ...this.owner(params),
444
+ type: 'question',
445
+ rpcId,
446
+ questionIds: questions.map((question) => question.id),
447
+ async: params.isBlocking === false
448
+ });
449
+ events.push({
450
+ type: 'question.requested',
451
+ requestId,
452
+ questions,
453
+ ...(params.isBlocking === false ? { async: true } : {}),
454
+ ...(this.isChildRequest(params) ? { background: true } : {})
455
+ });
456
+ return;
457
+ }
458
+ if (method === 'item/permissions/requestApproval') {
459
+ if (!isRecord(params.permissions)) {
460
+ return;
461
+ }
462
+ const permissions = params.permissions;
463
+ this.pending.set(requestId, { ...this.owner(params), type: 'permissions', rpcId, permissions });
464
+ events.push({
465
+ type: 'approval.requested',
466
+ requestId,
467
+ ref: str(params.itemId),
468
+ toolName: 'RequestPermissions',
469
+ input: { cwd: params.cwd, permissions },
470
+ description: this.requestDescription(params, str(params.reason)),
471
+ canAllowAlways: true,
472
+ allowAlways: { label: 'Allow for this session', description: 'Grant these permissions for the rest of this session.' },
473
+ ...(this.isChildRequest(params) ? { background: true } : {})
474
+ });
432
475
  return;
433
476
  }
434
477
  if (method === 'mcpServer/elicitation/request') {
@@ -525,7 +568,7 @@ export class CodexProtocol {
525
568
  events.push({ type: 'question.requested', requestId: ref, questions, async: true });
526
569
  return;
527
570
  }
528
- this.text(ref, str(item.text) ?? '', completed, events);
571
+ this.text(ref, str(item.text) ?? '', completed, events, str(item.phase) ?? undefined);
529
572
  return;
530
573
  }
531
574
  case 'plan':
@@ -574,6 +617,32 @@ export class CodexProtocol {
574
617
  case 'dynamicToolCall':
575
618
  this.tool(ref, str(item.tool) ?? 'tool', item.arguments ?? {}, completed, textOf(item.contentItems), item.success !== false, events);
576
619
  return;
620
+ case 'imageGeneration': {
621
+ const revisedPrompt = str(item.revisedPrompt) ?? undefined;
622
+ const transparentBackground = typeof item.transparentBackground === 'boolean' ? item.transparentBackground : undefined;
623
+ const input = {
624
+ ...(revisedPrompt === undefined ? {} : { revisedPrompt }),
625
+ ...(transparentBackground === undefined ? {} : { transparentBackground })
626
+ };
627
+ if (!completed) {
628
+ this.tool(ref, 'ImageGeneration', input, false, '', true, events);
629
+ return;
630
+ }
631
+ try {
632
+ if (item.failure || item.status !== 'completed') {
633
+ throw new Error(isRecord(item.failure) ? (str(item.failure.message) ?? 'Image generation failed') : (str(item.failure) ?? 'Image generation failed'));
634
+ }
635
+ const image = generatedImage(str(item.result) ?? '', revisedPrompt);
636
+ events.push({ type: 'image.generated', ref, ...image, ...input });
637
+ }
638
+ catch (error) {
639
+ this.tool(ref, 'ImageGeneration', input, true, error instanceof Error ? error.message : 'Image generation failed', false, events);
640
+ }
641
+ return;
642
+ }
643
+ case 'imageView':
644
+ this.tool(ref, 'ImageView', { path: str(item.path) ?? '' }, completed, '', true, events);
645
+ return;
577
646
  case 'webSearch': {
578
647
  const action = isRecord(item.action) ? item.action : {};
579
648
  this.tool(ref, 'WebSearch', { query: str(item.query) ?? str(action.query) ?? '' }, completed, '', true, events);
@@ -641,8 +710,8 @@ export class CodexProtocol {
641
710
  }
642
711
  return null;
643
712
  }
644
- text(ref, text, completed, events) {
645
- events.push(completed ? { type: 'text.done', ref, text } : { type: 'text.delta', ref, text });
713
+ text(ref, text, completed, events, phase) {
714
+ events.push({ type: completed ? 'text.done' : 'text.delta', ref, text, ...(phase === undefined ? {} : { phase }) });
646
715
  }
647
716
  /*
648
717
  * An agent Codex spawned, as a spawnAgent call reports it. It works in a thread of its own, so this row
@@ -16,7 +16,7 @@ export class DeltaCoalescer {
16
16
  this.tickMs = tickMs;
17
17
  }
18
18
  push(event) {
19
- if (event.type !== 'delta') {
19
+ if (event.type !== 'delta' || event.uiQueries !== undefined) {
20
20
  this.flush();
21
21
  this.send(event);
22
22
  return;
@@ -1,4 +1,4 @@
1
1
  import { CodedError } from '../coded-error.ts';
2
- export type ChatErrorCode = 'history-expired' | 'chat-not-found' | 'chat-unreadable' | 'chat-busy' | 'request-not-found' | 'chat-unsupported' | 'invalid-attachments' | 'subagent-not-found' | 'task-not-found' | 'transcript-missing' | 'transcript-format' | 'fork-failed' | 'turn-not-found' | 'item-not-found' | 'bookmark-not-found' | 'too-many-bookmarks' | 'visual-invalid' | 'visual-too-large';
2
+ export type ChatErrorCode = 'refused-query' | 'stale-ui-query' | 'history-expired' | 'chat-not-found' | 'chat-unreadable' | 'chat-busy' | 'request-not-found' | 'chat-unsupported' | 'invalid-attachments' | 'subagent-not-found' | 'task-not-found' | 'transcript-missing' | 'transcript-format' | 'fork-failed' | 'turn-not-found' | 'item-not-found' | 'bookmark-not-found' | 'too-many-bookmarks' | 'visual-invalid' | 'visual-too-large';
3
3
  export declare class ChatError<TCode extends string = ChatErrorCode> extends CodedError<TCode> {
4
4
  }
@@ -169,6 +169,39 @@ export function fakeCodexWith(options) {
169
169
  turnCompleted('failed', "You've hit your usage limit.", 'usageLimitExceeded');
170
170
  return;
171
171
  }
172
+ if (text.startsWith('image:')) {
173
+ const scenario = text.slice(6);
174
+ const entry = item('imageGeneration', {
175
+ status: 'inProgress',
176
+ result: '',
177
+ savedPath: '/never-read-generated-image.png',
178
+ revisedPrompt: 'A rabbit',
179
+ transparentBackground: true
180
+ });
181
+ started(entry);
182
+ const result = scenario === 'empty'
183
+ ? ''
184
+ : scenario === 'invalid'
185
+ ? 'not base64'
186
+ : scenario === 'not-image'
187
+ ? Buffer.from('text').toString('base64')
188
+ : scenario === 'too-large'
189
+ ? Buffer.alloc(10 * 1024 * 1024 + 1).toString('base64')
190
+ : 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=';
191
+ const done = {
192
+ ...entry,
193
+ status: scenario === 'failed' ? 'failed' : 'completed',
194
+ result,
195
+ ...(scenario === 'failed' ? { failure: { message: 'Image generation failed' } } : {})
196
+ };
197
+ completed(done);
198
+ if (scenario === 'replay') {
199
+ completed(done);
200
+ }
201
+ agentMessage('Here is the generated image.');
202
+ turnCompleted('completed');
203
+ return;
204
+ }
172
205
  if (text === 'slow') {
173
206
  slow = true;
174
207
  started(item('agentMessage', { text: '', phase: 'final_answer', memoryCitation: null, delivery: null, questions: null }));
@@ -0,0 +1,6 @@
1
+ import { type ChatAttachmentUpload } from '@adecore/agent-contracts';
2
+ export declare function generatedImage(data: string, prompt?: string): {
3
+ upload: ChatAttachmentUpload;
4
+ width?: number;
5
+ height?: number;
6
+ };
@@ -0,0 +1,74 @@
1
+ import { ChatAttachmentUploadSchema } from '@adecore/agent-contracts';
2
+ function imageInfo(bytes) {
3
+ if (bytes.length >= 33 &&
4
+ bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) &&
5
+ bytes.readUInt32BE(8) === 13 &&
6
+ bytes.toString('ascii', 12, 16) === 'IHDR') {
7
+ return { mime: 'image/png', extension: 'png', width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
8
+ }
9
+ if (bytes.length >= 13 && /GIF8[79]a/.test(bytes.toString('ascii', 0, 6))) {
10
+ return { mime: 'image/gif', extension: 'gif', width: bytes.readUInt16LE(6), height: bytes.readUInt16LE(8) };
11
+ }
12
+ if (bytes.length >= 20 &&
13
+ bytes.toString('ascii', 0, 4) === 'RIFF' &&
14
+ bytes.toString('ascii', 8, 12) === 'WEBP' &&
15
+ bytes.readUInt32LE(4) + 8 === bytes.length) {
16
+ const kind = bytes.toString('ascii', 12, 16);
17
+ if (kind === 'VP8X' && bytes.length >= 30) {
18
+ return { mime: 'image/webp', extension: 'webp', width: bytes.readUIntLE(24, 3) + 1, height: bytes.readUIntLE(27, 3) + 1 };
19
+ }
20
+ if (kind === 'VP8 ' && bytes.length >= 30 && bytes.subarray(23, 26).equals(Buffer.from([157, 1, 42]))) {
21
+ return { mime: 'image/webp', extension: 'webp', width: bytes.readUInt16LE(26) & 0x3fff, height: bytes.readUInt16LE(28) & 0x3fff };
22
+ }
23
+ if (kind === 'VP8L' && bytes.length >= 25 && bytes[20] === 0x2f) {
24
+ const size = bytes.readUInt32LE(21);
25
+ return { mime: 'image/webp', extension: 'webp', width: (size & 0x3fff) + 1, height: ((size >> 14) & 0x3fff) + 1 };
26
+ }
27
+ return null;
28
+ }
29
+ if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
30
+ let offset = 2;
31
+ while (offset + 4 <= bytes.length && bytes[offset] === 255) {
32
+ const marker = bytes[offset + 1];
33
+ if (marker === 218 || marker === 217) {
34
+ break;
35
+ }
36
+ if (marker === 255) {
37
+ offset++;
38
+ continue;
39
+ }
40
+ const size = bytes.readUInt16BE(offset + 2);
41
+ if (size < 2 || offset + 2 + size > bytes.length) {
42
+ return null;
43
+ }
44
+ if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker) && size >= 8) {
45
+ return { mime: 'image/jpeg', extension: 'jpg', width: bytes.readUInt16BE(offset + 7), height: bytes.readUInt16BE(offset + 5) };
46
+ }
47
+ offset += size + 2;
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ export function generatedImage(data, prompt) {
53
+ const parsed = ChatAttachmentUploadSchema.safeParse({ name: 'image', mime: 'application/octet-stream', data });
54
+ if (!parsed.success) {
55
+ throw new Error('The generated image is empty, invalid base64 or exceeds 10 MiB');
56
+ }
57
+ const bytes = Buffer.from(data, 'base64');
58
+ // Buffer's decoder accepts malformed padding and trailing bits; the protocol requires canonical base64.
59
+ if (bytes.toString('base64') !== data) {
60
+ throw new Error('The generated image contains invalid base64');
61
+ }
62
+ const info = imageInfo(bytes);
63
+ if (info === null || (info.width !== undefined && info.width === 0) || (info.height !== undefined && info.height === 0)) {
64
+ throw new Error('The generated bytes are not a supported image');
65
+ }
66
+ const stem = (prompt ?? '')
67
+ .normalize('NFKD')
68
+ .replace(/[^a-zA-Z0-9]+/g, '-')
69
+ .replace(/^-|-$/g, '')
70
+ .slice(0, 80)
71
+ .toLowerCase() || 'generated-image';
72
+ const upload = { name: `${stem}.${info.extension}`, mime: info.mime, data };
73
+ return { upload, ...(info.width === undefined ? {} : { width: info.width }), ...(info.height === undefined ? {} : { height: info.height }) };
74
+ }
@@ -150,7 +150,7 @@ export class ThreadProjector {
150
150
  break;
151
151
  case 'text.delta':
152
152
  this.closeThinking(events);
153
- this.appendText(this.itemId(generation, event.ref), event.text, events);
153
+ this.appendText(this.itemId(generation, event.ref), event.text, events, event.phase);
154
154
  break;
155
155
  case 'text.done': {
156
156
  if (event.parentRef) {
@@ -168,6 +168,12 @@ export class ThreadProjector {
168
168
  createdAt: existing?.createdAt ?? this.now(),
169
169
  turnId: existing?.turnId ?? info.activeTurnId,
170
170
  text: event.text,
171
+ ...(existing?.kind === 'assistant' ? { uiAnswers: existing.uiAnswers, uiQueries: existing.uiQueries } : {}),
172
+ ...(event.phase === undefined
173
+ ? existing?.kind === 'assistant' && existing.phase !== undefined
174
+ ? { phase: existing.phase }
175
+ : {}
176
+ : { phase: event.phase }),
171
177
  streaming: false
172
178
  }));
173
179
  break;
@@ -452,7 +458,7 @@ export class ThreadProjector {
452
458
  events.push(this.thread.upsert({ ...item, streaming: false, endedAt: this.now() }));
453
459
  }
454
460
  }
455
- appendText(id, text, events) {
461
+ appendText(id, text, events, phase) {
456
462
  const existing = this.thread.get(id);
457
463
  if (!existing) {
458
464
  events.push(this.thread.upsert({
@@ -461,6 +467,7 @@ export class ThreadProjector {
461
467
  createdAt: this.now(),
462
468
  turnId: this.thread.info.activeTurnId,
463
469
  text,
470
+ ...(phase === undefined ? {} : { phase }),
464
471
  streaming: true
465
472
  }));
466
473
  return;
@@ -82,6 +82,12 @@ export class ChatThread {
82
82
  break;
83
83
  case 'delta':
84
84
  this.appendText(event.itemId, event.text);
85
+ if (event.uiQueries) {
86
+ const item = this.items.get(event.itemId);
87
+ if (item?.kind === 'assistant') {
88
+ this.items.set(item.id, { ...item, uiQueries: event.uiQueries });
89
+ }
90
+ }
85
91
  break;
86
92
  case 'info':
87
93
  this.info = event.info;
@@ -0,0 +1,38 @@
1
+ import type { z } from 'zod';
2
+ import type { ChatUiLinkPayload, ChatUiLinkReading, ChatAssistantItem, ChatInfo, ChatUiChoicePayload, ChatUiQueryPayload, ChatUiQueryReading } from '@adecore/agent-contracts';
3
+ import { type UiValue } from '@adecore/intelligent-ui';
4
+ import { type UiLinkTarget } from '@adecore/intelligent-ui/links';
5
+ import type { ChatSession } from './chat-session.ts';
6
+ export interface ChatUiSource {
7
+ args: z.ZodType;
8
+ result: z.ZodType;
9
+ authorize(info: ChatInfo, access: unknown, args: Record<string, UiValue>): Promise<void>;
10
+ read(info: ChatInfo, args: Record<string, UiValue>, signal: AbortSignal, access: unknown): Promise<unknown>;
11
+ }
12
+ export interface ChatUiHost {
13
+ capture(info: ChatInfo): Promise<unknown>;
14
+ sources: Readonly<Record<string, ChatUiSource>>;
15
+ link?(info: ChatInfo, access: unknown, target: UiLinkTarget): Promise<ChatUiLinkReading>;
16
+ }
17
+ export declare class ChatUiQueries {
18
+ readonly schemas: Record<string, z.ZodType>;
19
+ private readonly captures;
20
+ private readonly freezing;
21
+ private readonly cache;
22
+ private readonly reading;
23
+ private readonly attempts;
24
+ private readonly active;
25
+ private totalActive;
26
+ private readonly host;
27
+ constructor(host: ChatUiHost);
28
+ observe(session: ChatSession, item: ChatAssistantItem, final: boolean): void;
29
+ private access;
30
+ private storedBlock;
31
+ private stored;
32
+ query(session: ChatSession, payload: ChatUiQueryPayload): Promise<ChatUiQueryReading>;
33
+ choiceValues(session: ChatSession, payload: ChatUiChoicePayload | ChatUiLinkPayload): Promise<Record<string, unknown>>;
34
+ link(session: ChatSession, payload: ChatUiLinkPayload): Promise<ChatUiLinkReading>;
35
+ forget(chatId: string): void;
36
+ private linkReading;
37
+ private reason;
38
+ }