@adecore/agents 0.23.1 → 0.24.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/README.md +3 -3
- package/dist/chat/attachment-store.js +4 -5
- package/dist/chat/backend.d.ts +6 -1
- package/dist/chat/background-work.d.ts +1 -1
- package/dist/chat/background-work.js +2 -1
- package/dist/chat/chat-core.d.ts +53 -161
- package/dist/chat/chat-core.js +146 -112
- package/dist/chat/chat-log.js +3 -3
- package/dist/chat/chat-process.d.ts +21 -0
- package/dist/chat/chat-process.js +109 -9
- package/dist/chat/chat-session.d.ts +10 -17
- package/dist/chat/chat-session.js +141 -182
- package/dist/chat/chat-store.d.ts +1 -0
- package/dist/chat/chat-store.js +105 -1
- package/dist/chat/claude-backend.d.ts +4 -0
- package/dist/chat/claude-backend.js +51 -27
- package/dist/chat/claude-protocol.d.ts +12 -0
- package/dist/chat/claude-protocol.js +137 -7
- package/dist/chat/claude-title.d.ts +1 -3
- package/dist/chat/claude-title.js +11 -39
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +36 -17
- package/dist/chat/codex-backend.d.ts +21 -1
- package/dist/chat/codex-backend.js +168 -10
- package/dist/chat/codex-protocol.d.ts +2 -0
- package/dist/chat/codex-protocol.js +37 -3
- package/dist/chat/codex-thread.js +1 -3
- package/dist/chat/codex-transport.d.ts +3 -0
- package/dist/chat/codex-transport.js +38 -35
- package/dist/chat/context-breakdown.js +2 -2
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/external-sessions.js +2 -2
- package/dist/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/dist/chat/index.d.ts +29 -0
- package/dist/chat/index.js +23 -0
- package/dist/chat/projector.d.ts +2 -1
- package/dist/chat/projector.js +12 -3
- package/dist/chat/request-summary.js +1 -4
- package/dist/chat/subagent-settlement.js +1 -3
- package/dist/chat/thread.d.ts +3 -0
- package/dist/chat/thread.js +20 -3
- package/dist/chat/ui-queries.d.ts +2 -0
- package/dist/chat/ui-queries.js +40 -5
- package/dist/chat/ui-replies.d.ts +33 -0
- package/dist/chat/ui-replies.js +152 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +3 -0
- package/dist/coordination/index.d.ts +15 -0
- package/dist/coordination/index.js +16 -0
- package/dist/host/handlers.js +8 -0
- package/dist/host/index.d.ts +7 -0
- package/dist/host/index.js +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/claude.js +3 -1
- package/dist/providers/codex.js +3 -1
- package/dist/providers/index.d.ts +14 -0
- package/dist/providers/index.js +12 -0
- package/dist/run-process.js +54 -14
- package/dist/skills.d.ts +1 -0
- package/dist/skills.js +4 -0
- package/dist/tasks/task-coordinator.d.ts +4 -0
- package/dist/tasks/task-coordinator.js +16 -2
- package/dist/tasks/wiring.js +7 -1
- package/dist/{providers/accounts/test-accounts.d.ts → testing/accounts.d.ts} +2 -2
- package/dist/{providers/accounts/test-accounts.js → testing/accounts.js} +3 -3
- package/dist/testing/bin/fake-claude.d.ts +1 -0
- package/dist/testing/bin/fake-claude.js +6 -0
- package/dist/testing/bin/fake-codex.d.ts +1 -0
- package/dist/testing/bin/fake-codex.js +6 -0
- package/dist/{chat → testing}/fake-claude.d.ts +1 -1
- package/dist/{chat → testing}/fake-claude.js +0 -4
- package/dist/{chat → testing}/fake-cli.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.js +70 -8
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +7 -0
- package/dist/{outbox → testing}/manual-clock.d.ts +1 -1
- package/dist/{watch-test-helpers.d.ts → testing/watch.d.ts} +1 -1
- package/dist/usage/exchange.d.ts +5 -3
- package/dist/usage/exchange.js +25 -16
- package/dist/usage/index-file.d.ts +1 -0
- package/dist/usage/index-file.js +20 -2
- package/dist/usage/index.d.ts +9 -0
- package/dist/usage/index.js +6 -0
- package/dist/usage/limits/normalize.js +1 -3
- package/dist/usage/pricing.d.ts +5 -3
- package/dist/usage/pricing.js +28 -19
- package/dist/usage/scanner.js +39 -11
- package/dist/usage/usage-service.js +8 -3
- package/package.json +72 -11
- package/src/chat/attachment-store.ts +4 -6
- package/src/chat/backend.ts +8 -0
- package/src/chat/background-work.ts +2 -2
- package/src/chat/chat-core.ts +221 -143
- package/src/chat/chat-log.ts +3 -3
- package/src/chat/chat-process.ts +128 -8
- package/src/chat/chat-session.ts +153 -204
- package/src/chat/chat-store.ts +104 -1
- package/src/chat/claude-backend.ts +58 -26
- package/src/chat/claude-protocol.ts +147 -8
- package/src/chat/claude-title.ts +11 -39
- package/src/chat/claude-transcript.ts +38 -18
- package/src/chat/codex-backend.ts +182 -20
- package/src/chat/codex-protocol.ts +40 -5
- package/src/chat/codex-thread.ts +1 -4
- package/src/chat/codex-transport.ts +45 -35
- package/src/chat/context-breakdown.ts +2 -2
- package/src/chat/errors.ts +1 -0
- package/src/chat/external-sessions.ts +2 -2
- package/src/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/src/chat/index.ts +49 -0
- package/src/chat/projector.ts +13 -4
- package/src/chat/request-summary.ts +8 -5
- package/src/chat/subagent-settlement.ts +1 -4
- package/src/chat/thread.ts +22 -3
- package/src/chat/ui-queries.ts +46 -5
- package/src/chat/ui-replies.ts +187 -0
- package/src/context/index.ts +26 -0
- package/src/coordination/index.ts +67 -0
- package/src/host/handlers.ts +8 -0
- package/src/host/index.ts +8 -0
- package/src/index.ts +20 -0
- package/src/providers/accounts/status.ts +1 -4
- package/src/providers/claude.ts +3 -1
- package/src/providers/codex.ts +3 -1
- package/src/providers/index.ts +15 -0
- package/src/run-process.ts +56 -16
- package/src/skills.ts +5 -0
- package/src/tasks/task-coordinator.ts +22 -2
- package/src/tasks/wiring.ts +7 -1
- package/src/{providers/accounts/test-accounts.ts → testing/accounts.ts} +4 -4
- package/src/testing/bin/fake-claude.ts +7 -0
- package/src/testing/bin/fake-codex.ts +7 -0
- package/src/{chat → testing}/fake-claude.ts +1 -5
- package/src/{chat → testing}/fake-cli.ts +1 -1
- package/src/{chat → testing}/fake-codex.ts +75 -6
- package/src/testing/index.ts +7 -0
- package/src/{outbox → testing}/manual-clock.ts +1 -1
- package/src/{watch-test-helpers.ts → testing/watch.ts} +1 -1
- package/src/usage/exchange.ts +31 -20
- package/src/usage/index-file.ts +23 -2
- package/src/usage/index.ts +10 -0
- package/src/usage/limits/normalize.ts +1 -4
- package/src/usage/pricing.ts +34 -23
- package/src/usage/scanner.ts +48 -11
- package/src/usage/usage-service.ts +8 -3
- /package/dist/{chat → testing}/fake-cli.js +0 -0
- /package/dist/{outbox → testing}/manual-clock.js +0 -0
- /package/dist/{watch-test-helpers.js → testing/watch.js} +0 -0
package/dist/chat/chat-core.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { ChatAttachmentUploadsSchema, generatedImageAttachment } from '@adecore/agent-contracts';
|
|
3
|
+
import { ChatAttachmentUploadsSchema, generatedImageAttachment, HOST_TASK_ORIGIN } from '@adecore/agent-contracts';
|
|
4
4
|
import { ClientSinks } from '../client-sinks.js';
|
|
5
5
|
import { errorText } from '../error-text.js';
|
|
6
6
|
import { AccountError, definedEnv, isDefaultAccountOf, launchEnv, storedAccount } from '../providers/accounts/launch.js';
|
|
@@ -42,21 +42,25 @@ const DEFAULT_RESUME_WORDS = {
|
|
|
42
42
|
/*
|
|
43
43
|
* The chats of one host: a `ChatSession` per chat, its record and its log, and the clients that read
|
|
44
44
|
* them. Everything here holds for any host. What a host adds (notes for the agent, rows of its own,
|
|
45
|
-
* what a clear or a removal takes along) it adds
|
|
46
|
-
*
|
|
45
|
+
* what a clear or a removal takes along) it adds through `ChatCoreOptions.hooks`. A subclass may add
|
|
46
|
+
* methods of its own over the public ones; the state behind them stays here.
|
|
47
47
|
*/
|
|
48
48
|
export class ChatCore {
|
|
49
|
-
|
|
50
|
-
store;
|
|
49
|
+
// The stores the host handed in, which a subclass reads to copy a chat's bookmarks or visuals.
|
|
51
50
|
bookmarks;
|
|
52
51
|
visuals;
|
|
52
|
+
providers;
|
|
53
|
+
store;
|
|
53
54
|
accounts;
|
|
54
55
|
attachments;
|
|
55
56
|
env;
|
|
57
|
+
hooks;
|
|
56
58
|
chats = new Map();
|
|
57
59
|
creating = new Map();
|
|
58
60
|
sinks = new ClientSinks((clientId) => this.composerPreferences.forget(clientId));
|
|
59
61
|
attached = new Map();
|
|
62
|
+
// How often a chat's bookmarks or visuals changed, so an attach can tell that its read went stale before it joined.
|
|
63
|
+
listChanges = new Map();
|
|
60
64
|
coalescers = new Map();
|
|
61
65
|
subagents;
|
|
62
66
|
/* Where Claude Code keeps the projects of its default account on this machine; empty when it has none. */
|
|
@@ -105,6 +109,7 @@ export class ChatCore {
|
|
|
105
109
|
this.nameChat = options.nameChat ?? null;
|
|
106
110
|
this.onInterruptedRun = options.onInterruptedRun ?? null;
|
|
107
111
|
this.limitResume = options.limitResume ?? null;
|
|
112
|
+
this.hooks = options.hooks ?? {};
|
|
108
113
|
this.commands = {
|
|
109
114
|
...(options.command ? { claude: options.command } : {}),
|
|
110
115
|
...(options.codexCommand ? { codex: options.codexCommand } : {})
|
|
@@ -114,11 +119,13 @@ export class ChatCore {
|
|
|
114
119
|
this.env = definedEnv(options.env ?? process.env);
|
|
115
120
|
// Only whoever reads the thread has anything to point a bookmark at or to place a visual in, so the lists go where the thread goes.
|
|
116
121
|
this.bookmarks?.listen((chatId, bookmarks) => {
|
|
122
|
+
this.listChanged(chatId);
|
|
117
123
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
118
124
|
this.sinks.to(clientId, { event: 'chat.bookmarks', payload: { chatId, bookmarks } });
|
|
119
125
|
}
|
|
120
126
|
});
|
|
121
127
|
this.visuals?.listen((chatId, visuals) => {
|
|
128
|
+
this.listChanged(chatId);
|
|
122
129
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
123
130
|
this.sinks.to(clientId, { event: 'chat.visuals', payload: { chatId, visuals } });
|
|
124
131
|
}
|
|
@@ -213,6 +220,10 @@ export class ChatCore {
|
|
|
213
220
|
subscribe(clientId, sink) {
|
|
214
221
|
return this.sinks.subscribe(clientId, sink);
|
|
215
222
|
}
|
|
223
|
+
/* Sends one client an event of the host's own, such as a change to something it follows that no chat event carries. */
|
|
224
|
+
sendTo(clientId, event) {
|
|
225
|
+
this.sinks.to(clientId, event);
|
|
226
|
+
}
|
|
216
227
|
/* Registers a chat; nothing is spawned until the first message. An existing chat answers its current info. */
|
|
217
228
|
create(payload) {
|
|
218
229
|
// A create in flight has the chat in the map before its first prompt is sent, so a second caller waits for all of it.
|
|
@@ -237,19 +248,20 @@ export class ChatCore {
|
|
|
237
248
|
const kind = stored?.info.provider ?? payload.provider ?? 'claude';
|
|
238
249
|
const provider = this.providers.get(kind);
|
|
239
250
|
const catalog = provider.catalog;
|
|
240
|
-
const selection = catalog.normalize(stored?.info.selection ?? this.openingSelection(payload.chatId, kind) ?? payload.selection);
|
|
251
|
+
const selection = catalog.normalize(stored?.info.selection ?? this.hooks.openingSelection?.(payload.chatId, kind) ?? payload.selection);
|
|
241
252
|
const cwd = stored?.info.cwd ?? payload.cwd ?? this.env.HOME ?? homedir();
|
|
242
|
-
await this.admit(payload.chatId, cwd);
|
|
253
|
+
await this.hooks.admit?.(payload.chatId, cwd);
|
|
243
254
|
// A thread on disk keeps its account too. One it lost still opens, and says why on its next turn.
|
|
244
255
|
// A new one without an account takes the person's pick for this CLI, which is refused rather than replaced when it went.
|
|
245
256
|
const account = stored ? stored.info.account : storedAccount(kind, payload.account ?? this.composerPreferences.for(kind).account);
|
|
246
257
|
if (!stored) {
|
|
247
258
|
this.requireAccount(kind, account);
|
|
248
259
|
}
|
|
260
|
+
const narrowed = (mode) => (this.hooks.runtimeModeFor ? this.hooks.runtimeModeFor(payload.chatId, mode) : mode);
|
|
249
261
|
// Asked again for a chat on disk, since the host may have listed it since.
|
|
250
|
-
const hidden = this.hiddenFor(payload.chatId) ? { hidden: true } : {};
|
|
262
|
+
const hidden = this.hooks.hiddenFor?.(payload.chatId) ? { hidden: true } : {};
|
|
251
263
|
const info = stored?.info
|
|
252
|
-
? { ...withoutHidden(stored.info), runtimeMode:
|
|
264
|
+
? { ...withoutHidden(stored.info), runtimeMode: narrowed(stored.info.runtimeMode), ...hidden }
|
|
253
265
|
: {
|
|
254
266
|
...hidden,
|
|
255
267
|
chatId: payload.chatId,
|
|
@@ -259,7 +271,7 @@ export class ChatCore {
|
|
|
259
271
|
agentSessionId: payload.resume ?? null,
|
|
260
272
|
model: null,
|
|
261
273
|
selection,
|
|
262
|
-
runtimeMode:
|
|
274
|
+
runtimeMode: narrowed(payload.runtimeMode ?? 'full-access'),
|
|
263
275
|
status: 'idle',
|
|
264
276
|
running: false,
|
|
265
277
|
activeTurnId: null,
|
|
@@ -270,10 +282,10 @@ export class ChatCore {
|
|
|
270
282
|
const resume = stored ? await this.owedResume(payload.chatId, stored) : { resumeTurnId: null, reason: null };
|
|
271
283
|
const claudeTitles = this.claudeTitles;
|
|
272
284
|
const nameChat = this.nameChat;
|
|
273
|
-
const base = this.envFor(payload.chatId, this.env);
|
|
285
|
+
const base = this.hooks.envFor ? this.hooks.envFor(payload.chatId, this.env) : this.env;
|
|
274
286
|
this.logs.set(payload.chatId, await this.openLog(payload.chatId, stored ?? null));
|
|
275
|
-
const promptNotes = this.promptNotesFor(payload.chatId);
|
|
276
|
-
const references = this.referencesFor(payload.chatId);
|
|
287
|
+
const promptNotes = this.hooks.promptNotesFor?.(payload.chatId);
|
|
288
|
+
const references = this.hooks.referencesFor?.(payload.chatId);
|
|
277
289
|
const session = new ChatSession({
|
|
278
290
|
info,
|
|
279
291
|
...(this.uiFenceLanguage ? { uiFenceLanguage: this.uiFenceLanguage } : {}),
|
|
@@ -290,9 +302,9 @@ export class ChatCore {
|
|
|
290
302
|
return env;
|
|
291
303
|
},
|
|
292
304
|
saveGeneratedImage: (ref, upload) => this.attachments.saveGenerated(payload.chatId, ref, upload),
|
|
293
|
-
instructions: () => this.instructionsFor(payload.chatId),
|
|
294
|
-
resumeNote: () => this.resumeNoteFor(payload.chatId),
|
|
295
|
-
folders: () => this.foldersFor(payload.chatId),
|
|
305
|
+
instructions: () => (this.hooks.instructionsFor ? this.hooks.instructionsFor(payload.chatId) : this.instructions),
|
|
306
|
+
resumeNote: () => this.hooks.resumeNoteFor?.(payload.chatId) ?? null,
|
|
307
|
+
folders: () => this.hooks.foldersFor?.(payload.chatId) ?? [],
|
|
296
308
|
...(promptNotes ? { promptNotes } : {}),
|
|
297
309
|
...(references ? { references } : {}),
|
|
298
310
|
...(this.checkpoints ? { checkpoints: this.checkpoints } : {}),
|
|
@@ -302,7 +314,7 @@ export class ChatCore {
|
|
|
302
314
|
save: () => this.persistNow(payload.chatId),
|
|
303
315
|
persistSoon: () => this.persistSoon(payload.chatId),
|
|
304
316
|
...(kind === 'claude' && claudeTitles
|
|
305
|
-
? { readTitle: (agentSessionId) => claudeTitles.forSession(agentSessionId, this.claudeProjectsDirOf(session.info)) }
|
|
317
|
+
? { readTitle: (agentSessionId) => claudeTitles.forSession(agentSessionId, this.claudeProjectsDirOf(session.info), session.info.cwd) }
|
|
306
318
|
: {}),
|
|
307
319
|
...(kind === 'codex' && nameChat ? { nameThread: (input) => nameChat(kind, input) } : {}),
|
|
308
320
|
...(kind === 'claude' ? { subagentSettlement: (toolUseId) => this.subagents.claudeSettlement(payload.chatId, toolUseId) } : {}),
|
|
@@ -314,7 +326,7 @@ export class ChatCore {
|
|
|
314
326
|
session.settleOwedResume();
|
|
315
327
|
await session.settleOrphanedSubagents();
|
|
316
328
|
}
|
|
317
|
-
await this.opened(session, stored ?? null);
|
|
329
|
+
await this.hooks.opened?.(session, stored ?? null);
|
|
318
330
|
return session.info;
|
|
319
331
|
}
|
|
320
332
|
async storedBackground(chatId) {
|
|
@@ -322,7 +334,12 @@ export class ChatCore {
|
|
|
322
334
|
const items = record?.items ?? [];
|
|
323
335
|
const background = items.filter((item) => isBackgroundWork(item) && (runsInBackground(item) || items.some((note) => note.id === `lost-background-${item.id}`)));
|
|
324
336
|
const last = items.findLast((item) => item.kind === 'turn');
|
|
325
|
-
return {
|
|
337
|
+
return {
|
|
338
|
+
turnId: background.at(-1)?.turnId ?? last?.id ?? null,
|
|
339
|
+
items: background,
|
|
340
|
+
commands: (record?.info.background ?? []).map(commandLabel),
|
|
341
|
+
goal: record?.info.goal?.status === 'active'
|
|
342
|
+
};
|
|
326
343
|
}
|
|
327
344
|
save(chatId) {
|
|
328
345
|
return this.persistNow(chatId, true);
|
|
@@ -385,9 +402,10 @@ export class ChatCore {
|
|
|
385
402
|
};
|
|
386
403
|
}
|
|
387
404
|
/*
|
|
388
|
-
* `attach` with the chat's bookmarks and visuals. They are read before the client joins,
|
|
389
|
-
*
|
|
390
|
-
* between the two. A file that cannot be
|
|
405
|
+
* `attach` with the chat's bookmarks and visuals. They are read before the client joins, and
|
|
406
|
+
* read again when either changed during the read, so a change reaches the client in the answer
|
|
407
|
+
* or as `chat.bookmarks` or `chat.visuals` and never falls between the two. A file that cannot be
|
|
408
|
+
* read leaves its list out rather than the thread.
|
|
391
409
|
*/
|
|
392
410
|
async attachWithBookmarks(chatId, clientId, historyLimit, since) {
|
|
393
411
|
this.require(chatId);
|
|
@@ -395,13 +413,22 @@ export class ChatCore {
|
|
|
395
413
|
console.error(`The ${what} of chat ${chatId} could not be read:`, errorText(e));
|
|
396
414
|
return undefined;
|
|
397
415
|
};
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
416
|
+
let bookmarks;
|
|
417
|
+
let visuals;
|
|
418
|
+
let changes;
|
|
419
|
+
do {
|
|
420
|
+
changes = this.listChanges.get(chatId);
|
|
421
|
+
[bookmarks, visuals] = await Promise.all([
|
|
422
|
+
this.bookmarks?.read(chatId).catch(unreadable('bookmarks')),
|
|
423
|
+
this.visuals?.list(chatId).catch(unreadable('visuals'))
|
|
424
|
+
]);
|
|
425
|
+
} while (this.listChanges.get(chatId) !== changes);
|
|
402
426
|
const result = this.attach(chatId, clientId, historyLimit, since);
|
|
403
427
|
return { ...result, ...(bookmarks === undefined ? {} : { bookmarks }), ...(visuals === undefined ? {} : { visuals }) };
|
|
404
428
|
}
|
|
429
|
+
listChanged(chatId) {
|
|
430
|
+
this.listChanges.set(chatId, (this.listChanges.get(chatId) ?? 0) + 1);
|
|
431
|
+
}
|
|
405
432
|
/* Marks a message of the chat's own thread; a message that already has one keeps it. */
|
|
406
433
|
addBookmark(chatId, itemId, name) {
|
|
407
434
|
const item = this.require(chatId).thread.get(itemId);
|
|
@@ -451,6 +478,7 @@ export class ChatCore {
|
|
|
451
478
|
}
|
|
452
479
|
return this.visuals;
|
|
453
480
|
}
|
|
481
|
+
/* The page of a chat's thread before `cursor`, or its newest page without one. */
|
|
454
482
|
history(chatId, cursor, limit) {
|
|
455
483
|
const session = this.require(chatId);
|
|
456
484
|
this.coalescers.get(chatId)?.flush();
|
|
@@ -500,7 +528,7 @@ export class ChatCore {
|
|
|
500
528
|
if (row.status !== 'running') {
|
|
501
529
|
return;
|
|
502
530
|
}
|
|
503
|
-
if (row.origin ===
|
|
531
|
+
if (row.origin === HOST_TASK_ORIGIN) {
|
|
504
532
|
throw new ChatError('chat-unsupported', 'This row stands for work the host runs; stop it there');
|
|
505
533
|
}
|
|
506
534
|
session.stopSubagent(toolUseId);
|
|
@@ -510,6 +538,10 @@ export class ChatCore {
|
|
|
510
538
|
await this.loaded(chatId);
|
|
511
539
|
this.require(chatId).stopTask(taskId);
|
|
512
540
|
}
|
|
541
|
+
async goal(chatId, action) {
|
|
542
|
+
await this.loaded(chatId);
|
|
543
|
+
await this.require(chatId).goal(action);
|
|
544
|
+
}
|
|
513
545
|
/* The whole conversation of a subagent of the CLI's own, for an agent that reads it as text. */
|
|
514
546
|
async subagentItems(chatId, toolUseId) {
|
|
515
547
|
return this.subagents.readAll(chatId, toolUseId);
|
|
@@ -518,6 +550,55 @@ export class ChatCore {
|
|
|
518
550
|
async hasStored(chatId) {
|
|
519
551
|
return this.chats.has(chatId) || (await this.store?.has(chatId)) === true;
|
|
520
552
|
}
|
|
553
|
+
/*
|
|
554
|
+
* Writes the whole record of a chat nobody has loaded, such as a fork's thread before anyone opens it.
|
|
555
|
+
* The files its messages and generated images point at are copied into its own folder, so removing
|
|
556
|
+
* the chat they came from leaves them.
|
|
557
|
+
*/
|
|
558
|
+
async writeRecord(chatId, info, items, preambles = []) {
|
|
559
|
+
if (!this.store) {
|
|
560
|
+
throw new ChatError('chat-unsupported', 'This host keeps no chat records');
|
|
561
|
+
}
|
|
562
|
+
if (this.chats.has(chatId) || this.creating.has(chatId)) {
|
|
563
|
+
throw new ChatError('chat-busy', `Chat ${chatId} is already loaded`);
|
|
564
|
+
}
|
|
565
|
+
const copies = new Map();
|
|
566
|
+
const copy = (attachment) => {
|
|
567
|
+
let pending = copies.get(attachment.id);
|
|
568
|
+
if (!pending) {
|
|
569
|
+
pending = this.attachments.copy(chatId, attachment);
|
|
570
|
+
copies.set(attachment.id, pending);
|
|
571
|
+
}
|
|
572
|
+
return pending;
|
|
573
|
+
};
|
|
574
|
+
const copied = await Promise.all(items.map(async (item) => {
|
|
575
|
+
if (item.kind === 'user' && item.attachments?.length) {
|
|
576
|
+
return { ...item, attachments: await Promise.all(item.attachments.map(copy)) };
|
|
577
|
+
}
|
|
578
|
+
const generated = generatedImageAttachment(item);
|
|
579
|
+
if (item.kind === 'tool' && generated) {
|
|
580
|
+
return { ...item, input: { ...item.input, attachment: await copy(generated) } };
|
|
581
|
+
}
|
|
582
|
+
return item;
|
|
583
|
+
}));
|
|
584
|
+
await this.store.write(chatId, info, copied, { seq: 0, resetSeq: 0 }, preambles);
|
|
585
|
+
}
|
|
586
|
+
/*
|
|
587
|
+
* Removes what a chat nobody loaded keeps on disk: its record and log, attachments, bookmarks and
|
|
588
|
+
* visuals. False, with nothing removed, for a chat that is loaded or loading; `kill` removes that one.
|
|
589
|
+
*/
|
|
590
|
+
async deleteRecord(chatId) {
|
|
591
|
+
if (this.chats.has(chatId) || this.creating.has(chatId)) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
await Promise.all([
|
|
595
|
+
this.store?.delete(chatId),
|
|
596
|
+
this.attachments.removeAll(chatId),
|
|
597
|
+
this.bookmarks?.removeChat(chatId),
|
|
598
|
+
this.visuals?.removeChat(chatId)
|
|
599
|
+
]);
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
521
602
|
/* The turn a chat took last, read off the record without loading a chat nobody has; null before its first. */
|
|
522
603
|
async lastTurn(chatId) {
|
|
523
604
|
const session = this.chats.get(chatId);
|
|
@@ -628,6 +709,13 @@ export class ChatCore {
|
|
|
628
709
|
const session = this.require(chatId);
|
|
629
710
|
return session.skills(() => this.skillIndex.list(session.info.provider, session.info.cwd));
|
|
630
711
|
}
|
|
712
|
+
/* Ends the chat's CLI and keeps its conversation; the next message resumes it in a process that reads its skills and plugins again. */
|
|
713
|
+
async restart(chatId) {
|
|
714
|
+
const session = this.require(chatId);
|
|
715
|
+
const ended = session.restart();
|
|
716
|
+
this.skillIndex.forget(session.info.provider, session.info.cwd);
|
|
717
|
+
await ended;
|
|
718
|
+
}
|
|
631
719
|
compact(chatId) {
|
|
632
720
|
const session = this.require(chatId);
|
|
633
721
|
if (session.busy) {
|
|
@@ -641,10 +729,10 @@ export class ChatCore {
|
|
|
641
729
|
if (session.busy && !force) {
|
|
642
730
|
throw new ChatError('chat-busy', `Chat ${chatId} is still working on the previous message`);
|
|
643
731
|
}
|
|
644
|
-
await this.clearing(chatId);
|
|
732
|
+
await this.hooks.clearing?.(chatId);
|
|
645
733
|
const ended = session.clear(force);
|
|
646
734
|
// Before the record is written, so what it takes along from the record is gone from the write as well.
|
|
647
|
-
const cleared = this.cleared(chatId);
|
|
735
|
+
const cleared = this.hooks.cleared?.(chatId);
|
|
648
736
|
// A debounced write still waiting holds the old thread and must not land after the empty one.
|
|
649
737
|
this.cancelWaiting(chatId);
|
|
650
738
|
// Folded right away: every line before the reset describes a thread that is gone.
|
|
@@ -683,6 +771,15 @@ export class ChatCore {
|
|
|
683
771
|
throw new ChatError('request-not-found', `No question to dismiss under ${itemId}`);
|
|
684
772
|
}
|
|
685
773
|
}
|
|
774
|
+
/* Holds a chat at `needs-you` for what the host asks a person; false for a chat that is not here. */
|
|
775
|
+
holdForPerson(chatId, key) {
|
|
776
|
+
const session = this.chats.get(chatId);
|
|
777
|
+
session?.holdForPerson(key);
|
|
778
|
+
return session !== undefined;
|
|
779
|
+
}
|
|
780
|
+
releaseHold(chatId, key) {
|
|
781
|
+
this.chats.get(chatId)?.releaseHold(key);
|
|
782
|
+
}
|
|
686
783
|
/* Removes a chat: its CLI, its record, its log, its attachments, its bookmarks and its visuals. */
|
|
687
784
|
async kill(chatId) {
|
|
688
785
|
await this.loaded(chatId);
|
|
@@ -703,9 +800,10 @@ export class ChatCore {
|
|
|
703
800
|
this.activity.delete(chatId);
|
|
704
801
|
this.chats.delete(chatId);
|
|
705
802
|
this.attached.delete(chatId);
|
|
803
|
+
this.listChanges.delete(chatId);
|
|
706
804
|
this.announced.delete(chatId);
|
|
707
805
|
const snapshot = session.thread.snapshot();
|
|
708
|
-
this.forgotten(chatId);
|
|
806
|
+
this.hooks.forgotten?.(chatId);
|
|
709
807
|
await writing;
|
|
710
808
|
await ended;
|
|
711
809
|
await Promise.all([
|
|
@@ -713,7 +811,7 @@ export class ChatCore {
|
|
|
713
811
|
this.attachments.removeAll(chatId),
|
|
714
812
|
this.bookmarks?.removeChat(chatId),
|
|
715
813
|
this.visuals?.removeChat(chatId),
|
|
716
|
-
this.removed(chatId, snapshot)
|
|
814
|
+
this.hooks.removed?.(chatId, snapshot)
|
|
717
815
|
]);
|
|
718
816
|
}
|
|
719
817
|
/*
|
|
@@ -754,7 +852,7 @@ export class ChatCore {
|
|
|
754
852
|
}
|
|
755
853
|
await this.create({ chatId });
|
|
756
854
|
}
|
|
757
|
-
await this.chats.get(chatId)?.resume(turnId, attempt, await this.resumeWords(chatId));
|
|
855
|
+
await this.chats.get(chatId)?.resume(turnId, attempt, (await this.hooks.resumeWords?.(chatId)) ?? DEFAULT_RESUME_WORDS);
|
|
758
856
|
}
|
|
759
857
|
/*
|
|
760
858
|
* Takes up a turn that stopped on a limit, once its reset or its retry came. Loads the chat when
|
|
@@ -773,7 +871,7 @@ export class ChatCore {
|
|
|
773
871
|
return;
|
|
774
872
|
}
|
|
775
873
|
const turn = session.thread.get(turnId);
|
|
776
|
-
const ended = this.endedAt(chatId);
|
|
874
|
+
const ended = this.hooks.endedAt?.(chatId) ?? null;
|
|
777
875
|
if (turn !== undefined && ended !== null && ended >= turn.createdAt) {
|
|
778
876
|
session.lapseResume();
|
|
779
877
|
return;
|
|
@@ -803,6 +901,10 @@ export class ChatCore {
|
|
|
803
901
|
if (this.chats.has(chatId) || this.creating.has(chatId)) {
|
|
804
902
|
continue;
|
|
805
903
|
}
|
|
904
|
+
// Parsing the whole thread of every settled chat was most of the cost of a start.
|
|
905
|
+
if (await this.store.settled(chatId).catch(() => false)) {
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
806
908
|
const record = await this.store.read(chatId).catch(() => null);
|
|
807
909
|
const unowed = record?.info.resumeAt !== undefined && this.limitResume?.owed(chatId) === false;
|
|
808
910
|
if (record !== null && (interruptedTurn(record) !== null || unowed)) {
|
|
@@ -847,7 +949,7 @@ export class ChatCore {
|
|
|
847
949
|
const log = this.logs.get(chatId);
|
|
848
950
|
// Not folded here: an older write still in flight may land after this one, and the log is what covers for it.
|
|
849
951
|
try {
|
|
850
|
-
this.store.writeSync(chatId, info, items, { seq: log?.seq ?? 0, resetSeq: log?.resetSeq ?? 0 }, session.preambles, this.recordExtras(chatId), session.uiAccessRecord);
|
|
952
|
+
this.store.writeSync(chatId, info, items, { seq: log?.seq ?? 0, resetSeq: log?.resetSeq ?? 0 }, session.preambles, this.hooks.recordExtras?.(chatId) ?? {}, session.uiAccessRecord);
|
|
851
953
|
}
|
|
852
954
|
catch (e) {
|
|
853
955
|
console.error(`Chat record for ${chatId} failed:`, errorText(e));
|
|
@@ -862,10 +964,6 @@ export class ChatCore {
|
|
|
862
964
|
async loaded(chatId) {
|
|
863
965
|
await this.creating.get(chatId)?.catch(() => undefined);
|
|
864
966
|
}
|
|
865
|
-
persist(chatId) {
|
|
866
|
-
this.cancelWaiting(chatId);
|
|
867
|
-
void this.persistNow(chatId).catch((e) => console.error(`Chat record for ${chatId} failed:`, errorText(e)));
|
|
868
|
-
}
|
|
869
967
|
require(chatId) {
|
|
870
968
|
const session = this.chats.get(chatId);
|
|
871
969
|
if (!session) {
|
|
@@ -873,74 +971,9 @@ export class ChatCore {
|
|
|
873
971
|
}
|
|
874
972
|
return session;
|
|
875
973
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
880
|
-
/* What of the instructions a resumed thread hears again in front of its next prompt, for a CLI that ignores them on a resume. */
|
|
881
|
-
resumeNoteFor(_chatId) {
|
|
882
|
-
return null;
|
|
883
|
-
}
|
|
884
|
-
/*
|
|
885
|
-
* Folders outside its working directory a chat's agent may reach without asking, absolute. Asked
|
|
886
|
-
* before every turn, so a host can hand one over for as long as it is needed: a change starts the
|
|
887
|
-
* next turn in a new process that resumes the thread, and never cuts a turn in flight.
|
|
888
|
-
*/
|
|
889
|
-
foldersFor(_chatId) {
|
|
890
|
-
return [];
|
|
891
|
-
}
|
|
892
|
-
/* Whether no list of chats shows this one, which `ChatInfo.hidden` says to every client. */
|
|
893
|
-
hiddenFor(_chatId) {
|
|
894
|
-
return false;
|
|
895
|
-
}
|
|
896
|
-
/* What the host says in front of a chat's next real prompt, beside what the chat keeps itself. */
|
|
897
|
-
promptNotesFor(_chatId) {
|
|
898
|
-
return undefined;
|
|
899
|
-
}
|
|
900
|
-
/* How the chats a person attached to a message are named to the agent; without it they are dropped. */
|
|
901
|
-
referencesFor(_chatId) {
|
|
902
|
-
return undefined;
|
|
903
|
-
}
|
|
904
|
-
/* The environment of this chat's CLI before its account adds its own. */
|
|
905
|
-
envFor(_chatId, base) {
|
|
906
|
-
return base;
|
|
907
|
-
}
|
|
908
|
-
/* Throws for a folder this chat may not start in; asked every time a chat is loaded. */
|
|
909
|
-
async admit(_chatId, _cwd) { }
|
|
910
|
-
/* The model a new chat starts on when the host already knows; the explicit one still wins. */
|
|
911
|
-
openingSelection(_chatId, _kind) {
|
|
912
|
-
return undefined;
|
|
913
|
-
}
|
|
914
|
-
/* The mode a chat runs in, which a host may narrow whatever its record or its client says. */
|
|
915
|
-
runtimeModeFor(_chatId, mode) {
|
|
916
|
-
return mode;
|
|
917
|
-
}
|
|
918
|
-
/* A chat just loaded or made, its stored turns settled; whatever the host lays down in it goes here. */
|
|
919
|
-
async opened(_session, _stored) { }
|
|
920
|
-
/* What the host keeps in the chat's record beside the thread. */
|
|
921
|
-
recordExtras(_chatId) {
|
|
922
|
-
return {};
|
|
923
|
-
}
|
|
924
|
-
/* The chat was cleared; what runs before the first await is in the record written right after. */
|
|
925
|
-
async cleared(_chatId) { }
|
|
926
|
-
async clearing(_chatId) { }
|
|
927
|
-
/* The chat left memory for good, before its files go. */
|
|
928
|
-
forgotten(_chatId) { }
|
|
929
|
-
/* The chat was removed; what the host keeps of it goes along. */
|
|
930
|
-
async removed(_chatId, _chat) { }
|
|
931
|
-
/* An event of a chat went out, after the observers and before the clients reading its thread. */
|
|
932
|
-
broadcasted(_chatId, _event) { }
|
|
933
|
-
/* When whoever opened this chat stopped it; a turn from before that is never taken up again. */
|
|
934
|
-
endedAt(_chatId) {
|
|
935
|
-
return null;
|
|
936
|
-
}
|
|
937
|
-
/* Why a turn the host went down in stays down when `onInterruptedRun` says no, in the words of the note it ends with. */
|
|
938
|
-
unownedReason(_chatId) {
|
|
939
|
-
return 'nothing holds this chat any more';
|
|
940
|
-
}
|
|
941
|
-
/* How a turn the host went down in is taken up in this chat. */
|
|
942
|
-
async resumeWords(_chatId) {
|
|
943
|
-
return DEFAULT_RESUME_WORDS;
|
|
974
|
+
persist(chatId) {
|
|
975
|
+
this.cancelWaiting(chatId);
|
|
976
|
+
void this.persistNow(chatId).catch((e) => console.error(`Chat record for ${chatId} failed:`, errorText(e)));
|
|
944
977
|
}
|
|
945
978
|
/* A write that may wait: a long thread is not rewritten for every small change. */
|
|
946
979
|
persistSoon(chatId) {
|
|
@@ -979,7 +1012,7 @@ export class ChatCore {
|
|
|
979
1012
|
}
|
|
980
1013
|
// A delta held back is already in the thread, so it gets its seq before the snapshot says where it ends.
|
|
981
1014
|
this.coalescers.get(chatId)?.flush();
|
|
982
|
-
const extras = this.recordExtras(chatId);
|
|
1015
|
+
const extras = this.hooks.recordExtras?.(chatId) ?? {};
|
|
983
1016
|
const uiAccess = session.uiAccessRecord;
|
|
984
1017
|
const unlogged = JSON.stringify([session.preambles, extras, uiAccess]);
|
|
985
1018
|
const folds = fold || log.size > COMPACT_ABOVE_BYTES || !log.onDisk;
|
|
@@ -1024,7 +1057,7 @@ export class ChatCore {
|
|
|
1024
1057
|
for (const sink of this.observers) {
|
|
1025
1058
|
sink({ event: 'chat.event', payload });
|
|
1026
1059
|
}
|
|
1027
|
-
this.broadcasted(chatId, event);
|
|
1060
|
+
this.hooks.broadcasted?.(chatId, event);
|
|
1028
1061
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
1029
1062
|
this.sinks.to(clientId, { event: 'chat.event', payload });
|
|
1030
1063
|
}
|
|
@@ -1047,14 +1080,15 @@ export class ChatCore {
|
|
|
1047
1080
|
*/
|
|
1048
1081
|
announceStatus(chatId, info) {
|
|
1049
1082
|
// A limit, the resume owed after it and subagents still at work are what a header shows of an idle chat, so they count as a change too,
|
|
1050
|
-
// and so does a request opening or settling while the chat already waits on another.
|
|
1083
|
+
// and so does a request opening or settling while the chat already waits on another, and a goal set, moving on or gone.
|
|
1051
1084
|
const said = JSON.stringify([
|
|
1052
1085
|
info.status,
|
|
1053
1086
|
info.limit ?? null,
|
|
1054
1087
|
info.resumeAt ?? null,
|
|
1055
1088
|
info.resumeAtReset ?? null,
|
|
1056
1089
|
info.delegating ?? false,
|
|
1057
|
-
info.requests?.map((request) => request.requestId) ?? []
|
|
1090
|
+
info.requests?.map((request) => request.requestId) ?? [],
|
|
1091
|
+
info.goal === undefined ? null : [info.goal.status, info.goal.objective]
|
|
1058
1092
|
]);
|
|
1059
1093
|
if (this.announced.get(chatId) === said) {
|
|
1060
1094
|
return;
|
|
@@ -1094,7 +1128,7 @@ export class ChatCore {
|
|
|
1094
1128
|
if (stored.info.agentSessionId === null) {
|
|
1095
1129
|
return skip('the agent had not started a session to resume yet');
|
|
1096
1130
|
}
|
|
1097
|
-
const ended = this.endedAt(chatId);
|
|
1131
|
+
const ended = this.hooks.endedAt?.(chatId) ?? null;
|
|
1098
1132
|
if (ended !== null && ended >= turn.createdAt) {
|
|
1099
1133
|
return skip('the agent that opened it was stopped');
|
|
1100
1134
|
}
|
|
@@ -1103,7 +1137,7 @@ export class ChatCore {
|
|
|
1103
1137
|
}
|
|
1104
1138
|
try {
|
|
1105
1139
|
const owed = await this.onInterruptedRun({ chatId, turnId: turn.id, attempt: (turn.attempt ?? 1) + (turn.resumePending ? 0 : 1) });
|
|
1106
|
-
return owed ? { resumeTurnId: turn.id, reason: null } : skip(this.unownedReason(chatId));
|
|
1140
|
+
return owed ? { resumeTurnId: turn.id, reason: null } : skip(this.hooks.unownedReason?.(chatId) ?? 'nothing holds this chat any more');
|
|
1107
1141
|
}
|
|
1108
1142
|
catch (e) {
|
|
1109
1143
|
console.error(`Owing a resume for chat ${chatId} failed:`, errorText(e));
|
package/dist/chat/chat-log.js
CHANGED
|
@@ -48,7 +48,7 @@ export class ChatLog {
|
|
|
48
48
|
constructor(path, state = { seq: 0, resetSeq: 0, lines: [] }) {
|
|
49
49
|
this.path = path;
|
|
50
50
|
this.lines = state.lines;
|
|
51
|
-
this.bytes = state.lines.reduce((sum, line) => sum + JSON.stringify(line)
|
|
51
|
+
this.bytes = state.lines.reduce((sum, line) => sum + Buffer.byteLength(JSON.stringify(line)) + 1, 0);
|
|
52
52
|
this.seqValue = Math.max(state.seq, state.lines.at(-1)?.seq ?? 0);
|
|
53
53
|
this.resetSeqValue = state.resetSeq;
|
|
54
54
|
const first = state.lines[0];
|
|
@@ -84,7 +84,7 @@ export class ChatLog {
|
|
|
84
84
|
}
|
|
85
85
|
const text = `${JSON.stringify(line)}\n`;
|
|
86
86
|
this.lines.push(line);
|
|
87
|
-
this.bytes += text
|
|
87
|
+
this.bytes += Buffer.byteLength(text);
|
|
88
88
|
if (this.path !== null) {
|
|
89
89
|
try {
|
|
90
90
|
this.fd ??= this.open(this.path);
|
|
@@ -122,7 +122,7 @@ export class ChatLog {
|
|
|
122
122
|
this.lines = this.lines.filter((line) => line.seq > seq);
|
|
123
123
|
this.base = seq;
|
|
124
124
|
const text = this.lines.map((line) => `${JSON.stringify(line)}\n`).join('');
|
|
125
|
-
this.bytes = text
|
|
125
|
+
this.bytes = Buffer.byteLength(text);
|
|
126
126
|
if (this.path === null) {
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
@@ -18,6 +18,12 @@ export interface ChatSpawnOptions {
|
|
|
18
18
|
}
|
|
19
19
|
export type SpawnChatProcess = (options: ChatSpawnOptions) => ChatProcess;
|
|
20
20
|
export declare function spawnChatProcess(options: ChatSpawnOptions): ChatProcess;
|
|
21
|
+
export interface ProcessTreeKillOptions {
|
|
22
|
+
platform?: NodeJS.Platform;
|
|
23
|
+
kill?(pid: number, signal: NodeJS.Signals): void;
|
|
24
|
+
run?(command: string[]): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function killProcessTree(pid: number, signal: 'SIGTERM' | 'SIGKILL', options?: ProcessTreeKillOptions): void;
|
|
21
27
|
export declare class StreamTail {
|
|
22
28
|
readonly done: Promise<void>;
|
|
23
29
|
private text;
|
|
@@ -25,6 +31,21 @@ export declare class StreamTail {
|
|
|
25
31
|
lines(): string | null;
|
|
26
32
|
private read;
|
|
27
33
|
}
|
|
34
|
+
export declare const MAX_LINE_LENGTH = 64000000;
|
|
35
|
+
export declare class LineSplitter {
|
|
36
|
+
private readonly onLine;
|
|
37
|
+
private readonly onOverflow;
|
|
38
|
+
private readonly max;
|
|
39
|
+
private parts;
|
|
40
|
+
private length;
|
|
41
|
+
private dropping;
|
|
42
|
+
constructor(onLine: (line: string) => void, onOverflow: () => void, max?: number);
|
|
43
|
+
push(text: string): void;
|
|
44
|
+
end(): void;
|
|
45
|
+
private add;
|
|
46
|
+
private take;
|
|
47
|
+
}
|
|
48
|
+
export declare function readStreamLines(stream: ReadableStream<Uint8Array>, onLine: (line: string) => void, onOverflow: () => void, max?: number): Promise<void>;
|
|
28
49
|
export interface ChatChildOptions extends Omit<ChatSpawnOptions, 'onExit'> {
|
|
29
50
|
spawn?: SpawnChatProcess;
|
|
30
51
|
onExit(exitCode: number | null, stderr: string | null): void;
|