@titan-design/session-analytics 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Henry Jewkes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @titan-design/session-analytics
2
+
3
+ Pricing, session classification and banding for mined Claude Code sessions. Pure functions
4
+ over plain objects: no database, no filesystem, no network.
5
+
6
+ Tier 2 of the titan-platform DAG. It imports no other titan package; a caller composes it
7
+ with `@titan-design/session-read` and `@titan-design/session-graph`.
8
+
9
+ ```ts
10
+ import { classifySession, contextBand, priceRequest } from "@titan-design/session-analytics";
11
+
12
+ priceRequest(
13
+ { inputTokens: 4, cacheReadTokens: 129_291, cacheCreation1hTokens: 1_359, outputTokens: 306 },
14
+ "claude-fable-5-1",
15
+ "2026-09-18T14:48:40.512Z",
16
+ ).costUsd; // 0.07484275
17
+ ```
18
+
19
+ ## What it exports
20
+
21
+ - `PRICE_TABLE`, `PRICE_TABLE_VERSION`, `findPrice(model, ts, prices?)` — USD per million
22
+ tokens by longest model prefix, then the latest row effective at `ts`.
23
+ - `priceRequest(tokens, model, ts, prices?)` — the five cost components, `costUsd` and
24
+ `priced`.
25
+ - `classifySession(facts)` — `agent_spawned`, `human_interactive`, `headless_sdk` or
26
+ `other_headless`, plus `coordinator` or `adhoc` for human sessions.
27
+ - `CONTEXT_BANDS`, `GAP_BANDS`, `bandOf`, `contextBand`, `gapBand`.
28
+
29
+ ## Two things that will bite you
30
+
31
+ Fable's cache read is **0.025** of its input rate, not the 0.1 every other model uses.
32
+ Misreading it overstated the 2026-09-20 audit by about $1,700.
33
+
34
+ An unknown model is **unpriced**: `priced: false` and zero cost. There is no default row,
35
+ because defaulting bills a new model at an old model's rate without saying so.
36
+
37
+ Full reference: `site/reference/session-analytics.md`.
@@ -0,0 +1,91 @@
1
+ /** USD per million tokens. Fitted against 392 `cost-state` rows in the 2026-09-20 audit. */
2
+ interface PriceRow {
3
+ /** Matched against the model string by longest prefix. */
4
+ modelPrefix: string;
5
+ /** ISO date. The latest row at or before a request's timestamp wins. */
6
+ effectiveFrom: string;
7
+ input: number;
8
+ cacheRead: number;
9
+ cacheWrite5m: number;
10
+ cacheWrite1h: number;
11
+ output: number;
12
+ }
13
+ /** Bump on any edit to PRICE_TABLE. The cost report prints it in its footer. */
14
+ declare const PRICE_TABLE_VERSION = 1;
15
+ declare const PRICE_TABLE: readonly PriceRow[];
16
+ /**
17
+ * The longest prefix that matches the model, then the latest row effective at `ts`.
18
+ * Returns null for a model no row covers: an unknown model is unpriced, never defaulted,
19
+ * because a default silently prices a new model at an old model's rate.
20
+ */
21
+ declare function findPrice(model: string, ts: string, prices?: readonly PriceRow[]): PriceRow | null;
22
+
23
+ /** Raw token counts for one API request, as the transcript reports them. */
24
+ interface RequestTokens {
25
+ inputTokens?: number;
26
+ cacheReadTokens?: number;
27
+ /** Total cache creation. Used only when the 5m/1h split is absent. */
28
+ cacheCreationTokens?: number;
29
+ cacheCreation5mTokens?: number;
30
+ cacheCreation1hTokens?: number;
31
+ outputTokens?: number;
32
+ }
33
+ interface PricedRequest {
34
+ inputUsd: number;
35
+ cacheReadUsd: number;
36
+ cacheWrite5mUsd: number;
37
+ cacheWrite1hUsd: number;
38
+ outputUsd: number;
39
+ costUsd: number;
40
+ /** False when no price row covers the model. Every component is then zero. */
41
+ priced: boolean;
42
+ }
43
+ declare function priceRequest(tokens: RequestTokens, model: string, ts: string, prices?: readonly PriceRow[]): PricedRequest;
44
+
45
+ type SessionClass = "agent_spawned" | "human_interactive" | "headless_sdk" | "other_headless";
46
+ /** Human sessions only. A coordinator drives other agents; an adhoc session does not. */
47
+ type HumanRole = "coordinator" | "adhoc";
48
+ /** The agent-chat spawn record for a session, as the origin resolver returns it. */
49
+ interface SessionOrigin {
50
+ depth: number;
51
+ parentName?: string | null;
52
+ /** "adopted" and "inherited" mark a pane the human took over, not a spawned worker. */
53
+ originKind?: string | null;
54
+ profile?: string | null;
55
+ }
56
+ interface SessionFacts {
57
+ /** The harness entrypoint. Agent-chat workers and `-p` miners both report "sdk-cli". */
58
+ startType?: string | null;
59
+ origin?: SessionOrigin | null;
60
+ firstUserText?: string | null;
61
+ humanTurnCount?: number;
62
+ inboundKinds?: readonly string[];
63
+ signalKinds?: readonly string[];
64
+ }
65
+ interface SessionClassification {
66
+ sessionClass: SessionClass;
67
+ reason: string;
68
+ humanRole: HumanRole | null;
69
+ }
70
+ /**
71
+ * Rule order matters. The origin row wins over `startType` because agent-chat workers
72
+ * run `claude -p` and so report "sdk-cli" exactly like a headless miner does.
73
+ */
74
+ declare function classifySession(facts: SessionFacts): SessionClassification;
75
+
76
+ /** Half-open [lo, hi) in the band's own unit. */
77
+ interface Band {
78
+ lo: number;
79
+ hi: number;
80
+ name: string;
81
+ }
82
+ /** Context size in tokens. From `cf_analyze.py` BANDS. */
83
+ declare const CONTEXT_BANDS: readonly Band[];
84
+ /** Idle gap in milliseconds. From `cf_analyze.py` IDLE. */
85
+ declare const GAP_BANDS: readonly Band[];
86
+ /** Null for a value no band covers, so a caller reports the gap instead of inventing one. */
87
+ declare function bandOf(value: number, bands: readonly Band[]): string | null;
88
+ declare function contextBand(tokens: number): string | null;
89
+ declare function gapBand(millis: number): string | null;
90
+
91
+ export { type Band, CONTEXT_BANDS, GAP_BANDS, type HumanRole, PRICE_TABLE, PRICE_TABLE_VERSION, type PriceRow, type PricedRequest, type RequestTokens, type SessionClass, type SessionClassification, type SessionFacts, type SessionOrigin, bandOf, classifySession, contextBand, findPrice, gapBand, priceRequest };
package/dist/index.js ADDED
@@ -0,0 +1,134 @@
1
+ // src/prices.ts
2
+ var PRICE_TABLE_VERSION = 1;
3
+ var OPUS = { input: 5, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 };
4
+ var SONNET = { input: 2, cacheRead: 0.2, cacheWrite5m: 2.5, cacheWrite1h: 4, output: 10 };
5
+ var FABLE = { input: 10, cacheRead: 0.25, cacheWrite5m: 12.5, cacheWrite1h: 20, output: 50 };
6
+ var HAIKU = { input: 1, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2, output: 5 };
7
+ var FREE = { input: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 0 };
8
+ var GENESIS = "2026-01-01";
9
+ var PRICE_TABLE = [
10
+ { modelPrefix: "claude-opus-5", effectiveFrom: GENESIS, ...OPUS },
11
+ { modelPrefix: "claude-opus-4-8", effectiveFrom: GENESIS, ...OPUS },
12
+ { modelPrefix: "claude-sonnet-5", effectiveFrom: GENESIS, ...SONNET },
13
+ { modelPrefix: "claude-fable-5-1", effectiveFrom: GENESIS, ...FABLE },
14
+ { modelPrefix: "claude-fable-5", effectiveFrom: GENESIS, ...FABLE },
15
+ { modelPrefix: "claude-haiku-4-5", effectiveFrom: GENESIS, ...HAIKU },
16
+ { modelPrefix: "<synthetic>", effectiveFrom: GENESIS, ...FREE }
17
+ ];
18
+ function findPrice(model, ts, prices = PRICE_TABLE) {
19
+ let best = null;
20
+ for (const row of prices) {
21
+ if (!model.startsWith(row.modelPrefix)) continue;
22
+ if (row.effectiveFrom > ts) continue;
23
+ if (best && !isBetterMatch(row, best)) continue;
24
+ best = row;
25
+ }
26
+ return best;
27
+ }
28
+ function isBetterMatch(row, best) {
29
+ if (row.modelPrefix.length !== best.modelPrefix.length) return row.modelPrefix.length > best.modelPrefix.length;
30
+ return row.effectiveFrom > best.effectiveFrom;
31
+ }
32
+
33
+ // src/price-request.ts
34
+ var UNPRICED = {
35
+ inputUsd: 0,
36
+ cacheReadUsd: 0,
37
+ cacheWrite5mUsd: 0,
38
+ cacheWrite1hUsd: 0,
39
+ outputUsd: 0,
40
+ costUsd: 0,
41
+ priced: false
42
+ };
43
+ function priceRequest(tokens, model, ts, prices = PRICE_TABLE) {
44
+ const row = findPrice(model, ts, prices);
45
+ if (!row) return { ...UNPRICED };
46
+ const { write5m, write1h } = splitCacheCreation(tokens);
47
+ const inputUsd = perMTok(tokens.inputTokens, row.input);
48
+ const cacheReadUsd = perMTok(tokens.cacheReadTokens, row.cacheRead);
49
+ const cacheWrite5mUsd = perMTok(write5m, row.cacheWrite5m);
50
+ const cacheWrite1hUsd = perMTok(write1h, row.cacheWrite1h);
51
+ const outputUsd = perMTok(tokens.outputTokens, row.output);
52
+ const costUsd = inputUsd + cacheReadUsd + cacheWrite5mUsd + cacheWrite1hUsd + outputUsd;
53
+ return { inputUsd, cacheReadUsd, cacheWrite5mUsd, cacheWrite1hUsd, outputUsd, costUsd, priced: true };
54
+ }
55
+ function splitCacheCreation(tokens) {
56
+ const write5m = tokens.cacheCreation5mTokens ?? 0;
57
+ const write1h = tokens.cacheCreation1hTokens ?? 0;
58
+ if (write5m > 0 || write1h > 0) return { write5m, write1h };
59
+ return { write5m: tokens.cacheCreationTokens ?? 0, write1h: 0 };
60
+ }
61
+ function perMTok(tokens, ratePerMTok) {
62
+ return (tokens ?? 0) * ratePerMTok / 1e6;
63
+ }
64
+
65
+ // src/classify-session.ts
66
+ var COORDINATOR_SIGNALS = /* @__PURE__ */ new Set(["agent_spawn", "chat_send"]);
67
+ function classifySession(facts) {
68
+ const [sessionClass, reason] = classOf(facts);
69
+ const humanRole = sessionClass === "human_interactive" ? humanRoleOf(facts) : null;
70
+ return { sessionClass, reason, humanRole };
71
+ }
72
+ function classOf(facts) {
73
+ const origin = facts.origin;
74
+ if (origin) {
75
+ if (isAdoptedPane(origin)) return ["human_interactive", "origin depth 0, human or adopted parent"];
76
+ return ["agent_spawned", "origin depth >= 1"];
77
+ }
78
+ if (facts.startType === "sdk-cli") return ["headless_sdk", "start_type sdk-cli with no origin row"];
79
+ if (isProgrammaticPrompt(facts)) return ["other_headless", "content-block prompt with <= 2 human turns"];
80
+ return ["human_interactive", "no origin row, no headless marker"];
81
+ }
82
+ function isAdoptedPane(origin) {
83
+ if (origin.depth !== 0) return false;
84
+ if (origin.parentName === "human") return true;
85
+ if (origin.originKind === "adopted" || origin.originKind === "inherited") return true;
86
+ return !origin.profile;
87
+ }
88
+ function isProgrammaticPrompt(facts) {
89
+ const text = (facts.firstUserText ?? "").trimStart();
90
+ return text.startsWith("[{") && (facts.humanTurnCount ?? 0) <= 2;
91
+ }
92
+ function humanRoleOf(facts) {
93
+ if (facts.inboundKinds?.includes("channel_message")) return "coordinator";
94
+ if (facts.signalKinds?.some((kind) => COORDINATOR_SIGNALS.has(kind))) return "coordinator";
95
+ return "adhoc";
96
+ }
97
+
98
+ // src/bands.ts
99
+ var CONTEXT_BANDS = [
100
+ { lo: 0, hi: 5e4, name: "<50k" },
101
+ { lo: 5e4, hi: 1e5, name: "50-100k" },
102
+ { lo: 1e5, hi: 2e5, name: "100-200k" },
103
+ { lo: 2e5, hi: Number.POSITIVE_INFINITY, name: "200k+" }
104
+ ];
105
+ var GAP_BANDS = [
106
+ { lo: 0, hi: 5 * 6e4, name: "<5m" },
107
+ { lo: 5 * 6e4, hi: 60 * 6e4, name: "5-60m" },
108
+ { lo: 60 * 6e4, hi: Number.POSITIVE_INFINITY, name: ">60m" }
109
+ ];
110
+ function bandOf(value, bands) {
111
+ for (const band of bands) {
112
+ if (value >= band.lo && value < band.hi) return band.name;
113
+ }
114
+ return null;
115
+ }
116
+ function contextBand(tokens) {
117
+ return bandOf(tokens, CONTEXT_BANDS);
118
+ }
119
+ function gapBand(millis) {
120
+ return bandOf(millis, GAP_BANDS);
121
+ }
122
+ export {
123
+ CONTEXT_BANDS,
124
+ GAP_BANDS,
125
+ PRICE_TABLE,
126
+ PRICE_TABLE_VERSION,
127
+ bandOf,
128
+ classifySession,
129
+ contextBand,
130
+ findPrice,
131
+ gapBand,
132
+ priceRequest
133
+ };
134
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/prices.ts","../src/price-request.ts","../src/classify-session.ts","../src/bands.ts"],"sourcesContent":["/** USD per million tokens. Fitted against 392 `cost-state` rows in the 2026-09-20 audit. */\nexport interface PriceRow {\n /** Matched against the model string by longest prefix. */\n modelPrefix: string;\n /** ISO date. The latest row at or before a request's timestamp wins. */\n effectiveFrom: string;\n input: number;\n cacheRead: number;\n cacheWrite5m: number;\n cacheWrite1h: number;\n output: number;\n}\n\n/** Bump on any edit to PRICE_TABLE. The cost report prints it in its footer. */\nexport const PRICE_TABLE_VERSION = 1;\n\nconst OPUS = { input: 5.0, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10.0, output: 25.0 };\nconst SONNET = { input: 2.0, cacheRead: 0.2, cacheWrite5m: 2.5, cacheWrite1h: 4.0, output: 10.0 };\n// Fable's cache read is 0.025 of input, not the 0.1 every other model uses. Reading it as\n// 0.1 overstated the 2026-09-20 audit by about $1,700; `fable-cache-read.test` pins the ratio.\nconst FABLE = { input: 10.0, cacheRead: 0.25, cacheWrite5m: 12.5, cacheWrite1h: 20.0, output: 50.0 };\nconst HAIKU = { input: 1.0, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2.0, output: 5.0 };\nconst FREE = { input: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0, output: 0 };\n\nconst GENESIS = \"2026-01-01\";\n\nexport const PRICE_TABLE: readonly PriceRow[] = [\n { modelPrefix: \"claude-opus-5\", effectiveFrom: GENESIS, ...OPUS },\n { modelPrefix: \"claude-opus-4-8\", effectiveFrom: GENESIS, ...OPUS },\n { modelPrefix: \"claude-sonnet-5\", effectiveFrom: GENESIS, ...SONNET },\n { modelPrefix: \"claude-fable-5-1\", effectiveFrom: GENESIS, ...FABLE },\n { modelPrefix: \"claude-fable-5\", effectiveFrom: GENESIS, ...FABLE },\n { modelPrefix: \"claude-haiku-4-5\", effectiveFrom: GENESIS, ...HAIKU },\n { modelPrefix: \"<synthetic>\", effectiveFrom: GENESIS, ...FREE },\n];\n\n/**\n * The longest prefix that matches the model, then the latest row effective at `ts`.\n * Returns null for a model no row covers: an unknown model is unpriced, never defaulted,\n * because a default silently prices a new model at an old model's rate.\n */\nexport function findPrice(model: string, ts: string, prices: readonly PriceRow[] = PRICE_TABLE): PriceRow | null {\n let best: PriceRow | null = null;\n for (const row of prices) {\n if (!model.startsWith(row.modelPrefix)) continue;\n if (row.effectiveFrom > ts) continue;\n if (best && !isBetterMatch(row, best)) continue;\n best = row;\n }\n return best;\n}\n\nfunction isBetterMatch(row: PriceRow, best: PriceRow): boolean {\n if (row.modelPrefix.length !== best.modelPrefix.length) return row.modelPrefix.length > best.modelPrefix.length;\n return row.effectiveFrom > best.effectiveFrom;\n}\n","import type { PriceRow } from \"./prices.js\";\nimport { PRICE_TABLE, findPrice } from \"./prices.js\";\n\n/** Raw token counts for one API request, as the transcript reports them. */\nexport interface RequestTokens {\n inputTokens?: number;\n cacheReadTokens?: number;\n /** Total cache creation. Used only when the 5m/1h split is absent. */\n cacheCreationTokens?: number;\n cacheCreation5mTokens?: number;\n cacheCreation1hTokens?: number;\n outputTokens?: number;\n}\n\nexport interface PricedRequest {\n inputUsd: number;\n cacheReadUsd: number;\n cacheWrite5mUsd: number;\n cacheWrite1hUsd: number;\n outputUsd: number;\n costUsd: number;\n /** False when no price row covers the model. Every component is then zero. */\n priced: boolean;\n}\n\nconst UNPRICED: PricedRequest = {\n inputUsd: 0,\n cacheReadUsd: 0,\n cacheWrite5mUsd: 0,\n cacheWrite1hUsd: 0,\n outputUsd: 0,\n costUsd: 0,\n priced: false,\n};\n\nexport function priceRequest(\n tokens: RequestTokens,\n model: string,\n ts: string,\n prices: readonly PriceRow[] = PRICE_TABLE,\n): PricedRequest {\n const row = findPrice(model, ts, prices);\n if (!row) return { ...UNPRICED };\n\n const { write5m, write1h } = splitCacheCreation(tokens);\n const inputUsd = perMTok(tokens.inputTokens, row.input);\n const cacheReadUsd = perMTok(tokens.cacheReadTokens, row.cacheRead);\n const cacheWrite5mUsd = perMTok(write5m, row.cacheWrite5m);\n const cacheWrite1hUsd = perMTok(write1h, row.cacheWrite1h);\n const outputUsd = perMTok(tokens.outputTokens, row.output);\n const costUsd = inputUsd + cacheReadUsd + cacheWrite5mUsd + cacheWrite1hUsd + outputUsd;\n return { inputUsd, cacheReadUsd, cacheWrite5mUsd, cacheWrite1hUsd, outputUsd, costUsd, priced: true };\n}\n\n/**\n * Older transcripts carry a cache_creation total with no ttl breakdown. Charging the\n * 5m rate there under-reads rather than over-reads, and 5m is the harness default.\n */\nfunction splitCacheCreation(tokens: RequestTokens): { write5m: number; write1h: number } {\n const write5m = tokens.cacheCreation5mTokens ?? 0;\n const write1h = tokens.cacheCreation1hTokens ?? 0;\n if (write5m > 0 || write1h > 0) return { write5m, write1h };\n return { write5m: tokens.cacheCreationTokens ?? 0, write1h: 0 };\n}\n\nfunction perMTok(tokens: number | undefined, ratePerMTok: number): number {\n return ((tokens ?? 0) * ratePerMTok) / 1_000_000;\n}\n","export type SessionClass = \"agent_spawned\" | \"human_interactive\" | \"headless_sdk\" | \"other_headless\";\n\n/** Human sessions only. A coordinator drives other agents; an adhoc session does not. */\nexport type HumanRole = \"coordinator\" | \"adhoc\";\n\n/** The agent-chat spawn record for a session, as the origin resolver returns it. */\nexport interface SessionOrigin {\n depth: number;\n parentName?: string | null;\n /** \"adopted\" and \"inherited\" mark a pane the human took over, not a spawned worker. */\n originKind?: string | null;\n profile?: string | null;\n}\n\nexport interface SessionFacts {\n /** The harness entrypoint. Agent-chat workers and `-p` miners both report \"sdk-cli\". */\n startType?: string | null;\n origin?: SessionOrigin | null;\n firstUserText?: string | null;\n humanTurnCount?: number;\n inboundKinds?: readonly string[];\n signalKinds?: readonly string[];\n}\n\nexport interface SessionClassification {\n sessionClass: SessionClass;\n reason: string;\n humanRole: HumanRole | null;\n}\n\nconst COORDINATOR_SIGNALS = new Set([\"agent_spawn\", \"chat_send\"]);\n\n/**\n * Rule order matters. The origin row wins over `startType` because agent-chat workers\n * run `claude -p` and so report \"sdk-cli\" exactly like a headless miner does.\n */\nexport function classifySession(facts: SessionFacts): SessionClassification {\n const [sessionClass, reason] = classOf(facts);\n const humanRole = sessionClass === \"human_interactive\" ? humanRoleOf(facts) : null;\n return { sessionClass, reason, humanRole };\n}\n\nfunction classOf(facts: SessionFacts): [SessionClass, string] {\n const origin = facts.origin;\n if (origin) {\n if (isAdoptedPane(origin)) return [\"human_interactive\", \"origin depth 0, human or adopted parent\"];\n return [\"agent_spawned\", \"origin depth >= 1\"];\n }\n if (facts.startType === \"sdk-cli\") return [\"headless_sdk\", \"start_type sdk-cli with no origin row\"];\n if (isProgrammaticPrompt(facts)) return [\"other_headless\", \"content-block prompt with <= 2 human turns\"];\n return [\"human_interactive\", \"no origin row, no headless marker\"];\n}\n\nfunction isAdoptedPane(origin: SessionOrigin): boolean {\n if (origin.depth !== 0) return false;\n if (origin.parentName === \"human\") return true;\n if (origin.originKind === \"adopted\" || origin.originKind === \"inherited\") return true;\n return !origin.profile;\n}\n\n/** Cron and RemoteTrigger daemons inject a structured content block, never typed prose. */\nfunction isProgrammaticPrompt(facts: SessionFacts): boolean {\n const text = (facts.firstUserText ?? \"\").trimStart();\n return text.startsWith(\"[{\") && (facts.humanTurnCount ?? 0) <= 2;\n}\n\nfunction humanRoleOf(facts: SessionFacts): HumanRole {\n if (facts.inboundKinds?.includes(\"channel_message\")) return \"coordinator\";\n if (facts.signalKinds?.some((kind) => COORDINATOR_SIGNALS.has(kind))) return \"coordinator\";\n return \"adhoc\";\n}\n","/** Half-open [lo, hi) in the band's own unit. */\nexport interface Band {\n lo: number;\n hi: number;\n name: string;\n}\n\n/** Context size in tokens. From `cf_analyze.py` BANDS. */\nexport const CONTEXT_BANDS: readonly Band[] = [\n { lo: 0, hi: 50_000, name: \"<50k\" },\n { lo: 50_000, hi: 100_000, name: \"50-100k\" },\n { lo: 100_000, hi: 200_000, name: \"100-200k\" },\n { lo: 200_000, hi: Number.POSITIVE_INFINITY, name: \"200k+\" },\n];\n\n/** Idle gap in milliseconds. From `cf_analyze.py` IDLE. */\nexport const GAP_BANDS: readonly Band[] = [\n { lo: 0, hi: 5 * 60_000, name: \"<5m\" },\n { lo: 5 * 60_000, hi: 60 * 60_000, name: \"5-60m\" },\n { lo: 60 * 60_000, hi: Number.POSITIVE_INFINITY, name: \">60m\" },\n];\n\n/** Null for a value no band covers, so a caller reports the gap instead of inventing one. */\nexport function bandOf(value: number, bands: readonly Band[]): string | null {\n for (const band of bands) {\n if (value >= band.lo && value < band.hi) return band.name;\n }\n return null;\n}\n\nexport function contextBand(tokens: number): string | null {\n return bandOf(tokens, CONTEXT_BANDS);\n}\n\nexport function gapBand(millis: number): string | null {\n return bandOf(millis, GAP_BANDS);\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@titan-design/session-analytics",
3
+ "version": "0.0.0",
4
+ "description": "Pure pricing, session classification and banding over mined session data",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "author": "Henry Jewkes",
19
+ "license": "MIT",
20
+ "homepage": "https://github.com/HJewkes/titan-platform#readme",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/HJewkes/titan-platform.git",
24
+ "directory": "packages/session-analytics"
25
+ },
26
+ "bugs": "https://github.com/HJewkes/titan-platform/issues",
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "typecheck": "tsc --noEmit",
36
+ "lint": "eslint src"
37
+ }
38
+ }