@adecore/agents 0.21.0 → 0.21.2

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 (105) hide show
  1. package/dist/async.js +3 -8
  2. package/dist/chat/chat-core.d.ts +7 -3
  3. package/dist/chat/chat-core.js +19 -9
  4. package/dist/chat/chat-log.js +2 -2
  5. package/dist/chat/chat-session.d.ts +13 -0
  6. package/dist/chat/chat-session.js +75 -5
  7. package/dist/chat/chat-store.d.ts +3 -2
  8. package/dist/chat/chat-store.js +44 -16
  9. package/dist/chat/chat-title.js +4 -8
  10. package/dist/chat/claude-backend.d.ts +1 -0
  11. package/dist/chat/claude-backend.js +8 -6
  12. package/dist/chat/claude-title.js +1 -1
  13. package/dist/chat/codex-backend.d.ts +2 -2
  14. package/dist/chat/codex-backend.js +5 -8
  15. package/dist/chat/codex-protocol.js +1 -1
  16. package/dist/chat/codex-thread.js +4 -5
  17. package/dist/chat/composer-preferences.js +2 -3
  18. package/dist/chat/limit-resume.js +2 -7
  19. package/dist/chat/readable-text.d.ts +2 -0
  20. package/dist/chat/readable-text.js +16 -0
  21. package/dist/chat/ui-queries.d.ts +12 -1
  22. package/dist/chat/ui-queries.js +107 -60
  23. package/dist/chat/visual-store.js +8 -6
  24. package/dist/context/refusal.js +3 -7
  25. package/dist/context/verb.js +1 -5
  26. package/dist/host/agent-host.js +2 -2
  27. package/dist/host/handlers.js +1 -2
  28. package/dist/host/wiring.js +1 -1
  29. package/dist/lineage.js +2 -3
  30. package/dist/messages/deliver-message.js +6 -11
  31. package/dist/messages/deliver-notice.js +8 -12
  32. package/dist/messages/notice-store.js +3 -5
  33. package/dist/outbox/outbox-worker.js +5 -2
  34. package/dist/providers/accounts/service.d.ts +2 -1
  35. package/dist/providers/accounts/service.js +11 -13
  36. package/dist/providers/claude.js +0 -1
  37. package/dist/providers/detect.js +2 -10
  38. package/dist/record-directory.js +0 -1
  39. package/dist/tasks/task-coordinator.d.ts +1 -0
  40. package/dist/tasks/task-coordinator.js +40 -36
  41. package/dist/tasks/task-store.js +1 -1
  42. package/dist/tasks/task-work.js +2 -2
  43. package/dist/tasks/waiting-child.js +5 -3
  44. package/dist/tasks/wake-parent.js +15 -10
  45. package/dist/tasks/wiring.js +32 -37
  46. package/dist/usage/aggregate.js +62 -46
  47. package/dist/usage/exchange.js +1 -1
  48. package/dist/usage/limits/monitor.d.ts +2 -2
  49. package/dist/usage/limits/monitor.js +2 -2
  50. package/dist/usage/limits/probe.js +1 -1
  51. package/dist/usage/pricing.js +1 -1
  52. package/dist/usage/roots.js +1 -2
  53. package/dist/usage/scanner.js +7 -9
  54. package/dist/usage/usage-service.d.ts +1 -2
  55. package/dist/usage/usage-service.js +3 -6
  56. package/package.json +3 -3
  57. package/src/async.ts +3 -9
  58. package/src/chat/chat-core.ts +29 -9
  59. package/src/chat/chat-log.ts +2 -2
  60. package/src/chat/chat-session.ts +85 -5
  61. package/src/chat/chat-store.ts +54 -16
  62. package/src/chat/chat-title.ts +4 -8
  63. package/src/chat/claude-backend.ts +9 -6
  64. package/src/chat/claude-title.ts +1 -1
  65. package/src/chat/claude-transcript.ts +1 -1
  66. package/src/chat/codex-backend.ts +5 -8
  67. package/src/chat/codex-protocol.ts +1 -1
  68. package/src/chat/codex-thread.ts +6 -5
  69. package/src/chat/composer-preferences.ts +2 -3
  70. package/src/chat/limit-resume.ts +2 -7
  71. package/src/chat/readable-text.ts +18 -0
  72. package/src/chat/thread.ts +1 -1
  73. package/src/chat/ui-queries.ts +120 -64
  74. package/src/chat/visual-store.ts +9 -6
  75. package/src/chat/wake-chat.ts +4 -1
  76. package/src/context/refusal.ts +3 -7
  77. package/src/context/verb.ts +1 -5
  78. package/src/host/agent-host.ts +2 -2
  79. package/src/host/handlers.ts +1 -2
  80. package/src/host/wiring.ts +1 -1
  81. package/src/lineage.ts +2 -3
  82. package/src/messages/deliver-message.ts +6 -11
  83. package/src/messages/deliver-notice.ts +8 -12
  84. package/src/messages/notice-store.ts +3 -5
  85. package/src/outbox/outbox-worker.ts +5 -2
  86. package/src/providers/accounts/service.ts +12 -13
  87. package/src/providers/accounts/variables.ts +1 -0
  88. package/src/providers/claude.ts +0 -1
  89. package/src/providers/detect.ts +2 -11
  90. package/src/record-directory.ts +0 -1
  91. package/src/skills.ts +0 -1
  92. package/src/tasks/task-coordinator.ts +41 -35
  93. package/src/tasks/task-store.ts +1 -1
  94. package/src/tasks/task-work.ts +2 -2
  95. package/src/tasks/waiting-child.ts +6 -3
  96. package/src/tasks/wake-parent.ts +21 -16
  97. package/src/tasks/wiring.ts +33 -41
  98. package/src/usage/aggregate.ts +80 -50
  99. package/src/usage/exchange.ts +1 -1
  100. package/src/usage/limits/monitor.ts +4 -4
  101. package/src/usage/limits/probe.ts +2 -2
  102. package/src/usage/pricing.ts +1 -1
  103. package/src/usage/roots.ts +1 -2
  104. package/src/usage/scanner.ts +8 -10
  105. package/src/usage/usage-service.ts +5 -9
