@manyoars/oars 0.3.0 → 0.4.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 +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,494 @@
|
|
|
1
|
+
// The OpenTelemetry GenAI source: an agent trace in OTLP JSON becomes the same capture records a Claude Code transcript does.
|
|
2
|
+
//
|
|
3
|
+
// CONVENTION-DERIVED: written from the public GenAI semantic conventions (github.com/open-telemetry/semantic-conventions-genai @ e6658f1, Apache-2.0; the
|
|
4
|
+
// conventions moved there from open-telemetry/semantic-conventions, which has a release but only "Moved" stubs for GenAI, and the new repository has no release)
|
|
5
|
+
// and the OTLP JSON encoding (opentelemetry-proto v1.11.1, docs/specification.md:453-484, trace.proto:96-327; the file exporter,
|
|
6
|
+
// opentelemetry-specification specification/protocol/file-exporter.md:64-75, writes one `ExportTraceServiceRequest` a line with no order guarantee). Nothing was
|
|
7
|
+
// copied and no real trace was read: `docs/sessions-store.md` names the one hand run that would prove the reader. Every GenAI attribute below is marked Development
|
|
8
|
+
// in the registry, so each one is optional here: a missing or renamed attribute means less is read, never a throw.
|
|
9
|
+
//
|
|
10
|
+
// Where it reads from (`otelRoot`): `$OARS_OTEL_TRACES`, else `<config>/oars/otel-traces` (a configured folder, so the nightly run is deterministic). Every `*.jsonl` and
|
|
11
|
+
// `*.json` file in it is read; the traces of all files are joined by `traceId` first, because a collector may split one trace across files.
|
|
12
|
+
//
|
|
13
|
+
// How a trace becomes a session:
|
|
14
|
+
// session one `traceId`; traces that share `gen_ai.conversation.id` are one session, each trace a turn in start-time order. An agent span nested inside another
|
|
15
|
+
// agent's (directly or through an agent-as-tool `execute_tool` span) is its own session `<traceId>-<spanId>`, a subagent of the span's session.
|
|
16
|
+
// prompt one per root agent span (`invoke_agent`, `invoke_workflow`, any `invoke_*` operation), else per trace root; its text is the last user message of
|
|
17
|
+
// `gen_ai.input.messages` (Opt-In: absent by default, and then the turn is written content-less, as an image-only prompt is).
|
|
18
|
+
// model call a span whose `gen_ai.operation.name` is `chat`, `generate_content`, or `text_completion`: one assistant line, its `requestId` the span id.
|
|
19
|
+
// tool call an `execute_tool` span: a block in the model call that ended before it began (or whose output messages name its `gen_ai.tool.call.id`). It failed iff the
|
|
20
|
+
// span's status code is ERROR (2) or `error.type` is set (stable, recording-errors.md): the error line is `error.type: status.message`, never the result.
|
|
21
|
+
// usage `gen_ai.usage.*`; `input_tokens` includes the cache reads and writes (the registry says it SHOULD), and capture's fields are disjoint, so the uncached
|
|
22
|
+
// part is input - read - write. A figure the span did not report is not written as zero.
|
|
23
|
+
// harness `otel`; the version slot holds the emitting framework from the instrumentation scope name (a closed list plus `other`). Nothing names a working folder.
|
|
24
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
25
|
+
import { homedir } from 'node:os';
|
|
26
|
+
import { basename, join } from 'node:path';
|
|
27
|
+
import { createHash } from 'node:crypto';
|
|
28
|
+
/** The folder of OTLP JSON trace files for an environment; a test passes its own so it never reads a real one. */
|
|
29
|
+
export const otelRoot = (env, home = homedir(), platform = process.platform) => {
|
|
30
|
+
const set = env.OARS_OTEL_TRACES;
|
|
31
|
+
if (set !== undefined && set !== '')
|
|
32
|
+
return set;
|
|
33
|
+
const config = platform === 'win32' ? (env.APPDATA ?? join(home, 'AppData', 'Roaming')) : (env.XDG_CONFIG_HOME ?? join(home, '.config'));
|
|
34
|
+
return join(config, 'oars', 'otel-traces');
|
|
35
|
+
};
|
|
36
|
+
const isObj = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
|
+
const str = (value) => (typeof value === 'string' && value !== '' ? value : undefined);
|
|
38
|
+
const parse = (text) => {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(text);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const missing = (error) => error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
47
|
+
const reasonOf = (error, file) => (error instanceof Error ? error.message : String(error)).split(file).join(basename(file));
|
|
48
|
+
const digest = (text) => createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
49
|
+
/** An OTLP `AnyValue` as plain data (the proto3 JSON mapping: `intValue` is a decimal string for int64). */
|
|
50
|
+
const anyOf = (value) => {
|
|
51
|
+
if (!isObj(value))
|
|
52
|
+
return undefined;
|
|
53
|
+
if (typeof value.stringValue === 'string')
|
|
54
|
+
return value.stringValue;
|
|
55
|
+
if (value.intValue !== undefined)
|
|
56
|
+
return Number(value.intValue);
|
|
57
|
+
if (typeof value.doubleValue === 'number')
|
|
58
|
+
return value.doubleValue;
|
|
59
|
+
if (typeof value.boolValue === 'boolean')
|
|
60
|
+
return value.boolValue;
|
|
61
|
+
if (isObj(value.arrayValue))
|
|
62
|
+
return (Array.isArray(value.arrayValue.values) ? value.arrayValue.values : []).map(anyOf);
|
|
63
|
+
if (isObj(value.kvlistValue))
|
|
64
|
+
return attributesOf(value.kvlistValue.values);
|
|
65
|
+
return undefined;
|
|
66
|
+
};
|
|
67
|
+
const attributesOf = (list) => {
|
|
68
|
+
const out = {};
|
|
69
|
+
for (const entry of Array.isArray(list) ? list : []) {
|
|
70
|
+
const key = isObj(entry) ? str(entry.key) : undefined;
|
|
71
|
+
if (key !== undefined && isObj(entry))
|
|
72
|
+
out[key] = anyOf(entry.value);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
};
|
|
76
|
+
/** A structured attribute (messages, arguments) is an AnyValue; an exporter that flattens it writes its JSON text instead, and both are read. */
|
|
77
|
+
const structured = (value) => (typeof value === 'string' ? (parse(value) ?? value) : value);
|
|
78
|
+
/** Unix nanoseconds (a decimal string, or a number) as whole milliseconds; undefined when absent, zero, or not an integer. */
|
|
79
|
+
const msOf = (value) => {
|
|
80
|
+
try {
|
|
81
|
+
const nanos = BigInt(typeof value === 'number' ? Math.trunc(value) : String(value));
|
|
82
|
+
return nanos > 0n ? Number(nanos / 1000000n) : undefined;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/** The frameworks the conventions name (gen-ai-agent-spans.md:180-195, execute-tool.md:675-690) by a word of their instrumentation scope name; anything else is `other`. */
|
|
89
|
+
const FRAMEWORKS = [
|
|
90
|
+
['langchain', /langchain|langgraph/i],
|
|
91
|
+
['openai-agents', /openai[-_.]agents/i],
|
|
92
|
+
['crewai', /crewai/i],
|
|
93
|
+
['google-adk', /google[-_.]adk|\badk\b/i],
|
|
94
|
+
['pydantic-ai', /pydantic[-_.]ai/i],
|
|
95
|
+
['strands', /strands/i],
|
|
96
|
+
['microsoft-agent-framework', /agent[-_.]framework/i],
|
|
97
|
+
['semantic-kernel', /semantic[-_.]kernel/i],
|
|
98
|
+
['llamaindex', /llama[-_.]?index/i],
|
|
99
|
+
['agno', /agno/i],
|
|
100
|
+
];
|
|
101
|
+
const frameworkOf = (scope) => FRAMEWORKS.find(([, pattern]) => pattern.test(scope))?.[0] ?? 'other';
|
|
102
|
+
/** `ExportTraceServiceRequest` objects (proto JSON, camelCase) as spans. */
|
|
103
|
+
const spansOf = (request, order, file) => {
|
|
104
|
+
if (!isObj(request))
|
|
105
|
+
return [];
|
|
106
|
+
const spans = [];
|
|
107
|
+
for (const resource of Array.isArray(request.resourceSpans) ? request.resourceSpans : []) {
|
|
108
|
+
for (const scoped of isObj(resource) && Array.isArray(resource.scopeSpans) ? resource.scopeSpans : []) {
|
|
109
|
+
const scope = isObj(scoped) && isObj(scoped.scope) ? (str(scoped.scope.name) ?? '') : '';
|
|
110
|
+
for (const raw of isObj(scoped) && Array.isArray(scoped.spans) ? scoped.spans : []) {
|
|
111
|
+
if (!isObj(raw))
|
|
112
|
+
continue;
|
|
113
|
+
const trace = str(raw.traceId);
|
|
114
|
+
const id = str(raw.spanId);
|
|
115
|
+
if (trace === undefined || id === undefined)
|
|
116
|
+
continue;
|
|
117
|
+
const status = isObj(raw.status) ? raw.status : {};
|
|
118
|
+
const attrs = attributesOf(raw.attributes);
|
|
119
|
+
order.next += 1;
|
|
120
|
+
spans.push({
|
|
121
|
+
trace,
|
|
122
|
+
id,
|
|
123
|
+
parent: str(raw.parentSpanId),
|
|
124
|
+
name: str(raw.name) ?? '',
|
|
125
|
+
start: msOf(raw.startTimeUnixNano),
|
|
126
|
+
end: msOf(raw.endTimeUnixNano),
|
|
127
|
+
// The status code is an integer enum in proto JSON (trace.proto:327: 2 is ERROR); a string-named enum is read too. `error.type` is stable and means failure whatever the status says.
|
|
128
|
+
failed: status.code === 2 || status.code === 'STATUS_CODE_ERROR' || str(attrs['error.type']) !== undefined,
|
|
129
|
+
statusMessage: str(status.message),
|
|
130
|
+
attrs,
|
|
131
|
+
events: (Array.isArray(raw.events) ? raw.events : []).filter(isObj).map((event) => ({ name: event.name, ...attributesOf(event.attributes) })),
|
|
132
|
+
framework: frameworkOf(scope),
|
|
133
|
+
order: order.next,
|
|
134
|
+
file,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return spans;
|
|
140
|
+
};
|
|
141
|
+
/** The requests of one file: the whole text as one JSON value (a `.json` file), else one request a line (the file exporter's JSON Lines). Throws when nothing parses. */
|
|
142
|
+
const requestsOf = (text) => {
|
|
143
|
+
const whole = parse(text);
|
|
144
|
+
if (isObj(whole))
|
|
145
|
+
return { requests: [whole], bad: [] };
|
|
146
|
+
const lines = text.split('\n').filter((raw) => raw.trim() !== '');
|
|
147
|
+
const requests = lines.map(parse).filter(isObj);
|
|
148
|
+
if (lines.length > 0 && requests.length === 0)
|
|
149
|
+
throw new Error('no parseable line');
|
|
150
|
+
return { requests, bad: lines.filter((raw) => !isObj(parse(raw))) };
|
|
151
|
+
};
|
|
152
|
+
const ops = (span) => {
|
|
153
|
+
const set = str(span.attrs['gen_ai.operation.name']);
|
|
154
|
+
if (set !== undefined)
|
|
155
|
+
return set;
|
|
156
|
+
// Development attributes may be missing or renamed: the span name leads with the operation (client-inference.md:65, execute-tool.md:58).
|
|
157
|
+
const lead = /^(chat|generate_content|text_completion|execute_tool|invoke_agent|invoke_workflow)\b/.exec(span.name)?.[1];
|
|
158
|
+
return lead ?? '';
|
|
159
|
+
};
|
|
160
|
+
const MODEL_OPS = new Set(['chat', 'generate_content', 'text_completion']);
|
|
161
|
+
const isModel = (span) => MODEL_OPS.has(ops(span));
|
|
162
|
+
const isTool = (span) => ops(span) === 'execute_tool';
|
|
163
|
+
const isAgent = (span) => ops(span).startsWith('invoke_');
|
|
164
|
+
const textParts = (message) => {
|
|
165
|
+
const parts = isObj(message) && Array.isArray(message.parts) ? message.parts : [];
|
|
166
|
+
return parts.flatMap((part) => (isObj(part) && part.type === 'text' && str(part.content) !== undefined ? [String(part.content)] : [])).join('\n');
|
|
167
|
+
};
|
|
168
|
+
const messagesOf = (value) => {
|
|
169
|
+
const list = structured(value);
|
|
170
|
+
return Array.isArray(list) ? list.filter(isObj) : [];
|
|
171
|
+
};
|
|
172
|
+
const promptText = (span) => {
|
|
173
|
+
const users = messagesOf(span.attrs['gen_ai.input.messages']).filter((message) => message.role === 'user');
|
|
174
|
+
return textParts(users[users.length - 1]);
|
|
175
|
+
};
|
|
176
|
+
/** The tool call ids a model call's output messages name. */
|
|
177
|
+
const namedCalls = (span) => messagesOf(span.attrs['gen_ai.output.messages']).flatMap((message) => (Array.isArray(message.parts) ? message.parts : []).flatMap((part) => (isObj(part) && part.type === 'tool_call' && str(part.id) !== undefined ? [String(part.id)] : [])));
|
|
178
|
+
const count = (value) => (typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined);
|
|
179
|
+
/** A tool span's error line: `error.type: status.message`, either alone when the other is missing, else the exception event's message. */
|
|
180
|
+
const failureText = (span) => {
|
|
181
|
+
const type = str(span.attrs['error.type']);
|
|
182
|
+
const message = span.statusMessage ?? str(span.events.find((event) => event.name === 'exception')?.['exception.message']);
|
|
183
|
+
if (type !== undefined && message !== undefined)
|
|
184
|
+
return `${type}: ${message}`;
|
|
185
|
+
return type ?? message ?? 'tool failed';
|
|
186
|
+
};
|
|
187
|
+
/** The sessions of a set of spans (every trace of every file). */
|
|
188
|
+
export const otelSessions = (spans) => {
|
|
189
|
+
const byId = new Map(spans.map((span) => [`${span.trace}/${span.id}`, span]));
|
|
190
|
+
const parentOf = (span) => (span.parent === undefined ? undefined : byId.get(`${span.trace}/${span.parent}`));
|
|
191
|
+
// Each lookup below is built once: a file-exporter folder grows without bound and every deposit reads all of it, so nothing here may scan every span per span.
|
|
192
|
+
const chainOf = new Map();
|
|
193
|
+
const ancestors = (span) => {
|
|
194
|
+
const held = chainOf.get(span);
|
|
195
|
+
if (held !== undefined)
|
|
196
|
+
return held;
|
|
197
|
+
const chain = [];
|
|
198
|
+
for (let at = parentOf(span); at !== undefined && chain.length < 256; at = parentOf(at))
|
|
199
|
+
chain.push(at);
|
|
200
|
+
chainOf.set(span, chain);
|
|
201
|
+
return chain;
|
|
202
|
+
};
|
|
203
|
+
const childrenOf = new Map();
|
|
204
|
+
const agentTraces = new Set();
|
|
205
|
+
for (const span of spans) {
|
|
206
|
+
if (isAgent(span))
|
|
207
|
+
agentTraces.add(span.trace);
|
|
208
|
+
if (span.parent === undefined)
|
|
209
|
+
continue;
|
|
210
|
+
const key = `${span.trace}/${span.parent}`;
|
|
211
|
+
const siblings = childrenOf.get(key);
|
|
212
|
+
if (siblings === undefined)
|
|
213
|
+
childrenOf.set(key, [span]);
|
|
214
|
+
else
|
|
215
|
+
siblings.push(span);
|
|
216
|
+
}
|
|
217
|
+
const roots = spans.filter((span) => parentOf(span) === undefined);
|
|
218
|
+
const startOf = (span) => span.start ?? span.end ?? 0;
|
|
219
|
+
const conversationOf = new Map();
|
|
220
|
+
for (const span of [...spans].sort((a, b) => startOf(a) - startOf(b))) {
|
|
221
|
+
const conversation = str(span.attrs['gen_ai.conversation.id']);
|
|
222
|
+
if (conversation !== undefined && !conversationOf.has(span.trace))
|
|
223
|
+
conversationOf.set(span.trace, conversation);
|
|
224
|
+
}
|
|
225
|
+
const tracePart = (trace) => (/^[A-Za-z0-9]{1,64}$/.test(trace) ? trace : digest(trace));
|
|
226
|
+
const sessionOfTrace = (trace) => {
|
|
227
|
+
const conversation = conversationOf.get(trace);
|
|
228
|
+
return conversation === undefined ? tracePart(trace) : `conv-${digest(conversation)}`;
|
|
229
|
+
};
|
|
230
|
+
// A unit is an agent span, or a trace root that is not one: whatever owns the model calls and tools beneath it.
|
|
231
|
+
const unitOf = new Map();
|
|
232
|
+
const agentsAbove = (span) => ancestors(span).find(isAgent);
|
|
233
|
+
const rootUnits = new Set();
|
|
234
|
+
for (const span of spans) {
|
|
235
|
+
if (isAgent(span)) {
|
|
236
|
+
const above = agentsAbove(span);
|
|
237
|
+
if (above === undefined) {
|
|
238
|
+
unitOf.set(span, { span, session: sessionOfTrace(span.trace), spawn: undefined });
|
|
239
|
+
rootUnits.add(span);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// The agents nested in another: found after the roots, outermost first, so a parent's session is known.
|
|
244
|
+
const nested = spans.filter((span) => isAgent(span) && !unitOf.has(span)).sort((a, b) => ancestors(a).length - ancestors(b).length);
|
|
245
|
+
for (const span of nested) {
|
|
246
|
+
const above = agentsAbove(span);
|
|
247
|
+
const parentUnit = above === undefined ? undefined : unitOf.get(above);
|
|
248
|
+
if (parentUnit === undefined)
|
|
249
|
+
continue;
|
|
250
|
+
const between = ancestors(span).slice(0, ancestors(span).indexOf(above ?? span)).find(isTool);
|
|
251
|
+
unitOf.set(span, { span, session: `${tracePart(span.trace)}-${span.id}`, spawn: { parent: parentUnit.session, callId: between === undefined ? span.id : (str(between.attrs['gen_ai.tool.call.id']) ?? between.id), tool: between } });
|
|
252
|
+
}
|
|
253
|
+
// A trace with no agent span: its earliest root is the one unit. The other roots are spans whose parent never reached the data (the root span ends last, so a batch exporter writes it last),
|
|
254
|
+
// and they fall through to that unit like any call outside an agent, instead of each opening a prompt of its own.
|
|
255
|
+
const earliestRoot = new Map();
|
|
256
|
+
for (const root of roots) {
|
|
257
|
+
if (isAgent(root) || agentTraces.has(root.trace))
|
|
258
|
+
continue;
|
|
259
|
+
const held = earliestRoot.get(root.trace);
|
|
260
|
+
if (held === undefined || startOf(root) < startOf(held) || (startOf(root) === startOf(held) && root.order < held.order))
|
|
261
|
+
earliestRoot.set(root.trace, root);
|
|
262
|
+
}
|
|
263
|
+
for (const root of earliestRoot.values()) {
|
|
264
|
+
unitOf.set(root, { span: root, session: sessionOfTrace(root.trace), spawn: undefined });
|
|
265
|
+
rootUnits.add(root);
|
|
266
|
+
}
|
|
267
|
+
// A call outside every agent span (a sibling of it under a custom root) belongs to the trace's first root unit instead of being dropped.
|
|
268
|
+
const firstRootUnit = new Map();
|
|
269
|
+
for (const root of rootUnits) {
|
|
270
|
+
const unit = unitOf.get(root);
|
|
271
|
+
if (unit !== undefined && !firstRootUnit.has(root.trace))
|
|
272
|
+
firstRootUnit.set(root.trace, unit);
|
|
273
|
+
}
|
|
274
|
+
const ownerOf = (span) => [span, ...ancestors(span)].map((candidate) => unitOf.get(candidate)).find((unit) => unit !== undefined) ?? firstRootUnit.get(span.trace);
|
|
275
|
+
const members = new Map();
|
|
276
|
+
for (const span of spans) {
|
|
277
|
+
if (!isModel(span) && !isTool(span))
|
|
278
|
+
continue;
|
|
279
|
+
const owner = ownerOf(span);
|
|
280
|
+
if (owner === undefined)
|
|
281
|
+
continue;
|
|
282
|
+
const held = members.get(owner);
|
|
283
|
+
if (held === undefined)
|
|
284
|
+
members.set(owner, [span]);
|
|
285
|
+
else
|
|
286
|
+
held.push(span);
|
|
287
|
+
}
|
|
288
|
+
const out = new Map();
|
|
289
|
+
const framework = (unit) => unit.span.framework;
|
|
290
|
+
const orderedUnits = [...unitOf.values()].sort((a, b) => startOf(a.span) - startOf(b.span) || a.span.order - b.span.order);
|
|
291
|
+
// A directly nested agent is a call of the parent, though no tool span says so; grouped by the unit that owns its parent span, in one pass.
|
|
292
|
+
const directOf = new Map();
|
|
293
|
+
for (const child of orderedUnits) {
|
|
294
|
+
if (child.spawn === undefined || child.spawn.tool !== undefined)
|
|
295
|
+
continue;
|
|
296
|
+
const owner = ownerOf(parentOf(child.span) ?? child.span);
|
|
297
|
+
if (owner === undefined || owner.session !== child.spawn.parent || !ancestors(child.span).includes(owner.span))
|
|
298
|
+
continue;
|
|
299
|
+
const held = directOf.get(owner);
|
|
300
|
+
if (held === undefined)
|
|
301
|
+
directOf.set(owner, [child.span]);
|
|
302
|
+
else
|
|
303
|
+
held.push(child.span);
|
|
304
|
+
}
|
|
305
|
+
for (const unit of orderedUnits) {
|
|
306
|
+
const entry = out.get(unit.session) ?? { lines: [], mtime: 0, spans: 0, subagent: undefined, file: unit.span.file };
|
|
307
|
+
out.set(unit.session, entry);
|
|
308
|
+
if (unit.spawn !== undefined) {
|
|
309
|
+
const agentName = str(unit.span.attrs['gen_ai.agent.name']);
|
|
310
|
+
entry.subagent = { parent: `otel-${unit.spawn.parent}`, agentType: agentName ?? null, spawnedBy: unit.spawn.callId };
|
|
311
|
+
}
|
|
312
|
+
const base = { sessionId: unit.session, version: framework(unit) };
|
|
313
|
+
const stamp = (ms) => {
|
|
314
|
+
// A missing or zero time says nothing about when: the latest time so far in this session stands.
|
|
315
|
+
if (ms === undefined)
|
|
316
|
+
return entry.mtime;
|
|
317
|
+
entry.mtime = Math.max(entry.mtime, ms);
|
|
318
|
+
return ms;
|
|
319
|
+
};
|
|
320
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
321
|
+
// A directly nested agent is a call of the parent; it is placed among the unit's own spans by start time.
|
|
322
|
+
const direct = new Set(directOf.get(unit) ?? []);
|
|
323
|
+
const mine = [...(members.get(unit) ?? []), ...direct].sort((a, b) => startOf(a) - startOf(b) || a.order - b.order);
|
|
324
|
+
// A first unit with no time of its own is dated by the earliest timed span it holds before it falls back to 0.
|
|
325
|
+
const earliest = mine.reduce((least, span) => {
|
|
326
|
+
const at = span.start ?? span.end;
|
|
327
|
+
return at !== undefined && (least === undefined || at < least) ? at : least;
|
|
328
|
+
}, undefined);
|
|
329
|
+
const begin = stamp(unit.span.start ?? unit.span.end ?? (entry.mtime === 0 ? earliest : undefined));
|
|
330
|
+
const text = promptText(unit.span);
|
|
331
|
+
const pieces = [];
|
|
332
|
+
pieces.push({ t: begin, rank: -1, line: { ...base, type: 'user', uuid: `${unit.session}-${unit.span.id}-u`, timestamp: iso(begin), message: { role: 'user', content: [text === '' ? { type: 'image' } : { type: 'text', text }] } } });
|
|
333
|
+
const calls = [];
|
|
334
|
+
const assistant = (span, requestId) => {
|
|
335
|
+
const content = [];
|
|
336
|
+
const t = Math.max(stamp(span.start ?? span.end), begin);
|
|
337
|
+
const model = str(span.attrs['gen_ai.response.model']) ?? str(span.attrs['gen_ai.request.model']);
|
|
338
|
+
const message = { role: 'assistant', content };
|
|
339
|
+
const line = { ...base, type: 'assistant', uuid: `${unit.session}-${span.id}-a`, requestId, timestamp: iso(t), ...(model === undefined ? {} : { requestedModel: model }), message };
|
|
340
|
+
pieces.push({ t, rank: 0, line });
|
|
341
|
+
return { content, line, message };
|
|
342
|
+
};
|
|
343
|
+
const attach = (callId, tool) => {
|
|
344
|
+
const named = calls.find((call) => call.names.includes(callId));
|
|
345
|
+
const before = calls.findLast((call) => call.end <= startOf(tool));
|
|
346
|
+
const chosen = named ?? before;
|
|
347
|
+
if (chosen !== undefined)
|
|
348
|
+
return chosen;
|
|
349
|
+
return assistant(tool, `${tool.id}-call`);
|
|
350
|
+
};
|
|
351
|
+
const result = (id, tool, failure) => {
|
|
352
|
+
const t = Math.max(stamp(tool.end ?? tool.start), begin);
|
|
353
|
+
pieces.push({
|
|
354
|
+
t,
|
|
355
|
+
rank: 1,
|
|
356
|
+
line: { ...base, type: 'user', uuid: `${unit.session}-${tool.id}-r-${id}`, timestamp: iso(t), noDuration: true, message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, is_error: failure !== undefined, content: failure ?? '' }] } },
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
for (const span of mine) {
|
|
360
|
+
if (direct.has(span)) {
|
|
361
|
+
attach(span.id, span).content.push({ type: 'tool_use', id: span.id, name: 'invoke_agent', native: 'invoke_agent', input: {} });
|
|
362
|
+
result(span.id, span, span.failed ? failureText(span) : undefined);
|
|
363
|
+
}
|
|
364
|
+
else if (isModel(span)) {
|
|
365
|
+
const { content, line, message } = assistant(span, span.id);
|
|
366
|
+
const reply = messagesOf(span.attrs['gen_ai.output.messages']).map(textParts).filter((part) => part !== '').join('\n');
|
|
367
|
+
if (reply !== '')
|
|
368
|
+
content.push({ type: 'text', text: reply });
|
|
369
|
+
const input = count(span.attrs['gen_ai.usage.input_tokens']);
|
|
370
|
+
const output = count(span.attrs['gen_ai.usage.output_tokens']);
|
|
371
|
+
const read = count(span.attrs['gen_ai.usage.cache_read.input_tokens']);
|
|
372
|
+
const write = count(span.attrs['gen_ai.usage.cache_write.input_tokens']);
|
|
373
|
+
const thinking = count(span.attrs['gen_ai.usage.reasoning.output_tokens']);
|
|
374
|
+
const reported = [input, output, read, write, thinking].some((figure) => figure !== undefined);
|
|
375
|
+
if (reported) {
|
|
376
|
+
// `input_tokens` includes the cache reads and writes (registry: it SHOULD), and capture's fields are disjoint; a figure not reported is not written, so it is not read as zero.
|
|
377
|
+
const usage = {
|
|
378
|
+
...(input === undefined ? {} : { input_tokens: Math.max(input - (read ?? 0) - (write ?? 0), 0) }),
|
|
379
|
+
...(read === undefined ? {} : { cache_read_input_tokens: read }),
|
|
380
|
+
...(write === undefined ? {} : { cache_creation_input_tokens: write }),
|
|
381
|
+
...(output === undefined ? {} : { output_tokens: output }),
|
|
382
|
+
...(thinking === undefined ? {} : { output_tokens_details: { thinking_tokens: thinking } }),
|
|
383
|
+
};
|
|
384
|
+
message.usage = usage;
|
|
385
|
+
}
|
|
386
|
+
calls.push({ span, content, line, end: span.end ?? startOf(span), names: namedCalls(span) });
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
const id = str(span.attrs['gen_ai.tool.call.id']) ?? span.id;
|
|
390
|
+
const toolName = str(span.attrs['gen_ai.tool.name']) ?? span.name.replace(/^execute_tool\s*/, '');
|
|
391
|
+
const argsValue = structured(span.attrs['gen_ai.tool.call.arguments']);
|
|
392
|
+
const args = isObj(argsValue) ? argsValue : {};
|
|
393
|
+
const exit = typeof span.attrs['process.exit.code'] === 'number' ? span.attrs['process.exit.code'] : undefined;
|
|
394
|
+
const command = exit !== undefined || str(span.attrs['process.executable.name']) !== undefined;
|
|
395
|
+
const skill = str(span.attrs['gen_ai.skill.name']);
|
|
396
|
+
const resource = str(span.attrs['gen_ai.skill.resource.name']);
|
|
397
|
+
const spawned = str(span.attrs['gen_ai.transfer.mode']) !== undefined || str(span.attrs['gen_ai.transfer.target.name']) !== undefined || (childrenOf.get(`${span.trace}/${span.id}`) ?? []).some(isAgent);
|
|
398
|
+
const mcp = str(span.attrs['mcp.method.name']) === 'tools/call';
|
|
399
|
+
const block = { type: 'tool_use', id, name: toolName, native: toolName, input: args };
|
|
400
|
+
let failed = span.failed;
|
|
401
|
+
if (command) {
|
|
402
|
+
const cmd = str(args.command);
|
|
403
|
+
block.name = 'Bash';
|
|
404
|
+
block.native = 'bash';
|
|
405
|
+
block.input = cmd === undefined ? {} : { command: cmd };
|
|
406
|
+
if (exit !== undefined && exit !== 0)
|
|
407
|
+
failed = true;
|
|
408
|
+
}
|
|
409
|
+
else if (skill !== undefined) {
|
|
410
|
+
block.native = resource === undefined ? 'load_skill' : 'load_skill_resource';
|
|
411
|
+
block.input = resource === undefined ? { skill } : { file_path: resource };
|
|
412
|
+
}
|
|
413
|
+
else if (spawned) {
|
|
414
|
+
block.native = 'invoke_agent';
|
|
415
|
+
}
|
|
416
|
+
else if (mcp) {
|
|
417
|
+
// The conventions carry no MCP server name (mcp.md:143-213), so the call is written under a server that names none.
|
|
418
|
+
block.name = `mcp__unnamed__${toolName}`;
|
|
419
|
+
block.native = block.name;
|
|
420
|
+
}
|
|
421
|
+
attach(id, span).content.push(block);
|
|
422
|
+
result(id, span, failed ? (exit !== undefined && exit !== 0 && !span.failed ? `Exit code ${String(exit)}` : failureText(span)) : undefined);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
pieces.sort((a, b) => a.t - b.t || a.rank - b.rank);
|
|
426
|
+
entry.lines.push(...pieces.map((piece) => piece.line));
|
|
427
|
+
entry.spans += 1 + mine.length;
|
|
428
|
+
}
|
|
429
|
+
return [...out.entries()].map(([id, held]) => ({ id, size: held.spans, mtimeMs: held.mtime, lines: held.lines, subagent: held.subagent, file: held.file }));
|
|
430
|
+
};
|
|
431
|
+
const SAFE = /^(?!\.+$)[A-Za-z0-9._-]+$/;
|
|
432
|
+
export const openOtelStore = async (root) => {
|
|
433
|
+
let names;
|
|
434
|
+
try {
|
|
435
|
+
names = (await readdir(root)).filter((name) => /\.jsonl?$/i.test(name)).sort();
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
if (missing(error))
|
|
439
|
+
return undefined;
|
|
440
|
+
return { sessions: [], transcript: () => '', rejected: 0, unreadable: [{ file: root, reason: reasonOf(error, root) }] };
|
|
441
|
+
}
|
|
442
|
+
const unreadable = [];
|
|
443
|
+
const spans = [];
|
|
444
|
+
const order = { next: 0 };
|
|
445
|
+
// The torn lines of each file stay with that file: a session that came from it counts them, else the file is named.
|
|
446
|
+
const torn = new Map();
|
|
447
|
+
for (const name of names) {
|
|
448
|
+
const file = join(root, name);
|
|
449
|
+
try {
|
|
450
|
+
if (!(await stat(file)).isFile())
|
|
451
|
+
continue;
|
|
452
|
+
const { requests, bad } = requestsOf(await readFile(file, 'utf8'));
|
|
453
|
+
if (bad.length > 0)
|
|
454
|
+
torn.set(file, bad);
|
|
455
|
+
for (const request of requests) {
|
|
456
|
+
spans.push(...spansOf(request, order, file));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
unreadable.push({ file, reason: reasonOf(error, file) });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const built = otelSessions(spans);
|
|
464
|
+
let rejected = 0;
|
|
465
|
+
const sessions = [];
|
|
466
|
+
const lines = new Map();
|
|
467
|
+
for (const session of built) {
|
|
468
|
+
if (!SAFE.test(session.id)) {
|
|
469
|
+
rejected += 1;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
lines.set(session.id, session.lines);
|
|
473
|
+
sessions.push({ id: session.id, folder: null, size: session.size, mtimeMs: session.mtimeMs, subagent: session.subagent, file: session.file });
|
|
474
|
+
}
|
|
475
|
+
sessions.sort((a, b) => a.mtimeMs - b.mtimeMs || a.id.localeCompare(b.id));
|
|
476
|
+
const tornOf = new Map();
|
|
477
|
+
for (const [file, bad] of torn) {
|
|
478
|
+
const owner = sessions.find((session) => session.file === file);
|
|
479
|
+
if (owner === undefined)
|
|
480
|
+
unreadable.push({ file, reason: `${String(bad.length)} ${bad.length === 1 ? 'line is' : 'lines are'} not JSON` });
|
|
481
|
+
else
|
|
482
|
+
tornOf.set(owner.id, bad);
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
sessions,
|
|
486
|
+
rejected,
|
|
487
|
+
unreadable,
|
|
488
|
+
transcript: (session) => {
|
|
489
|
+
const held = lines.get(session.id) ?? [];
|
|
490
|
+
const text = [...held.map((line) => JSON.stringify(line)), ...(tornOf.get(session.id) ?? [])].join('\n');
|
|
491
|
+
return text === '' ? '' : `${text}\n`;
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Store } from './store.js';
|
|
2
|
+
import type { Vocabulary } from './vocab-load.js';
|
|
3
|
+
/** The first capture version that records `errorCode`; a failed call captured before it has no class to count. */
|
|
4
|
+
export declare const ERROR_CODE_SINCE = 14;
|
|
5
|
+
/** The class of a failed call captured at a version that records classes but names none. */
|
|
6
|
+
export declare const NO_CLASS = "no class recorded";
|
|
7
|
+
export interface HarnessModelCount {
|
|
8
|
+
harness: string;
|
|
9
|
+
model: string;
|
|
10
|
+
calls: number;
|
|
11
|
+
failed: number;
|
|
12
|
+
}
|
|
13
|
+
export interface PackageRow {
|
|
14
|
+
package: string;
|
|
15
|
+
calls: number;
|
|
16
|
+
failed: number;
|
|
17
|
+
sessions: number;
|
|
18
|
+
/** First and last UTC day a counted call ran; null when no counted call has a timestamp. */
|
|
19
|
+
firstDay: string | null;
|
|
20
|
+
lastDay: string | null;
|
|
21
|
+
harnessModels: HarnessModelCount[];
|
|
22
|
+
/** Failed calls by `errorCode`, from calls captured at a version that records it. */
|
|
23
|
+
errorClasses: {
|
|
24
|
+
[code: string]: number;
|
|
25
|
+
};
|
|
26
|
+
/** Failed calls captured before `errorCode` existed: counted here, never as zero in `errorClasses`. */
|
|
27
|
+
notMeasured: number;
|
|
28
|
+
}
|
|
29
|
+
export interface PackageBooks {
|
|
30
|
+
rows: PackageRow[];
|
|
31
|
+
/** Pulled names outside the public list: how many names, and how many calls pulled any of them, never the names. */
|
|
32
|
+
notPublic: {
|
|
33
|
+
names: number;
|
|
34
|
+
calls: number;
|
|
35
|
+
};
|
|
36
|
+
/** What the A38 exclusion removed before any count. */
|
|
37
|
+
excluded: {
|
|
38
|
+
transcripts: number;
|
|
39
|
+
calls: number;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Calls per package from `store.tools`, the reader's own rows (latest capture of each transcript), without the transcripts `excluded` names.
|
|
44
|
+
* A transcript drops when it or its main session is excluded, so a subagent goes with its parent. `cutoff` is an ISO time; a call with no time is kept only without one.
|
|
45
|
+
*/
|
|
46
|
+
export declare const packageBooks: (store: Store, excluded: ReadonlySet<string>, vocab: Vocabulary, cutoff?: string) => PackageBooks;
|
|
47
|
+
/** Text for `books --by package`: one block per package in name order (no rank), then the not-public line and the exclusion line. */
|
|
48
|
+
export declare const formatPackageBooks: (result: PackageBooks) => string;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// `oars sessions books --by package`: the per-package counts a field report prints, read from the local store only. A pure function of a
|
|
2
|
+
// loaded store and the excluded sessions the A38 scan returns, so the numbers can be rerun by anyone. It prints counts: no rank, share, or comparison.
|
|
3
|
+
import { PUBLIC_MCP_SERVERS, PUBLIC_PACKAGES } from './public-vocabulary.js';
|
|
4
|
+
import { resolveMcpPull, resolvePull } from './package-resolve.js';
|
|
5
|
+
import { projectStoredPackage } from './projection.js';
|
|
6
|
+
import { harnessOf } from './schema.js';
|
|
7
|
+
/** The first capture version that records `errorCode`; a failed call captured before it has no class to count. */
|
|
8
|
+
export const ERROR_CODE_SINCE = 14;
|
|
9
|
+
/** The class of a failed call captured at a version that records classes but names none. */
|
|
10
|
+
export const NO_CLASS = 'no class recorded';
|
|
11
|
+
const NO_MODEL = 'no model';
|
|
12
|
+
const OTHER_MODEL = 'another model';
|
|
13
|
+
const tally = () => ({ calls: 0, failed: 0, sessions: new Set(), days: new Set(), harnessModels: new Map(), errorClasses: new Map(), notMeasured: 0 });
|
|
14
|
+
const isMeasured = (tool) => Number(tool.cv) >= ERROR_CODE_SINCE;
|
|
15
|
+
/** A pulled name is public when its ecosystem's pinned snapshot or the curated list names it (the rule the field-report script applied); an MCP server, when the published-server list does. */
|
|
16
|
+
const isPublic = (vocab, key) => {
|
|
17
|
+
const colon = key.indexOf(':');
|
|
18
|
+
if (key.startsWith('mcp:'))
|
|
19
|
+
return PUBLIC_MCP_SERVERS.has(key.slice(colon + 1));
|
|
20
|
+
return projectStoredPackage(vocab, { package: key, packageVersion: null }).package !== 'private' || PUBLIC_PACKAGES.has(key.slice(colon + 1));
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Calls per package from `store.tools`, the reader's own rows (latest capture of each transcript), without the transcripts `excluded` names.
|
|
24
|
+
* A transcript drops when it or its main session is excluded, so a subagent goes with its parent. `cutoff` is an ISO time; a call with no time is kept only without one.
|
|
25
|
+
*/
|
|
26
|
+
export const packageBooks = (store, excluded, vocab, cutoff) => {
|
|
27
|
+
const harnesses = new Map([...store.sessions, ...store.subagents].map((session) => [session.session, harnessOf(session)]));
|
|
28
|
+
const promptModel = new Map(store.prompts.map((prompt) => [`${prompt.session}#${String(prompt.index)}`, prompt.model]));
|
|
29
|
+
const transcripts = new Set();
|
|
30
|
+
for (const record of [...store.sessions, ...store.subagents])
|
|
31
|
+
if (excluded.has(record.session) || excluded.has(record.parent ?? record.session))
|
|
32
|
+
transcripts.add(record.session);
|
|
33
|
+
const removed = { transcripts: 0, calls: 0 };
|
|
34
|
+
const public_ = new Map();
|
|
35
|
+
const otherNames = new Set();
|
|
36
|
+
const otherCalls = new Set();
|
|
37
|
+
for (const tool of store.tools) {
|
|
38
|
+
// The window applies first, so the exclusion counts cover the same calls as the rows.
|
|
39
|
+
if (cutoff !== undefined && (tool.ts === null || tool.ts < cutoff))
|
|
40
|
+
continue;
|
|
41
|
+
if (excluded.has(tool.session) || excluded.has(tool.parent)) {
|
|
42
|
+
transcripts.add(tool.session);
|
|
43
|
+
removed.calls += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const keys = new Set();
|
|
47
|
+
for (const pull of tool.pulls ?? []) {
|
|
48
|
+
const resolved = resolvePull(pull)?.package ?? resolveMcpPull(pull);
|
|
49
|
+
if (resolved !== null)
|
|
50
|
+
keys.add(resolved);
|
|
51
|
+
}
|
|
52
|
+
if (keys.size === 0)
|
|
53
|
+
continue;
|
|
54
|
+
const harness = harnesses.get(tool.session) ?? 'claude-code';
|
|
55
|
+
const named = tool.model ?? (tool.prompt === null ? undefined : promptModel.get(`${tool.session}#${String(tool.prompt)}`)) ?? NO_MODEL;
|
|
56
|
+
const model = named === NO_MODEL || vocab.models.has(named.toLowerCase()) ? named.toLowerCase() : OTHER_MODEL;
|
|
57
|
+
const failed = tool.status === 'error';
|
|
58
|
+
for (const key of keys) {
|
|
59
|
+
if (!isPublic(vocab, key)) {
|
|
60
|
+
otherNames.add(key);
|
|
61
|
+
otherCalls.add(`${tool.session}#${String(tool.n)}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const row = public_.get(key) ?? tally();
|
|
65
|
+
public_.set(key, row);
|
|
66
|
+
row.calls += 1;
|
|
67
|
+
row.sessions.add(tool.parent);
|
|
68
|
+
if (tool.ts !== null)
|
|
69
|
+
row.days.add(tool.ts.slice(0, 10));
|
|
70
|
+
const cell = row.harnessModels.get(`${harness} ${model}`) ?? { harness, model, calls: 0, failed: 0 };
|
|
71
|
+
row.harnessModels.set(`${harness} ${model}`, cell);
|
|
72
|
+
cell.calls += 1;
|
|
73
|
+
if (!failed)
|
|
74
|
+
continue;
|
|
75
|
+
row.failed += 1;
|
|
76
|
+
cell.failed += 1;
|
|
77
|
+
if (!isMeasured(tool))
|
|
78
|
+
row.notMeasured += 1;
|
|
79
|
+
else
|
|
80
|
+
row.errorClasses.set(tool.errorCode ?? NO_CLASS, (row.errorClasses.get(tool.errorCode ?? NO_CLASS) ?? 0) + 1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
removed.transcripts = transcripts.size;
|
|
84
|
+
const rows = [...public_.entries()]
|
|
85
|
+
.map(([key, row]) => {
|
|
86
|
+
const days = [...row.days].sort();
|
|
87
|
+
return {
|
|
88
|
+
package: key,
|
|
89
|
+
calls: row.calls,
|
|
90
|
+
failed: row.failed,
|
|
91
|
+
sessions: row.sessions.size,
|
|
92
|
+
firstDay: days[0] ?? null,
|
|
93
|
+
lastDay: days[days.length - 1] ?? null,
|
|
94
|
+
harnessModels: [...row.harnessModels.values()].sort((a, b) => a.harness.localeCompare(b.harness) || a.model.localeCompare(b.model)),
|
|
95
|
+
errorClasses: Object.fromEntries([...row.errorClasses.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
96
|
+
notMeasured: row.notMeasured,
|
|
97
|
+
};
|
|
98
|
+
})
|
|
99
|
+
.sort((a, b) => a.package.localeCompare(b.package));
|
|
100
|
+
return { rows, notPublic: { names: otherNames.size, calls: otherCalls.size }, excluded: removed };
|
|
101
|
+
};
|
|
102
|
+
const plural = (count, one) => `${String(count)} ${one}${count === 1 ? '' : 's'}`;
|
|
103
|
+
/** Text for `books --by package`: one block per package in name order (no rank), then the not-public line and the exclusion line. */
|
|
104
|
+
export const formatPackageBooks = (result) => {
|
|
105
|
+
const blocks = result.rows.map((row) => {
|
|
106
|
+
const classes = Object.entries(row.errorClasses).map(([code, count]) => `${code} ${String(count)}`);
|
|
107
|
+
const errors = [...classes, ...(row.notMeasured === 0 ? [] : [`not measured ${String(row.notMeasured)} (captured before error classes were recorded)`])];
|
|
108
|
+
return [
|
|
109
|
+
`${row.package}: ${plural(row.calls, 'call')}, ${String(row.failed)} failed, ${plural(row.sessions, 'session')}, ${row.firstDay ?? '-'} to ${row.lastDay ?? '-'} (UTC)`,
|
|
110
|
+
...row.harnessModels.map((cell) => ` ${cell.harness} ${cell.model}: ${plural(cell.calls, 'call')}, ${String(cell.failed)} failed`),
|
|
111
|
+
...(errors.length === 0 ? [] : [` failed by class: ${errors.join(', ')}`]),
|
|
112
|
+
].join('\n');
|
|
113
|
+
});
|
|
114
|
+
const body = result.rows.length === 0 ? ['No public package pulls in the store.'] : blocks;
|
|
115
|
+
return [
|
|
116
|
+
...body,
|
|
117
|
+
`Not on the public list: ${plural(result.notPublic.names, 'name')}, ${plural(result.notPublic.calls, 'call')}, not named here.`,
|
|
118
|
+
`Exclusions applied: ${plural(result.excluded.transcripts, 'transcript')} and ${plural(result.excluded.calls, 'call')} removed before counting.`,
|
|
119
|
+
].join('\n');
|
|
120
|
+
};
|