@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 writes;
19
+ private readonly files;
18
20
  constructor(options: RecordDirectoryOptions<T>);
19
21
  load(): Promise<void>;
20
22
  get(id: string): T | undefined;
@@ -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
- // One write at a time per record, so an older one never lands after a newer one.
22
- writes = new KeyedSerializer();
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.writes.run(id, async () => {
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 true;
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.33.2",
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.33.2",
85
- "@adecore/intelligent-ui": "0.33.2",
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": {
@@ -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
  }
@@ -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
- // One write at a time per record, so an older one never lands after a newer one.
37
- private readonly writes = new KeyedSerializer();
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.writes.run(id, async () => {
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 true;
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> {