agentprof 的数据生命周期是 3 层:原始 Event 流 → 聚合成 Episodes(一个 turn 内 tool / hook / skill 调用集合)→ 计算 AnalysisReport(整 session rollup 统计)。每一层独立可序列化、可单元测试,分别解决「忠实记录」「聚合视图」「OLAP 报表」三类问题。本课把每个类型的真实字段、入口函数、容错策略一次讲清。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
meta | SessionMeta | ✓ | id / agent / started_at |
turn_summary | Vec<TurnSummaryRow> | ✓ | per-turn token + duration |
tool_rank | Vec<ToolRankRow> | ✓ | top tools by total_duration + p50/p95 |
hook_rank | Vec<HookRankRow> | ✓ | hook 调用排序 |
model_metrics | BTreeMap<String, ModelUsage> | ✓ | per-model token counts |
warnings | Vec<DeriveWarning> | — | analyzer-time 警告 |
parse_warnings | Vec<ParseWarning> | — | parser-time 警告 |
loaded_mcp_tools | BTreeSet<String> | — | 本 session 加载过的 MCP tool |
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 / SkillInvoked … | trait 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 / cache | struct AnalysisReport { meta, turn_summary, tool_rank, hook_rank, warnings, parse_warnings, model_metrics, loaded_mcp_tools } + fn cache_metrics() |
数据流图
四个节点的单向 pipeline — 每个箭头都是纯函数(无副作用、可单元测试、可在 storage 层缓存中间结果):
① 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 Event(crates/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 EventKind(adapter.rs:108,#[non_exhaustive])— 30 个变体覆盖 session lifecycle / message / tool / hook / skill / mode / permission / subagent / abort / unknown,外加Unknownforward-compat 兜底。- 每个 adapter 提供一个具体 enum(如
CopilotEvent)实现Eventtrait,承载该 agent 特有的 payload。
events.jsonl 是行流;Claude / Codex 后续接入的 OTel 也是 push 事件流 — 三家底层模型一致。把它抽象成 trait + 类型化 kind,让上层 analyzer 完全不关心是哪家 agent 在喂数据。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) -> Episodes(episode/derive.rs:101),单遍扫描产出全部聚合结果。 struct Episodes(episode/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)。
Read 被调用了几次、总耗时多少、失败几次」— 原始 events.jsonl 没有这个聚合视图,每次查询都重算太慢。Episode 层把它物化(且按 name BTreeMap 排序好),让上面的 rank / report 直接 iterate。DeriveWarning 入 Episodes::warnings。比如 ToolExecComplete 时间戳早于 ToolExecStart,会产出 DeriveWarning::NonMonotonicTimestamp(episode/warning.rs:39)且把 duration 截到 0,让报表照常生成。其他 lenient 变体还有 SynthesizedStart / OpenAtEndOfSession / AbortWithoutOpenElement / PayloadNameMissing。turn.end,整 session 全废;单遍 lenient + warning 是「保 99 % 准确 + 永远出报表」的平衡。详见 ADR-0004 episode derivation。3 AnalysisReport 层 — session rollup 点击展开
| 字段 | 类型 | 含义 |
|---|---|---|
meta | SessionMeta | session id / AgentKind / started_at / 是否 resume;克隆自输入 |
turn_summary | Vec<TurnSummaryRow> | 每 turn 一行:status / duration / model / mode / tool / hook / skill 计数 / output_tokens |
tool_rank | Vec<ToolRankRow> | 按 total_duration 降序:calls (success/failure/orphan/user-requested) / p50 / p95 / max |
hook_rank | Vec<HookRankRow> | 同上,针对 hook |
warnings | Vec<DeriveWarning> | analyzer-time 数据异常(如 NonMonotonicTimestamp) |
parse_warnings | Vec<ParseWarning> | parser-time 格式错误(如 schema 不匹配的行) |
model_metrics | Option<BTreeMap<String, ModelUsage>> | per-model token:input / output / cache_read / cache_write;None 表 session 无 shutdown 事件 |
loaded_mcp_tools | BTreeSet<String> | session 加载的 MCP tool 集合(不论是否被调用)— 用于算 waste / ROI |
cache_metrics() | fn(&self) -> Option<CacheMetrics> | 方法而非字段:根据 model_metrics 推算,None 表没 cache 活动,避免误把零当真实"零命中" |
--export md/json/html/speedscope、TUI 的 5 视图、HTML 看板、未来的 storage 缓存 — 所有 surface 共用一份 rollup,避免每个表现层各自 reaggregate(重复代码 + 容易漂移)。#[non_exhaustive] 允许后续无破坏性加字段。pub fn analyze(episodes: &Episodes, meta: &SessionMeta, parse_warnings: &[ParseWarning]) -> AnalysisReport(analyzer/mod.rs:415),内部分别调 turn_summary() / tool_rank() / hook_rank()。所有 Vec 都是确定性顺序(snapshot-stable);所有 Duration 序列化为整数 ms(duration_ms helper),便于 JSON diff 测试。core 不再能独立 analyze。AnalysisReport 作为 agentprof-core 的公开类型 + analyze() 唯一入口是当前的最优拆分。三层为什么必须独立可序列化
每一层都 derive(Serialize, Deserialize),目的是:
- storage 缓存 —
agentprof-storage(M2.2)把AnalysisReport直接存 SQLite blob,下次list/aggregate不必重算;#[serde(default)]+skip_serializing_if兜住向后兼容。 - snapshot 测试 —
insta对每层产出 YAML/JSON 快照,重构时一眼看见行为变化。 - 跨进程 IPC — 未来
serve(M2.4 OTLP receiver)可以把 receiver 拿到的Event流直接 IPC 给 analyzer 进程;AnalysisReport也可以推给浏览器看板。
下一步
本课命名了 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