Sign in

    runledger

    Offline integrity checks and token accounting for agent run logs

    agent
    audit
    jsonl
    tokens
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    15 hours ago
    Downloads
    20

    #RunLedger

    MoonBit 编写的 Agent 日志离线对账库与 CLI。把一次实验中的请求记录、Token 用量和预期任务清单放在一起检查,避免重复日志被重复统计、漏掉整道任务或把缺失用量算成零。

    项目源码公开于 nawei123/runledger,包已发布到 mooncakes.io。以下命令在源码目录运行,不要求 GPU、模型服务或 API Key。

    在其他 MoonBit 项目中安装库:

    moon add nawei123/runledger

    在使用该库的包的 moon.pkg 中添加 "nawei123/runledger" @ledger 导入。仅测试使用的依赖应放入 import { ... } for "test",避免开启 --deny-warn 时出现未使用导入警告。

    #快速运行

    安装 MoonBit CLI,获取源码并进入项目目录:

    git clone https://github.com/nawei123/runledger.git cd runledger moon run cmd/runledger -- audit examples/clean.jsonl examples/manifest.json moon run cmd/runledger -- audit examples/broken.jsonl examples/manifest.json moon run cmd/runledger -- audit-ollama examples/ollama.jsonl examples/manifest.json moon run cmd/runledger -- audit-spans examples/sdk-spans.jsonl examples/manifest.json moon run cmd/runledger -- audit-detail examples/clean.jsonl examples/manifest.json

    第一个命令返回 ok: true,共 4 次调用,输入 406 Token、输出 86 Token。第二个命令返回非零退出码,并报告重复调用、缺失用量、错误模型和缺失阶段。所有示例均为合成数据,不代表真实模型输出或比赛成绩。

    也可以先转换封装的 Ollama 最终响应,再检查:

    moon run cmd/runledger -- normalize-ollama examples/ollama.jsonl > /tmp/runledger-calls.jsonl moon run cmd/runledger -- audit /tmp/runledger-calls.jsonl examples/manifest.json

    原始 Ollama 响应缺少任务与运行标识,需要调用方添加封装字段,格式见 协议说明。仅支持最终 done=true 响应或明确错误对象;逐块流式响应必须先由调用方收集到最终结果。

    #三个使用场景

    1. 比较本地 LLM 优化前后的实验用量:保留每次失败尝试和重试,通过运行与调用 ID 去重,检查是否漏记 provider 用量。项目本身不比较答案准确率。
    2. 检查多阶段批量评测是否完整:从独立任务清单要求每道题同时具有成功的 answer 和 review;日志里完全没有出现的题目也会被报告。
    3. 在 CI 中阻止带有不一致日志的结果入库:使用 JSON 报告和退出码作为门禁;保留原始行号定位问题,无需启动模型。

    #数据规则

    • 重试使用独立 call_id;同一调用键是 (run_id, call_id)。完全相同的重复记录只算一次并报错;相互冲突的副本全部排除。
    • 成功和失败请求中已记录的用量都参与统计;缺少输入或输出用量时保留未知状态,不当成零用量的完整调用。
    • ok 表示本次选定检查均通过;usage_complete 只表示日志中用量账目完整,不表示覆盖了全部预期任务。完整工作流判断应提供 manifest。
    • 输出总量字段使用十进制字符串表示 Int64;规范化调用记录中的计数仍是 JSON 数字。各计数和累计量限制为 0..9007199254740991。
    • 用量表示 provider 已报告的观察值,不估算价格,不自行分词,不把缓存或推理 Token 明细再次加到总量上。

    #库接口

    核心接口为 audit(text)、audit_with_manifest(text, manifest_text)、import_ollama(text) 和 audit_records(parsed, manifest_text?)。新增 import_spans(text)、audit_detail(text, manifest_text?)、detail_records(parsed, manifest_text?) 提供SDK导入和分组报告。调用方可接入自有日志收集器;底层结构与完整接口见 pkg.generated.mbti。

    验收演示与发布检查 提供可复现报告生成命令及独立注册表消费者验证。

    运行全部本地检查:

    bash scripts/verify.sh moon test --target wasm-gc --deny-warn python3 scripts/check_invariants.py

    CLI 退出码:0 检查通过;1 有诊断;2 参数错误或输入文件无法读取。归一化命令如有格式错误,不输出部分 JSONL,而输出错误报告并退出 1。所有命令只读取输入文件、将结果写入 stdout,文件保存由调用方重定向。

    #兼容性与边界

    本地已验证 macOS arm64、moonc v0.10.17+d971670cd 的原生构建、35 项单元测试、9 项 CLI 集成测试;相同 35 项单元测试也在 wasm-gc 通过。GitHub Actions 配置覆盖 Linux 和 macOS,实际运行结果见 CI 工作流。

    当前逐文件读入内存。已对10万条合成调用进行本机端到端基准,结果及局限见研究与改进说明,未验证持续生产吞吐或内存峰值。支持规范化JSONL、指定Ollama封装及有明确边界的SDK span配置,不支持任意OTLP或所有Agent框架。实际SDK导出互操作已验证,模型用量仍为合成;真实脱敏提供者日志验证尚未完成。

    离线日志可被修改,检查结果不能证明调用确实发生、原始账单真实、答案正确或符合某项比赛要求。报告不会复制 prompt 或回答正文;标识字段和 manifest 缺失项仍会出现在报告里,分享前应自行脱敏。

    #生态定位与许可证

    查重与差异说明 列出了现有 JSON、tracing、Agent 监控和回放项目。RunLedger 聚焦已有调用记录的离线一致性与任务覆盖检查,不提供新的 JSON 解析器或 Agent 运行框架。

    Apache-2.0。依赖 moonbitlang/core 和 moonbitlang/x@0.5.6,未复制其他 Agent 项目实现。开发使用了 AI 辅助,设计、实现与测试需要由维护者复核并能够解释。

    #详细报告与外部数据

    audit-detail在原有报告外增加schema_version、行/身份统计、诊断计数和按运行/阶段/模型的分组。各组显示缺少输入及输出计数的调用数。若全局累计量溢出安全范围,group_totals_available为false并清空groups,避免分组和受限总量形成误导性对照。旧audit输出结构保持不变。

    manifest是检查合同,不是过滤器:混入的外部运行/模型记录会使检查失败,其已知用量仍属于输入日志观察值。分组内用量完整不表示任务覆盖完整;使用总量作实验结论前应检查全局report.ok与合同是否适用。

    SDK配置要求显式模型调用类型、任务标识和OK/ERROR状态;UNSET不会被当作成功,父级Agent汇总span不能混作模型调用,语义属性冲突会被报告。见配置说明。

    同类方案、借鉴关系、改进内容及基准方法见市场研究。本项目主张MoonBit生态内的工程集成与性能改进,不主张首创Token统计或trace合同。

    DecodeError

    pub(all) suberror DecodeError {
    DecodeError(String)
    }

    Invalid schema values are separate from malformed JSON syntax.

    AuditStats

    pub(all) struct AuditStats {
    parsed_calls : Int
    accepted_calls : Int
    duplicate_rows : Int
    conflicting_rows : Int
    } derive(ToJson)

    Row counts distinguish decoding, duplicate copies and accepted identities.

    AuditStats::to_json

    fn AuditStats::to_json(AuditStats) -> Json

    Call

    pub(all) struct Call {
    run_id : String
    task_id : String
    stage : String
    call_id : String
    model : String
    status : String
    input_tokens : Int64?
    output_tokens : Int64?
    }

    One provider request attempt. Retries must use distinct call IDs.
    impl ToJson for Call

    Call::to_json

    fn Call::to_json(self : Call) -> Json

    DetailedReport

    pub(all) struct DetailedReport {
    schema_version : Int
    report : Report
    stats : AuditStats
    group_totals_available : Bool
    groups : Array[UsageGroup]
    diagnostic_counts : Map[String, Int]
    } derive(ToJson)

    Additive envelope; the existing Report and audit commands remain unchanged.

    DetailedReport::to_json

    Finding

    pub(all) struct Finding {
    line : Int
    code : String
    message : String
    } derive(ToJson)

    Diagnostics never copy prompts or response text into the report.

    Finding::to_json

    fn Finding::to_json(Finding) -> Json

    Manifest

    pub(all) struct Manifest {
    run_id : String
    model : String
    task_ids : Array[String]
    required_stages : Array[String]
    } derive(ToJson)

    An external expectation catches tasks that are wholly absent from the log.

    Manifest::to_json

    fn Manifest::to_json(Manifest) -> Json

    Parsed

    pub(all) struct Parsed {
    records : Array[Record]
    findings : Array[Finding]
    } derive(ToJson)

    All malformed lines are retained as diagnostics instead of being skipped.

    Parsed::to_json

    fn Parsed::to_json(Parsed) -> Json

    Record

    pub(all) struct Record {
    line : Int
    call : Call
    } derive(ToJson)

    An accepted record and its physical, one-based source line.

    Record::to_json

    fn Record::to_json(Record) -> Json

    Report

    pub(all) struct Report {
    ok : Bool
    usage_complete : Bool
    totals : Totals
    findings : Array[Finding]
    } derive(ToJson)

    Report::to_json

    fn Report::to_json(Report) -> Json

    Totals

    pub(all) struct Totals {
    calls : Int
    input_tokens : Int64
    output_tokens : Int64
    unknown_usage_calls : Int
    failed_calls : Int
    } derive(ToJson)

    Totals describe recorded observations, never estimated provider billing.

    Totals::to_json

    fn Totals::to_json(Totals) -> Json

    UsageGroup

    pub(all) struct UsageGroup {
    run_id : String
    stage : String
    model : String
    totals : Totals
    usage_complete : Bool
    missing_input_calls : Int
    missing_output_calls : Int
    } derive(ToJson)

    Usage attributed to one run, stage and model after reconciliation.

    UsageGroup::to_json

    fn UsageGroup::to_json(UsageGroup) -> Json

    account

    fn account(parsed : Parsed) -> Report

    Check all request attempts, including failed attempts that may be billable. Missing usage stays unknown; it must never silently become a zero count.

    audit

    fn audit(text : String) -> Report

    Basic audit without an expected task manifest.

    audit_detail

    fn audit_detail(text : String, manifest_text : String?) -> DetailedReport

    Detailed normalized-JSONL audit with optional expected task manifest.

    audit_records

    fn audit_records(raw : Parsed, manifest_text : String?) -> Report

    Preserve adapter diagnostics and original line numbers through reconciliation.

    audit_with_manifest

    fn audit_with_manifest(text : String, manifest_text : String) -> Report

    Audit using an explicit run, model and task-stage coverage contract.

    check_coverage

    fn check_coverage(parsed : Parsed, manifest : Manifest) -> Array[Finding]

    Only successful attempts in the declared run/model may satisfy coverage.

    decode_call

    fn decode_call(value : Json) -> Call raise DecodeError

    Decode one normalized request attempt; unknown extra fields are ignored.

    decode_manifest

    fn decode_manifest(value : Json) -> Manifest raise DecodeError

    decode_ollama

    fn decode_ollama(value : Json) -> Call raise DecodeError

    Envelope fields supply the run/task identity missing from provider responses. Each line must contain one final response or one error, never a stream chunk.

    decode_span

    fn decode_span(value : Json) -> Call raise DecodeError

    Bounded flat SDK-span profile, not raw OTLP/protobuf or a backend API export. Require explicit model-call kind, terminal status and caller-supplied task metadata.

    detail_records

    fn detail_records(raw : Parsed, manifest_text : String?) -> DetailedReport

    Group order follows first accepted occurrence; no prompt or output is retained.

    encode_call

    fn encode_call(call : Call) -> Json

    Encode interoperable integer counters as JSON numbers, not Int64 strings. Call implements ToJson using this same canonical encoder.

    import_ollama

    fn import_ollama(text : String) -> Parsed

    Never includes response text in normalized records or diagnostic messages.

    import_spans

    fn import_spans(text : String) -> Parsed

    Every nonblank input line must be one selected model-call span; no silent filtering.

    parse_jsonl

    fn parse_jsonl(text : String) -> Parsed

    Inspect all nonblank lines, retaining physical line numbers for diagnostics.

    reconcile

    fn reconcile(parsed : Parsed) -> Parsed

    Scope call IDs by run. Exact duplicates are counted once and diagnosed. Conflicting copies are excluded entirely rather than trusting the first row.