Skip to content

feat(types): 给 Message 增加可选 per-turn token usage 字段, 让下游能做时间维度的 token 分析 #103

Description

@Jassy930

提议

希望给 aion_types::Message 加一个可选的 usage: Option<TokenUsage> 字段, 让 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 做使用统计仪表盘 (iOfficeAI/AionUi#2946), 第三个数据源是 aionrs。当前 Claude Code CLI 和 Codex CLI 都以 per-event JSONL 提供 token 流水, aionrs 只有 session 级聚合, 导致 aionrs 部分的趋势图精度退化 (跨午夜的长会话全部计入结束日期等)。
  • 其它 host 产品 (Cline / Continue 等) 接 aionrs 时也会遇到同样问题。

数据本来就在 engine 内存里, 缺的只是"挂到 Message 上"这一步。

简要设计

// crates/aion-types/src/message.rs
pub struct Message {
    pub role: Role,
    pub content: Vec<ContentBlock>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timestamp: Option<DateTime<Utc>>,
    /// 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<TokenUsage>,
}

impl Message {
    pub fn with_usage(mut self, usage: TokenUsage) -> Self {
        self.usage = Some(usage);
        self
    }
}

engine.rs 唯一改动 (大约 621 行附近):

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<TurnSummary> 字段, 但需要维护 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 需求我没考虑到的吗?

谢谢!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions