@adecore/agents 0.23.0 → 0.24.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 (153) hide show
  1. package/README.md +3 -3
  2. package/dist/chat/attachment-store.js +4 -5
  3. package/dist/chat/backend.d.ts +6 -1
  4. package/dist/chat/background-work.d.ts +1 -1
  5. package/dist/chat/background-work.js +2 -1
  6. package/dist/chat/chat-core.d.ts +53 -161
  7. package/dist/chat/chat-core.js +146 -112
  8. package/dist/chat/chat-log.js +3 -3
  9. package/dist/chat/chat-process.d.ts +21 -0
  10. package/dist/chat/chat-process.js +109 -9
  11. package/dist/chat/chat-session.d.ts +10 -17
  12. package/dist/chat/chat-session.js +141 -182
  13. package/dist/chat/chat-store.d.ts +1 -0
  14. package/dist/chat/chat-store.js +105 -1
  15. package/dist/chat/claude-backend.d.ts +4 -0
  16. package/dist/chat/claude-backend.js +51 -27
  17. package/dist/chat/claude-protocol.d.ts +12 -0
  18. package/dist/chat/claude-protocol.js +137 -7
  19. package/dist/chat/claude-title.d.ts +1 -3
  20. package/dist/chat/claude-title.js +11 -39
  21. package/dist/chat/claude-transcript.d.ts +1 -0
  22. package/dist/chat/claude-transcript.js +36 -17
  23. package/dist/chat/codex-backend.d.ts +21 -1
  24. package/dist/chat/codex-backend.js +168 -10
  25. package/dist/chat/codex-protocol.d.ts +2 -0
  26. package/dist/chat/codex-protocol.js +37 -3
  27. package/dist/chat/codex-thread.js +1 -3
  28. package/dist/chat/codex-transport.d.ts +3 -0
  29. package/dist/chat/codex-transport.js +38 -35
  30. package/dist/chat/context-breakdown.js +2 -2
  31. package/dist/chat/errors.d.ts +1 -1
  32. package/dist/chat/external-sessions.js +2 -2
  33. package/dist/chat/fixtures/claude-2.1.296-goal.json +136 -0
  34. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
  35. package/dist/chat/index.d.ts +29 -0
  36. package/dist/chat/index.js +23 -0
  37. package/dist/chat/projector.d.ts +2 -1
  38. package/dist/chat/projector.js +12 -3
  39. package/dist/chat/request-summary.js +1 -4
  40. package/dist/chat/subagent-settlement.js +1 -3
  41. package/dist/chat/thread.d.ts +3 -0
  42. package/dist/chat/thread.js +20 -3
  43. package/dist/chat/ui-queries.d.ts +2 -0
  44. package/dist/chat/ui-queries.js +40 -5
  45. package/dist/chat/ui-replies.d.ts +33 -0
  46. package/dist/chat/ui-replies.js +152 -0
  47. package/dist/context/index.d.ts +2 -0
  48. package/dist/context/index.js +3 -0
  49. package/dist/coordination/index.d.ts +15 -0
  50. package/dist/coordination/index.js +16 -0
  51. package/dist/host/handlers.js +8 -0
  52. package/dist/host/index.d.ts +7 -0
  53. package/dist/host/index.js +7 -0
  54. package/dist/index.d.ts +9 -0
  55. package/dist/index.js +10 -0
  56. package/dist/providers/accounts/status.js +1 -3
  57. package/dist/providers/claude.js +3 -1
  58. package/dist/providers/codex.js +3 -1
  59. package/dist/providers/index.d.ts +14 -0
  60. package/dist/providers/index.js +12 -0
  61. package/dist/run-process.js +54 -14
  62. package/dist/skills.d.ts +1 -0
  63. package/dist/skills.js +4 -0
  64. package/dist/tasks/task-coordinator.d.ts +4 -0
  65. package/dist/tasks/task-coordinator.js +16 -2
  66. package/dist/tasks/wiring.js +7 -1
  67. package/dist/{providers/accounts/test-accounts.d.ts → testing/accounts.d.ts} +2 -2
  68. package/dist/{providers/accounts/test-accounts.js → testing/accounts.js} +3 -3
  69. package/dist/testing/bin/fake-claude.d.ts +1 -0
  70. package/dist/testing/bin/fake-claude.js +6 -0
  71. package/dist/testing/bin/fake-codex.d.ts +1 -0
  72. package/dist/testing/bin/fake-codex.js +6 -0
  73. package/dist/{chat → testing}/fake-claude.d.ts +1 -1
  74. package/dist/{chat → testing}/fake-claude.js +0 -4
  75. package/dist/{chat → testing}/fake-cli.d.ts +1 -1
  76. package/dist/{chat → testing}/fake-codex.d.ts +1 -1
  77. package/dist/{chat → testing}/fake-codex.js +70 -8
  78. package/dist/testing/index.d.ts +6 -0
  79. package/dist/testing/index.js +7 -0
  80. package/dist/{outbox → testing}/manual-clock.d.ts +1 -1
  81. package/dist/{watch-test-helpers.d.ts → testing/watch.d.ts} +1 -1
  82. package/dist/usage/exchange.d.ts +5 -3
  83. package/dist/usage/exchange.js +25 -16
  84. package/dist/usage/index-file.d.ts +1 -0
  85. package/dist/usage/index-file.js +20 -2
  86. package/dist/usage/index.d.ts +9 -0
  87. package/dist/usage/index.js +6 -0
  88. package/dist/usage/limits/normalize.js +1 -3
  89. package/dist/usage/pricing.d.ts +5 -3
  90. package/dist/usage/pricing.js +28 -19
  91. package/dist/usage/scanner.js +39 -11
  92. package/dist/usage/usage-service.js +8 -3
  93. package/package.json +72 -11
  94. package/src/chat/attachment-store.ts +4 -6
  95. package/src/chat/backend.ts +8 -0
  96. package/src/chat/background-work.ts +2 -2
  97. package/src/chat/chat-core.ts +221 -143
  98. package/src/chat/chat-log.ts +3 -3
  99. package/src/chat/chat-process.ts +128 -8
  100. package/src/chat/chat-session.ts +153 -204
  101. package/src/chat/chat-store.ts +104 -1
  102. package/src/chat/claude-backend.ts +58 -26
  103. package/src/chat/claude-protocol.ts +147 -8
  104. package/src/chat/claude-title.ts +11 -39
  105. package/src/chat/claude-transcript.ts +38 -18
  106. package/src/chat/codex-backend.ts +182 -20
  107. package/src/chat/codex-protocol.ts +40 -5
  108. package/src/chat/codex-thread.ts +1 -4
  109. package/src/chat/codex-transport.ts +45 -35
  110. package/src/chat/context-breakdown.ts +2 -2
  111. package/src/chat/errors.ts +1 -0
  112. package/src/chat/external-sessions.ts +2 -2
  113. package/src/chat/fixtures/claude-2.1.296-goal.json +136 -0
  114. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
  115. package/src/chat/index.ts +49 -0
  116. package/src/chat/projector.ts +13 -4
  117. package/src/chat/request-summary.ts +8 -5
  118. package/src/chat/subagent-settlement.ts +1 -4
  119. package/src/chat/thread.ts +22 -3
  120. package/src/chat/ui-queries.ts +46 -5
  121. package/src/chat/ui-replies.ts +187 -0
  122. package/src/context/index.ts +26 -0
  123. package/src/coordination/index.ts +67 -0
  124. package/src/host/handlers.ts +8 -0
  125. package/src/host/index.ts +8 -0
  126. package/src/index.ts +20 -0
  127. package/src/providers/accounts/status.ts +1 -4
  128. package/src/providers/claude.ts +3 -1
  129. package/src/providers/codex.ts +3 -1
  130. package/src/providers/index.ts +15 -0
  131. package/src/run-process.ts +56 -16
  132. package/src/skills.ts +5 -0
  133. package/src/tasks/task-coordinator.ts +22 -2
  134. package/src/tasks/wiring.ts +7 -1
  135. package/src/{providers/accounts/test-accounts.ts → testing/accounts.ts} +4 -4
  136. package/src/testing/bin/fake-claude.ts +7 -0
  137. package/src/testing/bin/fake-codex.ts +7 -0
  138. package/src/{chat → testing}/fake-claude.ts +1 -5
  139. package/src/{chat → testing}/fake-cli.ts +1 -1
  140. package/src/{chat → testing}/fake-codex.ts +75 -6
  141. package/src/testing/index.ts +7 -0
  142. package/src/{outbox → testing}/manual-clock.ts +1 -1
  143. package/src/{watch-test-helpers.ts → testing/watch.ts} +1 -1
  144. package/src/usage/exchange.ts +31 -20
  145. package/src/usage/index-file.ts +23 -2
  146. package/src/usage/index.ts +10 -0
  147. package/src/usage/limits/normalize.ts +1 -4
  148. package/src/usage/pricing.ts +34 -23
  149. package/src/usage/scanner.ts +48 -11
  150. package/src/usage/usage-service.ts +8 -3
  151. /package/dist/{chat → testing}/fake-cli.js +0 -0
  152. /package/dist/{outbox → testing}/manual-clock.js +0 -0
  153. /package/dist/{watch-test-helpers.js → testing/watch.js} +0 -0
