@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
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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()
|
|
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, {
|
|
431
|
-
|
|
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 ?
|
|
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
|
package/dist/chat/errors.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/chat/fake-codex.js
CHANGED
|
@@ -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,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
|
+
}
|
package/dist/chat/projector.js
CHANGED
|
@@ -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;
|
package/dist/chat/thread.js
CHANGED
|
@@ -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
|
+
}
|