@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
|
@@ -4,6 +4,8 @@ export declare class AttachmentStore {
|
|
|
4
4
|
readonly dir: string;
|
|
5
5
|
constructor(home: string);
|
|
6
6
|
save(chatId: string, upload: ChatAttachmentUpload): Promise<ChatAttachment>;
|
|
7
|
+
saveGenerated(chatId: string, ref: string, upload: ChatAttachmentUpload): Promise<ChatAttachment>;
|
|
8
|
+
copy(chatId: string, attachment: ChatAttachment): Promise<ChatAttachment>;
|
|
7
9
|
removeAll(chatId: string): Promise<void>;
|
|
8
10
|
folderOf(chatId: string): string;
|
|
9
11
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { link, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { extname, join } from 'node:path';
|
|
4
4
|
import { attachmentImageMime } from '@adecore/agent-contracts';
|
|
5
5
|
// The extension the file gets on disk when its name does not carry a usable one.
|
|
@@ -50,6 +50,42 @@ export class AttachmentStore {
|
|
|
50
50
|
await writeFile(path, bytes, { mode: 0o600 });
|
|
51
51
|
return { id, name: upload.name, mime, size: bytes.byteLength, path };
|
|
52
52
|
}
|
|
53
|
+
async saveGenerated(chatId, ref, upload) {
|
|
54
|
+
const id = createHash('sha256').update(`codex-image:${ref}`).digest('hex');
|
|
55
|
+
const bytes = Buffer.from(upload.data, 'base64');
|
|
56
|
+
const folder = this.folderOf(chatId);
|
|
57
|
+
await mkdir(folder, { recursive: true, mode: 0o700 });
|
|
58
|
+
const path = join(folder, `${id}.${extensionFor('', upload.mime)}`);
|
|
59
|
+
const temporary = join(folder, `.${id}-${randomBytes(8).toString('hex')}.tmp`);
|
|
60
|
+
try {
|
|
61
|
+
await writeFile(temporary, bytes, { mode: 0o600, flag: 'wx' });
|
|
62
|
+
try {
|
|
63
|
+
// Publish complete bytes without replacing an image another replay has already saved.
|
|
64
|
+
await link(temporary, path);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error.code !== 'EEXIST') {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
const previous = await readFile(path);
|
|
71
|
+
if (!previous.equals(bytes)) {
|
|
72
|
+
throw new Error('The generated image id already belongs to another image');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await rm(temporary, { force: true });
|
|
78
|
+
}
|
|
79
|
+
return { id, name: upload.name, mime: upload.mime, size: bytes.byteLength, path };
|
|
80
|
+
}
|
|
81
|
+
async copy(chatId, attachment) {
|
|
82
|
+
const bytes = await readFile(attachment.path);
|
|
83
|
+
const folder = this.folderOf(chatId);
|
|
84
|
+
await mkdir(folder, { recursive: true, mode: 0o700 });
|
|
85
|
+
const path = join(folder, `${createHash('sha256').update(attachment.id).digest('hex')}.${extensionFor(attachment.name, attachment.mime)}`);
|
|
86
|
+
await writeFile(path, bytes, { mode: 0o600 });
|
|
87
|
+
return { ...attachment, path };
|
|
88
|
+
}
|
|
53
89
|
/* Everything a chat attached; used when the chat itself is deleted. */
|
|
54
90
|
async removeAll(chatId) {
|
|
55
91
|
await rm(this.folderOf(chatId), { recursive: true, force: true });
|
package/dist/chat/backend.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatAttachment, ChatFileChange, ChatQuestion, ChatSkill, ChatSubagentUsage, ChatTurnLimit, ChatWorkflow, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ChatAttachment, ChatAttachmentUpload, ChatFileChange, ChatQuestion, ChatSkill, ChatSubagentUsage, ChatTurnLimit, ChatWorkflow, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
|
|
2
2
|
import type { LimitsUpdate } from '../usage/limits/normalize.ts';
|
|
3
3
|
import type { SpawnChatProcess } from './chat-process.ts';
|
|
4
4
|
export interface BackendLaunch {
|
|
@@ -14,6 +14,7 @@ export interface BackendLaunch {
|
|
|
14
14
|
resumeNote: string | null;
|
|
15
15
|
folders?: readonly string[];
|
|
16
16
|
spawn?: SpawnChatProcess;
|
|
17
|
+
saveGeneratedImage?(ref: string, upload: ChatAttachmentUpload): Promise<ChatAttachment>;
|
|
17
18
|
}
|
|
18
19
|
export interface TurnInput {
|
|
19
20
|
promptId?: string;
|
|
@@ -47,11 +48,21 @@ export type BackendEvent = {
|
|
|
47
48
|
type: 'text.delta';
|
|
48
49
|
ref: string;
|
|
49
50
|
text: string;
|
|
51
|
+
phase?: string;
|
|
50
52
|
} | {
|
|
51
53
|
type: 'text.done';
|
|
52
54
|
ref: string;
|
|
53
55
|
text: string;
|
|
54
56
|
parentRef?: string | null;
|
|
57
|
+
phase?: string;
|
|
58
|
+
} | {
|
|
59
|
+
type: 'image.generated';
|
|
60
|
+
ref: string;
|
|
61
|
+
upload: ChatAttachmentUpload;
|
|
62
|
+
width?: number;
|
|
63
|
+
height?: number;
|
|
64
|
+
revisedPrompt?: string;
|
|
65
|
+
transparentBackground?: boolean;
|
|
55
66
|
} | {
|
|
56
67
|
type: 'thinking.delta';
|
|
57
68
|
ref: string;
|
package/dist/chat/chat-core.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type AgentKind, type ChatAttachment, type ChatAttachResult, type ChatAttachmentUpload, type ChatBookmark, type ChatCheckpointDiff, type ChatConfigurePayload, type ChatCreatePayload, type ChatEvent, type ChatHistoryResult, type ChatInfo, type ChatItem, type ChatQueuedMessage, type ChatSkill, type ChatSubagentPayload, type ChatSubagentResult, type ChatTurnItem, type ChatVisual, type ModelSelection, type RuntimeMode } from '@adecore/agent-contracts';
|
|
1
|
+
import { type AgentKind, type ChatAttachment, type ChatAttachResult, type ChatAttachmentUpload, type ChatBookmark, type ChatCheckpointDiff, type ChatConfigurePayload, type ChatCreatePayload, type ChatEvent, type ChatHistoryResult, type ChatInfo, type ChatItem, type ChatQueuedMessage, type ChatUiChoicePayload, type ChatUiQueryPayload, type ChatUiLinkPayload, type ChatSkill, type ChatSubagentPayload, type ChatSubagentResult, type ChatTurnItem, type ChatVisual, type ModelSelection, type RuntimeMode } from '@adecore/agent-contracts';
|
|
2
|
+
import { type ChatUiHost } from './ui-queries.ts';
|
|
2
3
|
import { ClientSinks } from '../client-sinks.ts';
|
|
3
4
|
import type { AgentEvent, AgentSink } from '../events.ts';
|
|
4
5
|
import type { OutboxOutcome } from '../outbox/outbox-worker.ts';
|
|
@@ -25,6 +26,8 @@ export interface InterruptedRun {
|
|
|
25
26
|
attempt: number;
|
|
26
27
|
}
|
|
27
28
|
export interface ChatCoreOptions {
|
|
29
|
+
intelligentUi?: ChatUiHost;
|
|
30
|
+
uiFenceLanguage?: string;
|
|
28
31
|
providers: ProviderRegistry;
|
|
29
32
|
store?: ChatStore;
|
|
30
33
|
checkpoints?: TurnCheckpoints;
|
|
@@ -95,8 +98,21 @@ export declare class ChatCore {
|
|
|
95
98
|
mime: string;
|
|
96
99
|
size: number;
|
|
97
100
|
path: string;
|
|
101
|
+
width?: number | undefined;
|
|
102
|
+
height?: number | undefined;
|
|
98
103
|
}[] | undefined;
|
|
99
104
|
createdAt: number;
|
|
105
|
+
uiChoice?: {
|
|
106
|
+
itemId: string;
|
|
107
|
+
blockId: string;
|
|
108
|
+
revision: string;
|
|
109
|
+
choiceId: string;
|
|
110
|
+
label: string;
|
|
111
|
+
sourceAt: number;
|
|
112
|
+
older: boolean;
|
|
113
|
+
values?: Record<string, unknown> | undefined;
|
|
114
|
+
at: number;
|
|
115
|
+
} | undefined;
|
|
100
116
|
}[] | undefined;
|
|
101
117
|
queuePaused?: boolean | undefined;
|
|
102
118
|
background?: {
|
|
@@ -174,6 +190,8 @@ export declare class ChatCore {
|
|
|
174
190
|
protected readonly subagents: SubagentReader;
|
|
175
191
|
readonly claudeProjectsDir: string;
|
|
176
192
|
readonly composerPreferences: ComposerPreferences;
|
|
193
|
+
private readonly uiQueries;
|
|
194
|
+
private readonly uiFenceLanguage;
|
|
177
195
|
private readonly checkpoints;
|
|
178
196
|
private readonly instructions;
|
|
179
197
|
private readonly skillIndex;
|
|
@@ -251,6 +269,44 @@ export declare class ChatCore {
|
|
|
251
269
|
queued: boolean;
|
|
252
270
|
turnId: string;
|
|
253
271
|
}>;
|
|
272
|
+
linkUi(payload: ChatUiLinkPayload, clientId: string): Promise<{
|
|
273
|
+
state: "chip" | "plain";
|
|
274
|
+
target?: {
|
|
275
|
+
type: "File";
|
|
276
|
+
path: string;
|
|
277
|
+
line?: number | undefined;
|
|
278
|
+
} | {
|
|
279
|
+
type: "Diff";
|
|
280
|
+
path: string;
|
|
281
|
+
} | {
|
|
282
|
+
type: "Commit";
|
|
283
|
+
sha: string;
|
|
284
|
+
} | {
|
|
285
|
+
type: "Node";
|
|
286
|
+
id: string;
|
|
287
|
+
} | undefined;
|
|
288
|
+
cwd?: string | undefined;
|
|
289
|
+
viewId?: string | undefined;
|
|
290
|
+
projectId?: string | undefined;
|
|
291
|
+
staged?: boolean | undefined;
|
|
292
|
+
conflicted?: boolean | undefined;
|
|
293
|
+
relativePath?: string | undefined;
|
|
294
|
+
label?: string | undefined;
|
|
295
|
+
code?: string | undefined;
|
|
296
|
+
reason?: string | undefined;
|
|
297
|
+
}>;
|
|
298
|
+
queryUi(payload: ChatUiQueryPayload, clientId: string): Promise<{
|
|
299
|
+
state: "failed" | "fresh" | "refused";
|
|
300
|
+
value?: unknown;
|
|
301
|
+
readId?: string | undefined;
|
|
302
|
+
readAt: number;
|
|
303
|
+
code?: string | undefined;
|
|
304
|
+
reason?: string | undefined;
|
|
305
|
+
}>;
|
|
306
|
+
choose(payload: ChatUiChoicePayload, clientId: string): Promise<{
|
|
307
|
+
queued: boolean;
|
|
308
|
+
turnId: string;
|
|
309
|
+
}>;
|
|
254
310
|
attachment(chatId: string, id: string): ChatAttachment | null;
|
|
255
311
|
unqueue(chatId: string, messageId: string): ChatQueuedMessage;
|
|
256
312
|
sendNow(chatId: string, messageId: string): void;
|
package/dist/chat/chat-core.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { ChatAttachmentUploadsSchema } from '@adecore/agent-contracts';
|
|
3
|
+
import { ChatAttachmentUploadsSchema, generatedImageAttachment } from '@adecore/agent-contracts';
|
|
4
|
+
import { ChatUiQueries } from './ui-queries.js';
|
|
4
5
|
import { ClientSinks } from '../client-sinks.js';
|
|
5
6
|
import { errorText } from '../error-text.js';
|
|
6
7
|
import { AccountError, definedEnv, isDefaultAccountOf, launchEnv, storedAccount } from '../providers/accounts/launch.js';
|
|
@@ -10,6 +11,7 @@ import { ChatLog, COMPACT_ABOVE_BYTES } from './chat-log.js';
|
|
|
10
11
|
import { ChatSession } from './chat-session.js';
|
|
11
12
|
import { ComposerPreferences } from './composer-preferences.js';
|
|
12
13
|
import { DeltaCoalescer } from './delta-coalescer.js';
|
|
14
|
+
import { readableAssistantText } from './readable-text.js';
|
|
13
15
|
import { ChatError } from './errors.js';
|
|
14
16
|
import { commandLabel, isBackgroundWork, runsInBackground } from './background-work.js';
|
|
15
17
|
import { SubagentReader } from './subagent-reader.js';
|
|
@@ -24,7 +26,8 @@ function isBookmarkable(item) {
|
|
|
24
26
|
/* The start of a message on one line; a message of only attachments is its file names. */
|
|
25
27
|
function excerptOf(item) {
|
|
26
28
|
const files = item.kind === 'user' ? (item.attachments ?? []).map((attachment) => attachment.name).join(', ') : '';
|
|
27
|
-
|
|
29
|
+
const text = item.kind === 'assistant' ? readableAssistantText(item) : item.text;
|
|
30
|
+
return (text.trim() === '' ? files : text).replace(/\s+/g, ' ').trim();
|
|
28
31
|
}
|
|
29
32
|
// One process worked on the turn and one more may take it up after a restart; a loop of resumes could redo a command forever.
|
|
30
33
|
const MAX_ATTEMPTS = 2;
|
|
@@ -59,6 +62,8 @@ export class ChatCore {
|
|
|
59
62
|
/* Where Claude Code keeps the projects of its default account on this machine; empty when it has none. */
|
|
60
63
|
claudeProjectsDir;
|
|
61
64
|
composerPreferences = new ComposerPreferences();
|
|
65
|
+
uiQueries;
|
|
66
|
+
uiFenceLanguage;
|
|
62
67
|
checkpoints;
|
|
63
68
|
instructions;
|
|
64
69
|
skillIndex;
|
|
@@ -72,7 +77,7 @@ export class ChatCore {
|
|
|
72
77
|
// How big each record was the last time it went to disk, and the writes waiting for a big one.
|
|
73
78
|
sizes = new Map();
|
|
74
79
|
waiting = new Map();
|
|
75
|
-
// What the last record held that no event carries (preambles, the host's extras), as its JSON.
|
|
80
|
+
// What the last record held that no event carries (preambles, the host's extras, captured UI access), as its JSON.
|
|
76
81
|
unlogged = new Map();
|
|
77
82
|
// The write in flight per chat, so the next one queues behind it instead of racing it.
|
|
78
83
|
writes = new Map();
|
|
@@ -84,6 +89,8 @@ export class ChatCore {
|
|
|
84
89
|
onInterruptedRun;
|
|
85
90
|
limitResume;
|
|
86
91
|
constructor(options) {
|
|
92
|
+
this.uiQueries = options.intelligentUi ? new ChatUiQueries(options.intelligentUi) : null;
|
|
93
|
+
this.uiFenceLanguage = options.uiFenceLanguage;
|
|
87
94
|
this.providers = options.providers;
|
|
88
95
|
this.store = options.store ?? null;
|
|
89
96
|
this.bookmarks = options.bookmarks ?? null;
|
|
@@ -269,8 +276,11 @@ export class ChatCore {
|
|
|
269
276
|
const references = this.referencesFor(payload.chatId);
|
|
270
277
|
const session = new ChatSession({
|
|
271
278
|
info,
|
|
279
|
+
...(this.uiFenceLanguage ? { uiFenceLanguage: this.uiFenceLanguage } : {}),
|
|
280
|
+
...(this.uiQueries ? { ui: { schemas: this.uiQueries.schemas, observe: (item, final) => this.uiQueries.observe(session, item, final) } } : {}),
|
|
272
281
|
items: stored?.items ?? [],
|
|
273
282
|
preambles: stored?.preambles ?? [],
|
|
283
|
+
uiAccess: stored?.uiAccess ?? {},
|
|
274
284
|
provider,
|
|
275
285
|
command: this.commands[kind] ?? provider.command,
|
|
276
286
|
...(this.spawn ? { spawn: this.spawn } : {}),
|
|
@@ -279,6 +289,7 @@ export class ChatCore {
|
|
|
279
289
|
this.accounts?.launched?.(kind, chatAccount);
|
|
280
290
|
return env;
|
|
281
291
|
},
|
|
292
|
+
saveGeneratedImage: (ref, upload) => this.attachments.saveGenerated(payload.chatId, ref, upload),
|
|
282
293
|
instructions: () => this.instructionsFor(payload.chatId),
|
|
283
294
|
resumeNote: () => this.resumeNoteFor(payload.chatId),
|
|
284
295
|
folders: () => this.foldersFor(payload.chatId),
|
|
@@ -362,11 +373,16 @@ export class ChatCore {
|
|
|
362
373
|
const log = this.logs.get(chatId);
|
|
363
374
|
const events = since === undefined ? null : log.after(since);
|
|
364
375
|
if (events !== null) {
|
|
365
|
-
return { info: session.info, items: [], events, seq: log.seq };
|
|
376
|
+
return { info: session.info, items: [], events: [...events, ...session.uiPreviews], seq: log.seq };
|
|
366
377
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
378
|
+
const page = historyLimit === undefined ? session.thread.snapshot() : session.thread.history(historyLimit);
|
|
379
|
+
return {
|
|
380
|
+
...page,
|
|
381
|
+
info: session.info,
|
|
382
|
+
items: session.previewItems(page.items),
|
|
383
|
+
...(historyLimit === undefined ? {} : { pending: session.previewItems(session.thread.pending()) }),
|
|
384
|
+
seq: log.seq
|
|
385
|
+
};
|
|
370
386
|
}
|
|
371
387
|
/*
|
|
372
388
|
* `attach` with the chat's bookmarks and visuals. They are read before the client joins, so a
|
|
@@ -548,6 +564,31 @@ export class ChatCore {
|
|
|
548
564
|
const attachments = await Promise.all(uploads.map((upload) => this.attachments.save(chatId, upload)));
|
|
549
565
|
return session.sendInput(text, { ...extras, ...(attachments.length > 0 ? { attachments } : {}) });
|
|
550
566
|
}
|
|
567
|
+
async linkUi(payload, clientId) {
|
|
568
|
+
if (!this.attached.get(payload.chatId)?.has(clientId) || !this.uiQueries) {
|
|
569
|
+
throw new ChatError('refused-query', 'Attach to the chat before resolving its UI links.');
|
|
570
|
+
}
|
|
571
|
+
return this.uiQueries.link(this.require(payload.chatId), payload);
|
|
572
|
+
}
|
|
573
|
+
async queryUi(payload, clientId) {
|
|
574
|
+
if (!this.attached.get(payload.chatId)?.has(clientId)) {
|
|
575
|
+
throw new ChatError('refused-query', 'Attach to the chat before reading its UI queries.');
|
|
576
|
+
}
|
|
577
|
+
if (!this.uiQueries) {
|
|
578
|
+
throw new ChatError('refused-query', 'This host does not provide UI query sources.');
|
|
579
|
+
}
|
|
580
|
+
return this.uiQueries.query(this.require(payload.chatId), payload);
|
|
581
|
+
}
|
|
582
|
+
async choose(payload, clientId) {
|
|
583
|
+
if (!this.attached.get(payload.chatId)?.has(clientId)) {
|
|
584
|
+
throw new ChatError('refused-query', 'Attach to the chat before answering its UI.');
|
|
585
|
+
}
|
|
586
|
+
const session = this.require(payload.chatId);
|
|
587
|
+
const queries = await this.uiQueries?.choiceValues(session, payload);
|
|
588
|
+
const result = session.choose(payload, queries);
|
|
589
|
+
await this.persisted(payload.chatId);
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
551
592
|
/* The file behind an attachment id: what this chat's thread or queue says it is, or a visual's page. */
|
|
552
593
|
attachment(chatId, id) {
|
|
553
594
|
const session = this.chats.get(chatId);
|
|
@@ -555,7 +596,8 @@ export class ChatCore {
|
|
|
555
596
|
return null;
|
|
556
597
|
}
|
|
557
598
|
for (const item of session.thread.list()) {
|
|
558
|
-
const
|
|
599
|
+
const generated = generatedImageAttachment(item);
|
|
600
|
+
const found = item.kind === 'user' ? item.attachments?.find((attachment) => attachment.id === id) : generated?.id === id ? generated : null;
|
|
559
601
|
if (found) {
|
|
560
602
|
return found;
|
|
561
603
|
}
|
|
@@ -599,7 +641,7 @@ export class ChatCore {
|
|
|
599
641
|
throw new ChatError('chat-busy', `Chat ${chatId} is still working on the previous message`);
|
|
600
642
|
}
|
|
601
643
|
await this.clearing(chatId);
|
|
602
|
-
session.clear(force);
|
|
644
|
+
const ended = session.clear(force);
|
|
603
645
|
// Before the record is written, so what it takes along from the record is gone from the write as well.
|
|
604
646
|
const cleared = this.cleared(chatId);
|
|
605
647
|
// A debounced write still waiting holds the old thread and must not land after the empty one.
|
|
@@ -607,7 +649,7 @@ export class ChatCore {
|
|
|
607
649
|
// Folded right away: every line before the reset describes a thread that is gone.
|
|
608
650
|
await Promise.all([
|
|
609
651
|
this.persistNow(chatId, true),
|
|
610
|
-
this.attachments.removeAll(chatId),
|
|
652
|
+
ended.then(() => this.attachments.removeAll(chatId)),
|
|
611
653
|
this.bookmarks?.removeChat(chatId),
|
|
612
654
|
this.visuals?.removeChat(chatId),
|
|
613
655
|
cleared
|
|
@@ -646,8 +688,9 @@ export class ChatCore {
|
|
|
646
688
|
const session = this.require(chatId);
|
|
647
689
|
// A write already out would put the record back after the delete.
|
|
648
690
|
const writing = this.writes.get(chatId);
|
|
649
|
-
|
|
691
|
+
const ended = session.dispose();
|
|
650
692
|
this.subagents.releaseChat(chatId);
|
|
693
|
+
this.uiQueries?.forget(chatId);
|
|
651
694
|
this.coalescers.get(chatId)?.dispose();
|
|
652
695
|
this.coalescers.delete(chatId);
|
|
653
696
|
this.logs.get(chatId)?.close();
|
|
@@ -663,6 +706,7 @@ export class ChatCore {
|
|
|
663
706
|
const snapshot = session.thread.snapshot();
|
|
664
707
|
this.forgotten(chatId);
|
|
665
708
|
await writing;
|
|
709
|
+
await ended;
|
|
666
710
|
await Promise.all([
|
|
667
711
|
this.store?.delete(chatId),
|
|
668
712
|
this.attachments.removeAll(chatId),
|
|
@@ -802,7 +846,7 @@ export class ChatCore {
|
|
|
802
846
|
const log = this.logs.get(chatId);
|
|
803
847
|
// Not folded here: an older write still in flight may land after this one, and the log is what covers for it.
|
|
804
848
|
try {
|
|
805
|
-
this.store.writeSync(chatId, info, items, { seq: log?.seq ?? 0, resetSeq: log?.resetSeq ?? 0 }, session.preambles, this.recordExtras(chatId));
|
|
849
|
+
this.store.writeSync(chatId, info, items, { seq: log?.seq ?? 0, resetSeq: log?.resetSeq ?? 0 }, session.preambles, this.recordExtras(chatId), session.uiAccessRecord);
|
|
806
850
|
}
|
|
807
851
|
catch (e) {
|
|
808
852
|
console.error(`Chat record for ${chatId} failed:`, errorText(e));
|
|
@@ -935,7 +979,8 @@ export class ChatCore {
|
|
|
935
979
|
// A delta held back is already in the thread, so it gets its seq before the snapshot says where it ends.
|
|
936
980
|
this.coalescers.get(chatId)?.flush();
|
|
937
981
|
const extras = this.recordExtras(chatId);
|
|
938
|
-
const
|
|
982
|
+
const uiAccess = session.uiAccessRecord;
|
|
983
|
+
const unlogged = JSON.stringify([session.preambles, extras, uiAccess]);
|
|
939
984
|
const folds = fold || log.size > COMPACT_ABOVE_BYTES || !log.onDisk;
|
|
940
985
|
// The log already holds every change to the thread, so the whole record is rewritten only to fold
|
|
941
986
|
// the log, on a chat's first write, or for what no event carries.
|
|
@@ -944,7 +989,7 @@ export class ChatCore {
|
|
|
944
989
|
}
|
|
945
990
|
const { info, items } = session.thread.snapshot();
|
|
946
991
|
const at = { seq: log.seq, resetSeq: log.resetSeq };
|
|
947
|
-
this.sizes.set(chatId, await this.store.write(chatId, info, items, at, session.preambles, extras));
|
|
992
|
+
this.sizes.set(chatId, await this.store.write(chatId, info, items, at, session.preambles, extras, uiAccess));
|
|
948
993
|
this.unlogged.set(chatId, unlogged);
|
|
949
994
|
// A chat killed while the write was out has no log left to fold.
|
|
950
995
|
if (this.logs.get(chatId) === log && folds) {
|
|
@@ -955,6 +1000,14 @@ export class ChatCore {
|
|
|
955
1000
|
return next;
|
|
956
1001
|
}
|
|
957
1002
|
emit(chatId, event) {
|
|
1003
|
+
if (event.type === 'delta' && event.ui !== undefined) {
|
|
1004
|
+
// Previews follow all pending text but have no log position or observer side effects.
|
|
1005
|
+
this.coalescers.get(chatId)?.flush();
|
|
1006
|
+
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
1007
|
+
this.sinks.to(clientId, { event: 'chat.event', payload: { chatId, event } });
|
|
1008
|
+
}
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
958
1011
|
this.activity.set(chatId, Date.now());
|
|
959
1012
|
let coalescer = this.coalescers.get(chatId);
|
|
960
1013
|
if (!coalescer) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { ChatAttachment, ChatCheckpointDiff, ChatEvent, ChatInfo, ChatItem, ChatQueuedMessage, ChatSkill, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ChatAttachment, ChatAssistantItem, ChatCheckpointDiff, ChatEvent, ChatInfo, ChatItem, ChatQueuedMessage, ChatSkill, ChatUiChoiceOrigin, ChatUiChoicePayload, ChatUiQueryState, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
|
|
2
|
+
import type { UiCompileOptions } from '@adecore/intelligent-ui/compiler';
|
|
2
3
|
import type { ChatProvider } from '../providers/provider.ts';
|
|
3
4
|
import type { LimitsUpdate } from '../usage/limits/normalize.ts';
|
|
5
|
+
import type { BackendLaunch } from './backend.ts';
|
|
4
6
|
import type { SpawnChatProcess } from './chat-process.ts';
|
|
5
7
|
import type { ChatTitleInput } from './chat-title.ts';
|
|
6
8
|
import type { SubagentSettlement } from './subagent-settlement.ts';
|
|
@@ -30,16 +32,23 @@ export interface ChatSessionOptions {
|
|
|
30
32
|
clock?: OutboxClock;
|
|
31
33
|
items?: ChatItem[];
|
|
32
34
|
preambles?: string[];
|
|
35
|
+
uiAccess?: Readonly<Record<string, unknown>>;
|
|
33
36
|
provider: ChatProvider;
|
|
34
37
|
command: string[];
|
|
35
38
|
env(account: string | undefined): Record<string, string>;
|
|
36
39
|
spawn?: SpawnChatProcess;
|
|
40
|
+
saveGeneratedImage?: BackendLaunch['saveGeneratedImage'];
|
|
37
41
|
instructions?(): string | null;
|
|
38
42
|
resumeNote?(): string | null;
|
|
39
43
|
folders?(): readonly string[];
|
|
40
44
|
promptNotes?: PromptNotes;
|
|
41
45
|
references?: ChatReferences;
|
|
42
46
|
checkpoints?: TurnCheckpoints;
|
|
47
|
+
uiFenceLanguage?: string;
|
|
48
|
+
ui?: {
|
|
49
|
+
schemas: UiCompileOptions['querySchemas'];
|
|
50
|
+
observe(item: ChatAssistantItem, final: boolean): void;
|
|
51
|
+
};
|
|
43
52
|
emit(event: ChatEvent): void;
|
|
44
53
|
onLimits?(update: LimitsUpdate): void;
|
|
45
54
|
persist(): void;
|
|
@@ -58,6 +67,7 @@ export interface LimitResumeHooks {
|
|
|
58
67
|
owed(): boolean;
|
|
59
68
|
}
|
|
60
69
|
export interface ChatSendExtras {
|
|
70
|
+
uiChoice?: ChatUiChoiceOrigin;
|
|
61
71
|
delivery?: 'steer' | 'queue';
|
|
62
72
|
mentions?: string[];
|
|
63
73
|
skills?: string[];
|
|
@@ -95,7 +105,16 @@ export declare class ChatSession {
|
|
|
95
105
|
private pendingPreambles;
|
|
96
106
|
private submission;
|
|
97
107
|
private readonly steering;
|
|
108
|
+
private readonly uiStreams;
|
|
109
|
+
private readonly uiCompiled;
|
|
110
|
+
private readonly capturedUiAccess;
|
|
111
|
+
private readonly uiScanned;
|
|
112
|
+
private readonly uiReferencing;
|
|
113
|
+
private readonly uiAttachments;
|
|
114
|
+
private readonly uiFenceLanguage;
|
|
98
115
|
constructor(options: ChatSessionOptions);
|
|
116
|
+
get uiPreviews(): ChatEvent[];
|
|
117
|
+
previewItems(items: ChatItem[]): ChatItem[];
|
|
99
118
|
get id(): string;
|
|
100
119
|
get info(): ChatInfo;
|
|
101
120
|
get running(): boolean;
|
|
@@ -112,6 +131,15 @@ export declare class ChatSession {
|
|
|
112
131
|
queued: boolean;
|
|
113
132
|
turnId: string;
|
|
114
133
|
};
|
|
134
|
+
uiAccess(itemId: string): unknown;
|
|
135
|
+
setUiAccess(itemId: string, access: unknown): void;
|
|
136
|
+
get uiAccessRecord(): Record<string, unknown>;
|
|
137
|
+
updateUiQueries(itemId: string, uiQueries: ChatUiQueryState): void;
|
|
138
|
+
choose(payload: ChatUiChoicePayload, queries?: Readonly<Record<string, unknown>>): {
|
|
139
|
+
queued: boolean;
|
|
140
|
+
turnId: string;
|
|
141
|
+
};
|
|
142
|
+
private recordUiAnswer;
|
|
115
143
|
sendInput(text: string, extras?: ChatSendExtras): Promise<{
|
|
116
144
|
queued: boolean;
|
|
117
145
|
turnId: string;
|
|
@@ -129,7 +157,7 @@ export declare class ChatSession {
|
|
|
129
157
|
approve(requestId: string, decision: 'allow' | 'allow-always' | 'deny', message?: string): boolean;
|
|
130
158
|
answer(requestId: string, answers: Record<string, string>): boolean;
|
|
131
159
|
dismiss(itemId: string): boolean;
|
|
132
|
-
clear(force: boolean): void
|
|
160
|
+
clear(force: boolean): Promise<void>;
|
|
133
161
|
settleStored(selection: ModelSelection, resume: ResumeDecision): void;
|
|
134
162
|
resume(turnId: string, attempt: number, words: ResumeWords): Promise<void>;
|
|
135
163
|
abandon(turnId: string, reason: string | null): void;
|
|
@@ -208,6 +236,11 @@ export declare class ChatSession {
|
|
|
208
236
|
private settleFromTranscripts;
|
|
209
237
|
private settleRows;
|
|
210
238
|
private emit;
|
|
239
|
+
private unscannedUiText;
|
|
240
|
+
private forgetUiScan;
|
|
241
|
+
private latestUiAttachment;
|
|
242
|
+
private findUiAttachment;
|
|
243
|
+
private clearUi;
|
|
211
244
|
private delegatingChange;
|
|
212
245
|
}
|
|
213
246
|
export interface ResumeDecision {
|