@adecore/agents 0.39.0 → 0.40.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.
Files changed (168) hide show
  1. package/dist/async.d.ts +5 -0
  2. package/dist/async.js +13 -5
  3. package/dist/chat/acp-protocol.js +3 -5
  4. package/dist/chat/chat-core.js +0 -2
  5. package/dist/chat/chat-process.d.ts +1 -0
  6. package/dist/chat/chat-process.js +1 -1
  7. package/dist/chat/chat-session.js +6 -8
  8. package/dist/chat/claude-protocol.js +1 -6
  9. package/dist/chat/claude-transcript.js +1 -3
  10. package/dist/chat/codex-protocol.js +1 -6
  11. package/dist/chat/projector.js +1 -3
  12. package/dist/chat/request-summary.js +1 -3
  13. package/dist/coded-error.js +1 -5
  14. package/dist/context/argv.js +1 -2
  15. package/dist/context/verb.js +1 -3
  16. package/dist/error-text.d.ts +1 -0
  17. package/dist/error-text.js +4 -0
  18. package/dist/fields.d.ts +3 -0
  19. package/dist/fields.js +12 -0
  20. package/dist/fs.d.ts +1 -0
  21. package/dist/fs.js +8 -4
  22. package/dist/lineage.js +11 -5
  23. package/dist/messages/notice-store.js +3 -4
  24. package/dist/providers/accounts/acp-account.js +3 -20
  25. package/dist/providers/accounts/service.d.ts +1 -2
  26. package/dist/providers/accounts/service.js +5 -9
  27. package/dist/providers/accounts/status.js +1 -3
  28. package/dist/providers/acp-agent-state.js +1 -3
  29. package/dist/providers/claude.js +0 -1
  30. package/dist/providers/codex.js +0 -1
  31. package/dist/providers/detect.js +1 -1
  32. package/dist/providers/shell-commands.js +1 -2
  33. package/dist/run-process.js +1 -3
  34. package/dist/skills.d.ts +1 -1
  35. package/dist/skills.js +5 -8
  36. package/dist/tasks/background-limit.js +3 -2
  37. package/dist/tasks/task-coordinator.js +11 -17
  38. package/dist/testing/watch.js +0 -1
  39. package/dist/usage/limits/monitor.js +1 -2
  40. package/dist/usage/readers/claude.js +3 -10
  41. package/dist/usage/readers/codex.js +2 -9
  42. package/dist/usage/readers/grok.js +3 -10
  43. package/dist/usage/readers/host-log.js +3 -10
  44. package/dist/usage/record.d.ts +2 -0
  45. package/dist/usage/record.js +13 -0
  46. package/package.json +6 -6
  47. package/src/async.ts +23 -5
  48. package/src/chat/acp-backend.ts +121 -0
  49. package/src/chat/acp-protocol.ts +172 -6
  50. package/src/chat/attachment-store.ts +21 -0
  51. package/src/chat/backend.ts +0 -2
  52. package/src/chat/bookmark-store.ts +23 -0
  53. package/src/chat/chat-core.ts +208 -7
  54. package/src/chat/chat-log.ts +19 -0
  55. package/src/chat/chat-process.ts +52 -1
  56. package/src/chat/chat-session.ts +401 -10
  57. package/src/chat/chat-store.ts +50 -0
  58. package/src/chat/chat-title.ts +9 -0
  59. package/src/chat/claude-backend.ts +50 -0
  60. package/src/chat/claude-protocol.ts +155 -8
  61. package/src/chat/claude-title.ts +10 -0
  62. package/src/chat/claude-transcript.ts +47 -4
  63. package/src/chat/codex-backend.ts +131 -0
  64. package/src/chat/codex-protocol.ts +180 -8
  65. package/src/chat/codex-thread.ts +23 -0
  66. package/src/chat/composer-preferences.ts +6 -0
  67. package/src/chat/context-breakdown.ts +11 -0
  68. package/src/chat/delta-coalescer.ts +8 -0
  69. package/src/chat/external-sessions.ts +28 -0
  70. package/src/chat/generated-image.ts +25 -0
  71. package/src/chat/index.ts +5 -0
  72. package/src/chat/input.ts +13 -0
  73. package/src/chat/json-rpc-transport.ts +28 -0
  74. package/src/chat/limit-resume.ts +13 -0
  75. package/src/chat/projector.ts +160 -4
  76. package/src/chat/readable-text.ts +3 -0
  77. package/src/chat/request-summary.ts +18 -4
  78. package/src/chat/subagent-projection.ts +3 -0
  79. package/src/chat/subagent-reader.ts +76 -0
  80. package/src/chat/subagent-settlement.ts +24 -0
  81. package/src/chat/thread-search.ts +13 -0
  82. package/src/chat/thread.ts +31 -0
  83. package/src/chat/ui-queries.ts +122 -0
  84. package/src/chat/ui-replies.ts +34 -0
  85. package/src/chat/visual-store.ts +49 -0
  86. package/src/chat/wake-chat.ts +3 -0
  87. package/src/client-sinks.ts +2 -0
  88. package/src/coded-error.ts +1 -5
  89. package/src/context/argv.ts +18 -2
  90. package/src/context/index.ts +1 -0
  91. package/src/context/refusal.ts +3 -0
  92. package/src/context/verb.ts +32 -4
  93. package/src/coordination/index.ts +10 -0
  94. package/src/error-text.ts +7 -0
  95. package/src/fields.ts +14 -0
  96. package/src/fs.ts +20 -4
  97. package/src/host/agent-host.ts +20 -0
  98. package/src/host/environment.ts +6 -0
  99. package/src/host/handlers.ts +18 -0
  100. package/src/host/memory-port.ts +4 -0
  101. package/src/host/wiring.ts +10 -0
  102. package/src/index.ts +1 -0
  103. package/src/lineage.ts +24 -5
  104. package/src/messages/deliver-message.ts +6 -0
  105. package/src/messages/deliver-notice.ts +13 -0
  106. package/src/messages/notice-store.ts +16 -4
  107. package/src/modes.ts +1 -0
  108. package/src/outbox/outbox-worker.ts +31 -0
  109. package/src/outbox/outbox.ts +5 -0
  110. package/src/providers/accounts/accounts.ts +36 -0
  111. package/src/providers/accounts/acp-account.ts +22 -27
  112. package/src/providers/accounts/launch.ts +4 -0
  113. package/src/providers/accounts/service.ts +110 -12
  114. package/src/providers/accounts/shadow-home.ts +10 -0
  115. package/src/providers/accounts/status.ts +13 -4
  116. package/src/providers/accounts/store.ts +25 -0
  117. package/src/providers/accounts/variables.ts +11 -1
  118. package/src/providers/acp-agent-state.ts +24 -3
  119. package/src/providers/acp-provider.ts +5 -0
  120. package/src/providers/acp-usage-log.ts +2 -0
  121. package/src/providers/catalog.ts +17 -0
  122. package/src/providers/claude.ts +10 -1
  123. package/src/providers/codex-rules.ts +3 -0
  124. package/src/providers/codex.ts +2 -1
  125. package/src/providers/detect.ts +6 -1
  126. package/src/providers/provider.ts +1 -1
  127. package/src/providers/registry.ts +18 -2
  128. package/src/providers/shell-commands.ts +37 -2
  129. package/src/record-directory.ts +19 -0
  130. package/src/run-process.ts +24 -3
  131. package/src/serializer.ts +3 -0
  132. package/src/skills.ts +73 -10
  133. package/src/tasks/background-limit.ts +9 -2
  134. package/src/tasks/end-children.ts +9 -0
  135. package/src/tasks/give-task.ts +8 -0
  136. package/src/tasks/task-coordinator.ts +81 -17
  137. package/src/tasks/task-store.ts +28 -0
  138. package/src/tasks/waiting-child.ts +21 -0
  139. package/src/tasks/wake-parent.ts +26 -0
  140. package/src/tasks/wiring.ts +22 -0
  141. package/src/testing/accounts.ts +2 -0
  142. package/src/testing/fake-acp-capture.ts +16 -0
  143. package/src/testing/fake-acp.ts +66 -0
  144. package/src/testing/fake-claude.ts +177 -0
  145. package/src/testing/fake-cli.ts +29 -0
  146. package/src/testing/fake-codex.ts +122 -0
  147. package/src/testing/manual-clock.ts +2 -0
  148. package/src/testing/watch.ts +7 -1
  149. package/src/title-file.ts +12 -0
  150. package/src/usage/accounts.ts +6 -0
  151. package/src/usage/aggregate.ts +19 -0
  152. package/src/usage/exchange.ts +12 -0
  153. package/src/usage/index-file.ts +27 -0
  154. package/src/usage/limits/monitor.ts +31 -3
  155. package/src/usage/limits/normalize.ts +30 -0
  156. package/src/usage/limits/probe.ts +17 -0
  157. package/src/usage/pricing.ts +35 -0
  158. package/src/usage/projects.ts +8 -0
  159. package/src/usage/readers/claude.ts +12 -9
  160. package/src/usage/readers/codex.ts +40 -8
  161. package/src/usage/readers/grok.ts +17 -9
  162. package/src/usage/readers/host-log.ts +9 -9
  163. package/src/usage/record.ts +22 -0
  164. package/src/usage/roots.ts +6 -0
  165. package/src/usage/scanner.ts +61 -0
  166. package/src/usage/usage-service.ts +14 -0
  167. package/src/watch-seam.ts +8 -0
  168. package/src/windows-command.ts +12 -0
