@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.
- package/dist/chat/attachment-store.d.ts +2 -0
- package/dist/chat/attachment-store.js +38 -2
- package/dist/chat/backend.d.ts +12 -1
- package/dist/chat/chat-core.d.ts +57 -1
- package/dist/chat/chat-core.js +67 -14
- package/dist/chat/chat-session.d.ts +35 -2
- package/dist/chat/chat-session.js +261 -11
- package/dist/chat/chat-store.d.ts +3 -2
- package/dist/chat/chat-store.js +42 -11
- package/dist/chat/codex-backend.d.ts +2 -0
- package/dist/chat/codex-backend.js +51 -1
- package/dist/chat/codex-protocol.js +30 -3
- package/dist/chat/delta-coalescer.js +1 -1
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/fake-codex.js +33 -0
- package/dist/chat/generated-image.d.ts +6 -0
- package/dist/chat/generated-image.js +74 -0
- package/dist/chat/projector.js +9 -2
- package/dist/chat/readable-text.d.ts +2 -0
- package/dist/chat/readable-text.js +16 -0
- package/dist/chat/thread.js +6 -0
- package/dist/chat/ui-queries.d.ts +49 -0
- package/dist/chat/ui-queries.js +358 -0
- package/dist/host/handlers.js +3 -0
- package/dist/tasks/task-coordinator.js +6 -2
- package/package.json +5 -4
- package/src/chat/attachment-store.ts +38 -2
- package/src/chat/backend.ts +14 -2
- package/src/chat/chat-core.ts +84 -13
- package/src/chat/chat-session.ts +304 -12
- package/src/chat/chat-store.ts +53 -11
- package/src/chat/codex-backend.ts +50 -1
- package/src/chat/codex-protocol.ts +31 -3
- package/src/chat/delta-coalescer.ts +1 -1
- package/src/chat/errors.ts +2 -0
- package/src/chat/fake-codex.ts +34 -0
- package/src/chat/generated-image.ts +88 -0
- package/src/chat/projector.ts +9 -2
- package/src/chat/readable-text.ts +18 -0
- package/src/chat/thread.ts +6 -0
- package/src/chat/ui-queries.ts +416 -0
- package/src/host/handlers.ts +3 -0
- 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 ?
|
|
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
|
/*
|
package/src/chat/errors.ts
CHANGED
package/src/chat/fake-codex.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/chat/projector.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/chat/thread.ts
CHANGED
|
@@ -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;
|