@assuredloop/orca-bot-kit 0.10.0 → 0.12.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 +13 -4
- package/SETUP.md +16 -15
- package/package.json +2 -1
- package/rules/limits.md +3 -3
- package/skills/obk-bot-building/SKILL.md +56 -23
- package/src/book.js +70 -9
- package/src/bot.js +25 -9
- package/src/cli.js +112 -32
- package/src/conversations.js +24 -0
- package/src/message.js +83 -11
- package/src/orca.js +6 -6
- package/src/permissions.js +430 -16
- package/src/up.js +50 -16
- package/src/usage.js +79 -39
package/src/usage.js
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Which conversation belongs to which session is the book's to say and is never
|
|
11
11
|
// guessed, as everywhere else in the kit (ADR 0012). A session's conversations
|
|
12
|
-
// are the one the book names for it now and the ones in its history.
|
|
12
|
+
// are the one the book names for it now and the ones in its history. A session
|
|
13
|
+
// since retired keeps a block of its own, marked retired, so a window that spans
|
|
14
|
+
// a retirement loses nothing (#379). One in the
|
|
13
15
|
// bot's folder that no session claims is reported under the bot instead, with
|
|
14
16
|
// its figures, so that what it spent is visible rather than quietly dropped.
|
|
15
17
|
//
|
|
@@ -29,14 +31,17 @@
|
|
|
29
31
|
// uncached input on both. Taken as written, one is many times the other for
|
|
30
32
|
// the same work, and the cached tokens get priced at full rate.
|
|
31
33
|
// 3. Claude Code writes the same call down more than once, so a call counts
|
|
32
|
-
// once per `requestId` and `message.id`.
|
|
34
|
+
// once per `requestId` and `message.id`. A subagent's calls are in a file
|
|
35
|
+
// of their own beside the conversation that started it, and belong to it;
|
|
36
|
+
// the same call can be in more than one of those files, so a call counts
|
|
37
|
+
// once across all of a conversation's files together (#371).
|
|
33
38
|
|
|
34
39
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
35
40
|
|
|
36
41
|
import { botDir, botNames, readBot } from './bot.js';
|
|
37
42
|
import { readBook, sessionIdsIn } from './book.js';
|
|
38
|
-
import { transcriptsIn } from './conversations.js';
|
|
39
|
-
import { harnessOf } from './launch.js';
|
|
43
|
+
import { claudeSubagentTranscripts, transcriptsIn } from './conversations.js';
|
|
44
|
+
import { HARNESSES, harnessOf } from './launch.js';
|
|
40
45
|
|
|
41
46
|
/** The kinds a call's tokens are reported in, the same on either harness. */
|
|
42
47
|
const KINDS = ['input', 'output', 'cache_read', 'cache_write', 'reasoning'];
|
|
@@ -98,6 +103,9 @@ function forBot(bots, name, onlySession, window) {
|
|
|
98
103
|
// them runs on is asked. The bot's own is the answer when it has no sessions.
|
|
99
104
|
const harnesses = new Set(bot.sessions.map((session) => harnessOf(session, bot.harness)));
|
|
100
105
|
if (harnesses.size === 0) harnesses.add(bot.harness);
|
|
106
|
+
// The book does not say which harness a retired session ran on, so any is.
|
|
107
|
+
const retired = Array.isArray(book.retired) ? book.retired : [];
|
|
108
|
+
if (retired.length > 0) for (const harness of HARNESSES) harnesses.add(harness);
|
|
101
109
|
|
|
102
110
|
const onRecord = new Map();
|
|
103
111
|
for (const harness of harnesses) {
|
|
@@ -112,15 +120,22 @@ function forBot(bots, name, onlySession, window) {
|
|
|
112
120
|
|
|
113
121
|
const claimed = sessionIdsIn(book);
|
|
114
122
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
123
|
+
// Two entries can name the same conversation, one resumed after its session
|
|
124
|
+
// was retired, and one entry can name it twice, now and in its history: it is
|
|
125
|
+
// counted once, in the first block asked for that names it.
|
|
126
|
+
const taken = new Set();
|
|
127
|
+
const block = (entry) => {
|
|
128
|
+
const ids = [...new Set(idsIn(entry))].filter((id) => onRecord.has(id) && !taken.has(id));
|
|
129
|
+
for (const id of ids) taken.add(id);
|
|
130
|
+
const { conversations, gaps } = all(ids.map((id) => onRecord.get(id)), window);
|
|
131
|
+
return { conversations, not_counted: gaps };
|
|
132
|
+
};
|
|
133
|
+
const sessions = [
|
|
134
|
+
...bot.sessions.map((session) => ({ name: session.name, entry: book.sessions[session.name] ?? {} })),
|
|
135
|
+
...retired.map((entry) => ({ name: entry?.name, retired: entry?.retired, entry: entry ?? {} })),
|
|
136
|
+
]
|
|
137
|
+
.filter(({ name }) => onlySession === undefined || name === onlySession)
|
|
138
|
+
.map(({ name, entry, ...was }) => ({ name, ...('retired' in was ? { retired: was.retired } : {}), ...block(entry) }));
|
|
124
139
|
|
|
125
140
|
const unclaimed = all([...onRecord.values()].filter((one) => !claimed.has(one.id)), window);
|
|
126
141
|
|
|
@@ -151,9 +166,9 @@ const idsIn = (entry) => [
|
|
|
151
166
|
* shorter page. What it could not count is still said.
|
|
152
167
|
*/
|
|
153
168
|
function counted(one, window) {
|
|
154
|
-
const read = one.harness === 'claude' ? fromClaude : fromCodex;
|
|
155
169
|
const tally = {
|
|
156
170
|
calls: 0,
|
|
171
|
+
subagentCalls: 0,
|
|
157
172
|
compactions: 0,
|
|
158
173
|
tokens: Object.fromEntries(KINDS.map((kind) => [kind, 0])),
|
|
159
174
|
models: new Set(),
|
|
@@ -166,15 +181,25 @@ function counted(one, window) {
|
|
|
166
181
|
gaps: noGaps(),
|
|
167
182
|
};
|
|
168
183
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
184
|
+
// A Claude Code conversation's subagents wrote their calls to files of their
|
|
185
|
+
// own, and what they spent is the conversation's.
|
|
186
|
+
const subagents = one.harness === 'claude' ? claudeSubagentTranscripts(one.file) : { files: [], unreadable: 0 };
|
|
187
|
+
tally.gaps.unreadable_transcripts += subagents.unreadable;
|
|
188
|
+
const sources = [one.file, ...subagents.files]
|
|
189
|
+
.map((file, index) => {
|
|
190
|
+
const { entries, unreadable, broken } = transcript(file);
|
|
191
|
+
tally.gaps.unreadable_transcripts += unreadable;
|
|
192
|
+
tally.gaps.broken_lines += broken;
|
|
193
|
+
return { entries, subagent: index > 0 };
|
|
194
|
+
});
|
|
195
|
+
if (one.harness === 'claude') fromClaude(sources, window, tally);
|
|
196
|
+
else fromCodex(sources[0].entries, window, tally);
|
|
173
197
|
if (tally.calls === 0 && KINDS.every((kind) => tally.tokens[kind] === 0)) return { gaps: tally.gaps };
|
|
174
198
|
|
|
175
199
|
return { gaps: tally.gaps, row: {
|
|
176
200
|
id: one.id,
|
|
177
201
|
calls: tally.calls,
|
|
202
|
+
subagent_calls: tally.subagentCalls,
|
|
178
203
|
tokens: tally.tokens,
|
|
179
204
|
by_model: [...tally.byModel].map(([model, its]) => ({ model, calls: its.calls, tokens: its.tokens })),
|
|
180
205
|
models: [...tally.models],
|
|
@@ -204,6 +229,15 @@ function counted(one, window) {
|
|
|
204
229
|
* the runs add up to the whole, with a call still being written at a boundary
|
|
205
230
|
* charged once, part to each side (#169).
|
|
206
231
|
*
|
|
232
|
+
* A call is one `requestId` and `message.id` across the main transcript and its
|
|
233
|
+
* subagents' files together, since the same call can be in more than one of
|
|
234
|
+
* them, and its records are taken in the order of their times. Copies written
|
|
235
|
+
* at the very same moment are taken smallest first, since a call's figures only
|
|
236
|
+
* grow: the largest is the latest. One with a figure missing may be the latest
|
|
237
|
+
* of them, so it is taken as that, and what it hides is said rather than
|
|
238
|
+
* measured past it. It is a subagent's when the main transcript has no record
|
|
239
|
+
* of it.
|
|
240
|
+
*
|
|
207
241
|
* A record with no time, or with a figure missing, is left out whole and said to
|
|
208
242
|
* be; the call it belongs to is counted from its other records, if it has any.
|
|
209
243
|
* But when the record last written before the window's start is one of those,
|
|
@@ -211,10 +245,10 @@ function counted(one, window) {
|
|
|
211
245
|
* the window cannot be told apart from what it grew by before. So the call is
|
|
212
246
|
* not counted in that window, and is said to be.
|
|
213
247
|
*/
|
|
214
|
-
function fromClaude(
|
|
248
|
+
function fromClaude(sources, window, tally) {
|
|
215
249
|
const byCall = new Map();
|
|
216
250
|
|
|
217
|
-
for (const entry of entries) {
|
|
251
|
+
for (const { entries, subagent } of sources) for (const entry of entries) {
|
|
218
252
|
const when = Date.parse(entry.timestamp ?? '');
|
|
219
253
|
if (entry.type === 'system' && entry.subtype === 'compact_boundary') {
|
|
220
254
|
if (Number.isNaN(when)) tally.gaps.records_without_time += 1;
|
|
@@ -233,10 +267,11 @@ function fromClaude(entries, window, tally) {
|
|
|
233
267
|
if (broken && inside(when, window)) tally.gaps.records_without_numbers += 1;
|
|
234
268
|
const call = `${entry.requestId}\u0000${entry.message?.id}`;
|
|
235
269
|
if (!byCall.has(call)) byCall.set(call, []);
|
|
236
|
-
byCall.get(call).push({ entry, when, broken });
|
|
270
|
+
byCall.get(call).push({ entry, when, broken, subagent });
|
|
237
271
|
}
|
|
238
272
|
|
|
239
273
|
for (const records of byCall.values()) {
|
|
274
|
+
records.sort((one, other) => one.when - other.when || one.broken - other.broken || size(one.entry) - size(other.entry));
|
|
240
275
|
const made = records[0].when;
|
|
241
276
|
const atEnd = records.findLast(({ when, broken }) => !broken && when < window.to);
|
|
242
277
|
if (atEnd === undefined) continue;
|
|
@@ -252,10 +287,17 @@ function fromClaude(entries, window, tally) {
|
|
|
252
287
|
const isNew = inside(made, window);
|
|
253
288
|
if (!isNew && KINDS.every((kind) => grew[kind] === 0)) continue;
|
|
254
289
|
|
|
255
|
-
|
|
290
|
+
const subagent = records.every((record) => record.subagent);
|
|
291
|
+
count(tally, grew, atEnd.entry.message?.model, atEnd.entry.effort, isNew ? made : atEnd.when, isNew ? 1 : 0, subagent);
|
|
256
292
|
}
|
|
257
293
|
}
|
|
258
294
|
|
|
295
|
+
/** How much one Claude Code record says its call has used so far, all kinds together. */
|
|
296
|
+
const size = (entry) => {
|
|
297
|
+
const now = figures(entry);
|
|
298
|
+
return KINDS.reduce((sum, kind) => sum + now[kind], 0);
|
|
299
|
+
};
|
|
300
|
+
|
|
259
301
|
/** The figures Claude Code writes on every call, all of which a record needs to be counted. */
|
|
260
302
|
const CLAUDE_FIELDS = ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens'];
|
|
261
303
|
|
|
@@ -300,15 +342,15 @@ const CODEX_FIELDS = [
|
|
|
300
342
|
* The running total is followed through events outside the window as well, since
|
|
301
343
|
* what a call added can only be measured against the event before it.
|
|
302
344
|
*
|
|
303
|
-
* An event whose running total has a figure missing
|
|
304
|
-
* and after it the running total is not known, so neither
|
|
305
|
-
* complete event added. Measured against the event before the
|
|
306
|
-
* would take in what the broken one used, at its own time and
|
|
307
|
-
* another window; taken by its own per-call figure, it may be the
|
|
308
|
-
* written down again. Telling those apart is guessing, so it is not
|
|
309
|
-
* either, and is said to be; the running total follows on from it. A
|
|
310
|
-
* figure is needed and has something missing is not counted, and is
|
|
311
|
-
* be; nothing missing is taken as zero.
|
|
345
|
+
* An event whose running total has a figure missing, or has none at all, is left
|
|
346
|
+
* out and said to be, and after it the running total is not known, so neither
|
|
347
|
+
* is what the next complete event added. Measured against the event before the
|
|
348
|
+
* broken one, it would take in what the broken one used, at its own time and
|
|
349
|
+
* perhaps in another window; taken by its own per-call figure, it may be the
|
|
350
|
+
* broken one written down again. Telling those apart is guessing, so it is not
|
|
351
|
+
* counted either, and is said to be; the running total follows on from it. A
|
|
352
|
+
* call whose figure is needed and has something missing is not counted, and is
|
|
353
|
+
* said to be; nothing missing is taken as zero.
|
|
312
354
|
*/
|
|
313
355
|
function fromCodex(entries, window, tally) {
|
|
314
356
|
let model;
|
|
@@ -338,14 +380,11 @@ function fromCodex(entries, window, tally) {
|
|
|
338
380
|
const info = entry.payload?.info;
|
|
339
381
|
if (info === undefined || info === null) continue;
|
|
340
382
|
|
|
341
|
-
const total = info.total_token_usage
|
|
383
|
+
const total = info.total_token_usage;
|
|
342
384
|
let used;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// not counted either until a complete running total says where things are.
|
|
347
|
-
used = broken ? null : perCall(info);
|
|
348
|
-
} else if (!complete(total, CODEX_READ)) {
|
|
385
|
+
// No running total at all is a running total with every figure missing:
|
|
386
|
+
// the next one's rise would take this call in again (#302).
|
|
387
|
+
if (!complete(total, CODEX_READ)) {
|
|
349
388
|
used = null;
|
|
350
389
|
broken = true;
|
|
351
390
|
} else {
|
|
@@ -418,8 +457,9 @@ function kindsOf(raw) {
|
|
|
418
457
|
* the total that has to be complete, and a row headed by nothing would be worse
|
|
419
458
|
* than no row.
|
|
420
459
|
*/
|
|
421
|
-
function count(tally, used, model, effort, when, calls = 1) {
|
|
460
|
+
function count(tally, used, model, effort, when, calls = 1, subagent = false) {
|
|
422
461
|
tally.calls += calls;
|
|
462
|
+
if (subagent) tally.subagentCalls += calls;
|
|
423
463
|
for (const kind of KINDS) tally.tokens[kind] += used[kind];
|
|
424
464
|
add(tally.models, model);
|
|
425
465
|
add(tally.efforts, effort);
|