Pi Session Query - 会话查询原语库
把一个 Pi Agent 会话记录文件(JSONL)解析成 Session 对象,暴露五组查询原语,供查询脚本组合完成任意复杂会话分析。
路径约定:本文档所有相对路径均相对本 SKILL.md 所在目录(下称「技能目录」= 本文件 location 的 dirname)。脚本入口为 <技能目录>/scripts/query.py,勿按当前工作目录(CWD)解析。
定位
与 daily-recap 互补:
|
daily-recap |
pi-session-query |
| 视角 |
线性提取摘要 |
完整树形解析 |
| 范围 |
批量、跨文件、跨机器 |
单文件、全 block 还原 |
| 产出 |
摘要(标题/时间/首末消息) |
原语组合的任意查询 |
| 树还原 |
有意不还原(只要摘要) |
正确还原主路径 + 分支 |
调用形态
# <技能目录> = 本 SKILL.md 所在目录(本文件 location 的 dirname);scripts/query.py 相对它解析,勿按 CWD
# session:jsonl 路径 或 session id(自动去 ~/.pi/agent/sessions glob)
# script:路径、-(stdin)、-c/--code CODE(内联)
uv run --script <技能目录>/scripts/query.py <session> <查询脚本路径>
uv run --script <技能目录>/scripts/query.py <session> -
uv run --script <技能目录>/scripts/query.py <session> -c '<查询脚本源码>'
# session id 自省(agent 场景,省去手找文件路径)
uv run --script <技能目录>/scripts/query.py $PI_SUBAGENT_PARENT_SESSION -c 'print(s.title())'
session id 解析:参数非已存在文件、且不含路径分隔符/不以 .jsonl 结尾时视为 id,去 ~/.pi/agent/sessions/(或环境变量 PI_SESSIONS_DIR)递归 glob *_<id>.jsonl。命中多个报 ambiguous_session。
运行器解析会话成 Session 对象 s,注入到查询脚本命名空间并 exec。查询脚本自行 print 输出(通常 print(encode(...)) 用 TOON 省 token)。
注入的命名空间
查询脚本 exec 时可用的全局变量:
| 名字 |
说明 |
s |
已载入的 Session 对象(会话文件已解析) |
Session |
Session 类(可重新构造) |
encode |
TOON 编码(toon-format 不可用或编码异常时降级 JSON + stderr 警告) |
decode |
TOON 解码 |
truncate |
截断长文本,附 size hint |
__name__ |
"__main__"(if __name__ == "__main__" 守卫生效) |
__file__ |
脚本来源标签:路径、<stdin>、<inline>(traceback 定位用) |
查询脚本可用标准库 + 注入的原语 + toon;第三方库 MVP 不支持。
原语 API
Session 对象的五组原语,均返回最小 schema 的 dict/list(遵循 AXI 输出原则)。
会话元数据
| 方法 |
返回 |
s.header() |
{version, id, timestamp, cwd, parent_session} |
s.title() |
会话标题(最新 session_info.name),无则 None |
树结构
| 方法 |
返回 |
s.leaf() |
末端节点:物理最后一条 entry(不用"最后 assistant 消息"启发式——会话以 user/toolResult 结束时会错;与 Pi 源码 buildSessionPath 一致) |
s.entry(id) |
单个 entry 摘要 |
s.parent_chain(id) |
从 id 回溯到根的链(self-first) |
s.children(id) |
id 的直接子节点 |
s.branch_leaves() |
所有叶子节点(分支末端) |
s.tree() |
{total_entries, root_id, leaves, branch_count} |
s.common_ancestor(a, b) |
a、b 的最近公共祖先(LCA) |
路径与消息
| 方法 |
返回 |
s.path(leaf_id=None) |
root-to-leaf 完整链,按 timestamp 序(含 compaction 等所有 entry)。默认 leaf 为物理最后一条 |
s.context_entries(leaf_id=None) |
compaction-aware 活跃 entry 列表(Pi buildContextEntries 语义:省略最后一个 compaction 的 firstKeptEntryId 之前的条目) |
s.messages(leaf_id=None, *, role=None, tool=None, content=None, time=None) |
path 上 message 摘要,可按角色/工具名/正文子串/时间范围过滤(time=(start, end) ISO 8601,按字典序) |
s.blocks(entry_id) |
content[] block:assistant 给 thinking/text/toolCall/image,toolResult 给 text/image;image 只给 mimeType+size,不返回 base64 |
s.tool_pairs(leaf_id=None) |
path 上 toolCall ↔ toolResult 按 call id 配对(精确匹配,含复合 id) |
分析专用
| 方法 |
返回 |
s.diff(a, b) |
两条分支(leaf id)在公共祖先之后的各自走向:{common_ancestor, a_only, b_only}(root-first,从祖先向外) |
s.tool_stats(leaf_id=None) |
{total, by_tool, errors, error_rate} |
s.token_stats(leaf_id=None) |
累计 token 消耗与成本(来自 assistant usage):{input, output, cache_read, cache_write, total, cost, assistant_turns, by_model} |
s.compaction_points(leaf_id=None) |
path 上的压缩点:[{id, timestamp, tokensBefore, firstKeptEntryId, summary}] |
leaf_id 参数缺省时取物理最后一条 entry。path/messages/tool_pairs/token_stats 等都基于"指定 leaf 到根"的路径,可分析任意分支。
查询脚本示例
还原主路径对话流:
print(encode([{"role": m["role"], "text": m["text"]} for m in s.messages()]))
查看主路径上所有助手思考:
for m in s.messages(role="assistant"):
for b in s.blocks(m["id"]):
if b["type"] == "thinking":
print(m["id"], b["text"])
配对工具调用与结果、统计出错率:
print(encode(s.tool_stats()))
print(encode(s.tool_pairs()))
对比两条分支差异:
leaves = [e["id"] for e in s.branch_leaves()]
if len(leaves) >= 2:
print(encode(s.diff(leaves[0], leaves[1])))
定位压缩点并查看摘要:
print(encode(s.compaction_points()))
错误与退出码
| 场景 |
exit |
stdout |
| 用法错误(参数缺失/互斥) |
2 |
{"error":true,"type":"usage",...} |
| 会话/查询脚本文件不存在 |
2 |
{"error":true,"type":"file_not_found",...} |
| session id 无匹配文件 |
2 |
{"error":true,"type":"session_not_found",...} |
| session id 匹配多个文件 |
2 |
{"error":true,"type":"ambiguous_session","matches":[...],...} |
| 缺 session header |
1 |
{"error":true,"type":"missing_header",...} |
| v1/v2 会话(v3 only:v1 线性无 id/parentId) |
1 |
{"error":true,"type":"unsupported_version","version":N,...} |
| 文件无有效条目 |
1 |
{"error":true,"type":"empty",...} |
| 查询脚本抛异常 |
1 |
{"error":true,"type":"query_error","traceback":...} |
| 某行 JSON 无法解析 |
0 |
跳过该行,stderr 警告 |
错误输出固定用 JSON(不依赖 toon),确保可解析。查询脚本返回空结果是 []/{total:0} 等明确空状态,非报错。
脚本位置
<技能目录>/scripts/query.py(相对本 SKILL.md 所在目录) - PEP 723 单文件脚本,依赖 toon-format>=0.9.0b1,<1.0(PyPI beta)。原语库 + 运行器自包含于单文件,查询脚本通过 exec 注入的命名空间访问原语,无需 import 本地模块。
参考
- Pi 会话格式 ->
~/github_code/pi/packages/coding-agent/docs/session-format.md(权威格式参考,已覆盖 block/message/entry 类型、树结构、context building)
- AXI 输出原则 ->
~/github_code/axi/(TOON 输出、最小 schema、截断、聚合、空状态、结构化错误)
- TOON 格式 -> toonformat.dev;Python 库
toon-format(PyPI 0.9.0b1,beta,API 可能变)
1---2name: pi-session-query3description: Pi 会话 JSONL 查询原语库 + 运行器:树形解析会话,供查询脚本组合完成会话分析。4---56# Pi Session Query - 会话查询原语库78把一个 Pi Agent 会话记录文件(JSONL)解析成 `Session` 对象,暴露五组查询原语,供查询脚本组合完成任意复杂会话分析。910> **路径约定**:本文档所有相对路径均相对本 SKILL.md 所在目录(下称「技能目录」= 本文件 location 的 dirname)。脚本入口为 `<技能目录>/scripts/query.py`,勿按当前工作目录(CWD)解析。1112## 定位1314与 [daily-recap](https://github.com/CNife/skills/tree/main/knowledge/daily-recap) 互补:1516| | daily-recap | pi-session-query |17|---|---|---|18| 视角 | 线性提取摘要 | 完整树形解析 |19| 范围 | 批量、跨文件、跨机器 | 单文件、全 block 还原 |20| 产出 | 摘要(标题/时间/首末消息) | 原语组合的任意查询 |21| 树还原 | 有意不还原(只要摘要) | 正确还原主路径 + 分支 |2223## 调用形态2425```bash26# <技能目录> = 本 SKILL.md 所在目录(本文件 location 的 dirname);scripts/query.py 相对它解析,勿按 CWD27# session:jsonl 路径 或 session id(自动去 ~/.pi/agent/sessions glob)28# script:路径、-(stdin)、-c/--code CODE(内联)29uv run --script <技能目录>/scripts/query.py <session> <查询脚本路径>30uv run --script <技能目录>/scripts/query.py <session> -31uv run --script <技能目录>/scripts/query.py <session> -c '<查询脚本源码>'3233# session id 自省(agent 场景,省去手找文件路径)34uv run --script <技能目录>/scripts/query.py $PI_SUBAGENT_PARENT_SESSION -c 'print(s.title())'35```3637session id 解析:参数非已存在文件、且不含路径分隔符/不以 `.jsonl` 结尾时视为 id,去 `~/.pi/agent/sessions/`(或环境变量 `PI_SESSIONS_DIR`)递归 glob `*_<id>.jsonl`。命中多个报 `ambiguous_session`。3839运行器解析会话成 `Session` 对象 `s`,注入到查询脚本命名空间并 exec。查询脚本自行 `print` 输出(通常 `print(encode(...))` 用 TOON 省 token)。4041## 注入的命名空间4243查询脚本 exec 时可用的全局变量:4445| 名字 | 说明 |46|------|------|47| `s` | 已载入的 `Session` 对象(会话文件已解析) |48| `Session` | `Session` 类(可重新构造) |49| `encode` | TOON 编码(toon-format 不可用或编码异常时降级 JSON + stderr 警告) |50| `decode` | TOON 解码 |51| `truncate` | 截断长文本,附 size hint |52| `__name__` | `"__main__"`(`if __name__ == "__main__"` 守卫生效) |53| `__file__` | 脚本来源标签:路径、`<stdin>`、`<inline>`(traceback 定位用) |5455查询脚本可用标准库 + 注入的原语 + toon;第三方库 MVP 不支持。5657## 原语 API5859`Session` 对象的五组原语,均返回最小 schema 的 dict/list(遵循 AXI 输出原则)。6061### 会话元数据6263| 方法 | 返回 |64|------|------|65| `s.header()` | `{version, id, timestamp, cwd, parent_session}` |66| `s.title()` | 会话标题(最新 `session_info.name`),无则 `None` |6768### 树结构6970| 方法 | 返回 |71|------|------|72| `s.leaf()` | 末端节点:物理最后一条 entry(不用"最后 assistant 消息"启发式——会话以 user/toolResult 结束时会错;与 Pi 源码 `buildSessionPath` 一致) |73| `s.entry(id)` | 单个 entry 摘要 |74| `s.parent_chain(id)` | 从 id 回溯到根的链(self-first) |75| `s.children(id)` | id 的直接子节点 |76| `s.branch_leaves()` | 所有叶子节点(分支末端) |77| `s.tree()` | `{total_entries, root_id, leaves, branch_count}` |78| `s.common_ancestor(a, b)` | a、b 的最近公共祖先(LCA) |7980### 路径与消息8182| 方法 | 返回 |83|------|------|84| `s.path(leaf_id=None)` | root-to-leaf 完整链,按 timestamp 序(含 compaction 等所有 entry)。默认 leaf 为物理最后一条 |85| `s.context_entries(leaf_id=None)` | compaction-aware 活跃 entry 列表(Pi `buildContextEntries` 语义:省略最后一个 compaction 的 `firstKeptEntryId` 之前的条目) |86| `s.messages(leaf_id=None, *, role=None, tool=None, content=None, time=None)` | path 上 message 摘要,可按角色/工具名/正文子串/时间范围过滤(`time=(start, end)` ISO 8601,按字典序) |87| `s.blocks(entry_id)` | content[] block:assistant 给 thinking/text/toolCall/image,toolResult 给 text/image;image 只给 mimeType+size,不返回 base64 |88| `s.tool_pairs(leaf_id=None)` | path 上 toolCall ↔ toolResult 按 call id 配对(精确匹配,含复合 id) |8990### 分析专用9192| 方法 | 返回 |93|------|------|94| `s.diff(a, b)` | 两条分支(leaf id)在公共祖先之后的各自走向:`{common_ancestor, a_only, b_only}`(root-first,从祖先向外) |95| `s.tool_stats(leaf_id=None)` | `{total, by_tool, errors, error_rate}` |96| `s.token_stats(leaf_id=None)` | 累计 token 消耗与成本(来自 assistant `usage`):`{input, output, cache_read, cache_write, total, cost, assistant_turns, by_model}` |97| `s.compaction_points(leaf_id=None)` | path 上的压缩点:`[{id, timestamp, tokensBefore, firstKeptEntryId, summary}]` |9899`leaf_id` 参数缺省时取物理最后一条 entry。path/messages/tool_pairs/token_stats 等都基于"指定 leaf 到根"的路径,可分析任意分支。100101## 查询脚本示例102103还原主路径对话流:104105```python106print(encode([{"role": m["role"], "text": m["text"]} for m in s.messages()]))107```108109查看主路径上所有助手思考:110111```python112for m in s.messages(role="assistant"):113 for b in s.blocks(m["id"]):114 if b["type"] == "thinking":115 print(m["id"], b["text"])116```117118配对工具调用与结果、统计出错率:119120```python121print(encode(s.tool_stats()))122print(encode(s.tool_pairs()))123```124125对比两条分支差异:126127```python128leaves = [e["id"] for e in s.branch_leaves()]129if len(leaves) >= 2:130 print(encode(s.diff(leaves[0], leaves[1])))131```132133定位压缩点并查看摘要:134135```python136print(encode(s.compaction_points()))137```138139## 错误与退出码140141| 场景 | exit | stdout |142|------|:----:|--------|143| 用法错误(参数缺失/互斥) | 2 | `{"error":true,"type":"usage",...}` |144| 会话/查询脚本文件不存在 | 2 | `{"error":true,"type":"file_not_found",...}` |145| session id 无匹配文件 | 2 | `{"error":true,"type":"session_not_found",...}` |146| session id 匹配多个文件 | 2 | `{"error":true,"type":"ambiguous_session","matches":[...],...}` |147| 缺 session header | 1 | `{"error":true,"type":"missing_header",...}` |148| v1/v2 会话(v3 only:v1 线性无 id/parentId) | 1 | `{"error":true,"type":"unsupported_version","version":N,...}` |149| 文件无有效条目 | 1 | `{"error":true,"type":"empty",...}` |150| 查询脚本抛异常 | 1 | `{"error":true,"type":"query_error","traceback":...}` |151| 某行 JSON 无法解析 | 0 | 跳过该行,stderr 警告 |152153错误输出固定用 JSON(不依赖 toon),确保可解析。查询脚本返回空结果是 `[]`/`{total:0}` 等明确空状态,非报错。154155## 脚本位置156157`<技能目录>/scripts/query.py`(相对本 SKILL.md 所在目录) - PEP 723 单文件脚本,依赖 `toon-format>=0.9.0b1,<1.0`(PyPI beta)。原语库 + 运行器自包含于单文件,查询脚本通过 exec 注入的命名空间访问原语,无需 import 本地模块。158159## 参考160161- Pi 会话格式 -> `~/github_code/pi/packages/coding-agent/docs/session-format.md`(权威格式参考,已覆盖 block/message/entry 类型、树结构、context building)162- AXI 输出原则 -> `~/github_code/axi/`(TOON 输出、最小 schema、截断、聚合、空状态、结构化错误)163- TOON 格式 -> toonformat.dev;Python 库 `toon-format`(PyPI 0.9.0b1,beta,API 可能变)