@zhushanwen/pi-session-reader 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/__tests__/archive-ledger.test.ts +447 -0
- package/src/__tests__/cross-package-subagent-core.test.ts +34 -25
- package/src/__tests__/entry-anchor.test.ts +3 -3
- package/src/__tests__/execution-tree.test.ts +85 -86
- package/src/__tests__/family-zcode.test.ts +24 -14
- package/src/__tests__/family.test.ts +4 -4
- package/src/__tests__/find.test.ts +50 -5
- package/src/__tests__/index.test.ts +3 -3
- package/src/__tests__/rebuild-manifest-dual-write.test.ts +29 -22
- package/src/__tests__/render.test.ts +2 -2
- package/src/__tests__/result.test.ts +20 -11
- package/src/__tests__/roots.test.ts +64 -26
- package/src/__tests__/subagents.test.ts +108 -52
- package/src/__tests__/tool-handler.test.ts +133 -83
- package/src/__tests__/workflow.test.ts +5 -5
- package/src/__tests__/zcode-manifest.test.ts +18 -10
- package/src/__tests__/zcode-routing.test.ts +21 -19
- package/src/__tests__/zcode-whitelist.test.ts +1 -1
- package/src/core/family.ts +6 -6
- package/src/core/render.ts +2 -2
- package/src/core/toolcall.ts +1 -1
- package/src/core/workflow.ts +9 -9
- package/src/discovery/archive-ledger.ts +221 -0
- package/src/discovery/archive-match.ts +84 -0
- package/src/discovery/entry-anchor.ts +5 -5
- package/src/discovery/find.ts +12 -1
- package/src/discovery/roots.ts +149 -14
- package/src/discovery/subagents.ts +24 -17
- package/src/discovery/whitelist.ts +4 -4
- package/src/discovery/workflows.ts +8 -8
- package/src/discovery/zcode-manifest.ts +22 -27
- package/src/doctor.ts +3 -0
- package/src/extract.ts +1 -1
- package/src/index.ts +5 -2
- package/src/no-match.ts +29 -4
- package/src/result-action.ts +7 -1
- package/src/search-across.ts +3 -3
- package/src/tool-format.ts +24 -13
- package/src/tool-handler.ts +68 -29
- package/src/tui/session-command.ts +1 -1
- package/src/zcode-anchor-classify.ts +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
4
|
+
import { join, dirname } from 'node:path'
|
|
5
5
|
|
|
6
6
|
import { readZcodeManifest, listZcodeManifests } from '../discovery/zcode-manifest.js'
|
|
7
7
|
import { findZcodeEntryAnchor } from '../discovery/entry-anchor.js'
|
|
@@ -10,10 +10,10 @@ import { findZcodeEntryAnchor } from '../discovery/entry-anchor.js'
|
|
|
10
10
|
// 变体 M(设计 §4 场景 1a):zcode manifest 直读(§3.1 第①步主路径)+ 枚举(family 用)。
|
|
11
11
|
//
|
|
12
12
|
// fixture 对齐 F21 磁盘真实形态:zcode manifest **无 sessionFile 键**
|
|
13
|
-
//
|
|
13
|
+
// (派生视图函数照写 sessionFile: undefined,JSON.stringify 丢键)、engine:'zcode'、
|
|
14
14
|
// engineHandle.sessionRef 双键齐。
|
|
15
15
|
//
|
|
16
|
-
// 单元边界:只测「认出 zcode manifest +
|
|
16
|
+
// 单元边界:只测「认出 zcode manifest + 提取锚」;白名单门禁 / 错误码映射(U9)、
|
|
17
17
|
// entry 兜底定位语义(U7 已交付)不在本文件展开。
|
|
18
18
|
// ============================================================
|
|
19
19
|
|
|
@@ -21,9 +21,13 @@ const DB_PATH = '/data/agent/engines/zcode/session-db/db.sqlite'
|
|
|
21
21
|
|
|
22
22
|
let agentDir: string
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
/**
|
|
25
|
+
* 在存储树分区 <root>/<sid>/subagents/records/ 下写一个 manifest 文件(手写 JSON 字符串,
|
|
26
|
+
* 忠实控制键)。[session-store-layout-inversion D6/单新根] 新布局无 cwd-slug 层,
|
|
27
|
+
* slug 参数消亡(保留签名兼容既有调用点,弃用改名 _slug);分区名固定合法 sid。
|
|
28
|
+
*/
|
|
29
|
+
function writeManifest(_slug: string, name: string, content: string): string {
|
|
30
|
+
const recordsDir = join(dirname(agentDir), 'ext-sessions', 'root-session-test', 'subagents', 'records')
|
|
27
31
|
mkdirSync(recordsDir, { recursive: true })
|
|
28
32
|
const file = join(recordsDir, name)
|
|
29
33
|
writeFileSync(file, content, 'utf8')
|
|
@@ -73,7 +77,10 @@ function recordLine(saId: string, sessionRef: Record<string, string>): string {
|
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
beforeEach(() => {
|
|
76
|
-
agentDir =
|
|
80
|
+
// agentDir 两层化(<mkdtemp>/agent):存储树推导 = dirname(agentDir)/ext-sessions,
|
|
81
|
+
// 单层直接当 agentDir 会把扫描根泄到 tmpdir 公共层(跨测试污染——先例 reaper/family-zcode 同修)
|
|
82
|
+
agentDir = join(mkdtempSync(join(tmpdir(), 'zcode-manifest-test-')), 'agent')
|
|
83
|
+
mkdirSync(agentDir, { recursive: true })
|
|
77
84
|
})
|
|
78
85
|
afterEach(() => {
|
|
79
86
|
rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
@@ -353,12 +360,13 @@ describe('listZcodeManifests 枚举(family 节点列表来源)', () => {
|
|
|
353
360
|
expect(manifests.find((m) => m.id === 'sa-s2')?.slug).toBe('worker') // agentName 兜底
|
|
354
361
|
})
|
|
355
362
|
|
|
356
|
-
it('
|
|
357
|
-
|
|
363
|
+
it('存储树根不存在 → 空数组(不报错;单节点不可达不炸整屏)', async () => {
|
|
364
|
+
// agentDir 两层化:扫描根 = dirname(agentDir)/ext-sessions(mkdtemp 私有层内,防公共层污染)
|
|
365
|
+
const bareDir = join(mkdtempSync(join(tmpdir(), 'zcode-manifest-bare-')), 'agent')
|
|
358
366
|
try {
|
|
359
367
|
expect(await listZcodeManifests(bareDir)).toEqual([])
|
|
360
368
|
} finally {
|
|
361
|
-
rmSync(bareDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
369
|
+
rmSync(dirname(bareDir), { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
362
370
|
}
|
|
363
371
|
})
|
|
364
372
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import { mkdtempSync, writeFileSync, mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
4
|
+
import { join, dirname } from 'node:path'
|
|
5
5
|
import { DatabaseSync } from 'node:sqlite'
|
|
6
6
|
import { Check } from 'typebox/value'
|
|
7
7
|
|
|
@@ -16,7 +16,7 @@ import sessionReaderExtension from '../index.js'
|
|
|
16
16
|
import { setPiHandle } from '@zhushanwen/pi-extension-logger'
|
|
17
17
|
|
|
18
18
|
// ============================================================
|
|
19
|
-
// U9 路由 + 白名单闸 +
|
|
19
|
+
// U9 路由 + 白名单闸 + 错误范围集成测试(design session-reader-shared-core
|
|
20
20
|
// §3.1 三层定位链 / §3.4 七码 + 检查顺序三段递进 / §4 场景 1a 变体 M/E +
|
|
21
21
|
// 场景 2 family + 场景 5 八类失败 + 场景 3 pi 零回归 + D4 schema 零变化)。
|
|
22
22
|
//
|
|
@@ -53,8 +53,8 @@ beforeEach(() => {
|
|
|
53
53
|
mkdirSync(agentDir, { recursive: true })
|
|
54
54
|
mkdirSync(liveDir, { recursive: true })
|
|
55
55
|
dbPath = join(root, ...ISOLATED_DB_SEGMENTS)
|
|
56
|
-
// 结构化日志捕获:logger.warn 走 appendEntry 通道(不进 LLM
|
|
57
|
-
// 是「降级留痕不进 LLM
|
|
56
|
+
// 结构化日志捕获:logger.warn 走 appendEntry 通道(不进 LLM 可见部分——本文件同时
|
|
57
|
+
// 是「降级留痕不进 LLM 可见部分」契约的断言范围)。测试后清注入防跨文件泄漏。
|
|
58
58
|
appendEntries = vi.fn()
|
|
59
59
|
setPiHandle({ appendEntry: appendEntries } as unknown as Parameters<typeof setPiHandle>[0])
|
|
60
60
|
})
|
|
@@ -135,7 +135,7 @@ function assistantMsg(
|
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
137
|
* 建真实 fixture 隔离库(WAL + session/message/part/schema_migration 四表,行形态 =
|
|
138
|
-
* zcode-session-source helpers 同款宿主 schema 0.16.5
|
|
138
|
+
* zcode-session-source helpers 同款宿主 schema 0.16.5 消费方子集)。
|
|
139
139
|
* 测试进程跑 node(node:sqlite 可用);生产读链的驱动选择在 source 包内。
|
|
140
140
|
*/
|
|
141
141
|
function createZcodeDb(path: string, schemaVersion = '0.16.5', sessions: SessionRows[] = []): void {
|
|
@@ -175,7 +175,9 @@ function writeZcodeManifest(
|
|
|
175
175
|
saId: string,
|
|
176
176
|
opts: { sessionRef?: Record<string, string>; omitEngineHandle?: boolean; engine?: string | true } = {},
|
|
177
177
|
): void {
|
|
178
|
-
|
|
178
|
+
// [session-store-layout-inversion D6/单新根] records 读侧 = 存储树分区固定位置
|
|
179
|
+
// <root>/<rootSid>/subagents/records/(旧根 <agentDir>/subagents 不再读写)。
|
|
180
|
+
const recordsDir = join(dirname(agentDir), 'ext-sessions', ROOT_SESSION, 'subagents', 'records')
|
|
179
181
|
mkdirSync(recordsDir, { recursive: true })
|
|
180
182
|
const m: Record<string, unknown> = {
|
|
181
183
|
id: saId,
|
|
@@ -210,8 +212,8 @@ function recordLine(entryId: string, saId: string, sessionRef: Record<string, st
|
|
|
210
212
|
|
|
211
213
|
/**
|
|
212
214
|
* pi 形态的 subagent-record custom entry 行(回归 A1 instrument):engine 键缺省 +
|
|
213
|
-
* sessionRef 无 dbPath——subagent-core record-entry.ts 的 pi
|
|
214
|
-
* 可选、sessionRef 整体透传不枚举内部键;最小可信子集,跨包漂移由 entry-anchor.test
|
|
215
|
+
* sessionRef 无 dbPath——subagent-core record-entry.ts 的 pi 派生视图形态(engineHandle
|
|
216
|
+
* 可选、sessionRef 整体透传不枚举内部键;最小可信子集,跨包漂移由 entry-anchor.test 检查)。
|
|
215
217
|
*/
|
|
216
218
|
function piRecordLine(entryId: string, saId: string): string {
|
|
217
219
|
return JSON.stringify({
|
|
@@ -220,7 +222,7 @@ function piRecordLine(entryId: string, saId: string): string {
|
|
|
220
222
|
id: entryId,
|
|
221
223
|
parentId: null,
|
|
222
224
|
data: {
|
|
223
|
-
// 登记 §3.3
|
|
225
|
+
// 登记 §3.3:条目范围 = v2 族——pi 形态 = 注册条(身份)+ 终态条(engineHandle 半键)。
|
|
224
226
|
v: 2,
|
|
225
227
|
kind: 'settled',
|
|
226
228
|
id: saId,
|
|
@@ -260,7 +262,7 @@ function writeRootSessionFile(): void {
|
|
|
260
262
|
|
|
261
263
|
/** pi manifest + pi subagent session 文件(pi 穿插断言用;engine 缺省 = pi 存量形态)。 */
|
|
262
264
|
function writePiFixture(): void {
|
|
263
|
-
const subDir = join(agentDir, '
|
|
265
|
+
const subDir = join(dirname(agentDir), 'ext-sessions', ROOT_SESSION, 'subagents', 'sessions')
|
|
264
266
|
mkdirSync(subDir, { recursive: true })
|
|
265
267
|
const piSession = join(subDir, '019e6c96-pi-sub-agent-00000000004a4.jsonl')
|
|
266
268
|
writeFileSync(
|
|
@@ -272,7 +274,7 @@ function writePiFixture(): void {
|
|
|
272
274
|
].join('\n') + '\n',
|
|
273
275
|
'utf8',
|
|
274
276
|
)
|
|
275
|
-
const recordsDir = join(agentDir, '
|
|
277
|
+
const recordsDir = join(dirname(agentDir), 'ext-sessions', ROOT_SESSION, 'subagents', 'records')
|
|
276
278
|
mkdirSync(recordsDir, { recursive: true })
|
|
277
279
|
writeFileSync(
|
|
278
280
|
join(recordsDir, 'sa-pi-1.json'),
|
|
@@ -433,7 +435,7 @@ describe('场景 5 八类失败(§3.4 表逐行 + 检查顺序三段递进)'
|
|
|
433
435
|
await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.toThrow(
|
|
434
436
|
'zcode_anchor_missing',
|
|
435
437
|
)
|
|
436
|
-
// 归因进结构化日志不进 LLM
|
|
438
|
+
// 归因进结构化日志不进 LLM 可见部分(§3.4):缺失键名可区分(missing-dbPath)
|
|
437
439
|
expect(logCallsWithReason('missing-dbPath')).toBeGreaterThanOrEqual(1)
|
|
438
440
|
})
|
|
439
441
|
|
|
@@ -521,14 +523,14 @@ describe('场景 5 八类失败(§3.4 表逐行 + 检查顺序三段递进)'
|
|
|
521
523
|
})
|
|
522
524
|
|
|
523
525
|
// ---------------------------------------------------------------------------
|
|
524
|
-
//
|
|
526
|
+
// 驱动探测失败的错误契约(跨包漂移检查 + L4 包装形态映射)
|
|
525
527
|
// ---------------------------------------------------------------------------
|
|
526
528
|
|
|
527
|
-
describe('
|
|
529
|
+
describe('驱动探测失败错误范围:跨包契约 + L4 包装形态映射', () => {
|
|
528
530
|
it('跨包契约:SQLITE_DRIVER_UNSUPPORTED_MARK 子串在 zcode-session-source sqlite-driver 源文内(漂移即红)', () => {
|
|
529
531
|
// 驱动探测错误无专用错误类型(该包不导出子类),reader 按消息特征识别——mark 与
|
|
530
532
|
// sqlite-driver 源内错误字面量的绑定由本测试机器锚定(entry-anchor.test.ts 协议
|
|
531
|
-
//
|
|
533
|
+
// 字面量跨包漂移检查同范式):源文措辞变更不再含 mark → host_unsupported 契约死亡,红。
|
|
532
534
|
const source = readFileSync(
|
|
533
535
|
new URL('../../../../../packages/zcode-session-source/src/sqlite-driver.ts', import.meta.url),
|
|
534
536
|
'utf8',
|
|
@@ -689,7 +691,7 @@ describe('pi 穿插不受影响:同 fixture 内 pi 会话读取与 zcode 调
|
|
|
689
691
|
/** `session_read` 输入 schema 的 JSON 序列化快照(防无意漂移的逐字节锚)。
|
|
690
692
|
*
|
|
691
693
|
* 快照锚的意图 = 无意漂移拦截(参数增删 / 文案改动即红,迫使有意确认);
|
|
692
|
-
*
|
|
694
|
+
* 有意的声明侧压缩须同批重新生成快照。当前快照 = tool-declaration-slimming
|
|
693
695
|
* U-A1 压缩终态(D1 三层分工:action 功能语义唯一承载点 = 枚举 description;
|
|
694
696
|
* 工具 description 只留定位 + 边界;guidelines 只留纪律)。重新生成:
|
|
695
697
|
* tsx 起 extension 捕获 registerTool 的 parameters 后 JSON.stringify。
|
|
@@ -748,12 +750,12 @@ describe('session_read schema 快照(D4:不加任何工具参数,逐字节
|
|
|
748
750
|
|
|
749
751
|
describe('session_read 锚词保留清单(D2:guidelines 纪律逐词锚定)', () => {
|
|
750
752
|
// SCHEMA_SNAPSHOT 只逐字节锚 parameters schema,不覆盖 guidelines——本块是
|
|
751
|
-
// guidelines 锚词的机器防线:弱模型回归(P7/PB-weak
|
|
753
|
+
// guidelines 锚词的机器防线:弱模型回归(P7/PB-weak)后对本工具声明侧的
|
|
752
754
|
// 再压缩 / 措辞改动,逐词红灯迫使有意确认。清单与 index.ts 注释登记一致:
|
|
753
755
|
// 1. "Progressive reading" —— 阅读纪律 outline → expand → detail
|
|
754
756
|
// 2. "Never read raw .jsonl" —— 禁直读原始 session 文件
|
|
755
|
-
// 3. "sa- ids route" —— zcode 路由对 LLM
|
|
756
|
-
// 4. "👉 recovery hint" ——
|
|
757
|
+
// 3. "sa- ids route" —— zcode 路由对 LLM 的唯一可见部分(U9)
|
|
758
|
+
// 4. "👉 recovery hint" —— 错误范围恢复指针契约
|
|
757
759
|
// 5. "doctor" —— 环境自检入口(另被上方快照 action 枚举间接锚定)
|
|
758
760
|
it('guidelines 5 锚词逐个在位(缺一词即红)', () => {
|
|
759
761
|
const registerTool = vi.fn()
|
|
@@ -77,7 +77,7 @@ describe('assertZcodeDbPathAllowed:检查顺序三段递进(存在性先于
|
|
|
77
77
|
)
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
it('第 2 段路径:存在但集合外 → zcode_db_path_forbidden
|
|
80
|
+
it('第 2 段路径:存在但集合外 → zcode_db_path_forbidden(安全侧错误语义更重)', () => {
|
|
81
81
|
const outside = join(root, 'evil.sqlite')
|
|
82
82
|
writeFileSync(outside, '', 'utf8')
|
|
83
83
|
try {
|
package/src/core/family.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface SubagentRef extends SessionRef {
|
|
|
38
38
|
task?: string
|
|
39
39
|
/** agent 类型名(manifest.agentName,P-fallback 时 identity.data.agent;同语义异名) */
|
|
40
40
|
agentName?: string
|
|
41
|
-
/** 模型 id(仅 manifest 有;P-fallback 时 undefined)。仅 details 可见,不进文本渲染(D2
|
|
41
|
+
/** 模型 id(仅 manifest 有;P-fallback 时 undefined)。仅 details 可见,不进文本渲染(D2②:展示侧只取 status/agentName/task) */
|
|
42
42
|
model?: string
|
|
43
43
|
/** 终态 completed/failed/running(仅 manifest 有;P-fallback 时 undefined) */
|
|
44
44
|
status?: string
|
|
@@ -60,7 +60,7 @@ export interface Family {
|
|
|
60
60
|
forks: SessionRef[]
|
|
61
61
|
/** 含隔代(design §3.3 D-7 Q1:对 fork 链每个节点查 subagentsByRoot 合并) */
|
|
62
62
|
subagents: SubagentRef[]
|
|
63
|
-
/** M1 恒返回 [],workflow
|
|
63
|
+
/** M1 恒返回 [],workflow 路径需读 workflow-state 文件(IO)归 M2 */
|
|
64
64
|
workflows: WorkflowRef[]
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -146,12 +146,12 @@ export function buildZcodeSubagentsByRoot(
|
|
|
146
146
|
return byRoot
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
// ----
|
|
149
|
+
// ---- 类型检查:从 unknown 的 entry.data 提取 subagent identity 字段 ----
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* identity 尾行 data 结构(读取视图,DM-IdentityData)。
|
|
153
153
|
*
|
|
154
|
-
*
|
|
154
|
+
* 检查只强制 rootSessionId+slug(m0 契约),其余富字段全 optional——由 buildFamilyFromFs
|
|
155
155
|
* 按 manifest 主/P-fallback 回退路径组装时决定是否填入(manifest 主填全,P-fallback
|
|
156
156
|
* 只填 task/agent/sessionFile,model/status 缺)。
|
|
157
157
|
*/
|
|
@@ -178,7 +178,7 @@ interface SubagentIdentityData {
|
|
|
178
178
|
function isSubagentIdentityData(v: unknown): v is SubagentIdentityData {
|
|
179
179
|
if (typeof v !== 'object' || v === null) return false
|
|
180
180
|
const obj = v as Record<string, unknown>
|
|
181
|
-
|
|
181
|
+
// 检查放宽:只校验 rootSessionId+slug 必填(m0 契约),task/agent/model/status/sessionFile optional
|
|
182
182
|
return typeof obj.rootSessionId === 'string' && typeof obj.slug === 'string'
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -380,7 +380,7 @@ export function resolveFamily(sessionId: string, index: FamilyIndex): Family {
|
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
// 5. workflows:M1 恒 [],workflow
|
|
383
|
+
// 5. workflows:M1 恒 [],workflow 路径需读 workflow-state 文件(IO)归 M2
|
|
384
384
|
const workflows: WorkflowRef[] = []
|
|
385
385
|
|
|
386
386
|
return { root, parents, forks, subagents, workflows }
|
package/src/core/render.ts
CHANGED
|
@@ -79,7 +79,7 @@ export interface OutlineOptions {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// ---------------------------------------------------------------------------
|
|
82
|
-
// content
|
|
82
|
+
// content 类型检查与提取(message.content 是 unknown,design §3.4)
|
|
83
83
|
// ---------------------------------------------------------------------------
|
|
84
84
|
|
|
85
85
|
interface ContentBlock {
|
|
@@ -454,7 +454,7 @@ export function renderOutline(
|
|
|
454
454
|
const allBranches = options?.allBranches ?? false
|
|
455
455
|
const granularity = options?.granularity ?? 'turn'
|
|
456
456
|
|
|
457
|
-
//
|
|
457
|
+
// 字节/条目统计保持全量口径(折叠是展示层行为,不丢数据);行与轮次计数走对话集
|
|
458
458
|
const parsedBytes = leafParsedBytes(turns)
|
|
459
459
|
const leafEntryCount = turns.reduce((s, t) => s + t.entries.length, 0)
|
|
460
460
|
// totalEntries = leaf + 旁支子树 + 孤儿;totalBytes 无原始文件字节数(签名不含 ParseResult),
|
package/src/core/toolcall.ts
CHANGED
|
@@ -69,7 +69,7 @@ function coerceArgs(raw: unknown): Record<string, unknown> {
|
|
|
69
69
|
/**
|
|
70
70
|
* 从 entry.message.content 的 `{type:"toolCall"}` block 提取工具调用列表。
|
|
71
71
|
*
|
|
72
|
-
* content 是 unknown
|
|
72
|
+
* content 是 unknown 做类型检查;非数组或无 toolCall block 返 []。
|
|
73
73
|
* 仅纳入 id + name 均为 string 的 block(O2 靠 id 关联 toolResult,O1 靠 name 聚合;
|
|
74
74
|
* 缺 id 的块无法关联,缺 name 无法摘要,跳过——实测 0 缺失)。
|
|
75
75
|
*/
|
package/src/core/workflow.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { isRecord } from '@zhushanwen/pi-ext-guards'
|
|
17
17
|
|
|
18
|
-
// ----
|
|
18
|
+
// ---- 类型检查 helpers ----
|
|
19
19
|
|
|
20
20
|
/** unknown → string 收窄(非 string → undefined)。快照可选字符串字段的统一入口。 */
|
|
21
21
|
function strOr(v: unknown): string | undefined {
|
|
@@ -74,9 +74,9 @@ export interface WorkflowOverview {
|
|
|
74
74
|
runId: string
|
|
75
75
|
/** WorkflowRef.stateFile 透传 */
|
|
76
76
|
stateFile: string
|
|
77
|
-
/** NEW state.status / OLD 顶层 status / record
|
|
77
|
+
/** NEW state.status / OLD 顶层 status / record 流派生视图([D2] 三态:running | interrupted | done) */
|
|
78
78
|
status: string
|
|
79
|
-
/** details 格式标记(随 details.runs 透传给程序化消费方,文本渲染不读)。v2
|
|
79
|
+
/** details 格式标记(随 details.runs 透传给程序化消费方,文本渲染不读)。v2 读侧形状与 v1 一致(pi-subagent-workflow 8.x 一次性生命周期) */
|
|
80
80
|
version: 'wf-run-v1' | 'wf-run-v2' | 'legacy'
|
|
81
81
|
/** NEW spec.scriptName / spec.name / OLD name */
|
|
82
82
|
script?: string
|
|
@@ -222,12 +222,12 @@ function parseLegacyRunSnapshot(
|
|
|
222
222
|
*
|
|
223
223
|
* 分支(C-parserunsnapshot-dualformat,TC-wf-snapshot-version-union):
|
|
224
224
|
* - 非对象 → null(调用方跳过,ES-wf-snapshot-unparseable)
|
|
225
|
-
* - NEW (snapshot.v === 'wf-run-v1' 或 'wf-run-v2'
|
|
225
|
+
* - NEW (snapshot.v === 'wf-run-v1' 或 'wf-run-v2',读侧形状一致):state.* / meta.* / spec.*
|
|
226
226
|
* - OLD (无 v,有 callCache 数组或顶层 status):顶层 status/budget/startedAt + callCache
|
|
227
227
|
* - 既非 NEW 也非 OLD → null(未来版本(如 wf-run-v3)/ 异构内容)
|
|
228
228
|
*
|
|
229
229
|
* runId/stateFile 透传参数(不读 snapshot.runId,保证与 family.workflows 一致,避免 OLD 顶层
|
|
230
|
-
* runId 可信度低的不一致)。零 any(全程 typeof/Array.isArray/isRecord
|
|
230
|
+
* runId 可信度低的不一致)。零 any(全程 typeof/Array.isArray/isRecord 检查收窄)。
|
|
231
231
|
*/
|
|
232
232
|
export function parseRunSnapshot(
|
|
233
233
|
snapshot: unknown,
|
|
@@ -236,7 +236,7 @@ export function parseRunSnapshot(
|
|
|
236
236
|
): WorkflowOverview | null {
|
|
237
237
|
if (!isRecord(snapshot)) return null
|
|
238
238
|
|
|
239
|
-
// NEW 格式(v === 'wf-run-v1' || 'wf-run-v2',v2
|
|
239
|
+
// NEW 格式(v === 'wf-run-v1' || 'wf-run-v2',v2 读侧形状兼容 v1)
|
|
240
240
|
const v = snapshot.v
|
|
241
241
|
if (v === 'wf-run-v1' || v === 'wf-run-v2') {
|
|
242
242
|
return parseNewRunSnapshot(snapshot, v, runId, stateFile)
|
|
@@ -331,7 +331,7 @@ interface RecordStreamLine {
|
|
|
331
331
|
[key: string]: unknown
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
/** 流解析产物:run
|
|
334
|
+
/** 流解析产物:run 装配侧(runId 透传 + stateFile=record 流路径)。 */
|
|
335
335
|
interface RecordStreamFold {
|
|
336
336
|
workflowName?: string
|
|
337
337
|
startedAt?: number
|
|
@@ -371,7 +371,7 @@ function parseRecordStreamLine(line: string): RecordStreamLine | undefined {
|
|
|
371
371
|
* sessionId——家族链跳转入口);
|
|
372
372
|
* - run-interrupted:[D2] 中断转移 → status 'interrupted'(非终局);
|
|
373
373
|
* - run-resumed:[D2] 复活转移 → 清中断标记(status 回 running——lifecycle fold
|
|
374
|
-
*
|
|
374
|
+
* 派生视图语义:run-resumed 是 interrupted → running 的转移证据,跳过会使 resume
|
|
375
375
|
* 续跑期间的概览停在 interrupted 直至终局);
|
|
376
376
|
* - run-settled:终局帧 → status 'done' + outcome(done/failed/cancelled/time_limited);
|
|
377
377
|
* - 其余(phase-started / phase-settled):跳过(状态机转移不进概览文本——phase 分组展示
|
|
@@ -445,7 +445,7 @@ function applyAgentSettledToFold(
|
|
|
445
445
|
})
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
/** [parseRunRecordStream 拆分] status
|
|
448
|
+
/** [parseRunRecordStream 拆分] status 三态派生视图([D2]):终局帧 > 中断转移 > running(空流也 running——活跃兜底)。 */
|
|
449
449
|
function projectRecordStreamStatus(fold: RecordStreamFold): {
|
|
450
450
|
status: string
|
|
451
451
|
reason: string | undefined
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 归档簿记域(session-archive 设计 D7:session-reader 归档根感知)。
|
|
3
|
+
*
|
|
4
|
+
* taiji 归档功能把闲置 session 整体搬入 `<agentDir>/archive/`(D1 镜像布局)并
|
|
5
|
+
* zstd 压缩主文件,每个归档单元一份明文簿记 `<原主文件名>.archive.json`(D3
|
|
6
|
+
* schema)。本模块是 extension 侧的簿记消费子集:解析 + 扫描 + 归档态渲染——
|
|
7
|
+
* **类型自含**(universal 包不依赖 taiji workspace shared,簿记字段子集在本地
|
|
8
|
+
* 接口声明),零内部依赖(不 import 本包其他模块,roots.ts 单向消费本模块,
|
|
9
|
+
* 规避 archive-ledger→find→roots 的值级循环 import)。
|
|
10
|
+
*
|
|
11
|
+
* 收集契约(D7 硬约束):归档根**只收 `.archive.json` 簿记、不收 `.jsonl`**——
|
|
12
|
+
* staging 期归档目录内暂存明文主文件,一旦误收会被当「可完整读取的在场 session」
|
|
13
|
+
* 进候选集,归档标注随之失效;`.restoring` 后缀(恢复中,D9)不以
|
|
14
|
+
* `.archive.json` 结尾,天然不进收集。
|
|
15
|
+
*/
|
|
16
|
+
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
17
|
+
import type { Dirent } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
/** 归档根目录名(`<agentDir>/archive/`,与 sessions/ 平级——D1)。 */
|
|
21
|
+
export const ARCHIVE_DIRNAME = 'archive'
|
|
22
|
+
|
|
23
|
+
/** 归档根目录(`<agentDir>/archive/`,agentDir 注入推导)。 */
|
|
24
|
+
export function resolveArchiveDir(agentDir: string): string {
|
|
25
|
+
return join(agentDir, ARCHIVE_DIRNAME)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 存储树分区目录名合法性正则(镜像 shared `PI_SESSION_ID_PATTERN` 与 roots.ts
|
|
30
|
+
* 同字面副本:首尾字母数字,中间 [A-Za-z0-9._-],禁 `:`)。簿记 sessionId 是
|
|
31
|
+
* 路径消费与展示的锚,形态非法(含穿越片段)的簿记整条拒收。
|
|
32
|
+
*/
|
|
33
|
+
const PI_SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/
|
|
34
|
+
|
|
35
|
+
/** 摘要单 turn(D4 算法产出:user 原文 + agent 最后一段文本;agent null = 该 turn 以工具调用结束)。 */
|
|
36
|
+
export interface ArchiveLedgerTurn { // oe-exempt:20261020:framework:SDK 契约接口——跨进程/跨包协议载体,接口先立、单实现常态
|
|
37
|
+
user: string
|
|
38
|
+
agent: string | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 归档簿记消费子集(D3 schema 的本包视角)。仅声明本域消费字段——rawBytes/
|
|
43
|
+
* codec/sha256 等压缩侧字段不进(extension 侧不消费压缩体,D7:命中归档
|
|
44
|
+
* session 时不读压缩体)。
|
|
45
|
+
*/
|
|
46
|
+
export interface ArchiveLedgerEntry { // oe-exempt:20261020:framework:SDK 契约接口——跨进程/跨包协议载体,接口先立、单实现常态
|
|
47
|
+
/** 簿记文件绝对路径(`archive/<encodeCwd>/<原主文件名>.archive.json`) */
|
|
48
|
+
ledgerPath: string
|
|
49
|
+
sessionId: string
|
|
50
|
+
/** 归档时从明文 JSONL 提取的名称(免解压名称数据源,D3——名称片段匹配的键之一) */
|
|
51
|
+
name?: string
|
|
52
|
+
/** staging(搬移完成、摘要/压缩未完成)| archived(终态,摘要已入簿记) */
|
|
53
|
+
status: 'staging' | 'archived'
|
|
54
|
+
/** 摘要 turns;staging 期缺省 null(归档态返回走占位文案) */
|
|
55
|
+
summary: ArchiveLedgerTurn[] | null
|
|
56
|
+
/** 原工作目录(簿记可选字段,展示备用) */
|
|
57
|
+
cwd?: string
|
|
58
|
+
/** 归档时间(ISO 字符串,消歧展示用) */
|
|
59
|
+
archivedAt?: string
|
|
60
|
+
/** 簿记文件 mtime(ms,多命中排序键——簿记终态原子重写时间即归档完成时间) */
|
|
61
|
+
mtime: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** summary 字段的解析结果:turns 数组 / null / 非法(undefined)。 */
|
|
65
|
+
function parseSummary(raw: unknown): ArchiveLedgerTurn[] | null | undefined {
|
|
66
|
+
if (raw === null) return null
|
|
67
|
+
// raw 非 null 已由上一行保证(summary 合法形态恰为 turns 数组或显式 null),此处只需挡非对象。
|
|
68
|
+
if (typeof raw !== 'object') return undefined
|
|
69
|
+
const turnsRaw = (raw as Record<string, unknown>).turns
|
|
70
|
+
if (!Array.isArray(turnsRaw)) return undefined
|
|
71
|
+
const turns: ArchiveLedgerTurn[] = []
|
|
72
|
+
for (const t of turnsRaw) {
|
|
73
|
+
if (typeof t !== 'object' || t === null) return undefined
|
|
74
|
+
const obj = t as Record<string, unknown>
|
|
75
|
+
if (typeof obj.user !== 'string') return undefined
|
|
76
|
+
if (obj.agent !== null && typeof obj.agent !== 'string') return undefined
|
|
77
|
+
turns.push({ user: obj.user, agent: obj.agent as string | null })
|
|
78
|
+
}
|
|
79
|
+
return turns
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 解析簿记 JSON 文本(D3 消费子集)。非法形态返回 undefined 整条拒收:
|
|
84
|
+
* sessionId 缺失/形态非法、status 词表外、summary 结构非法——坏簿记不中断
|
|
85
|
+
* 扫描(调用方跳过),与 scanJsonlRecursive 的容错哲学一致。
|
|
86
|
+
*/
|
|
87
|
+
export function parseArchiveLedgerText(text: string): Omit<ArchiveLedgerEntry, 'ledgerPath' | 'mtime'> | undefined {
|
|
88
|
+
let raw: unknown
|
|
89
|
+
try {
|
|
90
|
+
raw = JSON.parse(text)
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
if (typeof raw !== 'object' || raw === null) return undefined
|
|
95
|
+
const obj = raw as Record<string, unknown>
|
|
96
|
+
if (typeof obj.sessionId !== 'string' || !PI_SESSION_ID_PATTERN.test(obj.sessionId)) {
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
if (obj.status !== 'staging' && obj.status !== 'archived') return undefined
|
|
100
|
+
const summary = parseSummary(obj.summary)
|
|
101
|
+
if (summary === undefined) return undefined
|
|
102
|
+
const out: Omit<ArchiveLedgerEntry, 'ledgerPath' | 'mtime'> = {
|
|
103
|
+
sessionId: obj.sessionId,
|
|
104
|
+
status: obj.status,
|
|
105
|
+
summary,
|
|
106
|
+
}
|
|
107
|
+
if (typeof obj.name === 'string' && obj.name.length > 0) out.name = obj.name
|
|
108
|
+
if (typeof obj.cwd === 'string' && obj.cwd.length > 0) out.cwd = obj.cwd
|
|
109
|
+
if (typeof obj.archivedAt === 'string' && obj.archivedAt.length > 0) out.archivedAt = obj.archivedAt
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 归档根扫描时整体跳过的子目录名:`assets/` 是附件三目录区(D1 映射表),
|
|
115
|
+
* 体积可达归档单元的大头且无簿记,递归进去只耗 IO。
|
|
116
|
+
*/
|
|
117
|
+
const SKIP_DIRS_ARCHIVE = new Set(['assets'])
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 递归扫描归档根下全部 `.archive.json` 簿记并解析(D7 独立簿记收集器)。
|
|
121
|
+
* 只收 `.archive.json` 后缀——`.jsonl`(staging 明文主文件)与 `.restoring`
|
|
122
|
+
* (恢复中簿记)天然排除。单个簿记读/解析失败跳过不中断(容错)。根不存在/
|
|
123
|
+
* 无权限 → 空数组,不抛错(roots.ts 单根失败契约)。
|
|
124
|
+
*/
|
|
125
|
+
export async function scanArchiveLedgers(archiveRoot: string): Promise<ArchiveLedgerEntry[]> {
|
|
126
|
+
const results: ArchiveLedgerEntry[] = []
|
|
127
|
+
|
|
128
|
+
async function walk(currentDir: string): Promise<void> {
|
|
129
|
+
let entries: Dirent[]
|
|
130
|
+
try {
|
|
131
|
+
entries = await readdir(currentDir, { withFileTypes: true })
|
|
132
|
+
} catch {
|
|
133
|
+
return // 目录不存在/无权限 → 静默返回(容错,同 scanJsonlRecursive)
|
|
134
|
+
}
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
const full = join(currentDir, entry.name)
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
if (SKIP_DIRS_ARCHIVE.has(entry.name)) continue
|
|
139
|
+
await walk(full)
|
|
140
|
+
} else if (entry.isFile() && entry.name.endsWith('.archive.json')) {
|
|
141
|
+
let text: string
|
|
142
|
+
try {
|
|
143
|
+
text = await readFile(full, 'utf8')
|
|
144
|
+
} catch {
|
|
145
|
+
continue // 并发删除/不可读 → 跳过该簿记
|
|
146
|
+
}
|
|
147
|
+
const parsed = parseArchiveLedgerText(text)
|
|
148
|
+
if (parsed === undefined) continue // 坏簿记跳过
|
|
149
|
+
try {
|
|
150
|
+
const s = await stat(full)
|
|
151
|
+
results.push({ ...parsed, ledgerPath: full, mtime: s.mtimeMs })
|
|
152
|
+
} catch {
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await walk(archiveRoot)
|
|
160
|
+
return results
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ============================================================
|
|
164
|
+
// 归档态渲染(D7 返回形态——硬契约:agent 必须能区分摘要与完整历史)
|
|
165
|
+
// ============================================================
|
|
166
|
+
|
|
167
|
+
/** 归档态头部标注(D7 字面契约)。 */
|
|
168
|
+
const ARCHIVED_HEADER = '状态: 已归档 (archived) — 完整内容已压缩离线,以下仅为摘要'
|
|
169
|
+
|
|
170
|
+
/** staging 态头部标注(V10:头部 + 占位句 + 名称/id 元数据)。 */
|
|
171
|
+
const STAGING_HEADER = '状态: 已归档 (staging) — 摘要生成中,请稍后再查'
|
|
172
|
+
|
|
173
|
+
/** 尾注恢复指引(D7 字面契约)。 */
|
|
174
|
+
const RESTORE_NOTE = '注: 如需完整历史,请让用户在归档列表中恢复该会话。'
|
|
175
|
+
|
|
176
|
+
/** 纯图片/附件 user 消息的占位(D4:空串 user 文本,D11 同款占位语义)。 */
|
|
177
|
+
const USER_ATTACHMENT_PLACEHOLDER = '[图片/附件消息]'
|
|
178
|
+
|
|
179
|
+
/** turn 无 agent 文本的占位(D4:agent null = 以工具调用结束)。 */
|
|
180
|
+
const AGENT_NO_TEXT_PLACEHOLDER = '(此 turn 以工具调用结束,无文本回复)'
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 渲染归档态返回正文(纯函数,两态):
|
|
184
|
+
*
|
|
185
|
+
* - archived:头部标注 + 摘要 turns(`── turn N ──` 分隔,user / assistant (摘要)
|
|
186
|
+
* 两行,两类占位规则同 D11)+ 尾注恢复指引——**不含任何完整历史内容**
|
|
187
|
+
* (turns 只来自簿记 summary,agent 可区分摘要与完整历史的硬契约)。
|
|
188
|
+
* - staging:头部(staging 标注)+ 名称/id 元数据 + 尾注;无摘要可渲染。
|
|
189
|
+
*/
|
|
190
|
+
export function formatArchivedText(entry: ArchiveLedgerEntry): string {
|
|
191
|
+
const lines: string[] = []
|
|
192
|
+
if (entry.status === 'staging') {
|
|
193
|
+
lines.push(`[session-reader] session: ${entry.name ?? entry.sessionId}(id: ${entry.sessionId})`)
|
|
194
|
+
lines.push(STAGING_HEADER)
|
|
195
|
+
lines.push(RESTORE_NOTE)
|
|
196
|
+
return lines.join('\n')
|
|
197
|
+
}
|
|
198
|
+
lines.push(`[session-reader] session: ${entry.name ?? entry.sessionId}`)
|
|
199
|
+
lines.push(ARCHIVED_HEADER)
|
|
200
|
+
const turns = entry.summary ?? []
|
|
201
|
+
turns.forEach((t, i) => {
|
|
202
|
+
lines.push(`── turn ${i + 1} ──`)
|
|
203
|
+
lines.push(`user: ${t.user.length > 0 ? t.user : USER_ATTACHMENT_PLACEHOLDER}`)
|
|
204
|
+
lines.push(`assistant (摘要): ${t.agent ?? AGENT_NO_TEXT_PLACEHOLDER}`)
|
|
205
|
+
})
|
|
206
|
+
lines.push(RESTORE_NOTE)
|
|
207
|
+
return lines.join('\n')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** 归档多命中消歧文案(F2 同构:列候选 + 👉 更精确重试,不抛错)。 */
|
|
211
|
+
export function formatArchivedDisambiguation(query: string, entries: ArchiveLedgerEntry[]): string {
|
|
212
|
+
// mtime 倒序(最新归档在前,与 find 排序口径一致)
|
|
213
|
+
const sorted = [...entries].sort((a, b) => b.mtime - a.mtime)
|
|
214
|
+
const lines = [`多个归档 session 匹配 "${query}",按归档时间倒序:`]
|
|
215
|
+
for (const e of sorted) {
|
|
216
|
+
const when = e.archivedAt !== undefined ? `,归档于 ${e.archivedAt}` : ''
|
|
217
|
+
lines.push(` - ${e.name ?? '(未命名)'}(id: ${e.sessionId}${when})`)
|
|
218
|
+
}
|
|
219
|
+
lines.push('👉 用完整 session id 或更长的名称片段重试(归档 session 直接用 session 参数引用,返回摘要)。')
|
|
220
|
+
return lines.join('\n')
|
|
221
|
+
}
|