@adecore/agents 0.46.2 → 0.47.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/chat-core.d.ts +11 -2
- package/dist/chat/chat-core.js +88 -17
- package/dist/chat/chat-session.d.ts +2 -2
- package/dist/chat/chat-session.js +12 -2
- package/dist/chat/payload-store.js +1 -1
- package/dist/messages/deliver-notice.d.ts +3 -2
- package/dist/messages/deliver-notice.js +5 -1
- package/dist/messages/notice-notes.d.ts +6 -2
- package/dist/messages/notice-notes.js +2 -1
- package/package.json +3 -3
- package/src/chat/chat-core.ts +113 -17
- package/src/chat/chat-session.ts +13 -2
- package/src/chat/payload-store.ts +1 -1
- package/src/messages/deliver-notice.ts +8 -3
- package/src/messages/notice-notes.ts +8 -3
package/dist/chat/chat-core.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentKind, type ChatAttachment, type ChatAttachResult, type ChatAttachmentUpload, type ChatBookmark, type ChatCheckpointDiff, type ChatConfigurePayload, type ChatCreatePayload, type ChatEvent, type ChatHistoryResult, type ChatGoalAction, 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';
|
|
1
|
+
import { type AgentKind, type ChatAttachment, type ChatAttachResult, type ChatAttachmentUpload, type ChatBookmark, type ChatCheckpointDiff, type ChatConfigurePayload, type ChatCreatePayload, type ChatEvent, type ChatHistoryResult, type ChatGoalAction, type ChatInfo, type ChatItem, type ChatNoteMessage, 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
2
|
import type { AgentEvent, AgentSink } from '../events.ts';
|
|
3
3
|
import type { OutboxOutcome } from '../outbox/outbox-worker.ts';
|
|
4
4
|
import { type AccountLaunches } from '../providers/accounts/launch.ts';
|
|
@@ -82,6 +82,7 @@ export interface ChatCoreOptions {
|
|
|
82
82
|
};
|
|
83
83
|
accounts?: AccountLaunches;
|
|
84
84
|
hooks?: ChatCoreHooks;
|
|
85
|
+
unloadAfterMs?: number;
|
|
85
86
|
}
|
|
86
87
|
export declare class ChatCore {
|
|
87
88
|
protected readonly bookmarks: BookmarkStore | null;
|
|
@@ -123,6 +124,9 @@ export declare class ChatCore {
|
|
|
123
124
|
private readonly nameChat;
|
|
124
125
|
private readonly onInterruptedRun;
|
|
125
126
|
private readonly limitResume;
|
|
127
|
+
private readonly unloadAfterMs;
|
|
128
|
+
private readonly unloadTimers;
|
|
129
|
+
private readonly unloadedInfo;
|
|
126
130
|
constructor(options: ChatCoreOptions);
|
|
127
131
|
codexProcess(info: Pick<ChatInfo, 'cwd' | 'account'>): CodexProcessSpec;
|
|
128
132
|
claudeProjectsDirOf(info: Pick<ChatInfo, 'account'>): string;
|
|
@@ -176,7 +180,7 @@ export declare class ChatCore {
|
|
|
176
180
|
writeRecord(chatId: string, info: ChatInfo, items: ChatItem[], preambles?: string[]): Promise<void>;
|
|
177
181
|
deleteRecord(chatId: string): Promise<boolean>;
|
|
178
182
|
lastTurn(chatId: string): Promise<ChatTurnItem | null>;
|
|
179
|
-
addNote(chatId: string, level: 'info' | 'warning' | 'error', text: string): Promise<void>;
|
|
183
|
+
addNote(chatId: string, level: 'info' | 'warning' | 'error', text: string, message?: ChatNoteMessage): Promise<void>;
|
|
180
184
|
deliverNote(chatId: string, delivery: {
|
|
181
185
|
noteId: string;
|
|
182
186
|
note: string;
|
|
@@ -257,6 +261,11 @@ export declare class ChatCore {
|
|
|
257
261
|
updatedAt: number;
|
|
258
262
|
}[];
|
|
259
263
|
list(): ChatInfo[];
|
|
264
|
+
unloadIdle(chatId: string): Promise<boolean>;
|
|
265
|
+
private unloadable;
|
|
266
|
+
private scheduleUnload;
|
|
267
|
+
private cancelUnload;
|
|
268
|
+
private forgetLoaded;
|
|
260
269
|
get(chatId: string): ChatSession | undefined;
|
|
261
270
|
resumeRun(chatId: string, turnId: string, attempt: number): Promise<void>;
|
|
262
271
|
takeUpAfterLimit(chatId: string, turnId: string): Promise<OutboxOutcome>;
|
package/dist/chat/chat-core.js
CHANGED
|
@@ -94,6 +94,10 @@ export class ChatCore {
|
|
|
94
94
|
nameChat;
|
|
95
95
|
onInterruptedRun;
|
|
96
96
|
limitResume;
|
|
97
|
+
unloadAfterMs;
|
|
98
|
+
unloadTimers = new Map();
|
|
99
|
+
// The last info of each chat that left memory, so `list` still answers what it was doing.
|
|
100
|
+
unloadedInfo = new Map();
|
|
97
101
|
constructor(options) {
|
|
98
102
|
this.uiQueries = options.intelligentUi ? new ChatUiQueries(options.intelligentUi) : null;
|
|
99
103
|
this.uiFenceLanguage = options.uiFenceLanguage;
|
|
@@ -111,6 +115,7 @@ export class ChatCore {
|
|
|
111
115
|
this.nameChat = options.nameChat ?? null;
|
|
112
116
|
this.onInterruptedRun = options.onInterruptedRun ?? null;
|
|
113
117
|
this.limitResume = options.limitResume ?? null;
|
|
118
|
+
this.unloadAfterMs = options.store && options.unloadAfterMs !== undefined ? options.unloadAfterMs : null;
|
|
114
119
|
this.hooks = options.hooks ?? {};
|
|
115
120
|
this.commands = {
|
|
116
121
|
...(options.command ? { claude: options.command } : {}),
|
|
@@ -325,12 +330,15 @@ export class ChatCore {
|
|
|
325
330
|
...(this.limitResume ? { limitResume: limitHooks(this.limitResume, payload.chatId) } : {})
|
|
326
331
|
});
|
|
327
332
|
this.chats.set(session.id, session);
|
|
333
|
+
this.unloadedInfo.delete(session.id);
|
|
328
334
|
if (stored) {
|
|
329
335
|
session.settleStored(selection, resume);
|
|
330
336
|
session.settleOwedResume();
|
|
331
337
|
await session.settleOrphanedSubagents();
|
|
332
338
|
}
|
|
333
339
|
await this.hooks.opened?.(session, stored ?? null);
|
|
340
|
+
// A chat loaded for someone who never attaches, such as a host reading it, leaves again on its own.
|
|
341
|
+
this.scheduleUnload(session.id);
|
|
334
342
|
return session.info;
|
|
335
343
|
}
|
|
336
344
|
async storedBackground(chatId) {
|
|
@@ -392,6 +400,7 @@ export class ChatCore {
|
|
|
392
400
|
// client is on the list: the text is either in the snapshot or in the stream, never both.
|
|
393
401
|
this.coalescers.get(chatId)?.flush();
|
|
394
402
|
clients.add(clientId);
|
|
403
|
+
this.cancelUnload(chatId);
|
|
395
404
|
const log = this.logs.get(chatId);
|
|
396
405
|
const events = since === undefined ? null : log.after(since);
|
|
397
406
|
if (events !== null) {
|
|
@@ -509,12 +518,14 @@ export class ChatCore {
|
|
|
509
518
|
detach(chatId, clientId) {
|
|
510
519
|
this.coalescers.get(chatId)?.flush();
|
|
511
520
|
this.attached.get(chatId)?.delete(clientId);
|
|
521
|
+
this.scheduleUnload(chatId);
|
|
512
522
|
}
|
|
513
523
|
detachAll(clientId) {
|
|
514
524
|
for (const [chatId, clients] of this.attached) {
|
|
515
525
|
if (clients.has(clientId)) {
|
|
516
526
|
this.coalescers.get(chatId)?.flush();
|
|
517
527
|
clients.delete(clientId);
|
|
528
|
+
this.scheduleUnload(chatId);
|
|
518
529
|
}
|
|
519
530
|
}
|
|
520
531
|
this.subagents.releaseClient(clientId);
|
|
@@ -613,6 +624,7 @@ export class ChatCore {
|
|
|
613
624
|
if (this.chats.has(chatId) || this.creating.has(chatId)) {
|
|
614
625
|
return false;
|
|
615
626
|
}
|
|
627
|
+
this.unloadedInfo.delete(chatId);
|
|
616
628
|
await Promise.all([
|
|
617
629
|
this.store?.delete(chatId),
|
|
618
630
|
this.attachments.removeAll(chatId),
|
|
@@ -631,7 +643,7 @@ export class ChatCore {
|
|
|
631
643
|
return stored?.items.findLast((item) => item.kind === 'turn') ?? null;
|
|
632
644
|
}
|
|
633
645
|
/* A line from the host in a chat's thread, loading the chat when nobody has; nothing for a chat that is gone. */
|
|
634
|
-
async addNote(chatId, level, text) {
|
|
646
|
+
async addNote(chatId, level, text, message) {
|
|
635
647
|
await this.loaded(chatId);
|
|
636
648
|
if (!(await this.hasStored(chatId))) {
|
|
637
649
|
return;
|
|
@@ -639,7 +651,7 @@ export class ChatCore {
|
|
|
639
651
|
if (!this.chats.has(chatId)) {
|
|
640
652
|
await this.create({ chatId });
|
|
641
653
|
}
|
|
642
|
-
this.chats.get(chatId)?.addNote(level, text);
|
|
654
|
+
this.chats.get(chatId)?.addNote(level, text, message);
|
|
643
655
|
}
|
|
644
656
|
/*
|
|
645
657
|
* Leaves a note and its preamble in a chat, loading that chat when nobody has; false when it was
|
|
@@ -897,21 +909,9 @@ export class ChatCore {
|
|
|
897
909
|
// A write already out would put the record back after the delete.
|
|
898
910
|
const writing = this.writes.get(chatId);
|
|
899
911
|
const ended = session.dispose();
|
|
900
|
-
this.
|
|
901
|
-
this.uiQueries?.forget(chatId);
|
|
902
|
-
this.coalescers.get(chatId)?.dispose();
|
|
903
|
-
this.coalescers.delete(chatId);
|
|
904
|
-
this.logs.get(chatId)?.close();
|
|
905
|
-
this.logs.delete(chatId);
|
|
906
|
-
this.cancelWaiting(chatId);
|
|
907
|
-
this.sizes.delete(chatId);
|
|
908
|
-
this.unlogged.delete(chatId);
|
|
909
|
-
this.writes.delete(chatId);
|
|
910
|
-
this.activity.delete(chatId);
|
|
911
|
-
this.chats.delete(chatId);
|
|
912
|
+
this.forgetLoaded(chatId);
|
|
912
913
|
this.attached.delete(chatId);
|
|
913
|
-
this.
|
|
914
|
-
this.announced.delete(chatId);
|
|
914
|
+
this.unloadedInfo.delete(chatId);
|
|
915
915
|
const snapshot = session.thread.snapshot();
|
|
916
916
|
this.hooks.forgotten?.(chatId);
|
|
917
917
|
await writing;
|
|
@@ -944,7 +944,75 @@ export class ChatCore {
|
|
|
944
944
|
return targets;
|
|
945
945
|
}
|
|
946
946
|
list() {
|
|
947
|
-
return [...this.chats.values()].map((session) => session.info);
|
|
947
|
+
return [...[...this.chats.values()].map((session) => session.info), ...this.unloadedInfo.values()];
|
|
948
|
+
}
|
|
949
|
+
/*
|
|
950
|
+
* Puts a chat nobody needs in memory back on disk alone: nobody reads it, it has no CLI, no turn,
|
|
951
|
+
* request, hold or move is under way, and no resume is owed. Answers whether it left. The next
|
|
952
|
+
* create, attach or note loads it again from its record, as after a restart.
|
|
953
|
+
*/
|
|
954
|
+
async unloadIdle(chatId) {
|
|
955
|
+
this.cancelUnload(chatId);
|
|
956
|
+
if (!this.unloadable(chatId)) {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
await this.persistNow(chatId, true);
|
|
960
|
+
const session = this.chats.get(chatId);
|
|
961
|
+
// Someone may have attached or sent while the record was written.
|
|
962
|
+
if (!session || !this.unloadable(chatId)) {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
this.forgetLoaded(chatId);
|
|
966
|
+
this.attached.delete(chatId);
|
|
967
|
+
this.unloadedInfo.set(chatId, session.info);
|
|
968
|
+
await session.dispose();
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
unloadable(chatId) {
|
|
972
|
+
const session = this.chats.get(chatId);
|
|
973
|
+
return (session !== undefined &&
|
|
974
|
+
this.store !== null &&
|
|
975
|
+
(this.attached.get(chatId)?.size ?? 0) === 0 &&
|
|
976
|
+
!this.creating.has(chatId) &&
|
|
977
|
+
!this.relocations.has(chatId) &&
|
|
978
|
+
session.pid === null &&
|
|
979
|
+
session.relocatable &&
|
|
980
|
+
session.info.resumeAt === undefined &&
|
|
981
|
+
(session.info.requests?.length ?? 0) === 0);
|
|
982
|
+
}
|
|
983
|
+
scheduleUnload(chatId) {
|
|
984
|
+
if (this.unloadAfterMs === null || (this.attached.get(chatId)?.size ?? 0) > 0) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
this.cancelUnload(chatId);
|
|
988
|
+
const timer = setTimeout(() => {
|
|
989
|
+
this.unloadTimers.delete(chatId);
|
|
990
|
+
void this.unloadIdle(chatId).catch((e) => console.error(`Unloading chat ${chatId} failed:`, errorText(e)));
|
|
991
|
+
}, this.unloadAfterMs);
|
|
992
|
+
timer.unref?.();
|
|
993
|
+
this.unloadTimers.set(chatId, timer);
|
|
994
|
+
}
|
|
995
|
+
cancelUnload(chatId) {
|
|
996
|
+
clearTimeout(this.unloadTimers.get(chatId));
|
|
997
|
+
this.unloadTimers.delete(chatId);
|
|
998
|
+
}
|
|
999
|
+
/* Drops what the core keeps of a loaded chat beside its session, which a kill and an unload share. */
|
|
1000
|
+
forgetLoaded(chatId) {
|
|
1001
|
+
this.subagents.releaseChat(chatId);
|
|
1002
|
+
this.uiQueries?.forget(chatId);
|
|
1003
|
+
this.coalescers.get(chatId)?.dispose();
|
|
1004
|
+
this.coalescers.delete(chatId);
|
|
1005
|
+
this.logs.get(chatId)?.close();
|
|
1006
|
+
this.logs.delete(chatId);
|
|
1007
|
+
this.cancelWaiting(chatId);
|
|
1008
|
+
this.cancelUnload(chatId);
|
|
1009
|
+
this.sizes.delete(chatId);
|
|
1010
|
+
this.unlogged.delete(chatId);
|
|
1011
|
+
this.writes.delete(chatId);
|
|
1012
|
+
this.activity.delete(chatId);
|
|
1013
|
+
this.chats.delete(chatId);
|
|
1014
|
+
this.listChanges.delete(chatId);
|
|
1015
|
+
this.announced.delete(chatId);
|
|
948
1016
|
}
|
|
949
1017
|
get(chatId) {
|
|
950
1018
|
return this.chats.get(chatId);
|
|
@@ -1032,6 +1100,7 @@ export class ChatCore {
|
|
|
1032
1100
|
}
|
|
1033
1101
|
for (const chatId of this.chats.keys()) {
|
|
1034
1102
|
this.cancelWaiting(chatId);
|
|
1103
|
+
this.cancelUnload(chatId);
|
|
1035
1104
|
}
|
|
1036
1105
|
// A thread that cannot be written still ends its CLI, whose process group would otherwise outlive the host.
|
|
1037
1106
|
const written = await Promise.allSettled([...this.chats.keys()].map((chatId) => this.persistNow(chatId, true)));
|
|
@@ -1173,6 +1242,8 @@ export class ChatCore {
|
|
|
1173
1242
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
1174
1243
|
this.sinks.to(clientId, { event: 'chat.event', payload });
|
|
1175
1244
|
}
|
|
1245
|
+
// A chat nobody reads counts its time from what it said last, such as its CLI going down.
|
|
1246
|
+
this.scheduleUnload(chatId);
|
|
1176
1247
|
// After the thread, so a client reading it has already had this info and the status is a no-op there.
|
|
1177
1248
|
if (event.type === 'info' || event.type === 'reset') {
|
|
1178
1249
|
this.announceStatus(chatId, event.info);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatAttachment, ChatCheckpointDiff, ChatEvent, ChatGoalAction, ChatInfo, ChatItem, ChatQueuedMessage, ChatQuoteSource, ChatSkill, ChatUiChoiceOrigin, ChatUiChoicePayload, ChatUiQueryState, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ChatAttachment, ChatCheckpointDiff, ChatEvent, ChatGoalAction, ChatInfo, ChatItem, ChatNoteMessage, ChatQueuedMessage, ChatQuoteSource, ChatSkill, ChatUiChoiceOrigin, ChatUiChoicePayload, ChatUiQueryState, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
|
|
2
2
|
import type { ChatProvider } from '../providers/provider.ts';
|
|
3
3
|
import type { LimitsUpdate } from '../usage/limits/normalize.ts';
|
|
4
4
|
import type { BackendLaunch } from './backend.ts';
|
|
@@ -211,7 +211,7 @@ export declare class ChatSession {
|
|
|
211
211
|
preamble: string;
|
|
212
212
|
}): boolean;
|
|
213
213
|
upsertItems(items: readonly ChatItem[]): void;
|
|
214
|
-
addNote(level: 'info' | 'warning' | 'error', text: string): void;
|
|
214
|
+
addNote(level: 'info' | 'warning' | 'error', text: string, message?: ChatNoteMessage): void;
|
|
215
215
|
end(reason: string): void;
|
|
216
216
|
restart(): Promise<void>;
|
|
217
217
|
dropProcess(): Promise<void>;
|
|
@@ -1057,8 +1057,18 @@ export class ChatSession {
|
|
|
1057
1057
|
this.options.persistSoon();
|
|
1058
1058
|
}
|
|
1059
1059
|
/* A line in the thread outside any turn, for something the host has to say about work it gave up on. */
|
|
1060
|
-
addNote(level, text) {
|
|
1061
|
-
this.emit([
|
|
1060
|
+
addNote(level, text, message) {
|
|
1061
|
+
this.emit([
|
|
1062
|
+
this.thread.upsert({
|
|
1063
|
+
id: newId('note'),
|
|
1064
|
+
kind: 'note',
|
|
1065
|
+
createdAt: Date.now(),
|
|
1066
|
+
turnId: null,
|
|
1067
|
+
level,
|
|
1068
|
+
text,
|
|
1069
|
+
...(message === undefined ? {} : { message })
|
|
1070
|
+
})
|
|
1071
|
+
]);
|
|
1062
1072
|
this.options.persist();
|
|
1063
1073
|
}
|
|
1064
1074
|
/*
|
|
@@ -8,7 +8,7 @@ import { CHAT_PAYLOAD_INLINE_BYTES, ChatCheckpointDiffSchema } from '@adecore/ag
|
|
|
8
8
|
import { isNotFound, writeAtomic, writeAtomicSync } from '../fs.js';
|
|
9
9
|
import { ChatError } from './errors.js';
|
|
10
10
|
/* How much of a value stored apart the item keeps: what a timeline shows before anyone asks for the rest. */
|
|
11
|
-
export const PAYLOAD_PREVIEW_BYTES =
|
|
11
|
+
export const PAYLOAD_PREVIEW_BYTES = 2 * 1024;
|
|
12
12
|
const FOLDER_SUFFIX = '.payloads';
|
|
13
13
|
const ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
14
14
|
const CONTINUATION_MASK = 0xc0;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatItem } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ChatItem, ChatNoteMessage } from '@adecore/agent-contracts';
|
|
2
2
|
import type { ChatCore } from '../chat/chat-core.ts';
|
|
3
3
|
import type { Notice, NoticeStore } from './notice-store.ts';
|
|
4
4
|
export interface MessageWords {
|
|
@@ -20,6 +20,7 @@ export declare function chatNoticeTargets(chats: Pick<ChatCore, 'get' | 'hasStor
|
|
|
20
20
|
export declare function deliverToChat(store: Pick<NoticeStore, 'put'>, targets: ChatNoticeTargets, notice: Omit<Notice, 'createdAt'>): Promise<ChatDelivery>;
|
|
21
21
|
export interface NoticeChat {
|
|
22
22
|
has(id: string): Promise<boolean>;
|
|
23
|
-
note(id: string, text: string): Promise<void>;
|
|
23
|
+
note(id: string, text: string, message: ChatNoteMessage): Promise<void>;
|
|
24
24
|
}
|
|
25
|
+
export declare function noticeMessage(notice: Notice): ChatNoteMessage;
|
|
25
26
|
export declare function showNotices(store: Pick<NoticeStore, 'show'>, chat: NoticeChat, words: Pick<MessageWords, 'shown'>, targetId: string): Promise<void>;
|
|
@@ -57,6 +57,10 @@ export async function deliverToChat(store, targets, notice) {
|
|
|
57
57
|
}
|
|
58
58
|
return { outcome: 'wake', waiting };
|
|
59
59
|
}
|
|
60
|
+
/* The message itself on the line in the thread, so a client can draw it as said to the chat. */
|
|
61
|
+
export function noticeMessage(notice) {
|
|
62
|
+
return { from: notice.from, title: notice.fromTitle, text: notice.text };
|
|
63
|
+
}
|
|
60
64
|
/*
|
|
61
65
|
* Shows a person the message in the chat's thread the moment it lands, not when the model gets round
|
|
62
66
|
* to it. An id no chat holds shows and marks nothing, so a chat that opens on it later still has all of it.
|
|
@@ -66,6 +70,6 @@ export async function showNotices(store, chat, words, targetId) {
|
|
|
66
70
|
return;
|
|
67
71
|
}
|
|
68
72
|
for (const notice of await store.show(targetId)) {
|
|
69
|
-
await chat.note(targetId, words.shown(notice));
|
|
73
|
+
await chat.note(targetId, words.shown(notice), noticeMessage(notice));
|
|
70
74
|
}
|
|
71
75
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { ChatNoteMessage } from '@adecore/agent-contracts';
|
|
1
2
|
import type { PromptNotes } from '../chat/chat-session.ts';
|
|
2
|
-
import type
|
|
3
|
+
import { type MessageWords } from './deliver-notice.ts';
|
|
3
4
|
import type { NoticeStore } from './notice-store.ts';
|
|
4
5
|
export declare class NoticeNotes implements PromptNotes {
|
|
5
6
|
private readonly store;
|
|
@@ -12,4 +13,7 @@ export declare class NoticeNotes implements PromptNotes {
|
|
|
12
13
|
};
|
|
13
14
|
reset(): void;
|
|
14
15
|
}
|
|
15
|
-
export declare function unshownNotes(store: Pick<NoticeStore, 'show'>, chatId: string, words: Pick<MessageWords, 'shown'>): Promise<
|
|
16
|
+
export declare function unshownNotes(store: Pick<NoticeStore, 'show'>, chatId: string, words: Pick<MessageWords, 'shown'>): Promise<{
|
|
17
|
+
text: string;
|
|
18
|
+
message: ChatNoteMessage;
|
|
19
|
+
}[]>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { noticeMessage } from './deliver-notice.js';
|
|
1
2
|
/*
|
|
2
3
|
* What another node left for a chat, taken from the queue in front of its next prompt so the model
|
|
3
4
|
* hears it once. Nothing of it is shown: a person read each message in the thread as it landed
|
|
@@ -22,5 +23,5 @@ export class NoticeNotes {
|
|
|
22
23
|
* reload writes none again. The model still hears the messages themselves from the queue.
|
|
23
24
|
*/
|
|
24
25
|
export async function unshownNotes(store, chatId, words) {
|
|
25
|
-
return (await store.show(chatId)).map((notice) => words.shown(notice));
|
|
26
|
+
return (await store.show(chatId)).map((notice) => ({ text: words.shown(notice), message: noticeMessage(notice) }));
|
|
26
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
"test": "bun --conditions=source test"
|
|
94
94
|
},
|
|
95
95
|
"dependencies": {
|
|
96
|
-
"@adecore/agent-contracts": "0.
|
|
97
|
-
"@adecore/intelligent-ui": "0.
|
|
96
|
+
"@adecore/agent-contracts": "0.47.0",
|
|
97
|
+
"@adecore/intelligent-ui": "0.47.0",
|
|
98
98
|
"zod": "^4.6.7"
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
package/src/chat/chat-core.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type ChatGoalAction,
|
|
18
18
|
type ChatInfo,
|
|
19
19
|
type ChatItem,
|
|
20
|
+
type ChatNoteMessage,
|
|
20
21
|
type ChatQueuedMessage,
|
|
21
22
|
type ChatUiChoicePayload,
|
|
22
23
|
type ChatUiQueryPayload,
|
|
@@ -215,6 +216,12 @@ export interface ChatCoreOptions {
|
|
|
215
216
|
// The accounts a chat may run under; without them every chat runs under its CLI's default account.
|
|
216
217
|
accounts?: AccountLaunches;
|
|
217
218
|
hooks?: ChatCoreHooks;
|
|
219
|
+
/*
|
|
220
|
+
* How long a chat stays in memory once nobody reads it, its CLI is gone and nothing is under way;
|
|
221
|
+
* after that only its info stays, for `list`, until something loads it again. Without it, or
|
|
222
|
+
* without a store, a loaded chat stays until it is removed or the host stops.
|
|
223
|
+
*/
|
|
224
|
+
unloadAfterMs?: number;
|
|
218
225
|
}
|
|
219
226
|
|
|
220
227
|
/*
|
|
@@ -272,6 +279,10 @@ export class ChatCore {
|
|
|
272
279
|
private readonly nameChat: ChatCoreOptions['nameChat'] | null;
|
|
273
280
|
private readonly onInterruptedRun: ChatCoreOptions['onInterruptedRun'] | null;
|
|
274
281
|
private readonly limitResume: ChatCoreOptions['limitResume'] | null;
|
|
282
|
+
private readonly unloadAfterMs: number | null;
|
|
283
|
+
private readonly unloadTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
284
|
+
// The last info of each chat that left memory, so `list` still answers what it was doing.
|
|
285
|
+
private readonly unloadedInfo = new Map<string, ChatInfo>();
|
|
275
286
|
|
|
276
287
|
constructor(options: ChatCoreOptions) {
|
|
277
288
|
this.uiQueries = options.intelligentUi ? new ChatUiQueries(options.intelligentUi) : null;
|
|
@@ -290,6 +301,7 @@ export class ChatCore {
|
|
|
290
301
|
this.nameChat = options.nameChat ?? null;
|
|
291
302
|
this.onInterruptedRun = options.onInterruptedRun ?? null;
|
|
292
303
|
this.limitResume = options.limitResume ?? null;
|
|
304
|
+
this.unloadAfterMs = options.store && options.unloadAfterMs !== undefined ? options.unloadAfterMs : null;
|
|
293
305
|
this.hooks = options.hooks ?? {};
|
|
294
306
|
|
|
295
307
|
this.commands = {
|
|
@@ -549,6 +561,7 @@ export class ChatCore {
|
|
|
549
561
|
});
|
|
550
562
|
|
|
551
563
|
this.chats.set(session.id, session);
|
|
564
|
+
this.unloadedInfo.delete(session.id);
|
|
552
565
|
|
|
553
566
|
if (stored) {
|
|
554
567
|
session.settleStored(selection, resume);
|
|
@@ -557,6 +570,8 @@ export class ChatCore {
|
|
|
557
570
|
}
|
|
558
571
|
|
|
559
572
|
await this.hooks.opened?.(session, stored ?? null);
|
|
573
|
+
// A chat loaded for someone who never attaches, such as a host reading it, leaves again on its own.
|
|
574
|
+
this.scheduleUnload(session.id);
|
|
560
575
|
|
|
561
576
|
return session.info;
|
|
562
577
|
}
|
|
@@ -642,6 +657,7 @@ export class ChatCore {
|
|
|
642
657
|
// client is on the list: the text is either in the snapshot or in the stream, never both.
|
|
643
658
|
this.coalescers.get(chatId)?.flush();
|
|
644
659
|
clients.add(clientId);
|
|
660
|
+
this.cancelUnload(chatId);
|
|
645
661
|
const log = this.logs.get(chatId)!;
|
|
646
662
|
const events = since === undefined ? null : log.after(since);
|
|
647
663
|
|
|
@@ -797,6 +813,7 @@ export class ChatCore {
|
|
|
797
813
|
detach(chatId: string, clientId: string): void {
|
|
798
814
|
this.coalescers.get(chatId)?.flush();
|
|
799
815
|
this.attached.get(chatId)?.delete(clientId);
|
|
816
|
+
this.scheduleUnload(chatId);
|
|
800
817
|
}
|
|
801
818
|
|
|
802
819
|
detachAll(clientId: string): void {
|
|
@@ -804,6 +821,7 @@ export class ChatCore {
|
|
|
804
821
|
if (clients.has(clientId)) {
|
|
805
822
|
this.coalescers.get(chatId)?.flush();
|
|
806
823
|
clients.delete(clientId);
|
|
824
|
+
this.scheduleUnload(chatId);
|
|
807
825
|
}
|
|
808
826
|
}
|
|
809
827
|
|
|
@@ -931,6 +949,8 @@ export class ChatCore {
|
|
|
931
949
|
return false;
|
|
932
950
|
}
|
|
933
951
|
|
|
952
|
+
this.unloadedInfo.delete(chatId);
|
|
953
|
+
|
|
934
954
|
await Promise.all([
|
|
935
955
|
this.store?.delete(chatId),
|
|
936
956
|
this.attachments.removeAll(chatId),
|
|
@@ -955,7 +975,7 @@ export class ChatCore {
|
|
|
955
975
|
}
|
|
956
976
|
|
|
957
977
|
/* A line from the host in a chat's thread, loading the chat when nobody has; nothing for a chat that is gone. */
|
|
958
|
-
async addNote(chatId: string, level: 'info' | 'warning' | 'error', text: string): Promise<void> {
|
|
978
|
+
async addNote(chatId: string, level: 'info' | 'warning' | 'error', text: string, message?: ChatNoteMessage): Promise<void> {
|
|
959
979
|
await this.loaded(chatId);
|
|
960
980
|
|
|
961
981
|
if (!(await this.hasStored(chatId))) {
|
|
@@ -966,7 +986,7 @@ export class ChatCore {
|
|
|
966
986
|
await this.create({ chatId });
|
|
967
987
|
}
|
|
968
988
|
|
|
969
|
-
this.chats.get(chatId)?.addNote(level, text);
|
|
989
|
+
this.chats.get(chatId)?.addNote(level, text, message);
|
|
970
990
|
}
|
|
971
991
|
|
|
972
992
|
/*
|
|
@@ -1295,21 +1315,9 @@ export class ChatCore {
|
|
|
1295
1315
|
// A write already out would put the record back after the delete.
|
|
1296
1316
|
const writing = this.writes.get(chatId);
|
|
1297
1317
|
const ended = session.dispose();
|
|
1298
|
-
this.
|
|
1299
|
-
this.uiQueries?.forget(chatId);
|
|
1300
|
-
this.coalescers.get(chatId)?.dispose();
|
|
1301
|
-
this.coalescers.delete(chatId);
|
|
1302
|
-
this.logs.get(chatId)?.close();
|
|
1303
|
-
this.logs.delete(chatId);
|
|
1304
|
-
this.cancelWaiting(chatId);
|
|
1305
|
-
this.sizes.delete(chatId);
|
|
1306
|
-
this.unlogged.delete(chatId);
|
|
1307
|
-
this.writes.delete(chatId);
|
|
1308
|
-
this.activity.delete(chatId);
|
|
1309
|
-
this.chats.delete(chatId);
|
|
1318
|
+
this.forgetLoaded(chatId);
|
|
1310
1319
|
this.attached.delete(chatId);
|
|
1311
|
-
this.
|
|
1312
|
-
this.announced.delete(chatId);
|
|
1320
|
+
this.unloadedInfo.delete(chatId);
|
|
1313
1321
|
const snapshot = session.thread.snapshot();
|
|
1314
1322
|
this.hooks.forgotten?.(chatId);
|
|
1315
1323
|
await writing;
|
|
@@ -1349,7 +1357,91 @@ export class ChatCore {
|
|
|
1349
1357
|
}
|
|
1350
1358
|
|
|
1351
1359
|
list(): ChatInfo[] {
|
|
1352
|
-
return [...this.chats.values()].map((session) => session.info);
|
|
1360
|
+
return [...[...this.chats.values()].map((session) => session.info), ...this.unloadedInfo.values()];
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/*
|
|
1364
|
+
* Puts a chat nobody needs in memory back on disk alone: nobody reads it, it has no CLI, no turn,
|
|
1365
|
+
* request, hold or move is under way, and no resume is owed. Answers whether it left. The next
|
|
1366
|
+
* create, attach or note loads it again from its record, as after a restart.
|
|
1367
|
+
*/
|
|
1368
|
+
async unloadIdle(chatId: string): Promise<boolean> {
|
|
1369
|
+
this.cancelUnload(chatId);
|
|
1370
|
+
|
|
1371
|
+
if (!this.unloadable(chatId)) {
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
await this.persistNow(chatId, true);
|
|
1376
|
+
const session = this.chats.get(chatId);
|
|
1377
|
+
|
|
1378
|
+
// Someone may have attached or sent while the record was written.
|
|
1379
|
+
if (!session || !this.unloadable(chatId)) {
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
this.forgetLoaded(chatId);
|
|
1384
|
+
this.attached.delete(chatId);
|
|
1385
|
+
this.unloadedInfo.set(chatId, session.info);
|
|
1386
|
+
await session.dispose();
|
|
1387
|
+
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
private unloadable(chatId: string): boolean {
|
|
1392
|
+
const session = this.chats.get(chatId);
|
|
1393
|
+
|
|
1394
|
+
return (
|
|
1395
|
+
session !== undefined &&
|
|
1396
|
+
this.store !== null &&
|
|
1397
|
+
(this.attached.get(chatId)?.size ?? 0) === 0 &&
|
|
1398
|
+
!this.creating.has(chatId) &&
|
|
1399
|
+
!this.relocations.has(chatId) &&
|
|
1400
|
+
session.pid === null &&
|
|
1401
|
+
session.relocatable &&
|
|
1402
|
+
session.info.resumeAt === undefined &&
|
|
1403
|
+
(session.info.requests?.length ?? 0) === 0
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
private scheduleUnload(chatId: string): void {
|
|
1408
|
+
if (this.unloadAfterMs === null || (this.attached.get(chatId)?.size ?? 0) > 0) {
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
this.cancelUnload(chatId);
|
|
1413
|
+
|
|
1414
|
+
const timer = setTimeout(() => {
|
|
1415
|
+
this.unloadTimers.delete(chatId);
|
|
1416
|
+
void this.unloadIdle(chatId).catch((e) => console.error(`Unloading chat ${chatId} failed:`, errorText(e)));
|
|
1417
|
+
}, this.unloadAfterMs);
|
|
1418
|
+
|
|
1419
|
+
timer.unref?.();
|
|
1420
|
+
this.unloadTimers.set(chatId, timer);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
private cancelUnload(chatId: string): void {
|
|
1424
|
+
clearTimeout(this.unloadTimers.get(chatId));
|
|
1425
|
+
this.unloadTimers.delete(chatId);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/* Drops what the core keeps of a loaded chat beside its session, which a kill and an unload share. */
|
|
1429
|
+
private forgetLoaded(chatId: string): void {
|
|
1430
|
+
this.subagents.releaseChat(chatId);
|
|
1431
|
+
this.uiQueries?.forget(chatId);
|
|
1432
|
+
this.coalescers.get(chatId)?.dispose();
|
|
1433
|
+
this.coalescers.delete(chatId);
|
|
1434
|
+
this.logs.get(chatId)?.close();
|
|
1435
|
+
this.logs.delete(chatId);
|
|
1436
|
+
this.cancelWaiting(chatId);
|
|
1437
|
+
this.cancelUnload(chatId);
|
|
1438
|
+
this.sizes.delete(chatId);
|
|
1439
|
+
this.unlogged.delete(chatId);
|
|
1440
|
+
this.writes.delete(chatId);
|
|
1441
|
+
this.activity.delete(chatId);
|
|
1442
|
+
this.chats.delete(chatId);
|
|
1443
|
+
this.listChanges.delete(chatId);
|
|
1444
|
+
this.announced.delete(chatId);
|
|
1353
1445
|
}
|
|
1354
1446
|
|
|
1355
1447
|
get(chatId: string): ChatSession | undefined {
|
|
@@ -1460,6 +1552,7 @@ export class ChatCore {
|
|
|
1460
1552
|
|
|
1461
1553
|
for (const chatId of this.chats.keys()) {
|
|
1462
1554
|
this.cancelWaiting(chatId);
|
|
1555
|
+
this.cancelUnload(chatId);
|
|
1463
1556
|
}
|
|
1464
1557
|
|
|
1465
1558
|
// A thread that cannot be written still ends its CLI, whose process group would otherwise outlive the host.
|
|
@@ -1649,6 +1742,9 @@ export class ChatCore {
|
|
|
1649
1742
|
this.sinks.to(clientId, { event: 'chat.event', payload });
|
|
1650
1743
|
}
|
|
1651
1744
|
|
|
1745
|
+
// A chat nobody reads counts its time from what it said last, such as its CLI going down.
|
|
1746
|
+
this.scheduleUnload(chatId);
|
|
1747
|
+
|
|
1652
1748
|
// After the thread, so a client reading it has already had this info and the status is a no-op there.
|
|
1653
1749
|
if (event.type === 'info' || event.type === 'reset') {
|
|
1654
1750
|
this.announceStatus(chatId, event.info);
|
package/src/chat/chat-session.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ChatGoalAction,
|
|
7
7
|
ChatInfo,
|
|
8
8
|
ChatItem,
|
|
9
|
+
ChatNoteMessage,
|
|
9
10
|
ChatQueuedMessage,
|
|
10
11
|
ChatQuoteSource,
|
|
11
12
|
ChatSkill,
|
|
@@ -1470,8 +1471,18 @@ export class ChatSession {
|
|
|
1470
1471
|
}
|
|
1471
1472
|
|
|
1472
1473
|
/* A line in the thread outside any turn, for something the host has to say about work it gave up on. */
|
|
1473
|
-
addNote(level: 'info' | 'warning' | 'error', text: string): void {
|
|
1474
|
-
this.emit([
|
|
1474
|
+
addNote(level: 'info' | 'warning' | 'error', text: string, message?: ChatNoteMessage): void {
|
|
1475
|
+
this.emit([
|
|
1476
|
+
this.thread.upsert({
|
|
1477
|
+
id: newId('note'),
|
|
1478
|
+
kind: 'note',
|
|
1479
|
+
createdAt: Date.now(),
|
|
1480
|
+
turnId: null,
|
|
1481
|
+
level,
|
|
1482
|
+
text,
|
|
1483
|
+
...(message === undefined ? {} : { message })
|
|
1484
|
+
})
|
|
1485
|
+
]);
|
|
1475
1486
|
this.options.persist();
|
|
1476
1487
|
}
|
|
1477
1488
|
|
|
@@ -9,7 +9,7 @@ import { isNotFound, writeAtomic, writeAtomicSync } from '../fs.ts';
|
|
|
9
9
|
import { ChatError } from './errors.ts';
|
|
10
10
|
|
|
11
11
|
/* How much of a value stored apart the item keeps: what a timeline shows before anyone asks for the rest. */
|
|
12
|
-
export const PAYLOAD_PREVIEW_BYTES =
|
|
12
|
+
export const PAYLOAD_PREVIEW_BYTES = 2 * 1024;
|
|
13
13
|
|
|
14
14
|
const FOLDER_SUFFIX = '.payloads';
|
|
15
15
|
const ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatItem } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ChatItem, ChatNoteMessage } from '@adecore/agent-contracts';
|
|
2
2
|
import type { ChatCore } from '../chat/chat-core.ts';
|
|
3
3
|
import type { Notice, NoticeStore } from './notice-store.ts';
|
|
4
4
|
|
|
@@ -111,7 +111,12 @@ export interface NoticeChat {
|
|
|
111
111
|
/* Whether the host holds a chat under the id, running or on disk. */
|
|
112
112
|
has(id: string): Promise<boolean>;
|
|
113
113
|
/* Puts a line in that chat's thread, loading the chat when nobody has. */
|
|
114
|
-
note(id: string, text: string): Promise<void>;
|
|
114
|
+
note(id: string, text: string, message: ChatNoteMessage): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* The message itself on the line in the thread, so a client can draw it as said to the chat. */
|
|
118
|
+
export function noticeMessage(notice: Notice): ChatNoteMessage {
|
|
119
|
+
return { from: notice.from, title: notice.fromTitle, text: notice.text };
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/*
|
|
@@ -124,6 +129,6 @@ export async function showNotices(store: Pick<NoticeStore, 'show'>, chat: Notice
|
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
for (const notice of await store.show(targetId)) {
|
|
127
|
-
await chat.note(targetId, words.shown(notice));
|
|
132
|
+
await chat.note(targetId, words.shown(notice), noticeMessage(notice));
|
|
128
133
|
}
|
|
129
134
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { ChatNoteMessage } from '@adecore/agent-contracts';
|
|
1
2
|
import type { PromptNotes } from '../chat/chat-session.ts';
|
|
2
|
-
import type
|
|
3
|
+
import { noticeMessage, type MessageWords } from './deliver-notice.ts';
|
|
3
4
|
import type { NoticeStore } from './notice-store.ts';
|
|
4
5
|
|
|
5
6
|
/*
|
|
@@ -29,6 +30,10 @@ export class NoticeNotes implements PromptNotes {
|
|
|
29
30
|
* The lines a chat that just loaded owes a person for what landed while nobody held it, marked so a
|
|
30
31
|
* reload writes none again. The model still hears the messages themselves from the queue.
|
|
31
32
|
*/
|
|
32
|
-
export async function unshownNotes(
|
|
33
|
-
|
|
33
|
+
export async function unshownNotes(
|
|
34
|
+
store: Pick<NoticeStore, 'show'>,
|
|
35
|
+
chatId: string,
|
|
36
|
+
words: Pick<MessageWords, 'shown'>
|
|
37
|
+
): Promise<{ text: string; message: ChatNoteMessage }[]> {
|
|
38
|
+
return (await store.show(chatId)).map((notice) => ({ text: words.shown(notice), message: noticeMessage(notice) }));
|
|
34
39
|
}
|