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:
Mario Zechner 2025-11-26 18:04:16 +01:00
parent 82d4ac93e1
commit 213bc4df1c
11 changed files with 478 additions and 63 deletions

View file

@ -2,6 +2,7 @@
import { join, resolve } from "path";
import { type AgentRunner, createAgentRunner } from "./agent.js";
import * as log from "./log.js";
import { parseSandboxArg, type SandboxConfig, validateSandbox } from "./sandbox.js";
import { MomBot, type SlackContext } from "./slack.js";
@ -53,9 +54,7 @@ function parseArgs(): { workingDir: string; sandbox: SandboxConfig } {
const { workingDir, sandbox } = parseArgs();
console.log("Starting mom bot...");
console.log(` Working directory: ${workingDir}`);
console.log(` Sandbox: ${sandbox.type === "host" ? "host" : `docker:${sandbox.container}`}`);
log.logStartup(workingDir, sandbox.type === "host" ? "host" : `docker:${sandbox.container}`);
if (!MOM_SLACK_APP_TOKEN || !MOM_SLACK_BOT_TOKEN || (!ANTHROPIC_API_KEY && !ANTHROPIC_OAUTH_TOKEN)) {
console.error("Missing required environment variables:");
@ -75,11 +74,17 @@ async function handleMessage(ctx: SlackContext, source: "channel" | "dm"): Promi
const channelId = ctx.message.channel;
const messageText = ctx.message.text.toLowerCase().trim();
const logCtx = {
channelId: ctx.message.channel,
userName: ctx.message.userName,
channelName: ctx.channelName,
};
// Check for stop command
if (messageText === "stop") {
const runner = activeRuns.get(channelId);
if (runner) {
console.log(`Stop requested for channel ${channelId}`);
log.logStopRequest(logCtx);
runner.abort();
await ctx.respond("_Stopping..._");
} else {
@ -94,7 +99,7 @@ async function handleMessage(ctx: SlackContext, source: "channel" | "dm"): Promi
return;
}
console.log(`${source === "channel" ? "Channel mention" : "DM"} from <@${ctx.message.user}>: ${ctx.message.text}`);
log.logUserMessage(logCtx, ctx.message.text);
const channelDir = join(workingDir, channelId);
const runner = createAgentRunner(sandbox);
@ -109,7 +114,7 @@ async function handleMessage(ctx: SlackContext, source: "channel" | "dm"): Promi
if (msg.includes("aborted") || msg.includes("Aborted")) {
// Already said "Stopping..." - nothing more to say
} else {
console.error("Agent error:", error);
log.logAgentError(logCtx, msg);
await ctx.respond(`❌ Error: ${msg}`);
}
} finally {