@adecore/agents 0.33.2 → 0.34.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.
|
@@ -13,7 +13,7 @@ export interface ChatSpawnOptions {
|
|
|
13
13
|
command: string[];
|
|
14
14
|
cwd: string;
|
|
15
15
|
env: Record<string, string>;
|
|
16
|
-
onExit(exitCode: number | null): void;
|
|
16
|
+
onExit(exitCode: number | null, signal?: string | null): void;
|
|
17
17
|
onError?(error: Error): void;
|
|
18
18
|
}
|
|
19
19
|
export type SpawnChatProcess = (options: ChatSpawnOptions) => ChatProcess;
|
|
@@ -48,7 +48,7 @@ export declare class LineSplitter {
|
|
|
48
48
|
export declare function readStreamLines(stream: ReadableStream<Uint8Array>, onLine: (line: string) => void, onOverflow: () => void, max?: number): Promise<void>;
|
|
49
49
|
export interface ChatChildOptions extends Omit<ChatSpawnOptions, 'onExit'> {
|
|
50
50
|
spawn?: SpawnChatProcess;
|
|
51
|
-
onExit(exitCode: number | null, stderr: string | null): void;
|
|
51
|
+
onExit(exitCode: number | null, stderr: string | null, signal?: string | null): void;
|
|
52
52
|
onError?(error: Error): void;
|
|
53
53
|
}
|
|
54
54
|
export declare class ChatChild {
|
|
@@ -26,12 +26,12 @@ export function spawnChatProcess(options) {
|
|
|
26
26
|
throw new Error(`Could not start ${command}: no such executable`);
|
|
27
27
|
}
|
|
28
28
|
child.stdin.on('error', (error) => options.onError?.(error));
|
|
29
|
-
child.on('exit', (exitCode) => {
|
|
29
|
+
child.on('exit', (exitCode, signal) => {
|
|
30
30
|
// The last lines may still be on their way through the pipe; they belong before the exit.
|
|
31
31
|
const report = () => {
|
|
32
32
|
clearTimeout(timer);
|
|
33
33
|
child.stdout.off('end', report);
|
|
34
|
-
setImmediate(() => options.onExit(exitCode));
|
|
34
|
+
setImmediate(() => options.onExit(exitCode, signal));
|
|
35
35
|
};
|
|
36
36
|
const timer = setTimeout(report, STDOUT_DRAIN_MS);
|
|
37
37
|
if (child.stdout.readableEnded) {
|
|
@@ -225,11 +225,11 @@ export class ChatChild {
|
|
|
225
225
|
cwd: options.cwd,
|
|
226
226
|
env: options.env,
|
|
227
227
|
...(options.onError ? { onError: options.onError } : {}),
|
|
228
|
-
onExit: (exitCode) => {
|
|
228
|
+
onExit: (exitCode, signal = null) => {
|
|
229
229
|
this.hasExited = true;
|
|
230
230
|
this.clearGrace();
|
|
231
231
|
settle();
|
|
232
|
-
void this.report(exitCode, options.onExit);
|
|
232
|
+
void this.report(exitCode, signal, options.onExit);
|
|
233
233
|
}
|
|
234
234
|
});
|
|
235
235
|
this.tail = this.process.stderr ? new StreamTail(this.process.stderr) : null;
|
|
@@ -274,9 +274,9 @@ export class ChatChild {
|
|
|
274
274
|
this.graceTimer = null;
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
|
-
async report(exitCode, onExit) {
|
|
277
|
+
async report(exitCode, signal, onExit) {
|
|
278
278
|
if (exitCode === null || exitCode === 0 || this.tail === null) {
|
|
279
|
-
onExit(exitCode, null);
|
|
279
|
+
onExit(exitCode, null, signal);
|
|
280
280
|
return;
|
|
281
281
|
}
|
|
282
282
|
let release = () => undefined;
|
|
@@ -288,6 +288,6 @@ export class ChatChild {
|
|
|
288
288
|
})
|
|
289
289
|
]);
|
|
290
290
|
clearTimeout(timer);
|
|
291
|
-
onExit(exitCode, this.tail.lines());
|
|
291
|
+
onExit(exitCode, this.tail.lines(), signal);
|
|
292
292
|
}
|
|
293
293
|
}
|
|
@@ -5,6 +5,7 @@ export interface RecordDirectoryOptions<T> {
|
|
|
5
5
|
schema: z.ZodType<T>;
|
|
6
6
|
idOf(record: T): string;
|
|
7
7
|
write?: (path: string, text: string) => Promise<void>;
|
|
8
|
+
remove?: (path: string) => Promise<void>;
|
|
8
9
|
keep?(record: T): T | null;
|
|
9
10
|
}
|
|
10
11
|
export declare class RecordDirectory<T> {
|
|
@@ -12,9 +13,10 @@ export declare class RecordDirectory<T> {
|
|
|
12
13
|
private readonly schema;
|
|
13
14
|
private readonly idOf;
|
|
14
15
|
private readonly writeFile;
|
|
16
|
+
private readonly removeFile;
|
|
15
17
|
private readonly keep;
|
|
16
18
|
private readonly records;
|
|
17
|
-
private readonly
|
|
19
|
+
private readonly files;
|
|
18
20
|
constructor(options: RecordDirectoryOptions<T>);
|
|
19
21
|
load(): Promise<void>;
|
|
20
22
|
get(id: string): T | undefined;
|
package/dist/record-directory.js
CHANGED
|
@@ -16,15 +16,20 @@ export class RecordDirectory {
|
|
|
16
16
|
schema;
|
|
17
17
|
idOf;
|
|
18
18
|
writeFile;
|
|
19
|
+
removeFile;
|
|
19
20
|
keep;
|
|
20
21
|
records = new Map();
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
/*
|
|
23
|
+
* One write or remove at a time per record, so an older write never lands after a newer one and
|
|
24
|
+
* no delete starts while another of the same file is pending, which Windows can fail with EFAULT.
|
|
25
|
+
*/
|
|
26
|
+
files = new KeyedSerializer();
|
|
23
27
|
constructor(options) {
|
|
24
28
|
this.dir = options.dir;
|
|
25
29
|
this.schema = options.schema;
|
|
26
30
|
this.idOf = options.idOf;
|
|
27
31
|
this.writeFile = options.write ?? writeAtomic;
|
|
32
|
+
this.removeFile = options.remove ?? removeFile;
|
|
28
33
|
this.keep = options.keep ?? null;
|
|
29
34
|
}
|
|
30
35
|
/* Reads what an earlier run of the host wrote down. Call before anything can ask. */
|
|
@@ -59,7 +64,7 @@ export class RecordDirectory {
|
|
|
59
64
|
}
|
|
60
65
|
const record = this.keep === null ? parsed.data : this.keep(parsed.data);
|
|
61
66
|
if (record === null) {
|
|
62
|
-
await removeFile(join(this.dir, name));
|
|
67
|
+
await this.removeFile(join(this.dir, name));
|
|
63
68
|
continue;
|
|
64
69
|
}
|
|
65
70
|
this.records.set(this.idOf(record), record);
|
|
@@ -77,28 +82,31 @@ export class RecordDirectory {
|
|
|
77
82
|
/*
|
|
78
83
|
* Holds the record in memory at once, so the next question in the same tick already sees it, and
|
|
79
84
|
* answers whether it is still the one this directory holds by the time the file is written. A
|
|
80
|
-
* record removed while its file was being written does not come back: the rename is undone
|
|
85
|
+
* record removed while its file was being written does not come back: the rename is undone, unless
|
|
86
|
+
* a newer record took the id meanwhile, whose own write is queued behind this one and replaces the file.
|
|
81
87
|
*/
|
|
82
88
|
write(record) {
|
|
83
89
|
const id = this.idOf(record);
|
|
84
90
|
this.records.set(id, record);
|
|
85
|
-
return this.
|
|
91
|
+
return this.files.run(id, async () => {
|
|
86
92
|
if (this.records.get(id) !== record) {
|
|
87
93
|
return false;
|
|
88
94
|
}
|
|
89
95
|
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
90
96
|
await this.writeFile(this.pathOf(id), JSON.stringify(record));
|
|
97
|
+
if (this.records.get(id) === record) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
91
100
|
if (!this.records.has(id)) {
|
|
92
|
-
await removeFile(this.pathOf(id));
|
|
93
|
-
return false;
|
|
101
|
+
await this.removeFile(this.pathOf(id));
|
|
94
102
|
}
|
|
95
|
-
return
|
|
103
|
+
return false;
|
|
96
104
|
});
|
|
97
105
|
}
|
|
98
|
-
/* Forgets a record here and now; its file follows. */
|
|
106
|
+
/* Forgets a record here and now; its file follows, after what is queued for it. */
|
|
99
107
|
remove(id) {
|
|
100
108
|
this.records.delete(id);
|
|
101
|
-
return removeFile(this.pathOf(id));
|
|
109
|
+
return this.files.run(id, () => this.removeFile(this.pathOf(id)));
|
|
102
110
|
}
|
|
103
111
|
async prune(drop) {
|
|
104
112
|
for (const listed of this.all()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"test": "bun --conditions=source test"
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@adecore/agent-contracts": "0.
|
|
85
|
-
"@adecore/intelligent-ui": "0.
|
|
84
|
+
"@adecore/agent-contracts": "0.34.0",
|
|
85
|
+
"@adecore/intelligent-ui": "0.34.0",
|
|
86
86
|
"zod": "^4.6.5"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
package/src/chat/chat-process.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface ChatSpawnOptions {
|
|
|
24
24
|
command: string[];
|
|
25
25
|
cwd: string;
|
|
26
26
|
env: Record<string, string>;
|
|
27
|
-
onExit(exitCode: number | null): void;
|
|
27
|
+
onExit(exitCode: number | null, signal?: string | null): void;
|
|
28
28
|
onError?(error: Error): void;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -56,12 +56,12 @@ export function spawnChatProcess(options: ChatSpawnOptions): ChatProcess {
|
|
|
56
56
|
throw new Error(`Could not start ${command}: no such executable`);
|
|
57
57
|
}
|
|
58
58
|
child.stdin.on('error', (error) => options.onError?.(error));
|
|
59
|
-
child.on('exit', (exitCode) => {
|
|
59
|
+
child.on('exit', (exitCode, signal) => {
|
|
60
60
|
// The last lines may still be on their way through the pipe; they belong before the exit.
|
|
61
61
|
const report = (): void => {
|
|
62
62
|
clearTimeout(timer);
|
|
63
63
|
child.stdout.off('end', report);
|
|
64
|
-
setImmediate(() => options.onExit(exitCode));
|
|
64
|
+
setImmediate(() => options.onExit(exitCode, signal));
|
|
65
65
|
};
|
|
66
66
|
const timer = setTimeout(report, STDOUT_DRAIN_MS);
|
|
67
67
|
if (child.stdout.readableEnded) {
|
|
@@ -259,7 +259,7 @@ export async function readStreamLines(
|
|
|
259
259
|
export interface ChatChildOptions extends Omit<ChatSpawnOptions, 'onExit'> {
|
|
260
260
|
spawn?: SpawnChatProcess;
|
|
261
261
|
// `stderr` is the tail of what the CLI wrote there, for an exit with an error code only.
|
|
262
|
-
onExit(exitCode: number | null, stderr: string | null): void;
|
|
262
|
+
onExit(exitCode: number | null, stderr: string | null, signal?: string | null): void;
|
|
263
263
|
onError?(error: Error): void;
|
|
264
264
|
}
|
|
265
265
|
|
|
@@ -287,11 +287,11 @@ export class ChatChild {
|
|
|
287
287
|
cwd: options.cwd,
|
|
288
288
|
env: options.env,
|
|
289
289
|
...(options.onError ? { onError: options.onError } : {}),
|
|
290
|
-
onExit: (exitCode) => {
|
|
290
|
+
onExit: (exitCode, signal = null) => {
|
|
291
291
|
this.hasExited = true;
|
|
292
292
|
this.clearGrace();
|
|
293
293
|
settle();
|
|
294
|
-
void this.report(exitCode, options.onExit);
|
|
294
|
+
void this.report(exitCode, signal, options.onExit);
|
|
295
295
|
}
|
|
296
296
|
});
|
|
297
297
|
this.tail = this.process.stderr ? new StreamTail(this.process.stderr) : null;
|
|
@@ -341,9 +341,9 @@ export class ChatChild {
|
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
private async report(exitCode: number | null, onExit: ChatChildOptions['onExit']): Promise<void> {
|
|
344
|
+
private async report(exitCode: number | null, signal: string | null, onExit: ChatChildOptions['onExit']): Promise<void> {
|
|
345
345
|
if (exitCode === null || exitCode === 0 || this.tail === null) {
|
|
346
|
-
onExit(exitCode, null);
|
|
346
|
+
onExit(exitCode, null, signal);
|
|
347
347
|
return;
|
|
348
348
|
}
|
|
349
349
|
let release: () => void = () => undefined;
|
|
@@ -355,6 +355,6 @@ export class ChatChild {
|
|
|
355
355
|
})
|
|
356
356
|
]);
|
|
357
357
|
clearTimeout(timer);
|
|
358
|
-
onExit(exitCode, this.tail.lines());
|
|
358
|
+
onExit(exitCode, this.tail.lines(), signal);
|
|
359
359
|
}
|
|
360
360
|
}
|
package/src/record-directory.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface RecordDirectoryOptions<T> {
|
|
|
14
14
|
schema: z.ZodType<T>;
|
|
15
15
|
idOf(record: T): string;
|
|
16
16
|
write?: (path: string, text: string) => Promise<void>;
|
|
17
|
+
// Stands in for `removeFile` whole, so it must take a missing file as removed: a write and a remove delete twice.
|
|
18
|
+
remove?: (path: string) => Promise<void>;
|
|
17
19
|
/*
|
|
18
20
|
* What a record read from disk is worth keeping as, for a store that drops what went stale while
|
|
19
21
|
* the host was down. Null takes the file with it.
|
|
@@ -31,16 +33,21 @@ export class RecordDirectory<T> {
|
|
|
31
33
|
private readonly schema: z.ZodType<T>;
|
|
32
34
|
private readonly idOf: (record: T) => string;
|
|
33
35
|
private readonly writeFile: (path: string, text: string) => Promise<void>;
|
|
36
|
+
private readonly removeFile: (path: string) => Promise<void>;
|
|
34
37
|
private readonly keep: ((record: T) => T | null) | null;
|
|
35
38
|
private readonly records = new Map<string, T>();
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
/*
|
|
40
|
+
* One write or remove at a time per record, so an older write never lands after a newer one and
|
|
41
|
+
* no delete starts while another of the same file is pending, which Windows can fail with EFAULT.
|
|
42
|
+
*/
|
|
43
|
+
private readonly files = new KeyedSerializer();
|
|
38
44
|
|
|
39
45
|
constructor(options: RecordDirectoryOptions<T>) {
|
|
40
46
|
this.dir = options.dir;
|
|
41
47
|
this.schema = options.schema;
|
|
42
48
|
this.idOf = options.idOf;
|
|
43
49
|
this.writeFile = options.write ?? writeAtomic;
|
|
50
|
+
this.removeFile = options.remove ?? removeFile;
|
|
44
51
|
this.keep = options.keep ?? null;
|
|
45
52
|
}
|
|
46
53
|
|
|
@@ -74,7 +81,7 @@ export class RecordDirectory<T> {
|
|
|
74
81
|
}
|
|
75
82
|
const record = this.keep === null ? parsed.data : this.keep(parsed.data);
|
|
76
83
|
if (record === null) {
|
|
77
|
-
await removeFile(join(this.dir, name));
|
|
84
|
+
await this.removeFile(join(this.dir, name));
|
|
78
85
|
continue;
|
|
79
86
|
}
|
|
80
87
|
this.records.set(this.idOf(record), record);
|
|
@@ -96,29 +103,32 @@ export class RecordDirectory<T> {
|
|
|
96
103
|
/*
|
|
97
104
|
* Holds the record in memory at once, so the next question in the same tick already sees it, and
|
|
98
105
|
* answers whether it is still the one this directory holds by the time the file is written. A
|
|
99
|
-
* record removed while its file was being written does not come back: the rename is undone
|
|
106
|
+
* record removed while its file was being written does not come back: the rename is undone, unless
|
|
107
|
+
* a newer record took the id meanwhile, whose own write is queued behind this one and replaces the file.
|
|
100
108
|
*/
|
|
101
109
|
write(record: T): Promise<boolean> {
|
|
102
110
|
const id = this.idOf(record);
|
|
103
111
|
this.records.set(id, record);
|
|
104
|
-
return this.
|
|
112
|
+
return this.files.run(id, async () => {
|
|
105
113
|
if (this.records.get(id) !== record) {
|
|
106
114
|
return false;
|
|
107
115
|
}
|
|
108
116
|
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
109
117
|
await this.writeFile(this.pathOf(id), JSON.stringify(record));
|
|
118
|
+
if (this.records.get(id) === record) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
110
121
|
if (!this.records.has(id)) {
|
|
111
|
-
await removeFile(this.pathOf(id));
|
|
112
|
-
return false;
|
|
122
|
+
await this.removeFile(this.pathOf(id));
|
|
113
123
|
}
|
|
114
|
-
return
|
|
124
|
+
return false;
|
|
115
125
|
});
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
/* Forgets a record here and now; its file follows. */
|
|
128
|
+
/* Forgets a record here and now; its file follows, after what is queued for it. */
|
|
119
129
|
remove(id: string): Promise<void> {
|
|
120
130
|
this.records.delete(id);
|
|
121
|
-
return removeFile(this.pathOf(id));
|
|
131
|
+
return this.files.run(id, () => this.removeFile(this.pathOf(id)));
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
async prune(drop: (record: T) => boolean): Promise<void> {
|