@adecore/agents 0.38.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 (170) 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.d.ts +1 -0
  5. package/dist/chat/chat-core.js +9 -2
  6. package/dist/chat/chat-process.d.ts +1 -0
  7. package/dist/chat/chat-process.js +1 -1
  8. package/dist/chat/chat-session.d.ts +2 -0
  9. package/dist/chat/chat-session.js +9 -10
  10. package/dist/chat/claude-protocol.js +1 -6
  11. package/dist/chat/claude-transcript.js +1 -3
  12. package/dist/chat/codex-protocol.js +1 -6
  13. package/dist/chat/projector.js +1 -3
  14. package/dist/chat/request-summary.js +1 -3
  15. package/dist/coded-error.js +1 -5
  16. package/dist/context/argv.js +1 -2
  17. package/dist/context/verb.js +1 -3
  18. package/dist/error-text.d.ts +1 -0
  19. package/dist/error-text.js +4 -0
  20. package/dist/fields.d.ts +3 -0
  21. package/dist/fields.js +12 -0
  22. package/dist/fs.d.ts +1 -0
  23. package/dist/fs.js +8 -4
  24. package/dist/lineage.js +11 -5
  25. package/dist/messages/notice-store.js +3 -4
  26. package/dist/providers/accounts/acp-account.js +3 -20
  27. package/dist/providers/accounts/service.d.ts +1 -2
  28. package/dist/providers/accounts/service.js +5 -9
  29. package/dist/providers/accounts/status.js +1 -3
  30. package/dist/providers/acp-agent-state.js +1 -3
  31. package/dist/providers/claude.js +0 -1
  32. package/dist/providers/codex.js +0 -1
  33. package/dist/providers/detect.js +1 -1
  34. package/dist/providers/shell-commands.js +1 -2
  35. package/dist/run-process.js +1 -3
  36. package/dist/skills.d.ts +1 -1
  37. package/dist/skills.js +5 -8
  38. package/dist/tasks/background-limit.js +3 -2
  39. package/dist/tasks/task-coordinator.js +11 -17
  40. package/dist/testing/watch.js +0 -1
  41. package/dist/usage/limits/monitor.js +1 -2
  42. package/dist/usage/readers/claude.js +3 -10
  43. package/dist/usage/readers/codex.js +2 -9
  44. package/dist/usage/readers/grok.js +3 -10
  45. package/dist/usage/readers/host-log.js +3 -10
  46. package/dist/usage/record.d.ts +2 -0
  47. package/dist/usage/record.js +13 -0
  48. package/package.json +6 -6
  49. package/src/async.ts +23 -5
  50. package/src/chat/acp-backend.ts +121 -0
  51. package/src/chat/acp-protocol.ts +172 -6
  52. package/src/chat/attachment-store.ts +21 -0
  53. package/src/chat/backend.ts +0 -2
  54. package/src/chat/bookmark-store.ts +23 -0
  55. package/src/chat/chat-core.ts +218 -7
  56. package/src/chat/chat-log.ts +19 -0
  57. package/src/chat/chat-process.ts +52 -1
  58. package/src/chat/chat-session.ts +414 -13
  59. package/src/chat/chat-store.ts +50 -0
  60. package/src/chat/chat-title.ts +9 -0
  61. package/src/chat/claude-backend.ts +50 -0
  62. package/src/chat/claude-protocol.ts +155 -8
  63. package/src/chat/claude-title.ts +10 -0
  64. package/src/chat/claude-transcript.ts +47 -4
  65. package/src/chat/codex-backend.ts +131 -0
  66. package/src/chat/codex-protocol.ts +180 -8
  67. package/src/chat/codex-thread.ts +23 -0
  68. package/src/chat/composer-preferences.ts +6 -0
  69. package/src/chat/context-breakdown.ts +11 -0
  70. package/src/chat/delta-coalescer.ts +8 -0
  71. package/src/chat/external-sessions.ts +28 -0
  72. package/src/chat/generated-image.ts +25 -0
  73. package/src/chat/index.ts +5 -0
  74. package/src/chat/input.ts +13 -0
  75. package/src/chat/json-rpc-transport.ts +28 -0
  76. package/src/chat/limit-resume.ts +13 -0
  77. package/src/chat/projector.ts +160 -4
  78. package/src/chat/readable-text.ts +3 -0
  79. package/src/chat/request-summary.ts +18 -4
  80. package/src/chat/subagent-projection.ts +3 -0
  81. package/src/chat/subagent-reader.ts +76 -0
  82. package/src/chat/subagent-settlement.ts +24 -0
  83. package/src/chat/thread-search.ts +13 -0
  84. package/src/chat/thread.ts +31 -0
  85. package/src/chat/ui-queries.ts +122 -0
  86. package/src/chat/ui-replies.ts +34 -0
  87. package/src/chat/visual-store.ts +49 -0
  88. package/src/chat/wake-chat.ts +3 -0
  89. package/src/client-sinks.ts +2 -0
  90. package/src/coded-error.ts +1 -5
  91. package/src/context/argv.ts +18 -2
  92. package/src/context/index.ts +1 -0
  93. package/src/context/refusal.ts +3 -0
  94. package/src/context/verb.ts +32 -4
  95. package/src/coordination/index.ts +10 -0
  96. package/src/error-text.ts +7 -0
  97. package/src/fields.ts +14 -0
  98. package/src/fs.ts +20 -4
  99. package/src/host/agent-host.ts +20 -0
  100. package/src/host/environment.ts +6 -0
  101. package/src/host/handlers.ts +18 -0
  102. package/src/host/memory-port.ts +4 -0
  103. package/src/host/wiring.ts +10 -0
  104. package/src/index.ts +1 -0
  105. package/src/lineage.ts +24 -5
  106. package/src/messages/deliver-message.ts +6 -0
  107. package/src/messages/deliver-notice.ts +13 -0
  108. package/src/messages/notice-store.ts +16 -4
  109. package/src/modes.ts +1 -0
  110. package/src/outbox/outbox-worker.ts +31 -0
  111. package/src/outbox/outbox.ts +5 -0
  112. package/src/providers/accounts/accounts.ts +36 -0
  113. package/src/providers/accounts/acp-account.ts +22 -27
  114. package/src/providers/accounts/launch.ts +4 -0
  115. package/src/providers/accounts/service.ts +110 -12
  116. package/src/providers/accounts/shadow-home.ts +10 -0
  117. package/src/providers/accounts/status.ts +13 -4
  118. package/src/providers/accounts/store.ts +25 -0
  119. package/src/providers/accounts/variables.ts +11 -1
  120. package/src/providers/acp-agent-state.ts +24 -3
  121. package/src/providers/acp-provider.ts +5 -0
  122. package/src/providers/acp-usage-log.ts +2 -0
  123. package/src/providers/catalog.ts +17 -0
  124. package/src/providers/claude.ts +10 -1
  125. package/src/providers/codex-rules.ts +3 -0
  126. package/src/providers/codex.ts +2 -1
  127. package/src/providers/detect.ts +6 -1
  128. package/src/providers/provider.ts +1 -1
  129. package/src/providers/registry.ts +18 -2
  130. package/src/providers/shell-commands.ts +37 -2
  131. package/src/record-directory.ts +19 -0
  132. package/src/run-process.ts +24 -3
  133. package/src/serializer.ts +3 -0
  134. package/src/skills.ts +73 -10
  135. package/src/tasks/background-limit.ts +9 -2
  136. package/src/tasks/end-children.ts +9 -0
  137. package/src/tasks/give-task.ts +8 -0
  138. package/src/tasks/task-coordinator.ts +81 -17
  139. package/src/tasks/task-store.ts +28 -0
  140. package/src/tasks/waiting-child.ts +21 -0
  141. package/src/tasks/wake-parent.ts +26 -0
  142. package/src/tasks/wiring.ts +22 -0
  143. package/src/testing/accounts.ts +2 -0
  144. package/src/testing/fake-acp-capture.ts +16 -0
  145. package/src/testing/fake-acp.ts +66 -0
  146. package/src/testing/fake-claude.ts +177 -0
  147. package/src/testing/fake-cli.ts +29 -0
  148. package/src/testing/fake-codex.ts +122 -0
  149. package/src/testing/manual-clock.ts +2 -0
  150. package/src/testing/watch.ts +7 -1
  151. package/src/title-file.ts +12 -0
  152. package/src/usage/accounts.ts +6 -0
  153. package/src/usage/aggregate.ts +19 -0
  154. package/src/usage/exchange.ts +12 -0
  155. package/src/usage/index-file.ts +27 -0
  156. package/src/usage/limits/monitor.ts +31 -3
  157. package/src/usage/limits/normalize.ts +30 -0
  158. package/src/usage/limits/probe.ts +17 -0
  159. package/src/usage/pricing.ts +35 -0
  160. package/src/usage/projects.ts +8 -0
  161. package/src/usage/readers/claude.ts +12 -9
  162. package/src/usage/readers/codex.ts +40 -8
  163. package/src/usage/readers/grok.ts +17 -9
  164. package/src/usage/readers/host-log.ts +9 -9
  165. package/src/usage/record.ts +22 -0
  166. package/src/usage/roots.ts +6 -0
  167. package/src/usage/scanner.ts +61 -0
  168. package/src/usage/usage-service.ts +14 -0
  169. package/src/watch-seam.ts +8 -0
  170. package/src/windows-command.ts +12 -0
