TheFocus.AI TheFocus.AI
02 core Lesson 6

Chapter 06: Session Logging

Record every message in JSONL format for debugging, analysis, and future compaction. Add slash commands for session control.

TUTOR WITH THEFOCUS.AI

Agent Integration

Copy this prompt into Claude, ChatGPT, or any external AI assistant. It points the assistant to the course instructions and links it to your student profile to track your progress and customize observations.

Please tutor me in this lesson using the following context. First, read the instructions at: https://courses.thefocus.ai/llms.txt My Student ID is: <none> The lesson markdown source is at: https://courses.thefocus.ai/build-ai-coding-agent/02-core/06-sessions.md

You are not enrolled yet. Enroll to generate a Student ID to track lesson completions and store learning notes.

Chapter 06: Session Logging

What We’re Building

Session logging records every message — system prompts, user input, tool calls, tool results, assistant responses — into a dated JSONL file. This is essential for:

  • Debugging: What did the agent actually do?
  • Analysis: Which tools are used most? Where does it fail?
  • Compaction: Summarize old context to stay within the token limit (Chapter 11)
  • Resumption: Pick up where you left off

Step 1: Add Session Logging

Paste this prompt into your agent:

Add session-logging functionality to the system. Create a jsonl file in .session_logs/ with the name of the current date, like YYYY-MM-DD-HH-MM.jsonl that contains an entry for each message. We’ll use these files to examine and tune the system, and manage subsessions and session compactions and stuff like that.

Step 2: The Implementation

The agent will create src/lib/logger.ts:

import { randomUUID } from "node:crypto";
import * as fs from "node:fs";
import type { Message } from "./types";

export interface Logger {
  log(message: Message): void;
  child(agentName: string): Logger;
}

export class SessionLogger implements Logger {
  private logFile: string;
  private sessionId: string;
  private parentId: string | null;
  private agentName: string;

  constructor(
    logFile: string,
    sessionId: string = randomUUID(),
    parentId: string | null = null,
    agentName: string = "main",
  ) {
    this.logFile = logFile;
    this.sessionId = sessionId;
    this.parentId = parentId;
    this.agentName = agentName;
  }

  log(message: Message): void {
    const entry = {
      timestamp: new Date().toISOString(),
      sessionId: this.sessionId,
      parentId: this.parentId,
      agentName: this.agentName,
      message,
    };
    fs.appendFileSync(this.logFile, `${JSON.stringify(entry)}\n`);
  }

  child(agentName: string): Logger {
    const childSessionId = randomUUID();
    return new SessionLogger(
      this.logFile,
      childSessionId,
      this.sessionId,
      agentName,
    );
  }
}

Key design decisions:

  • sessionId: UUID unique to this session, links all messages from one run
  • parentId: For subagents, links child messages back to the parent session
  • agentName: Which agent (main, code-map, tech-researcher, etc.) generated this message
  • timestamp: ISO format for chronological ordering

The Startup Sequence (in src/index.ts)

const logDir = path.join(process.cwd(), ".session_logs");
if (!fs.existsSync(logDir)) {
  fs.mkdirSync(logDir, { recursive: true });
}

const now = new Date();
const logFileName = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}.jsonl`;
const logFile = path.join(logDir, logFileName);
const logger = new SessionLogger(logFile);

Step 3: Log Every Message

The logger is passed through the system and records every message type:

// System prompt
logger.log(history[0]);

// User messages
logger.log(userMsg);

// Assistant messages (including tool calls)
logger.log(assistantMsg);

// Tool results
toolResults.forEach((msg) => logger.log(msg));

A typical log entry looks like:

{
  "timestamp": "2025-04-15T14:23:01.234Z",
  "sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "parentId": null,
  "agentName": "main",
  "message": {
    "role": "assistant",
    "tool_calls": [
      {
        "function": {
          "name": "read_file",
          "arguments": "{\"path\": \"src/agent.ts\"}"
        }
      }
    ]
  }
}

Step 4: Subagent Logging

When a subagent is spawned via run_agent, it gets a child logger:

const subLogger = logger ? logger.child(subAgentName) : undefined;

The child logger:

  • Gets a new sessionId (UUID)
  • Records the parent’s sessionId as parentId
  • Sets the agentName to the subagent’s name

This creates a tree structure in your logs — you can reconstruct which parent spawned which subagent and what happened in each.

Step 5: Verify

Start your agent, have a few turns of conversation, then check the logs:

ls -la .session_logs/
cat .session_logs/2025-04-15-14-23.jsonl | head -5

You should see every message from your session, timestamped and attributed.

Step 6: Add Slash Commands

Paste this additional prompt:

Add slash commands for /help, /models, /search, /compact, /restore, /fork, /session. These should work in the REPL input.

The agent will add command parsing to src/index.ts that intercepts lines starting with /:

CommandWhat It Does
/helpShow available commands and agent info
/modelsDisplay model stats and pricing
/searchSearch session logs for a pattern
/compactSummarize conversation history (see Chapter 11)
/restore <id>Resume a previous session
/fork <id>Branch off a previous session
/sessionShow current session info (ID, messages, cost)

Why JSONL?

JSONL (JSON Lines) is one JSON object per line. It’s ideal for logging because:

  • Append-only: You can add to the file without reading the whole thing
  • Streamable: Process line by line without loading into memory
  • Crash-safe: Even if the program crashes, all lines up to the crash are valid
  • grep-friendly: You can use standard Unix tools to search
# Find all tool calls
grep '"tool_calls"' .session_logs/2025-04-15-14-23.jsonl | wc -l

# Find all errors
grep -i "error" .session_logs/2025-04-15-14-23.jsonl

# Extract just user messages
grep '"role":"user"' .session_logs/2025-04-15-14-23.jsonl

← Chapter 05 · Next: Chapter 07 →

Previous Lesson 6 of 11 Next