@zhushanwen/pi-session-reader 0.11.0 → 0.12.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__/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/entry-anchor.ts +5 -5
- package/src/discovery/find.ts +9 -1
- package/src/discovery/roots.ts +95 -7
- 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/extract.ts +1 -1
- package/src/index.ts +2 -2
- package/src/no-match.ts +1 -1
- package/src/result-action.ts +1 -1
- package/src/search-across.ts +3 -3
- package/src/tool-format.ts +12 -12
- package/src/tool-handler.ts +27 -19
- 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
|
|
@@ -29,16 +29,16 @@ export interface ZcodeAnchor {
|
|
|
29
29
|
* `subagent-record` custom entry 的 customType 值。
|
|
30
30
|
*
|
|
31
31
|
* 磁盘 JSONL 协议字符串(跨侧契约):写侧单源 = subagent-core record-entry.ts 的
|
|
32
|
-
* SUBAGENT_RECORD_CUSTOM_TYPE。reader
|
|
32
|
+
* SUBAGENT_RECORD_CUSTOM_TYPE。reader 生产依赖范围不引 subagent-core(该包是
|
|
33
33
|
* devDependency + vitest alias 形态),故按协议字符串本地持有;两侧漂移由
|
|
34
|
-
* entry-anchor.test.ts
|
|
34
|
+
* entry-anchor.test.ts 的跨包断言检查。
|
|
35
35
|
*/
|
|
36
36
|
const SUBAGENT_RECORD_CUSTOM_TYPE = 'subagent-record'
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* v2 条目 data 的认识版本词表(W1 起 = 2)。与写侧 subagent-core
|
|
40
|
-
* SUBAGENT_RECORD_ENTRY_VERSION 同值、同「本地持有 +
|
|
41
|
-
* 再升 v3
|
|
40
|
+
* SUBAGENT_RECORD_ENTRY_VERSION 同值、同「本地持有 + 测试检查漂移」模式——写侧
|
|
41
|
+
* 再升 v3 时本检查先红,提醒评估新版本的锚读取范围。
|
|
42
42
|
*/
|
|
43
43
|
const SUBAGENT_RECORD_ENTRY_V2 = 2
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ const SUBAGENT_RECORD_ENTRY_V2 = 2
|
|
|
46
46
|
* 从单条 entry 提取 zcode 锚。形状校验窄而严,任一不满足 = 该条不算命中(返回
|
|
47
47
|
* undefined,调用方继续扫——缺键条目不遮蔽更早的完整条目):
|
|
48
48
|
* - `type === 'custom'` ∧ `customType === 'subagent-record'`
|
|
49
|
-
* - 版本/判别键(schema
|
|
49
|
+
* - 版本/判别键(schema 版本检查,不认识的版本跳过而非猜测——写侧
|
|
50
50
|
* record-entry.ts classifySubagentRecordEntryData v2-only 同口径):
|
|
51
51
|
* `data.v === 2` ∧ `data.kind === 'settled'`(W1 注册/终态两条小条目——
|
|
52
52
|
* **锚只取自终态条**:registered 条目不携带 engineHandle,且设计 D1 裁决
|
package/src/discovery/find.ts
CHANGED
|
@@ -433,6 +433,14 @@ async function matchCandidates(
|
|
|
433
433
|
}
|
|
434
434
|
// 第一级 uuid 片段匹配(sessionId 或文件路径含 query)——cheap,已有 header。
|
|
435
435
|
// 零命中才进入第二级(互斥回退,非合并排序,§6.7 子决策 1)。
|
|
436
|
+
// [session-store-layout-inversion] 完整 id 精确等值短路:存储树分区目录名 =
|
|
437
|
+
// rootSessionId(`<root>/<rootSessionId>/subagents/sessions/<sid>.jsonl`),根会话 id
|
|
438
|
+
// 会出现在其全部 subagent 候选的路径里——不短路则完整 id 直查根会话被伪 multi 消歧。
|
|
439
|
+
// 等值命中是 includes 命中的子集,短路只消伪 multi 不减真召回。
|
|
440
|
+
const exactIdHits = candidates.filter((c) => c.ref.sessionId === query)
|
|
441
|
+
if (exactIdHits.length > 0) {
|
|
442
|
+
return exactIdHits.map((c) => ({ ...c }))
|
|
443
|
+
}
|
|
436
444
|
const exactHits = candidates.filter(
|
|
437
445
|
(c) => c.ref.sessionId.includes(query) || c.meta.path.includes(query),
|
|
438
446
|
)
|
|
@@ -626,7 +634,7 @@ export interface SessionFileRef {
|
|
|
626
634
|
*
|
|
627
635
|
* id 取自首行 header(复用 collectCandidates 的首行扫描——与 find 候选、resolveSessionId
|
|
628
636
|
* 同一 id 语义,agent 从 find 输出复制的完整 id 恒能命中;不按文件名反推,避免「文件改名
|
|
629
|
-
* 后与 header id
|
|
637
|
+
* 后与 header id 失配」的误报风险)。workflow-state 跳过、坏 header 跳过、被去重根不产候选
|
|
630
638
|
* 均随 collectCandidates 继承。同 id 多根出现([live] 与 [default] 不去重,V2 语义)时取
|
|
631
639
|
* mtime 新者(与 loadTitleIndex 合并语义一致)。
|
|
632
640
|
*
|
package/src/discovery/roots.ts
CHANGED
|
@@ -64,6 +64,68 @@ export interface SessionRoot {
|
|
|
64
64
|
const SESSIONS_DIRNAME = 'sessions'
|
|
65
65
|
const SUBAGENTS_DIRNAME = 'subagents'
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* taiji session 存储树根段(`<dataDir>/ext-sessions/`)。
|
|
69
|
+
*
|
|
70
|
+
* session-reader 是 extension 不 import @taiji/shared——本常量是 shared
|
|
71
|
+
* `SESSION_STORE_DIR_SEGMENT` 的同字面镜像,dataDir 推导先例 = `dirname(agentDir)`
|
|
72
|
+
* 基址(布局 B `agentDir = <dataDir>/agent`;同 tool-handler doExport 导出落点)。
|
|
73
|
+
* 字面量一致性由对拍机检锚点 `scripts/check-artifact-dir-formula-sync.mjs`(域 9
|
|
74
|
+
* session-reader discovery 断言位)锁定,树根段改名时 shared 与此处同批改。
|
|
75
|
+
*/
|
|
76
|
+
const SESSION_STORE_DIR_SEGMENT = 'ext-sessions'
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 存储树分区目录名合法性正则(镜像 shared `PI_SESSION_ID_PATTERN`,同域规则:首尾
|
|
80
|
+
* 字母数字,中间允许 [A-Za-z0-9._-];禁 `:`——btw 虚拟 id 必拒)。对拍机检锚点同上。
|
|
81
|
+
*
|
|
82
|
+
* [session-store-layout-inversion] 存储树扫描(subagent 根 / records manifest)以
|
|
83
|
+
* 此过滤 `<root>/` 一级目录——既是剪枝也是路径注入防线(目录名即 sessionId)。
|
|
84
|
+
*/
|
|
85
|
+
const PI_SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* session 存储树根目录(`<dataDir>/ext-sessions/`,dataDir = dirname(agentDir))。
|
|
89
|
+
*
|
|
90
|
+
* 新布局全部 session 维度衍生数据的统一落点(布局权威 = shared getSessionStoreDir,
|
|
91
|
+
* 本函数是 extension 侧镜像推导);旧布局 `<agentDir>/subagents` 不再读写(设计裁决
|
|
92
|
+
* D6:旧布局存量原位保留、不可见)。
|
|
93
|
+
*/
|
|
94
|
+
export function resolveSessionStoreRoot(agentDir: string): string {
|
|
95
|
+
return join(dirname(agentDir), SESSION_STORE_DIR_SEGMENT)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 存储树某根会话分区的 subagent 域目录(`<root>/<rootSessionId>/subagents/`)。 */
|
|
99
|
+
export function resolveStoreSubagentsDir(agentDir: string, rootSessionId: string): string {
|
|
100
|
+
return join(resolveSessionStoreRoot(agentDir), rootSessionId, SUBAGENTS_DIRNAME)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** subagent 会话 jsonl 目录(`<root>/<rootSessionId>/subagents/sessions/`)。 */
|
|
104
|
+
export function resolveStoreSubagentSessionsDir(agentDir: string, rootSessionId: string): string {
|
|
105
|
+
return join(resolveStoreSubagentsDir(agentDir, rootSessionId), 'sessions')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** ExecutionRecord manifest 目录(`<root>/<rootSessionId>/subagents/records/`)。 */
|
|
109
|
+
export function resolveStoreSubagentRecordsDir(agentDir: string, rootSessionId: string): string {
|
|
110
|
+
return join(resolveStoreSubagentsDir(agentDir, rootSessionId), 'records')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 枚举存储树 `<root>/` 一级合法会话分区目录名(isPiSessionId 过滤,见
|
|
115
|
+
* PI_SESSION_ID_PATTERN 注释)。根不存在/无权限 → 空数组,不抛错。
|
|
116
|
+
*/
|
|
117
|
+
export async function listSessionStorePartitions(storeRoot: string): Promise<string[]> {
|
|
118
|
+
let entries: Dirent[]
|
|
119
|
+
try {
|
|
120
|
+
entries = await readdir(storeRoot, { withFileTypes: true })
|
|
121
|
+
} catch {
|
|
122
|
+
return []
|
|
123
|
+
}
|
|
124
|
+
return entries
|
|
125
|
+
.filter((e) => e.isDirectory() && PI_SESSION_ID_PATTERN.test(e.name))
|
|
126
|
+
.map((e) => e.name)
|
|
127
|
+
}
|
|
128
|
+
|
|
67
129
|
/**
|
|
68
130
|
* `[live]` 规范化(§6.1/§7B 要点 3):`getSessionDir()` 在纯 pi 返回按 cwd 编码的子目录
|
|
69
131
|
*(`<agentDir>/sessions/--Users-x--`),在 taiji 覆盖态返回根本身。判据:basename
|
|
@@ -90,7 +152,10 @@ function sessionRootSpecs(signals: SessionRootSignals): SessionRoot[] {
|
|
|
90
152
|
}
|
|
91
153
|
specs.push(spec('default', 'main', join(signals.agentDir, SESSIONS_DIRNAME)))
|
|
92
154
|
specs.push(spec('legacy', 'main', join(dirname(signals.agentDir), SESSIONS_DIRNAME)))
|
|
93
|
-
|
|
155
|
+
// [session-store-layout-inversion] subagent 根 = 存储树根(`<dataDir>/ext-sessions`),
|
|
156
|
+
// 扫描在 sid 分区内定向探 subagents/sessions/(scanStoreSubagentJsonl);旧布局
|
|
157
|
+
// `<agentDir>/subagents` 不再读(设计裁决 D6:存量原位保留、不可见)。
|
|
158
|
+
specs.push(spec('subagent', 'subagent', resolveSessionStoreRoot(signals.agentDir)))
|
|
94
159
|
return specs
|
|
95
160
|
}
|
|
96
161
|
|
|
@@ -120,7 +185,7 @@ const SKIP_DIRS_NONE = new Set<string>()
|
|
|
120
185
|
|
|
121
186
|
// ============================================================
|
|
122
187
|
// 扫描 options(u8 追加:doctor「同一数据源两处渲染,不重复实现探测」§6.3——
|
|
123
|
-
// doctor 经 options 驱动 subagent 根不扫。原 u8
|
|
188
|
+
// doctor 经 options 驱动 subagent 根不扫。原 u8 的进程内缓存注入点已删除
|
|
124
189
|
// (ext-simplify-04 U3):每次调用实扫,本模块不再持有任何缓存语义)
|
|
125
190
|
// ============================================================
|
|
126
191
|
|
|
@@ -139,11 +204,12 @@ export interface SessionRootScanOptions {
|
|
|
139
204
|
*
|
|
140
205
|
* 优先级 live > default > legacy > subagent;按 realpath 去重:同路径多信号只扫一次,
|
|
141
206
|
* 保留最高优先级 kind 标签,被去重的根以 `dedupedInto` 标注(不产生 fileCount/scanMs)。
|
|
142
|
-
*
|
|
207
|
+
* 逐根扫描:main 源整树递归(跳 workflow-state);subagent 源 = 存储树定向扫描
|
|
208
|
+
*([session-store-layout-inversion] 单新根,见 scanStoreSubagentJsonl)。单根失败
|
|
143
209
|
* (不存在/无权限)→ 空结果继续,不抛错(沿用 listXxxSessions 契约)。
|
|
144
210
|
*
|
|
145
211
|
* options(u8 追加,全部缺省安全):`subagents:'stat'` 让 subagent 根只做存在性检查。
|
|
146
|
-
* 无缓存——每次调用实扫(原 u8 cache
|
|
212
|
+
* 无缓存——每次调用实扫(原 u8 cache 注入点已删除,ext-simplify-04 U3;find/F1
|
|
147
213
|
* 自检行计数与 doctor 表计数同取本次实扫,§7B 要点 8)。
|
|
148
214
|
*/
|
|
149
215
|
export async function resolveSessionRoots(
|
|
@@ -166,9 +232,13 @@ export async function resolveSessionRoots(
|
|
|
166
232
|
out.push({ ...candidate, exists: await pathExists(candidate.path) })
|
|
167
233
|
continue
|
|
168
234
|
}
|
|
169
|
-
|
|
235
|
+
// subagent 源 = 存储树定向扫描(sid 分区剪枝,见 scanStoreSubagentJsonl);
|
|
236
|
+
// main 源维持整树递归(跳 workflow-state)
|
|
170
237
|
const t0 = performance.now()
|
|
171
|
-
const files =
|
|
238
|
+
const files =
|
|
239
|
+
candidate.source === 'subagent'
|
|
240
|
+
? await scanStoreSubagentJsonl(candidate.path)
|
|
241
|
+
: await scanJsonlRecursive(candidate.path, SKIP_DIRS_MAIN)
|
|
172
242
|
const scanMs = performance.now() - t0
|
|
173
243
|
const exists = await pathExists(candidate.path)
|
|
174
244
|
const root: SessionRoot = {
|
|
@@ -194,7 +264,7 @@ export interface SessionFileMeta {
|
|
|
194
264
|
/**
|
|
195
265
|
* main sessions 扫描时整体跳过的子目录名。
|
|
196
266
|
* `workflow-state` 目录存放 workflow 运行状态文件(wf-*.jsonl,首行 `{"v":"wf-run-v1"|"wf-run-v2"...}`,
|
|
197
|
-
* 版本随 subagent-workflow 快照格式演进,读取侧 v1/v2 兼容),非 session 文件——属 family
|
|
267
|
+
* 版本随 subagent-workflow 快照格式演进,读取侧 v1/v2 兼容),非 session 文件——属 family 路径
|
|
198
268
|
* 独立处理(design §3.3 D-7),扫描 main sessions 时排除,
|
|
199
269
|
* 否则会把 wf 文件误收为 session(且 find.ts 读其首行 header 时会因 type≠session 被丢弃,
|
|
200
270
|
* 在此排除可避免这批无效首行扫描)。
|
|
@@ -210,6 +280,24 @@ function isSessionJsonl(name: string): boolean {
|
|
|
210
280
|
return name.endsWith('.jsonl')
|
|
211
281
|
}
|
|
212
282
|
|
|
283
|
+
/**
|
|
284
|
+
* 存储树 subagent 根定向扫描([session-store-layout-inversion] 读侧单新根):
|
|
285
|
+
* 只探 `<root>/<rootSessionId>/subagents/sessions/` 固定位置。`<root>/` 一级只进
|
|
286
|
+
* isPiSessionId 形态目录(剪枝:不进 logs/attachments 等大目录,也挡非分区名目录);
|
|
287
|
+
* 每 sid 分区内 records/ model-switch/ 不扫(会话文件域独立)。sessions/ 内部维持
|
|
288
|
+
* 递归 + stat(复用 scanJsonlRecursive,`.finalized` 天然排除)。存储树根或分区目录
|
|
289
|
+
* 不存在/无权限 → 空结果,不抛错(listXxxSessions 契约)。
|
|
290
|
+
*/
|
|
291
|
+
async function scanStoreSubagentJsonl(storeRoot: string): Promise<SessionFileMeta[]> {
|
|
292
|
+
const results: SessionFileMeta[] = []
|
|
293
|
+
for (const sid of await listSessionStorePartitions(storeRoot)) {
|
|
294
|
+
results.push(
|
|
295
|
+
...(await scanJsonlRecursive(join(storeRoot, sid, SUBAGENTS_DIRNAME, 'sessions'), SKIP_DIRS_NONE)),
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
return results
|
|
299
|
+
}
|
|
300
|
+
|
|
213
301
|
/**
|
|
214
302
|
* 递归扫描 rootDir 下所有 .jsonl 文件(排除 .finalized),返回绝对路径 + mtime + size。
|
|
215
303
|
* `skipDirs` 命名的目录整体跳过。目录不存在/无权限 → 返回空数组,不抛错(design §2 坏路径容错)。
|