@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.
- package/dist/chat/attachment-store.d.ts +2 -0
- package/dist/chat/attachment-store.js +38 -2
- package/dist/chat/backend.d.ts +13 -1
- package/dist/chat/chat-core.d.ts +53 -1
- package/dist/chat/chat-core.js +53 -10
- package/dist/chat/chat-session.d.ts +29 -2
- package/dist/chat/chat-session.js +258 -15
- package/dist/chat/claude-backend.d.ts +4 -0
- package/dist/chat/claude-backend.js +39 -0
- package/dist/chat/claude-protocol.d.ts +2 -0
- package/dist/chat/claude-protocol.js +12 -1
- package/dist/chat/codex-backend.d.ts +4 -0
- package/dist/chat/codex-backend.js +77 -5
- package/dist/chat/codex-protocol.d.ts +1 -0
- package/dist/chat/codex-protocol.js +74 -5
- 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/thread.js +6 -0
- package/dist/chat/ui-queries.d.ts +38 -0
- package/dist/chat/ui-queries.js +311 -0
- package/dist/host/handlers.js +4 -1
- package/package.json +5 -4
- package/src/chat/attachment-store.ts +38 -2
- package/src/chat/backend.ts +16 -2
- package/src/chat/chat-core.ts +60 -9
- package/src/chat/chat-session.ts +294 -16
- package/src/chat/claude-backend.ts +40 -0
- package/src/chat/claude-protocol.ts +16 -1
- package/src/chat/codex-backend.ts +77 -5
- package/src/chat/codex-protocol.ts +78 -6
- 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/thread.ts +6 -0
- package/src/chat/ui-queries.ts +360 -0
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
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()
|
|
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, {
|
|
489
|
-
|
|
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 ?
|
|
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
|
/*
|
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
|
);
|
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;
|