@@ -9,7 +9,7 @@ import type {
9
9
  UsageSummaryPayload,
10
10
  UsageTotals
11
11
  } from '@adecore/agent-contracts';
12
- import { cacheSavingsOf, costOf, type PriceBook } from './pricing.ts';
12
+ import { cacheSavingsOf, costOf, type ModelPrice, type PriceBook, type PriceLookup } from './pricing.ts';
13
13
  import { ProjectResolver, type KnownProject } from './projects.ts';
14
14
  import type { UsageRecord } from './record.ts';
15
15
 
@@ -80,6 +80,75 @@ interface ProjectAccumulator {
80
80
  costUsd: number;
81
81
  }
82
82
 
83
+ interface ModelRow extends ModelAccumulator {
84
+ /* The basis of the first record of a model prices the whole row: a model has one price. */
85
+ basis: UsagePriceBasis;
86
+ pricedAs: string | null;
87
+ }
88
+
89
+ function foldBucket(
90
+ buckets: Map<string, BucketAccumulator>,
91
+ slot: string,
92
+ record: UsageRecord,
93
+ account: string | null,
94
+ price: ModelPrice | null,
95
+ cost: number
96
+ ): void {
97
+ const key = `${slot}\0${record.provider}\0${record.model}\0${account ?? ''}`;
98
+ let bucket = buckets.get(key);
99
+ if (bucket === undefined) {
100
+ bucket = {
101
+ slot,
102
+ provider: record.provider,
103
+ model: record.model,
104
+ account,
105
+ totals: emptyTotals(),
106
+ costUsd: 0,
107
+ priced: false,
108
+ cacheSavingsUsd: 0,
109
+ sessions: new Set()
110
+ };
111
+ buckets.set(key, bucket);
112
+ }
113
+ bucket.totals = addTotals(bucket.totals, record.totals);
114
+ bucket.costUsd += cost;
115
+ bucket.priced ||= price !== null;
116
+ bucket.cacheSavingsUsd += price === null ? 0 : cacheSavingsOf(record.totals, price);
117
+ if (record.sessionId !== '') {
118
+ bucket.sessions.add(record.sessionId);
119
+ }
120
+ }
121
+
122
+ function foldModel(models: Map<string, ModelRow>, record: UsageRecord, account: string | null, lookup: PriceLookup, cost: number): void {
123
+ const key = `${record.provider}\0${record.model}\0${account ?? ''}`;
124
+ let model = models.get(key);
125
+ if (model === undefined) {
126
+ model = {
127
+ provider: record.provider,
128
+ model: record.model,
129
+ account,
130
+ totals: emptyTotals(),
131
+ costUsd: 0,
132
+ priced: false,
133
+ basis: lookup.basis,
134
+ pricedAs: lookup.pricedAs
135
+ };
136
+ models.set(key, model);
137
+ }
138
+ model.totals = addTotals(model.totals, record.totals);
139
+ model.costUsd += cost;
140
+ model.priced ||= lookup.price !== null;
141
+ }
142
+
143
+ function foldProject(project: ProjectAccumulator, record: UsageRecord, cost: number): void {
144
+ project.totals = addTotals(project.totals, record.totals);
145
+ project.costUsd += cost;
146
+ const share = project.byProvider.get(record.provider) ?? { costUsd: 0, tokens: 0 };
147
+ share.costUsd += cost;
148
+ share.tokens += totalTokensOf(record.totals);
149
+ project.byProvider.set(record.provider, share);
150
+ }
151
+
83
152
  /* The account a record was made under, the default one under its CLI's kind. */
