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
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.
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
sessionIdasparentId - Sets the
agentNameto 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 /:
| Command | What It Does |
|---|---|
/help | Show available commands and agent info |
/models | Display model stats and pricing |
/search | Search session logs for a pattern |
/compact | Summarize conversation history (see Chapter 11) |
/restore <id> | Resume a previous session |
/fork <id> | Branch off a previous session |
/session | Show 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