上一篇:Agent Loop:Observe, Decide, Act / Coding Agent 执行循环
下一篇:Context Builder:AI Coding Agent 上下文构建器
English version: Transcript Logging for AI Agent Runs
文章介绍
Day 06 我们把 mini harness 从单轮 chat 升级成了真正的 agent loop:
observe -> decide -> act -> observe -> final
但只“能跑”还不够。一个 coding agent 如果没有执行日志,调试会非常痛苦:
- 模型第几轮决定调用工具?
- 当时传给模型的 messages 是什么?
- 工具入参是什么?
- 工具返回了什么?
- 最终是正常结束、工具失败,还是达到 max steps?
Day 07 要做的是 transcript logging:把一次 agent run 的关键事件写成 JSONL。

今天要解决什么
今天完成四个交付:
- 定义 transcript event 类型。
- 新增 JSONL transcript writer。
- 在 agent loop 里记录 run、model、tool、final/error 事件。
- 保持 CLI 参数不变:继续使用
--transcript <path>。
Demo 命令:
npm run dev -- --transcript logs/runs/day-07.jsonl "inspect the repo and summarize the structure"
运行后会生成一组 JSONL 事件:
run_started
model_request
model_response
tool_call
tool_result
model_request
model_response
tool_call
tool_result
model_request
model_response
run_finished
它在 Cursor/Codex 里对应哪一层
今天实现的是 agent observability layer。
在 Cursor、Codex、Claude Code 这类 coding agent 里,transcript 通常服务于三类场景:
Debug: 为什么 agent 做了这个决定?
Audit: 它读了什么、调用了什么、输出了什么?
Replay: 能不能用同一段上下文复现问题?
没有 transcript,agent loop 就像一个黑盒。用户只看到最终答案,开发者很难判断问题来自:
- prompt 不清楚
- context 太大或太少
- tool schema 设计不好
- tool handler 返回了错误
- 模型输出了非法 tool call
- max steps 太小
Day 07 的目标不是做完整 tracing 平台,而是先把最关键的一层落下来:每个事件一行,可追加、可 grep、可用脚本解析。
设计思路
1. JSONL 比单个 JSON 更适合执行日志
Day 06 之前,CLI 的 --transcript 会把整个 summary 作为一行 JSON 追加到文件里。
这能保存结果,但不适合 agent loop。因为一次 run 内部有多个阶段,事件是流式发生的:
run_started -> model_request -> model_response -> tool_call -> tool_result -> ...
JSONL 的优势是简单:
- 一行一个事件。
- 追加写入,不需要维护一个大 JSON 数组。
- 可以边跑边写。
- 可以用
sed、tail、jq、Node 脚本逐行处理。
2. 事件类型先覆盖最小闭环
Day 07 定义的事件包括:
run_started
model_request
model_response
tool_call
tool_result
run_finished
run_error
这几类事件刚好覆盖 Day 06 的执行循环。
其中 model_request 记录当时的 messages,model_response 记录 assistant message 和解析出的 toolCall,tool_result 记录工具输出或错误。
3. transcript writer 负责文件与脱敏细节
新增文件:apps/mini-harness/src/transcript/writer.ts
writer 的职责很窄:
export type TranscriptWriter = {
path: string;
write(event: TranscriptEvent): Promise<void>;
};
调用方只关心事件,writer 负责:
- 解析相对路径到 workspace。
- 创建父目录。
- 每个事件追加一行 JSON。
- 对可能的 secret key 做简单 redaction。
- 对超长字符串做截断。
4. agent loop 只发事件,不关心写到哪里
createRunSummary 新增一个可选回调:
onTranscriptEvent?: (event: TranscriptEvent) => Promise<void> | void;
agent loop 在关键节点调用:
await emitTranscriptEvent(input, {
type: "model_response",
runId,
createdAt: nowIso(),
step: index,
message: assistantMessage,
durationMs: modelDurationMs,
toolCall,
});
这个设计刻意让 agent runtime 不直接依赖文件系统。今天 CLI 传入 JSONL writer;未来也可以换成数据库、OpenTelemetry span、WebSocket dashboard,或者测试里的内存 recorder。
实现步骤
1. 新增 transcript writer
文件:apps/mini-harness/src/transcript/writer.ts
核心函数:
export function createTranscriptWriter(path: string): TranscriptWriter {
const baseDirectory = process.env.INIT_CWD ?? process.cwd();
const absolutePath = resolve(baseDirectory, path);
return {
path,
async write(event) {
await mkdir(dirname(absolutePath), { recursive: true });
await appendFile(absolutePath, `${JSON.stringify(redactAndTruncate(event))}\n`, "utf8");
},
};
}
这里继续沿用 CLI 的相对路径语义:用户传 logs/runs/day-07.jsonl,文件就写到当前 workspace 下。
2. 在 agent loop 中插入事件
文件:apps/mini-harness/src/agent/run.ts
run 开始时写:
run_started
每一轮模型请求前写:
model_request
模型返回后写:
model_response
如果模型请求工具,再写:
tool_call
tool_result
正常结束写:
run_finished
异常则写:
run_error
3. 更新 CLI 的 --transcript
文件:apps/mini-harness/src/cli.ts
--transcript 的用户接口保持不变:
npm run dev -- --transcript logs/runs/day-07.jsonl "inspect the repo"
内部实现从旧的 writeTranscript(summary) 改为:
const transcriptWriter = options.transcriptPath
? createTranscriptWriter(options.transcriptPath)
: undefined;
然后把 writer 传给 agent loop:
onTranscriptEvent: input.transcriptWriter?.write
4. 保留手动工具 dispatch 的日志能力
如果用户只运行:
npm run dev -- --transcript logs/runs/tool.jsonl --tool list_files --tool-input '{"path":".","limit":5}'
CLI 也会写:
run_started
tool_call
tool_result
run_finished
这让手动工具入口和 agent loop 入口都能被审计。
Demo
See Day 07 demo.
核心命令:
npm run dev -- --transcript logs/runs/day-07.jsonl "inspect the repo and summarize the structure"
查看事件类型:
node -e "const fs=require('fs'); for (const line of fs.readFileSync('logs/runs/day-07.jsonl','utf8').trim().split('\n')) console.log(JSON.parse(line).type)"
输出摘要:
run_started
model_request
model_response
tool_call
tool_result
model_request
model_response
tool_call
tool_result
model_request
model_response
run_finished
当前系统能力变化
到 Day 07,mini harness 已经有了最小可观测性:
- 每次 run 有唯一
runId。 - 每个事件有
createdAt。 - 模型请求和响应可追踪。
- 工具调用和工具结果可追踪。
- final 和 error 都能进入 transcript。
- 日志格式是普通 JSONL,方便用脚本处理。
这一步非常关键。后面加入 context builder、git diff、patch editing、approval sandbox 后,agent 的行为会越来越复杂。如果没有 transcript,很难判断复杂行为是系统设计问题,还是模型单次输出问题。
遇到的问题
1. 日志可能包含敏感信息
transcript 记录 messages 和 tool result,里面可能出现 API key、token、路径、源码片段。
Day 07 先做了轻量处理:
- 对常见 secret key 名称做 redaction。
- 对
sk-...形态的 key 做 redaction。 - 对超长字符串做截断。
这不是完整安全方案。真正产品里还需要按工具、字段、项目规则做更细的 redaction policy。
2. model_request 会变大
每轮 model_request 都包含当时完整 messages。这样最利于 debug,但文件会变大。
短期接受这个 tradeoff。因为这个系列的目标是教学和可观察性优先。后续可以改成:
- 只记录 message hash。
- 只记录新增 message。
- 把大内容拆到 artifact 文件。
- 对 transcript 做压缩。
3. JSONL 是 append-only
当前 writer 每次运行都会追加到同一个文件。这样适合长期日志,但 demo 重复运行时行数会增加。
这符合 CLI transcript 的语义。需要干净 demo 时,可以换一个新路径,例如:
npm run dev -- --transcript logs/runs/day-07-try-01.jsonl "inspect the repo"
明天做什么
Day 08 会实现 Context Builder:把 system prompt、user request、history summary、tool definitions 和选中的项目片段组织成更明确的上下文结构。
有了 transcript 后,Context Builder 的每次输入输出也可以被记录下来,debug 会更直接。
Comments