@@ -145,6 +145,7 @@ function usageWithFlags(usage: string, detail: readonly string[]): string {
145
145
  .map((line) => line.split('\t'))
146
146
  .filter(([, syntax]) => !flagSpelled(usage, syntax!.split(' ')[0]!))
147
147
  .map(([, syntax, need]) => (need === 'required' ? syntax! : `[${syntax}]`));
148
+
148
149
  return [usage, ...missing].filter((part) => part !== '').join(' ');
149
150
  }
150
151
 
@@ -182,9 +183,6 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
182
183
  */
183
184
  const dryRunVerbs: string[] = [];
184
185
 
185
- /* What a refusal over arguments points at; for an action `name` is both words, which is what `help` takes. */
186
- const usageLinesOf = (name: string, usage: string): string[] => [`usage\t${name}\t${usage}`, `detail\t${cli} help ${name}`];
187
-
188
186
  /* One set of arguments parsed and run. `name` is what a refusal calls it, so `view new` refuses under both its words. */
189
187
  const runArgs = async <Positionals extends z.ZodType, Flags extends z.ZodObject>(
190
188
  name: string,
@@ -193,11 +191,13 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
193
191
  argv: readonly string[],
194
192
  call: Call
195
193
  ): Promise<string[]> => {
196
- const usageLines = usageLinesOf(name, usage);
194
+ const usageLines = [`usage\t${name}\t${usage}`, `detail\t${cli} help ${name}`];
197
195
  const { values, switches } = flagsOf(spec);
198
196
  const parsed = parseArgv(argv, values, switches);
197
+
199
198
  if (!parsed.ok) {
200
199
  const dryRunAsked = argv.some((word) => word === `--${DRY_RUN_FLAG}` || word.startsWith(`--${DRY_RUN_FLAG}=`));
200
+
201
201
  if (parsed.code === 'unknown-flag' && spec.dryRun !== true && dryRunAsked) {
202
202
  throw new VerbRefusal(
203
203
  'no-dry-run',
@@ -205,20 +205,27 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
205
205
  dryRunVerbs.map((verb) => `verb\t${verb}\ttakes --${DRY_RUN_FLAG}`)
206
206
  );
207
207
  }
208
+
208
209
  throw new VerbRefusal(parsed.code, parsed.message, usageLines);
209
210
  }
211
+
210
212
  const positionals = spec.positionals.safeParse(parsed.positionals);
213
+
211
214
  if (!positionals.success) {
212
215
  throw new VerbRefusal('bad-arguments', issueMessage(positionals.error, 'argument'), usageLines);
213
216
  }
217
+
214
218
  const { [REVISION_FLAG]: revision, ...given } = parsed.flags;
215
219
  const flags = spec.flags.safeParse(given);
220
+
216
221
  if (!flags.success) {
217
222
  throw new VerbRefusal('bad-arguments', issueMessage(flags.error, 'flag'), usageLines);
218
223
  }
224
+
219
225
  if (revision !== undefined && !/^\d+$/.test(revision)) {
220
226
  throw new VerbRefusal('bad-arguments', `--${REVISION_FLAG} takes the whole number a list printed as revision`, usageLines);
221
227
  }
228
+
222
229
  return spec.run(
223
230
  { positionals: positionals.data, flags: flags.data, switches: parsed.switches, dryRun: parsed.switches.has(DRY_RUN_FLAG) },
224
231
  revision === undefined ? call : { ...call, expectedRevision: Number(revision) }
@@ -231,10 +238,13 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
231
238
  spec: VerbSpec<Positionals, Flags, Call>
232
239
  ): Omit<Verb<Call>, 'served'> => {
233
240
  const { values, switches } = flagsOf(spec);
241
+
234
242
  if (spec.dryRun === true) {
235
243
  dryRunVerbs.push(name);
236
244
  }
245
+
237
246
  const usage = usageWithFlags(spec.usage, spec.detail);
247
+
238
248
  return {
239
249
  name,
240
250
  usage,
@@ -258,6 +268,7 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
258
268
 
259
269
  const defineNoun = (spec: NounSpec<Call>): Noun<Call> => {
260
270
  const words = spec.actions.map((action) => action.word);
271
+
261
272
  return {
262
273
  served: 'noun',
263
274
  name: spec.name,
@@ -268,6 +279,7 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
268
279
  async run(argv, call) {
269
280
  const [word, ...rest] = argv;
270
281
  const action = spec.actions.find((candidate) => candidate.word === word);
282
+
271
283
  if (!action) {
272
284
  throw new VerbRefusal(
273
285
  'unknown-action',
@@ -275,6 +287,7 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
275
287
  [...spec.actions.map((candidate) => `usage\t${candidate.name}\t${candidate.usage}`), `detail\t${cli} help ${spec.name}`]
276
288
  );
277
289
  }
290
+
278
291
  return action.run(rest, call);
279
292
  }
280
293
  };
@@ -282,6 +295,7 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
282
295
 
283
296
  const defineHelp = (spec: HelpSpec<Call>): Verb<Call> => {
284
297
  const detailLines = (entry: VerbHelp): string[] => [`usage\t${entry.name}\t${entry.usage}`, `about\t${entry.summary}`, ...entry.detail, spec.refusal];
298
+
285
299
  const nounLines = (noun: Noun<Call>): string[] => [
286
300
  `usage\t${noun.name}\t${noun.usage}`,
287
301
  `about\t${noun.summary}`,
@@ -290,9 +304,11 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
290
304
  `detail\t${cli} help ${noun.name} <action>\tone action in full`,
291
305
  spec.refusal
292
306
  ];
307
+
293
308
  const topics = spec.topics ?? [];
294
309
  const topicLines = topics.map((topic) => `topic\t${topic.name}\t${topic.summary}`);
295
310
  const topicPointer = topics.length > 0 ? [`detail\t${cli} help <topic>\tone topic in full`] : [];
311
+
296
312
  return defineVerb({
297
313
  name: 'help',
298
314
  usage: '[noun] [action]',
@@ -311,6 +327,7 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
311
327
  flags: z.object({}),
312
328
  run: async ({ positionals: [name, actionWord] }) => {
313
329
  const entries = spec.entries();
330
+
314
331
  if (name === undefined) {
315
332
  return [
316
333
  ...summaryLines(entries),
@@ -323,16 +340,20 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
323
340
  spec.refusal
324
341
  ];
325
342
  }
343
+
326
344
  const entry = entries.find((candidate) => candidate.name === name);
327
345
  const topic = entry ? undefined : topics.find((candidate) => candidate.name === name);
346
+
328
347
  if (topic) {
329
348
  if (actionWord !== undefined) {
330
349
  throw new VerbRefusal('bad-arguments', `${name} is a topic and has no actions; ${cli} help ${name} prints it`, [
331
350
  `detail\t${cli} help ${name}\tthe topic in full`
332
351
  ]);
333
352
  }
353
+
334
354
  return [`about\t${topic.summary}`, ...topic.body];
335
355
  }
356
+
336
357
  if (!entry) {
337
358
  throw new VerbRefusal('unknown-verb', topics.length > 0 ? `${name} is not a verb, a noun or a topic` : `${name} is not a verb or a noun`, [
338
359
  `detail\t${cli} help\tevery verb and noun, with what each takes`,
@@ -340,21 +361,27 @@ export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: st
340
361
  ...topicLines
341
362
  ]);
342
363
  }
364
+
343
365
  if (entry.served !== 'noun') {
344
366
  if (actionWord !== undefined) {
345
367
  throw new VerbRefusal('bad-arguments', `${name} is a verb and has no actions; ${cli} help ${name} details it`, [
346
368
  `detail\t${cli} help ${name}\tone verb in full`
347
369
  ]);
348
370
  }
371
+
349
372
  return detailLines(entry);
350
373
  }
374
+
351
375
  if (actionWord === undefined) {
352
376
  return nounLines(entry);
353
377
  }
378
+
354
379
  const action = entry.actions.find((candidate) => candidate.word === actionWord);
380
+
355
381
  if (!action) {
356
382
  throw new VerbRefusal('unknown-action', `${actionWord} is not an action of ${name}`, actionLines(entry));
357
383
  }
384
+
358
385
  return detailLines(action);
359
386
  }
360
387
  });
@@ -373,6 +400,7 @@ export function lengthOf(input: unknown): number {
373
400
  if (typeof input === 'string' || Array.isArray(input)) {
374
401
  return input.length;
375
402
  }
403
+
376
404
  return 0;
377
405
  }
378
406
 
@@ -1,5 +1,6 @@
1
1
  // Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
2
2
  export { type OutboxEntryOf, OutboxStore, type OutboxWorkShape, type OutboxEntryFields, type OutboxStoreOptions } from '../outbox/outbox.ts';
3
+
3
4
  export {
4
5
  RETRY_DELAYS_MS,
5
6
  type OutboxClock,
@@ -9,7 +10,9 @@ export {
9
10
  OutboxWorker,
10
11
  type OutboxWorkerOptions
11
12
  } from '../outbox/outbox-worker.ts';
13
+
12
14
  export { restartBackgroundLimits, type RestartedOutbox } from '../tasks/background-limit.ts';
15
+
13
16
  export {
14
17
  EndChildrenWorkSchema,
15
18
  endChildren,
@@ -19,8 +22,10 @@ export {
19
22
  type EndChildrenDeps,
20
23
  type EndChildren
21
24
  } from '../tasks/end-children.ts';
25
+
22
26
  export { type TaskCoordinatorDeps, TaskCoordinator } from '../tasks/task-coordinator.ts';
23
27
  export { TaskStore, type TaskListener, type TaskEvent, type TaskSink } from '../tasks/task-store.ts';
28
+
24
29
  export {
25
30
  BackgroundLimitWorkSchema,
26
31
  WakeParentWorkSchema,
@@ -35,6 +40,7 @@ export {
35
40
  type AnyOutboxEntry,
36
41
  type TaskOutbox
37
42
  } from '../tasks/task-work.ts';
43
+
38
44
  export {
39
45
  type ChatRequests,
40
46
  chatRequests,
@@ -44,8 +50,10 @@ export {
44
50
  type WaitingObserverDeps,
45
51
  type WaitingWords
46
52
  } from '../tasks/waiting-child.ts';
53
+
47
54
  export { RESULT_PREVIEW_BYTES, wakePrompt } from '../tasks/wake-parent.ts';
48
55
  export { type TaskVerbs, type TaskWords, wireTasks, type TaskChats, type TaskRecord, type TaskWiringDeps, type TaskWiring } from '../tasks/wiring.ts';
56
+
49
57
  export {
50
58
  DeliverMessageWorkSchema,
51
59
  deliverMessageHandler,
@@ -53,6 +61,7 @@ export {
53
61
  type DeliverMessageEntry,
54
62
  type DeliverMessageDeps
55
63
  } from '../messages/deliver-message.ts';
64
+
56
65
  export {
57
66
  type MessageWords,
58
67
  type ChatNoticeTargets,
@@ -62,6 +71,7 @@ export {
62
71
  showNotices,
63
72
  type NoticeChat
64
73
  } from '../messages/deliver-notice.ts';
74
+
65
75
  export { NoticeNotes, unshownNotes } from '../messages/notice-notes.ts';
66
76
  export { MAX_NOTICES, NOTICE_MAX_AGE_MS, type Notice, NoticeStore } from '../messages/notice-store.ts';
67
77
  export { AgentLineageStore } from '../lineage.ts';
package/src/error-text.ts CHANGED
@@ -9,12 +9,19 @@ export function describeError(error: unknown, debug: boolean): string {
9
9
  if (!(error instanceof Error)) {
10
10
  return String(error);
11
11
  }
12
+
12
13
  if (debug) {
13
14
  return error.stack ?? `${error.name}: ${error.message}`;
14
15
  }
16
+
15
17
  return error.message === '' ? error.name : error.message;
16
18
  }
17
19
 
20
+ /* What was thrown, as an Error a rejection can carry. */
21
+ export function asError(thrown: unknown): Error {
22
+ return thrown instanceof Error ? thrown : new Error(String(thrown));
23
+ }
24
+
18
25
  // The host decides once whether a stack is worth showing; the package reads no variable of its own.
19
26
  let showStacks = false;
20
27
 
package/src/fields.ts ADDED
@@ -0,0 +1,14 @@
1
+ /* A field of a frame or a file that no schema promises a string in. */
2
+ export function str(value: unknown): string | null {
3
+ return typeof value === 'string' ? value : null;
4
+ }
5
+
6
+ /* A count of a frame, zero for anything that is not a finite number. */
7
+ export function num(value: unknown): number {
8
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
9
+ }
10
+
11
+ /* The same, with an empty string read as nothing said. */
12
+ export function filledStr(value: unknown): string | null {
13
+ return typeof value === 'string' && value !== '' ? value : null;
14
+ }
package/src/fs.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { renameSync, writeFileSync } from 'node:fs';
2
2
  import { open, rename, rm, stat, writeFile } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
+ import { wait } from './async.ts';
4
5
 
5
6
  // Windows can refuse a rename while a watcher or an indexer holds the target; a few short retries cover it.
6
7
  const RENAME_RETRIES = 5;
@@ -11,8 +12,12 @@ function sleepSync(ms: number): void {
11
12
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
12
13
  }
13
14
 
15
+ export function hasErrorCode(e: unknown, ...codes: string[]): boolean {
16
+ return typeof e === 'object' && e !== null && 'code' in e && codes.includes(e.code as string);
17
+ }
18
+
14
19
  function isTransient(e: unknown): boolean {
15
- return typeof e === 'object' && e !== null && 'code' in e && (e.code === 'EPERM' || e.code === 'EBUSY' || e.code === 'EACCES');
20
+ return hasErrorCode(e, 'EPERM', 'EBUSY', 'EACCES');
16
21
  }
17
22
 
18
23
  // Two writes of the same file in flight at once (a fire-and-forget save next to an awaited one) must not share a temp name.
@@ -30,6 +35,7 @@ export interface WriteAtomicOptions {
30
35
 
31
36
  async function writeFlushed(path: string, content: string | Uint8Array, mode: number): Promise<void> {
32
37
  const handle = await open(path, 'w', mode);
38
+
33
39
  try {
34
40
  await handle.writeFile(content);
35
41
  await handle.sync();
@@ -43,7 +49,9 @@ async function syncDirectory(path: string): Promise<void> {
43
49
  if (process.platform === 'win32') {
44
50
  return;
45
51
  }
52
+
46
53
  const handle = await open(path, 'r');
54
+
47
55
  try {
48
56
  await handle.sync();
49
57
  } finally {
@@ -58,22 +66,27 @@ async function syncDirectory(path: string): Promise<void> {
58
66
  */
59
67
  export async function writeAtomic(target: string, content: string | Uint8Array, mode = 0o600, options: WriteAtomicOptions = {}): Promise<void> {
60
68
  const temp = tempNameFor(target);
69
+
61
70
  if (options.durable) {
62
71
  await writeFlushed(temp, content, mode);
63
72
  } else {
64
73
  await writeFile(temp, content, { mode });
65
74
  }
75
+
66
76
  for (let attempt = 0; ; attempt++) {
67
77
  try {
68
78
  await rename(temp, target);
79
+
69
80
  break;
70
81
  } catch (e) {
71
82
  if (!isTransient(e) || attempt >= RENAME_RETRIES) {
72
83
  throw e;
73
84
  }
74
- await new Promise((resolve) => setTimeout(resolve, RENAME_RETRY_MS));
85
+
86
+ await wait(RENAME_RETRY_MS);
75
87
  }
76
88
  }
89
+
77
90
  if (options.durable) {
78
91
  await syncDirectory(dirname(target));
79
92
  }
@@ -88,11 +101,13 @@ export function replaceSync(temp: string, target: string): void {
88
101
  for (let attempt = 0; ; attempt++) {
89
102
  try {
90
103
  renameSync(temp, target);
104
+
91
105
  return;
92
106
  } catch (e) {
93
107
  if (!isTransient(e) || attempt >= RENAME_RETRIES) {
94
108
  throw e;
95
109
  }
110
+
96
111
  sleepSync(RENAME_RETRY_MS);
97
112
  }
98
113
  }
@@ -105,7 +120,7 @@ export function writeAtomicSync(target: string, content: string | Uint8Array, mo
105
120
  }
106
121
 
107
122
  export function isNotFound(e: unknown): boolean {
108
- return typeof e === 'object' && e !== null && 'code' in e && e.code === 'ENOENT';
123
+ return hasErrorCode(e, 'ENOENT');
109
124
  }
110
125
 
111
126
  // Bun on Windows fails a forced rm with EFAULT while another delete of the same file is pending; it goes either way.
@@ -113,7 +128,7 @@ export async function removeFile(path: string, remove: (path: string, options: {
113
128
  try {
114
129
  await remove(path, { force: true });
115
130
  } catch (e) {
116
- if (!(typeof e === 'object' && e !== null && 'code' in e && e.code === 'EFAULT')) {
131
+ if (!hasErrorCode(e, 'EFAULT')) {
117
132
  throw e;
118
133
  }
119
134
  }
@@ -122,6 +137,7 @@ export async function removeFile(path: string, remove: (path: string, options: {
122
137
  export async function fileExists(path: string): Promise<boolean> {
123
138
  try {
124
139
  await stat(path);
140
+
125
141
  return true;
126
142
  } catch {
127
143
  return false;
@@ -53,34 +53,42 @@ export class AgentHost<Core extends ChatCore = ChatCore> {
53
53
  static async open<Core extends ChatCore = ChatCore>(options: AgentHostOptions<Core>): Promise<AgentHost<Core>> {
54
54
  const host = new AgentHost(options);
55
55
  await host.accounts.load();
56
+
56
57
  if (host.background) {
57
58
  host.wiring.start();
58
59
  }
60
+
59
61
  return host;
60
62
  }
61
63
 
62
64
  /* Serves one client over `port` until the returned function or `close` lets go of it. */
63
65
  connect(port: FramePort): () => void {
64
66
  const clientId = `port-${this.nextClient++}`;
67
+
65
68
  const releases = [
66
69
  port.onFrame((frame) => void this.receive(port, clientId, frame)),
67
70
  this.wiring.connect(clientId, (event) => port.send({ type: 'event', event: event.event, payload: event.payload }))
68
71
  ];
72
+
69
73
  const disconnect = (): void => {
70
74
  if (!this.connections.delete(disconnect)) {
71
75
  return;
72
76
  }
77
+
73
78
  for (const release of releases) {
74
79
  release();
75
80
  }
76
81
  };
82
+
77
83
  this.connections.add(disconnect);
84
+
78
85
  return disconnect;
79
86
  }
80
87
 
81
88
  /* Lets go of every client, writes every thread and ends every CLI; settles once they exited. */
82
89
  close(): Promise<void> {
83
90
  this.closing ??= this.shutDown();
91
+
84
92
  return this.closing;
85
93
  }
86
94
 
@@ -88,35 +96,47 @@ export class AgentHost<Core extends ChatCore = ChatCore> {
88
96
  for (const disconnect of [...this.connections]) {
89
97
  disconnect();
90
98
  }
99
+
91
100
  await this.wiring.stop();
92
101
  }
93
102
 
94
103
  private async receive(port: FramePort, clientId: string, frame: unknown): Promise<void> {
95
104
  const parsed = parseRequest(frame);
105
+
96
106
  if (!parsed.ok) {
97
107
  // The id when the frame carried a usable one, so the client can settle its promise.
98
108
  const id = typeof frame === 'object' && frame !== null && 'id' in frame && typeof frame.id === 'string' && frame.id !== '' ? frame.id : null;
99
109
  port.send(errorReply(id, 'bad-request', parsed.message));
110
+
100
111
  return;
101
112
  }
113
+
102
114
  const { id, type, payload } = parsed.value;
115
+
103
116
  if (!isAgentRequest(type)) {
104
117
  port.send(errorReply(id, 'unknown-request', `Unknown request type: ${type}`));
118
+
105
119
  return;
106
120
  }
121
+
107
122
  const checked = AGENT_REQUEST_SCHEMAS[type].payload.safeParse(payload);
123
+
108
124
  if (!checked.success) {
109
125
  port.send(errorReply(id, 'bad-request', `Invalid payload for ${type}`));
126
+
110
127
  return;
111
128
  }
129
+
112
130
  try {
113
131
  const handler = this.wiring.handlers[type] as AgentHandler<AgentRequestType>;
114
132
  port.send({ id, ok: true, result: await handler(checked.data as never, clientId) });
115
133
  } catch (e) {
116
134
  if (e instanceof CodedError) {
117
135
  port.send(errorReply(id, e.code, e.message));
136
+
118
137
  return;
119
138
  }
139
+
120
140
  console.error(`Handler for ${type} failed:`, errorText(e));
121
141
  port.send(errorReply(id, 'internal', 'Request failed'));
122
142
  }
@@ -13,19 +13,25 @@ const COMPATIBILITY_PREFIXES = ['RUIMTE_HOOK_', 'RUIMTE_CONTEXT_'];
13
13
  export function cliEnvironment(env: Record<string, string | undefined> = process.env, policy: CliEnvironmentPolicy = {}): Record<string, string> {
14
14
  const variables = new Set<string>([...RUIMTE_SESSION_VARIABLES, ...(policy.variables ?? [])]);
15
15
  const prefixes = [...COMPATIBILITY_PREFIXES, ...(policy.prefixes ?? [])];
16
+
16
17
  for (const host of policy.sessionPrefixes ?? []) {
17
18
  const prefix = host.replace(/_+$/, '');
19
+
18
20
  if (prefix === '') {
19
21
  throw new Error('A session environment prefix must not be empty');
20
22
  }
23
+
21
24
  variables.add(`${prefix}_SESSION_ID`);
22
25
  prefixes.push(`${prefix}_HOOK_`, `${prefix}_CONTEXT_`);
23
26
  }
27
+
24
28
  const clean: Record<string, string> = {};
29
+
25
30
  for (const [name, value] of Object.entries(env)) {
26
31
  if (value !== undefined && !variables.has(name) && !prefixes.some((prefix) => name.startsWith(prefix))) {
27
32
  clean[name] = value;
28
33
  }
29
34
  }
35
+
30
36
  return clean;
31
37
  }
@@ -42,6 +42,7 @@ export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T
42
42
  // What a chat the host starts on its own is made with, while this client is connected.
43
43
  'chat.setPreferences': (payload, clientId) => {
44
44
  chats.composerPreferences.set(clientId, payload);
45
+
45
46
  return {};
46
47
  },
47
48
  'chat.attach': (payload, clientId) => chats.attachWithBookmarks(payload.chatId, clientId, payload.historyLimit, payload.since),
@@ -49,18 +50,22 @@ export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T
49
50
  'chat.subagent': (payload, clientId) => chats.subagent(clientId, payload),
50
51
  'chat.stopSubagent': async (payload) => {
51
52
  await chats.stopSubagent(payload.chatId, payload.toolUseId);
53
+
52
54
  return {};
53
55
  },
54
56
  'chat.stopTask': async (payload) => {
55
57
  await chats.stopTask(payload.chatId, payload.taskId);
58
+
56
59
  return {};
57
60
  },
58
61
  'chat.goal': async (payload) => {
59
62
  await chats.goal(payload.chatId, payload.action);
63
+
60
64
  return {};
61
65
  },
62
66
  'chat.detach': (payload, clientId) => {
63
67
  chats.detach(payload.chatId, clientId);
68
+
64
69
  return {};
65
70
  },
66
71
  'chat.send': (payload) =>
@@ -83,15 +88,18 @@ export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T
83
88
  'chat.unqueue': (payload) => ({ message: chats.unqueue(payload.chatId, payload.messageId) }),
84
89
  'chat.sendNow': (payload) => {
85
90
  chats.sendNow(payload.chatId, payload.messageId);
91
+
86
92
  return {};
87
93
  },
88
94
  'skills.list': async (payload) => ({ skills: await chats.skills(payload.chatId) }),
89
95
  'chat.compact': (payload) => {
90
96
  chats.compact(payload.chatId);
97
+
91
98
  return {};
92
99
  },
93
100
  'chat.clear': async (payload) => {
94
101
  await chats.clear(payload.chatId, payload.force === true);
102
+
95
103
  return {};
96
104
  },
97
105
  'chat.turnDiff': async (payload) => ({ diff: await chats.turnDiff(payload.chatId, payload.turnId) }),
@@ -101,18 +109,22 @@ export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T
101
109
  'chat.continueOn': unsupported('continue a chat under another account'),
102
110
  'chat.cancel': (payload) => {
103
111
  chats.cancel(payload.chatId, payload.subagents === true);
112
+
104
113
  return {};
105
114
  },
106
115
  'chat.approve': (payload) => {
107
116
  chats.approve(payload.chatId, payload.requestId, payload.decision, payload.message);
117
+
108
118
  return {};
109
119
  },
110
120
  'chat.answer': (payload) => {
111
121
  chats.answer(payload.chatId, payload.requestId, payload.answers);
122
+
112
123
  return {};
113
124
  },
114
125
  'chat.dismiss': (payload) => {
115
126
  chats.dismiss(payload.chatId, payload.itemId);
127
+
116
128
  return {};
117
129
  },
118
130
  'chat.addBookmark': async (payload) => ({ bookmarks: await chats.addBookmark(payload.chatId, payload.itemId, payload.name) }),
@@ -121,10 +133,12 @@ export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T
121
133
  'chat.removeVisual': async (payload) => ({ visuals: await chats.removeVisual(payload.chatId, payload.visualId) }),
122
134
  'chat.kill': async (payload) => {
123
135
  await chats.kill(payload.chatId);
136
+
124
137
  return {};
125
138
  },
126
139
  'chat.restart': async (payload) => {
127
140
  await chats.restart(payload.chatId);
141
+
128
142
  return {};
129
143
  },
130
144
  'chat.list': () => ({ chats: chats.list() })
@@ -139,6 +153,7 @@ export function accountHandlers(accounts: ProviderAccountsService): { [T in Acco
139
153
  'accounts.create': (payload) => accounts.create(payload),
140
154
  'accounts.watchLogin': (payload) => {
141
155
  void accounts.watchLogin(payload.id);
156
+
142
157
  return {};
143
158
  }
144
159
  };
@@ -150,15 +165,18 @@ export function usageHandlers(usage: UsageService, limits: UsageMonitor): { [T i
150
165
  // While a client has the page open the host keeps scanning; every other client gets the event too.
151
166
  'usage.subscribe': (_payload, clientId) => {
152
167
  usage.follow(clientId);
168
+
153
169
  return {};
154
170
  },
155
171
  'usage.unsubscribe': (_payload, clientId) => {
156
172
  usage.unfollow(clientId);
173
+
157
174
  return {};
158
175
  },
159
176
  'usage.limits': () => limits.snapshot(),
160
177
  'usage.refreshLimits': async () => {
161
178
  await limits.refresh(true);
179
+
162
180
  return limits.snapshot();
163
181
  }
164
182
  };
@@ -7,9 +7,11 @@ import type { FramePort } from '@adecore/agent-contracts';
7
7
  */
8
8
  export function memoryPortPair(): [FramePort, FramePort] {
9
9
  const listeners: [Set<(frame: unknown) => void>, Set<(frame: unknown) => void>] = [new Set(), new Set()];
10
+
10
11
  const end = (own: number): FramePort => ({
11
12
  send: (frame) => {
12
13
  const copy = structuredClone(frame);
14
+
13
15
  queueMicrotask(() => {
14
16
  for (const listener of listeners[1 - own]!) {
15
17
  listener(copy);
@@ -18,10 +20,12 @@ export function memoryPortPair(): [FramePort, FramePort] {
18
20
  },
19
21
  onFrame: (listener) => {
20
22
  listeners[own]!.add(listener);
23
+
21
24
  return () => {
22
25
  listeners[own]!.delete(listener);
23
26
  };
24
27
  }
25
28
  });
29
+
26
30
  return [end(0), end(1)];
27
31
  }