{"version":3,"file":"agent-session-stats.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-stats.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure helpers for AgentSession reporting and export.\n *\n * These functions derive statistics, context-usage, forkable user messages, and\n * JSONL exports from session state without touching the live agent or extension\n * runner. AgentSession delegates to them so the class stays focused on lifecycle\n * and event wiring.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport { calculateContextTokens, estimateContextTokens } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model, UserMessage } from \"@kolisachint/hoocode-ai\";\nimport type { ContextUsage } from \"./extensions/index.js\";\nimport type { SessionEntry, SessionManager } from \"./session-manager.js\";\nimport { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from \"./session-manager.js\";\n\n/** Session statistics for /session command */\nexport interface SessionStats {\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tuserMessages: number;\n\tassistantMessages: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\ttotalMessages: number;\n\ttokens: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n\tcost: number;\n\tcontextUsage?: ContextUsage;\n}\n\n/** Token + cost totals accumulated across a session's assistant messages. */\nexport interface AssistantUsageTotals {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\n/**\n * Sum token + cost usage over every assistant message in a set of session\n * entries. Shared by the footer (cumulative session vitals) and the per-request\n * cost line the transcript emits at agent_end, which diffs two snapshots of this\n * against each other — one source of truth, so the two can never disagree about\n * what a request cost.\n */\nexport function sumAssistantUsage(entries: readonly SessionEntry[]): AssistantUsageTotals {\n\tconst totals: AssistantUsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\" || entry.message.role !== \"assistant\") continue;\n\t\ttotals.input += entry.message.usage.input;\n\t\ttotals.output += entry.message.usage.output;\n\t\ttotals.cacheRead += entry.message.usage.cacheRead;\n\t\ttotals.cacheWrite += entry.message.usage.cacheWrite;\n\t\ttotals.cost += entry.message.usage.cost.total;\n\t}\n\treturn totals;\n}\n\n/** Extract concatenated text from a user message content value. */\nexport function extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {\n\tif (typeof content === \"string\") return content;\n\tif (Array.isArray(content)) {\n\t\treturn content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\t}\n\treturn \"\";\n}\n\n/** Compute aggregate statistics (message counts, token usage, cost) for a session. */\nexport function computeSessionStats(params: {\n\tmessages: AgentMessage[];\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tcontextUsage: ContextUsage | undefined;\n}): SessionStats {\n\tconst { messages, sessionFile, sessionId, contextUsage } = params;\n\tconst userMessages = messages.filter((m) => m.role === \"user\").length;\n\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\").length;\n\tconst toolResults = messages.filter((m) => m.role === \"toolResult\").length;\n\n\tlet toolCalls = 0;\n\tlet totalInput = 0;\n\tlet totalOutput = 0;\n\tlet totalCacheRead = 0;\n\tlet totalCacheWrite = 0;\n\tlet totalCost = 0;\n\n\tfor (const message of messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t}\n\t}\n\n\treturn {\n\t\tsessionFile,\n\t\tsessionId,\n\t\tuserMessages,\n\t\tassistantMessages,\n\t\ttoolCalls,\n\t\ttoolResults,\n\t\ttotalMessages: messages.length,\n\t\ttokens: {\n\t\t\tinput: totalInput,\n\t\t\toutput: totalOutput,\n\t\t\tcacheRead: totalCacheRead,\n\t\t\tcacheWrite: totalCacheWrite,\n\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t},\n\t\tcost: totalCost,\n\t\tcontextUsage,\n\t};\n}\n\n/**\n * Estimate current context-window usage.\n *\n * After compaction, the last assistant usage reflects pre-compaction context\n * size, so usage is only trusted from an assistant that responded after the\n * latest compaction boundary. When no such assistant exists yet, tokens are\n * reported as null (unknown until the next LLM response).\n */\nexport function computeContextUsage(params: {\n\tmodel: Model<any> | undefined;\n\tsessionManager: SessionManager;\n\tmessages: AgentMessage[];\n}): ContextUsage | undefined {\n\tconst { model, sessionManager, messages } = params;\n\tif (!model) return undefined;\n\n\tconst contextWindow = model.contextWindow ?? 0;\n\tif (contextWindow <= 0) return undefined;\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\tif (latestCompaction) {\n\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\tlet hasPostCompactionUsage = false;\n\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\tconst entry = branchEntries[i];\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\tconst assistant = entry.message;\n\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!hasPostCompactionUsage) {\n\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t}\n\t}\n\n\tconst estimate = estimateContextTokens(messages);\n\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\treturn {\n\t\ttokens: estimate.tokens,\n\t\tcontextWindow,\n\t\tpercent,\n\t};\n}\n\n/** Collect all user messages on the session (for the fork selector). */\nexport function collectUserMessagesForForking(\n\tsessionManager: SessionManager,\n): Array<{ entryId: string; text: string }> {\n\tconst entries = sessionManager.getEntries();\n\tconst result: Array<{ entryId: string; text: string }> = [];\n\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tif (entry.message.role !== \"user\") continue;\n\n\t\tconst text = extractUserMessageText(entry.message.content);\n\t\tif (text) {\n\t\t\tresult.push({ entryId: entry.id, text });\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Get the text content of the last non-empty assistant message (for /copy).\n * Returns undefined if no assistant message with text exists.\n */\nexport function getLastAssistantText(messages: AgentMessage[]): string | undefined {\n\tconst lastAssistant = messages\n\t\t.slice()\n\t\t.reverse()\n\t\t.find((m) => {\n\t\t\tif (m.role !== \"assistant\") return false;\n\t\t\tconst msg = m as AssistantMessage;\n\t\t\t// Skip aborted messages with no content\n\t\t\tif (msg.stopReason === \"aborted\" && msg.content.length === 0) return false;\n\t\t\treturn true;\n\t\t});\n\n\tif (!lastAssistant) return undefined;\n\n\tlet text = \"\";\n\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\tif (content.type === \"text\") {\n\t\t\ttext += content.text;\n\t\t}\n\t}\n\n\treturn text.trim() || undefined;\n}\n\n/** How much of a session a copy takes. */\nexport interface TranscriptSelection {\n\t/**\n\t * Exchanges to take, counting back from the newest. Undefined takes the lot.\n\t * An \"exchange\" starts at a user message and runs to the next one.\n\t */\n\tturns?: number;\n\t/** Label for the user's messages; the agent's is the app's name. */\n\tuserLabel?: string;\n\t/** Label for the agent's messages. */\n\tagentLabel?: string;\n}\n\n/**\n * The conversation as markdown, for the clipboard and for a file.\n *\n * ## Why not what is on screen\n *\n * What the terminal shows is markdown already *rendered*: a table is box\n * drawing, a code block is a bordered panel, every line is hard-wrapped to the\n * width the window happened to be. Selecting that with the mouse and pasting it\n * somewhere gives you a picture of a conversation — dotted rules and broken\n * table edges — because a terminal's clipboard carries glyphs and nothing else.\n * The structure the user wanted is in the source the model wrote, which is what\n * this returns.\n *\n * ## What is in it\n *\n * The conversation: what was asked and what was answered, each under a heading\n * so a reader can find the turn they came for. Tool calls are left out. They\n * are the agent's working, they are the bulk of a long session by a wide\n * margin, and nobody has ever pasted a file-read into a document on purpose —\n * `/export` is there for the whole record, including the parts this drops.\n */\nexport function sessionToMarkdown(messages: AgentMessage[], selection: TranscriptSelection = {}): string {\n\tconst userLabel = selection.userLabel ?? \"You\";\n\tconst agentLabel = selection.agentLabel ?? \"Agent\";\n\n\tconst blocks: { label: string; text: string; startsTurn: boolean }[] = [];\n\tfor (const message of messages) {\n\t\tif (message.role === \"user\") {\n\t\t\tconst text = extractUserMessageText((message as UserMessage).content).trim();\n\t\t\tif (text) blocks.push({ label: userLabel, text, startsTurn: true });\n\t\t} else if (message.role === \"assistant\") {\n\t\t\tconst text = (message as AssistantMessage).content\n\t\t\t\t.filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n\t\t\t\t.map((part) => part.text)\n\t\t\t\t.join(\"\")\n\t\t\t\t.trim();\n\t\t\tif (text) blocks.push({ label: agentLabel, text, startsTurn: false });\n\t\t}\n\t}\n\n\tif (selection.turns !== undefined && selection.turns > 0) {\n\t\tconst starts = blocks.map((block, index) => (block.startsTurn ? index : -1)).filter((index) => index >= 0);\n\t\tconst from = starts[Math.max(0, starts.length - selection.turns)];\n\t\tif (from !== undefined) blocks.splice(0, from);\n\t}\n\n\treturn blocks.map((block) => `## ${block.label}\\n\\n${block.text}`).join(\"\\n\\n\");\n}\n\n/**\n * Export the current session branch to a JSONL file.\n * Writes the session header followed by all entries on the current branch path,\n * re-chaining parentIds into a linear sequence.\n * @returns The resolved output file path.\n */\nexport function exportSessionBranchToJsonl(sessionManager: SessionManager, outputPath?: string): string {\n\tconst filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`);\n\tconst dir = dirname(filePath);\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\n\tconst header: SessionHeader = {\n\t\ttype: \"session\",\n\t\tversion: CURRENT_SESSION_VERSION,\n\t\tid: sessionManager.getSessionId(),\n\t\ttimestamp: new Date().toISOString(),\n\t\tcwd: sessionManager.getCwd(),\n\t};\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst lines = [JSON.stringify(header)];\n\n\t// Re-chain parentIds to form a linear sequence\n\tlet prevId: string | null = null;\n\tfor (const entry of branchEntries) {\n\t\tconst linear = { ...entry, parentId: prevId };\n\t\tlines.push(JSON.stringify(linear));\n\t\tprevId = entry.id;\n\t}\n\n\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\treturn filePath;\n}\n"]}