@adecore/agents 0.20.1 → 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.
Files changed (43) 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 +12 -1
  4. package/dist/chat/chat-core.d.ts +57 -1
  5. package/dist/chat/chat-core.js +67 -14
  6. package/dist/chat/chat-session.d.ts +35 -2
  7. package/dist/chat/chat-session.js +261 -11
  8. package/dist/chat/chat-store.d.ts +3 -2
  9. package/dist/chat/chat-store.js +42 -11
  10. package/dist/chat/codex-backend.d.ts +2 -0
  11. package/dist/chat/codex-backend.js +51 -1
  12. package/dist/chat/codex-protocol.js +30 -3
  13. package/dist/chat/delta-coalescer.js +1 -1
  14. package/dist/chat/errors.d.ts +1 -1
  15. package/dist/chat/fake-codex.js +33 -0
  16. package/dist/chat/generated-image.d.ts +6 -0
  17. package/dist/chat/generated-image.js +74 -0
  18. package/dist/chat/projector.js +9 -2
  19. package/dist/chat/readable-text.d.ts +2 -0
  20. package/dist/chat/readable-text.js +16 -0
  21. package/dist/chat/thread.js +6 -0
  22. package/dist/chat/ui-queries.d.ts +49 -0
  23. package/dist/chat/ui-queries.js +358 -0
  24. package/dist/host/handlers.js +3 -0
  25. package/dist/tasks/task-coordinator.js +6 -2
  26. package/package.json +5 -4
  27. package/src/chat/attachment-store.ts +38 -2
  28. package/src/chat/backend.ts +14 -2
  29. package/src/chat/chat-core.ts +84 -13
  30. package/src/chat/chat-session.ts +304 -12
  31. package/src/chat/chat-store.ts +53 -11
  32. package/src/chat/codex-backend.ts +50 -1
  33. package/src/chat/codex-protocol.ts +31 -3
  34. package/src/chat/delta-coalescer.ts +1 -1
  35. package/src/chat/errors.ts +2 -0
  36. package/src/chat/fake-codex.ts +34 -0
  37. package/src/chat/generated-image.ts +88 -0
  38. package/src/chat/projector.ts +9 -2
  39. package/src/chat/readable-text.ts +18 -0
  40. package/src/chat/thread.ts +6 -0
  41. package/src/chat/ui-queries.ts +416 -0
  42. package/src/host/handlers.ts +3 -0
  43. package/src/tasks/task-coordinator.ts +6 -2
@@ -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';
@@ -632,7 +633,7 @@ export class CodexProtocol {
632
633
  events.push({ type: 'question.requested', requestId: ref, questions, async: true });
633
634
  return;
634
635
  }
635
- this.text(ref, str(item.text) ?? '', completed, events);
636
+ this.text(ref, str(item.text) ?? '', completed, events, str(item.phase) ?? undefined);
636
637
  return;
637
638
  }
638
639
  case 'plan':
@@ -705,6 +706,33 @@ export class CodexProtocol {
705
706
  case 'dynamicToolCall':
706
707
  this.tool(ref, str(item.tool) ?? 'tool', item.arguments ?? {}, completed, textOf(item.contentItems), item.success !== false, events);
707
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;
708
736
  case 'webSearch': {
709
737
  const action = isRecord(item.action) ? item.action : {};
710
738
  this.tool(ref, 'WebSearch', { query: str(item.query) ?? str(action.query) ?? '' }, completed, '', true, events);
@@ -775,8 +803,8 @@ export class CodexProtocol {
775
803
  return null;
776
804
  }
777
805
 
778
- private text(ref: string, text: string, completed: boolean, events: BackendEvent[]): void {
779
- 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 }) });
780
808
  }
781
809
 
782
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
  );
@@ -0,0 +1,18 @@
1
+ import type { ChatAssistantItem } from '@adecore/agent-contracts';
2
+ import { uiFallbackText } from '@adecore/intelligent-ui/text';
3
+
4
+ /*
5
+ * An assistant's words as another agent or a person outside the thread reads them: each UI block becomes
6
+ * its text fallback, with the first query values when they were frozen for the block as stored. It works
7
+ * from the stored block ranges, so it holds whatever fence language the host compiled.
8
+ */
9
+ export function readableAssistantText(item: Pick<ChatAssistantItem, 'text' | 'ui' | 'uiQueries'>): string {
10
+ if (!item.ui?.length) {
11
+ return item.text;
12
+ }
13
+ const blocks = item.ui.map((block) => {
14
+ const frozen = item.uiQueries?.blocks[block.id];
15
+ return frozen && frozen.revision === block.revision ? { ...block, fallback: frozen.fallback } : block;
16
+ });
17
+ return uiFallbackText(item.text, blocks);
18
+ }
@@ -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;