84
153
  export function accountOfRecord(record: UsageRecord): string {
85
154
  return record.account ?? record.provider;
@@ -98,9 +167,7 @@ export async function aggregate(
98
167
  ): Promise<Aggregation> {
99
168
  const format = slotFormatter(payload.timeZone, payload.resolution);
100
169
  const buckets = new Map<string, BucketAccumulator>();
101
- const models = new Map<string, ModelAccumulator>();
102
- /* The basis of the first record of a model prices the whole row: a model has one price. */
103
- const modelBasis = new Map<string, { basis: UsagePriceBasis; pricedAs: string | null }>();
170
+ const models = new Map<string, ModelRow>();
104
171
  const projects = new Map<string, ProjectAccumulator>();
105
172
  const sessions = new Set<string>();
106
173
  const resolver = new ProjectResolver(known);
@@ -118,45 +185,14 @@ export async function aggregate(
118
185
  if (day < payload.from || day > payload.to) {
119
186
  continue;
120
187
  }
121
- const { price, basis, pricedAs } = prices.look(record.model);
122
- const cost = price === null ? 0 : costOf(record.totals, price);
123
-
124
- const bucketKey = `${slot}\0${record.provider}\0${record.model}\0${account ?? ''}`;
125
- let bucket = buckets.get(bucketKey);
126
- if (bucket === undefined) {
127
- bucket = {
128
- slot,
129
- provider: record.provider,
130
- model: record.model,
131
- account,
132
- totals: emptyTotals(),
133
- costUsd: 0,
134
- priced: false,
135
- cacheSavingsUsd: 0,
136
- sessions: new Set()
137
- };
138
- buckets.set(bucketKey, bucket);
139
- }
140
- bucket.totals = addTotals(bucket.totals, record.totals);
141
- bucket.costUsd += cost;
142
- bucket.priced ||= price !== null;
143
- bucket.cacheSavingsUsd += price === null ? 0 : cacheSavingsOf(record.totals, price);
188
+ const lookup = prices.look(record.model);
189
+ const cost = lookup.price === null ? 0 : costOf(record.totals, lookup.price);
190
+ foldBucket(buckets, slot, record, account, lookup.price, cost);
191
+ foldModel(models, record, account, lookup, cost);
144
192
  if (record.sessionId !== '') {
145
- bucket.sessions.add(record.sessionId);
146
193
  sessions.add(`${record.provider}\0${record.sessionId}`);
147
194
  }
148
195
 
149
- const modelKey = `${record.provider}\0${record.model}\0${account ?? ''}`;
150
- let model = models.get(modelKey);
151
- if (model === undefined) {
152
- model = { provider: record.provider, model: record.model, account, totals: emptyTotals(), costUsd: 0, priced: false };
153
- models.set(modelKey, model);
154
- modelBasis.set(modelKey, { basis, pricedAs });
155
- }
156
- model.totals = addTotals(model.totals, record.totals);
157
- model.costUsd += cost;
158
- model.priced ||= price !== null;
159
-
160
196
  let folder = folders.get(record.cwd);
161
197
  if (folder === undefined) {
162
198
  const resolved = await resolver.resolve(record.cwd);
@@ -166,13 +202,7 @@ export async function aggregate(
166
202
  projects.set(folder, { folder, name: resolved.name, projectId: resolved.projectId, byProvider: new Map(), totals: emptyTotals(), costUsd: 0 });
167
203
  }
168
204
  }
169
- const project = projects.get(folder)!;
170
- project.totals = addTotals(project.totals, record.totals);
171
- project.costUsd += cost;
172
- const share = project.byProvider.get(record.provider) ?? { costUsd: 0, tokens: 0 };
173
- share.costUsd += cost;
174
- share.tokens += totalTokensOf(record.totals);
175
- project.byProvider.set(record.provider, share);
205
+ foldProject(projects.get(folder)!, record, cost);
176
206
  }
177
207
 
178
208
  return {
@@ -194,15 +224,15 @@ export async function aggregate(
194
224
  a.model.localeCompare(b.model) ||
195
225
  (a.account ?? '').localeCompare(b.account ?? '')
196
226
  ),
197
- models: [...models.entries()]
198
- .map(([key, model]) => ({
227
+ models: [...models.values()]
228
+ .map((model) => ({
199
229
  provider: model.provider,
200
230
  model: model.model,
201
231
  ...(model.account === null ? {} : { account: model.account }),
202
232
  totals: model.totals,
203
233
  costUsd: model.priced ? model.costUsd : null,
204
- priceBasis: modelBasis.get(key)?.basis ?? 'unknown',
205
- pricedAs: modelBasis.get(key)?.pricedAs ?? null
234
+ priceBasis: model.basis,
235
+ pricedAs: model.pricedAs
206
236
  }))
207
237
  .sort((a, b) => (b.costUsd ?? -1) - (a.costUsd ?? -1)),
208
238
  projects: [...projects.values()]
@@ -1,8 +1,8 @@
1
1
  import { mkdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import type { UsageRate } from '@adecore/agent-contracts';
4
- import { isNotFound, writeAtomic } from '../fs.ts';
5
4
  import { errorText } from '../error-text.ts';
5
+ import { isNotFound, writeAtomic } from '../fs.ts';
6
6
 
7
7
  export const FRANKFURTER_URL = 'https://api.frankfurter.app/latest';
8
8
 
@@ -1,12 +1,12 @@
1
1
  import type { UsageLimitsProvider, UsageLimitsSnapshot, UsageProvider } from '@adecore/agent-contracts';
2
+ import type { CodexClientInfo } from '../../chat/codex-transport.ts';
3
+ import { ClientSinks } from '../../client-sinks.ts';
4
+ import { errorText } from '../../error-text.ts';
5
+ import type { AgentEvent, AgentSink } from '../../events.ts';
2
6
  import { definedEnv } from '../../providers/accounts/launch.ts';
3
7
  import type { ProviderRegistry } from '../../providers/registry.ts';
4
- import type { AgentEvent, AgentSink } from '../../events.ts';
5
8
  import { mergeWindows, type LimitsUpdate } from './normalize.ts';
6
- import type { CodexClientInfo } from '../../chat/codex-transport.ts';
7
9
  import { probeClaude, probeCodex, type ProbeResult } from './probe.ts';
8
- import { ClientSinks } from '../../client-sinks.ts';
9
- import { errorText } from '../../error-text.ts';
10
10
 
11
11
  /* Often enough that a bar is never far behind, rarely enough that two CLIs are not started all day. */
12
12
  const PROBE_INTERVAL_MS = 5 * 60_000;
@@ -1,9 +1,9 @@
1
- import { withTimeout } from '../../async.ts';
2
1
  import type { UsageLimitsProvider } from '@adecore/agent-contracts';
2
+ import { withTimeout } from '../../async.ts';
3
3
  import { spawnChatProcess, type ChatProcess, type SpawnChatProcess } from '../../chat/chat-process.ts';
4
4
  import { CodexTransport, DEFAULT_CODEX_CLIENT, type CodexClientInfo } from '../../chat/codex-transport.ts';
5
- import { readClaudeUsage, readCodexLimits, type ProviderReading } from './normalize.ts';
6
5
  import { errorText } from '../../error-text.ts';
6
+ import { readClaudeUsage, readCodexLimits, type ProviderReading } from './normalize.ts';
7
7
 
8
8
  /* A CLI that has not answered by now is not going to; the next pass tries again. */
9
9
  const PROBE_TIMEOUT_MS = 20_000;
@@ -1,8 +1,8 @@
1
1
  import { mkdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import type { UsagePriceBasis, UsagePricing, UsageTotals } from '@adecore/agent-contracts';
4
- import { isNotFound, writeAtomic } from '../fs.ts';
5
4
  import { errorText } from '../error-text.ts';
5
+ import { isNotFound, writeAtomic } from '../fs.ts';
6
6
  import bundled from './prices-snapshot.json' with { type: 'json' };
7
7
 
8
8
  export const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
@@ -12,8 +12,7 @@ export interface UsageRootPath {
12
12
  /* Where in its config folder each CLI leaves its transcripts. */
13
13
  const TRANSCRIPTS: Record<UsageProvider, string> = { claude: 'projects', codex: 'sessions' };
14
14
 
15
- /* Where each CLI leaves its transcripts. The same environment variables the hooks installer reads,
16
- so the scanner and the installer never disagree about where a CLI keeps its things. */
15
+ /* The transcripts of the default accounts, found through the variables the CLIs themselves read. */
17
16
  export function usageRoots(env: Record<string, string | undefined> = process.env): UsageRootPath[] {
18
17
  const home = env.HOME ?? homedir();
19
18
  return [
@@ -1,13 +1,13 @@
1
1
  import { mkdir, open, readFile, readdir, stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import type { UsageRoot } from '@adecore/agent-contracts';
4
+ import { errorText } from '../error-text.ts';
4
5
  import { isNotFound, writeAtomic } from '../fs.ts';
5
6
  import { decodeIndex, encodeIndex, type IndexedFile, type UsageIndex } from './index-file.ts';
6
7
  import { foldByKey, type UsageRecord } from './record.ts';
7
8
  import { claudeMightCarryUsage, parseClaudeLine } from './readers/claude.ts';
8
9
  import { cloneCodexState, codexMightCarryUsage, createCodexState, parseCodexLine, type CodexParserState } from './readers/codex.ts';
9
10
  import { usageRoots, type UsageRootPath } from './roots.ts';
10
- import { errorText } from '../error-text.ts';
11
11
 
12
12
  /* What a file holds from `from` to its end, as it is at the read. */
13
13
  async function readFrom(path: string, from: number): Promise<string> {
@@ -69,11 +69,11 @@ async function listJsonl(dir: string, out: string[] = []): Promise<string[]> {
69
69
  * read again next time. The Codex state is snapshotted before that tail for the same reason.
70
70
  */
71
71
  function parseChunk(text: string, provider: 'claude' | 'codex', from: number, state: CodexParserState | null): ParsedFile {
72
- let records: UsageRecord[] = [];
73
72
  const end = text.lastIndexOf('\n');
74
73
  const whole = end === -1 ? '' : text.slice(0, end);
75
74
  const tail = text.slice(end + 1);
76
- const read = (chunk: string, codex: CodexParserState | null): void => {
75
+ const read = (chunk: string, codex: CodexParserState | null): UsageRecord[] => {
76
+ const records: UsageRecord[] = [];
77
77
  for (const raw of chunk.split('\n')) {
78
78
  const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
79
79
  if (line === '') {
@@ -95,14 +95,12 @@ function parseChunk(text: string, provider: 'claude' | 'codex', from: number, st
95
95
  }
96
96
  }
97
97
  }
98
+ return records;
98
99
  };
99
100
  const codex = provider === 'codex' ? (state ?? createCodexState()) : null;
100
- read(whole, codex);
101
+ const counted = read(whole, codex);
101
102
  const committed = codex === null ? null : cloneCodexState(codex);
102
- const counted = records;
103
- records = [];
104
- read(tail, codex);
105
- return { records: counted, tail: records, offset: from + (end === -1 ? 0 : Buffer.byteLength(whole, 'utf8') + 1), codex: committed };
103
+ return { records: counted, tail: read(tail, codex), offset: from + (end === -1 ? 0 : Buffer.byteLength(whole, 'utf8') + 1), codex: committed };
106
104
  }
107
105
 
108
106
  export interface UsageScannerOptions {
@@ -139,8 +137,8 @@ function withAccount(records: UsageRecord[], accountOf: AccountOf): UsageRecord[
139
137
  }
140
138
 
141
139
  /*
142
- * Read appended transcript bytes incrementally, but restart after rewrites or truncation. Preserve
143
- * totals for deleted transcripts because their usage still happened.
140
+ * Reads only what a transcript appended since the last scan, and starts a file over once it was
141
+ * rewritten or cut. A deleted transcript keeps its totals, since its usage still happened.
144
142
  */
145
143
  export class UsageScanner {
146
144
  private readonly home: string;
@@ -1,21 +1,21 @@
1
1
  import type { UsageAccount, UsageSummaryPayload, UsageSummaryResult } from '@adecore/agent-contracts';
2
+ import { ClientSinks } from '../client-sinks.ts';
3
+ import { errorText } from '../error-text.ts';
2
4
  import type { AgentEvent, AgentSink } from '../events.ts';
3
5
  import { accountOfRecord, aggregate } from './aggregate.ts';
4
6
  import { ExchangeRates } from './exchange.ts';
5
7
  import { PriceBook } from './pricing.ts';
6
8
  import type { KnownProject } from './projects.ts';
7
9
  import type { UsageRecord } from './record.ts';
8
- import { UsageScanner, type ScanReport, type UsageScannerOptions } from './scanner.ts';
9
10
  import type { UsageRootPath } from './roots.ts';
10
- import { errorText } from '../error-text.ts';
11
- import { ClientSinks } from '../client-sinks.ts';
11
+ import { UsageScanner, type ScanReport, type UsageScannerOptions } from './scanner.ts';
12
12
 
13
13
  /* A scan this fresh answers the question the page is asking, so nothing is opened for it. */
14
14
  const SCAN_TTL_MS = 60_000;
15
15
 
16
16
  export interface UsageServiceOptions {
17
17
  home: string;
18
- /* Off with `--no-price-fetch`: the bundled table then prices everything and no rate is asked for. */
18
+ /* Off, the bundled table prices everything and no rate is asked for. */
19
19
  allowPriceFetch?: boolean;
20
20
  /* The projects the host knows, so a folder can wear the name it has in the app. */
21
21
  knownProjects(): Promise<KnownProject[]>;
@@ -151,11 +151,7 @@ export class UsageService {
151
151
  }
152
152
  this.report = report;
153
153
  if (report.changedFiles > 0) {
154
- this.emit({ event: 'usage.changed', payload: { scannedAt: report.at } });
154
+ this.sinks.emit({ event: 'usage.changed', payload: { scannedAt: report.at } });
155
155
  }
156
156
  }
157
-
158
- private emit(event: AgentEvent): void {
159
- this.sinks.emit(event);
160
- }
161
157
  }