@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
@@ -48,6 +48,7 @@ export class CodexBackend implements ChatBackend {
48
48
  readonly acknowledgesTurns = true;
49
49
  private promptId: string | null = null;
50
50
  private readonly launch: BackendLaunch;
51
+ private imageWrites: Promise<void> | null = null;
51
52
  private readonly host: BackendHost;
52
53
  private readonly protocol: CodexProtocol;
53
54
  private readonly client: CodexClientInfo;
@@ -126,7 +127,7 @@ export class CodexBackend implements ChatBackend {
126
127
  }
127
128
  }
128
129
 
129
- sendTurn(input: TurnInput): void {
130
+ private turnInput(input: TurnInput): unknown[] {
130
131
  const images = input.attachments.filter((attachment) => attachmentImageMime(attachment) !== null);
131
132
  if (images.length > 0 && this.imageInputSupported === false) {
132
133
  throw new Error(`${this.launch.modelName} does not support image input. Choose a model that accepts images.`);
@@ -151,6 +152,11 @@ export class CodexBackend implements ChatBackend {
151
152
  if (note !== '') {
152
153
  parts.push(`\n\n${note}`);
153
154
  }
155
+ return [...textInput(parts.join('')), ...images.map((attachment) => ({ type: 'localImage', path: attachment.path }))];
156
+ }
157
+
158
+ sendTurn(input: TurnInput): void {
159
+ const nativeInput = this.turnInput(input);
154
160
  const effort = this.launch.selection.options.effort;
155
161
  const tier = codexServiceTier(this.launch.selection);
156
162
  this.turnUnnamed = true;
@@ -159,7 +165,7 @@ export class CodexBackend implements ChatBackend {
159
165
  'turn/start',
160
166
  {
161
167
  threadId: this.threadId,
162
- input: [...textInput(parts.join('')), ...images.map((attachment) => ({ type: 'localImage', path: attachment.path }))],
168
+ input: nativeInput,
163
169
  model: this.launch.selection.model,
164
170
  ...(typeof effort === 'string' ? { effort } : {}),
165
171
  ...(tier === null ? {} : { serviceTier: tier })
@@ -168,6 +174,24 @@ export class CodexBackend implements ChatBackend {
168
174
  );
169
175
  }
170
176
 
177
+ async steerTurn(input: TurnInput): Promise<boolean> {
178
+ const transport = this.transport;
179
+ const turnId = this.protocol.turnId;
180
+ if (!transport || !turnId || this.turnUnnamed) {
181
+ return false;
182
+ }
183
+ try {
184
+ await transport.request('turn/steer', { threadId: this.threadId, expectedTurnId: turnId, input: this.turnInput(input) });
185
+ return true;
186
+ } catch (error) {
187
+ const rejected = error instanceof Error && error.name === 'CodexRpcError';
188
+ if (rejected && /no active turn|not.*active|turn.*mismatch|expected.*turn|method not found/i.test(error.message)) {
189
+ return false;
190
+ }
191
+ throw error;
192
+ }
193
+ }
194
+
171
195
  private acceptPrompt(promptId: string | undefined): void {
172
196
  if (promptId !== undefined && this.promptId === promptId) {
173
197
  this.emit({ type: 'turn.accepted', promptId });
@@ -308,9 +332,9 @@ export class CodexBackend implements ChatBackend {
308
332
  return true;
309
333
  }
310
334
 
311
- /* Codex asks its async question once and waits; forgetting it locally is all a dismissal is. */
312
335
  dismissRequest(requestId: string): boolean {
313
- return this.protocol.dismissQuestion(requestId);
336
+ const answer = this.protocol.dismissQuestionAnswer(requestId);
337
+ return answer?.kind === 'respond' ? this.respond(requestId, answer.rpcId, answer.result) : this.protocol.dismissQuestion(requestId);
314
338
  }
315
339
 
316
340
  declineRequest(requestId: string, _message: string): boolean {
@@ -328,7 +352,7 @@ export class CodexBackend implements ChatBackend {
328
352
  dispose(): Promise<void> {
329
353
  const transport = this.transport;
330
354
  this.transport = null;
331
- return transport?.dispose() ?? Promise.resolve();
355
+ return Promise.all([transport?.dispose(), this.imageWrites]).then(() => undefined);
332
356
  }
333
357
 
334
358
  /* A request whose failure the person has to hear about, since it is the turn that cannot go on. */
@@ -392,6 +416,54 @@ export class CodexBackend implements ChatBackend {
392
416
  this.interruptOwed = false;
393
417
  this.promptId = null;
394
418
  }
419
+ if (event.type === 'image.generated' || this.imageWrites !== null) {
420
+ const pending = (this.imageWrites ?? Promise.resolve()).then(async () => {
421
+ if (event.type === 'image.generated') {
422
+ await this.storeImage(event);
423
+ } else {
424
+ this.host.onEvent(event);
425
+ }
426
+ });
427
+ this.imageWrites = pending;
428
+ void pending
429
+ .finally(() => {
430
+ if (this.imageWrites === pending) {
431
+ this.imageWrites = null;
432
+ }
433
+ })
434
+ .catch(() => undefined);
435
+ return;
436
+ }
395
437
  this.host.onEvent(event);
396
438
  }
439
+
440
+ private async storeImage(event: Extract<BackendEvent, { type: 'image.generated' }>): Promise<void> {
441
+ const input = { revisedPrompt: event.revisedPrompt, transparentBackground: event.transparentBackground };
442
+ let attachment;
443
+ try {
444
+ if (!this.launch.saveGeneratedImage) {
445
+ throw new Error('This machine cannot save generated images');
446
+ }
447
+ attachment = await this.launch.saveGeneratedImage(event.ref, event.upload);
448
+ } catch (error) {
449
+ this.host.onEvent({ type: 'tool.started', ref: event.ref, name: 'ImageGeneration', input, parentRef: null });
450
+ this.host.onEvent({ type: 'tool.done', ref: event.ref, output: errorText(error), state: 'error' });
451
+ return;
452
+ }
453
+ this.host.onEvent({
454
+ type: 'tool.started',
455
+ ref: event.ref,
456
+ name: 'ImageGeneration',
457
+ input: {
458
+ ...input,
459
+ attachment: {
460
+ ...attachment,
461
+ ...(event.width === undefined ? {} : { width: event.width }),
462
+ ...(event.height === undefined ? {} : { height: event.height })
463
+ }
464
+ },
465
+ parentRef: null
466
+ });
467
+ this.host.onEvent({ type: 'tool.done', ref: event.ref, output: null, state: 'done' });
468
+ }
397
469
  }
@@ -1,3 +1,4 @@
1
+ import { generatedImage } from './generated-image.ts';
1
2
  import type { ChatFileChange, ChatQuestion, ChatTurnLimit, UsageWindow } from '@adecore/agent-contracts';
2
3
  import { readCodexLimits } from '../usage/limits/normalize.ts';
3
4
  import { codexRuntimeMode } from '../providers/codex.ts';
@@ -138,7 +139,8 @@ type RequestOwner = { threadId: string | null; turnId: string | null };
138
139
  type Pending = RequestOwner &
139
140
  (
140
141
  | { type: 'approval'; rpcId: CodexRpcId; kind: 'command' | 'fileChange'; amendment: string[] | null; decisions: string[] }
141
- | { type: 'question'; rpcId: CodexRpcId | null; questionIds: string[] }
142
+ | { type: 'question'; rpcId: CodexRpcId | null; questionIds: string[]; async?: boolean }
143
+ | { type: 'permissions'; rpcId: CodexRpcId; permissions: Frame }
142
144
  | { type: 'elicitation'; rpcId: CodexRpcId; persist: ElicitationPersist | null }
143
145
  );
144
146
 
@@ -361,6 +363,13 @@ export class CodexProtocol {
361
363
  /* The reply that settles an approval, or null when nothing waits under that id. */
362
364
  approvalDecision(requestId: string, decision: ApprovalDecision): { rpcId: CodexRpcId; result: unknown } | null {
363
365
  const pending = this.pending.get(requestId);
366
+ if (pending?.type === 'permissions') {
367
+ this.pending.delete(requestId);
368
+ return {
369
+ rpcId: pending.rpcId,
370
+ result: { permissions: decision === 'deny' ? {} : pending.permissions, scope: decision === 'allow-always' ? 'session' : 'turn' }
371
+ };
372
+ }
364
373
  if (pending?.type === 'elicitation') {
365
374
  this.pending.delete(requestId);
366
375
  return { rpcId: pending.rpcId, result: elicitationAnswer(decision, pending.persist) };
@@ -394,6 +403,11 @@ export class CodexProtocol {
394
403
  return { kind: 'respond', rpcId: pending.rpcId, result: { answers: byId } };
395
404
  }
396
405
 
406
+ dismissQuestionAnswer(requestId: string): CodexAnswer | null {
407
+ const pending = this.pending.get(requestId);
408
+ return pending?.type === 'question' && pending.async === true && pending.rpcId !== null ? this.questionAnswer(requestId, {}) : null;
409
+ }
410
+
397
411
  /* Drops an asynchronous question the person walked away from; a blocking one has to be answered. */
398
412
  dismissQuestion(requestId: string): boolean {
399
413
  const pending = this.pending.get(requestId);
@@ -485,8 +499,39 @@ export class CodexProtocol {
485
499
  if (questions.length === 0) {
486
500
  return;
487
501
  }
488
- this.pending.set(requestId, { ...this.owner(params), type: 'question', rpcId, questionIds: questions.map((question) => question.id) });
489
- events.push({ type: 'question.requested', requestId, questions, ...(this.isChildRequest(params) ? { background: true } : {}) });
502
+ this.pending.set(requestId, {
503
+ ...this.owner(params),
504
+ type: 'question',
505
+ rpcId,
506
+ questionIds: questions.map((question) => question.id),
507
+ async: params.isBlocking === false
508
+ });
509
+ events.push({
510
+ type: 'question.requested',
511
+ requestId,
512
+ questions,
513
+ ...(params.isBlocking === false ? { async: true } : {}),
514
+ ...(this.isChildRequest(params) ? { background: true } : {})
515
+ });
516
+ return;
517
+ }
518
+ if (method === 'item/permissions/requestApproval') {
519
+ if (!isRecord(params.permissions)) {
520
+ return;
521
+ }
522
+ const permissions = params.permissions;
523
+ this.pending.set(requestId, { ...this.owner(params), type: 'permissions', rpcId, permissions });
524
+ events.push({
525
+ type: 'approval.requested',
526
+ requestId,
527
+ ref: str(params.itemId),
528
+ toolName: 'RequestPermissions',
529
+ input: { cwd: params.cwd, permissions },
530
+ description: this.requestDescription(params, str(params.reason)),
531
+ canAllowAlways: true,
532
+ allowAlways: { label: 'Allow for this session', description: 'Grant these permissions for the rest of this session.' },
533
+ ...(this.isChildRequest(params) ? { background: true } : {})
534
+ });
490
535
  return;
491
536
  }
492
537
  if (method === 'mcpServer/elicitation/request') {
@@ -588,7 +633,7 @@ export class CodexProtocol {
588
633
  events.push({ type: 'question.requested', requestId: ref, questions, async: true });
589
634
  return;
590
635
  }
591
- this.text(ref, str(item.text) ?? '', completed, events);
636
+ this.text(ref, str(item.text) ?? '', completed, events, str(item.phase) ?? undefined);
592
637
  return;
593
638
  }
594
639
  case 'plan':
@@ -661,6 +706,33 @@ export class CodexProtocol {
661
706
  case 'dynamicToolCall':
662
707
  this.tool(ref, str(item.tool) ?? 'tool', item.arguments ?? {}, completed, textOf(item.contentItems), item.success !== false, events);
663
708
  return;
709
+ case 'imageGeneration': {
710
+ const revisedPrompt = str(item.revisedPrompt) ?? undefined;
711
+ const transparentBackground = typeof item.transparentBackground === 'boolean' ? item.transparentBackground : undefined;
712
+ const input = {
713
+ ...(revisedPrompt === undefined ? {} : { revisedPrompt }),
714
+ ...(transparentBackground === undefined ? {} : { transparentBackground })
715
+ };
716
+ if (!completed) {
717
+ this.tool(ref, 'ImageGeneration', input, false, '', true, events);
718
+ return;
719
+ }
720
+ try {
721
+ if (item.failure || item.status !== 'completed') {
722
+ throw new Error(
723
+ isRecord(item.failure) ? (str(item.failure.message) ?? 'Image generation failed') : (str(item.failure) ?? 'Image generation failed')
724
+ );
725
+ }
726
+ const image = generatedImage(str(item.result) ?? '', revisedPrompt);
727
+ events.push({ type: 'image.generated', ref, ...image, ...input });
728
+ } catch (error) {
729
+ this.tool(ref, 'ImageGeneration', input, true, error instanceof Error ? error.message : 'Image generation failed', false, events);
730
+ }
731
+ return;
732
+ }
733
+ case 'imageView':
734
+ this.tool(ref, 'ImageView', { path: str(item.path) ?? '' }, completed, '', true, events);
735
+ return;
664
736
  case 'webSearch': {
665
737
  const action = isRecord(item.action) ? item.action : {};
666
738
  this.tool(ref, 'WebSearch', { query: str(item.query) ?? str(action.query) ?? '' }, completed, '', true, events);
@@ -731,8 +803,8 @@ export class CodexProtocol {
731
803
  return null;
732
804
  }
733
805
 
734
- private text(ref: string, text: string, completed: boolean, events: BackendEvent[]): void {
735
- events.push(completed ? { type: 'text.done', ref, text } : { type: 'text.delta', ref, text });
806
+ private text(ref: string, text: string, completed: boolean, events: BackendEvent[], phase?: string): void {
807
+ events.push({ type: completed ? 'text.done' : 'text.delta', ref, text, ...(phase === undefined ? {} : { phase }) });
736
808
  }
737
809
 
738
810
  /*
@@ -21,7 +21,7 @@ export class DeltaCoalescer {
21
21
  }
22
22
 
23
23
  push(event: ChatEvent): void {
24
- if (event.type !== 'delta') {
24
+ if (event.type !== 'delta' || event.uiQueries !== undefined) {
25
25
  this.flush();
26
26
  this.send(event);
27
27
  return;
@@ -1,6 +1,8 @@
1
1
  import { CodedError } from '../coded-error.ts';
2
2
 
3
3
  export type ChatErrorCode =
4
+ | 'refused-query'
5
+ | 'stale-ui-query'
4
6
  | 'history-expired'
5
7
  | 'chat-not-found'
6
8
  // A record on disk this version cannot read, which is never written over.
@@ -197,6 +197,40 @@ export function fakeCodexWith(options: FakeCodexOptions): FakeCli {
197
197
  turnCompleted('failed', "You've hit your usage limit.", 'usageLimitExceeded');
198
198
  return;
199
199
  }
200
+ if (text.startsWith('image:')) {
201
+ const scenario = text.slice(6);
202
+ const entry = item('imageGeneration', {
203
+ status: 'inProgress',
204
+ result: '',
205
+ savedPath: '/never-read-generated-image.png',
206
+ revisedPrompt: 'A rabbit',
207
+ transparentBackground: true
208
+ });
209
+ started(entry);
210
+ const result =
211
+ scenario === 'empty'
212
+ ? ''
213
+ : scenario === 'invalid'
214
+ ? 'not base64'
215
+ : scenario === 'not-image'
216
+ ? Buffer.from('text').toString('base64')
217
+ : scenario === 'too-large'
218
+ ? Buffer.alloc(10 * 1024 * 1024 + 1).toString('base64')
219
+ : 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=';
220
+ const done = {
221
+ ...entry,
222
+ status: scenario === 'failed' ? 'failed' : 'completed',
223
+ result,
224
+ ...(scenario === 'failed' ? { failure: { message: 'Image generation failed' } } : {})
225
+ };
226
+ completed(done);
227
+ if (scenario === 'replay') {
228
+ completed(done);
229
+ }
230
+ agentMessage('Here is the generated image.');
231
+ turnCompleted('completed');
232
+ return;
233
+ }
200
234
  if (text === 'slow') {
201
235
  slow = true;
202
236
  started(item('agentMessage', { text: '', phase: 'final_answer', memoryCitation: null, delivery: null, questions: null }));
@@ -0,0 +1,88 @@
1
+ import { ChatAttachmentUploadSchema, type ChatAttachmentUpload } from '@adecore/agent-contracts';
2
+
3
+ interface ImageInfo {
4
+ mime: string;
5
+ extension: string;
6
+ width?: number;
7
+ height?: number;
8
+ }
9
+
10
+ function imageInfo(bytes: Buffer): ImageInfo | null {
11
+ if (
12
+ bytes.length >= 33 &&
13
+ bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) &&
14
+ bytes.readUInt32BE(8) === 13 &&
15
+ bytes.toString('ascii', 12, 16) === 'IHDR'
16
+ ) {
17
+ return { mime: 'image/png', extension: 'png', width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
18
+ }
19
+ if (bytes.length >= 13 && /GIF8[79]a/.test(bytes.toString('ascii', 0, 6))) {
20
+ return { mime: 'image/gif', extension: 'gif', width: bytes.readUInt16LE(6), height: bytes.readUInt16LE(8) };
21
+ }
22
+ if (
23
+ bytes.length >= 20 &&
24
+ bytes.toString('ascii', 0, 4) === 'RIFF' &&
25
+ bytes.toString('ascii', 8, 12) === 'WEBP' &&
26
+ bytes.readUInt32LE(4) + 8 === bytes.length
27
+ ) {
28
+ const kind = bytes.toString('ascii', 12, 16);
29
+ if (kind === 'VP8X' && bytes.length >= 30) {
30
+ return { mime: 'image/webp', extension: 'webp', width: bytes.readUIntLE(24, 3) + 1, height: bytes.readUIntLE(27, 3) + 1 };
31
+ }
32
+ if (kind === 'VP8 ' && bytes.length >= 30 && bytes.subarray(23, 26).equals(Buffer.from([157, 1, 42]))) {
33
+ return { mime: 'image/webp', extension: 'webp', width: bytes.readUInt16LE(26) & 0x3fff, height: bytes.readUInt16LE(28) & 0x3fff };
34
+ }
35
+ if (kind === 'VP8L' && bytes.length >= 25 && bytes[20] === 0x2f) {
36
+ const size = bytes.readUInt32LE(21);
37
+ return { mime: 'image/webp', extension: 'webp', width: (size & 0x3fff) + 1, height: ((size >> 14) & 0x3fff) + 1 };
38
+ }
39
+ return null;
40
+ }
41
+ if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
42
+ let offset = 2;
43
+ while (offset + 4 <= bytes.length && bytes[offset] === 255) {
44
+ const marker = bytes[offset + 1]!;
45
+ if (marker === 218 || marker === 217) {
46
+ break;
47
+ }
48
+ if (marker === 255) {
49
+ offset++;
50
+ continue;
51
+ }
52
+ const size = bytes.readUInt16BE(offset + 2);
53
+ if (size < 2 || offset + 2 + size > bytes.length) {
54
+ return null;
55
+ }
56
+ if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker) && size >= 8) {
57
+ return { mime: 'image/jpeg', extension: 'jpg', width: bytes.readUInt16BE(offset + 7), height: bytes.readUInt16BE(offset + 5) };
58
+ }
59
+ offset += size + 2;
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+
65
+ export function generatedImage(data: string, prompt?: string): { upload: ChatAttachmentUpload; width?: number; height?: number } {
66
+ const parsed = ChatAttachmentUploadSchema.safeParse({ name: 'image', mime: 'application/octet-stream', data });
67
+ if (!parsed.success) {
68
+ throw new Error('The generated image is empty, invalid base64 or exceeds 10 MiB');
69
+ }
70
+ const bytes = Buffer.from(data, 'base64');
71
+ // Buffer's decoder accepts malformed padding and trailing bits; the protocol requires canonical base64.
72
+ if (bytes.toString('base64') !== data) {
73
+ throw new Error('The generated image contains invalid base64');
74
+ }
75
+ const info = imageInfo(bytes);
76
+ if (info === null || (info.width !== undefined && info.width === 0) || (info.height !== undefined && info.height === 0)) {
77
+ throw new Error('The generated bytes are not a supported image');
78
+ }
79
+ const stem =
80
+ (prompt ?? '')
81
+ .normalize('NFKD')
82
+ .replace(/[^a-zA-Z0-9]+/g, '-')
83
+ .replace(/^-|-$/g, '')
84
+ .slice(0, 80)
85
+ .toLowerCase() || 'generated-image';
86
+ const upload = { name: `${stem}.${info.extension}`, mime: info.mime, data };
87
+ return { upload, ...(info.width === undefined ? {} : { width: info.width }), ...(info.height === undefined ? {} : { height: info.height }) };
88
+ }
@@ -187,7 +187,7 @@ export class ThreadProjector {
187
187
  break;
188
188
  case 'text.delta':
189
189
  this.closeThinking(events);
190
- this.appendText(this.itemId(generation, event.ref), event.text, events);
190
+ this.appendText(this.itemId(generation, event.ref), event.text, events, event.phase);
191
191
  break;
192
192
  case 'text.done': {
193
193
  if (event.parentRef) {
@@ -206,6 +206,12 @@ export class ThreadProjector {
206
206
  createdAt: existing?.createdAt ?? this.now(),
207
207
  turnId: existing?.turnId ?? info.activeTurnId,
208
208
  text: event.text,
209
+ ...(existing?.kind === 'assistant' ? { uiAnswers: existing.uiAnswers, uiQueries: existing.uiQueries } : {}),
210
+ ...(event.phase === undefined
211
+ ? existing?.kind === 'assistant' && existing.phase !== undefined
212
+ ? { phase: existing.phase }
213
+ : {}
214
+ : { phase: event.phase }),
209
215
  streaming: false
210
216
  })
211
217
  );
@@ -507,7 +513,7 @@ export class ThreadProjector {
507
513
  }
508
514
  }
509
515
 
510
- private appendText(id: string, text: string, events: ChatEvent[]): void {
516
+ private appendText(id: string, text: string, events: ChatEvent[], phase?: string): void {
511
517
  const existing = this.thread.get(id);
512
518
  if (!existing) {
513
519
  events.push(
@@ -517,6 +523,7 @@ export class ThreadProjector {
517
523
  createdAt: this.now(),
518
524
  turnId: this.thread.info.activeTurnId,
519
525
  text,
526
+ ...(phase === undefined ? {} : { phase }),
520
527
  streaming: true
521
528
  })
522
529
  );
@@ -93,6 +93,12 @@ export class ChatThread {
93
93
  break;
94
94
  case 'delta':
95
95
  this.appendText(event.itemId, event.text);
96
+ if (event.uiQueries) {
97
+ const item = this.items.get(event.itemId);
98
+ if (item?.kind === 'assistant') {
99
+ this.items.set(item.id, { ...item, uiQueries: event.uiQueries });
100
+ }
101
+ }
96
102
  break;
97
103
  case 'info':
98
104
  this.info = event.info;