📊 agentprof 可视化指南 · 目录 Wiki 8 / 14
Wiki

数据模型

agentprof 的数据生命周期是 3 层:原始 Event 流 → 聚合成 Episodes(一个 turn 内 tool / hook / skill 调用集合)→ 计算 AnalysisReport(整 session rollup 统计)。每一层独立可序列化、可单元测试,分别解决「忠实记录」「聚合视图」「OLAP 报表」三类问题。本课把每个类型的真实字段、入口函数、容错策略一次讲清。

字段类型必填说明
metaSessionMetaid / agent / started_at
turn_summaryVec<TurnSummaryRow>per-turn token + duration
tool_rankVec<ToolRankRow>top tools by total_duration + p50/p95
hook_rankVec<HookRankRow>hook 调用排序
model_metricsBTreeMap<String, ModelUsage>per-model token counts
warningsVec<DeriveWarning>analyzer-time 警告
parse_warningsVec<ParseWarning>parser-time 警告
loaded_mcp_toolsBTreeSet<String>本 session 加载过的 MCP tool
🛢️ 工程类比 — 像数据库的 normalize → denormalize
  • Event = 行级日志 — 单事件 append-only 流,类似数据库的 transaction log,原子可重放。
  • Episodes = 分组聚合视图 — 把同一 tool / hook / skill 的多次调用 group by name,类似 SQL 的 GROUP BY 中间结果。
  • AnalysisReport = OLAP cube — 跨 turn / tool / hook / model 的多维 rollup,给 CLI / TUI / HTML / JSON 多 surface 共用一份事实表。

三层各自独立解决一类问题,下层永远是上层的输入,不反过来依赖。

三层的职责与实际类型

层次责任实际类型(crate 内真实名)
Event(原始事件)单个原始事件:TurnStart / ToolExecStart / ToolExecComplete / HookStart / HookEnd / SkillInvokedtrait Event + enum EventKind#[non_exhaustive], 30 个变体),每 adapter 一个具体 enum(如 CopilotEvent
Episodes(turn 聚合)一个 session 内所有 tool / hook / skill / mode / abort 的分组视图,按 name 聚合调用struct Episodes { turns, tools: BTreeMap<String, ToolEpisode>, hooks, skills, mode_segments, aborts, warnings, model_metrics, loaded_mcp_tools }
AnalysisReport(session rollup)整 session 的多维 rollup 表 — meta / per-turn 行 / tool rank / hook rank / per-model token / cachestruct AnalysisReport { meta, turn_summary, tool_rank, hook_rank, warnings, parse_warnings, model_metrics, loaded_mcp_tools } + fn cache_metrics()

数据流图

四个节点的单向 pipeline — 每个箭头都是纯函数(无副作用、可单元测试、可在 storage 层缓存中间结果):

events.jsonl Event 流 Episodes (一 turn 聚合) AnalysisReport (rollup)

parse_events_jsonl() 把磁盘的 events.jsonl 读成 Vec<CopilotEvent>(实现 Event trait)+ 一组 ParseWarning;② derive_episodes(&events, &meta) 单遍扫描产出 Episodes;③ analyze(&episodes, &meta, &parse_warnings) 计算 AnalysisReport。三段全在 agentprof-core不依赖任何其他 workspace crate

👇 三张卡片分别拆解:① Event 层(trait + EventKind)· ② Episodes 层(derive_episodes + Span 容错)· ③ AnalysisReport 层(完整字段表)。每张都给「为什么 / agentprof 怎么做 / 其他选择」。

1 Event 层 — 单事件流 点击展开
📐 类型定义
  • trait Eventcrates/agentprof-core/src/adapter.rs:174)— 4 个必备方法:id() -> &str / kind() -> EventKind / timestamp() -> DateTime<Utc> / parent_id() -> Option<&str>;可选 payload_name() / payload_success() / payload_error_message()(默认 None)。
  • enum EventKindadapter.rs:108#[non_exhaustive])— 30 个变体覆盖 session lifecycle / message / tool / hook / skill / mode / permission / subagent / abort / unknown,外加 Unknown forward-compat 兜底。
  • 每个 adapter 提供一个具体 enum(如 CopilotEvent)实现 Event trait,承载该 agent 特有的 payload。
🤔 为什么
Copilot CLI 的 events.jsonl 是行流;Claude / Codex 后续接入的 OTel 也是 push 事件流 — 三家底层模型一致。把它抽象成 trait + 类型化 kind,让上层 analyzer 完全不关心是哪家 agent 在喂数据。
✅ agentprof 怎么做
trait + extension methods(payload_name / payload_success / payload_error_message)让所有 adapter 暴露统一接口;EventKind#[non_exhaustive] 允许将来新增变体不破坏 SemVer;Unknown 变体保证未识别事件类型也能保留 timestamp / parent,不丢数据。
🔀 其他选择
直接用 serde_json::Value — 实现最简单,但失去类型安全(写错字段名编译过不报错)+ 失去编译期 exhaustive match 校验,重构时容易漏分支;每 adapter 单独 trait — 同名 tool 还得各自实现 rollup 不能复用 analyzer;trait + EventKind 是「类型化 + 跨 adapter 复用」的最优解。
2 Episode 层 — turn 内聚合 点击展开
📐 类型 & 入口
  • 入口pub fn derive_episodes<E: Event>(events: &[E], meta: &SessionMeta) -> Episodesepisode/derive.rs:101),单遍扫描产出全部聚合结果。
  • struct Episodesepisode/episodes.rs:31)字段:turns: Vec<Turn> / tools: BTreeMap<String, ToolEpisode>(按 tool name 聚合)/ hooks: BTreeMap<String, HookEpisode> / skills: BTreeMap<String, SkillEpisode> / mode_segments / aborts / warnings: Vec<DeriveWarning> / model_metrics / loaded_mcp_tools
  • struct ToolEpisode { name, source: ToolSource, calls: Vec<ToolCall>, total_duration, fail_count };每个 ToolCall { span: Span, turn_id, status: ToolCallStatus, user_requested, arguments }struct Span { started_at, ended_at }episode/turn.rs:138)。
