{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/reflect/usage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,QAAA,MAAM,kBAAkB,kLAad,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAChC,aAAa,EAAE,CAAC,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,EAAE,EAAE,MAAM,CAAC;KACX,CAAC;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,gBAAgB,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,gBAAgB,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,gBAAgB,CAAC,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,uBAAuB;IACvC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IAC1C,KAAK,EAAE,IAAI,CAAC;IACZ,KAAK,CAAC,EAAE,IAAI,CAAC;CACb;AAwGD,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAExF;AAED,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,QAAQ,EACf,KAAK,EAAE,eAAe,EACtB,GAAG,EAAE,cAAc,EACnB,OAAO,GAAE,uBAA4B,GACnC,OAAO,CAAC,gBAAgB,CAAC,CAmB3B;AAoBD,wBAAsB,mBAAmB,CACxC,KAAK,EAAE,QAAQ,EACf,OAAO,EAAE,0BAA0B,GACjC,OAAO,CAAC,iBAAiB,CAAC,CAsB5B;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAoB1E","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { EvoPaths } from \"../paths.ts\";\nimport { appendJsonLine, truncateIncompleteFinalLine, withFileLock } from \"../storage.ts\";\nimport type { ModelRunResult } from \"./model-runner.ts\";\n\nconst MODEL_USAGE_PHASES = [\n\t\"reflector\",\n\t\"research-plan\",\n\t\"builder\",\n\t\"evaluator\",\n\t\"adversarial-review\",\n\t\"report\",\n\t\"critic\",\n\t\"replay-old\",\n\t\"replay-candidate\",\n\t\"permit\",\n\t\"retrospective\",\n\t\"triage\",\n] as const;\n\nexport type ModelUsagePhase = (typeof MODEL_USAGE_PHASES)[number];\n\nexport interface ModelUsageTokens {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\ttotal: number;\n}\n\nexport interface ModelUsageRecord {\n\tschemaVersion: 1;\n\ttimestamp: string;\n\tphase: ModelUsagePhase;\n\tmodel: {\n\t\tprovider: string;\n\t\tid: string;\n\t};\n\tsessionId: string;\n\ttokens: ModelUsageTokens;\n\tcost: number;\n}\n\nexport interface ModelUsageTotals {\n\truns: number;\n\ttokens: ModelUsageTokens;\n\tcost: number;\n}\n\nexport interface ModelUsageSummary extends ModelUsageTotals {\n\tsince: string;\n\tuntil: string;\n\tbyPhase: Partial<Record<ModelUsagePhase, ModelUsageTotals>>;\n}\n\nexport interface RecordModelUsageOptions {\n\tnow?: () => Date;\n}\n\nexport interface SummarizeModelUsageOptions {\n\tsince: Date;\n\tuntil?: Date;\n}\n\nconst MODEL_USAGE_PHASE_SET = new Set<ModelUsagePhase>(MODEL_USAGE_PHASES);\n\nfunction journalPath(paths: EvoPaths): string {\n\treturn join(paths.reports, \"model-usage.jsonl\");\n}\n\nfunction errorCode(error: unknown): string | undefined {\n\tif (typeof error !== \"object\" || error === null || !(\"code\" in error)) return undefined;\n\treturn typeof error.code === \"string\" ? error.code : undefined;\n}\n\nfunction nonNegativeSafeInteger(value: unknown, label: string): number {\n\tif (typeof value !== \"number\" || !Number.isSafeInteger(value) || value < 0) {\n\t\tthrow new Error(`${label} must be a non-negative safe integer`);\n\t}\n\treturn value;\n}\n\nfunction nonNegativeFiniteNumber(value: unknown, label: string): number {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\tthrow new Error(`${label} must be a non-negative finite number`);\n\t}\n\treturn value;\n}\n\nfunction nonEmptyString(value: unknown, label: string): string {\n\tif (typeof value !== \"string\" || !value || value.length > 500) {\n\t\tthrow new Error(`${label} must be a non-empty string of at most 500 characters`);\n\t}\n\treturn value;\n}\n\nfunction parseTokens(value: unknown, label: string): ModelUsageTokens {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new Error(`${label} must be an object`);\n\t}\n\tconst record = value as Record<string, unknown>;\n\treturn {\n\t\tinput: nonNegativeSafeInteger(record.input, `${label}.input`),\n\t\toutput: nonNegativeSafeInteger(record.output, `${label}.output`),\n\t\tcacheRead: nonNegativeSafeInteger(record.cacheRead, `${label}.cacheRead`),\n\t\tcacheWrite: nonNegativeSafeInteger(record.cacheWrite, `${label}.cacheWrite`),\n\t\ttotal: nonNegativeSafeInteger(record.total, `${label}.total`),\n\t};\n}\n\nfunction parseModelUsageRecord(value: unknown, line: number): ModelUsageRecord {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new Error(`Model usage journal line ${line} must be an object`);\n\t}\n\tconst record = value as Record<string, unknown>;\n\tif (record.schemaVersion !== 1) throw new Error(`Model usage journal line ${line} has an invalid schema version`);\n\tif (typeof record.timestamp !== \"string\" || !Number.isFinite(Date.parse(record.timestamp))) {\n\t\tthrow new Error(`Model usage journal line ${line} has an invalid timestamp`);\n\t}\n\tif (typeof record.phase !== \"string\" || !MODEL_USAGE_PHASE_SET.has(record.phase as ModelUsagePhase)) {\n\t\tthrow new Error(`Model usage journal line ${line} has an invalid phase`);\n\t}\n\tif (typeof record.model !== \"object\" || record.model === null || Array.isArray(record.model)) {\n\t\tthrow new Error(`Model usage journal line ${line} has an invalid model`);\n\t}\n\tconst model = record.model as Record<string, unknown>;\n\treturn {\n\t\tschemaVersion: 1,\n\t\ttimestamp: record.timestamp,\n\t\tphase: record.phase as ModelUsagePhase,\n\t\tmodel: {\n\t\t\tprovider: nonEmptyString(model.provider, `Model usage journal line ${line} model.provider`),\n\t\t\tid: nonEmptyString(model.id, `Model usage journal line ${line} model.id`),\n\t\t},\n\t\tsessionId: nonEmptyString(record.sessionId, `Model usage journal line ${line} sessionId`),\n\t\ttokens: parseTokens(record.tokens, `Model usage journal line ${line} tokens`),\n\t\tcost: nonNegativeFiniteNumber(record.cost, `Model usage journal line ${line} cost`),\n\t};\n}\n\nasync function readRecordsUnlocked(paths: EvoPaths): Promise<ModelUsageRecord[]> {\n\tawait truncateIncompleteFinalLine(journalPath(paths));\n\tlet content: string;\n\ttry {\n\t\tcontent = await readFile(journalPath(paths), \"utf8\");\n\t} catch (error) {\n\t\tif (errorCode(error) === \"ENOENT\") return [];\n\t\tthrow error;\n\t}\n\tif (!content) return [];\n\tif (!content.endsWith(\"\\n\")) throw new Error(\"Model usage journal has an incomplete final line\");\n\treturn content\n\t\t.slice(0, -1)\n\t\t.split(\"\\n\")\n\t\t.map((line, index) => {\n\t\t\tif (!line) throw new Error(`Model usage journal line ${index + 1} is empty`);\n\t\t\tlet value: unknown;\n\t\t\ttry {\n\t\t\t\tvalue = JSON.parse(line);\n\t\t\t} catch {\n\t\t\t\tthrow new Error(`Model usage journal line ${index + 1} is invalid JSON`);\n\t\t\t}\n\t\t\treturn parseModelUsageRecord(value, index + 1);\n\t\t});\n}\n\nexport async function readModelUsageRecords(paths: EvoPaths): Promise<ModelUsageRecord[]> {\n\treturn withFileLock(paths, \"model-usage\", () => readRecordsUnlocked(paths));\n}\n\nexport async function recordModelUsage(\n\tpaths: EvoPaths,\n\tphase: ModelUsagePhase,\n\trun: ModelRunResult,\n\toptions: RecordModelUsageOptions = {},\n): Promise<ModelUsageRecord> {\n\tif (!MODEL_USAGE_PHASE_SET.has(phase)) throw new Error(`Unsupported model usage phase: ${phase}`);\n\tconst record = parseModelUsageRecord(\n\t\t{\n\t\t\tschemaVersion: 1,\n\t\t\ttimestamp: (options.now?.() ?? new Date()).toISOString(),\n\t\t\tphase,\n\t\t\tmodel: run.model,\n\t\t\tsessionId: run.stats.sessionId,\n\t\t\ttokens: run.stats.tokens,\n\t\t\tcost: run.stats.cost,\n\t\t},\n\t\t1,\n\t);\n\treturn withFileLock(paths, \"model-usage\", async () => {\n\t\tawait readRecordsUnlocked(paths);\n\t\tawait appendJsonLine(journalPath(paths), record);\n\t\treturn record;\n\t});\n}\n\nfunction emptyTotals(): ModelUsageTotals {\n\treturn {\n\t\truns: 0,\n\t\ttokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\tcost: 0,\n\t};\n}\n\nfunction addRecord(totals: ModelUsageTotals, record: ModelUsageRecord): void {\n\ttotals.runs += 1;\n\ttotals.tokens.input += record.tokens.input;\n\ttotals.tokens.output += record.tokens.output;\n\ttotals.tokens.cacheRead += record.tokens.cacheRead;\n\ttotals.tokens.cacheWrite += record.tokens.cacheWrite;\n\ttotals.tokens.total += record.tokens.total;\n\ttotals.cost += record.cost;\n}\n\nexport async function summarizeModelUsage(\n\tpaths: EvoPaths,\n\toptions: SummarizeModelUsageOptions,\n): Promise<ModelUsageSummary> {\n\tconst until = options.until ?? new Date();\n\tconst sinceMs = options.since.getTime();\n\tconst untilMs = until.getTime();\n\tif (!Number.isFinite(sinceMs) || !Number.isFinite(untilMs) || sinceMs > untilMs) {\n\t\tthrow new Error(\"Model usage summary window is invalid\");\n\t}\n\tconst summary: ModelUsageSummary = {\n\t\t...emptyTotals(),\n\t\tsince: options.since.toISOString(),\n\t\tuntil: until.toISOString(),\n\t\tbyPhase: {},\n\t};\n\tfor (const record of await readModelUsageRecords(paths)) {\n\t\tconst timestamp = Date.parse(record.timestamp);\n\t\tif (timestamp < sinceMs || timestamp > untilMs) continue;\n\t\taddRecord(summary, record);\n\t\tconst phaseTotals = summary.byPhase[record.phase] ?? emptyTotals();\n\t\taddRecord(phaseTotals, record);\n\t\tsummary.byPhase[record.phase] = phaseTotals;\n\t}\n\treturn summary;\n}\n\nexport function renderModelUsageSummary(summary: ModelUsageSummary): string {\n\tconst rows = MODEL_USAGE_PHASES.flatMap((phase) => {\n\t\tconst totals = summary.byPhase[phase];\n\t\treturn totals\n\t\t\t? [\n\t\t\t\t\t`| ${phase} | ${totals.runs} | ${totals.tokens.input} | ${totals.tokens.output} | ${totals.tokens.cacheRead} | ${totals.tokens.cacheWrite} | ${totals.tokens.total} | $${totals.cost.toFixed(6)} |`,\n\t\t\t\t]\n\t\t\t: [];\n\t});\n\treturn [\n\t\t\"## Background model usage\",\n\t\t\"\",\n\t\t`Window: ${summary.since} through ${summary.until}`,\n\t\t\"\",\n\t\t\"| Phase | Runs | Input | Output | Cache read | Cache write | Total | Cost |\",\n\t\t\"|---|---:|---:|---:|---:|---:|---:|---:|\",\n\t\t...rows,\n\t\t`| **Total** | **${summary.runs}** | **${summary.tokens.input}** | **${summary.tokens.output}** | **${summary.tokens.cacheRead}** | **${summary.tokens.cacheWrite}** | **${summary.tokens.total}** | **$${summary.cost.toFixed(6)}** |`,\n\t\t\"\",\n\t].join(\"\\n\");\n}\n"]}