@@ -28,7 +28,6 @@ const CODEX_PRIORITY_TIER = 'priority';
28
28
  export function codexServiceTier(selection) {
29
29
  return selection.options.serviceTier === true ? CODEX_PRIORITY_TIER : null;
30
30
  }
31
- // What the app-server can do, as far as a client has to know.
32
31
  export const CODEX_CAPABILITIES = {
33
32
  chat: true,
34
33
  terminal: true,
@@ -16,7 +16,7 @@ export async function detectCli(command, env = process.env, spawn = spawnChatPro
16
16
  if ((await code) !== 0) {
17
17
  return { installed: false, version: null };
18
18
  }
19
- const version = /\d+\.\d+\.\d+/.exec(output)?.[0] ?? output.trim() ?? null;
19
+ const version = /\d+\.\d+\.\d+/.exec(output)?.[0] ?? output.trim();
20
20
  return { installed: true, version };
21
21
  }
22
22
  catch {
@@ -4,8 +4,7 @@ const REFUSED = new Set(['|', '>', '<', '$', '`', '(', ')', '{', '}', '*', '?',
4
4
  /*
5
5
  * Whether a shell command line is only simple commands, joined by `&&`, `||`, `;` or newlines, each of
6
6
  * which starts with one of `allowed`. Anything the tokenizer cannot prove simple answers false, so a
7
- * true is safe to act on without a person: a pipe, a redirect, an expansion, a subshell, a background
8
- * `&`, an unquoted glob or an unbalanced quote all refuse the whole line.
7
+ * true is safe to act on without a person.
9
8
  */
10
9
  export function isOnlyCommands(command, allowed) {
11
10
  let word = '';
@@ -1,8 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { killProcessTree } from './chat/chat-process.js';
2
+ import { KILL_GRACE_MS, killProcessTree } from './chat/chat-process.js';
3
3
  import { spawnTarget } from './windows-command.js';
4
- // What a command and everything it started get between the SIGTERM and the SIGKILL.
5
- const KILL_GRACE_MS = 2000;
6
4
  // Stdout and stderr together; a short command that prints more is ended and fails instead of filling memory.
7
5
  const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
8
6
  /* One short command to its end, with what it printed. A command that cannot start rejects. */
package/dist/skills.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface SkillRootOptions {
17
17
  export declare function parseFrontmatter(text: string): Record<string, string>;
18
18
  export declare function pluginSkillRoots(configDir: string): Promise<SkillRoot[]>;
19
19
  export declare function skillRootsFor(kind: AgentKind, cwd: string, options?: SkillRootOptions): Promise<SkillRoot[]>;
20
- export declare function whileBusy<T>(operation: () => Promise<T>, wait?: (ms: number) => Promise<void>): Promise<T>;
20
+ export declare function whileBusy<T>(operation: () => Promise<T>, sleep?: (ms: number) => Promise<void>): Promise<T>;
21
21
  export declare function mapLimited<T, R>(items: readonly T[], limit: number, work: (item: T) => Promise<R>): Promise<R[]>;
22
22
  export declare function scanSkillRoots(roots: SkillRoot[], limits?: SkillScanLimits): Promise<ChatSkill[]>;
23
23
  export declare function discoverSkills(kind: AgentKind, cwd: string, options?: SkillRootOptions): Promise<ChatSkill[]>;
package/dist/skills.js CHANGED
@@ -2,7 +2,8 @@ import { existsSync } from 'node:fs';
2
2
  import { open, readdir, readFile, realpath, stat } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join, parse } from 'node:path';
5
- import { isNotFound } from './fs.js';
5
+ import { wait } from './async.js';
6
+ import { hasErrorCode, isNotFound } from './fs.js';
6
7
  const SCAN_LIMITS = { directories: 2000, skills: 1000 };
7
8
  const ENTRIES_PER_FOLDER = 1000;
8
9
  // Paths one folder resolves or reads at once, so a large folder never runs the process out of file handles.
@@ -143,13 +144,10 @@ export async function skillRootsFor(kind, cwd, options = {}) {
143
144
  }
144
145
  /* Too many files open, in this process or on the system: it says nothing about the path that was asked for. */
145
146
  function isBusy(e) {
146
- return typeof e === 'object' && e !== null && 'code' in e && (e.code === 'EMFILE' || e.code === 'ENFILE');
147
- }
148
- function sleep(ms) {
149
- return new Promise((resolve) => setTimeout(resolve, ms));
147
+ return hasErrorCode(e, 'EMFILE', 'ENFILE');
150
148
  }
151
149
  /* Runs `operation` again a few times while the process is short of file handles, and throws if it still is. */
152
- export async function whileBusy(operation, wait = sleep) {
150
+ export async function whileBusy(operation, sleep = wait) {
153
151
  for (let attempt = 1;; attempt++) {
154
152
  try {
155
153
  return await operation();
@@ -158,7 +156,7 @@ export async function whileBusy(operation, wait = sleep) {
158
156
  if (!isBusy(e) || attempt > BUSY_RETRIES) {
159
157
  throw e;
160
158
  }
161
- await wait(BUSY_RETRY_MS * attempt);
159
+ await sleep(BUSY_RETRY_MS * attempt);
162
160
  }
163
161
  }
164
162
  }
@@ -310,7 +308,6 @@ export async function scanSkillRoots(roots, limits = SCAN_LIMITS) {
310
308
  export async function discoverSkills(kind, cwd, options = {}) {
311
309
  return scanSkillRoots(await skillRootsFor(kind, cwd, options));
312
310
  }
313
- // How long a scan stays good; a skill written mid-session shows up on the next look.
314
311
  const CACHE_TTL_MS = 30_000;
315
312
  /* The skills of one CLI in one folder, scanned at most once per half minute. There is no watcher. */
316
313
  export class SkillIndex {
@@ -41,12 +41,13 @@ export async function restartBackgroundLimits(outbox, now) {
41
41
  }
42
42
  /* The `background-limit` entries of a host's outbox, one per task at most, as the coordinator owes and drops them. */
43
43
  export function backgroundLimits(outbox) {
44
+ const limitOf = (entry, taskId) => isBackgroundLimit(entry) && entry.payload.taskId === taskId;
44
45
  return {
45
- owed: (taskId) => outbox.list().some((entry) => isBackgroundLimit(entry) && entry.payload.taskId === taskId),
46
+ owed: (taskId) => outbox.list().some((entry) => limitOf(entry, taskId)),
46
47
  owe: (task, commands, at) => outbox.enqueue(task.projectId, task.childId, { kind: 'background-limit', payload: { taskId: task.id, commands: [...commands] } }, at),
47
48
  lapse: async (taskId) => {
48
49
  for (const entry of outbox.list()) {
49
- if (isBackgroundLimit(entry) && entry.payload.taskId === taskId) {
50
+ if (limitOf(entry, taskId)) {
50
51
  await outbox.remove(entry.id);
51
52
  }
52
53
  }
@@ -52,11 +52,9 @@ export function resultOfTurn(turn, items, at) {
52
52
  /*
53
53
  * Settles tasks from what children do and does nothing else: it writes the task store and owes wakes
54
54
  * in the outbox, never starts a turn, since it runs inside the broadcast of the child it heard. A chat
55
- * child settles when a turn ends with none of its own tasks still open or waiting to wake it, no goal
56
- * still active and no work of its CLI's still running in the background, or once the commands it runs
57
- * there outlast their limit;
58
- * an agent that is not a chat, which the host tells it about, settles only on `done`, or fails when it
59
- * ends without one.
55
+ * child settles when a turn ends with no task of its own open or waiting to wake it, no active goal and
56
+ * no background work running, or once its background commands outlast their limit. An agent that is
57
+ * not a chat settles only on `done`, or fails when it ends without one.
60
58
  */
61
59
  export class TaskCoordinator {
62
60
  deps;
@@ -114,14 +112,9 @@ export class TaskCoordinator {
114
112
  * which says how that kind of agent reports back.
115
113
  */
116
114
  agentEnded(childId, text) {
117
- if (this.stopped) {
118
- return;
115
+ if (this.deps.placed(childId)) {
116
+ this.failed(childId, text);
119
117
  }
120
- const task = this.localTask(childId);
121
- if (!task || !this.deps.placed(childId)) {
122
- return;
123
- }
124
- this.settle(task, 'failed', { text, source: 'exit', at: this.deps.now() });
125
118
  }
126
119
  startFailed(childId, error) {
127
120
  this.failed(childId, `The agent could not be started: ${errorText(error)}`);
@@ -183,7 +176,7 @@ export class TaskCoordinator {
183
176
  * gets a turn of its own that names it; the turn that was in its way answers the person who sent it,
184
177
  * not the task, and nothing settles while that turn is still owed. A turn after it answers too, as the
185
178
  * one the CLI opens about work that turn left running. A child opened with its task has no turn of that
186
- * kind, so its first turn settles it, as it always did.
179
+ * kind, so its first turn settles it.
187
180
  */
188
181
  answers(items, turn, task) {
189
182
  if ((turn.taskIds ?? []).includes(task.id)) {
@@ -216,7 +209,7 @@ export class TaskCoordinator {
216
209
  this.settle(task, 'failed', { ...result, text: `${result.text}\n\n${goalLostNote(goal.objective)}`, source: 'exit' });
217
210
  return;
218
211
  }
219
- // A turn a person stopped or that failed ends the task as it always did, whatever still runs beside it.
212
+ // A turn a person stopped or that failed ends the task, whatever still runs beside it.
220
213
  if (turn.state === 'done' && this.heldInBackground(chatId, task, items)) {
221
214
  this.track(this.deps.tasks.holdBackground(task.id, {
222
215
  turnId: turn.id,
@@ -247,13 +240,14 @@ export class TaskCoordinator {
247
240
  if (background.itemIds.length === 0 && this.deps.limit.owed(task.id)) {
248
241
  return;
249
242
  }
243
+ const rowOf = (id) => items.find((item) => item.id === id);
250
244
  const lost = background.itemIds.filter((id) => {
251
- const item = items.find((item) => item.id === id);
245
+ const item = rowOf(id);
252
246
  return !item || (item.kind === 'subagent' ? item.status !== 'done' : item.kind !== 'tool' || item.state !== 'done');
253
247
  });
254
248
  if (lost.length > 0 || background.commands.length > 0) {
255
249
  const names = lost.map((id) => {
256
- const row = items.find((item) => item.id === id);
250
+ const row = rowOf(id);
257
251
  return row?.kind === 'subagent' ? row.description || row.toolUseId : row?.kind === 'tool' ? row.name : id;
258
252
  });
259
253
  this.settle(task, 'failed', {
@@ -264,7 +258,7 @@ export class TaskCoordinator {
264
258
  return;
265
259
  }
266
260
  const reports = background.itemIds.flatMap((id) => {
267
- const row = items.find((item) => item.id === id);
261
+ const row = rowOf(id);
268
262
  const report = row?.kind === 'subagent' ? row.result : row?.kind === 'tool' ? row.output : null;
269
263
  return report ? [report] : [];
270
264
  });
@@ -42,7 +42,6 @@ export class FakeWatch {
42
42
  this.due.delete(id);
43
43
  };
44
44
  };
45
- /* How many timers are waiting to run. */
46
45
  get pending() {
47
46
  return this.due.size;
48
47
  }
@@ -193,7 +193,6 @@ export class UsageMonitor {
193
193
  return this.probe(account.kind, this.registry.get(account.kind).command, env);
194
194
  }
195
195
  emit() {
196
- const event = { event: 'usage.limitsChanged', payload: this.snapshot() };
197
- this.sinks.emit(event);
196
+ this.sinks.emit({ event: 'usage.limitsChanged', payload: this.snapshot() });
198
197
  }
199
198
  }
@@ -1,4 +1,4 @@
1
- import { asObject, asString, int } from '../record.js';
1
+ import { asObject, asString, countsTokens, int, parseObject } from '../record.js';
2
2
  /* Only a line with this in it can carry counts, which keeps `JSON.parse` off about two thirds of them. */
3
3
  export function claudeMightCarryUsage(line) {
4
4
  return line.includes('"usage"');
@@ -9,14 +9,7 @@ export function claudeMightCarryUsage(line) {
9
9
  * Anthropic's, so `input_tokens` already excludes what was read from or written to the cache.
10
10
  */
11
11
  export function parseClaudeLine(line) {
12
- let parsed;
13
- try {
14
- parsed = JSON.parse(line);
15
- }
16
- catch {
17
- return null;
18
- }
19
- const record = asObject(parsed);
12
+ const record = parseObject(line);
20
13
  if (record === null || record.type !== 'assistant') {
21
14
  return null;
22
15
  }
@@ -44,7 +37,7 @@ export function parseClaudeLine(line) {
44
37
  output: int(usage.output_tokens),
45
38
  reasoning: outputDetails === null ? 0 : int(outputDetails.thinking_tokens)
46
39
  };
47
- if (totals.input + totals.cacheRead + totals.cacheWrite + totals.output === 0) {
40
+ if (!countsTokens(totals)) {
48
41
  return null;
49
42
  }
50
43
  return {
@@ -1,4 +1,4 @@
1
- import { asObject, asString, int } from '../record.js';
1
+ import { asObject, asString, int, parseObject } from '../record.js';
2
2
  export function codexMightCarryUsage(line) {
3
3
  return ['token_count', 'turn_context', 'session_meta', 'thread_settings_applied', 'token_usage_record', 'compacted'].some((type) => line.includes(type));
4
4
  }
@@ -59,14 +59,7 @@ export function isForkOrSubagent(payload) {
59
59
  /* Codex includes cache reads and writes in input_tokens. Compaction can appear separately from
60
60
  cumulative token_count events, so response identities prevent charging the same request twice. */
61
61
  export function parseCodexLine(line, state) {
62
- let parsed;
63
- try {
64
- parsed = JSON.parse(line);
65
- }
66
- catch {
67
- return null;
68
- }
69
- const record = asObject(parsed);
62
+ const record = parseObject(line);
70
63
  const payload = record === null ? null : asObject(record.payload);
71
64
  if (record === null || payload === null) {
72
65
  return null;
@@ -1,5 +1,5 @@
1
1
  import { basename, dirname } from 'node:path';
2
- import { asObject, asString, int } from '../record.js';
2
+ import { asObject, asString, countsTokens, int, parseObject } from '../record.js';
3
3
  /* The file Grok keeps the counts of a session in, beside its other files in the session's folder. */
4
4
  export const GROK_USAGE_FILE = 'usage.json';
5
5
  // `costUsdTicks` counts in ticks of 10^-10 USD, as Grok's own docs on `grok usage` say.
@@ -26,7 +26,7 @@ function recordOf(counts, model, base, turn) {
26
26
  output: int(counts.outputTokens),
27
27
  reasoning: int(counts.reasoningTokens)
28
28
  };
29
- if (totals.input + totals.cacheRead + totals.cacheWrite + totals.output === 0) {
29
+ if (!countsTokens(totals)) {
30
30
  return null;
31
31
  }
32
32
  const ticks = int(counts.costUsdTicks);
@@ -45,14 +45,7 @@ function recordOf(counts, model, base, turn) {
45
45
  * read as one, which leaves what the scanner held of it.
46
46
  */
47
47
  export function parseGrokUsage(text, path) {
48
- let parsed;
49
- try {
50
- parsed = JSON.parse(text);
51
- }
52
- catch {
53
- return null;
54
- }
55
- const file = asObject(parsed);
48
+ const file = parseObject(text);
56
49
  if (file === null || !Array.isArray(file.turns)) {
57
50
  return null;
58
51
  }
@@ -1,4 +1,4 @@
1
- import { asObject, asString, int } from '../record.js';
1
+ import { asString, countsTokens, int, parseObject } from '../record.js';
2
2
  export function hostUsageLine(entry) {
3
3
  return `${JSON.stringify(entry)}\n`;
4
4
  }
@@ -8,14 +8,7 @@ export function hostLogMightCarryUsage(line) {
8
8
  }
9
9
  /* The provider is the root's, so one reader serves every API a host logs. */
10
10
  export function parseHostUsageLine(line, provider) {
11
- let parsed;
12
- try {
13
- parsed = JSON.parse(line);
14
- }
15
- catch {
16
- return null;
17
- }
18
- const entry = asObject(parsed);
11
+ const entry = parseObject(line);
19
12
  if (entry === null) {
20
13
  return null;
21
14
  }
@@ -33,7 +26,7 @@ export function parseHostUsageLine(line, provider) {
33
26
  output: int(entry.output),
34
27
  reasoning: int(entry.reasoning)
35
28
  };
36
- if (totals.input + totals.cacheRead + totals.cacheWrite + totals.output === 0) {
29
+ if (!countsTokens(totals)) {
37
30
  return null;
38
31
  }
39
32
  return {
@@ -13,5 +13,7 @@ export interface UsageRecord {
13
13
  }
14
14
  export declare function int(value: unknown): number;
15
15
  export declare function asObject(value: unknown): Record<string, unknown> | null;
16
+ export declare function parseObject(text: string): Record<string, unknown> | null;
17
+ export declare function countsTokens(totals: UsageTotals): boolean;
16
18
  export declare function asString(value: unknown): string;
17
19
  export declare function foldByKey(records: readonly UsageRecord[]): UsageRecord[];
@@ -6,6 +6,19 @@ export function int(value) {
6
6
  export function asObject(value) {
7
7
  return typeof value === 'object' && value !== null ? value : null;
8
8
  }
9
+ /* A line or file of a transcript as a JSON object, or null for anything else. */
10
+ export function parseObject(text) {
11
+ try {
12
+ return asObject(JSON.parse(text));
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ /* Whether a call moved any tokens at all; one that moved none is not a call worth a record. */
19
+ export function countsTokens(totals) {
20
+ return totals.input + totals.cacheRead + totals.cacheWrite + totals.output !== 0;
21
+ }
9
22
  export function asString(value) {
10
23
  return typeof value === 'string' ? value : '';
11
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -81,13 +81,13 @@
81
81
  "test": "bun --conditions=source test"
82
82
  },
83
83
  "dependencies": {
84
- "@adecore/agent-contracts": "0.38.0",
85
- "@adecore/intelligent-ui": "0.38.0",
86
- "zod": "^4.6.5"
84
+ "@adecore/agent-contracts": "0.40.0",
85
+ "@adecore/intelligent-ui": "0.40.0",
86
+ "zod": "^4.6.7"
87
87
  },
88
88
  "devDependencies": {
89
- "@types/bun": "^1.4.2",
90
- "@types/node": "^26.6.3",
89
+ "@types/bun": "^1.4.4",
90
+ "@types/node": "^26.6.5",
91
91
  "typescript": "^7.0.2"
92
92
  },
93
93
  "license": "FSL-1.1-MIT",
package/src/async.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { asError } from './error-text.ts';
2
+
1
3
  /*
2
4
  * Resolves after `ms`, or at once when the signal is raised. An abort resolves instead of rejecting:
3
5
  * every caller is a loop that checks the signal itself on its next turn.
@@ -5,6 +7,7 @@
5
7
  export function wait(ms: number, signal?: AbortSignal): Promise<void> {
6
8
  return new Promise((resolve) => {
7
9
  const timer = setTimeout(resolve, ms);
10
+
8
11
  signal?.addEventListener(
9
12
  'abort',
10
13
  () => {
@@ -16,19 +19,34 @@ export function wait(ms: number, signal?: AbortSignal): Promise<void> {
16
19
  });
17
20
  }
18
21
 
22
+ export interface DeadlineTimer {
23
+ setTimeout(run: () => void, ms: number): unknown;
24
+ clearTimeout(handle: unknown): void;
25
+ }
26
+
27
+ const SYSTEM_TIMER: DeadlineTimer = {
28
+ setTimeout: (run, ms) => setTimeout(run, ms),
29
+ clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>)
30
+ };
31
+
19
32
  /* The timer is cleared either way, so a call that answers at once does not hold the process until its deadline. */
20
- export function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
33
+ export function withDeadline<T>(promise: Promise<T>, ms: number, message: string, timer: DeadlineTimer = SYSTEM_TIMER): Promise<T> {
21
34
  return new Promise((resolve, reject) => {
22
- const timer = setTimeout(() => reject(new Error(message)), ms);
35
+ const handle = timer.setTimeout(() => reject(new Error(message)), ms);
36
+
23
37
  promise.then(
24
38
  (value) => {
25
- clearTimeout(timer);
39
+ timer.clearTimeout(handle);
26
40
  resolve(value);
27
41
  },
28
42
  (e: unknown) => {
29
- clearTimeout(timer);
30
- reject(e instanceof Error ? e : new Error(String(e)));
43
+ timer.clearTimeout(handle);
44
+ reject(asError(e));
31
45
  }
32
46
  );
33
47
  });
34
48
  }
49
+
50
+ export function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
51
+ return withDeadline(promise, ms, message);
52
+ }