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

Adapter trait + 怎么写新 adapter

agentprof 支持多 agent CLI 的关键抽象是 agentprof_core::adapter::Adapter trait — 每个 agent(Copilot CLI / Claude Code / OpenAI Codex)把它的 session 日志格式实现成一个 adapter。Copilot 已 ship(M1.2),Claude / Codex 在 AgentKind 中保留位置但 adapter 尚未接入(M3.1 / M3.2 路线图)。agentprof-core 完全不知道任何 agent 的具体文件格式 — 它只接受 Event trait 流,分层关注分离。

字段类型必填说明
type Event<assoc>Event + DeserializeOwned + Serialize + Debug
agent_kind()→ AgentKind返回该 adapter 对应的 enum variant
default_session_root()→ Option<PathBuf>约定的默认 session 根目录
discover_sessions()→ Vec<SessionRef>扫描目录列出 sessions
load_session()→ Vec<Self::Event>加载单个 session 的事件流
🔌 工程类比 — 像数据库 ODBC driver / 浏览器 codec
  • Adapter = driver:一套统一接口,每个供应商写自己的实现 — Postgres / MySQL / SQLite 都给 ODBC 提供 driver。
  • core = query 层:上游 analyzer / TUI / CLI 跨 driver 完全通用,换 agent CLI = 换 driver,不动 query 代码。
  • 关键约束:driver 永远向上吐数据,不调用 query 层 — 否则依赖图反向,破坏分层。

同理,Adapter 永远不返回 AnalysisReport(那是 analyzer 的活)— 它只产 RawSession<Self::Event>

3 个 agent 的当前状态

Agent当前状态数据源 / 接入路径
GitHub Copilot CLI✅ ship (M1.2) — CopilotAdapter~/.copilot/session-state/<uuid>/events.jsonl(流式 jsonl)
Anthropic Claude Code🚧 planned (M3.1) — AgentKind::Claude 已占位~/.claude/projects/<hash>/<uuid>.jsonl 或 OTel push(已有 ingest-otlp M2.4)
OpenAI Codex CLI🚧 planned (M3.2) — AgentKind::Codex 已占位~/.codex/sessions/... 或 OTel push

M3.1/M3.2 之前 registry::adapter_for(AgentKind::Claude) 返回 None,CLI 会输出 "agent not supported yet" 提示。AgentKind#[non_exhaustive] enum,新增 variant 不破坏 SemVer。

👇 三张卡片:① Adapter trait 的真实签名(4 方法 + 1 associated type)· ② CopilotAdapter 案例(文件结构 + 流式解析)· ③ 写新 adapter 的 6 步清单(M3.1 ClaudeAdapter 入门)。

