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 的事件流 |
- 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),它必须实现Eventtrait 并能 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。
AnalysisReportAdapter 的责任停在 RawSession<Self::Event>(事件 + meta),后续 derive_episodes() 和 analyze() 是 agentprof-core 自己的活。这条规约由 L1 §3 dependency rule 强制 — core 不依赖 adapters。events.jsonl,Claude 是 session.jsonl 嵌套 message,Codex 又另一套),但它们抽象到 「lifecycle / message / tool / hook」四类事件后高度同构。trait + 关联类型 Event 让 adapter 自由定义具体 enum,而 analyzer 只 iterate EventKind 通用 view。agentprof-core 定义;具体实现在 agentprof-adapters(per-agent 子模块);CLI 通过 registry::adapter_for(kind) dispatch。事件 schema 不同的部分(如 Copilot 的 code.changes)走具体 enum 的字段,统一关切(kind / timestamp / parent)走 Event trait 方法。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兜底。
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 入门) 点击展开
- 实现 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。 - 修
registry.rs:把adapter_for的返回类型从Option<CopilotAdapter>改为类型擦除形式(这是 M3.1 真正的设计决策,要走 brainstorming → ADR → plan 三阶段;当前 registry 头部 doc 已标待办);同时supported_agents()加入AgentKind::Claude。 - fixture:
crates/agentprof-adapters/tests/fixtures/claude/放至少 1 个匿名化过的session.jsonl;用户 prompt / file path / API key 等敏感字段必须替换为<redacted>占位(参考xtask anonymize子命令的未来计划,或手动 sed)。 - 集成测试:
crates/agentprof-adapters/tests/claude.rs至少 1 个assert_cmdcase:agentprof analyze --agent claude --path tests/fixtures/claude --export json应返回 exit 0 且 JSON 含至少 1 个tool_rank行。 - 文档:更新
docs/adapters.md(L2 adapter 指南)+crates/agentprof-adapters/README.md("支持的 agent"段);新 adapter mod 顶部写//!模块文档;公开类型加# Examples(否则 CImissing_docs报错)。 - CHANGELOG:
CHANGELOG.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:167的fn parse_agent(s: &str) -> AgentKind(从 DB 字符串还原 enum),漏改会导致list/aggregate跨 session 查询时该 agent 显示为Unknown。- lenient 解析:所有解析失败必须收集成
ParseWarning入RawSession::parse_warnings,不能panic!,也不建议 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()(clippyunwrap_used = "deny");expect()只允许在main.rs/#[cfg(test)]。
--agent claude 怎么用;缺 CHANGELOG → release notes 漂移。每一步都对应 L1 §9.1 「加新东西的菜谱」。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