feat: persist session usage details (#1050)
This commit is contained in:
176
src/agent/sessionHistory.ts
Normal file
176
src/agent/sessionHistory.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { SessionStatsSnapshot } from "./stats";
|
||||
|
||||
export interface SessionHistoryEntry {
|
||||
agent_id: string;
|
||||
session_id: string;
|
||||
timestamp: number;
|
||||
project: string;
|
||||
model: string;
|
||||
provider: string;
|
||||
usage: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
cached_input_tokens: number;
|
||||
cache_write_tokens: number;
|
||||
reasoning_tokens: number;
|
||||
context_tokens?: number;
|
||||
steps: number;
|
||||
};
|
||||
duration: {
|
||||
api_ms: number;
|
||||
wall_ms: number;
|
||||
};
|
||||
cost: {
|
||||
type: "hosted" | "byok";
|
||||
credits_used?: number;
|
||||
usd_byok?: number;
|
||||
};
|
||||
message_count?: number;
|
||||
tool_call_count?: number;
|
||||
exit_reason?: string;
|
||||
}
|
||||
|
||||
interface SessionStartData {
|
||||
agentId: string;
|
||||
sessionId: string;
|
||||
project: string;
|
||||
model: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Letta Code history directory
|
||||
*/
|
||||
function getHistoryDir(): string {
|
||||
const homeDir = os.homedir();
|
||||
return path.join(homeDir, ".letta-code");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session history file path
|
||||
*/
|
||||
function getHistoryFilePath(): string {
|
||||
return path.join(getHistoryDir(), "sessions.jsonl");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the history directory and file exist
|
||||
*/
|
||||
function ensureHistoryFile(): void {
|
||||
const dir = getHistoryDir();
|
||||
const filePath = getHistoryFilePath();
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
fs.writeFileSync(filePath, "");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record session start data (called when a session begins)
|
||||
*/
|
||||
export function recordSessionStart(data: SessionStartData): void {
|
||||
ensureHistoryFile();
|
||||
|
||||
const entry: SessionHistoryEntry = {
|
||||
agent_id: data.agentId,
|
||||
session_id: data.sessionId,
|
||||
timestamp: Date.now(),
|
||||
project: data.project,
|
||||
model: data.model,
|
||||
provider: data.provider,
|
||||
usage: {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
cached_input_tokens: 0,
|
||||
cache_write_tokens: 0,
|
||||
reasoning_tokens: 0,
|
||||
steps: 0,
|
||||
},
|
||||
duration: {
|
||||
api_ms: 0,
|
||||
wall_ms: 0,
|
||||
},
|
||||
cost: {
|
||||
type: "hosted", // Default, will be updated on session end
|
||||
},
|
||||
};
|
||||
|
||||
const filePath = getHistoryFilePath();
|
||||
fs.appendFileSync(filePath, `${JSON.stringify(entry)}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session entry with final stats (called when session ends)
|
||||
* For now, this just updates an in-memory tracking - actual file updates
|
||||
* would require rewriting the file or using a different storage approach
|
||||
*/
|
||||
export function recordSessionEnd(
|
||||
agentId: string,
|
||||
sessionId: string,
|
||||
stats: SessionStatsSnapshot,
|
||||
sessionInfo?: { project?: string; model?: string; provider?: string },
|
||||
cost?: { credits_used?: number; usd_byok?: number; type: "hosted" | "byok" },
|
||||
metadata?: {
|
||||
messageCount?: number;
|
||||
toolCallCount?: number;
|
||||
exitReason?: string;
|
||||
},
|
||||
): void {
|
||||
// For now, we'll append a new "end" entry with the final stats
|
||||
// A more sophisticated approach would update the existing entry
|
||||
const entry: SessionHistoryEntry = {
|
||||
agent_id: agentId,
|
||||
session_id: sessionId,
|
||||
timestamp: Date.now(),
|
||||
project: sessionInfo?.project ?? "",
|
||||
model: sessionInfo?.model ?? "",
|
||||
provider: sessionInfo?.provider ?? "",
|
||||
usage: {
|
||||
prompt_tokens: stats.usage.promptTokens,
|
||||
completion_tokens: stats.usage.completionTokens,
|
||||
total_tokens: stats.usage.totalTokens,
|
||||
cached_input_tokens: stats.usage.cachedInputTokens,
|
||||
cache_write_tokens: stats.usage.cacheWriteTokens,
|
||||
reasoning_tokens: stats.usage.reasoningTokens,
|
||||
context_tokens: stats.usage.contextTokens,
|
||||
steps: stats.usage.stepCount,
|
||||
},
|
||||
duration: {
|
||||
api_ms: stats.totalApiMs,
|
||||
wall_ms: stats.totalWallMs,
|
||||
},
|
||||
cost: cost || { type: "hosted" },
|
||||
message_count: metadata?.messageCount,
|
||||
tool_call_count: metadata?.toolCallCount,
|
||||
exit_reason: metadata?.exitReason,
|
||||
};
|
||||
|
||||
// For v1, we just append end entries - in future, we could update the start entry
|
||||
const filePath = getHistoryFilePath();
|
||||
fs.appendFileSync(filePath, `${JSON.stringify(entry)}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all session history entries
|
||||
*/
|
||||
export function getSessionHistory(): SessionHistoryEntry[] {
|
||||
const filePath = getHistoryFilePath();
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const lines = content.split("\n").filter((line) => line.trim());
|
||||
|
||||
return lines.map((line) => JSON.parse(line) as SessionHistoryEntry);
|
||||
}
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
type ModelReasoningEffort,
|
||||
} from "../agent/model";
|
||||
import { INTERRUPT_RECOVERY_ALERT } from "../agent/promptAssets";
|
||||
import { recordSessionEnd } from "../agent/sessionHistory";
|
||||
import { SessionStats } from "../agent/stats";
|
||||
import {
|
||||
INTERRUPTED_BY_USER,
|
||||
@@ -5493,6 +5494,28 @@ export default function App({
|
||||
const stats = sessionStatsRef.current.getSnapshot();
|
||||
telemetry.trackSessionEnd(stats, "exit_command");
|
||||
|
||||
// Record session to local history file
|
||||
try {
|
||||
recordSessionEnd(
|
||||
agentId,
|
||||
telemetry.getSessionId(),
|
||||
stats,
|
||||
{
|
||||
project: projectDirectory,
|
||||
model: currentModelLabel ?? "",
|
||||
provider: currentModelProvider ?? "",
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
messageCount: telemetry.getMessageCount(),
|
||||
toolCallCount: telemetry.getToolCallCount(),
|
||||
exitReason: "exit_command",
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
// Non-critical, don't fail the exit
|
||||
}
|
||||
|
||||
// Flush telemetry before exit
|
||||
await telemetry.flush();
|
||||
|
||||
@@ -5501,7 +5524,13 @@ export default function App({
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 100);
|
||||
}, [runEndHooks]);
|
||||
}, [
|
||||
runEndHooks,
|
||||
agentId,
|
||||
projectDirectory,
|
||||
currentModelLabel,
|
||||
currentModelProvider,
|
||||
]);
|
||||
|
||||
// Handler when user presses UP/ESC to load queue into input for editing
|
||||
const handleEnterQueueEditMode = useCallback(() => {
|
||||
@@ -7369,6 +7398,28 @@ export default function App({
|
||||
const stats = sessionStatsRef.current.getSnapshot();
|
||||
telemetry.trackSessionEnd(stats, "logout");
|
||||
|
||||
// Record session to local history file
|
||||
try {
|
||||
recordSessionEnd(
|
||||
agentId,
|
||||
telemetry.getSessionId(),
|
||||
stats,
|
||||
{
|
||||
project: projectDirectory,
|
||||
model: currentModelLabel ?? "",
|
||||
provider: currentModelProvider ?? "",
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
messageCount: telemetry.getMessageCount(),
|
||||
toolCallCount: telemetry.getToolCallCount(),
|
||||
exitReason: "logout",
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
// Non-critical, don't fail the exit
|
||||
}
|
||||
|
||||
// Flush telemetry before exit
|
||||
await telemetry.flush();
|
||||
|
||||
|
||||
@@ -238,6 +238,20 @@ class TelemetryManager {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current message count
|
||||
*/
|
||||
getMessageCount(): number {
|
||||
return this.messageCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tool call count
|
||||
*/
|
||||
getToolCallCount(): number {
|
||||
return this.toolCallCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track session start
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user