@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.
- package/dist/async.js +3 -8
- package/dist/chat/chat-core.d.ts +7 -3
- package/dist/chat/chat-core.js +19 -9
- package/dist/chat/chat-log.js +2 -2
- package/dist/chat/chat-session.d.ts +13 -0
- package/dist/chat/chat-session.js +75 -5
- package/dist/chat/chat-store.d.ts +3 -2
- package/dist/chat/chat-store.js +44 -16
- package/dist/chat/chat-title.js +4 -8
- package/dist/chat/claude-backend.d.ts +1 -0
- package/dist/chat/claude-backend.js +8 -6
- package/dist/chat/claude-title.js +1 -1
- package/dist/chat/codex-backend.d.ts +2 -2
- package/dist/chat/codex-backend.js +5 -8
- package/dist/chat/codex-protocol.js +1 -1
- package/dist/chat/codex-thread.js +4 -5
- package/dist/chat/composer-preferences.js +2 -3
- package/dist/chat/limit-resume.js +2 -7
- package/dist/chat/readable-text.d.ts +2 -0
- package/dist/chat/readable-text.js +16 -0
- package/dist/chat/ui-queries.d.ts +12 -1
- package/dist/chat/ui-queries.js +107 -60
- package/dist/chat/visual-store.js +8 -6
- package/dist/context/refusal.js +3 -7
- package/dist/context/verb.js +1 -5
- package/dist/host/agent-host.js +2 -2
- package/dist/host/handlers.js +1 -2
- package/dist/host/wiring.js +1 -1
- package/dist/lineage.js +2 -3
- package/dist/messages/deliver-message.js +6 -11
- package/dist/messages/deliver-notice.js +8 -12
- package/dist/messages/notice-store.js +3 -5
- package/dist/outbox/outbox-worker.js +5 -2
- package/dist/providers/accounts/service.d.ts +2 -1
- package/dist/providers/accounts/service.js +11 -13
- package/dist/providers/claude.js +0 -1
- package/dist/providers/detect.js +2 -10
- package/dist/record-directory.js +0 -1
- package/dist/tasks/task-coordinator.d.ts +1 -0
- package/dist/tasks/task-coordinator.js +40 -36
- package/dist/tasks/task-store.js +1 -1
- package/dist/tasks/task-work.js +2 -2
- package/dist/tasks/waiting-child.js +5 -3
- package/dist/tasks/wake-parent.js +15 -10
- package/dist/tasks/wiring.js +32 -37
- package/dist/usage/aggregate.js +62 -46
- package/dist/usage/exchange.js +1 -1
- package/dist/usage/limits/monitor.d.ts +2 -2
- package/dist/usage/limits/monitor.js +2 -2
- package/dist/usage/limits/probe.js +1 -1
- package/dist/usage/pricing.js +1 -1
- package/dist/usage/roots.js +1 -2
- package/dist/usage/scanner.js +7 -9
- package/dist/usage/usage-service.d.ts +1 -2
- package/dist/usage/usage-service.js +3 -6
- package/package.json +3 -3
- package/src/async.ts +3 -9
- package/src/chat/chat-core.ts +29 -9
- package/src/chat/chat-log.ts +2 -2
- package/src/chat/chat-session.ts +85 -5
- package/src/chat/chat-store.ts +54 -16
- package/src/chat/chat-title.ts +4 -8
- package/src/chat/claude-backend.ts +9 -6
- package/src/chat/claude-title.ts +1 -1
- package/src/chat/claude-transcript.ts +1 -1
- package/src/chat/codex-backend.ts +5 -8
- package/src/chat/codex-protocol.ts +1 -1
- package/src/chat/codex-thread.ts +6 -5
- package/src/chat/composer-preferences.ts +2 -3
- package/src/chat/limit-resume.ts +2 -7
- package/src/chat/readable-text.ts +18 -0
- package/src/chat/thread.ts +1 -1
- package/src/chat/ui-queries.ts +120 -64
- package/src/chat/visual-store.ts +9 -6
- package/src/chat/wake-chat.ts +4 -1
- package/src/context/refusal.ts +3 -7
- package/src/context/verb.ts +1 -5
- package/src/host/agent-host.ts +2 -2
- package/src/host/handlers.ts +1 -2
- package/src/host/wiring.ts +1 -1
- package/src/lineage.ts +2 -3
- package/src/messages/deliver-message.ts +6 -11
- package/src/messages/deliver-notice.ts +8 -12
- package/src/messages/notice-store.ts +3 -5
- package/src/outbox/outbox-worker.ts +5 -2
- package/src/providers/accounts/service.ts +12 -13
- package/src/providers/accounts/variables.ts +1 -0
- package/src/providers/claude.ts +0 -1
- package/src/providers/detect.ts +2 -11
- package/src/record-directory.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/tasks/task-coordinator.ts +41 -35
- package/src/tasks/task-store.ts +1 -1
- package/src/tasks/task-work.ts +2 -2
- package/src/tasks/waiting-child.ts +6 -3
- package/src/tasks/wake-parent.ts +21 -16
- package/src/tasks/wiring.ts +33 -41
- package/src/usage/aggregate.ts +80 -50
- package/src/usage/exchange.ts +1 -1
- package/src/usage/limits/monitor.ts +4 -4
- package/src/usage/limits/probe.ts +2 -2
- package/src/usage/pricing.ts +1 -1
- package/src/usage/roots.ts +1 -2
- package/src/usage/scanner.ts +8 -10
- package/src/usage/usage-service.ts +5 -9
package/dist/usage/aggregate.js
CHANGED
|
@@ -28,6 +28,59 @@ function slotOf(format, timestampMs, resolution) {
|
|
|
28
28
|
function emptyTotals() {
|
|
29
29
|
return { ...EMPTY_TOTALS };
|
|
30
30
|
}
|
|
31
|
+
function foldBucket(buckets, slot, record, account, price, cost) {
|
|
32
|
+
const key = `${slot}\0${record.provider}\0${record.model}\0${account ?? ''}`;
|
|
33
|
+
let bucket = buckets.get(key);
|
|
34
|
+
if (bucket === undefined) {
|
|
35
|
+
bucket = {
|
|
36
|
+
slot,
|
|
37
|
+
provider: record.provider,
|
|
38
|
+
model: record.model,
|
|
39
|
+
account,
|
|
40
|
+
totals: emptyTotals(),
|
|
41
|
+
costUsd: 0,
|
|
42
|
+
priced: false,
|
|
43
|
+
cacheSavingsUsd: 0,
|
|
44
|
+
sessions: new Set()
|
|
45
|
+
};
|
|
46
|
+
buckets.set(key, bucket);
|
|
47
|
+
}
|
|
48
|
+
bucket.totals = addTotals(bucket.totals, record.totals);
|
|
49
|
+
bucket.costUsd += cost;
|
|
50
|
+
bucket.priced ||= price !== null;
|
|
51
|
+
bucket.cacheSavingsUsd += price === null ? 0 : cacheSavingsOf(record.totals, price);
|
|
52
|
+
if (record.sessionId !== '') {
|
|
53
|
+
bucket.sessions.add(record.sessionId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function foldModel(models, record, account, lookup, cost) {
|
|
57
|
+
const key = `${record.provider}\0${record.model}\0${account ?? ''}`;
|
|
58
|
+
let model = models.get(key);
|
|
59
|
+
if (model === undefined) {
|
|
60
|
+
model = {
|
|
61
|
+
provider: record.provider,
|
|
62
|
+
model: record.model,
|
|
63
|
+
account,
|
|
64
|
+
totals: emptyTotals(),
|
|
65
|
+
costUsd: 0,
|
|
66
|
+
priced: false,
|
|
67
|
+
basis: lookup.basis,
|
|
68
|
+
pricedAs: lookup.pricedAs
|
|
69
|
+
};
|
|
70
|
+
models.set(key, model);
|
|
71
|
+
}
|
|
72
|
+
model.totals = addTotals(model.totals, record.totals);
|
|
73
|
+
model.costUsd += cost;
|
|
74
|
+
model.priced ||= lookup.price !== null;
|
|
75
|
+
}
|
|
76
|
+
function foldProject(project, record, cost) {
|
|
77
|
+
project.totals = addTotals(project.totals, record.totals);
|
|
78
|
+
project.costUsd += cost;
|
|
79
|
+
const share = project.byProvider.get(record.provider) ?? { costUsd: 0, tokens: 0 };
|
|
80
|
+
share.costUsd += cost;
|
|
81
|
+
share.tokens += totalTokensOf(record.totals);
|
|
82
|
+
project.byProvider.set(record.provider, share);
|
|
83
|
+
}
|
|
31
84
|
/* The account a record was made under, the default one under its CLI's kind. */
|
|
32
85
|
export function accountOfRecord(record) {
|
|
33
86
|
return record.account ?? record.provider;
|
|
@@ -41,8 +94,6 @@ export async function aggregate(records, payload, prices, known) {
|
|
|
41
94
|
const format = slotFormatter(payload.timeZone, payload.resolution);
|
|
42
95
|
const buckets = new Map();
|
|
43
96
|
const models = new Map();
|
|
44
|
-
/* The basis of the first record of a model prices the whole row: a model has one price. */
|
|
45
|
-
const modelBasis = new Map();
|
|
46
97
|
const projects = new Map();
|
|
47
98
|
const sessions = new Set();
|
|
48
99
|
const resolver = new ProjectResolver(known);
|
|
@@ -59,42 +110,13 @@ export async function aggregate(records, payload, prices, known) {
|
|
|
59
110
|
if (day < payload.from || day > payload.to) {
|
|
60
111
|
continue;
|
|
61
112
|
}
|
|
62
|
-
const
|
|
63
|
-
const cost = price === null ? 0 : costOf(record.totals, price);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (bucket === undefined) {
|
|
67
|
-
bucket = {
|
|
68
|
-
slot,
|
|
69
|
-
provider: record.provider,
|
|
70
|
-
model: record.model,
|
|
71
|
-
account,
|
|
72
|
-
totals: emptyTotals(),
|
|
73
|
-
costUsd: 0,
|
|
74
|
-
priced: false,
|
|
75
|
-
cacheSavingsUsd: 0,
|
|
76
|
-
sessions: new Set()
|
|
77
|
-
};
|
|
78
|
-
buckets.set(bucketKey, bucket);
|
|
79
|
-
}
|
|
80
|
-
bucket.totals = addTotals(bucket.totals, record.totals);
|
|
81
|
-
bucket.costUsd += cost;
|
|
82
|
-
bucket.priced ||= price !== null;
|
|
83
|
-
bucket.cacheSavingsUsd += price === null ? 0 : cacheSavingsOf(record.totals, price);
|
|
113
|
+
const lookup = prices.look(record.model);
|
|
114
|
+
const cost = lookup.price === null ? 0 : costOf(record.totals, lookup.price);
|
|
115
|
+
foldBucket(buckets, slot, record, account, lookup.price, cost);
|
|
116
|
+
foldModel(models, record, account, lookup, cost);
|
|
84
117
|
if (record.sessionId !== '') {
|
|
85
|
-
bucket.sessions.add(record.sessionId);
|
|
86
118
|
sessions.add(`${record.provider}\0${record.sessionId}`);
|
|
87
119
|
}
|
|
88
|
-
const modelKey = `${record.provider}\0${record.model}\0${account ?? ''}`;
|
|
89
|
-
let model = models.get(modelKey);
|
|
90
|
-
if (model === undefined) {
|
|
91
|
-
model = { provider: record.provider, model: record.model, account, totals: emptyTotals(), costUsd: 0, priced: false };
|
|
92
|
-
models.set(modelKey, model);
|
|
93
|
-
modelBasis.set(modelKey, { basis, pricedAs });
|
|
94
|
-
}
|
|
95
|
-
model.totals = addTotals(model.totals, record.totals);
|
|
96
|
-
model.costUsd += cost;
|
|
97
|
-
model.priced ||= price !== null;
|
|
98
120
|
let folder = folders.get(record.cwd);
|
|
99
121
|
if (folder === undefined) {
|
|
100
122
|
const resolved = await resolver.resolve(record.cwd);
|
|
@@ -104,13 +126,7 @@ export async function aggregate(records, payload, prices, known) {
|
|
|
104
126
|
projects.set(folder, { folder, name: resolved.name, projectId: resolved.projectId, byProvider: new Map(), totals: emptyTotals(), costUsd: 0 });
|
|
105
127
|
}
|
|
106
128
|
}
|
|
107
|
-
|
|
108
|
-
project.totals = addTotals(project.totals, record.totals);
|
|
109
|
-
project.costUsd += cost;
|
|
110
|
-
const share = project.byProvider.get(record.provider) ?? { costUsd: 0, tokens: 0 };
|
|
111
|
-
share.costUsd += cost;
|
|
112
|
-
share.tokens += totalTokensOf(record.totals);
|
|
113
|
-
project.byProvider.set(record.provider, share);
|
|
129
|
+
foldProject(projects.get(folder), record, cost);
|
|
114
130
|
}
|
|
115
131
|
return {
|
|
116
132
|
buckets: [...buckets.values()]
|
|
@@ -128,15 +144,15 @@ export async function aggregate(records, payload, prices, known) {
|
|
|
128
144
|
a.provider.localeCompare(b.provider) ||
|
|
129
145
|
a.model.localeCompare(b.model) ||
|
|
130
146
|
(a.account ?? '').localeCompare(b.account ?? '')),
|
|
131
|
-
models: [...models.
|
|
132
|
-
.map((
|
|
147
|
+
models: [...models.values()]
|
|
148
|
+
.map((model) => ({
|
|
133
149
|
provider: model.provider,
|
|
134
150
|
model: model.model,
|
|
135
151
|
...(model.account === null ? {} : { account: model.account }),
|
|
136
152
|
totals: model.totals,
|
|
137
153
|
costUsd: model.priced ? model.costUsd : null,
|
|
138
|
-
priceBasis:
|
|
139
|
-
pricedAs:
|
|
154
|
+
priceBasis: model.basis,
|
|
155
|
+
pricedAs: model.pricedAs
|
|
140
156
|
}))
|
|
141
157
|
.sort((a, b) => (b.costUsd ?? -1) - (a.costUsd ?? -1)),
|
|
142
158
|
projects: [...projects.values()]
|
package/dist/usage/exchange.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { isNotFound, writeAtomic } from '../fs.js';
|
|
4
3
|
import { errorText } from '../error-text.js';
|
|
4
|
+
import { isNotFound, writeAtomic } from '../fs.js';
|
|
5
5
|
export const FRANKFURTER_URL = 'https://api.frankfurter.app/latest';
|
|
6
6
|
/* The bank publishes one rate a day, so a rate a day old is the rate. */
|
|
7
7
|
const TTL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { UsageLimitsSnapshot, UsageProvider } from '@adecore/agent-contracts';
|
|
2
|
-
import type {
|
|
2
|
+
import type { CodexClientInfo } from '../../chat/codex-transport.ts';
|
|
3
3
|
import type { AgentSink } from '../../events.ts';
|
|
4
|
+
import type { ProviderRegistry } from '../../providers/registry.ts';
|
|
4
5
|
import { type LimitsUpdate } from './normalize.ts';
|
|
5
|
-
import type { CodexClientInfo } from '../../chat/codex-transport.ts';
|
|
6
6
|
import { type ProbeResult } from './probe.ts';
|
|
7
7
|
export type ProbeEnv = Record<string, string>;
|
|
8
8
|
export interface LimitAccount {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { ClientSinks } from '../../client-sinks.js';
|
|
2
|
+
import { errorText } from '../../error-text.js';
|
|
1
3
|
import { definedEnv } from '../../providers/accounts/launch.js';
|
|
2
4
|
import { mergeWindows } from './normalize.js';
|
|
3
5
|
import { probeClaude, probeCodex } from './probe.js';
|
|
4
|
-
import { ClientSinks } from '../../client-sinks.js';
|
|
5
|
-
import { errorText } from '../../error-text.js';
|
|
6
6
|
/* Often enough that a bar is never far behind, rarely enough that two CLIs are not started all day. */
|
|
7
7
|
const PROBE_INTERVAL_MS = 5 * 60_000;
|
|
8
8
|
const MAX_BACKOFF_MS = 60 * 60_000;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { withTimeout } from '../../async.js';
|
|
2
2
|
import { spawnChatProcess } from '../../chat/chat-process.js';
|
|
3
3
|
import { CodexTransport, DEFAULT_CODEX_CLIENT } from '../../chat/codex-transport.js';
|
|
4
|
-
import { readClaudeUsage, readCodexLimits } from './normalize.js';
|
|
5
4
|
import { errorText } from '../../error-text.js';
|
|
5
|
+
import { readClaudeUsage, readCodexLimits } from './normalize.js';
|
|
6
6
|
/* A CLI that has not answered by now is not going to; the next pass tries again. */
|
|
7
7
|
const PROBE_TIMEOUT_MS = 20_000;
|
|
8
8
|
const CODEX_READ_TIMEOUT_MS = 3_000;
|
package/dist/usage/pricing.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { isNotFound, writeAtomic } from '../fs.js';
|
|
4
3
|
import { errorText } from '../error-text.js';
|
|
4
|
+
import { isNotFound, writeAtomic } from '../fs.js';
|
|
5
5
|
import bundled from './prices-snapshot.json' with { type: 'json' };
|
|
6
6
|
export const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
7
7
|
/* A table a day old still prices everything anyone ran today, and nobody has to be online for it. */
|
package/dist/usage/roots.js
CHANGED
|
@@ -2,8 +2,7 @@ import { homedir } from 'node:os';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
/* Where in its config folder each CLI leaves its transcripts. */
|
|
4
4
|
const TRANSCRIPTS = { claude: 'projects', codex: 'sessions' };
|
|
5
|
-
/*
|
|
6
|
-
so the scanner and the installer never disagree about where a CLI keeps its things. */
|
|
5
|
+
/* The transcripts of the default accounts, found through the variables the CLIs themselves read. */
|
|
7
6
|
export function usageRoots(env = process.env) {
|
|
8
7
|
const home = env.HOME ?? homedir();
|
|
9
8
|
return [
|
package/dist/usage/scanner.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { mkdir, open, readFile, readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { errorText } from '../error-text.js';
|
|
3
4
|
import { isNotFound, writeAtomic } from '../fs.js';
|
|
4
5
|
import { decodeIndex, encodeIndex } from './index-file.js';
|
|
5
6
|
import { foldByKey } from './record.js';
|
|
6
7
|
import { claudeMightCarryUsage, parseClaudeLine } from './readers/claude.js';
|
|
7
8
|
import { cloneCodexState, codexMightCarryUsage, createCodexState, parseCodexLine } from './readers/codex.js';
|
|
8
9
|
import { usageRoots } from './roots.js';
|
|
9
|
-
import { errorText } from '../error-text.js';
|
|
10
10
|
/* What a file holds from `from` to its end, as it is at the read. */
|
|
11
11
|
async function readFrom(path, from) {
|
|
12
12
|
const handle = await open(path, 'r');
|
|
@@ -53,11 +53,11 @@ async function listJsonl(dir, out = []) {
|
|
|
53
53
|
* read again next time. The Codex state is snapshotted before that tail for the same reason.
|
|
54
54
|
*/
|
|
55
55
|
function parseChunk(text, provider, from, state) {
|
|
56
|
-
let records = [];
|
|
57
56
|
const end = text.lastIndexOf('\n');
|
|
58
57
|
const whole = end === -1 ? '' : text.slice(0, end);
|
|
59
58
|
const tail = text.slice(end + 1);
|
|
60
59
|
const read = (chunk, codex) => {
|
|
60
|
+
const records = [];
|
|
61
61
|
for (const raw of chunk.split('\n')) {
|
|
62
62
|
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
63
63
|
if (line === '') {
|
|
@@ -79,14 +79,12 @@ function parseChunk(text, provider, from, state) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
return records;
|
|
82
83
|
};
|
|
83
84
|
const codex = provider === 'codex' ? (state ?? createCodexState()) : null;
|
|
84
|
-
read(whole, codex);
|
|
85
|
+
const counted = read(whole, codex);
|
|
85
86
|
const committed = codex === null ? null : cloneCodexState(codex);
|
|
86
|
-
|
|
87
|
-
records = [];
|
|
88
|
-
read(tail, codex);
|
|
89
|
-
return { records: counted, tail: records, offset: from + (end === -1 ? 0 : Buffer.byteLength(whole, 'utf8') + 1), codex: committed };
|
|
87
|
+
return { records: counted, tail: read(tail, codex), offset: from + (end === -1 ? 0 : Buffer.byteLength(whole, 'utf8') + 1), codex: committed };
|
|
90
88
|
}
|
|
91
89
|
/*
|
|
92
90
|
* The account a record of a root belongs to, as a record keeps it (absent for the default account):
|
|
@@ -113,8 +111,8 @@ function withAccount(records, accountOf) {
|
|
|
113
111
|
});
|
|
114
112
|
}
|
|
115
113
|
/*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
114
|
+
* Reads only what a transcript appended since the last scan, and starts a file over once it was
|
|
115
|
+
* rewritten or cut. A deleted transcript keeps its totals, since its usage still happened.
|
|
118
116
|
*/
|
|
119
117
|
export class UsageScanner {
|
|
120
118
|
home;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { UsageAccount, UsageSummaryPayload, UsageSummaryResult } from '@adecore/agent-contracts';
|
|
2
2
|
import type { AgentSink } from '../events.ts';
|
|
3
3
|
import type { KnownProject } from './projects.ts';
|
|
4
|
-
import { type UsageScannerOptions } from './scanner.ts';
|
|
5
4
|
import type { UsageRootPath } from './roots.ts';
|
|
5
|
+
import { type UsageScannerOptions } from './scanner.ts';
|
|
6
6
|
export interface UsageServiceOptions {
|
|
7
7
|
home: string;
|
|
8
8
|
allowPriceFetch?: boolean;
|
|
@@ -31,5 +31,4 @@ export declare class UsageService {
|
|
|
31
31
|
stop(): void;
|
|
32
32
|
private rescan;
|
|
33
33
|
private runScan;
|
|
34
|
-
private emit;
|
|
35
34
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { ClientSinks } from '../client-sinks.js';
|
|
2
|
+
import { errorText } from '../error-text.js';
|
|
1
3
|
import { accountOfRecord, aggregate } from './aggregate.js';
|
|
2
4
|
import { ExchangeRates } from './exchange.js';
|
|
3
5
|
import { PriceBook } from './pricing.js';
|
|
4
6
|
import { UsageScanner } from './scanner.js';
|
|
5
|
-
import { errorText } from '../error-text.js';
|
|
6
|
-
import { ClientSinks } from '../client-sinks.js';
|
|
7
7
|
/* A scan this fresh answers the question the page is asking, so nothing is opened for it. */
|
|
8
8
|
const SCAN_TTL_MS = 60_000;
|
|
9
9
|
/* The accounts of the machine, and after them any a record names that the machine no longer has, under its id. */
|
|
@@ -121,10 +121,7 @@ export class UsageService {
|
|
|
121
121
|
}
|
|
122
122
|
this.report = report;
|
|
123
123
|
if (report.changedFiles > 0) {
|
|
124
|
-
this.emit({ event: 'usage.changed', payload: { scannedAt: report.at } });
|
|
124
|
+
this.sinks.emit({ event: 'usage.changed', payload: { scannedAt: report.at } });
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
emit(event) {
|
|
128
|
-
this.sinks.emit(event);
|
|
129
|
-
}
|
|
130
127
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"test": "bun --conditions=source test"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@adecore/agent-contracts": "0.21.
|
|
24
|
-
"@adecore/intelligent-ui": "0.21.
|
|
23
|
+
"@adecore/agent-contracts": "0.21.2",
|
|
24
|
+
"@adecore/intelligent-ui": "0.21.2",
|
|
25
25
|
"zod": "^4.6.5"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
package/src/async.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
/* The two waits the host does: a pause it can be woken from, and a deadline on somebody else's promise. */
|
|
2
|
-
|
|
3
1
|
/*
|
|
4
|
-
* Resolves after `ms`, or at once when the signal is raised. An abort
|
|
5
|
-
*
|
|
6
|
-
* rejection would only be caught and thrown away.
|
|
2
|
+
* Resolves after `ms`, or at once when the signal is raised. An abort resolves instead of rejecting:
|
|
3
|
+
* every caller is a loop that checks the signal itself on its next turn.
|
|
7
4
|
*/
|
|
8
5
|
export function wait(ms: number, signal?: AbortSignal): Promise<void> {
|
|
9
6
|
return new Promise((resolve) => {
|
|
@@ -19,10 +16,7 @@ export function wait(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
19
16
|
});
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
/*
|
|
23
|
-
* The promise, or `message` when it takes longer than `ms`. The timer is cleared either way, so a
|
|
24
|
-
* call that answers at once does not hold the process for the rest of its deadline.
|
|
25
|
-
*/
|
|
19
|
+
/* The timer is cleared either way, so a call that answers at once does not hold the process until its deadline. */
|
|
26
20
|
export function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
|
|
27
21
|
return new Promise((resolve, reject) => {
|
|
28
22
|
const timer = setTimeout(() => reject(new Error(message)), ms);
|
package/src/chat/chat-core.ts
CHANGED
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
type ModelSelection,
|
|
28
28
|
type RuntimeMode
|
|
29
29
|
} from '@adecore/agent-contracts';
|
|
30
|
-
import { ChatUiQueries, type ChatUiHost } from './ui-queries.ts';
|
|
31
30
|
import { ClientSinks } from '../client-sinks.ts';
|
|
32
31
|
import { errorText } from '../error-text.ts';
|
|
33
32
|
import type { AgentEvent, AgentSink } from '../events.ts';
|
|
@@ -38,6 +37,7 @@ import { SkillIndex } from '../skills.ts';
|
|
|
38
37
|
import type { LimitsUpdate } from '../usage/limits/normalize.ts';
|
|
39
38
|
import { usageRoots } from '../usage/roots.ts';
|
|
40
39
|
import type { AttachmentStore } from './attachment-store.ts';
|
|
40
|
+
import { commandLabel, isBackgroundWork, runsInBackground, type BackgroundWork } from './background-work.ts';
|
|
41
41
|
import type { BookmarkStore } from './bookmark-store.ts';
|
|
42
42
|
import { ChatLog, COMPACT_ABOVE_BYTES } from './chat-log.ts';
|
|
43
43
|
import type { SpawnChatProcess } from './chat-process.ts';
|
|
@@ -58,8 +58,9 @@ import type { CodexClientInfo } from './codex-transport.ts';
|
|
|
58
58
|
import { ComposerPreferences } from './composer-preferences.ts';
|
|
59
59
|
import { DeltaCoalescer } from './delta-coalescer.ts';
|
|
60
60
|
import { ChatError } from './errors.ts';
|
|
61
|
-
import {
|
|
61
|
+
import { readableAssistantText } from './readable-text.ts';
|
|
62
62
|
import { SubagentReader, type SubagentReaderOptions } from './subagent-reader.ts';
|
|
63
|
+
import { ChatUiQueries, type ChatUiHost } from './ui-queries.ts';
|
|
63
64
|
import type { VisualInput, VisualStore } from './visual-store.ts';
|
|
64
65
|
|
|
65
66
|
/* A turn that was running when the host went down, and the attempt that would take it up again. */
|
|
@@ -71,12 +72,12 @@ export interface InterruptedRun {
|
|
|
71
72
|
|
|
72
73
|
type BookmarkableItem = Extract<ChatItem, { kind: 'user' | 'assistant' }>;
|
|
73
74
|
|
|
74
|
-
/* A message a person or the agent wrote in the chat's own thread; a subagent's words belong to its row. */
|
|
75
75
|
function withoutHidden(info: ChatInfo): ChatInfo {
|
|
76
76
|
const { hidden: _hidden, ...rest } = info;
|
|
77
77
|
return rest;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/* A message a person or the agent wrote in the chat's own thread; a subagent's words belong to its row. */
|
|
80
81
|
function isBookmarkable(item: ChatItem): item is BookmarkableItem {
|
|
81
82
|
return item.kind === 'user' || (item.kind === 'assistant' && (item.parentToolUseId ?? null) === null);
|
|
82
83
|
}
|
|
@@ -84,7 +85,8 @@ function isBookmarkable(item: ChatItem): item is BookmarkableItem {
|
|
|
84
85
|
/* The start of a message on one line; a message of only attachments is its file names. */
|
|
85
86
|
function excerptOf(item: BookmarkableItem): string {
|
|
86
87
|
const files = item.kind === 'user' ? (item.attachments ?? []).map((attachment) => attachment.name).join(', ') : '';
|
|
87
|
-
|
|
88
|
+
const text = item.kind === 'assistant' ? readableAssistantText(item) : item.text;
|
|
89
|
+
return (text.trim() === '' ? files : text).replace(/\s+/g, ' ').trim();
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
// One process worked on the turn and one more may take it up after a restart; a loop of resumes could redo a command forever.
|
|
@@ -102,6 +104,8 @@ const DEFAULT_RESUME_WORDS: ResumeWords = {
|
|
|
102
104
|
|
|
103
105
|
export interface ChatCoreOptions {
|
|
104
106
|
intelligentUi?: ChatUiHost;
|
|
107
|
+
// The info string that marks a UI fence in a reply, for a host that keeps a name of its own.
|
|
108
|
+
uiFenceLanguage?: string;
|
|
105
109
|
providers: ProviderRegistry;
|
|
106
110
|
// Where the threads are kept; without it a chat lives as long as the process.
|
|
107
111
|
store?: ChatStore;
|
|
@@ -175,6 +179,7 @@ export class ChatCore {
|
|
|
175
179
|
readonly claudeProjectsDir: string;
|
|
176
180
|
readonly composerPreferences = new ComposerPreferences();
|
|
177
181
|
private readonly uiQueries: ChatUiQueries | null;
|
|
182
|
+
private readonly uiFenceLanguage: string | undefined;
|
|
178
183
|
private readonly checkpoints: TurnCheckpoints | null;
|
|
179
184
|
private readonly instructions: string | null;
|
|
180
185
|
private readonly skillIndex: SkillIndex;
|
|
@@ -188,7 +193,7 @@ export class ChatCore {
|
|
|
188
193
|
// How big each record was the last time it went to disk, and the writes waiting for a big one.
|
|
189
194
|
private readonly sizes = new Map<string, number>();
|
|
190
195
|
private readonly waiting = new Map<string, ReturnType<typeof setTimeout>>();
|
|
191
|
-
// What the last record held that no event carries (preambles, the host's extras), as its JSON.
|
|
196
|
+
// What the last record held that no event carries (preambles, the host's extras, captured UI access), as its JSON.
|
|
192
197
|
private readonly unlogged = new Map<string, string>();
|
|
193
198
|
// The write in flight per chat, so the next one queues behind it instead of racing it.
|
|
194
199
|
private readonly writes = new Map<string, Promise<void>>();
|
|
@@ -202,6 +207,7 @@ export class ChatCore {
|
|
|
202
207
|
|
|
203
208
|
constructor(options: ChatCoreOptions) {
|
|
204
209
|
this.uiQueries = options.intelligentUi ? new ChatUiQueries(options.intelligentUi) : null;
|
|
210
|
+
this.uiFenceLanguage = options.uiFenceLanguage;
|
|
205
211
|
this.providers = options.providers;
|
|
206
212
|
this.store = options.store ?? null;
|
|
207
213
|
this.bookmarks = options.bookmarks ?? null;
|
|
@@ -398,9 +404,11 @@ export class ChatCore {
|
|
|
398
404
|
const references = this.referencesFor(payload.chatId);
|
|
399
405
|
const session = new ChatSession({
|
|
400
406
|
info,
|
|
407
|
+
...(this.uiFenceLanguage ? { uiFenceLanguage: this.uiFenceLanguage } : {}),
|
|
401
408
|
...(this.uiQueries ? { ui: { schemas: this.uiQueries.schemas, observe: (item, final) => this.uiQueries!.observe(session, item, final) } } : {}),
|
|
402
409
|
items: stored?.items ?? [],
|
|
403
410
|
preambles: stored?.preambles ?? [],
|
|
411
|
+
uiAccess: stored?.uiAccess ?? {},
|
|
404
412
|
provider,
|
|
405
413
|
command: this.commands[kind] ?? provider.command,
|
|
406
414
|
...(this.spawn ? { spawn: this.spawn } : {}),
|
|
@@ -735,7 +743,10 @@ export class ChatCore {
|
|
|
735
743
|
return this.uiQueries.query(this.require(payload.chatId), payload);
|
|
736
744
|
}
|
|
737
745
|
|
|
738
|
-
async choose(payload: ChatUiChoicePayload): Promise<{ queued: boolean; turnId: string }> {
|
|
746
|
+
async choose(payload: ChatUiChoicePayload, clientId: string): Promise<{ queued: boolean; turnId: string }> {
|
|
747
|
+
if (!this.attached.get(payload.chatId)?.has(clientId)) {
|
|
748
|
+
throw new ChatError('refused-query', 'Attach to the chat before answering its UI.');
|
|
749
|
+
}
|
|
739
750
|
const session = this.require(payload.chatId);
|
|
740
751
|
const queries = await this.uiQueries?.choiceValues(session, payload);
|
|
741
752
|
const result = session.choose(payload, queries);
|
|
@@ -1022,7 +1033,15 @@ export class ChatCore {
|
|
|
1022
1033
|
const log = this.logs.get(chatId);
|
|
1023
1034
|
// Not folded here: an older write still in flight may land after this one, and the log is what covers for it.
|
|
1024
1035
|
try {
|
|
1025
|
-
this.store.writeSync(
|
|
1036
|
+
this.store.writeSync(
|
|
1037
|
+
chatId,
|
|
1038
|
+
info,
|
|
1039
|
+
items,
|
|
1040
|
+
{ seq: log?.seq ?? 0, resetSeq: log?.resetSeq ?? 0 },
|
|
1041
|
+
session.preambles,
|
|
1042
|
+
this.recordExtras(chatId),
|
|
1043
|
+
session.uiAccessRecord
|
|
1044
|
+
);
|
|
1026
1045
|
} catch (e) {
|
|
1027
1046
|
console.error(`Chat record for ${chatId} failed:`, errorText(e));
|
|
1028
1047
|
}
|
|
@@ -1181,7 +1200,8 @@ export class ChatCore {
|
|
|
1181
1200
|
// A delta held back is already in the thread, so it gets its seq before the snapshot says where it ends.
|
|
1182
1201
|
this.coalescers.get(chatId)?.flush();
|
|
1183
1202
|
const extras = this.recordExtras(chatId);
|
|
1184
|
-
const
|
|
1203
|
+
const uiAccess = session.uiAccessRecord;
|
|
1204
|
+
const unlogged = JSON.stringify([session.preambles, extras, uiAccess]);
|
|
1185
1205
|
const folds = fold || log.size > COMPACT_ABOVE_BYTES || !log.onDisk;
|
|
1186
1206
|
// The log already holds every change to the thread, so the whole record is rewritten only to fold
|
|
1187
1207
|
// the log, on a chat's first write, or for what no event carries.
|
|
@@ -1190,7 +1210,7 @@ export class ChatCore {
|
|
|
1190
1210
|
}
|
|
1191
1211
|
const { info, items } = session.thread.snapshot();
|
|
1192
1212
|
const at = { seq: log.seq, resetSeq: log.resetSeq };
|
|
1193
|
-
this.sizes.set(chatId, await this.store.write(chatId, info, items, at, session.preambles, extras));
|
|
1213
|
+
this.sizes.set(chatId, await this.store.write(chatId, info, items, at, session.preambles, extras, uiAccess));
|
|
1194
1214
|
this.unlogged.set(chatId, unlogged);
|
|
1195
1215
|
// A chat killed while the write was out has no log left to fold.
|
|
1196
1216
|
if (this.logs.get(chatId) === log && folds) {
|
package/src/chat/chat-log.ts
CHANGED
|
@@ -48,8 +48,8 @@ export interface ChatLogState {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/*
|
|
51
|
-
*
|
|
52
|
-
* one order. Recent lines stay in memory
|
|
51
|
+
* Numbers every broadcast event and appends it synchronously, so disk, memory and snapshot boundaries
|
|
52
|
+
* keep one order. Recent lines stay in memory to answer an attach with `since` at once.
|
|
53
53
|
*/
|
|
54
54
|
export class ChatLog {
|
|
55
55
|
private readonly path: string | null;
|