coding agent を一晩走らせると、ログはちゃんと残る。問題は、そのログを翌朝だれも読まないことだ。正確には、読めない。
prompt、response、tool call、approval 待ち、runtime error、token usage。全部を一つの transcript として保存すると、「何が起きたか」は残る。でも「どこを見ればいいか」が消える。これは地味にきつい。
この記事では model 比較はしない。agent の作業をあとから review できるようにするために、session stream をどういう粒度で切るかだけを書く。
chat 履歴と review 用 event は別物
まず、ID を分ける。
sessionId : ひとまとまりの agent 作業
turnId : user / agent の一往復
toolCallId : file、network、external API など tool 実行の単位
message ID だけで頑張ると、途中でつらくなる。たとえば npm test が timeout した turn と、その前に agent が書き換えた file diff と、その後の retry を同じ message 履歴から探すことになる。
GitHub Copilot の agent session streaming は、prompt / response / tool call などの session activity を streaming endpoint や REST API で見られる方向に寄っている。Vercel eve の Agent Runs も、run の中で turn、model call、tool call、runtime error を追える。
見る単位は「長い chat」ではなく「session の中で起きた event」になってきている。
最初の schema は小さくていい
最初から observability 基盤を作ろうとすると重い。まずは自分の team で normalize する event を決めるくらいでいい。
type AgentReviewEvent = {
sessionId: string
turnId: string
toolCallId?: string
actor: 'user' | 'agent' | 'tool'
eventType:
| 'prompt'
| 'response'
| 'tool_call_started'
| 'tool_call_completed'
| 'tool_call_failed'
| 'approval_required'
toolName?: string
durationMs?: number
errorKind?: string
createdAt: string
}
ここで tool_call_failed をただの response に混ぜない。approval 待ちも error ではない。review で見たいものは、chat UI で自然に見えるものとは少し違う。
token usage も欲しくなる。developer が見るなら step ごとの input / output token は役に立つ。ただ、最初の schema に全部を詰めるより、用途が見えてから足すほうが失敗しにくい。
type AgentReviewCost = {
sessionId: string
turnId: string
toolCallId?: string
inputTokens: number
outputTokens: number
model?: string
}
cost attribution はこれで完璧、とは言わない。少なくとも「どの session がやたら長かったか」を見つける入口にはなる。
developer view と operator view を同じ stream から作る
review UI を作るとき、raw log 画面だけを用意するとだいたい読まれない。
developer が欲しいのは raw tool name、input / output JSON、stack trace、per-step token count。実装の調査には必要だ。
一方で、operator や非エンジニア寄りの reviewer が見たいのは別のものになる。人間向けの tool 名、agent が何をしたかの短い要約、承認待ちか失敗か、見るべき session だけの list。JSON を全部見せても判断は速くならない。
Vercel eve の Agent Runs に Developer mode と Business mode があるのは、この分け方の参考になる。元の event は同じでも、表示の粒度は変えたほうがいい。
雑に作るなら、こんな感じで分ける。
type ReviewVisibility = 'developer' | 'operator'
type ReviewEventView = AgentReviewEvent & {
visibility: ReviewVisibility
displayName: string
summary?: string
tokenUsage?: {
input: number
output: number
}
}
summary は監査証跡の代わりではない。入口だ。raw JSON を保存しない理由にしてしまうと、失敗したときに何も追えなくなる。
retention は後回しにしない
agent log は、残そうと思えばいくらでも残る。だから先に保存期間を決めておく。
たとえば、こう分ける。
agent_review_retention:
recent_full_events: 48h
flagged_sessions: 30d
summaries: 90d
直近 window は full event を残す。問題がありそうな session は flag を立てて長めに残す。operator 向け summary は検索用に残してもいい。ただし、secret や private data を summary に入れない。
GitHub の REST API は直近 48 時間の usage records を pull する形として説明されている。Vercel eve の retention は plan によって 12 hours、1 day、3 days、Observability Plus で 30 days という差がある。
どちらかを正解として真似る必要はない。「全部保存する」の前に recent と flagged を分ける。
transcript viewer ではなく inbox にする
僕なら review UI は chat transcript から始めない。inbox から始める。
Session list
-> flagged turns
-> tool call detail
-> reviewer decision
session list では、duration、token usage、error count、approval count で sort する。長い会話を上から読む画面ではなく、怪しい session を先に見つける画面にする。
flagged turns では、失敗した turn、approval で止まった turn、token が急に増えた turn だけを見る。tool call detail では input / output / duration / error を確認する。
最後に reviewer decision を残す。
type ReviewDecision =
| 'reviewed'
| 'needs_followup'
| 'ignored'
type ReviewDecisionEvent = {
sessionId: string
turnId?: string
toolCallId?: string
decision: ReviewDecision
reviewerId: string
note?: string
createdAt: string
}
これがないと、「あとで確認した」は口頭運用になる。小さい team でも、ここは event として残したほうがいい。
streaming と REST pull は ingestion の違い
near real-time に見たいなら streaming。後追い batch なら REST pull で足りる。frontend の review model では、どちらも AgentReviewEvent に落とす。
WebSocket や previous_response_id は agent UI の turn state を扱ううえで便利だが、review UI の主役ではない。connection の持ち方と、あとから読む event model は分けて考える。
async function ingest(raw: unknown): Promise<AgentReviewEvent[]> {
const providerEvents = normalizeProviderEvent(raw)
return providerEvents.map((event) => ({
sessionId: event.sessionId,
turnId: event.turnId,
toolCallId: event.toolCallId,
actor: event.actor,
eventType: event.eventType,
toolName: event.toolName,
durationMs: event.durationMs,
errorKind: event.errorKind,
createdAt: event.createdAt,
}))
}
provider 固有の shape を画面に直接流すと、あとで別 agent runtime を足すときに UI が壊れる。自分たちの review event に一度落とす。小さくてもここは分ける。
最後に repo に置く checklist
実装を始める前に、これくらいを repo に置いておくと会話が速い。
agent_review:
ids:
- sessionId
- turnId
- toolCallId
event_types:
- prompt
- response
- tool_call_started
- tool_call_completed
- tool_call_failed
- approval_required
views:
- developer
- operator
retention:
recent_full_events: 48h
flagged_sessions: longer
decisions:
- reviewed
- needs_followup
- ignored
coding agent のログは、残すだけなら簡単になってきた。難しいのは、翌朝の人間が読める粒度に切ることだと思う。
chat 履歴を全部保存する前に、session、turn、tool call、decision を分ける。地味だけど、agent を team workflow に入れるならこの順番のほうが後で困らない。