5 min read
Day 07:Transcript Logging:Agent 执行日志与可观测性

上一篇: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。

Day 07 transcript logging flow

今天要解决什么

今天完成四个交付:

  1. 定义 transcript event 类型。
  2. 新增 JSONL transcript writer。
  3. 在 agent loop 里记录 run、model、tool、final/error 事件。
  4. 保持 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 数组。
  • 可以边跑边写。
  • 可以用 sedtailjq、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 和解析出的 toolCalltool_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

  • Loading comments…

Comments are posted immediately and emailed to the site owner. No account needed.