@adecore/agents 0.24.2 → 0.25.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 +7 -0
- package/dist/chat/chat-core.js +71 -0
- package/dist/chat/chat-session.d.ts +8 -0
- package/dist/chat/chat-session.js +50 -4
- package/dist/chat/index.d.ts +2 -2
- package/dist/chat/index.js +2 -2
- package/dist/chat/thread.d.ts +1 -0
- package/dist/chat/thread.js +3 -0
- package/dist/chat/ui-queries.d.ts +8 -1
- package/dist/chat/ui-queries.js +137 -7
- package/dist/chat/wake-chat.d.ts +1 -0
- package/dist/chat/wake-chat.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lineage.d.ts +1 -0
- package/dist/lineage.js +8 -0
- package/dist/messages/deliver-message.js +6 -2
- package/dist/record-directory.js +6 -3
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/package.json +3 -3
- package/src/chat/chat-core.ts +77 -0
- package/src/chat/chat-session.ts +58 -4
- package/src/chat/index.ts +2 -2
- package/src/chat/thread.ts +4 -0
- package/src/chat/ui-queries.ts +155 -8
- package/src/chat/wake-chat.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lineage.ts +9 -0
- package/src/messages/deliver-message.ts +6 -2
- package/src/record-directory.ts +6 -3
- package/src/testing/index.ts +2 -2
package/dist/chat/chat-core.d.ts
CHANGED
|
@@ -92,6 +92,7 @@ export declare class ChatCore {
|
|
|
92
92
|
private readonly hooks;
|
|
93
93
|
private readonly chats;
|
|
94
94
|
private readonly creating;
|
|
95
|
+
private readonly relocations;
|
|
95
96
|
private readonly sinks;
|
|
96
97
|
private readonly attached;
|
|
97
98
|
private readonly listChanges;
|
|
@@ -236,6 +237,12 @@ export declare class ChatCore {
|
|
|
236
237
|
dismiss(chatId: string, itemId: string): void;
|
|
237
238
|
holdForPerson(chatId: string, key: string): boolean;
|
|
238
239
|
releaseHold(chatId: string, key: string): void;
|
|
240
|
+
relocate(chatId: string, cwd: string, during?: () => Promise<void>): Promise<ChatInfo>;
|
|
241
|
+
isRelocating(chatId: string): boolean;
|
|
242
|
+
private relocateNow;
|
|
243
|
+
private refuseUnlessRelocatable;
|
|
244
|
+
private busyRelocating;
|
|
245
|
+
private refuseWhileRelocating;
|
|
239
246
|
kill(chatId: string): Promise<void>;
|
|
240
247
|
stop(chatId: string, reason: string): Promise<void>;
|
|
241
248
|
processTargets(): {
|
package/dist/chat/chat-core.js
CHANGED
|
@@ -57,6 +57,8 @@ export class ChatCore {
|
|
|
57
57
|
hooks;
|
|
58
58
|
chats = new Map();
|
|
59
59
|
creating = new Map();
|
|
60
|
+
// The relocations under way, settled (never rejected) when each ends.
|
|
61
|
+
relocations = new Map();
|
|
60
62
|
sinks = new ClientSinks((clientId) => this.composerPreferences.forget(clientId));
|
|
61
63
|
attached = new Map();
|
|
62
64
|
// How often a chat's bookmarks or visuals changed, so an attach can tell that its read went stale before it joined.
|
|
@@ -350,6 +352,7 @@ export class ChatCore {
|
|
|
350
352
|
configure(payload) {
|
|
351
353
|
const session = this.require(payload.chatId);
|
|
352
354
|
if (payload.account !== undefined) {
|
|
355
|
+
this.refuseWhileRelocating(payload.chatId);
|
|
353
356
|
this.switchAccount(session, payload.account);
|
|
354
357
|
}
|
|
355
358
|
return session.configure(payload);
|
|
@@ -712,12 +715,14 @@ export class ChatCore {
|
|
|
712
715
|
/* 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
716
|
async restart(chatId) {
|
|
714
717
|
const session = this.require(chatId);
|
|
718
|
+
this.refuseWhileRelocating(chatId);
|
|
715
719
|
const ended = session.restart();
|
|
716
720
|
this.skillIndex.forget(session.info.provider, session.info.cwd);
|
|
717
721
|
await ended;
|
|
718
722
|
}
|
|
719
723
|
compact(chatId) {
|
|
720
724
|
const session = this.require(chatId);
|
|
725
|
+
this.refuseWhileRelocating(chatId);
|
|
721
726
|
if (session.busy) {
|
|
722
727
|
throw new ChatError('chat-busy', `Chat ${chatId} is still working on the previous message`);
|
|
723
728
|
}
|
|
@@ -726,6 +731,7 @@ export class ChatCore {
|
|
|
726
731
|
/* Empties the thread and drops the CLI's session and the chat's bookmarks and visuals; `force` stops a turn that is in the way. */
|
|
727
732
|
async clear(chatId, force = false) {
|
|
728
733
|
const session = this.require(chatId);
|
|
734
|
+
this.refuseWhileRelocating(chatId);
|
|
729
735
|
if (session.busy && !force) {
|
|
730
736
|
throw new ChatError('chat-busy', `Chat ${chatId} is still working on the previous message`);
|
|
731
737
|
}
|
|
@@ -780,9 +786,74 @@ export class ChatCore {
|
|
|
780
786
|
releaseHold(chatId, key) {
|
|
781
787
|
this.chats.get(chatId)?.releaseHold(key);
|
|
782
788
|
}
|
|
789
|
+
/*
|
|
790
|
+
* Moves a loaded chat to another working folder. `during` is the host's own work and runs with the CLI
|
|
791
|
+
* already ended; messages sent meanwhile queue and go out in the new folder. Any throw keeps the old folder.
|
|
792
|
+
*/
|
|
793
|
+
async relocate(chatId, cwd, during) {
|
|
794
|
+
const session = this.require(chatId);
|
|
795
|
+
this.refuseUnlessRelocatable(session);
|
|
796
|
+
const running = this.relocateNow(session, cwd, during);
|
|
797
|
+
const tracked = running.then(() => undefined, () => undefined);
|
|
798
|
+
this.relocations.set(chatId, tracked);
|
|
799
|
+
try {
|
|
800
|
+
return await running;
|
|
801
|
+
}
|
|
802
|
+
finally {
|
|
803
|
+
if (this.relocations.get(chatId) === tracked) {
|
|
804
|
+
this.relocations.delete(chatId);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/* Whether a relocation of the chat is under way, which a host that forks or copies the chat has to refuse. */
|
|
809
|
+
isRelocating(chatId) {
|
|
810
|
+
return this.relocations.has(chatId);
|
|
811
|
+
}
|
|
812
|
+
async relocateNow(session, cwd, during) {
|
|
813
|
+
const chatId = session.id;
|
|
814
|
+
await this.hooks.admit?.(chatId, cwd);
|
|
815
|
+
// A message sent while the folder was asked about may have opened a turn.
|
|
816
|
+
if (!session.relocatable) {
|
|
817
|
+
throw this.busyRelocating(chatId);
|
|
818
|
+
}
|
|
819
|
+
const previous = session.info.cwd;
|
|
820
|
+
session.beginRelocate();
|
|
821
|
+
try {
|
|
822
|
+
await session.dropProcess();
|
|
823
|
+
await during?.();
|
|
824
|
+
session.setCwd(cwd);
|
|
825
|
+
await this.persistNow(chatId, true);
|
|
826
|
+
}
|
|
827
|
+
catch (e) {
|
|
828
|
+
session.setCwd(previous);
|
|
829
|
+
throw e;
|
|
830
|
+
}
|
|
831
|
+
finally {
|
|
832
|
+
session.endRelocate();
|
|
833
|
+
}
|
|
834
|
+
this.skillIndex.forget(session.info.provider, previous);
|
|
835
|
+
return session.info;
|
|
836
|
+
}
|
|
837
|
+
refuseUnlessRelocatable(session) {
|
|
838
|
+
if (this.relocations.has(session.id) || !session.relocatable) {
|
|
839
|
+
throw this.busyRelocating(session.id);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
busyRelocating(chatId) {
|
|
843
|
+
return new ChatError('chat-busy', `Chat ${chatId} is still working; stop it before moving it to another folder`);
|
|
844
|
+
}
|
|
845
|
+
refuseWhileRelocating(chatId) {
|
|
846
|
+
if (this.relocations.has(chatId)) {
|
|
847
|
+
throw new ChatError('chat-busy', `Chat ${chatId} is being moved to another folder`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
783
850
|
/* Removes a chat: its CLI, its record, its log, its attachments, its bookmarks and its visuals. */
|
|
784
851
|
async kill(chatId) {
|
|
785
852
|
await this.loaded(chatId);
|
|
853
|
+
const moving = this.relocations.get(chatId);
|
|
854
|
+
if (moving) {
|
|
855
|
+
await moving;
|
|
856
|
+
}
|
|
786
857
|
const session = this.require(chatId);
|
|
787
858
|
// A write already out would put the record back after the delete.
|
|
788
859
|
const writing = this.writes.get(chatId);
|
|
@@ -92,6 +92,8 @@ export declare class ChatSession {
|
|
|
92
92
|
private awaitingBackgroundReport;
|
|
93
93
|
private launchedSelection;
|
|
94
94
|
private launchedAccount;
|
|
95
|
+
private launchedCwd;
|
|
96
|
+
private relocating;
|
|
95
97
|
private launchedRuntimeMode;
|
|
96
98
|
private launchedFolders;
|
|
97
99
|
private turnReady;
|
|
@@ -125,6 +127,11 @@ export declare class ChatSession {
|
|
|
125
127
|
resumeAtReset?: boolean;
|
|
126
128
|
}): ChatInfo;
|
|
127
129
|
setAccount(account: string | undefined): ChatInfo;
|
|
130
|
+
setCwd(cwd: string): ChatInfo;
|
|
131
|
+
get relocatable(): boolean;
|
|
132
|
+
get isRelocating(): boolean;
|
|
133
|
+
beginRelocate(): void;
|
|
134
|
+
endRelocate(): void;
|
|
128
135
|
send(text: string, sent?: ChatSendExtras): {
|
|
129
136
|
queued: boolean;
|
|
130
137
|
turnId: string;
|
|
@@ -190,6 +197,7 @@ export declare class ChatSession {
|
|
|
190
197
|
addNote(level: 'info' | 'warning' | 'error', text: string): void;
|
|
191
198
|
end(reason: string): void;
|
|
192
199
|
restart(): Promise<void>;
|
|
200
|
+
dropProcess(): Promise<void>;
|
|
193
201
|
freeze(): void;
|
|
194
202
|
dispose(): Promise<void>;
|
|
195
203
|
turnDiff(turnId: string): Promise<ChatCheckpointDiff | null>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { HOST_TASK_ORIGIN, notResumedNote } from '@adecore/agent-contracts';
|
|
2
|
+
import { ENDED_NOTE_PREFIX, HOST_TASK_ORIGIN, notResumedNote } from '@adecore/agent-contracts';
|
|
3
3
|
import { resolveUiChoice, UI_FENCE_LANGUAGE, UiFailure } from '@adecore/intelligent-ui';
|
|
4
4
|
import { commandLabel, isBackgroundWork, reportsOnBackgroundWork, runningInBackground } from './background-work.js';
|
|
5
5
|
import { ChatError } from './errors.js';
|
|
@@ -55,6 +55,9 @@ export class ChatSession {
|
|
|
55
55
|
launchedSelection = null;
|
|
56
56
|
// The account the running CLI was started under, which what it reports about a plan belongs to.
|
|
57
57
|
launchedAccount = undefined;
|
|
58
|
+
launchedCwd = null;
|
|
59
|
+
// Set while the host moves the chat's folder: messages queue and nothing starts a process until it ends.
|
|
60
|
+
relocating = false;
|
|
58
61
|
launchedRuntimeMode = null;
|
|
59
62
|
launchedFolders = [];
|
|
60
63
|
// The checkpoint of the turn in flight; everything queued for that turn waits for it.
|
|
@@ -189,6 +192,39 @@ export class ChatSession {
|
|
|
189
192
|
this.drainQueue();
|
|
190
193
|
return this.thread.info;
|
|
191
194
|
}
|
|
195
|
+
/* The next process starts in this folder; the host moves the chat through `ChatCore.relocate`, which has already ended the old one. */
|
|
196
|
+
setCwd(cwd) {
|
|
197
|
+
if (cwd === this.thread.info.cwd) {
|
|
198
|
+
return this.thread.info;
|
|
199
|
+
}
|
|
200
|
+
this.emit([this.thread.patchInfo({ cwd })]);
|
|
201
|
+
this.options.persist();
|
|
202
|
+
return this.thread.info;
|
|
203
|
+
}
|
|
204
|
+
get relocatable() {
|
|
205
|
+
return (!this.frozen &&
|
|
206
|
+
!this.relocating &&
|
|
207
|
+
this.thread.info.activeTurnId === null &&
|
|
208
|
+
!this.processInUse() &&
|
|
209
|
+
this.pendingStart === null &&
|
|
210
|
+
!this.thread.held);
|
|
211
|
+
}
|
|
212
|
+
get isRelocating() {
|
|
213
|
+
return this.relocating;
|
|
214
|
+
}
|
|
215
|
+
/* Messages sent from here on queue, and the queue stays put until `endRelocate`. */
|
|
216
|
+
beginRelocate() {
|
|
217
|
+
this.relocating = true;
|
|
218
|
+
}
|
|
219
|
+
/*
|
|
220
|
+
* Lets the queue go, in whichever folder the chat has now. A wake that found the chat relocating
|
|
221
|
+
* was turned away and waits for an event of this chat, so one goes out even when the queue was empty.
|
|
222
|
+
*/
|
|
223
|
+
endRelocate() {
|
|
224
|
+
this.relocating = false;
|
|
225
|
+
this.drainQueue();
|
|
226
|
+
this.emit([this.thread.patchInfo({})]);
|
|
227
|
+
}
|
|
192
228
|
/*
|
|
193
229
|
* Internal follow-ups wait for a new turn. User input goes through `sendInput`, which can steer native work.
|
|
194
230
|
*/
|
|
@@ -196,7 +232,7 @@ export class ChatSession {
|
|
|
196
232
|
const extras = this.vetted(sent);
|
|
197
233
|
const turnId = newId('turn');
|
|
198
234
|
// Behind whatever still waits (a CLI that crashed leaves its queue), so everything goes out in the order it was sent.
|
|
199
|
-
if (this.busy || this.queue.length > 0 || this.replacementWaits()) {
|
|
235
|
+
if (this.busy || this.queue.length > 0 || this.replacementWaits() || this.relocating) {
|
|
200
236
|
if (this.busy && this.info.queuePaused) {
|
|
201
237
|
this.sendAfterStop = true;
|
|
202
238
|
}
|
|
@@ -422,6 +458,7 @@ export class ChatSession {
|
|
|
422
458
|
if (!next ||
|
|
423
459
|
this.steering.size > 0 ||
|
|
424
460
|
this.busy ||
|
|
461
|
+
this.relocating ||
|
|
425
462
|
(!explicit && (this.info.queuePaused || (limited !== null && (limited.limit.kind === 'usage' || this.info.resumeAt !== undefined))))) {
|
|
426
463
|
return;
|
|
427
464
|
}
|
|
@@ -790,7 +827,7 @@ export class ChatSession {
|
|
|
790
827
|
* with `origin: 'agent'` and close the wake it just made.
|
|
791
828
|
*/
|
|
792
829
|
wake(wake) {
|
|
793
|
-
if (this.frozen || this.thread.info.activeTurnId !== null || this.replacementWaits()) {
|
|
830
|
+
if (this.frozen || this.relocating || this.thread.info.activeTurnId !== null || this.replacementWaits()) {
|
|
794
831
|
return null;
|
|
795
832
|
}
|
|
796
833
|
this.turnOpened();
|
|
@@ -1007,7 +1044,7 @@ export class ChatSession {
|
|
|
1007
1044
|
}
|
|
1008
1045
|
}
|
|
1009
1046
|
if (turnId !== null) {
|
|
1010
|
-
events.push(this.thread.upsert({ id: newId(
|
|
1047
|
+
events.push(this.thread.upsert({ id: newId(`${ENDED_NOTE_PREFIX}note`), kind: 'note', createdAt: now, turnId, level: 'warning', text: reason }));
|
|
1011
1048
|
}
|
|
1012
1049
|
events.push(this.thread.patchInfo({
|
|
1013
1050
|
running: false,
|
|
@@ -1030,6 +1067,13 @@ export class ChatSession {
|
|
|
1030
1067
|
if (this.thread.info.activeTurnId !== null || this.processInUse()) {
|
|
1031
1068
|
throw new ChatError('chat-busy', `Chat ${this.id} is still working; stop it before restarting its agent session`);
|
|
1032
1069
|
}
|
|
1070
|
+
return this.dropProcess();
|
|
1071
|
+
}
|
|
1072
|
+
/*
|
|
1073
|
+
* Ends the CLI and keeps the conversation, like `restart` without its refusal: the caller has
|
|
1074
|
+
* checked `relocatable` and holds the queue. Settles once the process exited or was sent the SIGKILL.
|
|
1075
|
+
*/
|
|
1076
|
+
dropProcess() {
|
|
1033
1077
|
this.clearStartWarning();
|
|
1034
1078
|
this.clearWorkflowWarnings();
|
|
1035
1079
|
const backend = this.backend;
|
|
@@ -1482,6 +1526,7 @@ export class ChatSession {
|
|
|
1482
1526
|
(JSON.stringify(this.info.selection) !== JSON.stringify(this.launchedSelection) ||
|
|
1483
1527
|
this.info.runtimeMode !== this.launchedRuntimeMode ||
|
|
1484
1528
|
this.info.account !== this.launchedAccount ||
|
|
1529
|
+
this.info.cwd !== this.launchedCwd ||
|
|
1485
1530
|
!sameFolders(this.options.folders?.() ?? [], this.launchedFolders)));
|
|
1486
1531
|
}
|
|
1487
1532
|
hasNativeWork() {
|
|
@@ -1519,6 +1564,7 @@ export class ChatSession {
|
|
|
1519
1564
|
const info = this.thread.info;
|
|
1520
1565
|
this.launchedSelection = info.selection;
|
|
1521
1566
|
this.launchedRuntimeMode = info.runtimeMode;
|
|
1567
|
+
this.launchedCwd = info.cwd;
|
|
1522
1568
|
const folders = this.options.folders?.() ?? [];
|
|
1523
1569
|
this.launchedFolders = folders;
|
|
1524
1570
|
const launch = {
|
package/dist/chat/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { type ChatRecordExtras, type ChatRecord, ChatStore, type ChatSeq, type C
|
|
|
10
10
|
export { suggestChatTitle, type ChatTitleInput } from './chat-title.ts';
|
|
11
11
|
export type { ClaudeBackendOptions } from './claude-backend.ts';
|
|
12
12
|
export { ClaudeTitleReader } from './claude-title.ts';
|
|
13
|
-
export {
|
|
13
|
+
export { claudeProjectSlug, findClaudeSession } from './claude-transcript.ts';
|
|
14
14
|
export type { CodexBackendOptions } from './codex-backend.ts';
|
|
15
15
|
export { forkThreadOnce, type ThreadItemsParams, type CodexProcessSpec, type ThreadForkParams } from './codex-thread.ts';
|
|
16
16
|
export type { CodexClientInfo } from './codex-transport.ts';
|
|
@@ -26,4 +26,4 @@ export { ChatThread } from './thread.ts';
|
|
|
26
26
|
export { type ChatUiSource, type ChatUiHost, ChatUiRefusal } from './ui-queries.ts';
|
|
27
27
|
export { visualFileName, VisualStore, type VisualInput, type VisualStoreOptions, type VisualListener } from './visual-store.ts';
|
|
28
28
|
export { type WakeChat, chatOpener, type ChatOpenerDeps } from './wake-chat.ts';
|
|
29
|
-
export { type SkillRootOptions
|
|
29
|
+
export { type SkillRootOptions } from '../skills.ts';
|
package/dist/chat/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { ChatSession } from './chat-session.js';
|
|
|
9
9
|
export { ChatStore } from './chat-store.js';
|
|
10
10
|
export { suggestChatTitle } from './chat-title.js';
|
|
11
11
|
export { ClaudeTitleReader } from './claude-title.js';
|
|
12
|
-
export {
|
|
12
|
+
export { claudeProjectSlug, findClaudeSession } from './claude-transcript.js';
|
|
13
13
|
export { forkThreadOnce } from './codex-thread.js';
|
|
14
14
|
export { ChatError } from './errors.js';
|
|
15
15
|
export { listClaudeSessions, listCodexSessions, nodeSessionFs } from './external-sessions.js';
|
|
@@ -20,4 +20,4 @@ export { ChatThread } from './thread.js';
|
|
|
20
20
|
export { ChatUiRefusal } from './ui-queries.js';
|
|
21
21
|
export { visualFileName, VisualStore } from './visual-store.js';
|
|
22
22
|
export { chatOpener } from './wake-chat.js';
|
|
23
|
-
export {
|
|
23
|
+
export {} from '../skills.js';
|
package/dist/chat/thread.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare class ChatThread {
|
|
|
17
17
|
upsert(item: ChatItem): ChatEvent;
|
|
18
18
|
appendText(itemId: string, text: string): ChatEvent | null;
|
|
19
19
|
patchInfo(patch: Partial<ChatInfo>): ChatEvent;
|
|
20
|
+
get held(): boolean;
|
|
20
21
|
hold(key: string): boolean;
|
|
21
22
|
release(key: string): boolean;
|
|
22
23
|
reset(patch: Partial<ChatInfo>): ChatEvent;
|
package/dist/chat/thread.js
CHANGED
|
@@ -69,6 +69,9 @@ export class ChatThread {
|
|
|
69
69
|
this.info = { ...this.info, ...held };
|
|
70
70
|
return { type: 'info', info: this.info };
|
|
71
71
|
}
|
|
72
|
+
get held() {
|
|
73
|
+
return this.holds.size > 0;
|
|
74
|
+
}
|
|
72
75
|
/* False when the key held the chat already. */
|
|
73
76
|
hold(key) {
|
|
74
77
|
if (this.holds.has(key)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { z } from 'zod';
|
|
2
|
-
import type { ChatUiLinkPayload, ChatUiLinkReading, ChatAssistantItem, ChatInfo, ChatUiChoicePayload, ChatUiQueryPayload, ChatUiQueryReading } from '@adecore/agent-contracts';
|
|
2
|
+
import type { ChatUiLinkPayload, ChatUiLinkReading, ChatUiPlaceReading, ChatAssistantItem, ChatInfo, ChatUiChoicePayload, ChatUiQueryPayload, ChatUiQueryReading } from '@adecore/agent-contracts';
|
|
3
3
|
import { type UiValue } from '@adecore/intelligent-ui';
|
|
4
|
+
import { type UiPlaceQuery } from '@adecore/intelligent-ui/places';
|
|
4
5
|
import { type UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
5
6
|
import type { ChatSession } from './chat-session.ts';
|
|
6
7
|
import { ChatError } from './errors.ts';
|
|
@@ -14,6 +15,7 @@ export interface ChatUiHost {
|
|
|
14
15
|
capture(info: ChatInfo): Promise<unknown>;
|
|
15
16
|
sources: Readonly<Record<string, ChatUiSource>>;
|
|
16
17
|
link?(info: ChatInfo, access: unknown, target: UiLinkTarget): Promise<ChatUiLinkReading>;
|
|
18
|
+
place?(info: ChatInfo, query: UiPlaceQuery, signal: AbortSignal): Promise<Omit<ChatUiPlaceReading, 'address'>>;
|
|
17
19
|
}
|
|
18
20
|
declare class UiQueryError extends ChatError<string> {
|
|
19
21
|
readonly reason: string;
|
|
@@ -26,6 +28,7 @@ export declare class ChatUiQueries {
|
|
|
26
28
|
readonly schemas: Record<string, z.ZodType>;
|
|
27
29
|
private readonly captures;
|
|
28
30
|
private readonly freezing;
|
|
31
|
+
private readonly lives;
|
|
29
32
|
private readonly cache;
|
|
30
33
|
private readonly latest;
|
|
31
34
|
private readonly newest;
|
|
@@ -36,6 +39,10 @@ export declare class ChatUiQueries {
|
|
|
36
39
|
private readonly host;
|
|
37
40
|
constructor(host: ChatUiHost);
|
|
38
41
|
observe(session: ChatSession, item: ChatAssistantItem, final: boolean): void;
|
|
42
|
+
whenFrozen(session: ChatSession, itemId: string): Promise<void>;
|
|
43
|
+
private lookUpPlaces;
|
|
44
|
+
private lifeOf;
|
|
45
|
+
private placeReading;
|
|
39
46
|
private access;
|
|
40
47
|
private storedBlock;
|
|
41
48
|
private stored;
|
package/dist/chat/ui-queries.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ChatUiPlaceReadingSchema } from '@adecore/agent-contracts';
|
|
2
3
|
import { copyUiValue, UI_HOST_LIMITS, UiBudget, uiMayReferenceHost } from '@adecore/intelligent-ui';
|
|
4
|
+
import { uiPlaceQueries } from '@adecore/intelligent-ui/places';
|
|
3
5
|
import { UiLinkResolutionSchema, uiLinkTargets } from '@adecore/intelligent-ui/links';
|
|
4
6
|
import { uiQueryArguments, uiQueryFallback } from '@adecore/intelligent-ui/query';
|
|
5
7
|
import { ChatError } from './errors.js';
|
|
@@ -26,12 +28,19 @@ export class ChatUiRefusal extends UiQueryError {
|
|
|
26
28
|
const MAX_QUERY_BYTES = 64 * 1024;
|
|
27
29
|
const MAX_SNAPSHOT_BYTES = 128 * 1024;
|
|
28
30
|
const READ_TIMEOUT_MS = 8_000;
|
|
31
|
+
const PLACE_TIMEOUT_MS = 5_000;
|
|
32
|
+
// Lookups run a few at a time, share one budget per block, and stop trying once the host keeps timing out.
|
|
33
|
+
const PLACE_PARALLEL = 4;
|
|
34
|
+
const PLACE_BLOCK_BUDGET_MS = 15_000;
|
|
35
|
+
const PLACE_TIMEOUTS_IN_A_ROW = 3;
|
|
29
36
|
// A changed input reads at once, but a query never reads more often than this across its inputs.
|
|
30
37
|
const INPUT_READ_MS = 250;
|
|
31
38
|
export class ChatUiQueries {
|
|
32
39
|
schemas;
|
|
33
40
|
captures = new Map();
|
|
34
|
-
freezing = new
|
|
41
|
+
freezing = new Map();
|
|
42
|
+
// Ends with the chat, so a lookup that is under way stops when its chat does.
|
|
43
|
+
lives = new Map();
|
|
35
44
|
cache = new Map();
|
|
36
45
|
// The read id of the newest reading per cache key, which a remount or a second client is answered with.
|
|
37
46
|
latest = new Map();
|
|
@@ -60,8 +69,7 @@ export class ChatUiQueries {
|
|
|
60
69
|
if (this.freezing.has(key)) {
|
|
61
70
|
return;
|
|
62
71
|
}
|
|
63
|
-
|
|
64
|
-
void capture
|
|
72
|
+
const frozen = capture
|
|
65
73
|
.then(async () => {
|
|
66
74
|
for (const block of (item.ui ?? []).slice(0, 16)) {
|
|
67
75
|
const current = session.thread.get(item.id);
|
|
@@ -99,6 +107,10 @@ export class ChatUiQueries {
|
|
|
99
107
|
}
|
|
100
108
|
}
|
|
101
109
|
}
|
|
110
|
+
const { places, found, ended } = await this.lookUpPlaces(session, item.id, block, values);
|
|
111
|
+
if (ended) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
102
114
|
const latest = session.thread.get(item.id);
|
|
103
115
|
if (latest?.kind !== 'assistant' || !latest.uiQueries || latest.ui?.find((entry) => entry.id === block.id)?.revision !== block.revision) {
|
|
104
116
|
return;
|
|
@@ -114,7 +126,8 @@ export class ChatUiQueries {
|
|
|
114
126
|
revision: block.revision,
|
|
115
127
|
readings,
|
|
116
128
|
links,
|
|
117
|
-
|
|
129
|
+
...(Object.keys(places).length > 0 ? { places } : {}),
|
|
130
|
+
fallback: `${uiQueryFallback(block, values).slice(0, 8192)}${names ? `\nSources: ${names}` : ''}${failures ? `\n${failures}` : ''}${found.length > 0 ? `\nPlaces: ${found.join('; ')}` : ''}`
|
|
118
131
|
};
|
|
119
132
|
const blocks = { ...latest.uiQueries.blocks, [block.id]: frozen };
|
|
120
133
|
if (Buffer.byteLength(JSON.stringify(blocks)) > MAX_SNAPSHOT_BYTES) {
|
|
@@ -123,16 +136,131 @@ export class ChatUiQueries {
|
|
|
123
136
|
{ state: 'failed', readAt: Date.now(), code: 'snapshot-too-large', reason: 'The frozen query values are too large.' }
|
|
124
137
|
]));
|
|
125
138
|
frozen.links = Object.fromEntries(Object.keys(links).map((id) => [id, { state: 'plain', code: 'link-unchecked', reason: 'This link must be checked again.' }]));
|
|
139
|
+
if (frozen.places) {
|
|
140
|
+
frozen.places = Object.fromEntries(Object.entries(places).map(([id, reading]) => [
|
|
141
|
+
id,
|
|
142
|
+
{ state: 'missing', address: reading.address, code: 'snapshot-too-large', reason: 'The frozen places are too large.' }
|
|
143
|
+
]));
|
|
144
|
+
}
|
|
126
145
|
frozen.fallback = `${block.fallback.slice(0, 2048)}\nSources: ${names}\nThe frozen query values are too large.`;
|
|
127
146
|
}
|
|
128
147
|
if (Buffer.byteLength(JSON.stringify(blocks)) > MAX_SNAPSHOT_BYTES) {
|
|
129
|
-
|
|
148
|
+
// The block stays marked as frozen with nothing in it, so a reader stops waiting for readings that will not come.
|
|
149
|
+
blocks[block.id] = { revision: block.revision, readings: {}, fallback: '' };
|
|
130
150
|
}
|
|
131
151
|
session.updateUiQueries(item.id, { ...latest.uiQueries, blocks });
|
|
132
152
|
}
|
|
133
153
|
})
|
|
134
154
|
.catch(() => { })
|
|
135
155
|
.finally(() => this.freezing.delete(key));
|
|
156
|
+
this.freezing.set(key, frozen);
|
|
157
|
+
}
|
|
158
|
+
/* Settles once the reply's readings, links and places are frozen, or at once where nothing is being frozen. */
|
|
159
|
+
async whenFrozen(session, itemId) {
|
|
160
|
+
await this.freezing.get(`${session.id}:${itemId}`);
|
|
161
|
+
}
|
|
162
|
+
/*
|
|
163
|
+
* The places of a block that name an address, looked up four at a time within one budget for the block.
|
|
164
|
+
* Past the budget, or after the host timed out three times in a row, the rest freeze as missing without
|
|
165
|
+
* asking. Nothing is asked once the chat ended, the reply is gone or the block changed; the caller then
|
|
166
|
+
* drops the write.
|
|
167
|
+
*/
|
|
168
|
+
async lookUpPlaces(session, itemId, block, values) {
|
|
169
|
+
const places = Object.create(null);
|
|
170
|
+
const found = [];
|
|
171
|
+
if (!this.host.place) {
|
|
172
|
+
return { places, found, ended: false };
|
|
173
|
+
}
|
|
174
|
+
let queries;
|
|
175
|
+
try {
|
|
176
|
+
queries = Object.entries(uiPlaceQueries(block, {}, values)).slice(0, UI_HOST_LIMITS.places);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return { places, found, ended: false };
|
|
180
|
+
}
|
|
181
|
+
const life = this.lifeOf(session.id).signal;
|
|
182
|
+
const budget = new AbortController();
|
|
183
|
+
const timer = setTimeout(() => budget.abort(), PLACE_BLOCK_BUDGET_MS);
|
|
184
|
+
timer.unref?.();
|
|
185
|
+
const signal = AbortSignal.any([life, budget.signal]);
|
|
186
|
+
const readings = new Map();
|
|
187
|
+
let timeouts = 0;
|
|
188
|
+
let next = 0;
|
|
189
|
+
const stillThere = () => {
|
|
190
|
+
const current = session.thread.get(itemId);
|
|
191
|
+
return !life.aborted && current?.kind === 'assistant' && current.ui?.find((entry) => entry.id === block.id)?.revision === block.revision;
|
|
192
|
+
};
|
|
193
|
+
const worker = async () => {
|
|
194
|
+
for (let index = next++; index < queries.length; index = next++) {
|
|
195
|
+
const [id, query] = queries[index];
|
|
196
|
+
if (!stillThere()) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (budget.signal.aborted || timeouts >= PLACE_TIMEOUTS_IN_A_ROW) {
|
|
200
|
+
readings.set(id, { state: 'missing', address: query.address, code: 'timed-out', reason: 'The place lookup timed out.' });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const reading = await this.placeReading(session.info, query, signal);
|
|
204
|
+
timeouts = reading.code === 'timed-out' ? timeouts + 1 : 0;
|
|
205
|
+
readings.set(id, reading);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
try {
|
|
209
|
+
await Promise.all(Array.from({ length: Math.min(PLACE_PARALLEL, queries.length) }, worker));
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
}
|
|
214
|
+
for (const [id, query] of queries) {
|
|
215
|
+
const reading = readings.get(id);
|
|
216
|
+
if (reading === undefined) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
places[id] = reading;
|
|
220
|
+
if (reading.state === 'found') {
|
|
221
|
+
found.push(`${query.name || query.address}: ${reading.lat}, ${reading.lon}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { places, found, ended: life.aborted };
|
|
225
|
+
}
|
|
226
|
+
lifeOf(chatId) {
|
|
227
|
+
let life = this.lives.get(chatId);
|
|
228
|
+
if (life === undefined) {
|
|
229
|
+
life = new AbortController();
|
|
230
|
+
this.lives.set(chatId, life);
|
|
231
|
+
}
|
|
232
|
+
return life;
|
|
233
|
+
}
|
|
234
|
+
/*
|
|
235
|
+
* One lookup, cut off after five seconds or when `outer` aborts. Whatever goes wrong is a reading that says
|
|
236
|
+
* the place is missing. Only a refusal the host chose its words for (`ChatUiRefusal`) carries them: any
|
|
237
|
+
* other error stays out of the shared record, which every client reads.
|
|
238
|
+
*/
|
|
239
|
+
async placeReading(info, query, outer) {
|
|
240
|
+
const timeout = new AbortController();
|
|
241
|
+
const timer = setTimeout(() => timeout.abort(), PLACE_TIMEOUT_MS);
|
|
242
|
+
timer.unref?.();
|
|
243
|
+
const signal = AbortSignal.any([timeout.signal, outer]);
|
|
244
|
+
try {
|
|
245
|
+
const reading = await Promise.race([
|
|
246
|
+
this.host.place(info, query, signal),
|
|
247
|
+
new Promise((_, reject) => signal.addEventListener('abort', () => reject(new UiQueryError('refused-query', 'timed-out', 'The place lookup timed out.')), {
|
|
248
|
+
once: true
|
|
249
|
+
}))
|
|
250
|
+
]);
|
|
251
|
+
const parsed = ChatUiPlaceReadingSchema.parse({ ...copyUiValue(reading, new UiBudget()), address: query.address });
|
|
252
|
+
return parsed.state === 'found' && (parsed.lat === undefined || parsed.lon === undefined)
|
|
253
|
+
? { state: 'missing', address: query.address, code: 'place-unreadable', reason: 'The host found no point for this address.' }
|
|
254
|
+
: parsed;
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return error instanceof UiQueryError
|
|
258
|
+
? { state: 'missing', address: query.address, ...this.failure(error) }
|
|
259
|
+
: { state: 'missing', address: query.address, code: 'lookup-failed', reason: 'The place could not be looked up.' };
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
clearTimeout(timer);
|
|
263
|
+
}
|
|
136
264
|
}
|
|
137
265
|
/* The writer's access for a reply, captured once; null where it was denied or is unavailable. */
|
|
138
266
|
access(session, item) {
|
|
@@ -349,6 +477,8 @@ export class ChatUiQueries {
|
|
|
349
477
|
}
|
|
350
478
|
}
|
|
351
479
|
forget(chatId) {
|
|
480
|
+
this.lives.get(chatId)?.abort();
|
|
481
|
+
this.lives.delete(chatId);
|
|
352
482
|
for (const [id, cached] of this.cache) {
|
|
353
483
|
if (cached.chatId === chatId) {
|
|
354
484
|
this.evict(id);
|
|
@@ -387,7 +517,7 @@ export class ChatUiQueries {
|
|
|
387
517
|
return reading;
|
|
388
518
|
}
|
|
389
519
|
failure(error) {
|
|
390
|
-
|
|
391
|
-
return error instanceof UiQueryError ? { reason, code: error.reason } : { reason };
|
|
520
|
+
// The reason is frozen into a record every client reads, so a message the host did not choose for that stays out of it.
|
|
521
|
+
return error instanceof UiQueryError ? { reason: error.message.slice(0, 240), code: error.reason } : { reason: 'This query could not be read.' };
|
|
392
522
|
}
|
|
393
523
|
}
|
package/dist/chat/wake-chat.d.ts
CHANGED
package/dist/chat/wake-chat.js
CHANGED
|
@@ -18,6 +18,7 @@ export function chatOpener(deps) {
|
|
|
18
18
|
return session
|
|
19
19
|
? {
|
|
20
20
|
items: () => session.thread.list(),
|
|
21
|
+
relocating: () => session.isRelocating,
|
|
21
22
|
persist: () => deps.chats.save(chatId),
|
|
22
23
|
// A turn before the reset would stop on the same limit and spend one of the resume's tries, so it waits for the resume turn.
|
|
23
24
|
wake: (wake) => session.info.resumeAt === undefined && session.wake(wake) !== null
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { wait, withTimeout } from './async.ts';
|
|
|
2
2
|
export { CodedError } from './coded-error.ts';
|
|
3
3
|
export { describeError, setErrorStacks, errorText } from './error-text.ts';
|
|
4
4
|
export { tempNameFor, writeAtomic, replaceSync, writeAtomicSync, isNotFound, fileExists, type WriteAtomicOptions } from './fs.ts';
|
|
5
|
-
export {
|
|
5
|
+
export { RecordDirectory, type RecordDirectoryOptions } from './record-directory.ts';
|
|
6
6
|
export { runProcess, type ProcessResult, type RunProcessOptions } from './run-process.ts';
|
|
7
7
|
export { Serializer, KeyedSerializer } from './serializer.ts';
|
|
8
8
|
export { cleanTitle, readLines } from './title-file.ts';
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { wait, withTimeout } from './async.js';
|
|
|
3
3
|
export { CodedError } from './coded-error.js';
|
|
4
4
|
export { describeError, setErrorStacks, errorText } from './error-text.js';
|
|
5
5
|
export { tempNameFor, writeAtomic, replaceSync, writeAtomicSync, isNotFound, fileExists } from './fs.js';
|
|
6
|
-
export {
|
|
6
|
+
export { RecordDirectory } from './record-directory.js';
|
|
7
7
|
export { runProcess } from './run-process.js';
|
|
8
8
|
export { Serializer, KeyedSerializer } from './serializer.js';
|
|
9
9
|
export { cleanTitle, readLines } from './title-file.js';
|
package/dist/lineage.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export declare class AgentLineageStore {
|
|
|
24
24
|
load(): Promise<void>;
|
|
25
25
|
put(record: Omit<Lineage, 'createdAt' | 'endedAt'>): Promise<void>;
|
|
26
26
|
descendants(nodeId: string): string[];
|
|
27
|
+
rehome(nodeId: string, projectId: string): Promise<boolean>;
|
|
27
28
|
projectOf(nodeId: string): string | null;
|
|
28
29
|
endedAt(nodeId: string): number | null;
|
|
29
30
|
markEnded(nodeIds: readonly string[]): Promise<void>;
|
package/dist/lineage.js
CHANGED
|
@@ -60,6 +60,14 @@ export class AgentLineageStore {
|
|
|
60
60
|
}
|
|
61
61
|
return found;
|
|
62
62
|
}
|
|
63
|
+
/* Moves a node's record to another project with its dates as they were; false when nothing was written down for it or a prune took it meanwhile. */
|
|
64
|
+
async rehome(nodeId, projectId) {
|
|
65
|
+
const entry = this.opened.get(nodeId);
|
|
66
|
+
if (entry === undefined) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return this.opened.write({ ...entry, projectId });
|
|
70
|
+
}
|
|
63
71
|
/* The project a node was made in, for work owed about it after the node that opened it is gone. */
|
|
64
72
|
projectOf(nodeId) {
|
|
65
73
|
return this.opened.get(nodeId)?.projectId ?? null;
|