@manyoars/oars 0.3.0 → 0.4.1
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 +44 -29
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +102 -32
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/gate/hook-main.js +13 -1
- package/dist/gate/hook-process.d.ts +11 -1
- package/dist/gate/hook-process.js +50 -2
- package/dist/gate/hook-record.d.ts +3 -0
- package/dist/gate/hook-record.js +8 -0
- package/dist/gate/hook.d.ts +2 -0
- package/dist/gate/hook.js +3 -0
- package/dist/gate/replay.js +2 -5
- package/dist/gate/report.js +1 -1
- package/dist/gate/transcripts.js +1 -1
- package/dist/hook-bin.js +27 -2
- package/dist/issuer.d.ts +7 -9
- package/dist/issuer.js +10 -32
- package/dist/main.js +3 -0
- package/dist/sessions/actions.js +78 -0
- package/dist/sessions/allow.d.ts +10 -1
- package/dist/sessions/allow.js +29 -9
- package/dist/sessions/baseline.d.ts +1 -1
- package/dist/sessions/baseline.js +6 -4
- package/dist/sessions/call-shape.d.ts +19 -0
- package/dist/sessions/call-shape.js +85 -0
- package/dist/sessions/cell-increments.d.ts +5 -1
- package/dist/sessions/cell-increments.js +84 -50
- package/dist/sessions/cells.d.ts +33 -3
- package/dist/sessions/cells.js +89 -9
- package/dist/sessions/codex.js +93 -27
- package/dist/sessions/command.d.ts +11 -1
- package/dist/sessions/command.js +64 -18
- package/dist/sessions/coverage.d.ts +19 -0
- package/dist/sessions/coverage.js +56 -0
- package/dist/sessions/cursor.js +2 -2
- package/dist/sessions/derive.js +72 -6
- package/dist/sessions/exclusion.d.ts +6 -0
- package/dist/sessions/exclusion.js +8 -0
- package/dist/sessions/extract.d.ts +4 -2
- package/dist/sessions/extract.js +58 -26
- package/dist/sessions/hook-calls.js +1 -1
- package/dist/sessions/hook-index.d.ts +14 -1
- package/dist/sessions/hook-index.js +10 -3
- package/dist/sessions/hooks.d.ts +5 -1
- package/dist/sessions/hooks.js +133 -22
- package/dist/sessions/identity.d.ts +8 -3
- package/dist/sessions/identity.js +45 -15
- package/dist/sessions/install.d.ts +18 -2
- package/dist/sessions/install.js +53 -8
- package/dist/sessions/mcp.d.ts +3 -2
- package/dist/sessions/mcp.js +8 -4
- package/dist/sessions/motif.d.ts +2 -1
- package/dist/sessions/motif.js +2 -2
- package/dist/sessions/opencode.d.ts +33 -0
- package/dist/sessions/opencode.js +319 -0
- package/dist/sessions/otel.d.ts +60 -0
- package/dist/sessions/otel.js +494 -0
- package/dist/sessions/package-books.d.ts +48 -0
- package/dist/sessions/package-books.js +120 -0
- package/dist/sessions/package-resolve.d.ts +2 -0
- package/dist/sessions/package-resolve.js +3 -1
- package/dist/sessions/pi.d.ts +36 -0
- package/dist/sessions/pi.js +353 -0
- package/dist/sessions/pool-books.d.ts +21 -0
- package/dist/sessions/pool-books.js +75 -0
- package/dist/sessions/pool-choices.d.ts +5 -0
- package/dist/sessions/pool-choices.js +11 -0
- package/dist/sessions/pool-command.d.ts +76 -5
- package/dist/sessions/pool-command.js +263 -136
- package/dist/sessions/pool-nightly-deps.d.ts +3 -0
- package/dist/sessions/pool-nightly-deps.js +11 -0
- package/dist/sessions/pool-nightly.d.ts +10 -0
- package/dist/sessions/pool-nightly.js +85 -0
- package/dist/sessions/pool-outbox.d.ts +15 -2
- package/dist/sessions/pool-outbox.js +79 -26
- package/dist/sessions/pool-print.d.ts +1 -1
- package/dist/sessions/pool-raw.d.ts +27 -0
- package/dist/sessions/pool-raw.js +68 -0
- package/dist/sessions/pool-replica.d.ts +35 -0
- package/dist/sessions/pool-replica.js +91 -0
- package/dist/sessions/pool-send-lock.d.ts +20 -0
- package/dist/sessions/pool-send-lock.js +87 -0
- package/dist/sessions/pool-sent.d.ts +4 -0
- package/dist/sessions/pool-sent.js +26 -0
- package/dist/sessions/pool-totals.d.ts +10 -0
- package/dist/sessions/pool-totals.js +21 -0
- package/dist/sessions/pool-wire.d.ts +240 -30
- package/dist/sessions/pool-wire.js +695 -215
- package/dist/sessions/public-vocabulary.d.ts +2 -3
- package/dist/sessions/public-vocabulary.js +2 -9
- package/dist/sessions/query.d.ts +16 -4
- package/dist/sessions/query.js +32 -10
- package/dist/sessions/read-cost.js +14 -3
- package/dist/sessions/read-recovery-edge.d.ts +2 -0
- package/dist/sessions/read-recovery-edge.js +26 -0
- package/dist/sessions/read.d.ts +8 -1
- package/dist/sessions/read.js +6 -3
- package/dist/sessions/recall-bundle.d.ts +12 -0
- package/dist/sessions/recall-bundle.js +33 -0
- package/dist/sessions/recall-frames.d.ts +18 -0
- package/dist/sessions/recall-frames.js +44 -0
- package/dist/sessions/recall-next.d.ts +145 -0
- package/dist/sessions/recall-next.js +241 -0
- package/dist/sessions/recall-object.d.ts +2 -0
- package/dist/sessions/recall-object.js +1 -0
- package/dist/sessions/recall-push.d.ts +2 -2
- package/dist/sessions/recall-push.js +3 -51
- package/dist/sessions/recall-reads.d.ts +7 -0
- package/dist/sessions/recall-reads.js +7 -0
- package/dist/sessions/recovery-calls.d.ts +21 -0
- package/dist/sessions/recovery-calls.js +25 -0
- package/dist/sessions/redact.d.ts +11 -0
- package/dist/sessions/redact.js +59 -0
- package/dist/sessions/schema.d.ts +59 -9
- package/dist/sessions/schema.js +22 -3
- package/dist/sessions/session-shape.d.ts +6 -3
- package/dist/sessions/session-shape.js +19 -6
- package/dist/sessions/sources.js +75 -1
- package/dist/sessions/store.d.ts +13 -1
- package/dist/sessions/store.js +36 -16
- package/dist/sessions/survival.d.ts +2 -0
- package/dist/sessions/survival.js +2 -2
- package/dist/sessions/transcript-tail.d.ts +14 -0
- package/dist/sessions/transcript-tail.js +64 -0
- package/dist/sessions/vscode-copilot.js +24 -7
- package/dist/telemetry/command.d.ts +8 -0
- package/dist/telemetry/command.js +85 -0
- package/dist/telemetry/consent.d.ts +17 -0
- package/dist/telemetry/consent.js +63 -0
- package/dist/telemetry/crash.d.ts +8 -0
- package/dist/telemetry/crash.js +52 -0
- package/dist/telemetry/emit.d.ts +31 -0
- package/dist/telemetry/emit.js +42 -0
- package/dist/telemetry/fields.d.ts +16 -0
- package/dist/telemetry/fields.js +26 -0
- package/dist/telemetry/health.d.ts +21 -0
- package/dist/telemetry/health.js +108 -0
- package/dist/telemetry/hook-emit.d.ts +4 -0
- package/dist/telemetry/hook-emit.js +18 -0
- package/dist/telemetry/measures.d.ts +32 -0
- package/dist/telemetry/measures.js +128 -0
- package/dist/telemetry/observe.d.ts +13 -0
- package/dist/telemetry/observe.js +51 -0
- package/dist/telemetry/read-events.d.ts +9 -0
- package/dist/telemetry/read-events.js +17 -0
- package/dist/telemetry/record.d.ts +17 -0
- package/dist/telemetry/record.js +26 -0
- package/dist/telemetry/report.d.ts +77 -0
- package/dist/telemetry/report.js +188 -0
- package/dist/telemetry/schema.d.ts +40 -0
- package/dist/telemetry/schema.js +187 -0
- package/dist/telemetry/send-main.d.ts +1 -0
- package/dist/telemetry/send-main.js +11 -0
- package/dist/telemetry/send.d.ts +36 -0
- package/dist/telemetry/send.js +172 -0
- package/dist/telemetry/spool.d.ts +17 -0
- package/dist/telemetry/spool.js +70 -0
- package/dist/telemetry/wire.d.ts +25 -0
- package/dist/telemetry/wire.js +76 -0
- package/package.json +2 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Where a path target sits in the project, as a closed list. `none` is a call with no path target (a shell call, a search, a fetch). */
|
|
2
|
+
export const TARGET_CLASSES = ['source', 'test', 'config', 'doc', 'lockfile', 'generated', 'data', 'none', 'other'];
|
|
3
|
+
/** What an argument holds, as a closed list: a call's arguments are the sorted set of these. `none` is a call with no arguments. */
|
|
4
|
+
export const ARG_CLASSES = ['path', 'url', 'pattern', 'command', 'number', 'flag', 'text', 'none'];
|
|
5
|
+
const words = (list) => new Set(list.split(/\s+/).filter((word) => word !== ''));
|
|
6
|
+
const LOCKFILES = words('package-lock.json npm-shrinkwrap.json yarn.lock pnpm-lock.yaml bun.lockb bun.lock cargo.lock poetry.lock pipfile.lock uv.lock gemfile.lock composer.lock go.sum gradle.lockfile packages.lock.json podfile.lock pubspec.lock mix.lock flake.lock');
|
|
7
|
+
const GENERATED_DIRS = words('dist build out node_modules .next .nuxt .astro .svelte-kit coverage target __generated__ generated .turbo .cache __pycache__ vendor');
|
|
8
|
+
const TEST_DIRS = words('test tests __tests__ spec specs e2e __mocks__ fixtures testdata cypress');
|
|
9
|
+
const DOC_DIRS = words('docs doc documentation');
|
|
10
|
+
const SOURCE_EXTENSIONS = words('ts tsx mts cts js jsx mjs cjs py pyi java kt kts go rs rb php cs c h cc cpp hpp swift scala sh bash zsh ps1 psm1 sql astro vue svelte css scss sass less html htm lua dart ex exs erl clj hs ml r jl pl zig groovy gradle');
|
|
11
|
+
const DOC_EXTENSIONS = words('md mdx rst txt adoc org tex');
|
|
12
|
+
const CONFIG_EXTENSIONS = words('json jsonc json5 yaml yml toml ini cfg conf properties env plist editorconfig xml');
|
|
13
|
+
const DATA_EXTENSIONS = words('csv tsv ndjson jsonl parquet avro arrow db sqlite sqlite3 log xlsx xls dat bin pkl');
|
|
14
|
+
const CONFIG_BASENAMES = words('dockerfile makefile justfile procfile gemfile rakefile pom.xml build.gradle settings.gradle .gitignore .gitattributes .npmrc .nvmrc .prettierrc .eslintrc .dockerignore .editorconfig');
|
|
15
|
+
const DOC_BASENAMES = words('readme license licence changelog contributing notice authors codeowners');
|
|
16
|
+
const TEST_NAME = /(?:^|[._-])(?:test|tests|spec)(?:[._-]|$)|(?:_test|_spec|Test|Tests|Spec)\.[a-z]+$/;
|
|
17
|
+
const GENERATED_NAME = /\.(?:min\.(?:js|css)|d\.ts|map|generated\.[a-z]+|pb\.go|g\.dart)$/;
|
|
18
|
+
/**
|
|
19
|
+
* The class of a path target, from the base that hashed it and the path's own suffix. A call with no path target (`base` null) is `none`; the order below is the order a path is asked,
|
|
20
|
+
* so a lockfile is never `config` and a `tests/data.json` is `test`. The mapping is pinned in `call-shape.test.ts`. `path` is the key the target is hashed on (relative to the repository or the cwd, never the absolute path), so the folders above a repository do not decide the class; it is read here and goes no further.
|
|
21
|
+
*/
|
|
22
|
+
export const targetClassOf = (path, base) => {
|
|
23
|
+
if (base === null || path === undefined || path === '')
|
|
24
|
+
return 'none';
|
|
25
|
+
const parts = path.replace(/\\/g, '/').split('/').filter((part) => part !== '' && part !== '.');
|
|
26
|
+
const name = parts.at(-1) ?? '';
|
|
27
|
+
const lower = name.toLowerCase();
|
|
28
|
+
const dirs = parts.slice(0, -1).map((part) => part.toLowerCase());
|
|
29
|
+
const dot = lower.lastIndexOf('.');
|
|
30
|
+
const extension = dot > 0 ? lower.slice(dot + 1) : lower.startsWith('.') ? lower.slice(1) : '';
|
|
31
|
+
if (LOCKFILES.has(lower))
|
|
32
|
+
return 'lockfile';
|
|
33
|
+
if (dirs.some((dir) => GENERATED_DIRS.has(dir)) || GENERATED_NAME.test(lower))
|
|
34
|
+
return 'generated';
|
|
35
|
+
if (dirs.some((dir) => TEST_DIRS.has(dir)) || TEST_NAME.test(name))
|
|
36
|
+
return 'test';
|
|
37
|
+
const stem = dot > 0 ? lower.slice(0, dot) : lower;
|
|
38
|
+
if (DOC_EXTENSIONS.has(extension) || DOC_BASENAMES.has(stem) || dirs.some((dir) => DOC_DIRS.has(dir)))
|
|
39
|
+
return 'doc';
|
|
40
|
+
if (CONFIG_BASENAMES.has(lower) || /^\.[a-z0-9_.-]*(?:rc|ignore)(?:\.[a-z]+)?$/.test(lower) || /\.config\.[a-z]+$/.test(lower) || /^tsconfig[a-z0-9.-]*\.json$/.test(lower) || /^\.env(?:\.|$)/.test(lower))
|
|
41
|
+
return 'config';
|
|
42
|
+
if (SOURCE_EXTENSIONS.has(extension))
|
|
43
|
+
return 'source';
|
|
44
|
+
if (DATA_EXTENSIONS.has(extension))
|
|
45
|
+
return 'data';
|
|
46
|
+
if (CONFIG_EXTENSIONS.has(extension))
|
|
47
|
+
return 'config';
|
|
48
|
+
return 'other';
|
|
49
|
+
};
|
|
50
|
+
const PATH_ARGS = words('file_path notebook_path path cwd workdir directory dir filepath filename file');
|
|
51
|
+
const URL_ARGS = words('url uri href');
|
|
52
|
+
const PATTERN_ARGS = words('pattern regex regexp glob include exclude');
|
|
53
|
+
const COMMAND_ARGS = words('command cmd script');
|
|
54
|
+
const URL_VALUE = /^[a-z][a-z0-9+.-]*:\/\/\S+$/i;
|
|
55
|
+
const argClassOfEntry = (key, value) => {
|
|
56
|
+
if (value === null || value === undefined)
|
|
57
|
+
return null;
|
|
58
|
+
if (typeof value === 'number')
|
|
59
|
+
return 'number';
|
|
60
|
+
if (typeof value === 'boolean')
|
|
61
|
+
return 'flag';
|
|
62
|
+
const lower = key.toLowerCase();
|
|
63
|
+
if (PATH_ARGS.has(lower))
|
|
64
|
+
return 'path';
|
|
65
|
+
if (URL_ARGS.has(lower) || (typeof value === 'string' && URL_VALUE.test(value)))
|
|
66
|
+
return 'url';
|
|
67
|
+
if (PATTERN_ARGS.has(lower))
|
|
68
|
+
return 'pattern';
|
|
69
|
+
if (COMMAND_ARGS.has(lower))
|
|
70
|
+
return 'command';
|
|
71
|
+
return 'text';
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* The classes of a call's arguments, as a sorted set: which of path, URL, pattern, command, number, flag, and text its input holds, or just `none` when it holds no value. A key name
|
|
75
|
+
* gives the class of a string (`file_path` is a path, `pattern` a pattern, `command` a command), a number or a boolean gives its own, and any other value is text. The values are read here and go no further.
|
|
76
|
+
*/
|
|
77
|
+
export const argClassOf = (input) => {
|
|
78
|
+
const held = new Set();
|
|
79
|
+
for (const [key, value] of Object.entries(input)) {
|
|
80
|
+
const kind = argClassOfEntry(key, value);
|
|
81
|
+
if (kind !== null)
|
|
82
|
+
held.add(kind);
|
|
83
|
+
}
|
|
84
|
+
return held.size === 0 ? ['none'] : ARG_CLASSES.filter((kind) => held.has(kind)).sort();
|
|
85
|
+
};
|
|
@@ -15,6 +15,10 @@ export interface CellRow {
|
|
|
15
15
|
ok: number;
|
|
16
16
|
failed: number;
|
|
17
17
|
tokens: number;
|
|
18
|
+
/** How many of the batch's sessions recorded input, the coverage of `tokens`; the served column sums it. */
|
|
19
|
+
tokenSessions: number;
|
|
20
|
+
/** How many of the batch's prompts (or steps) `tokens` covers: the divisor of a per-prompt mean. */
|
|
21
|
+
tokenPrompts: number;
|
|
18
22
|
durationMs: number;
|
|
19
23
|
}
|
|
20
24
|
export interface StratumRow {
|
|
@@ -35,7 +39,7 @@ export interface Increments {
|
|
|
35
39
|
* Rules 1 and 2: a window names a shell step by its program and subcommand alone, and each flag and each pull of it adds a one-step cell of its own.
|
|
36
40
|
*/
|
|
37
41
|
export declare const cellsOf: (store: Store, graph?: RecallGraph) => CellSet;
|
|
38
|
-
/** A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds. */
|
|
42
|
+
/** A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds. It uses the open gate, which keeps the name shape and length but applies no redactor or exclusion term: a local read sends nothing. */
|
|
39
43
|
export declare const projectedStore: (store: Store, read: ShellReader) => Store;
|
|
40
44
|
/** One account's deposits as a store. A session's id is its skeleton's, which the client hashes per account, so it joins nothing across accounts. */
|
|
41
45
|
export declare const storeOf: (skeletons: readonly SessionSkeleton[]) => Store;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// The cell computation a deposit and a local read share (pool vocabulary spec Section 2, "the deposit form", and Section 4, rules 1 to 6). The pool's ingest turns a batch's skeleton into
|
|
2
2
|
// increments with `incrementsOf`, and a local read derives its cells with `cellsOf` over the store it holds, projected the way a deposit projects it (`projectedStore`). Both end in the
|
|
3
3
|
// one `cellsOf`, so a session gives the same cells at the pool and on the machine. This file reads no file system: the pool's Worker bundles it.
|
|
4
|
+
import { ARG_CLASSES } from './call-shape.js';
|
|
4
5
|
import { bucketFields, deriveCells, ENVIRONMENT_BUCKET, isEnvironmentCell } from './cells.js';
|
|
5
|
-
import { bucketOf, floorOf, projectedSteps } from './pool-wire.js';
|
|
6
|
+
import { bucketOf, floorOf, OTHER, projectedSteps } from './pool-wire.js';
|
|
6
7
|
import { NO_RECALLS } from './recall-consequence.js';
|
|
7
|
-
import { harnessOf } from './schema.js';
|
|
8
|
+
import { USAGE_FIELDS, harnessOf } from './schema.js';
|
|
8
9
|
const floorTokens = (value) => floorOf('tokens', bucketOf('tokens', value));
|
|
9
10
|
const floorUsage = ({ input, cacheWrite, cacheRead, output, thinking }) => ({
|
|
10
11
|
input: floorTokens(input),
|
|
@@ -55,14 +56,25 @@ export const cellsOf = (store, graph = NO_RECALLS) => {
|
|
|
55
56
|
const cells = [...whole.cells, ...marginal.cells.filter(({ motifId }) => named.has(motifId))].sort((a, b) => order(a.motifId, b.motifId) || order(a.frameBucket, b.frameBucket) || order(a.grain, b.grain) || order(a.day, b.day) || Number(a.exposed) - Number(b.exposed));
|
|
56
57
|
return { cells, motifs: [...motifs.values()].sort((a, b) => order(a.motifId, b.motifId)) };
|
|
57
58
|
};
|
|
58
|
-
/** A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds. */
|
|
59
|
+
/** A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds. It uses the open gate, which keeps the name shape and length but applies no redactor or exclusion term: a local read sends nothing. */
|
|
59
60
|
export const projectedStore = (store, read) => {
|
|
60
61
|
const harnesses = new Map([...store.sessions, ...store.subagents].map((session) => [session.session, harnessOf(session)]));
|
|
61
62
|
return { ...store, tools: store.tools.map((tool) => (tool.steps === undefined ? tool : { ...tool, steps: projectedSteps(tool, read, harnesses.get(tool.session) ?? 'claude-code') })) };
|
|
62
63
|
};
|
|
63
64
|
const NO_USAGE = { input: 0, cacheWrite: 0, cacheRead: 0, output: 0, thinking: 0 };
|
|
65
|
+
const isKnown = (transcript) => transcript.harness !== OTHER;
|
|
66
|
+
/** A frame whose own harness is `other` takes its transcript's: the two are the same harness, and a frame cannot name one the transcript does not. A repo size this reader does not know is a size the frame does not name. */
|
|
67
|
+
const frameOf = (frame, harness) => ({
|
|
68
|
+
...frame,
|
|
69
|
+
harness: frame.harness === OTHER ? harness : frame.harness,
|
|
70
|
+
stack: { ...frame.stack, size: frame.stack.size === OTHER ? null : frame.stack.size },
|
|
71
|
+
});
|
|
72
|
+
/** A class this reader does not know counts in `mixed`, the class a prompt that fits no other takes. */
|
|
73
|
+
const classOf = (cls) => (cls === OTHER ? 'mixed' : cls);
|
|
74
|
+
/** A status this reader does not know is a segment the call did not report. */
|
|
75
|
+
const statusOf = (status) => (status === OTHER ? 'none' : status);
|
|
64
76
|
/** A skeleton's transcript as the record `deriveCells` reads. Everything a skeleton leaves out stays empty, so no derivation here can come to depend on it unnoticed. */
|
|
65
|
-
const sessionRecord = (id, parent, { harness, frame }) => ({
|
|
77
|
+
const sessionRecord = (id, parent, spawnedIn, { harness, frame }) => ({
|
|
66
78
|
kind: 'session',
|
|
67
79
|
cv: '',
|
|
68
80
|
sid: '',
|
|
@@ -71,7 +83,7 @@ const sessionRecord = (id, parent, { harness, frame }) => ({
|
|
|
71
83
|
parent,
|
|
72
84
|
agentType: null,
|
|
73
85
|
spawnedBy: null,
|
|
74
|
-
spawnedIn
|
|
86
|
+
spawnedIn,
|
|
75
87
|
rev: 1,
|
|
76
88
|
at: '',
|
|
77
89
|
bytes: 0,
|
|
@@ -87,7 +99,8 @@ const sessionRecord = (id, parent, { harness, frame }) => ({
|
|
|
87
99
|
tools: {},
|
|
88
100
|
files: [],
|
|
89
101
|
fileHashes: [],
|
|
90
|
-
repo
|
|
102
|
+
// The store's salted key, so a read that groups by repo works on pooled data from one store (one machine). It joins no cell.
|
|
103
|
+
repo: frame.repoKey ?? null,
|
|
91
104
|
urls: [],
|
|
92
105
|
searches: [],
|
|
93
106
|
bashCmds: 0,
|
|
@@ -99,9 +112,9 @@ const sessionRecord = (id, parent, { harness, frame }) => ({
|
|
|
99
112
|
costUsd: null,
|
|
100
113
|
usage: NO_USAGE,
|
|
101
114
|
harness,
|
|
102
|
-
frame,
|
|
115
|
+
frame: frameOf(frame, harness),
|
|
103
116
|
});
|
|
104
|
-
const promptRecords = (id, { prompts }) => prompts.map(({ cls, errors, wallSec, usage, explore, frame }, index) => ({
|
|
117
|
+
const promptRecords = (id, { harness, prompts }) => prompts.map(({ cls, errors, wallSec, usage, usageResponses, explore, frame }, index) => ({
|
|
105
118
|
kind: 'prompt',
|
|
106
119
|
v: '',
|
|
107
120
|
session: id,
|
|
@@ -110,7 +123,7 @@ const promptRecords = (id, { prompts }) => prompts.map(({ cls, errors, wallSec,
|
|
|
110
123
|
ts: null,
|
|
111
124
|
wallSec: wallSec === null ? null : floorOf('seconds', wallSec),
|
|
112
125
|
head: '',
|
|
113
|
-
cls,
|
|
126
|
+
cls: classOf(cls),
|
|
114
127
|
tools: [],
|
|
115
128
|
shape: '',
|
|
116
129
|
errors: floorOf('count', errors),
|
|
@@ -120,54 +133,71 @@ const promptRecords = (id, { prompts }) => prompts.map(({ cls, errors, wallSec,
|
|
|
120
133
|
model: frame.model ?? '',
|
|
121
134
|
efforts: frame.effort === null ? [] : [frame.effort],
|
|
122
135
|
skills: [],
|
|
136
|
+
// A category the wire sent as null was never recorded: it reads as a placeholder zero here and is left out of `usageObserved`.
|
|
123
137
|
usage: {
|
|
124
|
-
input: floorOf('tokens', usage.input),
|
|
125
|
-
cacheWrite: floorOf('tokens', usage.cacheWrite),
|
|
126
|
-
cacheRead: floorOf('tokens', usage.cacheRead),
|
|
127
|
-
output: floorOf('tokens', usage.output),
|
|
128
|
-
thinking: floorOf('tokens', usage.thinking),
|
|
138
|
+
input: floorOf('tokens', usage.input ?? 0),
|
|
139
|
+
cacheWrite: floorOf('tokens', usage.cacheWrite ?? 0),
|
|
140
|
+
cacheRead: floorOf('tokens', usage.cacheRead ?? 0),
|
|
141
|
+
output: floorOf('tokens', usage.output ?? 0),
|
|
142
|
+
thinking: floorOf('tokens', usage.thinking ?? 0),
|
|
129
143
|
},
|
|
144
|
+
usageObserved: USAGE_FIELDS.filter((field) => usage[field] !== null),
|
|
145
|
+
...(usageResponses === undefined ? {} : { usageResponses: floorOf('count', usageResponses) }),
|
|
130
146
|
recovery: [],
|
|
131
147
|
// The lowest value of the bucket, as every other bucketed value counts; a prompt that sent none counts none.
|
|
132
148
|
explore: explore === null ? 0 : floorOf('calls', explore),
|
|
133
149
|
act: 0,
|
|
134
150
|
bigrams: {},
|
|
135
151
|
contextFingerprint: null,
|
|
136
|
-
frame,
|
|
152
|
+
frame: frameOf(frame, harness),
|
|
137
153
|
}));
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
targetBase: null,
|
|
152
|
-
keys: [],
|
|
153
|
-
durationMs,
|
|
154
|
-
toolUseId: '',
|
|
155
|
-
harnessVersion: null,
|
|
156
|
-
side,
|
|
157
|
-
prompt,
|
|
158
|
-
steps: steps.map(({ action, native, identity, status: reported, parent, tokens, ms }, seg) => ({
|
|
159
|
-
action,
|
|
160
|
-
native,
|
|
161
|
-
identity,
|
|
162
|
-
status: reported,
|
|
163
|
-
parent,
|
|
164
|
-
seg,
|
|
165
|
-
edges: [],
|
|
166
|
-
cost: { tokens: tokens === null ? null : floorOf('tokens', tokens), durationMs: ms === null ? null : floorOf('ms', ms) },
|
|
154
|
+
/** A step's place as the store names it: its call's position and its segment. A transcript's steps are numbered across its calls in order, which is what an edge's target counts. */
|
|
155
|
+
const stepPlaces = ({ tools }) => tools.flatMap(({ steps }, n) => steps.map((_, seg) => ({ n, seg })));
|
|
156
|
+
const toolRecords = (id, main, transcript, spawner) => {
|
|
157
|
+
const own = stepPlaces(transcript);
|
|
158
|
+
const spawning = spawner === null ? [] : stepPlaces(spawner);
|
|
159
|
+
return transcript.tools.map(({ side, prompt, status, durationMs, errorCode, keys, targetClass, argClass, steps }, n) => ({
|
|
160
|
+
kind: 'tool',
|
|
161
|
+
cv: '',
|
|
162
|
+
sid: '',
|
|
163
|
+
session: id,
|
|
164
|
+
rev: 1,
|
|
165
|
+
parent: main,
|
|
166
|
+
agentType: null,
|
|
167
167
|
n,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
ts: null,
|
|
169
|
+
tool: steps[0]?.native ?? '',
|
|
170
|
+
status: statusOf(status),
|
|
171
|
+
target: null,
|
|
172
|
+
targetBase: null,
|
|
173
|
+
// The names the wire carried, which are the only ones a skeleton-derived store ever holds; `keysDropped` is a count of names not sent and has no stored form.
|
|
174
|
+
keys: keys ?? [],
|
|
175
|
+
...(targetClass === undefined ? {} : { targetClass }),
|
|
176
|
+
...(argClass === undefined ? {} : { argClass: ARG_CLASSES.filter((kind) => argClass.includes(kind)).sort() }),
|
|
177
|
+
durationMs,
|
|
178
|
+
toolUseId: '',
|
|
179
|
+
harnessVersion: null,
|
|
180
|
+
side,
|
|
181
|
+
prompt,
|
|
182
|
+
...(errorCode === null ? {} : { errorCode }),
|
|
183
|
+
steps: steps.map(({ action, native, identity, status: reported, parent, tokens, ms, edges }, seg) => ({
|
|
184
|
+
action,
|
|
185
|
+
native,
|
|
186
|
+
identity,
|
|
187
|
+
status: statusOf(reported),
|
|
188
|
+
parent,
|
|
189
|
+
seg,
|
|
190
|
+
// A target is read back as the call position and segment it names, in this transcript or, for a `spawned` edge, in the spawning one. A target the transcript does not hold names nothing and is left out, and so does a kind this reader does not know (`other`).
|
|
191
|
+
edges: (edges ?? []).flatMap(({ kind, target }) => {
|
|
192
|
+
const place = (kind === 'spawned' ? spawning : own)[target];
|
|
193
|
+
return place === undefined || kind === OTHER ? [] : [{ kind, n: place.n, seg: place.seg }];
|
|
194
|
+
}),
|
|
195
|
+
cost: { tokens: tokens === null ? null : floorOf('tokens', tokens), durationMs: ms === null ? null : floorOf('ms', ms) },
|
|
196
|
+
n,
|
|
197
|
+
frameId: '',
|
|
198
|
+
})),
|
|
199
|
+
}));
|
|
200
|
+
};
|
|
171
201
|
/** One account's deposits as a store. A session's id is its skeleton's, which the client hashes per account, so it joins nothing across accounts. */
|
|
172
202
|
export const storeOf = (skeletons) => {
|
|
173
203
|
const store = { sessions: [], prompts: [], errors: [], subagents: [], tools: [], newerCaptures: 0 };
|
|
@@ -175,10 +205,13 @@ export const storeOf = (skeletons) => {
|
|
|
175
205
|
const nameOf = (index) => `${id}#${String(index)}`;
|
|
176
206
|
const main = nameOf(0);
|
|
177
207
|
transcripts.forEach((transcript, index) => {
|
|
208
|
+
if (!isKnown(transcript))
|
|
209
|
+
return;
|
|
178
210
|
const session = nameOf(index);
|
|
179
|
-
|
|
211
|
+
const spawner = transcript.spawnedIn === null ? null : (transcripts[transcript.spawnedIn] ?? null);
|
|
212
|
+
(index === 0 ? store.sessions : store.subagents).push(sessionRecord(session, index === 0 ? null : main, transcript.spawnedIn === null ? null : nameOf(transcript.spawnedIn), transcript));
|
|
180
213
|
store.prompts.push(...promptRecords(session, transcript));
|
|
181
|
-
store.tools.push(...toolRecords(session, main, transcript));
|
|
214
|
+
store.tools.push(...toolRecords(session, main, transcript, spawner));
|
|
182
215
|
});
|
|
183
216
|
}
|
|
184
217
|
return store;
|
|
@@ -190,7 +223,8 @@ export const incrementsOf = (skeleton) => {
|
|
|
190
223
|
for (const cell of derived.cells) {
|
|
191
224
|
const exposed = cell.exposed ? 1 : 0;
|
|
192
225
|
const key = `${cell.motifId}/${cell.frameBucket}/${cell.grain}/${cell.day}/${String(exposed)}`;
|
|
193
|
-
|
|
226
|
+
// The served column is a running sum, so a cell no session of this batch recorded tokens for adds nothing to it; the sum itself holds only what was observed, and `tokenSessions` and `tokenPrompts` say how much of the cell it covers.
|
|
227
|
+
cells.set(key, { key, harnessBucket: isEnvironmentCell(cell) ? ENVIRONMENT_BUCKET : bucketFields(cell.frameBucket).harnessBucket, motifId: cell.motifId, frameBucket: cell.frameBucket, grain: cell.grain, day: cell.day, exposed, sessions: cell.sessions, ok: cell.ok, failed: cell.failed, tokens: cell.tokens ?? 0, tokenSessions: cell.tokenSessions ?? 0, tokenPrompts: cell.tokenPrompts ?? 0, durationMs: cell.durationMs });
|
|
194
228
|
}
|
|
195
229
|
// A session counts once per value, so each value is one session of this batch.
|
|
196
230
|
const strata = new Map();
|
package/dist/sessions/cells.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Store } from './store.js';
|
|
|
6
6
|
export type Grain = 'day' | 'week';
|
|
7
7
|
export interface Cell {
|
|
8
8
|
motifId: string;
|
|
9
|
-
/** `harness/versionBucket/model`, public names (`frameBucketOf`). Effort is not in it (rule 3). */
|
|
9
|
+
/** `harness/versionBucket/model`, public names (`frameBucketOf`), or `env:<platform>:<shell>` for a (name, `platform`, `shell`) cell, which names no harness, version, or model (`ENVIRONMENT_BUCKET`). Effort is not in it (rule 3). */
|
|
10
10
|
frameBucket: string;
|
|
11
11
|
/** What `day` names: a UTC day, or a UTC week. A read takes the grain it answers from and never the other. */
|
|
12
12
|
grain: Grain;
|
|
@@ -20,8 +20,15 @@ export interface Cell {
|
|
|
20
20
|
ok: number;
|
|
21
21
|
/** Windows whose last step failed; a `class` cell: prompts with an error. A step with no result is in neither. */
|
|
22
22
|
failed: number;
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Summed over each window's steps; a `class` cell: the whole usage of the prompts that recorded input, counting only the categories each recorded (a placeholder zero beside
|
|
25
|
+
* `usageObserved` is not a measurement). Null when no session recorded any: absent, never a measured 0.
|
|
26
|
+
*/
|
|
27
|
+
tokens: number | null;
|
|
28
|
+
/** How many of `sessions` recorded the cost `tokens` sums (a class cell: input; a step cell: a step cost), which is the coverage the `tokens` figure has (a session that recorded input but not every category still counts as covered, so the figure can be partial by category); less than `sessions` makes it a partial sum. Absent on a cell read back from a stored sum written before the pool held coverage. */
|
|
29
|
+
tokenSessions?: number;
|
|
30
|
+
/** How many of the cell's prompts the `tokens` figure covers (a step cell counts steps): the divisor of a per-prompt mean, since `tokens` holds only prompts that recorded input. Absent like `tokenSessions`. */
|
|
31
|
+
tokenPrompts?: number;
|
|
25
32
|
durationMs: number;
|
|
26
33
|
/** Whether these counts sit inside a shown recall's consequence window, wholly or in part. It is part of the cell's key, so a motif's exposed and unexposed counts stay apart. */
|
|
27
34
|
exposed: boolean;
|
|
@@ -34,6 +41,27 @@ export declare const EFFORT_CLASS = "effort:";
|
|
|
34
41
|
* carries `week` and `exposure` but no frame axis: rule 3 bars joining `effort` to `model` or `harnessVersion`, and the pair would be four dimensions over `q_dims`.
|
|
35
42
|
*/
|
|
36
43
|
export declare const effortClass: (effort: string, thinking: number) => string;
|
|
44
|
+
/**
|
|
45
|
+
* The two day-cell families of a failed call by its `errorCode` (t868): `error:<code>/<name>` keeps the model and no version beside the code, and `error-version:<code>/<name>` keeps the harness version bucket and no model
|
|
46
|
+
* (`versionBucketOf`). Each joins three dimensions: name, code, and either model or version. A class motif with no prompt class in it, so a read of prompt classes skips both by prefix (`isPromptClass`).
|
|
47
|
+
*/
|
|
48
|
+
export declare const ERROR_CLASS = "error:";
|
|
49
|
+
export declare const ERROR_VERSION_CLASS = "error-version:";
|
|
50
|
+
/**
|
|
51
|
+
* The week cell of what followed a failed call (t868): `recovery:<code>/<failed action>><next action>`, where the next action is that of the call right after it in the same prompt, or `none` when nothing
|
|
52
|
+
* followed. It keeps the harness version bucket and no model, so it joins three dimensions: the failed-to-next edge, code, and version. A class motif with no prompt class in it, like the others.
|
|
53
|
+
*/
|
|
54
|
+
export declare const RECOVERY_CLASS = "recovery:";
|
|
55
|
+
export declare const NO_NEXT = "none";
|
|
56
|
+
export declare const recoveryClass: (code: string, failed: string, next: string) => string;
|
|
57
|
+
/** The three parts of a recovery class name, or null for a name that is not one. An action name holds no `>` or `/`, and a code holds no `/`. */
|
|
58
|
+
export declare const parseRecoveryClass: (cls: string) => {
|
|
59
|
+
code: string;
|
|
60
|
+
failed: string;
|
|
61
|
+
next: string;
|
|
62
|
+
} | null;
|
|
63
|
+
/** Whether a class motif names a prompt class, as opposed to one of the side cells (effort, or a failed call's code or what followed it). */
|
|
64
|
+
export declare const isPromptClass: (cls: string | null) => boolean;
|
|
37
65
|
/** The frame bucket of a cell that joins no axis. */
|
|
38
66
|
export declare const NO_AXES = "-";
|
|
39
67
|
/**
|
|
@@ -43,6 +71,8 @@ export declare const NO_AXES = "-";
|
|
|
43
71
|
export declare const harnessBucket: (version: string | null) => string;
|
|
44
72
|
/** Only what most comparisons cross: platform, shell, and stack are on the frame and in the records, so a later extractor can add them without new capture. */
|
|
45
73
|
export declare const frameBucketOf: (frame: Frame) => string;
|
|
74
|
+
/** The frame bucket of a cell that keeps the version and drops the model: `harness/versionBucket/-`. */
|
|
75
|
+
export declare const versionBucketOf: (frame: Frame) => string;
|
|
46
76
|
export declare const bucketFields: (bucket: string) => {
|
|
47
77
|
harness: string;
|
|
48
78
|
harnessBucket: string;
|
package/dist/sessions/cells.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// The cell (recall spec Section 2.5): counts for one motif in one frame bucket in one UTC week (or, for a live cell, one UTC day), order 0. Cells are computed from the capture records, which
|
|
2
2
|
// stay the primary record, so a cell file can be dropped and rebuilt and any later derivation can run over old data. Nothing here reads raw.
|
|
3
3
|
import { closedFrame, utcDay } from './frame.js';
|
|
4
|
-
import { classMotif, motifsOf, stepsOfCall } from './motif.js';
|
|
4
|
+
import { classMotif, motifName, motifsOf, stepsOfCall } from './motif.js';
|
|
5
5
|
import { bucketOf } from './pool-wire.js';
|
|
6
|
-
import { CALL_SHAPE_METHOD, cellRef, contentHash, METHODS } from './read.js';
|
|
6
|
+
import { CALL_SHAPE_METHOD, cellRef, contentHash, METHODS, RECOVERY_EDGE_METHOD } from './read.js';
|
|
7
|
+
import { recoveryCallsOf } from './recovery-calls.js';
|
|
7
8
|
import { NO_RECALLS } from './recall-consequence.js';
|
|
8
|
-
import { EXTRACTOR_VERSION, harnessOf } from './schema.js';
|
|
9
|
+
import { ERROR_CODES, EXTRACTOR_VERSION, harnessOf, observesUsage } from './schema.js';
|
|
9
10
|
export const UNKNOWN = 'unknown';
|
|
10
11
|
/** The class name an effort cell carries: a class motif with no prompt class in it, so a read of prompt classes skips it by this prefix. */
|
|
11
12
|
export const EFFORT_CLASS = 'effort:';
|
|
@@ -14,6 +15,26 @@ export const EFFORT_CLASS = 'effort:';
|
|
|
14
15
|
* carries `week` and `exposure` but no frame axis: rule 3 bars joining `effort` to `model` or `harnessVersion`, and the pair would be four dimensions over `q_dims`.
|
|
15
16
|
*/
|
|
16
17
|
export const effortClass = (effort, thinking) => `${EFFORT_CLASS}${effort}/thinking:${String(bucketOf('tokens', thinking))}`;
|
|
18
|
+
/**
|
|
19
|
+
* The two day-cell families of a failed call by its `errorCode` (t868): `error:<code>/<name>` keeps the model and no version beside the code, and `error-version:<code>/<name>` keeps the harness version bucket and no model
|
|
20
|
+
* (`versionBucketOf`). Each joins three dimensions: name, code, and either model or version. A class motif with no prompt class in it, so a read of prompt classes skips both by prefix (`isPromptClass`).
|
|
21
|
+
*/
|
|
22
|
+
export const ERROR_CLASS = 'error:';
|
|
23
|
+
export const ERROR_VERSION_CLASS = 'error-version:';
|
|
24
|
+
/**
|
|
25
|
+
* The week cell of what followed a failed call (t868): `recovery:<code>/<failed action>><next action>`, where the next action is that of the call right after it in the same prompt, or `none` when nothing
|
|
26
|
+
* followed. It keeps the harness version bucket and no model, so it joins three dimensions: the failed-to-next edge, code, and version. A class motif with no prompt class in it, like the others.
|
|
27
|
+
*/
|
|
28
|
+
export const RECOVERY_CLASS = 'recovery:';
|
|
29
|
+
export const NO_NEXT = 'none';
|
|
30
|
+
export const recoveryClass = (code, failed, next) => `${RECOVERY_CLASS}${code}/${failed}>${next}`;
|
|
31
|
+
/** The three parts of a recovery class name, or null for a name that is not one. An action name holds no `>` or `/`, and a code holds no `/`. */
|
|
32
|
+
export const parseRecoveryClass = (cls) => {
|
|
33
|
+
const found = /^recovery:([^/]+)\/([^>/]+)>([^>/]+)$/.exec(cls);
|
|
34
|
+
return found === null ? null : { code: found[1] ?? '', failed: found[2] ?? '', next: found[3] ?? '' };
|
|
35
|
+
};
|
|
36
|
+
/** Whether a class motif names a prompt class, as opposed to one of the side cells (effort, or a failed call's code or what followed it). */
|
|
37
|
+
export const isPromptClass = (cls) => cls !== null && ![EFFORT_CLASS, ERROR_CLASS, ERROR_VERSION_CLASS, RECOVERY_CLASS].some((prefix) => cls.startsWith(prefix));
|
|
17
38
|
/** The frame bucket of a cell that joins no axis. */
|
|
18
39
|
export const NO_AXES = '-';
|
|
19
40
|
const ONE_OPERATOR = 1;
|
|
@@ -29,6 +50,8 @@ export const harnessBucket = (version) => {
|
|
|
29
50
|
};
|
|
30
51
|
/** Only what most comparisons cross: platform, shell, and stack are on the frame and in the records, so a later extractor can add them without new capture. */
|
|
31
52
|
export const frameBucketOf = (frame) => [frame.harness, harnessBucket(frame.harnessVersion), frame.model ?? UNKNOWN].join('/');
|
|
53
|
+
/** The frame bucket of a cell that keeps the version and drops the model: `harness/versionBucket/-`. */
|
|
54
|
+
export const versionBucketOf = (frame) => [frame.harness, harnessBucket(frame.harnessVersion), NO_AXES].join('/');
|
|
32
55
|
export const bucketFields = (bucket) => {
|
|
33
56
|
const [harness = UNKNOWN, version = UNKNOWN, model = UNKNOWN] = bucket.split('/');
|
|
34
57
|
return { harness, harnessBucket: version, model };
|
|
@@ -69,7 +92,9 @@ export const cellIdOf = (cell) => {
|
|
|
69
92
|
cellIds.set(cell, id);
|
|
70
93
|
return id;
|
|
71
94
|
};
|
|
72
|
-
|
|
95
|
+
/** The usage categories a prompt's tokens add up: thinking is part of the output already billed, so it is not summed. */
|
|
96
|
+
const TOKEN_FIELDS = ['input', 'cacheWrite', 'cacheRead', 'output'];
|
|
97
|
+
const newTally = (motifId, frameBucket, grain, day, exposed) => ({ motifId, frameBucket, grain, day, sessions: new Set(), ok: 0, failed: 0, tokens: 0, recorded: new Set(), recordedUnits: 0, durationMs: 0, exposed });
|
|
73
98
|
const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
74
99
|
/** A frame for a record captured before frames existed, from the fields it does hold, held to the same public vocabulary. */
|
|
75
100
|
const oldFrame = (parts) => closedFrame({
|
|
@@ -143,11 +168,60 @@ export const deriveCells = (store, graph = NO_RECALLS) => {
|
|
|
143
168
|
if (status === 'error')
|
|
144
169
|
tally.failed += 1;
|
|
145
170
|
for (const step of window) {
|
|
146
|
-
|
|
171
|
+
// A step with no recorded cost has no tokens to count, so it joins neither the sum nor the coverage (its duration is still counted).
|
|
172
|
+
if (step.tokens !== null) {
|
|
173
|
+
tally.tokens += step.tokens;
|
|
174
|
+
tally.recorded.add(first.parent);
|
|
175
|
+
tally.recordedUnits += 1;
|
|
176
|
+
}
|
|
147
177
|
tally.durationMs += step.durationMs;
|
|
148
178
|
}
|
|
149
179
|
}
|
|
150
180
|
}
|
|
181
|
+
// What followed each failed call that has a code (t868): a week cell per (code, failed action, next action).
|
|
182
|
+
// A call is read by its first step's action, as a skeleton's is; a record captured before steps has the action its native name maps to.
|
|
183
|
+
const calls = line.map((call) => ({ side: call.side, prompt: call.prompt, status: call.status, steps: stepsOfCall(call, harness) }));
|
|
184
|
+
for (const [at, tool] of line.entries()) {
|
|
185
|
+
const followed = recoveryCallsOf(calls, at);
|
|
186
|
+
if (followed === null || tool.errorCode === undefined)
|
|
187
|
+
continue;
|
|
188
|
+
const code = ERROR_CODES.has(tool.errorCode) ? tool.errorCode : 'other';
|
|
189
|
+
const motif = classMotif(recoveryClass(code, calls[at]?.steps[0]?.action ?? 'other', followed.next[0] ?? NO_NEXT));
|
|
190
|
+
motifs.set(motif.motifId, motif);
|
|
191
|
+
const tally = tallyAt(motif, versionBucketOf(frame), 'week', weekStartOf(frame.day ?? UNKNOWN), false);
|
|
192
|
+
tally.sessions.add(first.parent);
|
|
193
|
+
tally.failed += 1;
|
|
194
|
+
}
|
|
195
|
+
// The failed calls by their `errorCode` (t868). Rule 6: a day cell exists for a known day only.
|
|
196
|
+
const failedDay = frame.day ?? UNKNOWN;
|
|
197
|
+
for (const tool of failedDay === UNKNOWN ? [] : line) {
|
|
198
|
+
if (tool.status !== 'error' || tool.errorCode === undefined)
|
|
199
|
+
continue;
|
|
200
|
+
const code = ERROR_CODES.has(tool.errorCode) ? tool.errorCode : 'other';
|
|
201
|
+
for (const counted of stepsOfCall(tool, harness)) {
|
|
202
|
+
if (counted.status !== 'error')
|
|
203
|
+
continue;
|
|
204
|
+
// The call's name at both levels: its action alone, and a named call (a flag or a pull would be a second dimension, so those steps are named by program and subcommand alone upstream).
|
|
205
|
+
for (const { motif: named } of motifsOf([counted])) {
|
|
206
|
+
if (named.level === 'named' && counted.identity === null)
|
|
207
|
+
continue;
|
|
208
|
+
const name = motifName(named);
|
|
209
|
+
for (const [prefix, bucket] of [[ERROR_CLASS, `${frame.harness}/${NO_AXES}/${frame.model ?? UNKNOWN}`], [ERROR_VERSION_CLASS, versionBucketOf(frame)]]) {
|
|
210
|
+
const motif = classMotif(`${prefix}${code}/${name}`);
|
|
211
|
+
motifs.set(motif.motifId, motif);
|
|
212
|
+
const tally = tallyAt(motif, bucket, 'day', failedDay, false);
|
|
213
|
+
tally.sessions.add(first.parent);
|
|
214
|
+
tally.failed += 1;
|
|
215
|
+
if (counted.tokens !== null) {
|
|
216
|
+
tally.tokens += counted.tokens;
|
|
217
|
+
tally.recorded.add(first.parent);
|
|
218
|
+
tally.recordedUnits += 1;
|
|
219
|
+
}
|
|
220
|
+
tally.durationMs += counted.durationMs;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
151
225
|
}
|
|
152
226
|
for (const prompt of store.prompts) {
|
|
153
227
|
const session = sessions.get(prompt.session);
|
|
@@ -160,14 +234,20 @@ export const deriveCells = (store, graph = NO_RECALLS) => {
|
|
|
160
234
|
tally.failed += 1;
|
|
161
235
|
else
|
|
162
236
|
tally.ok += 1;
|
|
163
|
-
|
|
164
|
-
|
|
237
|
+
// A prompt that recorded no input has no whole usage to count, and a category it never recorded is a placeholder zero, so neither enters the sum.
|
|
238
|
+
if (observesUsage(prompt, 'input')) {
|
|
239
|
+
tally.recorded.add(prompt.session);
|
|
240
|
+
tally.recordedUnits += 1;
|
|
241
|
+
for (const field of TOKEN_FIELDS)
|
|
242
|
+
if (observesUsage(prompt, field))
|
|
243
|
+
tally.tokens += prompt.usage[field];
|
|
244
|
+
}
|
|
165
245
|
tally.durationMs += (prompt.wallSec ?? 0) * 1000;
|
|
166
246
|
}
|
|
167
247
|
}
|
|
168
248
|
const cells = [...tallies.values()]
|
|
169
249
|
.sort((a, b) => compare(a.motifId, b.motifId) || compare(a.frameBucket, b.frameBucket) || compare(a.grain, b.grain) || compare(a.day, b.day) || Number(a.exposed) - Number(b.exposed))
|
|
170
|
-
.map((tally) => ({ motifId: tally.motifId, frameBucket: tally.frameBucket, grain: tally.grain, day: tally.day, sessions: tally.sessions.size, clusters: ONE_OPERATOR, ok: tally.ok, failed: tally.failed, tokens: tally.tokens, durationMs: tally.durationMs, exposed: tally.exposed }));
|
|
250
|
+
.map((tally) => ({ motifId: tally.motifId, frameBucket: tally.frameBucket, grain: tally.grain, day: tally.day, sessions: tally.sessions.size, clusters: ONE_OPERATOR, ok: tally.ok, failed: tally.failed, tokens: tally.recorded.size === 0 ? null : tally.tokens, tokenSessions: tally.recorded.size, tokenPrompts: tally.recordedUnits, durationMs: tally.durationMs, exposed: tally.exposed }));
|
|
171
251
|
return { cells, motifs: [...motifs.values()].sort((a, b) => compare(a.motifId, b.motifId)) };
|
|
172
252
|
};
|
|
173
253
|
export const serializeCells = (cells) => cells.map((cell) => `${JSON.stringify(cell)}\n`).join('');
|
|
@@ -176,6 +256,6 @@ const errorRowsRead = (errors) => errors.map(({ session, agent, toolUseId, promp
|
|
|
176
256
|
export const poolOf = (set, errors) => ({
|
|
177
257
|
cells: set.cells,
|
|
178
258
|
motifs: new Map(set.motifs.map((motif) => [motif.motifId, motif])),
|
|
179
|
-
version: contentHash([EXTRACTOR_VERSION, METHODS, CALL_SHAPE_METHOD, set.cells.map(cellIdOf), errorRowsRead(errors)]),
|
|
259
|
+
version: contentHash([EXTRACTOR_VERSION, METHODS, CALL_SHAPE_METHOD, RECOVERY_EDGE_METHOD, set.cells.map(cellIdOf), errorRowsRead(errors)]),
|
|
180
260
|
lastDay: set.cells.reduce((last, cell) => (cell.day !== UNKNOWN && (last === null || cell.day > last) ? cell.day : last), null),
|
|
181
261
|
});
|