1 Adapter trait 接口(真实签名) 点击展开
📐 类型定义(crates/agentprof-core/src/adapter.rs:608
pub trait Adapter: Send + Sync {
    /// Adapter-specific event enum (must implement core::adapter::Event).
    type Event: Event + serde::de::DeserializeOwned + serde::Serialize + std::fmt::Debug;

    fn agent_kind(&self) -> AgentKind;

    fn default_session_root(&self) -> Option<PathBuf>;

    fn discover_sessions(&self, root: &Path) -> Result<Vec<SessionRef>, AdapterError>;

    fn load_session(&self, sref: &SessionRef) -> Result<RawSession<Self::Event>, AdapterError>;
}
  • type Event — 关联类型,每 adapter 定义自己具体的 event enum(如 CopilotEvent),它必须实现 Event trait 并能 serde 双向。
  • agent_kind() — 返回 AgentKind(Copilot / Claude / Codex / future),让上层判别。
  • default_session_root() — 该 agent 默认日志目录(None 表示该平台没默认)。CLI 用它 fall back 当用户没传 --path
  • discover_sessions(root) — 遍历目录,返回 Vec<SessionRef>(轻量索引,不解析 payload)。错误:AdapterError::RootNotFound / AdapterError::Io
  • load_session(sref) — 真正读 + 解析单个 session 为 RawSession<Self::Event>。错误:AdapterError::MissingSessionStart / AdapterError::UnsupportedVersion
🚫 关键:trait 返回 AnalysisReport
Adapter 的责任停在 RawSession<Self::Event>(事件 + meta),后续 derive_episodes()analyze()agentprof-core 自己的活。这条规约由 L1 §3 dependency rule 强制 — core 不依赖 adapters
🤔 为什么
每个 agent 的 session 文件格式天差地别(Copilot 是 events.jsonl,Claude 是 session.jsonl 嵌套 message,Codex 又另一套),但它们抽象到 「lifecycle / message / tool / hook」四类事件后高度同构。trait + 关联类型 Event 让 adapter 自由定义具体 enum,而 analyzer 只 iterate EventKind 通用 view。
✅ agentprof 怎么做
Adapter trait 在 agentprof-core 定义;具体实现在 agentprof-adapters(per-agent 子模块);CLI 通过 registry::adapter_for(kind) dispatch。事件 schema 不同的部分(如 Copilot 的 code.changes)走具体 enum 的字段,统一关切(kind / timestamp / parent)走 Event trait 方法。
🔀 其他选择
让 adapter 直接吐 AnalysisReport — 实现门槛最低,但 core 反而要依赖 adapters(因为 report 是 core 的类型),破坏依赖图,且每个 adapter 都得重写 rollup 逻辑;把所有 agent 的 event 塞同一个 enum — 字段乘积爆炸,无关 agent 的字段污染 schema。trait + 关联类型是「类型安全 + 跨 agent 复用 analyzer」的平衡。
2 CopilotAdapter 案例(M1.2 已 ship) 点击展开
📁 文件结构
crates/agentprof-adapters/src/copilot/
├── mod.rs            // re-exports (CopilotAdapter, CopilotEvent, ...)
├── adapter.rs        // impl Adapter for CopilotAdapter
├── event.rs          // CopilotEvent enum + payload structs
├── parser.rs         // events.jsonl 流式解析
├── paths.rs          // ~/.copilot/session-state 路径约定
├── mcp_config.rs     // 读 MCP 配置(loaded_mcp_tools)
├── tool_sidecar.rs   // tool execution sidecar 解析
└── tools_changed.rs  // tool 集合变化事件

单元结构体 pub struct CopilotAdapter; 定义在 adapter.rs:27,从 copilot/mod.rs 通过 pub use adapter::CopilotAdapter; 暴露。

🔧 关键实现要点
  • agent_kind()AgentKind::Copilot(常量)。
  • default_session_root()~/.copilot/session-state(用 dirs::home_dir() 拼接,Windows / macOS 兼容)。
  • discover_sessions() → 遍历 root/<uuid>/,每个子目录里有 events.jsonl 即视为一个 session;SessionRef 含路径 + 文件 mtime(用于 list 排序)。
  • load_session() → 调 parser::parse_events_jsonl() 流式读 jsonl,每行 serde_json::from_str::<CopilotEvent>();解析失败行不 crash,产 ParseWarning 并继续(lenient — 与 episode derive 一致)。
  • type Event = CopilotEvent,其 EventKind 映射在 event.rs"session.info"EventKind::SessionInfo"tool.exec.start"EventKind::ToolExecStart、未识别 type 落入 EventKind::Unknown 兜底。
📋 registry 注册(crates/agentprof-adapters/src/registry.rs
pub const fn adapter_for(kind: AgentKind) -> Option<CopilotAdapter> {
    match kind {
        AgentKind::Copilot => Some(CopilotAdapter),
        _ => None,  // Claude / Codex 暂未接入
    }
}

pub const fn supported_agents() -> &'static [AgentKind] {
    &[AgentKind::Copilot]
}

⚠️ 当前签名是 Option<CopilotAdapter>(concrete 类型),一旦第二个 adapter(M3.1 Claude)ship,签名必须改 — 候选方案:Option<AnyAdapter> enum 或 trait-object 擦除。registry.rs 顶部 doc 已标记为待办。

3 怎么写新 adapter — 6 步清单(M3.1 ClaudeAdapter 入门) 点击展开
📝 6 步
  1. 实现 trait:新建 crates/agentprof-adapters/src/claude/(建议 mod 拆 adapter.rs / event.rs / parser.rs / paths.rs,照搬 copilot 结构);pub struct ClaudeAdapter; + impl Adapter for ClaudeAdapter 实现 4 方法 + 关联类型 type Event = ClaudeEvent
  2. registry.rs:把 adapter_for 的返回类型从 Option<CopilotAdapter> 改为类型擦除形式(这是 M3.1 真正的设计决策,要走 brainstorming → ADR → plan 三阶段;当前 registry 头部 doc 已标待办);同时 supported_agents() 加入 AgentKind::Claude
  3. fixturecrates/agentprof-adapters/tests/fixtures/claude/ 放至少 1 个匿名化过的 session.jsonl;用户 prompt / file path / API key 等敏感字段必须替换为 <redacted> 占位(参考 xtask anonymize 子命令的未来计划,或手动 sed)。
  4. 集成测试crates/agentprof-adapters/tests/claude.rs 至少 1 个 assert_cmd case:agentprof analyze --agent claude --path tests/fixtures/claude --export json 应返回 exit 0 且 JSON 含至少 1 个 tool_rank 行。
  5. 文档:更新 docs/adapters.md(L2 adapter 指南)+ crates/agentprof-adapters/README.md("支持的 agent"段);新 adapter mod 顶部写 //! 模块文档;公开类型加 # Examples(否则 CI missing_docs 报错)。
  6. CHANGELOGCHANGELOG.md [Unreleased] 段下 ### Added 加 entry:feat(adapters): add ClaudeAdapter for ~/.claude/projects/**/*.jsonl;Conventional Commits 风格。
⚠️ 注意点
  • EventKind#[non_exhaustive]adapter.rs:107)— 如果 Claude 有 Copilot 没有的事件类型(如 Claude 特有的 "thinking" 块),可直接在 EventKind 加 variant,不破坏 SemVer。
  • AgentKind 也是 #[non_exhaustive],但加 variant 时要同步更新 crates/agentprof-storage/src/query.rs:167fn parse_agent(s: &str) -> AgentKind(从 DB 字符串还原 enum),漏改会导致 list / aggregate 跨 session 查询时该 agent 显示为 Unknown
  • lenient 解析:所有解析失败必须收集成 ParseWarningRawSession::parse_warningspanic!,也建议 fail-fast 整 session(参考 ADR-0004 决策)。
  • 错误信息要带 session id + 文件路径 + 修复建议(L1 §7 编码规约 7):"failed to parse claude session abc-123 at /home/me/.claude/.../session.jsonl: ...; try `agentprof config show` to verify path"
  • 禁止 unwrap()(clippy unwrap_used = "deny");expect() 只允许在 main.rs / #[cfg(test)]
🤔 为什么 6 步缺一不可
缺 fixture → CI 没法 regression test;缺集成测试 → 只测得了 parser、测不到 CLI 链路;缺文档 → 用户不知道 --agent claude 怎么用;缺 CHANGELOG → release notes 漂移。每一步都对应 L1 §9.1 「加新东西的菜谱」
🔀 其他选择
把 Claude / Codex 都塞 CopilotAdapter 用 if-else 分流 — 短期最快但 CopilotAdapter 名字立刻撒谎,且 trait 关联类型只能绑一个 event enum;用插件机制 dylib 动态加载 — 跨平台 ABI 噩梦,且 Rust 没稳定的 trait-object plugin ABI。「per-agent struct + registry dispatch」是 Rust 生态对这类「同接口多实现」最 idiomatic 的答案。

下一步

本课讲清了 Adapter trait 接口、CopilotAdapter 案例和写新 adapter 的 6 步规约。下一课「Tokenizer 与 token 计数」拆解 agentprof-core::tokenizer — tiktoken-rs 怎么挑模型、为什么 cache token 单独计数、ROI 公式为何用 total_tokens 而非 input_tokens

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

📂 相关源码: agentprof-adapters/registry.rs  adapter_for

📂 相关源码: agentprof-adapters/copilot/adapter.rs  CopilotAdapter