@zhushanwen/pi-session-reader 0.1.0 → 0.2.1
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 +1 -1
- package/src/__tests__/execution-tree.test.ts +792 -0
- package/src/__tests__/family.test.ts +150 -0
- package/src/__tests__/find.test.ts +342 -1
- package/src/__tests__/index.test.ts +50 -0
- package/src/__tests__/parser.test.ts +2 -1
- package/src/__tests__/real-data.ts +20 -0
- package/src/__tests__/render.test.ts +2 -1
- package/src/__tests__/subagents.test.ts +323 -14
- package/src/__tests__/tool-handler.test.ts +872 -5
- package/src/__tests__/tree.test.ts +20 -0
- package/src/__tests__/turns.test.ts +2 -1
- package/src/__tests__/workflow.test.ts +353 -0
- package/src/core/execution-tree.ts +517 -0
- package/src/core/family.ts +40 -0
- package/src/core/tree.ts +14 -5
- package/src/core/workflow.ts +330 -0
- package/src/discovery/find.ts +167 -20
- package/src/discovery/roots.ts +3 -2
- package/src/discovery/subagents.ts +113 -152
- package/src/discovery/workflows.ts +164 -0
- package/src/index.ts +22 -4
- package/src/tool-handler.ts +334 -21
package/src/tool-handler.ts
CHANGED
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
* 闭包 catch 转 isError:true 文本返回——handler 可抛(纯逻辑可测),execute 不抛(pi 契约)。
|
|
12
12
|
* 例外:F2 多匹配与 F1 find 零匹配「不视为错误」,返回消歧/提示结果而非抛错。
|
|
13
13
|
*/
|
|
14
|
+
import { existsSync, openSync, readSync, closeSync } from 'node:fs'
|
|
14
15
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
15
|
-
import { join } from 'node:path'
|
|
16
|
+
import { join, isAbsolute } from 'node:path'
|
|
17
|
+
import { homedir } from 'node:os'
|
|
16
18
|
import { findSessions, type MatchedSession } from './discovery/find.js'
|
|
17
|
-
import { buildFamilyFromFs } from './discovery/subagents.js'
|
|
19
|
+
import { buildFamilyFromFs, listRecordManifests, type RecordManifest } from './discovery/subagents.js'
|
|
20
|
+
import { readRunSnapshot, resolveWorkflows } from './discovery/workflows.js'
|
|
18
21
|
import { parseSessionFile, type Entry, type ParseResult } from './core/parser.js'
|
|
22
|
+
import { parseRunSnapshot, renderWorkflowOverview, type WorkflowOverview } from './core/workflow.js'
|
|
19
23
|
import { buildTreeView } from './core/tree.js'
|
|
20
24
|
import { segmentTurns, type Turn } from './core/turns.js'
|
|
21
25
|
import { extractToolCalls, formatToolCallSummary, basename } from './core/toolcall.js'
|
|
@@ -28,7 +32,12 @@ import {
|
|
|
28
32
|
type EntryBrief,
|
|
29
33
|
type ToolResultSummaryEntry,
|
|
30
34
|
} from './core/render.js'
|
|
31
|
-
import type { Family } from './core/family.js'
|
|
35
|
+
import type { Family, SessionRef, WorkflowRef } from './core/family.js'
|
|
36
|
+
import {
|
|
37
|
+
buildExecutionTree,
|
|
38
|
+
formatExecutionTreeText,
|
|
39
|
+
type ExecutionTree,
|
|
40
|
+
} from './core/execution-tree.js'
|
|
32
41
|
|
|
33
42
|
// ---------------------------------------------------------------------------
|
|
34
43
|
// 公共类型(与 index.ts 的 TypeBox schema 对齐)
|
|
@@ -43,6 +52,7 @@ export type SessionReadAction =
|
|
|
43
52
|
| 'search'
|
|
44
53
|
| 'export'
|
|
45
54
|
| 'extract'
|
|
55
|
+
| 'workflow'
|
|
46
56
|
|
|
47
57
|
export interface SessionReadParams {
|
|
48
58
|
action: SessionReadAction
|
|
@@ -58,11 +68,17 @@ export interface SessionReadParams {
|
|
|
58
68
|
allBranches?: boolean
|
|
59
69
|
granularity?: 'turn' | 'entry'
|
|
60
70
|
cwd?: string
|
|
71
|
+
/** find/resolveSessionId: 按来源过滤。"main" = sessions/、"subagent" = subagents/。默认两者合并。 */
|
|
72
|
+
source?: 'main' | 'subagent'
|
|
73
|
+
/** workflow action: 可选,聚焦单个 runId(多 run 消歧)。不传 → 全部 run 概览。 */
|
|
74
|
+
runId?: string
|
|
61
75
|
limit?: number
|
|
62
76
|
/** extract action: 素材类型(必填)。其他 action 忽略。 */
|
|
63
77
|
what?: 'user-messages' | 'commands' | 'files' | 'commits' | 'tool-results'
|
|
64
78
|
/** extract action: 过滤 commands/tool-results 的工具名(可选)。 */
|
|
65
79
|
tool?: string
|
|
80
|
+
/** family action: 返回嵌套执行树(任意深度 subagent↔workflow-call 相互嵌套)。默认 false(flat family)。 */
|
|
81
|
+
recursive?: boolean
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
export interface ToolResult {
|
|
@@ -165,26 +181,134 @@ type ResolveResult =
|
|
|
165
181
|
| { kind: 'ok'; sessionId: string; fileName: string }
|
|
166
182
|
| { kind: 'multi'; query: string; candidates: MatchedSession[] }
|
|
167
183
|
|
|
184
|
+
/** readSessionHeaderId 读首行的 buffer 上限。session header(id/cwd/parentSession)实测 < 300 字节,4KB 足够。 */
|
|
185
|
+
const HEADER_READ_BYTES = 4096
|
|
186
|
+
|
|
168
187
|
/**
|
|
169
|
-
*
|
|
188
|
+
* 同步读 session 文件首行 header,返回 type==='session' 的 id。
|
|
170
189
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
|
|
174
|
-
*
|
|
190
|
+
* 任何异常(文件不存在/空文件/解析失败/type 不符)返回 undefined。与 find.ts readFirstLine/
|
|
191
|
+
* parseHeader 同构(定长 buffer 读首行 + JSON.parse + type 校验),但用同步 fs API
|
|
192
|
+
*(resolveSessionId 内仅调用 1 次,同步开销可接受),且不导出——避免与 w1 的 find.ts
|
|
193
|
+
* 文件交叉(CQ2 决策)。
|
|
194
|
+
*/
|
|
195
|
+
function readSessionHeaderId(filePath: string): string | undefined {
|
|
196
|
+
let fd: number | undefined
|
|
197
|
+
try {
|
|
198
|
+
fd = openSync(filePath, 'r')
|
|
199
|
+
const buf = Buffer.alloc(HEADER_READ_BYTES)
|
|
200
|
+
const bytesRead = readSync(fd, buf, 0, HEADER_READ_BYTES, 0)
|
|
201
|
+
if (bytesRead === 0) return undefined
|
|
202
|
+
const text = buf.subarray(0, bytesRead).toString('utf8')
|
|
203
|
+
const nl = text.indexOf('\n')
|
|
204
|
+
const line = nl === -1 ? text : text.slice(0, nl)
|
|
205
|
+
let raw: unknown
|
|
206
|
+
try {
|
|
207
|
+
raw = JSON.parse(line)
|
|
208
|
+
} catch {
|
|
209
|
+
return undefined
|
|
210
|
+
}
|
|
211
|
+
if (typeof raw !== 'object' || raw === null) return undefined
|
|
212
|
+
const o = raw as Record<string, unknown>
|
|
213
|
+
if (o.type !== 'session' || typeof o.id !== 'string') return undefined
|
|
214
|
+
return o.id
|
|
215
|
+
} catch {
|
|
216
|
+
return undefined
|
|
217
|
+
} finally {
|
|
218
|
+
if (fd !== undefined) {
|
|
219
|
+
try {
|
|
220
|
+
closeSync(fd)
|
|
221
|
+
} catch {
|
|
222
|
+
// closeSync 失败:fd 可能已无效,header 数据已读取,关闭失败不影响结果(best-effort)
|
|
223
|
+
void fd
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** ~ 前缀(home 目录简写),与 expandHome 配套避免 magic number。 */
|
|
230
|
+
const HOME_TILDE_PREFIX = '~/'
|
|
231
|
+
|
|
232
|
+
/** 展开 ~ 前缀到 homedir('~' → homedir;'~/x' → homedir/x;其余原样)。 */
|
|
233
|
+
function expandHome(p: string): string {
|
|
234
|
+
if (p === '~') return homedir()
|
|
235
|
+
if (p.startsWith(HOME_TILDE_PREFIX))
|
|
236
|
+
return join(homedir(), p.slice(HOME_TILDE_PREFIX.length))
|
|
237
|
+
return p
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 把 session 参数解析到唯一完整 id(design §6.1 M0 + U2/U3)。
|
|
175
242
|
*
|
|
176
|
-
*
|
|
243
|
+
* 三形态:
|
|
244
|
+
* - ① 绝对路径 / ~ 前缀 → 展开后读首行 header,sessionId=header 真实 id(文件名仅定位)
|
|
245
|
+
* - ② sa-id 前缀 → listRecordManifests 精确反查,sessionId=sessionFile header id
|
|
246
|
+
* (禁止降级 record.id——sa- 形态不可当 sessionId,CQ3 决策)
|
|
247
|
+
* - ③ 其余 → findSessions 透传 source 沿用 F1/F2
|
|
248
|
+
*
|
|
249
|
+
* 错误契约(U3):① 文件不存在/非 .jsonl/header 读不出 → F6 风格;
|
|
250
|
+
* ② sessionFile GC → ES1(manifest 元数据 + 👉);sa-id 0/>1 命中 → ES2(👉 family)。
|
|
251
|
+
*
|
|
252
|
+
* 仅用于 family/outline/expand/detail/search/export/extract(find action 自行调 findSessions,
|
|
177
253
|
* 零匹配时返回空 + 提示,不抛错)。
|
|
178
254
|
*/
|
|
179
255
|
async function resolveSessionId(
|
|
180
256
|
rawSession: string | undefined,
|
|
181
257
|
action: SessionReadAction,
|
|
182
258
|
agentDir: string,
|
|
259
|
+
source?: 'main' | 'subagent',
|
|
183
260
|
): Promise<ResolveResult> {
|
|
184
261
|
const session = stripHash(requireStr(rawSession, 'session', action))
|
|
185
|
-
|
|
262
|
+
|
|
263
|
+
// ① 绝对路径或 ~ 前缀(Windows 盘符由 isAbsolute 处理)
|
|
264
|
+
if (isAbsolute(session) || session === '~' || session.startsWith('~/')) {
|
|
265
|
+
const expanded = expandHome(session)
|
|
266
|
+
if (!expanded.endsWith('.jsonl')) {
|
|
267
|
+
throw err(
|
|
268
|
+
`读取失败:${session}(非 .jsonl session 文件)。👉 检查文件或换 session。`,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
if (!existsSync(expanded)) {
|
|
272
|
+
throw err(`读取失败:${session}(文件不存在)。👉 检查文件或换 session。`)
|
|
273
|
+
}
|
|
274
|
+
const headerId = readSessionHeaderId(expanded)
|
|
275
|
+
if (headerId === undefined) {
|
|
276
|
+
throw err(
|
|
277
|
+
`读取失败:${session}(首行非合法 session header)。👉 检查文件或换 session。`,
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
return { kind: 'ok', sessionId: headerId, fileName: expanded }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ② sa-id 前缀 → record manifest 精确反查
|
|
284
|
+
if (session.startsWith('sa-')) {
|
|
285
|
+
const manifests = await listRecordManifests(agentDir)
|
|
286
|
+
const hits = manifests.filter((m) => m.id === session)
|
|
287
|
+
if (hits.length === 0) {
|
|
288
|
+
throw err(formatSaIdNotFound(session))
|
|
289
|
+
}
|
|
290
|
+
if (hits.length > 1) {
|
|
291
|
+
throw err(formatSaIdAmbiguous(session, hits))
|
|
292
|
+
}
|
|
293
|
+
const record = hits[0]
|
|
294
|
+
if (!existsSync(record.sessionFile)) {
|
|
295
|
+
throw err(formatSessionGc(record))
|
|
296
|
+
}
|
|
297
|
+
const headerId = readSessionHeaderId(record.sessionFile)
|
|
298
|
+
if (headerId === undefined) {
|
|
299
|
+
// header 读不出不降级 record.id(sa- 形态不可当 sessionId,CQ3)
|
|
300
|
+
throw err(
|
|
301
|
+
`读取失败:${record.sessionFile}(首行非合法 session header)。👉 检查文件或换 session。`,
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
return { kind: 'ok', sessionId: headerId, fileName: record.sessionFile }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ③ 其余:findSessions 透传 source 沿用 F1/F2
|
|
308
|
+
const opts = { limit: 10, ...(source ? { source } : {}) }
|
|
309
|
+
const { matches } = await findSessions(session, agentDir, opts)
|
|
186
310
|
if (matches.length === 0) {
|
|
187
|
-
const recent = await findSessions('recent', agentDir,
|
|
311
|
+
const recent = await findSessions('recent', agentDir, opts)
|
|
188
312
|
throw err(formatNoMatch(session, recent.matches))
|
|
189
313
|
}
|
|
190
314
|
if (matches.length === 1) {
|
|
@@ -193,6 +317,37 @@ async function resolveSessionId(
|
|
|
193
317
|
return { kind: 'multi', query: session, candidates: matches }
|
|
194
318
|
}
|
|
195
319
|
|
|
320
|
+
/** ES1(SESSION_FILE_GC):sa-id 恰 1 命中但 sessionFile 不存在(GC/未写入)。含 manifest 元数据 + 👉。 */
|
|
321
|
+
function formatSessionGc(record: RecordManifest): string {
|
|
322
|
+
return (
|
|
323
|
+
`subagent "${record.id}" 的 session 文件不存在(可能已被 GC 或未写入):\n` +
|
|
324
|
+
` rootSessionId: ${record.rootSessionId}\n` +
|
|
325
|
+
` agentName: ${record.agentName ?? '(未记录)'}\n` +
|
|
326
|
+
` sessionFile: ${record.sessionFile}\n` +
|
|
327
|
+
`👉 改用 session_read { action:"family" } 查该 subagent 的后代,或换一个 completed subagent 重试。`
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** ES2(SA_ID_NO_MATCH):sa-id 无精确匹配(可能仍在运行 / 片段输入)。 */
|
|
332
|
+
function formatSaIdNotFound(saId: string): string {
|
|
333
|
+
return (
|
|
334
|
+
`subagent "${saId}" 无匹配 record(可能仍在运行——终态 record 在 completed/failed 后才写)。` +
|
|
335
|
+
`\n👉 用 session_read { action:"family" } 查活跃/已完成的 subagent;` +
|
|
336
|
+
`若是片段输入,请用完整 sa- id 或 action:"find" 重试。`
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** ES2(SA_ID_AMBIGUOUS):sa-id 多 manifest 命中(数据异常,record.id 应唯一)。 */
|
|
341
|
+
function formatSaIdAmbiguous(saId: string, records: RecordManifest[]): string {
|
|
342
|
+
return (
|
|
343
|
+
`subagent "${saId}" 匹配 ${records.length} 个 record(数据异常,record.id 应唯一):\n` +
|
|
344
|
+
records
|
|
345
|
+
.map((r) => ` ${r.id} (root=${r.rootSessionId} file=${r.sessionFile})`)
|
|
346
|
+
.join('\n') +
|
|
347
|
+
`\n👉 用 session_read { action:"family" } 或完整 session uuid 重试。`
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
196
351
|
/** F1 无匹配 message(含最近 10 + 👉)。 */
|
|
197
352
|
function formatNoMatch(query: string, recent: MatchedSession[]): string {
|
|
198
353
|
const lines: string[] = recent.length
|
|
@@ -462,6 +617,7 @@ async function doFind(params: SessionReadParams, agentDir: string): Promise<Tool
|
|
|
462
617
|
const { matches, truncated } = await findSessions(query, agentDir, {
|
|
463
618
|
cwd: params.cwd,
|
|
464
619
|
limit: params.limit ?? 20,
|
|
620
|
+
...(params.source ? { source: params.source } : {}),
|
|
465
621
|
})
|
|
466
622
|
if (matches.length === 0) {
|
|
467
623
|
const recent = await findSessions('recent', agentDir, { limit: 10 })
|
|
@@ -476,10 +632,36 @@ async function doFind(params: SessionReadParams, agentDir: string): Promise<Tool
|
|
|
476
632
|
}
|
|
477
633
|
}
|
|
478
634
|
|
|
479
|
-
/**
|
|
635
|
+
/**
|
|
636
|
+
* family:fork 父链/子代 + 隔代 subagent + workflow run(design §3.4 family)。
|
|
637
|
+
*
|
|
638
|
+
* recursive=false(默认)→ flat family(buildFamilyFromFs + formatFamilyText,m0/m1/m2 行为零回归)。
|
|
639
|
+
* recursive=true → 嵌套执行树(buildExecutionTree + formatExecutionTreeText,任意深度
|
|
640
|
+
* subagent↔workflow-call 相互嵌套,IF4)。错误契约同构:multi→disambiguate;构建抛错→catch 转 👉。
|
|
641
|
+
*/
|
|
480
642
|
async function doFamily(params: SessionReadParams, agentDir: string): Promise<ToolResult> {
|
|
481
|
-
const resolved = await resolveSessionId(params.session, 'family', agentDir)
|
|
643
|
+
const resolved = await resolveSessionId(params.session, 'family', agentDir, params.source)
|
|
482
644
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
645
|
+
|
|
646
|
+
// recursive=true:嵌套执行树(U7/U8)
|
|
647
|
+
if (params.recursive) {
|
|
648
|
+
let tree: ExecutionTree
|
|
649
|
+
try {
|
|
650
|
+
// MF-1:传 resolved.fileName 使 main root 填 sessionFile——main session 自身发起的
|
|
651
|
+
// workflow run(workflow-state-link)进入执行树,与 flat family 行为一致。
|
|
652
|
+
tree = await buildExecutionTree(resolved.sessionId, agentDir, resolved.fileName)
|
|
653
|
+
} catch (e) {
|
|
654
|
+
throw err(
|
|
655
|
+
`构建执行树失败:${resolved.sessionId}(${e instanceof Error ? e.message : String(e)})。👉 检查 session 或用 find 重新定位,或改用 recursive:false 看 flat family 兑底。`,
|
|
656
|
+
)
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
content: [{ type: 'text', text: formatExecutionTreeText(tree) }],
|
|
660
|
+
details: { tree },
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// recursive falsy(默认):flat family(m0/m1/m2 现状零回归)
|
|
483
665
|
let family: Family
|
|
484
666
|
try {
|
|
485
667
|
family = await buildFamilyFromFs(resolved.sessionId, agentDir)
|
|
@@ -493,7 +675,7 @@ async function doFamily(params: SessionReadParams, agentDir: string): Promise<To
|
|
|
493
675
|
|
|
494
676
|
/** outline:turn 级全貌 TOC(design §3.4 outline,~500 token)。 */
|
|
495
677
|
async function doOutline(params: SessionReadParams, agentDir: string): Promise<ToolResult> {
|
|
496
|
-
const resolved = await resolveSessionId(params.session, 'outline', agentDir)
|
|
678
|
+
const resolved = await resolveSessionId(params.session, 'outline', agentDir, params.source)
|
|
497
679
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
498
680
|
const { entries, totalBytes } = await safeParse(resolved.fileName)
|
|
499
681
|
const tree = buildTreeView(entries)
|
|
@@ -512,7 +694,7 @@ async function doOutline(params: SessionReadParams, agentDir: string): Promise<T
|
|
|
512
694
|
|
|
513
695
|
/** expand:单 turn 的 entry 列表(design §3.4 expand)。turn 越界抛 F4。 */
|
|
514
696
|
async function doExpand(params: SessionReadParams, agentDir: string): Promise<ToolResult> {
|
|
515
|
-
const resolved = await resolveSessionId(params.session, 'expand', agentDir)
|
|
697
|
+
const resolved = await resolveSessionId(params.session, 'expand', agentDir, params.source)
|
|
516
698
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
517
699
|
const turnIdx = parseTurnIndex(requireStr(params.turn, 'turn', 'expand'))
|
|
518
700
|
const { entries } = await safeParse(resolved.fileName)
|
|
@@ -534,7 +716,7 @@ async function doExpand(params: SessionReadParams, agentDir: string): Promise<To
|
|
|
534
716
|
|
|
535
717
|
/** detail:turns 范围的完整文本(design §3.4 detail)。默认省略 toolResult/thinking。 */
|
|
536
718
|
async function doDetail(params: SessionReadParams, agentDir: string): Promise<ToolResult> {
|
|
537
|
-
const resolved = await resolveSessionId(params.session, 'detail', agentDir)
|
|
719
|
+
const resolved = await resolveSessionId(params.session, 'detail', agentDir, params.source)
|
|
538
720
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
539
721
|
const range = parseTurnsRange(requireStr(params.turns, 'turns', 'detail'))
|
|
540
722
|
const { entries } = await safeParse(resolved.fileName)
|
|
@@ -563,7 +745,7 @@ async function doSearch(
|
|
|
563
745
|
agentDir: string,
|
|
564
746
|
signal?: AbortSignal,
|
|
565
747
|
): Promise<ToolResult> {
|
|
566
|
-
const resolved = await resolveSessionId(params.session, 'search', agentDir)
|
|
748
|
+
const resolved = await resolveSessionId(params.session, 'search', agentDir, params.source)
|
|
567
749
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
568
750
|
const pattern = requireStr(params.pattern, 'pattern', 'search')
|
|
569
751
|
const scope = params.scope ?? 'all'
|
|
@@ -615,7 +797,7 @@ async function doSearch(
|
|
|
615
797
|
/** export:物化摘要到 <agentDir>/tmp/session-view-<id>.md(design §3.4 export,D-8)。 */
|
|
616
798
|
async function doExport(params: SessionReadParams, agentDir: string): Promise<ToolResult> {
|
|
617
799
|
const format = params.format ?? 'outline'
|
|
618
|
-
const resolved = await resolveSessionId(params.session, 'export', agentDir)
|
|
800
|
+
const resolved = await resolveSessionId(params.session, 'export', agentDir, params.source)
|
|
619
801
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
620
802
|
|
|
621
803
|
let text: string
|
|
@@ -1064,7 +1246,7 @@ function extractToolResults(turns: Turn[], tool: string | undefined): ToolResult
|
|
|
1064
1246
|
* segmentTurns → 可选 turns 范围限定(复用 parseTurnsRange)→ F7 校验 what → 分发 5 预设。
|
|
1065
1247
|
*/
|
|
1066
1248
|
async function doExtract(params: SessionReadParams, agentDir: string): Promise<ToolResult> {
|
|
1067
|
-
const resolved = await resolveSessionId(params.session, 'extract', agentDir)
|
|
1249
|
+
const resolved = await resolveSessionId(params.session, 'extract', agentDir, params.source)
|
|
1068
1250
|
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
1069
1251
|
const { entries } = await safeParse(resolved.fileName)
|
|
1070
1252
|
// extract 遍历全量 entry(含旁支/压缩历史),与 outline/expand/detail 的 leaf 视图不同:
|
|
@@ -1114,6 +1296,135 @@ async function doExtract(params: SessionReadParams, agentDir: string): Promise<T
|
|
|
1114
1296
|
}
|
|
1115
1297
|
}
|
|
1116
1298
|
|
|
1299
|
+
// ===========================================================================
|
|
1300
|
+
// workflow action(w6:消费 w5 的 readRunSnapshot/parseRunSnapshot/renderWorkflowOverview)
|
|
1301
|
+
// ===========================================================================
|
|
1302
|
+
|
|
1303
|
+
/** doWorkflow 的 details 结构(ES-wf-no-runs/runid-not-found/snapshot-* 错误契约的具体类型)。 */
|
|
1304
|
+
interface WorkflowDetails {
|
|
1305
|
+
runs: WorkflowOverview[]
|
|
1306
|
+
runIds: string[]
|
|
1307
|
+
skippedRuns?: Array<{ runId: string; stateFile: string; reason: string }>
|
|
1308
|
+
requestedRunId?: string
|
|
1309
|
+
sessionId?: string
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/** 单个被跳过的 run 记录(snapshot 不可读/不可解析)。 */
|
|
1313
|
+
interface SkippedRun {
|
|
1314
|
+
runId: string
|
|
1315
|
+
stateFile: string
|
|
1316
|
+
reason: string
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* workflow:workflow run 概览(design §3.4 workflow,m2 IF-doWorkflow)。
|
|
1321
|
+
*
|
|
1322
|
+
* 流程:① resolveSessionId(multi 走 disambiguate)→ ② 读目标 session 的 workflow-state-link
|
|
1323
|
+
* → ③ 无 run → ES-wf-no-runs(提示+👉family,不抛错)→ ④ runId 过滤,无匹配 →
|
|
1324
|
+
* ES-wf-runid-not-found(列候选+👉,不抛错)→ ⑤ 逐 run readRunSnapshot+parseRunSnapshot,
|
|
1325
|
+
* 不可读/不可解析 → skippedRuns(不中断其他 run,ES-wf-snapshot-read-fail/unparseable)
|
|
1326
|
+
* → ⑥ renderWorkflowOverview 拼接。
|
|
1327
|
+
*
|
|
1328
|
+
* ② 的读取(MF-2):不用 buildFamilyFromFs(其 resolveFamily 只索引 main session,subagent
|
|
1329
|
+
* session 会抛「session not found in family index」)——resolveSessionId 已把 session 解析到
|
|
1330
|
+
* 真实文件(kind==='ok' 保证文件存在,三形态:绝对路径/sa-id 均 existsSync 校验,片段匹配
|
|
1331
|
+
* 来自实际 fs 扫描),直接用 resolved.fileName 构造单条目 sessionIdToPath 调 resolveWorkflows
|
|
1332
|
+
*(与 buildFamilyFromFs 步骤 6 的 workflow 腿同源)。pathToRef 仅含目标 session,call 引用
|
|
1333
|
+
* 走 sessionRefFromPath 文件名最小回退(sessionId+fileName,足够 LLM 跳 outline/detail 深读)。
|
|
1334
|
+
*
|
|
1335
|
+
* 错误契约(C2):workflow 概览探索语义,三类错误均返回 ToolResult 不抛错。
|
|
1336
|
+
* step 的 call sessionId/sessionFile 是 LLM 跳 outline/detail 的入口(m0 resolveSessionId
|
|
1337
|
+
* 三形态复用:sessionId/绝对路径/sa-id 均可深读,TC-wf-step-sessionfile-link)。
|
|
1338
|
+
*/
|
|
1339
|
+
async function doWorkflow(params: SessionReadParams, agentDir: string): Promise<ToolResult> {
|
|
1340
|
+
const resolved = await resolveSessionId(params.session, 'workflow', agentDir, params.source)
|
|
1341
|
+
if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
|
|
1342
|
+
|
|
1343
|
+
let workflows: WorkflowRef[]
|
|
1344
|
+
try {
|
|
1345
|
+
// MF-2:直读 resolved.fileName(subagent session 亦可),绕过 buildFamilyFromFs 的
|
|
1346
|
+
// main-only byId 索引(对 subagent 抛「session not found in family index」)。
|
|
1347
|
+
// resolveWorkflows 自身容错(读失败返回 []),「session 真不存在」的 F 级契约已由
|
|
1348
|
+
// resolveSessionId 保证(kind==='ok' 前已 existsSync/扫描校验)。
|
|
1349
|
+
const sessionIdToPath = new Map<string, string>([[resolved.sessionId, resolved.fileName]])
|
|
1350
|
+
const pathToRef = new Map<string, SessionRef>()
|
|
1351
|
+
workflows = await resolveWorkflows(resolved.sessionId, sessionIdToPath, pathToRef)
|
|
1352
|
+
} catch (e) {
|
|
1353
|
+
throw err(
|
|
1354
|
+
`读取 workflow run 失败:${resolved.sessionId}(${e instanceof Error ? e.message : String(e)})。👉 检查 session 或用 find 重新定位。`,
|
|
1355
|
+
)
|
|
1356
|
+
}
|
|
1357
|
+
const allRunIds = workflows.map((w) => w.runId)
|
|
1358
|
+
|
|
1359
|
+
// ③ ES-wf-no-runs:session 未发起任何 workflow run(不抛错,返提示+👉family)
|
|
1360
|
+
if (workflows.length === 0) {
|
|
1361
|
+
const text =
|
|
1362
|
+
`session ${resolved.sessionId} 无 workflow run。\n` +
|
|
1363
|
+
`👉 用 session_read { action:'family' } 查该 session 的 subagent 后代,或确认 session 是否发起过 workflow。`
|
|
1364
|
+
const details: WorkflowDetails = { runs: [], runIds: [], sessionId: resolved.sessionId }
|
|
1365
|
+
return { content: [{ type: 'text', text }], details }
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// ④ runId 过滤(可选,多 run 消歧)
|
|
1369
|
+
const requestedRunId =
|
|
1370
|
+
params.runId !== undefined && params.runId.trim() !== '' ? params.runId.trim() : undefined
|
|
1371
|
+
let selected: WorkflowRef[] = workflows
|
|
1372
|
+
if (requestedRunId !== undefined) {
|
|
1373
|
+
selected = workflows.filter((w) => w.runId === requestedRunId)
|
|
1374
|
+
if (selected.length === 0) {
|
|
1375
|
+
// ES-wf-runid-not-found:列出可用 runId + 👉(不抛错,与 F2 多匹配消歧同构)
|
|
1376
|
+
const lines = allRunIds.map((rid) => ` ${rid}`).join('\n')
|
|
1377
|
+
const text =
|
|
1378
|
+
`runId "${requestedRunId}" 无匹配。可用 runId:\n${lines}\n` +
|
|
1379
|
+
`👉 用上述完整 runId 重试,或不传 runId 看全部 run 概览。`
|
|
1380
|
+
const details: WorkflowDetails = { runs: [], runIds: allRunIds, requestedRunId }
|
|
1381
|
+
return { content: [{ type: 'text', text }], details }
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// ⑤⑥ 逐 run 读 snapshot → parse → render
|
|
1386
|
+
const runs: WorkflowOverview[] = []
|
|
1387
|
+
const runIds: string[] = []
|
|
1388
|
+
const skippedRuns: SkippedRun[] = []
|
|
1389
|
+
const contentParts: string[] = []
|
|
1390
|
+
|
|
1391
|
+
for (const wf of selected) {
|
|
1392
|
+
const snap = await readRunSnapshot(wf.stateFile)
|
|
1393
|
+
if (snap === undefined) {
|
|
1394
|
+
// ES-wf-snapshot-read-fail:文件不存在/读失败/全行不可解析 → 跳过,不中断其他 run
|
|
1395
|
+
skippedRuns.push({ runId: wf.runId, stateFile: wf.stateFile, reason: 'snapshot-unreadable' })
|
|
1396
|
+
contentParts.push(`run ${wf.runId}: 快照不可读(stateFile=${wf.stateFile})已跳过`)
|
|
1397
|
+
continue
|
|
1398
|
+
}
|
|
1399
|
+
const overview = parseRunSnapshot(snap, wf.runId, wf.stateFile)
|
|
1400
|
+
if (overview === null) {
|
|
1401
|
+
// ES-wf-snapshot-unparseable:对象既非 NEW 也非 OLD → 跳过
|
|
1402
|
+
skippedRuns.push({ runId: wf.runId, stateFile: wf.stateFile, reason: 'snapshot-unparseable' })
|
|
1403
|
+
contentParts.push(`run ${wf.runId}: 快照格式不可识别(stateFile=${wf.stateFile})已跳过`)
|
|
1404
|
+
continue
|
|
1405
|
+
}
|
|
1406
|
+
runs.push(overview)
|
|
1407
|
+
runIds.push(wf.runId)
|
|
1408
|
+
contentParts.push(renderWorkflowOverview(overview))
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
const details: WorkflowDetails = { runs, runIds }
|
|
1412
|
+
if (requestedRunId !== undefined) details.requestedRunId = requestedRunId
|
|
1413
|
+
if (skippedRuns.length > 0) details.skippedRuns = skippedRuns
|
|
1414
|
+
|
|
1415
|
+
// 全部 run 都跳过的兑底提示(ES-wf-snapshot-read-fail 末段)
|
|
1416
|
+
let text: string
|
|
1417
|
+
if (runs.length === 0) {
|
|
1418
|
+
text =
|
|
1419
|
+
contentParts.join('\n') +
|
|
1420
|
+
`\n👉 检查 stateFile 或用 session_read { action:'family' } 看 call session 直接深读。`
|
|
1421
|
+
} else {
|
|
1422
|
+
text = contentParts.join('\n\n')
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return { content: [{ type: 'text', text }], details }
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1117
1428
|
// ===========================================================================
|
|
1118
1429
|
// 入口:按 action 分发
|
|
1119
1430
|
// ===========================================================================
|
|
@@ -1148,12 +1459,14 @@ export async function handleSessionRead(
|
|
|
1148
1459
|
return doExport(params, agentDir)
|
|
1149
1460
|
case 'extract':
|
|
1150
1461
|
return doExtract(params, agentDir)
|
|
1462
|
+
case 'workflow':
|
|
1463
|
+
return doWorkflow(params, agentDir)
|
|
1151
1464
|
default: {
|
|
1152
|
-
// exhaustive guard:switch 覆盖全部
|
|
1465
|
+
// exhaustive guard:switch 覆盖全部 9 action,此处 params.action 收窄为 never;
|
|
1153
1466
|
// 仅防御运行时非法 action(schema 正常校验下不可达)
|
|
1154
1467
|
const exhaustive: never = params.action
|
|
1155
1468
|
throw err(
|
|
1156
|
-
`未知 action "${JSON.stringify(exhaustive)}"。👉 合法 action: find/family/outline/expand/detail/search/export/extract。`,
|
|
1469
|
+
`未知 action "${JSON.stringify(exhaustive)}"。👉 合法 action: find/family/outline/expand/detail/search/export/extract/workflow。`,
|
|
1157
1470
|
)
|
|
1158
1471
|
}
|
|
1159
1472
|
}
|