* session: add restricted Python DSL extraction protocol and make it the default
Introduce a restricted Python memory SDK output protocol as an alternative to
the JSON extraction protocol, and switch the default to python. Both protocols
share the same ResolvedOperations post-processing, schema rules, and patch-repair
path via a new ExtractionOutputProtocol abstraction.
- Add extraction_output_protocol/{base,json,python}.py; python compiles a
restricted AST into the same operations model as json.
- Default memory.extraction_output_format flips json -> python.
- Surface the offending source line on Python syntax errors and add targeted
triple-quote retry guidance for string-literal breaks.
- Preserve every distinct fact on canonical merges; remove hardcoded memory
type names from prompts so custom memory_types render dynamically.
- Downgrade benign batch-delete link-inheritance read failures to WARNING.
- Add memory_organization A/B benchmark and message_format pretty-printer.
Tests: extraction protocol, config loader, memory react suites pass.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* session: drop redundant entity split hint and duplicate abstractmethod
- entities.yaml: remove the size-triggered split hint; when to split/compact is
decided at read time by memory_maintenance_notice, so the static schema
description only keeps the identity semantics and fact-preservation rule.
- vlm/base.py: remove a duplicated @abstractmethod on get_completion_async.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* vlm: drop redundant *.vlm.call span decorators for trace parity
volcengine already dropped its @tracer("volcengine.vlm.call") wrapper to avoid
duplicate spans now that the request is logged via tracer.info(llm_input_messages=...).
Remove the symmetric litellm/openai decorators so all three backends behave the same.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* benchmark/locomo: fix commit_session kwarg for CLI AsyncHTTPClient
ov.AsyncHTTPClient resolves to openviking_cli.client._http_compat.AsyncHTTPClient,
whose commit_session takes a flat telemetry= kwarg and has no options= parameter.
Passing options={...} (the SDK-client shape) raised TypeError during import.
Use telemetry=True to match the CLI client, consistent with the other locomo
import scripts.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* session: don't parse error-target sentinels as URIs in extraction telemetry
The by-type extraction telemetry treated result.errors[].uri as a valid viking
URI and fell back to MemoryUpdater.memory_type_from_uri(), but that field is an
error *target* — it can be a sentinel like "unknown" or "events(page_id=100)".
VikingURI() then raised 'URI must start with viking://', turning a single
recorded extraction error into a crash of the whole long_term extraction step.
Count failed errors by the known uri->type map only, defaulting to "unknown".
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* bot: pass context_type via FindOptions after SDK find/search sync
The SDK find/search sync moved context_type from a top-level find() kwarg into
FindOptions. VikingBot still called client.find(context_type='memory') for peer
recall, so every per-turn type-quota recall raised 'unexpected keyword argument
context_type' and silently returned no memories. The answer agent then fell back
to manual multi-round search (iteration ~1.3 -> ~3.9) and accuracy dropped from
~83% to ~72-77%. Pass it via options={'context_type': 'memory'} instead.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* bot: adapt VikingClient.find to SDK FindOptions for context_type/filter
The SDK find/search sync moved context_type and filter out of top-level find()
kwargs into FindOptions. VikingClient.find still forwarded them as top-level
kwargs to the SDK client, so peer memory recall raised 'unexpected keyword
argument context_type' (and after the prior partial fix, 'options') and returned
no memories — the answer agent fell back to manual multi-round search, spiking
iteration ~1.3 -> ~4 and dropping accuracy ~83% -> ~76%.
Do the SDK adaptation once in VikingClient.find (pack context_type/filter into
options={...}); callers keep the stable VikingClient.find(context_type=...)
interface, so memory.py reverts to passing context_type= directly. Verified via
a single-question smoke: type_quota recall returns 13 memories, injection is
non-empty, iteration=1, answer correct.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* session: route event resolution repair through the output protocol
The event resolution-repair instruction was hardcoded to demand a JSON object,
but under the default Python protocol the repaired response is parsed by the
Python SDK compiler. When a first-pass event had out-of-bounds ranges, an
assistant-only span, or an ambiguous peer, the repair round returned JSON, the
compiler rejected it as an invalid program, retries were exhausted, and the
recoverable event memory was never written.
Add ExtractionOutputProtocol.render_resolution_repair(); JSON keeps the existing
JSON-object wording, Python asks for corrected sdk.create_events(...) calls.
_build_resolution_repair_instruction now delegates to the active protocol, like
patch-repair and the final instruction already do.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* session,bot,benchmark: address PR review findings
- extract_loop: document 32768 as the extraction output floor (tuned for Doubao;
lower-max models override via vlm.max_tokens) and extract
_resolve_effective_max_output_tokens; ov.conf.example notes the override.
- python_protocol: alias non-identifier memory_type/field names on the Python DSL
surface only (real names kept in URIs/storage/JSON); map aliases back when
compiling, instead of hard-rejecting kebab-case custom schemas.
- run_full_eval.sh: move auto-commit + GIT_COMMIT_ID capture AFTER arg parsing so
--auto-commit is honored and run metadata records the committed HEAD.
- litellm_vlm: strip Gemini cache_control from the already-sanitized messages so
empty assistant turns are not reintroduced; sanitize_openai_messages passes
through non-dict entries.
- Tests for each fix.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* session: reject Python DSL alias collisions instead of silently overwriting
_identifier_alias() is not one-to-one: memory_type 'project-notes' and
'project_notes' (or fields 'note-body'/'note_body') fold to the same DSL alias.
The alias->real dict comprehensions would silently drop one, making a schema/
field unreachable and routing writes to the wrong target. Add
_validate_alias_uniqueness(), invoked in render_contract() and the compiler
__init__ (so parse() paths without render are also guarded), which fails loudly
with a rename hint. Distinct-identifier names never collide, so real configs are
unaffected. Tests cover type, field, and no-render parse collisions.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* session: bound str.replace result size before allocation in Python DSL
The _MAX_EXPRESSION_SIZE guard covered * (repeat) and + (concat) but not the
whitelisted string methods: only join() had a projected-size check, so
('x'*1000).replace('x','y'*10000) could still allocate a >1MB result and bypass
the limit. Add _check_replace_size() that bounds source + occurrences*(len(new)
-len(old)) BEFORE calling str.replace (which builds the whole result in C), so
the oversized string is never allocated. replace is the only whitelisted method
that can materially inflate output (join already guarded; upper/lower/strip/
split/startswith/endswith do not grow). Tests cover an oversized replace being
rejected and a normal replace passing.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* session: close f-string width and str %-format inflation in Python DSL
The replace guard alone was insufficient: f-string format specs (f"{'x':>1000001}")
and str %-formatting ("%1000001s" % "x") also turn a small integer literal into an
arbitrarily large string with no repeat operator, bypassing _MAX_EXPRESSION_SIZE.
Neither has a legitimate use in memory content, so disallow them outright rather
than bounding width inflation: reject any non-empty f-string format spec and reject
str/bytes %-formatting (numeric % still allowed). Combined with the existing
*/+/join/replace pre-allocation checks, all small-input->large-output amplifiers
are now closed. Tests cover f-string width rejection, plain f-string, and str %.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
---------
Co-authored-by: TRAE CLI <traecli@bytedance.com>
LoCoMo 评测脚本使用指南
本目录包含 LoCoMo(Long-Term Conversation Memory)评测脚本,用于评估对话记忆系统的性能。
目录结构
benchmark/locomo/
├── vikingbot/ # VikingBot 评测脚本
│ ├── run_eval.py # 运行 QA 评估
│ ├── judge.py # LLM 裁判打分
│ ├── import_to_ov.py # 导入数据到 OpenViking
│ ├── import_and_eval_one.sh # 单题/批量测试脚本
│ ├── stat_judge_result.py # 统计评分结果
│ ├── run_full_eval.sh # 一键运行完整评测流程
│ ├── data/ # 测试数据目录
│ └── result/ # 评测结果目录
├── openclaw/ # OpenClaw 评测脚本
│ ├── import_to_ov.py # 导入数据到 OpenViking
│ ├── eval.py # OpenClaw 评估脚本 (ingest/qa)
│ ├── judge.py # LLM 裁判打分(适配 OpenClaw)
│ ├── stat_judge_result.py # 统计评分结果和 token 使用
│ ├── run_full_eval.sh # 一键运行完整评测流程
│ ├── data/ # 测试数据目录
│ └── result/ # 评测结果目录
├── mem0/ # mem0 评测脚本(详见 mem0/README.md)
├── supermemory/ # Supermemory 评测脚本(详见 supermemory/README.md)
├── claudecode/ # Claude Code 评测脚本(详见 claudecode/README.md)
└── hermes/ # Hermes Agent 评测脚本(详见 hermes/README.md)
VikingBot 评测流程
前置配置说明
- vikingbot评测须确保 OpenViking 服务端已配置 root_api_key,即开启多租户模式。每个sample数据都会使用sample_id如
conv-26作为user_id,存储在OpenViking中。
{
"server": {
"root_api_key": "your-key"
}
}
- OpenViking数据导入account会优先使用
ovcli.conf中的account值,若未配置默认使用default; - vikingbot必须配置OpenViking 的root级别API KEY,默认使用上述的server.root_api_key,也可单独配置;
- vikingbot查询数据account默认为
default,如更改必须与导入OpenViking的account一致,即ovcli.conf中的account值,可通过ov.conf如下配置:
{
"bot": {
"ov_server": {
"root_api_key": "your-root-key",
"account_id": "默认default,必须和ovcli.conf中的account_id一致"
}
}
}
完整一键评测
使用 run_full_eval.sh 可以一键运行完整评测流程:
cd benchmark/locomo/vikingbot
bash run_full_eval.sh # 完整流程
bash run_full_eval.sh --skip-import # 跳过导入,仅评测
单题/批量测试
使用 import_and_eval_one.sh 可以快速测试单个问题或批量测试某个 sample:
cd benchmark/locomo/vikingbot
单题测试:
./import_and_eval_one.sh 0 2 # sample 索引 0, question 2
./import_and_eval_one.sh conv-26 2 # sample_id conv-26, question 2
./import_and_eval_one.sh conv-26 2 --skip-import # 跳过导入
批量测试单个 sample:
./import_and_eval_one.sh conv-26 # conv-26 所有问题
./import_and_eval_one.sh conv-26 --skip-import
分步使用说明
步骤 1: 导入对话数据
使用 import_to_ov.py 将 LoCoMo 数据集导入到 OpenViking:
python import_to_ov.py --input <数据文件路径> [选项]
参数说明:
--input: 输入文件路径(JSON 或 TXT 格式),默认./data/locomo10.json--sample: 指定样本索引(0-based),默认处理所有样本--sessions: 指定会话范围,例如1-4或3,默认所有会话--parallel: 并发导入数,默认 5--force-ingest: 强制重新导入,即使已导入过--clear-ingest-record: 清除所有导入记录--openviking-url: OpenViking 服务地址,默认http://localhost:1933--account: 导入时使用的 account,默认default
示例:
# 导入第一个样本的 1-4 会话
python import_to_ov.py --input ./data/locomo10.json --sample 0 --sessions 1-4
# 强制重新导入所有数据
python import_to_ov.py --input ./data/locomo10.json --force-ingest
步骤 2: 运行 QA 评估
使用 run_eval.py 运行问答评估:
python run_eval.py <输入数据> [选项]
参数说明:
input: 输入 JSON/CSV 文件路径,默认./data/locomo10.json--output: 输出 CSV 文件路径,默认./result/locomo_qa_result.csv--sample: 指定样本索引--count: 运行的 QA 问题数量,默认全部--threads: 并发线程数,默认 5
示例:
# 使用默认参数运行
python run_eval.py
# 指定输入输出文件,使用 20 线程
python run_eval.py ./data/locomo_qa_1528.csv --output ./result/my_result.csv --threads 20
步骤 3: LLM 裁判打分
使用 judge.py 对评估结果进行打分:
python judge.py [选项]
参数说明:
--input: QA 结果 CSV 文件路径,默认./result/locomo_qa_result.csv--token: API Token(也可通过ARK_API_KEY或OPENAI_API_KEY环境变量设置)--base-url: API 基础 URL,默认https://ark.cn-beijing.volces.com/api/v3--model: 裁判模型名称,默认doubao-seed-2-0-pro-260215--parallel: 并发请求数,默认 5
示例:
python judge.py --input ./result/locomo_qa_result.csv --token <your_token> --parallel 10
步骤 4: 统计结果
使用 stat_judge_result.py 统计评分结果:
python stat_judge_result.py --input <评分结果文件>
参数说明:
--input: 评分结果 CSV 文件路径
输出统计信息包括:
- 正确率(Accuracy)
- 平均耗时
- 平均迭代次数
- Token 使用情况
OpenClaw 评测流程
完整一键评测
使用 openclaw/run_full_eval.sh 可以一键运行完整评测流程:
cd benchmark/locomo/openclaw
bash run_full_eval.sh # 只导入 OpenViking(跳过已导入的)
bash run_full_eval.sh --with-claw-import # 同时导入 OpenViking 和 OpenClaw(并行执行)
bash run_full_eval.sh --skip-import # 跳过导入步骤,直接运行 QA 评估
bash run_full_eval.sh --force-ingest # 强制重新导入所有数据
bash run_full_eval.sh --sample 0 # 只处理第 0 个 sample
脚本参数说明:
| 参数 | 说明 |
|---|---|
--skip-import |
跳过导入步骤,直接运行 QA 评估 |
--with-claw-import |
同时导入 OpenViking 和 OpenClaw(并行执行) |
--force-ingest |
强制重新导入所有数据(忽略已导入记录) |
--sample <index> |
只处理指定的 sample(0-based) |
脚本执行流程:
- 导入数据到 OpenViking(可选同时导入 OpenClaw)
- 等待 60 秒确保数据导入完成
- 运行 QA 评估(
eval.py qa,输出到result/qa_results.csv) - 裁判打分(
judge.py,并行度 40) - 统计结果(
stat_judge_result.py,同时统计 QA 和 Import 的 token 使用)
脚本内部配置参数:
在 run_full_eval.sh 脚本顶部可以修改以下配置:
| 变量 | 说明 | 默认值 |
|---|---|---|
INPUT_FILE |
输入数据文件路径 | ../data/locomo10.json |
RESULT_DIR |
结果输出目录 | ./result |
GATEWAY_TOKEN |
OpenClaw Gateway Token | 需要设置为实际 openclaw 网关 token |
分步使用说明
OpenClaw 评测包含以下脚本:
import_to_ov.py: 导入数据到 OpenVikingeval.py: OpenClaw 评估脚本(ingest/qa 两种模式)judge.py: LLM 裁判打分stat_judge_result.py: 统计评分结果和 token 使用
import_to_ov.py - 导入对话数据到 OpenViking
python import_to_ov.py [选项]
参数说明:
--input: 输入文件路径(JSON 或 TXT),默认../data/locomo10.json--sample: 指定样本索引(0-based)--sessions: 指定会话范围,如1-4--question-index: 根据 question 的 evidence 自动推断需要的 session--force-ingest: 强制重新导入--no-user-id: 不传入 user_id 给 OpenViking 客户端--openviking-url: OpenViking 服务地址,默认http://localhost:1933--success-csv: 成功记录 CSV 路径,默认./result/import_success.csv--error-log: 错误日志路径,默认./result/import_errors.log
示例:
# 导入所有数据(跳过已导入的)
python import_to_ov.py
# 强制重新导入,不使用 user id
python import_to_ov.py --force-ingest --no-user-id
# 只导入第 0 个 sample
python import_to_ov.py --sample 0
eval.py - OpenClaw 评估脚本
该脚本有两种模式:
模式 1: ingest - 导入对话数据到 OpenClaw
python eval.py ingest <输入文件> [选项]
参数说明:
--sample: 指定样本索引--sessions: 指定会话范围,如1-4--force-ingest: 强制重新导入--agent-id: Agent ID,默认locomo-eval--token: OpenClaw Gateway Token
示例:
# 导入第一个样本的 1-4 会话到 OpenClaw
python eval.py ingest locomo10.json --sample 0 --sessions 1-4 --token <token>
模式 2: qa - 运行 QA 评估
- 该评测指定了
X-OpenClaw-Session-Key,确保每次 OpenClaw 使用相同的 session_id - Token 计算统计
session.jsonl文件中的所有 assistant 轮次的 Token 消耗 - 每道题目执行完后会归档 session 文件
- 支持并发运行(
--parallel参数) - 问题会自动添加时间上下文(从最后一个 session 提取)
python eval.py qa <输入文件> [选项]
参数说明:
--output: 输出文件路径(不含 .csv 后缀)--sample: 指定样本索引--count: 运行的 QA 问题数量--user: 用户 ID,默认eval-1--parallel: 并发数,默认 10,最大 40--token: OpenClaw Gateway Token(或设置OPENCLAW_GATEWAY_TOKEN环境变量)
示例:
# 运行所有 sample 的 QA 评估
python eval.py qa locomo10.json --token <token> --parallel 15
# 只运行第 0 个 sample
python eval.py qa locomo10.json --sample 0 --output qa_results_sample0
judge.py - LLM 裁判打分
python judge.py [选项]
参数说明:
--input: QA 结果 CSV 文件路径--parallel: 并发请求数,默认 40
示例:
python judge.py --input ./result/qa_results.csv --parallel 40
stat_judge_result.py - 统计结果
同时统计 QA 结果和 OpenViking Import 的 token 使用:
python stat_judge_result.py [选项]
参数说明:
--input: QA 结果 CSV 文件路径,默认./result/qa_results_sample0.csv--import-csv: Import 成功 CSV 文件路径,默认./result/import_success.csv
输出统计包括:
- QA 结果统计:正确率、token 使用(no-cache、cacheRead、output)
- OpenViking Import 统计:embedding_tokens、vlm_tokens、total_tokens
示例:
python stat_judge_result.py --input ./result/qa_results_sample0.csv --import-csv ./result/import_success.csv
测试数据格式
LoCoMo JSON 格式
[
{
"sample_id": "sample_001",
"conversation": {
"speaker_a": "Alice",
"speaker_b": "Bob",
"session_1": [
{
"speaker": "Alice",
"text": "你好,我是 Alice",
"img_url": [],
"blip_caption": ""
}
],
"session_1_date_time": "9:36 am on 2 April, 2023"
},
"qa": [
{
"question": "Alice 叫什么名字?",
"answer": "Alice",
"category": "1",
"evidence": []
}
]
}
]
CSV 格式(QA 数据)
必须包含字段:
sample_id: 样本 IDquestion: 问题answer: 标准答案
输出文件说明
| 文件 | 说明 |
|---|---|
result/locomo_qa_result.csv |
QA 评估原始结果 |
result/judge_result.csv |
包含裁判打分的结果 |
result/summary.txt |
统计摘要 |
result/import_success.csv |
导入成功记录 |
result/import_errors.log |
导入错误日志 |
环境变量
| 变量名 | 说明 |
|---|---|
ARK_API_KEY |
火山引擎 API Key(用于 judge.py) |
OPENAI_API_KEY |
OpenAI API Key(备选) |
OPENCLAW_GATEWAY_TOKEN |
OpenClaw Gateway Token |
常见问题
Q: 如何中断后继续评测?
A: 所有脚本都支持断点续传,重新运行相同命令会自动跳过已处理的项目。
Q: 如何强制重新运行?
A: 使用 --force-ingest(导入)或删除结果 CSV 文件。
Q: 评测速度慢怎么办?
A: 增加 --threads(run_eval.py)或 --parallel(其他脚本)参数值。
Q: 评测效果低,怎么排查 OpenViking 导入与评测查询的 account/user 是否一致?
A: 先核对三处是否对齐:ovcli.conf.account(导入 account)、ov.conf.bot.ov_server.account_id(Vikingbot 查询 account)、评测脚本使用的 user(Vikingbot 按 sample_id,OpenClaw 默认 eval-1)。这几项不一致时,常见现象是“导入看起来成功,但评测回答质量明显下降或查不到上下文”。
常见问题排查
1. 检查 OpenViking 数据导入是否成功
导入完成后,查看 import_success.csv:
cd benchmark/locomo/openclaw
wc -l result/import_success.csv
- 预期结果:总共约 270+ session(包含表头)
- 如果数量不符:
- 检查
result/import_errors.log查看错误日志 - 使用
--force-ingest重新导入
- 检查
2. 检查 QA 回答是否正常
查看 qa_results.csv 的 response 列:
cd benchmark/locomo/openclaw
# 查看前几行
head -n 5 result/qa_results.csv
# 查看是否有 ERROR
grep -i "error" result/qa_results.csv
检查内容:
response列不应为空或报错信息result列(judge 后)应有CORRECT或WRONG
3. 验证 OpenViking 记忆是否被正确加载
如果 QA 回答不正常,可以检查 session 文件确认记忆是否被加载:
-
从
qa_results.csv的jsonl_filename列获取 session 文件名:jsonl_filename 5d497c96-9fb6-480c-be06-0c0849e193e9.jsonl.20260408_181433 -
在 OpenClaw 工作目录查看对应的 session 文件:
ls ~/.openclaw/agents/locomo-eval/sessions/ -
查看 session 文件内容,确认 query 前是否有记忆内容:
cat ~/.openclaw/agents/locomo-eval/sessions/<jsonl_filename> | grep -A 20 "type.*message"
预期结果:在用户提问(query)之前,应该有从 OpenViking 加载的记忆内容。
4. 评测效果低时,先口语化排查 account/user
如果你感觉“明明导入了,回答还是不对劲”,先按这个顺序看:
- 打开
~/.openviking/ovcli.conf,看account是不是你这次要用的账号。 - 打开
~/.openviking/ov.conf,重点看:bot.ov_server.account_idserver.host/server.port
- 跑 Vikingbot 脚本时留意 preflight 日志里打印的
account和OpenViking URL,确认和你配置里看到的一致。 - 记住查询侧是谁在查:
- Vikingbot 评测默认用
sample_id当 user。 - OpenClaw QA 默认是
--user eval-1 --agent-id locomo-eval。 - 你如果改过 OpenClaw 的
--user或--agent-id,要保证 ingest 和 qa 两边用的是同一套值。
- Vikingbot 评测默认用
一句话:导入时的 account/user、评测时的 account/user、以及连接的服务地址,这三件事只要有一个没对齐,就很容易出现效果低或“查不到上下文”。
5. Token 统计异常
如果 stat_judge_result.py 输出的 token 数量异常:
- Import token 为 0:检查
import_success.csv是否存在且有数据 - QA token 为 0:检查
qa_results.csv的input_tokens/output_tokens列 - CacheRead 很高:说明多次运行相同问题,命中了缓存