mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 13:02:15 +00:00
mom: add centralized logging, usage tracking, and improve prompt caching
Major improvements to mom's logging and cost reporting: Centralized Logging System: - Add src/log.ts with type-safe logging functions - Colored console output (green=user, yellow=mom, dim=details) - Consistent format: [HH:MM:SS] [context] message - Replace scattered console.log/error calls throughout codebase Usage Tracking & Cost Reporting: - Track tokens (input, output, cache read/write) and costs per run - Display summary at end of each run in console and Slack thread - Example: 💰 Usage: 12,543 in + 847 out (5,234 cache read) = $0.0234 Prompt Caching Optimization: - Move recent messages from system prompt to user message - System prompt now mostly static (only changes with memory files) - Enables effective use of Anthropic's prompt caching - Significantly reduces costs on subsequent requests Model & Cost Improvements: - Switch from Claude Opus 4.5 to Sonnet 4.5 (~40% cost reduction) - Fix Claude Opus 4.5 cache pricing in ai package (was 3x too expensive) - Add manual override in generate-models.ts until upstream fix merges - Submitted PR to models.dev: https://github.com/sst/models.dev/pull/439 UI/UX Improvements: - Extract actual text from tool results instead of JSON wrapper - Cleaner Slack thread formatting with duration and labels - Tool args formatting shows paths with offset:limit notation - Add chalk for colored terminal output Dependencies: - Add chalk package for terminal colors
This commit is contained in:
parent
82d4ac93e1
commit
213bc4df1c
11 changed files with 478 additions and 63 deletions
226
packages/mom/src/log.ts
Normal file
226
packages/mom/src/log.ts
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
import chalk from "chalk";
|
||||
|
||||
export interface LogContext {
|
||||
channelId: string;
|
||||
userName?: string;
|
||||
channelName?: string; // For display like #dev-team vs C16HET4EQ
|
||||
}
|
||||
|
||||
function timestamp(): string {
|
||||
const now = new Date();
|
||||
const hh = String(now.getHours()).padStart(2, "0");
|
||||
const mm = String(now.getMinutes()).padStart(2, "0");
|
||||
const ss = String(now.getSeconds()).padStart(2, "0");
|
||||
return `[${hh}:${mm}:${ss}]`;
|
||||
}
|
||||
|
||||
function formatContext(ctx: LogContext): string {
|
||||
// DMs: [DM:username]
|
||||
// Channels: [#channel-name:username] or [C16HET4EQ:username] if no name
|
||||
if (ctx.channelId.startsWith("D")) {
|
||||
return `[DM:${ctx.userName || ctx.channelId}]`;
|
||||
}
|
||||
const channel = ctx.channelName || ctx.channelId;
|
||||
const user = ctx.userName || "unknown";
|
||||
return `[${channel.startsWith("#") ? channel : `#${channel}`}:${user}]`;
|
||||
}
|
||||
|
||||
function truncate(text: string, maxLen: number): string {
|
||||
if (text.length <= maxLen) return text;
|
||||
return text.substring(0, maxLen) + `\n(truncated at ${maxLen} chars)`;
|
||||
}
|
||||
|
||||
function formatToolArgs(args: Record<string, unknown>): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(args)) {
|
||||
// Skip the label - it's already shown in the tool name
|
||||
if (key === "label") continue;
|
||||
|
||||
// For read tool, format path with offset/limit
|
||||
if (key === "path" && typeof value === "string") {
|
||||
const offset = args.offset as number | undefined;
|
||||
const limit = args.limit as number | undefined;
|
||||
if (offset !== undefined && limit !== undefined) {
|
||||
lines.push(`${value}:${offset}-${offset + limit}`);
|
||||
} else {
|
||||
lines.push(value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip offset/limit since we already handled them
|
||||
if (key === "offset" || key === "limit") continue;
|
||||
|
||||
// For other values, format them
|
||||
if (typeof value === "string") {
|
||||
// Multi-line strings get indented
|
||||
if (value.includes("\n")) {
|
||||
lines.push(value);
|
||||
} else {
|
||||
lines.push(value);
|
||||
}
|
||||
} else {
|
||||
lines.push(JSON.stringify(value));
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// User messages
|
||||
export function logUserMessage(ctx: LogContext, text: string): void {
|
||||
console.log(chalk.green(`${timestamp()} ${formatContext(ctx)} ${text}`));
|
||||
}
|
||||
|
||||
// Tool execution
|
||||
export function logToolStart(ctx: LogContext, toolName: string, label: string, args: Record<string, unknown>): void {
|
||||
const formattedArgs = formatToolArgs(args);
|
||||
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↳ ${toolName}: ${label}`));
|
||||
if (formattedArgs) {
|
||||
// Indent the args
|
||||
const indented = formattedArgs
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
console.log(chalk.dim(indented));
|
||||
}
|
||||
}
|
||||
|
||||
export function logToolSuccess(ctx: LogContext, toolName: string, durationMs: number, result: string): void {
|
||||
const duration = (durationMs / 1000).toFixed(1);
|
||||
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ ${toolName} (${duration}s)`));
|
||||
|
||||
const truncated = truncate(result, 1000);
|
||||
if (truncated) {
|
||||
const indented = truncated
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
console.log(chalk.dim(indented));
|
||||
}
|
||||
}
|
||||
|
||||
export function logToolError(ctx: LogContext, toolName: string, durationMs: number, error: string): void {
|
||||
const duration = (durationMs / 1000).toFixed(1);
|
||||
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ ${toolName} (${duration}s)`));
|
||||
|
||||
const truncated = truncate(error, 1000);
|
||||
const indented = truncated
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
console.log(chalk.dim(indented));
|
||||
}
|
||||
|
||||
// Response streaming
|
||||
export function logResponseStart(ctx: LogContext): void {
|
||||
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`));
|
||||
}
|
||||
|
||||
export function logResponseComplete(ctx: LogContext, charCount: number): void {
|
||||
console.log(
|
||||
chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Response sent (${charCount.toLocaleString()} chars)`),
|
||||
);
|
||||
}
|
||||
|
||||
// Attachments
|
||||
export function logDownloadStart(ctx: LogContext, filename: string, localPath: string): void {
|
||||
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ↓ Downloading attachment`));
|
||||
console.log(chalk.dim(` ${filename} → ${localPath}`));
|
||||
}
|
||||
|
||||
export function logDownloadSuccess(ctx: LogContext, sizeKB: number): void {
|
||||
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Downloaded (${sizeKB.toLocaleString()} KB)`));
|
||||
}
|
||||
|
||||
export function logDownloadError(ctx: LogContext, filename: string, error: string): void {
|
||||
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✗ Download failed`));
|
||||
console.log(chalk.dim(` ${filename}: ${error}`));
|
||||
}
|
||||
|
||||
// Control
|
||||
export function logStopRequest(ctx: LogContext): void {
|
||||
console.log(chalk.green(`${timestamp()} ${formatContext(ctx)} stop`));
|
||||
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} ⊗ Stop requested - aborting`));
|
||||
}
|
||||
|
||||
// System
|
||||
export function logWarning(message: string, details?: string): void {
|
||||
console.log(chalk.yellow(`${timestamp()} [system] ⚠ ${message}`));
|
||||
if (details) {
|
||||
const indented = details
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
console.log(chalk.dim(indented));
|
||||
}
|
||||
}
|
||||
|
||||
export function logAgentError(ctx: LogContext | "system", error: string): void {
|
||||
const context = ctx === "system" ? "[system]" : formatContext(ctx);
|
||||
console.log(chalk.yellow(`${timestamp()} ${context} ✗ Agent error`));
|
||||
const indented = error
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
console.log(chalk.dim(indented));
|
||||
}
|
||||
|
||||
// Usage summary
|
||||
export function logUsageSummary(
|
||||
ctx: LogContext,
|
||||
usage: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
|
||||
},
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("*Usage Summary*");
|
||||
lines.push(`Tokens: ${usage.input.toLocaleString()} in, ${usage.output.toLocaleString()} out`);
|
||||
if (usage.cacheRead > 0 || usage.cacheWrite > 0) {
|
||||
lines.push(`Cache: ${usage.cacheRead.toLocaleString()} read, ${usage.cacheWrite.toLocaleString()} write`);
|
||||
}
|
||||
lines.push(
|
||||
`Cost: $${usage.cost.input.toFixed(4)} in, $${usage.cost.output.toFixed(4)} out` +
|
||||
(usage.cacheRead > 0 || usage.cacheWrite > 0
|
||||
? `, $${usage.cost.cacheRead.toFixed(4)} cache read, $${usage.cost.cacheWrite.toFixed(4)} cache write`
|
||||
: ""),
|
||||
);
|
||||
lines.push(`*Total: $${usage.cost.total.toFixed(4)}*`);
|
||||
|
||||
const summary = lines.join("\n");
|
||||
|
||||
// Log to console
|
||||
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💰 Usage`));
|
||||
console.log(
|
||||
chalk.dim(
|
||||
` ${usage.input.toLocaleString()} in + ${usage.output.toLocaleString()} out` +
|
||||
(usage.cacheRead > 0 || usage.cacheWrite > 0
|
||||
? ` (${usage.cacheRead.toLocaleString()} cache read, ${usage.cacheWrite.toLocaleString()} cache write)`
|
||||
: "") +
|
||||
` = $${usage.cost.total.toFixed(4)}`,
|
||||
),
|
||||
);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Startup (no context needed)
|
||||
export function logStartup(workingDir: string, sandbox: string): void {
|
||||
console.log("Starting mom bot...");
|
||||
console.log(` Working directory: ${workingDir}`);
|
||||
console.log(` Sandbox: ${sandbox}`);
|
||||
}
|
||||
|
||||
export function logConnected(): void {
|
||||
console.log("⚡️ Mom bot connected and listening!");
|
||||
console.log("");
|
||||
}
|
||||
|
||||
export function logDisconnected(): void {
|
||||
console.log("Mom bot disconnected.");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue