feat(types): 给 Message 增加可选 per-turn token usage 字段, 让下游能做时间维度的 token 分析
- Dominant language
- Rust
- Stars
- 124
- Forks
- 75
- PR merge metrics
- No merged PRs in 30d
Description
## 提议
希望给 `aion_types::Message` 加一个可选的 `usage: Option` 字段, 让 session.json 持久化时 assistant Message 能带上**本 turn 的 token 用量**, 而不只是 `Session.total_usage` 这个全量终值。
## 动机
aionrs 在 `engine.rs` turn loop 里已经有完整的 `turn_usage` 变量 (engine.rs:515), 在 `LlmEvent::Done` 时被填充, 然后只用于:
1. 累加到 `total_usage` (engine.rs:555-558)
2. 触发 compact 水位检测 (engine.rs:565-576)
之后 turn_usage 就被丢弃, 没有挂到对应的 assistant Message 上。下游想做时间维度的 token 分析 (按日/周趋势、跨 model 切换的成本归因、cache miss 序列分析) 时, 拿到的 session.json 里只有终值, 无法重建 per-turn 流水。
具体使用场景:
- 我在为 [AionUi](https://github.com/iOfficeAI/AionUi) 做使用统计仪表盘 ([iOfficeAI/AionUi#2946](https://github.com/iOfficeAI/AionUi/pull/2946)), 第三个数据源是 aionrs。当前 Claude Code CLI 和 Codex CLI 都以 per-event JSONL 提供 token 流水, aionrs 只有 session 级聚合, 导致 aionrs 部分的趋势图精度退化 (跨午夜的长会话全部计入结束日期等)。
- 其它 host 产品 (Cline / Continue 等) 接 aionrs 时也会遇到同样问题。
数据本来就在 engine 内存里, 缺的只是"挂到 Message 上"这一步。
## 简要设计
```rust
// crates/aion-types/src/message.rs
pub struct Message {
pub role: Role,
pub content: Vec,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option>,
/// Token usage attributed to this turn. None for user/tool-result
/// messages, legacy sessions, and messages constructed outside engine.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option,
}
impl Message {
pub fn with_usage(mut self, usage: TokenUsage) -> Self {
self.usage = Some(usage);
self
}
}
```
engine.rs 唯一改动 (大约 621 行附近):
```rust
self.messages.push(
Message::now(Role::Assistant, assistant_content).with_usage(turn_usage.clone()),
);
```
## 关键属性
- **向后兼容**: `#[serde(default)]` + `skip_serializing_if` → 旧 session.json 反序列化为 `None`, 新 session.json 被旧 binary 加载会 ignore 未知字段
- **不变量**: `Σ messages[i].usage` (assistant turn) `== session.total_usage`
- **零额外计算**: 复用现有 `turn_usage`
- **改动量**: ~80 行 (含序列化兼容性测试 + 不变量测试)
- **非破坏**: 用 builder 模式不动现有 `Message::new` / `Message::now` 调用点
## 备选方案 (供讨论)
如果倾向不动 `Message` struct, 备选是在 `Session` 上加一个平行的 `turns: Vec` 字段, 但需要维护 index 一致性、查询不如直接挂自然。详见提案文档。
## 详细提案
完整设计文档 (含改动清单、测试矩阵、兼容性场景、备选方案):
https://github.com/Jassy930/AionUi/blob/e8d546690/docs/prds/settings/agent_usage/aionrs-pr-proposal.md
如果方向 OK 我来提 PR, 想先听一下:
1. 字段直接挂 `Message` 还是用备选 `Session.turns`?
2. 字段名 `usage` 是否合适 (会不会和未来其它 usage 概念冲突)?
3. 是否需要同时让 `Message.timestamp` 在 engine 生成的 assistant Message 上从 `Option` 变成 always-set?
4. 还有别的 host 需求我没考虑到的吗?
谢谢!
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with crates/aion-types/src/message.rs and the turn loop in engine.rs around lines 515 and 621, then review how turn_usage is filled at LlmEvent::Done and accumulated into total_usage. Check the existing serialization behavior and add the proposed compatibility and usage-total invariant tests. Done means the design choice is resolved, per-turn usage persists on assistant messages, and legacy sessions remain loadable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100