@adecore/agents 0.21.1 → 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 +2 -2
- package/dist/chat/chat-core.js +4 -4
- package/dist/chat/chat-log.js +2 -2
- package/dist/chat/chat-store.js +4 -7
- 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/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 +0 -1
- 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 +34 -34
- 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 +4 -4
- package/src/chat/chat-log.ts +2 -2
- package/src/chat/chat-store.ts +4 -8
- 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/thread.ts +1 -1
- 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 +0 -1
- 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 +35 -33
- 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
|
@@ -2,6 +2,7 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { AgentKindSchema } from '@adecore/agent-contracts';
|
|
5
|
+
import { DEFAULT_CODEX_CLIENT } from '../../chat/codex-transport.js';
|
|
5
6
|
import { ClientSinks } from '../../client-sinks.js';
|
|
6
7
|
import { errorText } from '../../error-text.js';
|
|
7
8
|
import { accountEnv, accountFolder, canContinue, defaultFolder, expandHome, isDefaultAccount, isKnownKind, transcriptFolder } from './accounts.js';
|
|
@@ -10,7 +11,6 @@ import { prepareShadowHome } from './shadow-home.js';
|
|
|
10
11
|
import { askAccountAs } from './status.js';
|
|
11
12
|
import { acceptAccounts, accountsPath, InvalidAccountError, readAccounts, wireAccounts, writeAccounts } from './store.js';
|
|
12
13
|
import { DEFAULT_ACCOUNTS_HOST, platformSecrets, secretKey, SecretsUnavailableError } from './variables.js';
|
|
13
|
-
import { DEFAULT_CODEX_CLIENT } from '../../chat/codex-transport.js';
|
|
14
14
|
/* Who is signed in changes by hand and rarely; often enough to notice, rarely enough not to start CLIs all day. */
|
|
15
15
|
const CHECK_INTERVAL_MS = 15 * 60_000;
|
|
16
16
|
const MAX_BACKOFF_MS = 60 * 60_000;
|
|
@@ -161,20 +161,10 @@ export class ProviderAccountsService {
|
|
|
161
161
|
return accountEnv(key, account, provider, baseEnv, variables(account));
|
|
162
162
|
}
|
|
163
163
|
transcriptFolder(kind, id) {
|
|
164
|
-
|
|
165
|
-
const account = this.accounts[key];
|
|
166
|
-
if (account === undefined || account.kind !== kind) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
return transcriptFolder(key, account, this.providers.get(kind), this.env);
|
|
164
|
+
return this.folderOf(kind, id, transcriptFolder);
|
|
170
165
|
}
|
|
171
166
|
homeFolder(kind, id) {
|
|
172
|
-
|
|
173
|
-
const account = this.accounts[key];
|
|
174
|
-
if (account === undefined || account.kind !== kind) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
return accountFolder(key, account, this.providers.get(kind), this.env);
|
|
167
|
+
return this.folderOf(kind, id, accountFolder);
|
|
178
168
|
}
|
|
179
169
|
canContinue(kind, from, to) {
|
|
180
170
|
const fromId = from ?? kind;
|
|
@@ -268,6 +258,14 @@ export class ProviderAccountsService {
|
|
|
268
258
|
await this.queue(true);
|
|
269
259
|
return this.snapshot();
|
|
270
260
|
}
|
|
261
|
+
folderOf(kind, id, folder) {
|
|
262
|
+
const key = id ?? kind;
|
|
263
|
+
const account = this.accounts[key];
|
|
264
|
+
if (account === undefined || account.kind !== kind) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
return folder(key, account, this.providers.get(kind), this.env);
|
|
268
|
+
}
|
|
271
269
|
async runWatch(id, watch) {
|
|
272
270
|
while (this.now() < watch.deadline) {
|
|
273
271
|
await this.sleep(LOGIN_POLL_MS);
|
package/dist/providers/claude.js
CHANGED
|
@@ -25,7 +25,6 @@ export function claudeRuntimeMode(mode) {
|
|
|
25
25
|
}
|
|
26
26
|
return Object.keys(PERMISSION_MODE).find((key) => PERMISSION_MODE[key] === mode);
|
|
27
27
|
}
|
|
28
|
-
/* The argument list for one Claude Code chat process. */
|
|
29
28
|
export function claudeArgs(launch) {
|
|
30
29
|
// The `=` form matters, since the flag is variadic and would swallow a prompt argument after it (measured on Claude Code 2.1.274).
|
|
31
30
|
const args = [
|
package/dist/providers/detect.js
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { spawnChatProcess } from '../chat/chat-process.js';
|
|
2
|
-
|
|
3
|
-
const defined = {};
|
|
4
|
-
for (const [key, value] of Object.entries(env)) {
|
|
5
|
-
if (value !== undefined) {
|
|
6
|
-
defined[key] = value;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
return defined;
|
|
10
|
-
}
|
|
2
|
+
import { definedEnv } from './accounts/launch.js';
|
|
11
3
|
/* Asks a CLI for its version; a missing binary is a plain "not installed", never an error. */
|
|
12
4
|
export async function detectCli(command, env = process.env, spawn = spawnChatProcess) {
|
|
13
5
|
try {
|
|
@@ -15,7 +7,7 @@ export async function detectCli(command, env = process.env, spawn = spawnChatPro
|
|
|
15
7
|
const code = new Promise((resolve) => {
|
|
16
8
|
exited = resolve;
|
|
17
9
|
});
|
|
18
|
-
const proc = spawn({ command: [command, '--version'], cwd: process.cwd(), env:
|
|
10
|
+
const proc = spawn({ command: [command, '--version'], cwd: process.cwd(), env: definedEnv(env), onExit: (exitCode) => exited(exitCode) });
|
|
19
11
|
proc.stdin.end();
|
|
20
12
|
const output = await new Response(proc.stdout).text();
|
|
21
13
|
if ((await code) !== 0) {
|
package/dist/record-directory.js
CHANGED
|
@@ -100,7 +100,6 @@ export class RecordDirectory {
|
|
|
100
100
|
this.records.delete(id);
|
|
101
101
|
return rm(this.pathOf(id), { force: true });
|
|
102
102
|
}
|
|
103
|
-
/* Drops every record the predicate says is gone. */
|
|
104
103
|
async prune(drop) {
|
|
105
104
|
for (const record of this.all()) {
|
|
106
105
|
if (drop(record)) {
|
|
@@ -71,7 +71,6 @@ export class TaskCoordinator {
|
|
|
71
71
|
}
|
|
72
72
|
await this.deps.tasks.settled();
|
|
73
73
|
}
|
|
74
|
-
/* For `chats.observe()`. */
|
|
75
74
|
chatEvent(event) {
|
|
76
75
|
if (this.stopped || event.event !== 'chat.event') {
|
|
77
76
|
return;
|
|
@@ -201,38 +200,7 @@ export class TaskCoordinator {
|
|
|
201
200
|
return;
|
|
202
201
|
}
|
|
203
202
|
if (after === 'gone') {
|
|
204
|
-
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const lost = task.background.itemIds.filter((id) => {
|
|
208
|
-
const item = items.find((item) => item.id === id);
|
|
209
|
-
return !item || (item.kind === 'subagent' ? item.status !== 'done' : item.kind !== 'tool' || item.state !== 'done');
|
|
210
|
-
});
|
|
211
|
-
if (lost.length > 0 || task.background.commands.length > 0) {
|
|
212
|
-
this.settle(task, 'failed', {
|
|
213
|
-
text: `The agent ended before its background work completed: ${[
|
|
214
|
-
...lost.map((id) => {
|
|
215
|
-
const row = items.find((item) => item.id === id);
|
|
216
|
-
return row?.kind === 'subagent' ? row.description || row.toolUseId : row?.kind === 'tool' ? row.name : id;
|
|
217
|
-
}),
|
|
218
|
-
...task.background.commands
|
|
219
|
-
].join(', ')}. This work will not send a result.`,
|
|
220
|
-
source: 'exit',
|
|
221
|
-
at: this.deps.now()
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
const reports = task.background.itemIds.flatMap((id) => {
|
|
226
|
-
const row = items.find((item) => item.id === id);
|
|
227
|
-
const report = row?.kind === 'subagent' ? row.result : row?.kind === 'tool' ? row.output : null;
|
|
228
|
-
return report ? [report] : [];
|
|
229
|
-
});
|
|
230
|
-
this.settle(task, 'done', {
|
|
231
|
-
text: reports.join('\n\n') || resultOfTurn(turn, items, this.deps.now()).result.text,
|
|
232
|
-
source: 'turn',
|
|
233
|
-
at: this.deps.now()
|
|
234
|
-
});
|
|
235
|
-
}
|
|
203
|
+
this.settleGone(task, task.background, turn, items);
|
|
236
204
|
return;
|
|
237
205
|
}
|
|
238
206
|
}
|
|
@@ -243,6 +211,38 @@ export class TaskCoordinator {
|
|
|
243
211
|
const { status, result } = resultOfTurn(turn, items, this.deps.now());
|
|
244
212
|
this.settle(task, status, result);
|
|
245
213
|
}
|
|
214
|
+
/* The child's process went after its turn was held for background work; what that work left decides the task. */
|
|
215
|
+
settleGone(task, background, turn, items) {
|
|
216
|
+
if (background.itemIds.length === 0 && this.deps.limit.owed(task.id)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const lost = background.itemIds.filter((id) => {
|
|
220
|
+
const item = items.find((item) => item.id === id);
|
|
221
|
+
return !item || (item.kind === 'subagent' ? item.status !== 'done' : item.kind !== 'tool' || item.state !== 'done');
|
|
222
|
+
});
|
|
223
|
+
if (lost.length > 0 || background.commands.length > 0) {
|
|
224
|
+
const names = lost.map((id) => {
|
|
225
|
+
const row = items.find((item) => item.id === id);
|
|
226
|
+
return row?.kind === 'subagent' ? row.description || row.toolUseId : row?.kind === 'tool' ? row.name : id;
|
|
227
|
+
});
|
|
228
|
+
this.settle(task, 'failed', {
|
|
229
|
+
text: `The agent ended before its background work completed: ${[...names, ...background.commands].join(', ')}. This work will not send a result.`,
|
|
230
|
+
source: 'exit',
|
|
231
|
+
at: this.deps.now()
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const reports = background.itemIds.flatMap((id) => {
|
|
236
|
+
const row = items.find((item) => item.id === id);
|
|
237
|
+
const report = row?.kind === 'subagent' ? row.result : row?.kind === 'tool' ? row.output : null;
|
|
238
|
+
return report ? [report] : [];
|
|
239
|
+
});
|
|
240
|
+
this.settle(task, 'done', {
|
|
241
|
+
text: reports.join('\n\n') || resultOfTurn(turn, items, this.deps.now()).result.text,
|
|
242
|
+
source: 'turn',
|
|
243
|
+
at: this.deps.now()
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
246
|
/*
|
|
247
247
|
* Whether work in the background keeps this task open past the turn. Commands and monitors alone hold
|
|
248
248
|
* it only until the limit, owed from the moment they are all that is left; a subagent or a workflow
|
|
@@ -297,7 +297,7 @@ export class TaskCoordinator {
|
|
|
297
297
|
if (!task || items === null || this.delegating(chatId)) {
|
|
298
298
|
return;
|
|
299
299
|
}
|
|
300
|
-
const last =
|
|
300
|
+
const last = items.findLast((item) => item.kind === 'turn');
|
|
301
301
|
if (last && last.state !== 'running' && last.createdAt >= task.createdAt) {
|
|
302
302
|
this.turnEnded(chatId, last);
|
|
303
303
|
}
|
package/dist/tasks/task-store.js
CHANGED
|
@@ -80,7 +80,7 @@ export class TaskStore {
|
|
|
80
80
|
return this.sorted().filter((task) => task.projectId === projectId);
|
|
81
81
|
}
|
|
82
82
|
/*
|
|
83
|
-
* Ends an open task with its result;
|
|
83
|
+
* Ends an open task with its result; null when it was not open any more, which is how the first of
|
|
84
84
|
* `done`, the end of a turn and an exit wins. A cancelled task wakes nobody.
|
|
85
85
|
*/
|
|
86
86
|
async settle(id, status, result, now) {
|
package/dist/tasks/task-work.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export const BackgroundLimitWorkSchema = z.object({
|
|
3
3
|
kind: z.literal('background-limit'),
|
|
4
|
-
// The target is the child
|
|
5
|
-
// `restarted` marks an entry an earlier run of the host owed, whose commands went down with it.
|
|
4
|
+
// The target is the child. `commands` are what held its task open when the limit started, for a child whose
|
|
5
|
+
// process went since; `restarted` marks an entry an earlier run of the host owed, whose commands went down with it.
|
|
6
6
|
payload: z.object({ taskId: z.string().min(1), commands: z.array(z.string()), restarted: z.literal(true).optional() })
|
|
7
7
|
});
|
|
8
8
|
export const WakeParentWorkSchema = z.object({
|
|
@@ -51,7 +51,6 @@ export class WaitingObserver {
|
|
|
51
51
|
stop() {
|
|
52
52
|
this.stopped = true;
|
|
53
53
|
}
|
|
54
|
-
/* For `chats.observe()`. */
|
|
55
54
|
chatEvent(event) {
|
|
56
55
|
if (this.stopped || event.event !== 'chat.event') {
|
|
57
56
|
return;
|
|
@@ -71,13 +70,16 @@ export class WaitingObserver {
|
|
|
71
70
|
.catch((e) => (this.deps.log ?? console.error)(`Owing a note about ${chatId} failed: ${errorText(e)}`));
|
|
72
71
|
}
|
|
73
72
|
}
|
|
73
|
+
function oneLine(text) {
|
|
74
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
75
|
+
}
|
|
74
76
|
function quoted(text) {
|
|
75
|
-
return JSON.stringify(text
|
|
77
|
+
return JSON.stringify(oneLine(text));
|
|
76
78
|
}
|
|
77
79
|
/* The one line a note shows before "Show more": a question longer than that is read in the node. */
|
|
78
80
|
const NOTE_QUESTION_LENGTH = 200;
|
|
79
81
|
function quotedShort(text) {
|
|
80
|
-
const line = text
|
|
82
|
+
const line = oneLine(text);
|
|
81
83
|
return JSON.stringify(line.length > NOTE_QUESTION_LENGTH ? `${clipText(line, NOTE_QUESTION_LENGTH - 1)}…` : line);
|
|
82
84
|
}
|
|
83
85
|
/* One question as the parent's CLI reads it, with the choices it may pick from. */
|
|
@@ -43,6 +43,19 @@ export function wakeLabel(tasks) {
|
|
|
43
43
|
export function wakeNote(tasks) {
|
|
44
44
|
return `Woken by ${tasks.length} finished ${tasks.length === 1 ? 'task' : 'tasks'}`;
|
|
45
45
|
}
|
|
46
|
+
/* An unacknowledged attempt waits for a person's next turn, rather than retrying on every chat event. */
|
|
47
|
+
function deliveryBlocked(items, tasks) {
|
|
48
|
+
return items.some((item, index) => item.kind === 'turn' &&
|
|
49
|
+
item.deliveryPending &&
|
|
50
|
+
item.taskIds?.some((id) => tasks.some((task) => task.id === id)) &&
|
|
51
|
+
(item.state === 'running' || !items.slice(index + 1).some((later) => later.kind === 'turn' && later.origin !== 'agent')));
|
|
52
|
+
}
|
|
53
|
+
function deliveryPending(items, tasks) {
|
|
54
|
+
return tasks.some((task) => {
|
|
55
|
+
const turn = items.findLast((item) => item.kind === 'turn' && item.taskIds?.includes(task.id));
|
|
56
|
+
return turn?.kind === 'turn' && turn.deliveryPending;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
46
59
|
/*
|
|
47
60
|
* CLI acceptance lives in the saved turn before the task store consumes its result, so a restart
|
|
48
61
|
* between those writes can finish delivery without opening another turn.
|
|
@@ -75,12 +88,7 @@ export function wakeParentHandler(deps) {
|
|
|
75
88
|
if (tasks.length === 0) {
|
|
76
89
|
return held();
|
|
77
90
|
}
|
|
78
|
-
|
|
79
|
-
const blocked = items.some((item, index) => item.kind === 'turn' &&
|
|
80
|
-
item.deliveryPending &&
|
|
81
|
-
item.taskIds?.some((id) => tasks.some((task) => task.id === id)) &&
|
|
82
|
-
(item.state === 'running' || !items.slice(index + 1).some((later) => later.kind === 'turn' && later.origin !== 'agent')));
|
|
83
|
-
if (blocked) {
|
|
91
|
+
if (deliveryBlocked(items, tasks)) {
|
|
84
92
|
return 'wait';
|
|
85
93
|
}
|
|
86
94
|
const teamsOut = deps.tasks.openBatches(parentId).length;
|
|
@@ -93,10 +101,7 @@ export function wakeParentHandler(deps) {
|
|
|
93
101
|
return 'wait';
|
|
94
102
|
}
|
|
95
103
|
await chat.persist?.();
|
|
96
|
-
if (
|
|
97
|
-
const turn = chat.items().findLast((item) => item.kind === 'turn' && item.taskIds?.includes(task.id));
|
|
98
|
-
return turn?.kind === 'turn' && turn.deliveryPending;
|
|
99
|
-
})) {
|
|
104
|
+
if (deliveryPending(chat.items(), tasks)) {
|
|
100
105
|
return 'wait';
|
|
101
106
|
}
|
|
102
107
|
await deps.tasks.markWoken(tasks.map((task) => task.id));
|
package/dist/tasks/wiring.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { reportsOnBackgroundWork } from '../chat/background-work.js';
|
|
2
2
|
import { chatOpener, loadChat } from '../chat/wake-chat.js';
|
|
3
3
|
import { errorText } from '../error-text.js';
|
|
4
|
+
import { chatNoticeTargets } from '../messages/deliver-notice.js';
|
|
4
5
|
import { backgroundLimitHandler, backgroundLimits } from './background-limit.js';
|
|
5
6
|
import { giveTaskHandler } from './give-task.js';
|
|
6
7
|
import { TaskCoordinator } from './task-coordinator.js';
|
|
@@ -16,6 +17,7 @@ export function wireTasks(deps) {
|
|
|
16
17
|
const { chats, outbox } = deps;
|
|
17
18
|
const enqueue = (projectId, target, work, notBefore) => outbox.enqueue(projectId, target, work, notBefore);
|
|
18
19
|
const limit = backgroundLimits(outbox);
|
|
20
|
+
const owesGiveTask = (taskId) => outbox.list().some((entry) => isGiveTask(entry) && entry.payload.taskId === taskId);
|
|
19
21
|
const coordinator = new TaskCoordinator({
|
|
20
22
|
tasks: deps.tasks,
|
|
21
23
|
now,
|
|
@@ -23,7 +25,7 @@ export function wireTasks(deps) {
|
|
|
23
25
|
placed: (nodeId) => deps.placed(nodeId),
|
|
24
26
|
loading: (chatId) => chats.isLoading(chatId),
|
|
25
27
|
// Until the turn that carries a task ran, no turn of the child is its answer.
|
|
26
|
-
owedTurn:
|
|
28
|
+
owedTurn: owesGiveTask,
|
|
27
29
|
oweWake: (task) => oweWake({ enqueue }, task),
|
|
28
30
|
alert: (nodeId, title, body) => deps.alert(nodeId, title, body),
|
|
29
31
|
...(deps.retryAt ? { retryAt: deps.retryAt } : {}),
|
|
@@ -58,38 +60,38 @@ export function wireTasks(deps) {
|
|
|
58
60
|
}
|
|
59
61
|
});
|
|
60
62
|
const chatFor = chatOpener({ chats, placed: deps.placed });
|
|
63
|
+
const targets = chatNoticeTargets(chats);
|
|
64
|
+
/* Owes again what an earlier run left of an open task, and lets its child's last turn settle it. */
|
|
65
|
+
const recoverOpen = async (task) => {
|
|
66
|
+
if (task.requiresTaskTurn === true && deps.placed(task.childId) && !owesGiveTask(task.id)) {
|
|
67
|
+
await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
|
|
68
|
+
}
|
|
69
|
+
const stored = await chats.storedBackground(task.childId);
|
|
70
|
+
const storedWork = stored.items.length > 0 || stored.commands.length > 0;
|
|
71
|
+
if (!deps.placed(task.childId) || (task.background === undefined && task.requiresTaskTurn !== true && !storedWork)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (task.background === undefined && stored.turnId !== null && storedWork) {
|
|
75
|
+
await deps.tasks.holdBackground(task.id, {
|
|
76
|
+
turnId: stored.turnId,
|
|
77
|
+
itemIds: stored.items.map((item) => item.id),
|
|
78
|
+
commands: stored.commands
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const chat = await loadChat({ chats, placed: deps.placed }, task.childId);
|
|
82
|
+
if (chat && chat.info.activeTurnId === null) {
|
|
83
|
+
const last = chat.thread.list().findLast((item) => item.kind === 'turn');
|
|
84
|
+
if (last?.kind === 'turn') {
|
|
85
|
+
coordinator.chatEvent({ event: 'chat.event', payload: { chatId: task.childId, event: { type: 'item', item: last } } });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
61
89
|
return {
|
|
62
90
|
coordinator,
|
|
63
91
|
recover: async () => {
|
|
64
92
|
for (const task of deps.tasks.all()) {
|
|
65
|
-
if (task.status === 'open'
|
|
66
|
-
|
|
67
|
-
await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const storedBackground = task.status === 'open' ? await chats.storedBackground(task.childId) : { turnId: null, items: [], commands: [] };
|
|
71
|
-
if (task.status === 'open' &&
|
|
72
|
-
deps.placed(task.childId) &&
|
|
73
|
-
(task.background !== undefined ||
|
|
74
|
-
task.requiresTaskTurn === true ||
|
|
75
|
-
storedBackground.items.length > 0 ||
|
|
76
|
-
storedBackground.commands.length > 0)) {
|
|
77
|
-
if (task.background === undefined &&
|
|
78
|
-
storedBackground.turnId !== null &&
|
|
79
|
-
(storedBackground.items.length > 0 || storedBackground.commands.length > 0)) {
|
|
80
|
-
await deps.tasks.holdBackground(task.id, {
|
|
81
|
-
turnId: storedBackground.turnId,
|
|
82
|
-
itemIds: storedBackground.items.map((item) => item.id),
|
|
83
|
-
commands: storedBackground.commands
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
const chat = await loadChat({ chats, placed: deps.placed }, task.childId);
|
|
87
|
-
if (chat && chat.info.activeTurnId === null) {
|
|
88
|
-
const last = chat.thread.list().findLast((item) => item.kind === 'turn');
|
|
89
|
-
if (last?.kind === 'turn') {
|
|
90
|
-
coordinator.chatEvent({ event: 'chat.event', payload: { chatId: task.childId, event: { type: 'item', item: last } } });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
+
if (task.status === 'open') {
|
|
94
|
+
await recoverOpen(task);
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
await coordinator.settled();
|
|
@@ -110,14 +112,7 @@ export function wireTasks(deps) {
|
|
|
110
112
|
await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
|
|
111
113
|
return task;
|
|
112
114
|
},
|
|
113
|
-
chatState:
|
|
114
|
-
const session = chats.get(nodeId);
|
|
115
|
-
if (session) {
|
|
116
|
-
return session.info.activeTurnId === null ? 'idle' : 'running';
|
|
117
|
-
}
|
|
118
|
-
// A chat nobody has loaded is idle: the turn opens on the thread the host reads back from disk.
|
|
119
|
-
return (await chats.hasStored(nodeId)) ? 'idle' : 'none';
|
|
120
|
-
},
|
|
115
|
+
chatState: (nodeId) => targets.chat(nodeId),
|
|
121
116
|
done: (childId, text) => coordinator.done(childId, text),
|
|
122
117
|
involving: (nodeId) => deps.tasks.involving(nodeId)
|
|
123
118
|
},
|
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 [
|