@50arms/session-telemetry 0.0.0-stage → 0.1.66
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 +68 -2
- package/dist/.source-hash +1 -0
- package/dist/activity-observer.d.ts +186 -0
- package/dist/activity-observer.js +917 -0
- package/dist/api-tool-names.d.ts +12 -0
- package/dist/api-tool-names.js +19 -0
- package/dist/browser.d.ts +29 -0
- package/dist/browser.js +10 -0
- package/dist/catalog-prices.d.ts +27 -0
- package/dist/catalog-prices.js +31 -0
- package/dist/claim-tracker.d.ts +53 -0
- package/dist/claim-tracker.js +213 -0
- package/dist/dispatch-usage.d.ts +321 -0
- package/dist/dispatch-usage.js +657 -0
- package/dist/git-head.d.ts +1 -0
- package/dist/git-head.js +19 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +49 -0
- package/dist/internal-logger.d.ts +16 -0
- package/dist/internal-logger.js +27 -0
- package/dist/ledger-timing.d.ts +26 -0
- package/dist/ledger-timing.js +31 -0
- package/dist/pricing.d.ts +93 -0
- package/dist/pricing.js +77 -0
- package/dist/real-work.d.ts +13 -0
- package/dist/real-work.js +57 -0
- package/dist/session-log-watcher.d.ts +616 -0
- package/dist/session-log-watcher.js +1285 -0
- package/dist/session-title-marks.d.ts +2 -0
- package/dist/session-title-marks.js +16 -0
- package/dist/stats-wire.d.ts +345 -0
- package/dist/stats-wire.js +636 -0
- package/dist/subagent-live.d.ts +36 -0
- package/dist/subagent-live.js +153 -0
- package/dist/tool-summary.d.ts +19 -0
- package/dist/tool-summary.js +58 -0
- package/dist/transcript-timing.d.ts +100 -0
- package/dist/transcript-timing.js +231 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.js +1 -0
- package/dist/usage-ledger.d.ts +256 -0
- package/dist/usage-ledger.js +581 -0
- package/dist/write-queue.d.ts +9 -0
- package/dist/write-queue.js +16 -0
- package/package.json +43 -4
package/dist/git-head.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-5333 - `git rev-parse HEAD` in a directory, for the activity observer's start commit.
|
|
3
|
+
*
|
|
4
|
+
* Returns the full object id git prints, or null when the directory is not a git checkout, does not exist, or git cannot be run
|
|
5
|
+
* (not installed, timed out). A null is a true "not known": the observer then tries the sub-agent's next working directory, and
|
|
6
|
+
* the replay that wants the commit is told there is none.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import { GIT_OBJECT_ID } from "./stats-wire.js";
|
|
10
|
+
/** A checkout that takes longer than this to answer `rev-parse` is not read. */
|
|
11
|
+
const GIT_HEAD_TIMEOUT_MS = 5_000;
|
|
12
|
+
export function readGitHead(cwd) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
execFile("git", ["-C", cwd, "rev-parse", "HEAD"], { encoding: "utf8", timeout: GIT_HEAD_TIMEOUT_MS, windowsHide: true }, (error, stdout) => {
|
|
15
|
+
const out = error === null ? stdout.trim() : "";
|
|
16
|
+
resolve(GIT_OBJECT_ID.test(out) ? out : null);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2962 — `@50arms/session-telemetry` main entry point.
|
|
3
|
+
*
|
|
4
|
+
* The shared Claude Code transcript collector: incremental tailing of the
|
|
5
|
+
* main and sub-agent JSONL transcripts, with no whole-transcript buffer
|
|
6
|
+
* (small per-reply bookkeeping only, kept in memory for the life of the
|
|
7
|
+
* process — see `session-log-watcher.ts`'s header). Consumed by the
|
|
8
|
+
* danxbot worker (Node, workspace-linked — see the README) and, from
|
|
9
|
+
* DX-2954, the `platform-mcp` MCP server's plan event relay (Node, npm-installed at
|
|
10
|
+
* a pinned exact version).
|
|
11
|
+
*
|
|
12
|
+
* A browser-safe subset with NO `node:` imports lives at the `./browser`
|
|
13
|
+
* subpath (see `browser.ts`) for consumers that cannot import Node builtins
|
|
14
|
+
* — the dashboard server, the dashboard SPA, and any future browser-side
|
|
15
|
+
* usage display.
|
|
16
|
+
*/
|
|
17
|
+
export { SessionLogWatcher, DISPATCH_TAG_PREFIX, encodeClaudeProjectsCwd, deriveSessionDir, sessionUuidExistsInCleanRoom, findNewestJsonlFile, findSessionFileByDispatchId, convertJsonlEntry, skipReplays, } from "./session-log-watcher.js";
|
|
18
|
+
export type { EntryConsumer, UsageLineConsumer, SessionLogWatcherOptions, SourceCursorStart, CursorPastEofInfo, SourceDiscoveredInfo, EntryMeta, } from "./session-log-watcher.js";
|
|
19
|
+
export type { AgentLogEntry, AgentLineage } from "./types.js";
|
|
20
|
+
export { truncStr, summarizeToolInput, buildAssistantSummary, buildToolResultSummary, } from "./tool-summary.js";
|
|
21
|
+
export * from "./usage-ledger.js";
|
|
22
|
+
export * from "./real-work.js";
|
|
23
|
+
export * from "./ledger-timing.js";
|
|
24
|
+
export * from "./transcript-timing.js";
|
|
25
|
+
export * from "./pricing.js";
|
|
26
|
+
export * from "./catalog-prices.js";
|
|
27
|
+
export * from "./stats-wire.js";
|
|
28
|
+
export * from "./api-tool-names.js";
|
|
29
|
+
export * from "./claim-tracker.js";
|
|
30
|
+
export * from "./dispatch-usage.js";
|
|
31
|
+
export * from "./activity-observer.js";
|
|
32
|
+
export * from "./git-head.js";
|
|
33
|
+
export * from "./subagent-live.js";
|
|
34
|
+
export * from "./write-queue.js";
|
|
35
|
+
export * from "./session-title-marks.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2962 — `@50arms/session-telemetry` main entry point.
|
|
3
|
+
*
|
|
4
|
+
* The shared Claude Code transcript collector: incremental tailing of the
|
|
5
|
+
* main and sub-agent JSONL transcripts, with no whole-transcript buffer
|
|
6
|
+
* (small per-reply bookkeeping only, kept in memory for the life of the
|
|
7
|
+
* process — see `session-log-watcher.ts`'s header). Consumed by the
|
|
8
|
+
* danxbot worker (Node, workspace-linked — see the README) and, from
|
|
9
|
+
* DX-2954, the `platform-mcp` MCP server's plan event relay (Node, npm-installed at
|
|
10
|
+
* a pinned exact version).
|
|
11
|
+
*
|
|
12
|
+
* A browser-safe subset with NO `node:` imports lives at the `./browser`
|
|
13
|
+
* subpath (see `browser.ts`) for consumers that cannot import Node builtins
|
|
14
|
+
* — the dashboard server, the dashboard SPA, and any future browser-side
|
|
15
|
+
* usage display.
|
|
16
|
+
*/
|
|
17
|
+
export { SessionLogWatcher, DISPATCH_TAG_PREFIX, encodeClaudeProjectsCwd, deriveSessionDir, sessionUuidExistsInCleanRoom, findNewestJsonlFile, findSessionFileByDispatchId, convertJsonlEntry, skipReplays, } from "./session-log-watcher.js";
|
|
18
|
+
// `truncStr` is a generic, dependency-free string helper that moved here
|
|
19
|
+
// alongside the rest of `tool-summary.ts` (it has no other home now that
|
|
20
|
+
// file is gone from root) — re-exported for danxbot root consumers that
|
|
21
|
+
// truncate a string for display but have nothing else to do with the
|
|
22
|
+
// transcript collector, e.g. `src/dashboard/dispatch-scout.ts`.
|
|
23
|
+
export { truncStr, summarizeToolInput, buildAssistantSummary, buildToolResultSummary, } from "./tool-summary.js";
|
|
24
|
+
// DX-2963 — the one usage counter and the one pricing function (also on the
|
|
25
|
+
// browser-safe `./browser` subpath; both are pure arithmetic).
|
|
26
|
+
export * from "./usage-ledger.js";
|
|
27
|
+
export * from "./real-work.js";
|
|
28
|
+
export * from "./ledger-timing.js";
|
|
29
|
+
export * from "./transcript-timing.js";
|
|
30
|
+
export * from "./pricing.js";
|
|
31
|
+
export * from "./catalog-prices.js";
|
|
32
|
+
// DX-2954 — the ONE stats collector and its wire, shared by the danxbot worker
|
|
33
|
+
// and the MCP server's relay (operator sessions) so both report the same stats the
|
|
34
|
+
// same way (epic DX-2958): the stats payload/response schema and its strict
|
|
35
|
+
// parser, the collector that ships ledger entries + transcript cursors +
|
|
36
|
+
// running-set changes, the transcript-side running-set producer, and the FIFO
|
|
37
|
+
// that orders a dispatch's writes.
|
|
38
|
+
export * from "./stats-wire.js";
|
|
39
|
+
export * from "./api-tool-names.js";
|
|
40
|
+
export * from "./claim-tracker.js";
|
|
41
|
+
export * from "./dispatch-usage.js";
|
|
42
|
+
export * from "./activity-observer.js";
|
|
43
|
+
// DX-5333 — the start commit a sub-agent's activity row records.
|
|
44
|
+
export * from "./git-head.js";
|
|
45
|
+
// DX-4508 — the local live sub-agent view `platform-mcp subagents-live` streams to the plugin pane.
|
|
46
|
+
export * from "./subagent-live.js";
|
|
47
|
+
export * from "./write-queue.js";
|
|
48
|
+
// DX-6078 - the one list of the 50arms plugin's session-title marks, stripped by the MCP's title reader and by the dashboard's title writes.
|
|
49
|
+
export * from "./session-title-marks.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2962: minimal package-local logging shim. The collector previously
|
|
3
|
+
* imported danxbot root's `src/logger.ts` (`createLogger`), but that file
|
|
4
|
+
* is root-repo-only and this package must never reach back into the root
|
|
5
|
+
* repo (see the package README "Isolation"). `createLogger`'s own
|
|
6
|
+
* consumers span ~300+ unrelated root files, so it is not something this
|
|
7
|
+
* extraction "moves" — only the three log levels the collector actually
|
|
8
|
+
* calls (`info`/`warn`/`error`) are reproduced here, deliberately smaller
|
|
9
|
+
* than the root abstraction it replaces for this package's own use.
|
|
10
|
+
*/
|
|
11
|
+
export interface Logger {
|
|
12
|
+
info(message: string): void;
|
|
13
|
+
warn(message: string, error?: unknown): void;
|
|
14
|
+
error(message: string, error?: unknown): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function createLogger(component: string): Logger;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function emit(level, component, message, error) {
|
|
2
|
+
const entry = {
|
|
3
|
+
timestamp: new Date().toISOString(),
|
|
4
|
+
level,
|
|
5
|
+
component,
|
|
6
|
+
message,
|
|
7
|
+
};
|
|
8
|
+
if (error instanceof Error) {
|
|
9
|
+
entry.error = error.message;
|
|
10
|
+
entry.stack = error.stack;
|
|
11
|
+
}
|
|
12
|
+
else if (error !== undefined) {
|
|
13
|
+
entry.error = String(error);
|
|
14
|
+
}
|
|
15
|
+
// DX-2954 — every level goes to STDERR. The MCP server's
|
|
16
|
+
// relay and the `subagents-live` subcommand run this collector; the subcommand's STDOUT is a protocol
|
|
17
|
+
// channel the plugin parses (and the MCP server's is JSON-RPC), so a log line there
|
|
18
|
+
// would corrupt it. The worker captures both streams.
|
|
19
|
+
console.error(JSON.stringify(entry));
|
|
20
|
+
}
|
|
21
|
+
export function createLogger(component) {
|
|
22
|
+
return {
|
|
23
|
+
info: (message) => emit("info", component, message),
|
|
24
|
+
warn: (message, error) => emit("warn", component, message, error),
|
|
25
|
+
error: (message, error) => emit("error", component, message, error),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-3964 - one reply's measured time and code churn. Its own block on
|
|
3
|
+
* {@link LedgerEntry}, never part of {@link LedgerUsage}: `LedgerUsage` is the
|
|
4
|
+
* priced token set that `LEDGER_USAGE_FIELDS` iterates into pricing, per-model
|
|
5
|
+
* buckets and the worker's token estimate, and none of those may see these.
|
|
6
|
+
* Every field only GROWS as the reply's lines (and the tool results that settle
|
|
7
|
+
* them) are read, so the ledger's `excess = max - credited` makes a replay credit
|
|
8
|
+
* nothing twice.
|
|
9
|
+
*/
|
|
10
|
+
export interface LedgerTiming {
|
|
11
|
+
/** Request sent to the reply's last block written, ms (`## Definitions` on DX-3964). */
|
|
12
|
+
apiMs: number;
|
|
13
|
+
/** Credited gaps in the source's own timeline owned by this reply, ms. */
|
|
14
|
+
agentMs: number;
|
|
15
|
+
/** Lines the reply's successful Edit/Write calls added. */
|
|
16
|
+
linesAdded: number;
|
|
17
|
+
/** Lines the reply's successful Edit/Write calls removed. */
|
|
18
|
+
linesRemoved: number;
|
|
19
|
+
}
|
|
20
|
+
export declare const LEDGER_TIMING_FIELDS: readonly ["apiMs", "agentMs", "linesAdded", "linesRemoved"];
|
|
21
|
+
export declare function zeroTiming(): LedgerTiming;
|
|
22
|
+
export declare function maxTiming(a: LedgerTiming, b: LedgerTiming): LedgerTiming;
|
|
23
|
+
export declare function addTiming(a: LedgerTiming, b: LedgerTiming): LedgerTiming;
|
|
24
|
+
/** Per-field `max(0, a - b)`. */
|
|
25
|
+
export declare function excessTiming(a: LedgerTiming, b: LedgerTiming): LedgerTiming;
|
|
26
|
+
export declare function isZeroTiming(t: LedgerTiming): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const LEDGER_TIMING_FIELDS = [
|
|
2
|
+
"apiMs",
|
|
3
|
+
"agentMs",
|
|
4
|
+
"linesAdded",
|
|
5
|
+
"linesRemoved",
|
|
6
|
+
];
|
|
7
|
+
export function zeroTiming() {
|
|
8
|
+
return { apiMs: 0, agentMs: 0, linesAdded: 0, linesRemoved: 0 };
|
|
9
|
+
}
|
|
10
|
+
export function maxTiming(a, b) {
|
|
11
|
+
const out = zeroTiming();
|
|
12
|
+
for (const f of LEDGER_TIMING_FIELDS)
|
|
13
|
+
out[f] = Math.max(a[f], b[f]);
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
export function addTiming(a, b) {
|
|
17
|
+
const out = zeroTiming();
|
|
18
|
+
for (const f of LEDGER_TIMING_FIELDS)
|
|
19
|
+
out[f] = a[f] + b[f];
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
/** Per-field `max(0, a - b)`. */
|
|
23
|
+
export function excessTiming(a, b) {
|
|
24
|
+
const out = zeroTiming();
|
|
25
|
+
for (const f of LEDGER_TIMING_FIELDS)
|
|
26
|
+
out[f] = Math.max(0, a[f] - b[f]);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
export function isZeroTiming(t) {
|
|
30
|
+
return LEDGER_TIMING_FIELDS.every((f) => t[f] === 0);
|
|
31
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2963 - the ONE pricing function, shared by every consumer that turns token counts into dollars: the danxbot server's usage-ledger
|
|
3
|
+
* writer (which prices each credit once, when it is credited), the router's Haiku calls, the bench and the live sub-agent view. Exported
|
|
4
|
+
* through the browser-safe `./browser` subpath (pure arithmetic, no `node:` imports).
|
|
5
|
+
*
|
|
6
|
+
* DX-5611 (PLAN-53 G-3) - this package holds NO prices. Every function takes the price lookup of its caller, built from the dashboard's
|
|
7
|
+
* model-version catalog (`model_versions` / `model_version_prices`), so a price Anthropic changes reaches every consumer the day the catalog
|
|
8
|
+
* declares it. A model id the lookup has no price for is never silently $0: `priceUsage` returns `unpriced`, and the ledger writer records
|
|
9
|
+
* a visible `unpriced_model` dispatch event for it.
|
|
10
|
+
*
|
|
11
|
+
* DX-2963 - cache writes are TWO prices, never one: a 5-minute write is 1.25x base input and a 1-hour write is 2x base input. Claude Code
|
|
12
|
+
* reports each lifetime separately (`usage.cache_creation.ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens`), and both are material,
|
|
13
|
+
* so each field is priced with its own price.
|
|
14
|
+
*/
|
|
15
|
+
/** Price per MILLION tokens, USD. */
|
|
16
|
+
export interface ModelPrice {
|
|
17
|
+
input: number;
|
|
18
|
+
output: number;
|
|
19
|
+
cacheWrite5m: number;
|
|
20
|
+
cacheWrite1h: number;
|
|
21
|
+
cacheRead: number;
|
|
22
|
+
}
|
|
23
|
+
/** Token counts for one priced unit (a ledger credit, a model bucket, a call). */
|
|
24
|
+
export interface PricedTokens {
|
|
25
|
+
input: number;
|
|
26
|
+
output: number;
|
|
27
|
+
cacheRead: number;
|
|
28
|
+
cacheWrite5m: number;
|
|
29
|
+
cacheWrite1h: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Claude Code's literal model tag on an API-error entry (rate limit, context
|
|
33
|
+
* overflow). Its tokens count in a dispatch's flat totals but it is never a
|
|
34
|
+
* real priced model, so it never enters `usage_by_model` or cost.
|
|
35
|
+
*/
|
|
36
|
+
export declare const SYNTHETIC_MODEL = "<synthetic>";
|
|
37
|
+
/** What a model costs at a speed, per million tokens; `null` when there is no price. */
|
|
38
|
+
export type PriceLookup = (model: string, speed: string) => ModelPrice | null;
|
|
39
|
+
/**
|
|
40
|
+
* DX-4867 - the rate cards of a model at a speed: its standard card, and the second card a reply whose prompt (input + cache reads + cache
|
|
41
|
+
* writes) is longer than `aboveTokens` is billed at, for the models that have one.
|
|
42
|
+
*/
|
|
43
|
+
export interface PriceCards {
|
|
44
|
+
standard: ModelPrice;
|
|
45
|
+
longPrompt: {
|
|
46
|
+
aboveTokens: number;
|
|
47
|
+
price: ModelPrice;
|
|
48
|
+
} | null;
|
|
49
|
+
}
|
|
50
|
+
/** What a model's rate cards are at a speed; `null` when the model has no price at that speed. */
|
|
51
|
+
export type PriceCardLookup = (model: string, speed: string) => PriceCards | null;
|
|
52
|
+
export type PriceResult = {
|
|
53
|
+
kind: "priced";
|
|
54
|
+
costUsd: number;
|
|
55
|
+
} | {
|
|
56
|
+
kind: "unpriced";
|
|
57
|
+
model: string;
|
|
58
|
+
speed: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Price one unit of usage with `priceOf`. NEVER throws (DX-2963): a model, or a fast reply on a model with no fast price, that `priceOf`
|
|
62
|
+
* does not know returns `{kind: "unpriced"}` so the caller counts the tokens, leaves the cost out and records a visible `unpriced_model`
|
|
63
|
+
* event, instead of one new model id failing every write. `<synthetic>` is never passed here: callers exclude it before pricing.
|
|
64
|
+
*/
|
|
65
|
+
export declare function priceUsage(model: string, speed: string, tokens: PricedTokens, priceOf: PriceLookup): PriceResult;
|
|
66
|
+
/** The prompt tokens of one reply: everything it sent in, cached or not. */
|
|
67
|
+
export declare function promptTokensOf(tokens: PricedTokens): number;
|
|
68
|
+
/** DX-4867 - whether a reply's prompt is long enough for the long-prompt card of `cards` (a model with one card never switches). */
|
|
69
|
+
export declare function usesLongPromptCard(cards: PriceCards, tokens: PricedTokens): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* DX-4867 - price ONE reply (not a bucket of replies): like {@link priceUsage}, but a reply whose prompt is longer than the model's
|
|
72
|
+
* long-prompt threshold is charged its long-prompt card. A model with one rate card prices exactly as `priceUsage` does.
|
|
73
|
+
*/
|
|
74
|
+
export declare function priceReplyUsage(model: string, speed: string, tokens: PricedTokens, cardsOf: PriceCardLookup): PriceResult;
|
|
75
|
+
/**
|
|
76
|
+
* DX-4595 - dollars for `tokens` at `price` (USD per million tokens): each bucket with its own price. The ONE copy of the formula: `priceUsage`
|
|
77
|
+
* charges the declared price with it, and spend pacing charges an account's own (editable) prices with it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function costOfTokens(tokens: PricedTokens, price: ModelPrice): number;
|
|
80
|
+
/**
|
|
81
|
+
* DX-4508 — one ledger credit's cost, exactly as the server's usage-ledger writer stores it (moved here from
|
|
82
|
+
* `src/dashboard/usage-ledger.ts` so the live sub-agent view, `subagent-live.ts`, prices a reply with the SAME rule
|
|
83
|
+
* instead of a copy): a `<synthetic>` reply costs nothing and is never unpriced; a reply with no model is unpriced only
|
|
84
|
+
* when it carries tokens; any other model is priced by `priceOf` (so as {@link priceUsage} does), an unpriced
|
|
85
|
+
* one costing 0 and naming itself. DX-4595: spend pacing passes an account's own prices as `priceOf` and so gets the same rule.
|
|
86
|
+
*/
|
|
87
|
+
export declare function priceLedgerCredit(model: string | null, speed: string, tokens: PricedTokens, priceOf: PriceLookup): {
|
|
88
|
+
costUsd: number;
|
|
89
|
+
unpriced: {
|
|
90
|
+
model: string;
|
|
91
|
+
speed: string;
|
|
92
|
+
} | null;
|
|
93
|
+
};
|
package/dist/pricing.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2963 - the ONE pricing function, shared by every consumer that turns token counts into dollars: the danxbot server's usage-ledger
|
|
3
|
+
* writer (which prices each credit once, when it is credited), the router's Haiku calls, the bench and the live sub-agent view. Exported
|
|
4
|
+
* through the browser-safe `./browser` subpath (pure arithmetic, no `node:` imports).
|
|
5
|
+
*
|
|
6
|
+
* DX-5611 (PLAN-53 G-3) - this package holds NO prices. Every function takes the price lookup of its caller, built from the dashboard's
|
|
7
|
+
* model-version catalog (`model_versions` / `model_version_prices`), so a price Anthropic changes reaches every consumer the day the catalog
|
|
8
|
+
* declares it. A model id the lookup has no price for is never silently $0: `priceUsage` returns `unpriced`, and the ledger writer records
|
|
9
|
+
* a visible `unpriced_model` dispatch event for it.
|
|
10
|
+
*
|
|
11
|
+
* DX-2963 - cache writes are TWO prices, never one: a 5-minute write is 1.25x base input and a 1-hour write is 2x base input. Claude Code
|
|
12
|
+
* reports each lifetime separately (`usage.cache_creation.ephemeral_5m_input_tokens` / `ephemeral_1h_input_tokens`), and both are material,
|
|
13
|
+
* so each field is priced with its own price.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Claude Code's literal model tag on an API-error entry (rate limit, context
|
|
17
|
+
* overflow). Its tokens count in a dispatch's flat totals but it is never a
|
|
18
|
+
* real priced model, so it never enters `usage_by_model` or cost.
|
|
19
|
+
*/
|
|
20
|
+
export const SYNTHETIC_MODEL = "<synthetic>";
|
|
21
|
+
/**
|
|
22
|
+
* Price one unit of usage with `priceOf`. NEVER throws (DX-2963): a model, or a fast reply on a model with no fast price, that `priceOf`
|
|
23
|
+
* does not know returns `{kind: "unpriced"}` so the caller counts the tokens, leaves the cost out and records a visible `unpriced_model`
|
|
24
|
+
* event, instead of one new model id failing every write. `<synthetic>` is never passed here: callers exclude it before pricing.
|
|
25
|
+
*/
|
|
26
|
+
export function priceUsage(model, speed, tokens, priceOf) {
|
|
27
|
+
const price = priceOf(model, speed);
|
|
28
|
+
return price === null ? { kind: "unpriced", model, speed } : { kind: "priced", costUsd: costOfTokens(tokens, price) };
|
|
29
|
+
}
|
|
30
|
+
/** The prompt tokens of one reply: everything it sent in, cached or not. */
|
|
31
|
+
export function promptTokensOf(tokens) {
|
|
32
|
+
return tokens.input + tokens.cacheRead + tokens.cacheWrite5m + tokens.cacheWrite1h;
|
|
33
|
+
}
|
|
34
|
+
/** DX-4867 - whether a reply's prompt is long enough for the long-prompt card of `cards` (a model with one card never switches). */
|
|
35
|
+
export function usesLongPromptCard(cards, tokens) {
|
|
36
|
+
return cards.longPrompt !== null && promptTokensOf(tokens) > cards.longPrompt.aboveTokens;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* DX-4867 - price ONE reply (not a bucket of replies): like {@link priceUsage}, but a reply whose prompt is longer than the model's
|
|
40
|
+
* long-prompt threshold is charged its long-prompt card. A model with one rate card prices exactly as `priceUsage` does.
|
|
41
|
+
*/
|
|
42
|
+
export function priceReplyUsage(model, speed, tokens, cardsOf) {
|
|
43
|
+
const cards = cardsOf(model, speed);
|
|
44
|
+
if (cards === null)
|
|
45
|
+
return { kind: "unpriced", model, speed };
|
|
46
|
+
const price = usesLongPromptCard(cards, tokens) ? cards.longPrompt.price : cards.standard;
|
|
47
|
+
return { kind: "priced", costUsd: costOfTokens(tokens, price) };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* DX-4595 - dollars for `tokens` at `price` (USD per million tokens): each bucket with its own price. The ONE copy of the formula: `priceUsage`
|
|
51
|
+
* charges the declared price with it, and spend pacing charges an account's own (editable) prices with it.
|
|
52
|
+
*/
|
|
53
|
+
export function costOfTokens(tokens, price) {
|
|
54
|
+
return ((tokens.input * price.input +
|
|
55
|
+
tokens.output * price.output +
|
|
56
|
+
tokens.cacheRead * price.cacheRead +
|
|
57
|
+
tokens.cacheWrite5m * price.cacheWrite5m +
|
|
58
|
+
tokens.cacheWrite1h * price.cacheWrite1h) /
|
|
59
|
+
1_000_000);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* DX-4508 — one ledger credit's cost, exactly as the server's usage-ledger writer stores it (moved here from
|
|
63
|
+
* `src/dashboard/usage-ledger.ts` so the live sub-agent view, `subagent-live.ts`, prices a reply with the SAME rule
|
|
64
|
+
* instead of a copy): a `<synthetic>` reply costs nothing and is never unpriced; a reply with no model is unpriced only
|
|
65
|
+
* when it carries tokens; any other model is priced by `priceOf` (so as {@link priceUsage} does), an unpriced
|
|
66
|
+
* one costing 0 and naming itself. DX-4595: spend pacing passes an account's own prices as `priceOf` and so gets the same rule.
|
|
67
|
+
*/
|
|
68
|
+
export function priceLedgerCredit(model, speed, tokens, priceOf) {
|
|
69
|
+
if (model === SYNTHETIC_MODEL)
|
|
70
|
+
return { costUsd: 0, unpriced: null };
|
|
71
|
+
if (model === null) {
|
|
72
|
+
const zero = tokens.input === 0 && tokens.output === 0 && tokens.cacheRead === 0 && tokens.cacheWrite5m === 0 && tokens.cacheWrite1h === 0;
|
|
73
|
+
return zero ? { costUsd: 0, unpriced: null } : { costUsd: 0, unpriced: { model: "(none)", speed } };
|
|
74
|
+
}
|
|
75
|
+
const price = priceOf(model, speed);
|
|
76
|
+
return price === null ? { costUsd: 0, unpriced: { model, speed } } : { costUsd: costOfTokens(tokens, price), unpriced: null };
|
|
77
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Tools that edit or write a file. */
|
|
2
|
+
export declare const FILE_WRITE_TOOL_NAMES: ReadonlySet<string>;
|
|
3
|
+
/** Tools that spawn a sub-agent (Claude Code names the tool `Agent`; it was `Task` before). */
|
|
4
|
+
export declare const SUBAGENT_TOOL_NAMES: ReadonlySet<string>;
|
|
5
|
+
/** The fields of a tool use the real-work decision reads. */
|
|
6
|
+
export interface RealWorkToolUse {
|
|
7
|
+
name: string;
|
|
8
|
+
input: Record<string, unknown> | null;
|
|
9
|
+
}
|
|
10
|
+
/** Whether one tool use is real work. */
|
|
11
|
+
export declare function isRealWorkToolUse(use: RealWorkToolUse): boolean;
|
|
12
|
+
/** Whether any of a reply line's tool uses is real work. */
|
|
13
|
+
export declare function hasRealWork(uses: readonly RealWorkToolUse[]): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-4939 - what counts as a reply's REAL WORK, the ONE list.
|
|
3
|
+
*
|
|
4
|
+
* After a compaction an agent spends calls re-reading its card, plan and git state before it does real work again (its
|
|
5
|
+
* RE-ORIENTATION). PLAN-42 measured that as the calls up to the first reply that edits or writes a file, writes a card, or
|
|
6
|
+
* spawns a sub-agent. The collector marks each reply with that flag (`LedgerEntry.realWork`), decided from the reply's tool
|
|
7
|
+
* uses by name only, so the cost report can measure re-orientation exactly instead of inferring it from context growth.
|
|
8
|
+
*
|
|
9
|
+
* A Bash call is never real work here: whether a command writes anything cannot be told from its name. `SendMessage` to a sub-agent never
|
|
10
|
+
* is either: it continues work whose start (the `Agent` spawn) already counted, and a reply that only talks to an agent has not done anything
|
|
11
|
+
* itself. A card write is a write to the card's own content or state (`/api/issues`, `/api/issues/:id` and its sub-routes such as `edit`,
|
|
12
|
+
* `transition`, `checklists`, `problems`, `retro`), read from the tool input's `method` and `path`. A comment (`.../comments`) and a quality-gate
|
|
13
|
+
* verdict (`.../quality-gates/...`) are not card content, and a plan, Slack or dispatch-callback call is no card write. The input is all there is
|
|
14
|
+
* to judge by, so the one thing that can still be miscounted is a card route this list does not name: it follows the rule (anything under
|
|
15
|
+
* `/api/issues` other than a comment or a verdict), so it counts.
|
|
16
|
+
*
|
|
17
|
+
* Pure, no `node:` imports (exported through the browser-safe subpath too).
|
|
18
|
+
*/
|
|
19
|
+
import { API_SERVER_PREFIXES, API_TOOL_BASE_NAMES } from "./api-tool-names.js";
|
|
20
|
+
/** Tools that edit or write a file. */
|
|
21
|
+
export const FILE_WRITE_TOOL_NAMES = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
22
|
+
/** Tools that spawn a sub-agent (Claude Code names the tool `Agent`; it was `Task` before). */
|
|
23
|
+
export const SUBAGENT_TOOL_NAMES = new Set(["Agent", "Task"]);
|
|
24
|
+
/**
|
|
25
|
+
* The platform MCP's tool names: the operator session's server is the plugin's (`plugin_50arms_platform`) and a dispatched agent's is `platform`; every older
|
|
26
|
+
* server key and the older tool name `danxbot_api` are recognised too (`API_SERVER_PREFIXES`, DX-6847 / DX-6864), because a transcript is read by what its
|
|
27
|
+
* session called. `50arms_api` writes a card when its `method` is not GET and its `path` is a card route that is not a comment or a quality-gate verdict
|
|
28
|
+
* (`isCardContentWrite`); `attach_file` always writes (it attaches a file to a card).
|
|
29
|
+
*/
|
|
30
|
+
const DASHBOARD_TOOL = new RegExp(`^(?:${API_SERVER_PREFIXES.join("|")})(${API_TOOL_BASE_NAMES.join("|")}|attach_file)$`);
|
|
31
|
+
/** The sub-routes of a card that are not its content: a comment and a quality-gate verdict. */
|
|
32
|
+
const NOT_CARD_CONTENT = new Set(["comments", "quality-gates"]);
|
|
33
|
+
/** Whether a dashboard call is a write to a card's content or state: not a GET, on `/api/issues[/:id[/<sub>...]]`, the sub-route not a comment or a verdict. */
|
|
34
|
+
function isCardContentWrite(input) {
|
|
35
|
+
if (input === null || typeof input.method !== "string" || typeof input.path !== "string")
|
|
36
|
+
return false;
|
|
37
|
+
if (input.method.toUpperCase() === "GET")
|
|
38
|
+
return false;
|
|
39
|
+
const [api, issues, , sub] = input.path.split("?")[0].split("/").filter((s) => s !== "");
|
|
40
|
+
return api === "api" && issues === "issues" && !(sub !== undefined && NOT_CARD_CONTENT.has(sub));
|
|
41
|
+
}
|
|
42
|
+
/** Whether one tool use is real work. */
|
|
43
|
+
export function isRealWorkToolUse(use) {
|
|
44
|
+
if (FILE_WRITE_TOOL_NAMES.has(use.name) || SUBAGENT_TOOL_NAMES.has(use.name))
|
|
45
|
+
return true;
|
|
46
|
+
const dashboard = DASHBOARD_TOOL.exec(use.name);
|
|
47
|
+
if (dashboard === null)
|
|
48
|
+
return false;
|
|
49
|
+
if (dashboard[1] === "attach_file")
|
|
50
|
+
return true;
|
|
51
|
+
// An input without a method or a path is not counted: never a guess that it wrote a card.
|
|
52
|
+
return isCardContentWrite(use.input);
|
|
53
|
+
}
|
|
54
|
+
/** Whether any of a reply line's tool uses is real work. */
|
|
55
|
+
export function hasRealWork(uses) {
|
|
56
|
+
return uses.some(isRealWorkToolUse);
|
|
57
|
+
}
|