@@ -26,7 +26,7 @@ export class ExchangeRates {
26
26
  home;
27
27
  allowFetch;
28
28
  rate = null;
29
- loadedFromDisk = false;
29
+ stored = null;
30
30
  inFlight = null;
31
31
  constructor(home, allowFetch = true) {
32
32
  this.home = home;
@@ -35,9 +35,16 @@ export class ExchangeRates {
35
35
  get current() {
36
36
  return this.rate;
37
37
  }
38
- /* Refreshes at most one rate at a time; a second caller waits for the first one's answer. */
38
+ /* Reads the rate an earlier run stored, once; asking for a new one is left to `ensure`. */
39
+ load() {
40
+ this.stored ??= this.readStored();
41
+ return this.stored;
42
+ }
43
+ /* Refreshes at most one rate at a time, a second caller waiting for the first one's answer; true when a newer rate came in. */
39
44
  ensure() {
40
- this.inFlight ??= this.refresh().finally(() => {
45
+ this.inFlight ??= this.load()
46
+ .then(() => this.fetchFresh())
47
+ .finally(() => {
41
48
  this.inFlight = null;
42
49
  });
43
50
  return this.inFlight;
@@ -45,24 +52,24 @@ export class ExchangeRates {
45
52
  get file() {
46
53
  return join(this.home, 'usage', 'exchange-rate.json');
47
54
  }
48
- async refresh() {
49
- if (!this.loadedFromDisk) {
50
- this.loadedFromDisk = true;
51
- try {
52
- const stored = JSON.parse(await readFile(this.file, 'utf8'));
53
- if (stored?.currency === TARGET_CURRENCY && typeof stored.rate === 'number' && stored.rate > 0) {
54
- this.rate = stored;
55
- }
55
+ async readStored() {
56
+ try {
57
+ const stored = JSON.parse(await readFile(this.file, 'utf8'));
58
+ if (stored?.currency === TARGET_CURRENCY && typeof stored.rate === 'number' && stored.rate > 0) {
59
+ this.rate = stored;
56
60
  }
57
- catch (e) {
58
- if (!isNotFound(e)) {
59
- console.warn('The stored exchange rate would not read; asking for a new one:', errorText(e));
60
- }
61
+ }
62
+ catch (e) {
63
+ if (!isNotFound(e)) {
64
+ console.warn('The stored exchange rate would not read; asking for a new one:', errorText(e));
61
65
  }
62
66
  }
67
+ }
68
+ async fetchFresh() {
63
69
  if (!this.allowFetch || (this.rate !== null && Date.now() - this.rate.fetchedAt < TTL_MS)) {
64
- return;
70
+ return false;
65
71
  }
72
+ let changed = false;
66
73
  try {
67
74
  const response = await fetch(`${FRANKFURTER_URL}?from=USD&to=${TARGET_CURRENCY}`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
68
75
  if (!response.ok) {
@@ -73,6 +80,7 @@ export class ExchangeRates {
73
80
  throw new Error(`The answer carried no ${TARGET_CURRENCY} rate`);
74
81
  }
75
82
  this.rate = { ...parsed, fetchedAt: Date.now() };
83
+ changed = true;
76
84
  await mkdir(join(this.home, 'usage'), { recursive: true, mode: 0o700 });
77
85
  await writeAtomic(this.file, JSON.stringify(this.rate));
78
86
  }
@@ -80,5 +88,6 @@ export class ExchangeRates {
80
88
  // Yesterday's rate still converts; a rate that could not be refreshed is not a failure.
81
89
  console.warn('Could not refresh the exchange rate:', errorText(e));
82
90
  }
91
+ return changed;
83
92
  }
84
93
  }
@@ -9,6 +9,7 @@ export interface IndexedFile {
9
9
  records: UsageRecord[];
10
10
  tail: UsageRecord[];
11
11
  codex: CodexParserState | null;
12
+ head: string | null;
12
13
  }
13
14
  export type UsageIndex = Map<string, IndexedFile>;
14
15
  export declare function encodeIndex(index: UsageIndex): string;
@@ -38,7 +38,16 @@ export function encodeIndex(index) {
38
38
  record.account === undefined ? 0 : accounts.index(record.account) + 1
39
39
  ];
40
40
  for (const [path, file] of index) {
41
- files[path] = { p: file.provider, s: file.size, m: file.mtimeMs, o: file.offset, c: file.codex, r: file.records.map(row), t: file.tail.map(row) };
41
+ files[path] = {
42
+ p: file.provider,
43
+ s: file.size,
44
+ m: file.mtimeMs,
45
+ o: file.offset,
46
+ c: file.codex,
47
+ r: file.records.map(row),
48
+ t: file.tail.map(row),
49
+ ...(file.head === null ? {} : { h: file.head })
50
+ };
42
51
  }
43
52
  const document = {
44
53
  version: INDEX_VERSION,
@@ -116,7 +125,16 @@ export function decodeIndex(text) {
116
125
  const records = decode(file.r);
117
126
  const tail = decode(Array.isArray(file.t) ? file.t : []);
118
127
  if (!broken) {
119
- index.set(path, { provider: file.p, size: file.s, mtimeMs: file.m, offset: file.o, records, tail, codex: file.c ?? null });
128
+ index.set(path, {
129
+ provider: file.p,
130
+ size: file.s,
131
+ mtimeMs: file.m,
132
+ offset: file.o,
133
+ records,
134
+ tail,
135
+ codex: file.c ?? null,
136
+ head: typeof file.h === 'string' ? file.h : null
137
+ });
120
138
  }
121
139
  }
122
140
  return index;
@@ -0,0 +1,9 @@
1
+ export { usageAccountsOf, usageRootsOf, limitAccountsOf, chatSessionAccounts } from './accounts.ts';
2
+ export { UsageMonitor, type ProbeEnv, type LimitAccount, type LimitAccounts, type UsageMonitorOptions } from './limits/monitor.ts';
3
+ export type { LimitsUpdate, ProviderReading } from './limits/normalize.ts';
4
+ export type { ProbeResult } from './limits/probe.ts';
5
+ export { LITELLM_URL } from './pricing.ts';
6
+ export type { KnownProject } from './projects.ts';
7
+ export { type UsageRootPath, usageRoots } from './roots.ts';
8
+ export type { UsageScannerOptions } from './scanner.ts';
9
+ export { UsageService, type UsageServiceOptions } from './usage-service.ts';
@@ -0,0 +1,6 @@
1
+ // Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
2
+ export { usageAccountsOf, usageRootsOf, limitAccountsOf, chatSessionAccounts } from './accounts.js';
3
+ export { UsageMonitor } from './limits/monitor.js';
4
+ export { LITELLM_URL } from './pricing.js';
5
+ export { usageRoots } from './roots.js';
6
+ export { UsageService } from './usage-service.js';
@@ -1,10 +1,8 @@
1
+ import { isRecord } from '@adecore/agent-contracts';
1
2
  const MINUTE_MS = 60_000;
2
3
  const FIVE_HOURS_MS = 5 * 60 * MINUTE_MS;
3
4
  const WEEK_MS = 7 * 24 * 60 * MINUTE_MS;
4
5
  const MONTH_MS = 30 * 24 * 60 * MINUTE_MS;
5
- function isRecord(value) {
6
- return typeof value === 'object' && value !== null && !Array.isArray(value);
7
- }
8
6
  function num(value) {
9
7
  return typeof value === 'number' && Number.isFinite(value) ? value : null;
10
8
  }
@@ -23,12 +23,14 @@ export declare class PriceBook {
23
23
  private table;
24
24
  private source;
25
25
  private fetchedAt;
26
- private loadedFromDisk;
26
+ private stored;
27
27
  private inFlight;
28
28
  constructor(home: string, allowFetch?: boolean);
29
29
  get pricing(): UsagePricing;
30
30
  look(model: string): PriceLookup;
31
- ensure(): Promise<void>;
31
+ load(): Promise<void>;
32
+ ensure(): Promise<boolean>;
32
33
  private get file();
33
- private refresh;
34
+ private readStored;
35
+ private fetchFresh;
34
36
  }
@@ -118,7 +118,7 @@ export class PriceBook {
118
118
  table;
119
119
  source = 'snapshot';
120
120
  fetchedAt = null;
121
- loadedFromDisk = false;
121
+ stored = null;
122
122
  inFlight = null;
123
123
  constructor(home, allowFetch = true) {
124
124
  this.home = home;
@@ -131,9 +131,16 @@ export class PriceBook {
131
131
  look(model) {
132
132
  return lookupPrice(this.table, model);
133
133
  }
134
- /* Refreshes at most one table at a time; a second caller waits for the first one's answer. */
134
+ /* Reads the table an earlier run stored, once; asking LiteLLM is left to `ensure`. */
135
+ load() {
136
+ this.stored ??= this.readStored();
137
+ return this.stored;
138
+ }
139
+ /* Refreshes at most one table at a time, a second caller waiting for the first one's answer; true when a newer table came in. */
135
140
  ensure() {
136
- this.inFlight ??= this.refresh().finally(() => {
141
+ this.inFlight ??= this.load()
142
+ .then(() => this.fetchFresh())
143
+ .finally(() => {
137
144
  this.inFlight = null;
138
145
  });
139
146
  return this.inFlight;
@@ -141,27 +148,27 @@ export class PriceBook {
141
148
  get file() {
142
149
  return join(this.home, 'usage', 'prices.json');
143
150
  }
144
- async refresh() {
145
- if (!this.loadedFromDisk) {
146
- this.loadedFromDisk = true;
147
- try {
148
- const snapshot = JSON.parse(await readFile(this.file, 'utf8'));
149
- const table = parsePriceTable(snapshot?.document);
150
- if (table.size > 0 && typeof snapshot.fetchedAt === 'number') {
151
- this.table = table;
152
- this.source = 'litellm';
153
- this.fetchedAt = snapshot.fetchedAt;
154
- }
151
+ async readStored() {
152
+ try {
153
+ const snapshot = JSON.parse(await readFile(this.file, 'utf8'));
154
+ const table = parsePriceTable(snapshot?.document);
155
+ if (table.size > 0 && typeof snapshot.fetchedAt === 'number') {
156
+ this.table = table;
157
+ this.source = 'litellm';
158
+ this.fetchedAt = snapshot.fetchedAt;
155
159
  }
156
- catch (e) {
157
- if (!isNotFound(e)) {
158
- console.warn('The stored price table would not read; using the bundled one:', errorText(e));
159
- }
160
+ }
161
+ catch (e) {
162
+ if (!isNotFound(e)) {
163
+ console.warn('The stored price table would not read; using the bundled one:', errorText(e));
160
164
  }
161
165
  }
166
+ }
167
+ async fetchFresh() {
162
168
  if (!this.allowFetch || (this.fetchedAt !== null && Date.now() - this.fetchedAt < TTL_MS)) {
163
- return;
169
+ return false;
164
170
  }
171
+ let changed = false;
165
172
  try {
166
173
  const response = await fetch(LITELLM_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
167
174
  if (!response.ok) {
@@ -175,6 +182,7 @@ export class PriceBook {
175
182
  this.table = table;
176
183
  this.source = 'litellm';
177
184
  this.fetchedAt = Date.now();
185
+ changed = true;
178
186
  await mkdir(join(this.home, 'usage'), { recursive: true, mode: 0o700 });
179
187
  await writeAtomic(this.file, JSON.stringify({ fetchedAt: this.fetchedAt, document }));
180
188
  }
@@ -182,5 +190,6 @@ export class PriceBook {
182
190
  // Whatever is loaded keeps pricing; a table that could not be refreshed is not a failure.
183
191
  console.warn('Could not refresh the price table:', errorText(e));
184
192
  }
193
+ return changed;
185
194
  }
186
195
  }
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { mkdir, open, readFile, readdir, stat } from 'node:fs/promises';
2
3
  import { join } from 'node:path';
3
4
  import { errorText } from '../error-text.js';
@@ -7,26 +8,46 @@ import { foldByKey } from './record.js';
7
8
  import { claudeMightCarryUsage, parseClaudeLine } from './readers/claude.js';
8
9
  import { cloneCodexState, codexMightCarryUsage, createCodexState, parseCodexLine } from './readers/codex.js';
9
10
  import { usageRoots } from './roots.js';
10
- /* What a file holds from `from` to its end, as it is at the read. */
11
- async function readFrom(path, from) {
11
+ const HASH_CHUNK_BYTES = 65536;
12
+ /*
13
+ * Hashes the bytes before `from` and returns the rest, in one pass over one handle, so verifying
14
+ * what a transcript committed and parsing what it added never reads a byte twice.
15
+ */
16
+ async function readHashed(path, from) {
12
17
  const handle = await open(path, 'r');
13
18
  try {
19
+ const hash = createHash('sha256');
20
+ const chunk = Buffer.alloc(Math.min(from, HASH_CHUNK_BYTES));
21
+ let offset = 0;
22
+ while (offset < from) {
23
+ const { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, from - offset), offset);
24
+ if (bytesRead === 0) {
25
+ break;
26
+ }
27
+ hash.update(chunk.subarray(0, bytesRead));
28
+ offset += bytesRead;
29
+ }
14
30
  const { size } = await handle.stat();
15
- const buffer = Buffer.alloc(Math.max(0, size - from));
31
+ const bytes = Buffer.alloc(Math.max(0, size - from));
16
32
  let filled = 0;
17
- while (filled < buffer.length) {
18
- const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, from + filled);
33
+ while (filled < bytes.length) {
34
+ const { bytesRead } = await handle.read(bytes, filled, bytes.length - filled, from + filled);
19
35
  if (bytesRead === 0) {
20
36
  break;
21
37
  }
22
38
  filled += bytesRead;
23
39
  }
24
- return buffer.subarray(0, filled).toString('utf8');
40
+ return { bytes: bytes.subarray(0, filled), hash };
25
41
  }
26
42
  finally {
27
43
  await handle.close();
28
44
  }
29
45
  }
46
+ /* Covers every committed byte, since a transcript can be rewritten after an unchanged session header. */
47
+ function headOf(hash) {
48
+ // Older indexes hashed only the first 4 KB; they cannot establish that the rest stayed unchanged.
49
+ return `sha256:${hash.copy().digest('base64')}`;
50
+ }
30
51
  async function listJsonl(dir, out = []) {
31
52
  let entries;
32
53
  try {
@@ -111,7 +132,7 @@ function withAccount(records, accountOf) {
111
132
  });
112
133
  }
113
134
  /*
114
- * Reads only what a transcript appended since the last scan, and starts a file over once it was
135
+ * Parses only what a transcript appended since the last scan, and starts a file over once it was
115
136
  * rewritten or cut. A deleted transcript keeps its totals, since its usage still happened.
116
137
  */
117
138
  export class UsageScanner {
@@ -210,10 +231,16 @@ export class UsageScanner {
210
231
  if (known !== undefined && known.provider === provider && known.size === info.size && known.mtimeMs === info.mtimeMs) {
211
232
  return false;
212
233
  }
213
- const grown = known !== undefined && known.provider === provider && info.size > known.size;
234
+ const appended = known !== undefined && known.provider === provider && info.size > known.size && known.head?.startsWith('sha256:') === true;
235
+ let read = await readHashed(path, appended ? known.offset : 0);
236
+ // A file only counts as grown while it starts the way it did; one rewritten larger is read again from the start.
237
+ const grown = appended && headOf(read.hash) === known.head;
238
+ if (appended && !grown) {
239
+ read = await readHashed(path, 0);
240
+ }
214
241
  const from = grown ? known.offset : 0;
215
- const text = await readFrom(path, from);
216
- const parsed = parseChunk(text, provider, from, grown ? known.codex : null);
242
+ const parsed = parseChunk(read.bytes.toString('utf8'), provider, from, grown ? known.codex : null);
243
+ read.hash.update(read.bytes.subarray(0, parsed.offset - from));
217
244
  const before = grown ? known.records : [];
218
245
  const entry = {
219
246
  provider,
@@ -223,7 +250,8 @@ export class UsageScanner {
223
250
  // Folding here keeps the streaming copies of one message out of the index entirely.
224
251
  records: foldByKey([...before, ...withAccount(parsed.records, accountOf)]),
225
252
  tail: withAccount(parsed.tail, accountOf),
226
- codex: parsed.codex
253
+ codex: parsed.codex,
254
+ head: headOf(read.hash)
227
255
  };
228
256
  this.index.set(path, entry);
229
257
  this.dirty = true;
@@ -105,14 +105,19 @@ export class UsageService {
105
105
  return this.inFlight;
106
106
  }
107
107
  async runScan() {
108
- // The table and the rate are fetched beside the walk, so a slow answer never holds up the scan.
108
+ // A fetch never holds up the scan or the answer waiting on it; a table or rate it brings in is announced on its own.
109
+ void Promise.all([this.prices.ensure(), this.rates.ensure()]).then((fetched) => {
110
+ if (fetched.includes(true)) {
111
+ this.sinks.emit({ event: 'usage.changed', payload: { scannedAt: this.report.at } });
112
+ }
113
+ });
109
114
  const [report] = await Promise.all([
110
115
  this.scanner.scan().catch((e) => {
111
116
  console.error('The usage scan failed:', errorText(e));
112
117
  return null;
113
118
  }),
114
- this.prices.ensure(),
115
- this.rates.ensure()
119
+ this.prices.load(),
120
+ this.rates.load()
116
121
  ]);
117
122
  this.failed = report === null;
118
123
  if (report === null) {
package/package.json CHANGED
@@ -1,17 +1,78 @@
1
1
  {
2
2
  "name": "@adecore/agents",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
5
5
  "type": "module",
6
6
  "exports": {
7
- "./*.json": {
8
- "source": "./src/*.json",
9
- "default": "./dist/*.json"
10
- },
11
- "./*": {
12
- "source": "./src/*.ts",
13
- "types": "./dist/*.d.ts",
14
- "default": "./dist/*.js"
7
+ ".": {
8
+ "source": "./src/index.ts",
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./chat": {
13
+ "source": "./src/chat/index.ts",
14
+ "types": "./dist/chat/index.d.ts",
15
+ "default": "./dist/chat/index.js"
16
+ },
17
+ "./context": {
18
+ "source": "./src/context/index.ts",
19
+ "types": "./dist/context/index.d.ts",
20
+ "default": "./dist/context/index.js"
21
+ },
22
+ "./coordination": {
23
+ "source": "./src/coordination/index.ts",
24
+ "types": "./dist/coordination/index.d.ts",
25
+ "default": "./dist/coordination/index.js"
26
+ },
27
+ "./host": {
28
+ "source": "./src/host/index.ts",
29
+ "types": "./dist/host/index.d.ts",
30
+ "default": "./dist/host/index.js"
31
+ },
32
+ "./providers": {
33
+ "source": "./src/providers/index.ts",
34
+ "types": "./dist/providers/index.d.ts",
35
+ "default": "./dist/providers/index.js"
36
+ },
37
+ "./usage": {
38
+ "source": "./src/usage/index.ts",
39
+ "types": "./dist/usage/index.d.ts",
40
+ "default": "./dist/usage/index.js"
41
+ },
42
+ "./testing": {
43
+ "source": "./src/testing/index.ts",
44
+ "types": "./dist/testing/index.d.ts",
45
+ "default": "./dist/testing/index.js"
46
+ },
47
+ "./testing/bin/fake-claude": {
48
+ "source": "./src/testing/bin/fake-claude.ts",
49
+ "types": "./dist/testing/bin/fake-claude.d.ts",
50
+ "default": "./dist/testing/bin/fake-claude.js"
51
+ },
52
+ "./testing/bin/fake-codex": {
53
+ "source": "./src/testing/bin/fake-codex.ts",
54
+ "types": "./dist/testing/bin/fake-codex.d.ts",
55
+ "default": "./dist/testing/bin/fake-codex.js"
56
+ },
57
+ "./providers/claude-models.json": {
58
+ "source": "./src/providers/claude-models.json",
59
+ "default": "./dist/providers/claude-models.json"
60
+ },
61
+ "./providers/codex-models.json": {
62
+ "source": "./src/providers/codex-models.json",
63
+ "default": "./dist/providers/codex-models.json"
64
+ },
65
+ "./usage/prices-snapshot.json": {
66
+ "source": "./src/usage/prices-snapshot.json",
67
+ "default": "./dist/usage/prices-snapshot.json"
68
+ },
69
+ "./testing/fixtures/*": {
70
+ "source": "./src/chat/fixtures/*",
71
+ "default": "./dist/chat/fixtures/*"
72
+ },
73
+ "./package.json": {
74
+ "source": "./package.json",
75
+ "default": "./package.json"
15
76
  }
16
77
  },
17
78
  "scripts": {
@@ -20,8 +81,8 @@
20
81
  "test": "bun --conditions=source test"
21
82
  },
22
83
  "dependencies": {
23
- "@adecore/agent-contracts": "0.23.0",
24
- "@adecore/intelligent-ui": "0.23.0",
84
+ "@adecore/agent-contracts": "0.24.0",
85
+ "@adecore/intelligent-ui": "0.24.0",
25
86
  "zod": "^4.6.5"
26
87
  },
27
88
  "devDependencies": {
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import { link, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { extname, join } from 'node:path';
4
- import { attachmentImageMime, type ChatAttachment, type ChatAttachmentUpload } from '@adecore/agent-contracts';
4
+ import { isRecord, attachmentImageMime, type ChatAttachment, type ChatAttachmentUpload } from '@adecore/agent-contracts';
5
5
 
6
6
  // The extension the file gets on disk when its name does not carry a usable one.
7
7
  const EXTENSIONS: Record<string, string> = {
@@ -100,14 +100,12 @@ export class AttachmentStore {
100
100
 
101
101
  /* Where the files of one chat are; a store that keeps more of a chat's files beside them writes them here. */
102
102
  folderOf(chatId: string): string {
103
- return join(this.dir, encodeURIComponent(chatId));
103
+ // `encodeURIComponent` leaves `.` and `..` as they are, and those would name this folder or the host's own.
104
+ const segment = chatId === '.' || chatId === '..' ? chatId.replaceAll('.', '%2E') : encodeURIComponent(chatId);
105
+ return join(this.dir, segment);
104
106
  }
105
107
  }
106
108
 
107
- function isRecord(value: unknown): value is Record<string, unknown> {
108
- return typeof value === 'object' && value !== null && !Array.isArray(value);
109
- }
110
-
111
109
  // What an attachment looked like before the files moved out of the thread: the bytes inline.
112
110
  function isInline(value: unknown): value is { name: string; mediaType: string; data: string } {
113
111
  return isRecord(value) && typeof value.name === 'string' && typeof value.mediaType === 'string' && typeof value.data === 'string';
@@ -2,6 +2,8 @@ import type {
2
2
  ChatAttachment,
3
3
  ChatAttachmentUpload,
4
4
  ChatFileChange,
5
+ ChatGoal,
6
+ ChatGoalAction,
5
7
  ChatQuestion,
6
8
  ChatSkill,
7
9
  ChatSubagentUsage,
@@ -38,6 +40,8 @@ export interface BackendLaunch {
38
40
  // The part of it a resumed thread has to hear again in front of its next prompt, for a CLI that ignores new
39
41
  // instructions on a resume; null when there is nothing to repeat.
40
42
  resumeNote: string | null;
43
+ // The goal the chat last knew of, which a CLI that takes it up again from its own transcript on a resume still works toward.
44
+ goal?: ChatGoal;
41
45
  // Folders outside `cwd` the agent may reach without asking. Claude Code takes them as `--add-dir`; Codex's
42
46
  // sandbox reads the whole disk already and keeps writes to the workspace, so it needs nothing.
43
47
  folders?: readonly string[];
@@ -158,6 +162,8 @@ export type BackendEvent =
158
162
  // What the CLI said in passing about the plan it runs on; it belongs to the machine, not the chat.
159
163
  | { type: 'limits'; update: LimitsUpdate }
160
164
  | { type: 'compaction'; preTokens: number | null }
165
+ // The goal the CLI works toward as it stands now, whole; null once there is none.
166
+ | { type: 'goal'; goal: ChatGoal | null }
161
167
  | { type: 'model'; model: string }
162
168
  | { type: 'note'; level: 'info' | 'warning' | 'error'; text: string }
163
169
  // The CLI answered a `stopTask` with an error, so the task still runs.
@@ -196,6 +202,8 @@ export interface ChatBackend {
196
202
  // False when nothing waits under that id, so the caller can answer the client with an error.
197
203
  respondApproval(requestId: string, decision: ApprovalDecision, message?: string): boolean;
198
204
  respondQuestion(requestId: string, answers: Record<string, string>): boolean;
205
+ // Pauses, resumes or clears the goal without a turn, for a protocol that has calls for it; what changed arrives as a `goal` event.
206
+ goal?(action: ChatGoalAction): Promise<void>;
199
207
  // Ends one command or monitor the CLI runs beside its turns, for a protocol that can name one.
200
208
  stopTask?(taskId: string): void;
201
209
  // Turns down a request the CLI still holds after a person stopped the turn, for a protocol that leaves one open past it.
@@ -1,4 +1,4 @@
1
- import type { AgentKind, ChatBackgroundTask, ChatItem, ChatSubagentItem, ChatToolItem } from '@adecore/agent-contracts';
1
+ import { HOST_TASK_ORIGIN, type AgentKind, type ChatBackgroundTask, type ChatItem, type ChatSubagentItem, type ChatToolItem } from '@adecore/agent-contracts';
2
2
 
3
3
  export type BackgroundWork = ChatSubagentItem | ChatToolItem;
4
4
 
@@ -10,7 +10,7 @@ export type BackgroundWork = ChatSubagentItem | ChatToolItem;
10
10
  * work it delegated itself (origin `ruimte` on the wire) is that work's own.
11
11
  */
12
12
  export function isBackgroundWork(item: ChatItem): item is BackgroundWork {
13
- return (item.kind === 'subagent' && item.background && item.origin !== 'ruimte') || (item.kind === 'tool' && item.name === 'Workflow');
13
+ return (item.kind === 'subagent' && item.background && item.origin !== HOST_TASK_ORIGIN) || (item.kind === 'tool' && item.name === 'Workflow');
14
14
  }
15
15
 
16
16
  export function runsInBackground(item: BackgroundWork): boolean {