@adecore/agents 0.23.1 → 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.
- package/README.md +3 -3
- package/dist/chat/attachment-store.js +4 -5
- package/dist/chat/backend.d.ts +6 -1
- package/dist/chat/background-work.d.ts +1 -1
- package/dist/chat/background-work.js +2 -1
- package/dist/chat/chat-core.d.ts +53 -161
- package/dist/chat/chat-core.js +146 -112
- package/dist/chat/chat-log.js +3 -3
- package/dist/chat/chat-process.d.ts +21 -0
- package/dist/chat/chat-process.js +109 -9
- package/dist/chat/chat-session.d.ts +10 -17
- package/dist/chat/chat-session.js +141 -182
- package/dist/chat/chat-store.d.ts +1 -0
- package/dist/chat/chat-store.js +105 -1
- package/dist/chat/claude-backend.d.ts +4 -0
- package/dist/chat/claude-backend.js +51 -27
- package/dist/chat/claude-protocol.d.ts +12 -0
- package/dist/chat/claude-protocol.js +137 -7
- package/dist/chat/claude-title.d.ts +1 -3
- package/dist/chat/claude-title.js +11 -39
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +36 -17
- package/dist/chat/codex-backend.d.ts +21 -1
- package/dist/chat/codex-backend.js +168 -10
- package/dist/chat/codex-protocol.d.ts +2 -0
- package/dist/chat/codex-protocol.js +37 -3
- package/dist/chat/codex-thread.js +1 -3
- package/dist/chat/codex-transport.d.ts +3 -0
- package/dist/chat/codex-transport.js +38 -35
- package/dist/chat/context-breakdown.js +2 -2
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/external-sessions.js +2 -2
- package/dist/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/dist/chat/index.d.ts +29 -0
- package/dist/chat/index.js +23 -0
- package/dist/chat/projector.d.ts +2 -1
- package/dist/chat/projector.js +12 -3
- package/dist/chat/request-summary.js +1 -4
- package/dist/chat/subagent-settlement.js +1 -3
- package/dist/chat/thread.d.ts +3 -0
- package/dist/chat/thread.js +20 -3
- package/dist/chat/ui-queries.d.ts +2 -0
- package/dist/chat/ui-queries.js +40 -5
- package/dist/chat/ui-replies.d.ts +33 -0
- package/dist/chat/ui-replies.js +152 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +3 -0
- package/dist/coordination/index.d.ts +15 -0
- package/dist/coordination/index.js +16 -0
- package/dist/host/handlers.js +8 -0
- package/dist/host/index.d.ts +7 -0
- package/dist/host/index.js +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/claude.js +3 -1
- package/dist/providers/codex.js +3 -1
- package/dist/providers/index.d.ts +14 -0
- package/dist/providers/index.js +12 -0
- package/dist/run-process.js +54 -14
- package/dist/skills.d.ts +1 -0
- package/dist/skills.js +4 -0
- package/dist/tasks/task-coordinator.d.ts +4 -0
- package/dist/tasks/task-coordinator.js +16 -2
- package/dist/tasks/wiring.js +7 -1
- package/dist/{providers/accounts/test-accounts.d.ts → testing/accounts.d.ts} +2 -2
- package/dist/{providers/accounts/test-accounts.js → testing/accounts.js} +3 -3
- package/dist/testing/bin/fake-claude.d.ts +1 -0
- package/dist/testing/bin/fake-claude.js +6 -0
- package/dist/testing/bin/fake-codex.d.ts +1 -0
- package/dist/testing/bin/fake-codex.js +6 -0
- package/dist/{chat → testing}/fake-claude.d.ts +1 -1
- package/dist/{chat → testing}/fake-claude.js +0 -4
- package/dist/{chat → testing}/fake-cli.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.js +70 -8
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +7 -0
- package/dist/{outbox → testing}/manual-clock.d.ts +1 -1
- package/dist/{watch-test-helpers.d.ts → testing/watch.d.ts} +1 -1
- package/dist/usage/exchange.d.ts +5 -3
- package/dist/usage/exchange.js +25 -16
- package/dist/usage/index-file.d.ts +1 -0
- package/dist/usage/index-file.js +20 -2
- package/dist/usage/index.d.ts +9 -0
- package/dist/usage/index.js +6 -0
- package/dist/usage/limits/normalize.js +1 -3
- package/dist/usage/pricing.d.ts +5 -3
- package/dist/usage/pricing.js +28 -19
- package/dist/usage/scanner.js +39 -11
- package/dist/usage/usage-service.js +8 -3
- package/package.json +72 -11
- package/src/chat/attachment-store.ts +4 -6
- package/src/chat/backend.ts +8 -0
- package/src/chat/background-work.ts +2 -2
- package/src/chat/chat-core.ts +221 -143
- package/src/chat/chat-log.ts +3 -3
- package/src/chat/chat-process.ts +128 -8
- package/src/chat/chat-session.ts +153 -204
- package/src/chat/chat-store.ts +104 -1
- package/src/chat/claude-backend.ts +58 -26
- package/src/chat/claude-protocol.ts +147 -8
- package/src/chat/claude-title.ts +11 -39
- package/src/chat/claude-transcript.ts +38 -18
- package/src/chat/codex-backend.ts +182 -20
- package/src/chat/codex-protocol.ts +40 -5
- package/src/chat/codex-thread.ts +1 -4
- package/src/chat/codex-transport.ts +45 -35
- package/src/chat/context-breakdown.ts +2 -2
- package/src/chat/errors.ts +1 -0
- package/src/chat/external-sessions.ts +2 -2
- package/src/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/src/chat/index.ts +49 -0
- package/src/chat/projector.ts +13 -4
- package/src/chat/request-summary.ts +8 -5
- package/src/chat/subagent-settlement.ts +1 -4
- package/src/chat/thread.ts +22 -3
- package/src/chat/ui-queries.ts +46 -5
- package/src/chat/ui-replies.ts +187 -0
- package/src/context/index.ts +26 -0
- package/src/coordination/index.ts +67 -0
- package/src/host/handlers.ts +8 -0
- package/src/host/index.ts +8 -0
- package/src/index.ts +20 -0
- package/src/providers/accounts/status.ts +1 -4
- package/src/providers/claude.ts +3 -1
- package/src/providers/codex.ts +3 -1
- package/src/providers/index.ts +15 -0
- package/src/run-process.ts +56 -16
- package/src/skills.ts +5 -0
- package/src/tasks/task-coordinator.ts +22 -2
- package/src/tasks/wiring.ts +7 -1
- package/src/{providers/accounts/test-accounts.ts → testing/accounts.ts} +4 -4
- package/src/testing/bin/fake-claude.ts +7 -0
- package/src/testing/bin/fake-codex.ts +7 -0
- package/src/{chat → testing}/fake-claude.ts +1 -5
- package/src/{chat → testing}/fake-cli.ts +1 -1
- package/src/{chat → testing}/fake-codex.ts +75 -6
- package/src/testing/index.ts +7 -0
- package/src/{outbox → testing}/manual-clock.ts +1 -1
- package/src/{watch-test-helpers.ts → testing/watch.ts} +1 -1
- package/src/usage/exchange.ts +31 -20
- package/src/usage/index-file.ts +23 -2
- package/src/usage/index.ts +10 -0
- package/src/usage/limits/normalize.ts +1 -4
- package/src/usage/pricing.ts +34 -23
- package/src/usage/scanner.ts +48 -11
- package/src/usage/usage-service.ts +8 -3
- /package/dist/{chat → testing}/fake-cli.js +0 -0
- /package/dist/{outbox → testing}/manual-clock.js +0 -0
- /package/dist/{watch-test-helpers.js → testing/watch.js} +0 -0
package/dist/usage/exchange.js
CHANGED
|
@@ -26,7 +26,7 @@ export class ExchangeRates {
|
|
|
26
26
|
home;
|
|
27
27
|
allowFetch;
|
|
28
28
|
rate = null;
|
|
29
|
-
|
|
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
|
-
/*
|
|
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.
|
|
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
|
|
49
|
-
|
|
50
|
-
this.
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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;
|
package/dist/usage/index-file.js
CHANGED
|
@@ -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] = {
|
|
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, {
|
|
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
|
}
|
package/dist/usage/pricing.d.ts
CHANGED
|
@@ -23,12 +23,14 @@ export declare class PriceBook {
|
|
|
23
23
|
private table;
|
|
24
24
|
private source;
|
|
25
25
|
private fetchedAt;
|
|
26
|
-
private
|
|
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
|
-
|
|
31
|
+
load(): Promise<void>;
|
|
32
|
+
ensure(): Promise<boolean>;
|
|
32
33
|
private get file();
|
|
33
|
-
private
|
|
34
|
+
private readStored;
|
|
35
|
+
private fetchFresh;
|
|
34
36
|
}
|
package/dist/usage/pricing.js
CHANGED
|
@@ -118,7 +118,7 @@ export class PriceBook {
|
|
|
118
118
|
table;
|
|
119
119
|
source = 'snapshot';
|
|
120
120
|
fetchedAt = null;
|
|
121
|
-
|
|
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
|
-
/*
|
|
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.
|
|
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
|
|
145
|
-
|
|
146
|
-
this.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
}
|
package/dist/usage/scanner.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
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
|
|
31
|
+
const bytes = Buffer.alloc(Math.max(0, size - from));
|
|
16
32
|
let filled = 0;
|
|
17
|
-
while (filled <
|
|
18
|
-
const { bytesRead } = await handle.read(
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
216
|
-
|
|
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
|
-
//
|
|
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.
|
|
115
|
-
this.rates.
|
|
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.
|
|
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
|
-
"
|
|
8
|
-
"source": "./src
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"
|
|
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.
|
|
24
|
-
"@adecore/intelligent-ui": "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
|
-
|
|
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';
|
package/src/chat/backend.ts
CHANGED
|
@@ -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
|
|
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 !==
|
|
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 {
|