🤔 为什么
rollup 要回答的核心问题是「一个 turn 内 tool Read 被调用了几次、总耗时多少、失败几次」— 原始 events.jsonl 没有这个聚合视图,每次查询都重算太慢。Episode 层把它物化(且按 name BTreeMap 排序好),让上面的 rank / report 直接 iterate。
✅ agentprof 怎么做
lenient 单遍 derive(ADR-0004 决策)— 遇到异常事件不 crash,而是发 DeriveWarningEpisodes::warnings。比如 ToolExecComplete 时间戳早于 ToolExecStart,会产出 DeriveWarning::NonMonotonicTimestampepisode/warning.rs:39)且把 duration 截到 0,让报表照常生成。其他 lenient 变体还有 SynthesizedStart / OpenAtEndOfSession / AbortWithoutOpenElement / PayloadNameMissing
🔀 其他选择
两遍扫描(先建索引再聚合)— 实现略简单但内存 / CPU 翻倍,对大 session 拖慢交互;严格 fail-fast(缺事件就 panic)— 太脆弱:Copilot CLI 偶尔会因为 SIGINT 漏写 turn.end,整 session 全废;单遍 lenient + warning 是「保 99 % 准确 + 永远出报表」的平衡。详见 ADR-0004 episode derivation
3 AnalysisReport 层 — session rollup 点击展开
📐 完整字段表
字段类型含义
metaSessionMetasession id / AgentKind / started_at / 是否 resume;克隆自输入
turn_summaryVec<TurnSummaryRow>每 turn 一行:status / duration / model / mode / tool / hook / skill 计数 / output_tokens
tool_rankVec<ToolRankRow>total_duration 降序:calls (success/failure/orphan/user-requested) / p50 / p95 / max
hook_rankVec<HookRankRow>同上,针对 hook
warningsVec<DeriveWarning>analyzer-time 数据异常(如 NonMonotonicTimestamp
parse_warningsVec<ParseWarning>parser-time 格式错误(如 schema 不匹配的行)
model_metricsOption<BTreeMap<String, ModelUsage>>per-model token:input / output / cache_read / cache_write;None 表 session 无 shutdown 事件
loaded_mcp_toolsBTreeSet<String>session 加载的 MCP tool 集合(不论是否被调用)— 用于算 waste / ROI
cache_metrics()fn(&self) -> Option<CacheMetrics>方法而非字段:根据 model_metrics 推算,None 表没 cache 活动,避免误把零当真实"零命中"
🤔 为什么
CLI 的 --export md/json/html/speedscope、TUI 的 5 视图、HTML 看板、未来的 storage 缓存 — 所有 surface 共用一份 rollup,避免每个表现层各自 reaggregate(重复代码 + 容易漂移)。#[non_exhaustive] 允许后续无破坏性加字段。
✅ agentprof 怎么做
一个入口pub fn analyze(episodes: &Episodes, meta: &SessionMeta, parse_warnings: &[ParseWarning]) -> AnalysisReportanalyzer/mod.rs:415),内部分别调 turn_summary() / tool_rank() / hook_rank()。所有 Vec 都是确定性顺序(snapshot-stable);所有 Duration 序列化为整数 ms(duration_ms helper),便于 JSON diff 测试。
🔀 其他选择
每 surface 自己 reaggregate(不要中间 rollup 类型)— 第一版最快但很快漂移:md 输出和 json 输出对不上同一个数;把 rollup 推到 storage 层 — 把 lib 逻辑塞进 storage 违反 §3 crate 边界,core 不再能独立 analyze。AnalysisReport 作为 agentprof-core 的公开类型 + analyze() 唯一入口是当前的最优拆分。

三层为什么必须独立可序列化

每一层都 derive(Serialize, Deserialize),目的是:

下一步

本课命名了 Event / Episodes / AnalysisReport 三层的所有真实类型。下一课「Tokenizer 与 token 计数」拆解 agentprof-core::tokenizer:tiktoken-rs 怎么挑模型、为何 cache token 单独计数、为何 ROI 用 total_tokens 而不是 input_tokens

📂 相关源码: agentprof-core/adapter.rs  Event

📂 相关源码: agentprof-core/episode/derive.rs  derive_episodes

📂 相关源码: agentprof-core/analyzer/mod.rs  analyze